Local modifications to ClusterLabs/Anvil by Alteeve
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

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;