You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
298 lines
7.4 KiB
298 lines
7.4 KiB
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;
|
|
|