|
|
|
import { useState } from 'react';
|
|
|
|
import {
|
|
|
|
Box,
|
|
|
|
inputLabelClasses as muiInputLabelClasses,
|
|
|
|
OutlinedInputProps as MUIOutlinedInputProps,
|
|
|
|
outlinedInputClasses as muiOutlinedInputClasses,
|
|
|
|
Slider as MUISlider,
|
|
|
|
sliderClasses as muiSliderClasses,
|
|
|
|
SliderProps as MUISliderProps,
|
|
|
|
FormControl,
|
|
|
|
} from '@mui/material';
|
|
|
|
|
|
|
|
import { BORDER_RADIUS, GREY } from '../lib/consts/DEFAULT_THEME';
|
|
|
|
|
|
|
|
import InputMessageBox from './InputMessageBox';
|
|
|
|
import { MessageBoxProps } from './MessageBox';
|
|
|
|
import OutlinedInput, { OutlinedInputProps } from './OutlinedInput';
|
|
|
|
import OutlinedInputLabel, {
|
|
|
|
OutlinedInputLabelProps,
|
|
|
|
} from './OutlinedInputLabel';
|
|
|
|
import { BodyText } from './Text';
|
|
|
|
|
|
|
|
type SliderOnBlur = Exclude<MUISliderProps['onBlur'], undefined>;
|
|
|
|
type SliderOnChange = Exclude<MUISliderProps['onChange'], undefined>;
|
|
|
|
type SliderOnFocus = Exclude<MUISliderProps['onFocus'], undefined>;
|
|
|
|
type SliderValue = Exclude<MUISliderProps['value'], undefined>;
|
|
|
|
|
|
|
|
type SliderOptionalProps = {
|
|
|
|
inputLabelProps?: Partial<OutlinedInputLabelProps>;
|
|
|
|
isAllowTextInput?: boolean;
|
|
|
|
labelId?: string;
|
|
|
|
messageBoxProps?: Partial<MessageBoxProps>;
|
|
|
|
sliderProps?: Omit<MUISliderProps, 'onChange'> & {
|
|
|
|
onChange?: (value: number | number[]) => void;
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
type SliderProps = {
|
|
|
|
label: string;
|
|
|
|
value: SliderValue;
|
|
|
|
} & SliderOptionalProps;
|
|
|
|
|
|
|
|
type TextInputOnChange = Exclude<MUIOutlinedInputProps['onChange'], undefined>;
|
|
|
|
|
|
|
|
const SLIDER_DEFAULT_PROPS: Required<SliderOptionalProps> = {
|
|
|
|
inputLabelProps: {},
|
|
|
|
isAllowTextInput: false,
|
|
|
|
labelId: '',
|
|
|
|
messageBoxProps: {},
|
|
|
|
sliderProps: {},
|
|
|
|
};
|
|
|
|
|
|
|
|
const SLIDER_INPUT_LABEL_DECORATOR_CLASS_PREFIX = 'SliderInputLabelDecorator';
|
|
|
|
const SLIDER_INPUT_LABEL_DECORATOR_CLASSES = {
|
|
|
|
root: `${SLIDER_INPUT_LABEL_DECORATOR_CLASS_PREFIX}-root`,
|
|
|
|
};
|
|
|
|
|
|
|
|
const createInputLabelDecorator = ({
|
|
|
|
isFocused,
|
|
|
|
label,
|
|
|
|
}: {
|
|
|
|
isFocused?: boolean;
|
|
|
|
label: string;
|
|
|
|
}) => {
|
|
|
|
const borderColor = GREY;
|
|
|
|
const borderStyle = 'solid';
|
|
|
|
const content = '""';
|
|
|
|
|
|
|
|
let rootTop = '0';
|
|
|
|
let labelGapMargin = '0 .6em 0 .4em';
|
|
|
|
let borderWidth = '1px 0 0 0';
|
|
|
|
let opacity = '0.3';
|
|
|
|
|
|
|
|
if (isFocused) {
|
|
|
|
rootTop = '-1px';
|
|
|
|
labelGapMargin = '0 1em 0 1em';
|
|
|
|
borderWidth = '2px 0 0 0';
|
|
|
|
opacity = '1';
|
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
|
|
|
<Box
|
|
|
|
className={SLIDER_INPUT_LABEL_DECORATOR_CLASSES.root}
|
|
|
|
sx={{
|
|
|
|
display: 'flex',
|
|
|
|
flexDirection: 'row',
|
|
|
|
position: 'absolute',
|
|
|
|
top: rootTop,
|
|
|
|
width: 'calc(100% - 6px)',
|
|
|
|
|
|
|
|
'> :last-child': {
|
|
|
|
flexGrow: 1,
|
|
|
|
},
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
<Box
|
|
|
|
sx={{
|
|
|
|
borderColor,
|
|
|
|
borderStyle,
|
|
|
|
borderWidth,
|
|
|
|
content,
|
|
|
|
opacity,
|
|
|
|
width: '.6em',
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
<BodyText
|
|
|
|
sx={{
|
|
|
|
fontSize: '.75em',
|
|
|
|
margin: labelGapMargin,
|
|
|
|
visibility: 'hidden',
|
|
|
|
}}
|
|
|
|
text={label}
|
|
|
|
/>
|
|
|
|
<Box
|
|
|
|
sx={{
|
|
|
|
borderColor,
|
|
|
|
borderStyle,
|
|
|
|
borderWidth,
|
|
|
|
content,
|
|
|
|
opacity,
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
</Box>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
const createOutlinedInput = ({
|
|
|
|
key,
|
|
|
|
isFocused,
|
|
|
|
...inputRestProps
|
|
|
|
}: OutlinedInputProps & {
|
|
|
|
key: string;
|
|
|
|
isFocused?: boolean;
|
|
|
|
}) => (
|
|
|
|
<OutlinedInput
|
|
|
|
{...{
|
|
|
|
key,
|
|
|
|
className: isFocused ? muiOutlinedInputClasses.focused : '',
|
|
|
|
type: 'number',
|
|
|
|
...inputRestProps,
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
|
|
|
|
const stringToNumber = (value: string, fallback: number) => {
|
|
|
|
const converted = Number.parseFloat(value);
|
|
|
|
return Number.isNaN(converted) ? fallback : converted;
|
|
|
|
};
|
|
|
|
|
|
|
|
const toRangeString = (value: SliderValue) =>
|
|
|
|
value instanceof Array
|
|
|
|
? value.map((element) => String(element))
|
|
|
|
: [String(value)];
|
|
|
|
|
|
|
|
const toSliderValue = (rangeString: string[], value: SliderValue) =>
|
|
|
|
value instanceof Array
|
|
|
|
? rangeString.map((element, index) => stringToNumber(element, value[index]))
|
|
|
|
: stringToNumber(rangeString[0], value);
|
|
|
|
|
|
|
|
const Slider = ({
|
|
|
|
messageBoxProps = SLIDER_DEFAULT_PROPS.messageBoxProps,
|
|
|
|
isAllowTextInput = SLIDER_DEFAULT_PROPS.isAllowTextInput,
|
|
|
|
label,
|
|
|
|
labelId = SLIDER_DEFAULT_PROPS.labelId,
|
|
|
|
inputLabelProps = SLIDER_DEFAULT_PROPS.inputLabelProps,
|
|
|
|
sliderProps = SLIDER_DEFAULT_PROPS.sliderProps,
|
|
|
|
value,
|
|
|
|
}: SliderProps): JSX.Element => {
|
|
|
|
const {
|
|
|
|
max,
|
|
|
|
min,
|
|
|
|
onChange: sliderChangeCallback,
|
|
|
|
sx: sliderSx,
|
|
|
|
valueLabelDisplay: sliderValueLabelDisplay,
|
|
|
|
} = sliderProps;
|
|
|
|
let assignableValue: SliderValue = value;
|
|
|
|
|
|
|
|
const [textRangeValue, SetTextRangeValue] = useState<{ range: string[] }>({
|
|
|
|
range: toRangeString(value),
|
|
|
|
});
|
|
|
|
|
|
|
|
const [isFocused, setIsFocused] = useState<boolean>(false);
|
|
|
|
|
|
|
|
const handleLocalSliderBlur: SliderOnBlur = () => {
|
|
|
|
setIsFocused(false);
|
|
|
|
};
|
|
|
|
|
|
|
|
const handleLocalSliderFocus: SliderOnFocus = () => {
|
|
|
|
setIsFocused(true);
|
|
|
|
};
|
|
|
|
|
|
|
|
const handleSliderChange: SliderOnChange = (event, newValue) => {
|
|
|
|
SetTextRangeValue({
|
|
|
|
range: toRangeString(newValue),
|
|
|
|
});
|
|
|
|
|
|
|
|
sliderChangeCallback?.call(null, newValue);
|
|
|
|
};
|
|
|
|
|
|
|
|
const handleTextInputChange: TextInputOnChange = () => {
|
|
|
|
assignableValue = toSliderValue(textRangeValue.range, assignableValue);
|
|
|
|
|
|
|
|
sliderChangeCallback?.call(null, assignableValue);
|
|
|
|
};
|
|
|
|
|
|
|
|
return (
|
|
|
|
<FormControl
|
|
|
|
sx={{
|
|
|
|
display: 'flex',
|
|
|
|
flexDirection: 'column',
|
|
|
|
|
|
|
|
'&:hover': {
|
|
|
|
[`& .${SLIDER_INPUT_LABEL_DECORATOR_CLASSES.root} div`]: {
|
|
|
|
opacity: 1,
|
|
|
|
},
|
|
|
|
|
|
|
|
[`& .${muiOutlinedInputClasses.notchedOutline}`]: {
|
|
|
|
borderColor: GREY,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
<OutlinedInputLabel
|
|
|
|
{...{
|
|
|
|
className: isFocused ? muiInputLabelClasses.focused : '',
|
|
|
|
id: labelId,
|
|
|
|
shrink: true,
|
|
|
|
...inputLabelProps,
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
{label}
|
|
|
|
</OutlinedInputLabel>
|
|
|
|
{createInputLabelDecorator({ isFocused, label })}
|
|
|
|
<Box
|
|
|
|
sx={{
|
|
|
|
alignItems: 'center',
|
|
|
|
display: 'flex',
|
|
|
|
flexDirection: 'row',
|
|
|
|
|
|
|
|
'> :first-child': { flexGrow: 1 },
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
<MUISlider
|
|
|
|
{...{
|
|
|
|
'aria-labelledby': labelId,
|
|
|
|
max,
|
|
|
|
min,
|
|
|
|
onBlur: handleLocalSliderBlur,
|
|
|
|
onChange: handleSliderChange,
|
|
|
|
onFocus: handleLocalSliderFocus,
|
|
|
|
sx: {
|
|
|
|
color: GREY,
|
|
|
|
marginLeft: '1em',
|
|
|
|
marginRight: '1em',
|
|
|
|
|
|
|
|
[`& .${muiSliderClasses.thumb}`]: {
|
|
|
|
borderRadius: BORDER_RADIUS,
|
|
|
|
transform: 'translate(-50%, -50%) rotate(45deg)',
|
|
|
|
},
|
|
|
|
|
|
|
|
...sliderSx,
|
|
|
|
},
|
|
|
|
value: assignableValue,
|
|
|
|
valueLabelDisplay: sliderValueLabelDisplay,
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
{textRangeValue.range.map((textValue, textValueIndex) =>
|
|
|
|
createOutlinedInput({
|
|
|
|
key: `slider-nested-text-input-${textValueIndex}`,
|
|
|
|
inputProps: { max, min },
|
|
|
|
isFocused,
|
|
|
|
onBlur: handleLocalSliderBlur,
|
|
|
|
onChange: (...args) => {
|
|
|
|
textRangeValue.range[textValueIndex] = args[0].target.value;
|
|
|
|
SetTextRangeValue({ ...textRangeValue });
|
|
|
|
|
|
|
|
handleTextInputChange(...args);
|
|
|
|
},
|
|
|
|
onFocus: handleLocalSliderFocus,
|
|
|
|
sx: isAllowTextInput
|
|
|
|
? undefined
|
|
|
|
: {
|
|
|
|
visibility: 'collapse',
|
|
|
|
},
|
|
|
|
value: textValue,
|
|
|
|
}),
|
|
|
|
)}
|
|
|
|
</Box>
|
|
|
|
<InputMessageBox {...messageBoxProps} />
|
|
|
|
</FormControl>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
Slider.defaultProps = SLIDER_DEFAULT_PROPS;
|
|
|
|
|
|
|
|
export type { SliderProps };
|
|
|
|
|
|
|
|
export default Slider;
|