@hitachivantara/uikit-react-core
Version:
UI Kit Core React components.
321 lines (320 loc) • 12.8 kB
JavaScript
import { setId } from "../utils/setId.js";
import { useUniqueId } from "../hooks/useUniqueId.js";
import { HvFormElement } from "../FormElement/FormElement.js";
import { HvWarningText } from "../FormElement/WarningText/WarningText.js";
import { useControlled } from "../hooks/useControlled.js";
import { getElementById } from "../utils/document.js";
import { HvLabelContainer } from "../FormElement/LabelContainer.js";
import { sliderStyles, useClasses } from "./Slider.styles.js";
import { calculateStepValue, convertStatusToArray, createKnobStyles, createMark, createTrackStyles, ensureValuesConsistency, generateDefaultKnobProperties, isSingleSlider, knobsPositionToScaledValue, knobsPositionsToKnobsValues, knobsValuesToKnobsPositions, scaledValueToKnobsPositionValue, statusArrayToFormStatus } from "./utils.js";
import { HvSliderInput } from "./SliderInput/SliderInput.js";
import { useDefaultProps } from "@hitachivantara/uikit-react-utils";
import { forwardRef, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { jsx, jsxs } from "react/jsx-runtime";
import Slider from "rc-slider";
import Tooltip from "rc-tooltip";
//#region src/Slider/Slider.tsx
/**
* Sliders let users select a value from a range along a track. They're ideal for settings like volume, brightness, or image filters.
*/
var HvSlider = forwardRef(function HvSlider(props, ref) {
const { id, className, name, label, status, statusMessage, disabled, classes: classesProp, sliderProps, knobProps, inputProps, requiredMessage = "The value is required", outOfRangeMessage = "The value is out of range", noOverlap = true, hideInput, required, readOnly, markProperties = [], defaultValues = [void 0], values: valuesProp = [], knobProperties: knobPropertiesProp, "aria-errormessage": ariaErrorMessage, maxPointValue = 100, minPointValue = 0, divisionQuantity = 100, markStep = 20, markDigits = 0, formatMark, onChange, onBlur, onBeforeChange, onAfterChange, formatTooltip, ...others } = useDefaultProps("HvSlider", props);
const { classes, cx } = useClasses(classesProp);
const hasLabel = label != null;
const isDirty = useRef(false);
const elementId = useUniqueId(id);
const sliderInputId = setId(elementId, "input");
const stepValue = useMemo(() => calculateStepValue(maxPointValue, minPointValue, divisionQuantity), [
divisionQuantity,
maxPointValue,
minPointValue
]);
const inverseStepValue = 1 / stepValue;
const marks = useMemo(() => createMark(markProperties, markStep, divisionQuantity, minPointValue, maxPointValue, stepValue, markDigits, !!disabled, formatMark), [
disabled,
divisionQuantity,
formatMark,
markDigits,
markProperties,
markStep,
minPointValue,
maxPointValue,
stepValue
]);
const canShowError = ariaErrorMessage == null && (status !== void 0 && statusMessage !== void 0 || status === void 0);
const isSingle = useMemo(() => isSingleSlider(valuesProp, defaultValues), [defaultValues, valuesProp]);
const [knobsPositions, setKnobsPositions] = useControlled(useMemo(() => {
return valuesProp?.length > 0 ? knobsValuesToKnobsPositions(valuesProp, inverseStepValue, minPointValue) : void 0;
}, [
inverseStepValue,
minPointValue,
valuesProp
]), useMemo(() => knobsValuesToKnobsPositions(defaultValues, inverseStepValue, minPointValue), [
defaultValues,
inverseStepValue,
minPointValue
]));
const { arrayStatus, arrayDefaultStatus } = useMemo(() => convertStatusToArray(knobsPositions.length, status), [knobsPositions.length, status]);
const [validationStatus, setValidationState] = useControlled(arrayStatus, arrayDefaultStatus);
const [validationMessage, setValidationMessage] = useControlled(statusMessage, "");
const [isDraggingTrack, setIsDraggingTrack] = useState(false);
const knobProperties = generateDefaultKnobProperties(knobsPositions.length, disabled, knobPropertiesProp);
const rangesCount = knobProperties.length - 1;
const trackStyles = createTrackStyles(knobProperties);
const knobStyles = createKnobStyles(knobProperties);
/**
* Generates an object which posses the current value and position of the knobs.
*
* @param {Array} knobsCurrentPosition - An array containing the current positions of the knobs.
* @returns {Object} - An object with the positions and values of the knobs.
* @memberof HvSlider
*/
const generateKnobsPositionAndValues = useCallback((knobsCurrentPosition) => {
const newKnobsPosition = knobsCurrentPosition.slice();
const knobsValues = [];
let duplicatedValue = null;
const findDuplicated = newKnobsPosition.filter((item, index) => newKnobsPosition.indexOf(item) !== index);
if (noOverlap && findDuplicated.length > 0) [duplicatedValue] = findDuplicated;
newKnobsPosition.forEach((position, index, array) => {
const newArray = array;
let newPosition = position;
if (noOverlap && newPosition === duplicatedValue) {
const previousValue = knobsPositions[index];
if (previousValue !== newPosition) {
newPosition += newPosition > previousValue ? -1 : 1;
newArray[index] = newPosition;
}
}
knobsValues[index] = knobsPositionToScaledValue(newPosition, minPointValue, stepValue);
});
return {
knobsPosition: newKnobsPosition,
knobsValues
};
}, [
knobsPositions,
minPointValue,
noOverlap,
stepValue
]);
const performValidation = useCallback(() => {
let invalid = false;
let requiredMsg = false;
setValidationState([...generateKnobsPositionAndValues(knobsPositions).knobsValues.map((knobValue) => {
if (required && (knobValue == null || Number.isNaN(knobValue))) {
invalid = true;
requiredMsg = true;
return "invalid";
}
if (knobValue < minPointValue || knobValue > maxPointValue) {
invalid = true;
return "invalid";
}
return "valid";
})]);
if (invalid) {
setValidationMessage(requiredMsg ? requiredMessage : outOfRangeMessage);
return;
}
setValidationMessage("");
}, [
generateKnobsPositionAndValues,
knobsPositions,
maxPointValue,
minPointValue,
outOfRangeMessage,
required,
requiredMessage,
setValidationMessage,
setValidationState
]);
useEffect(() => {
const inverseStepVl = 1 / calculateStepValue(maxPointValue, minPointValue, divisionQuantity);
if (valuesProp?.length > 0) setKnobsPositions(knobsValuesToKnobsPositions(valuesProp.length > 0 ? valuesProp : defaultValues, inverseStepVl, minPointValue));
}, [
defaultValues,
divisionQuantity,
maxPointValue,
minPointValue,
setKnobsPositions,
valuesProp
]);
useEffect(() => {
if (!isDirty.current) return;
performValidation();
}, [
knobsPositions,
requiredMessage,
performValidation
]);
const onMouseDownHandler = (event) => {
if (event.target.className.includes("track")) setIsDraggingTrack(true);
};
const onMouseUpHandler = () => {
setIsDraggingTrack(false);
};
const onBlurHandler = (event) => {
const knobs = generateKnobsPositionAndValues(knobsPositions);
performValidation();
onBlur?.(event, knobs.knobsValues, status);
};
/**
* Function executed while the knobs changes.
*
* executes the callback provided by the user with the values and position of the knobs,
* also lock the value of the knob in case one is fixed.
*/
const onChangeHandler = (knobsPosition) => {
isDirty.current = true;
const knobs = generateKnobsPositionAndValues(knobsPosition);
knobProperties.forEach((knobProperty, index) => {
if (knobProperty.fixed) knobs.knobsPosition[index] = scaledValueToKnobsPositionValue(defaultValues[index], minPointValue, inverseStepValue);
});
if (disabled || readOnly) return;
onChange?.(knobs.knobsValues);
setKnobsPositions(knobs.knobsPosition);
};
const onInputChangeHandler = (inputValues, index) => {
let newKnobPositions = knobsValuesToKnobsPositions(inputValues, inverseStepValue, minPointValue);
newKnobPositions = ensureValuesConsistency(newKnobPositions, index);
onChangeHandler(newKnobPositions);
};
/**
* Function executed before a change.
*
* executes the callback provided by the user with the values and position of the knobs
*/
const onBeforeChangeHandler = (knobsPosition) => {
const knobs = generateKnobsPositionAndValues(knobsPosition);
onBeforeChange?.(knobs.knobsValues);
};
/**
* Function executed after a change.
*
* executes the callback provided by the user with the values and position of the knobs
*/
const onAfterChangeHandler = (knobsPosition) => {
const knobs = generateKnobsPositionAndValues(knobsPosition);
onAfterChange?.(knobs.knobsValues);
};
/**
* Function used to create a custom knob for the slider.
*
* TODO: This should be isolated because is creating a sub component,
* but there were some problems regarding the underlying component losing
* references of the handlers disabling the focus.
*/
const createKnob = (knobNode, params) => {
const { value: knobValue, dragging, index } = params;
const { style = {}, ...restProps } = knobNode.props;
const scaledKnobValue = knobsPositionToScaledValue(knobValue, minPointValue, stepValue).toFixed(markDigits);
if (dragging) style.backgroundColor = knobProperties[index]?.dragColor;
else style.backgroundColor = knobProperties[index]?.color;
const isEmpty = Number.isNaN(knobsPositions[index]) || knobsPositions[index] == null;
const indexedHandleId = setId(setId(elementId, "knob"), index);
return /* @__PURE__ */ jsx("div", {
className: cx({
[classes.handleContainer]: !!(!disabled && !isEmpty),
[classes.handleContainerDisabled]: !!(disabled && !isEmpty),
[classes.handleHiddenContainer]: isEmpty || readOnly
}),
children: /* @__PURE__ */ jsx(Tooltip, {
prefixCls: "rc-slider-tooltip",
overlay: formatTooltip?.(scaledKnobValue) || scaledKnobValue,
visible: dragging,
placement: "top",
overlayClassName: classes.sliderTooltip,
getTooltipContainer: () => getElementById(indexedHandleId),
children: /* @__PURE__ */ jsx("div", {
id: indexedHandleId,
style,
className: classes.handle,
...restProps,
"aria-label": `${label}-knob-${index}`,
"aria-valuenow": knobsPositionToScaledValue(knobValue, minPointValue, stepValue),
"aria-valuemin": minPointValue,
"aria-valuemax": maxPointValue,
...knobProps?.[index]
})
})
}, index);
};
const knobsValues = knobsPositionsToKnobsValues(knobsPositions, stepValue, minPointValue).map((v) => Number(v.toFixed(markDigits)));
return /* @__PURE__ */ jsxs(HvFormElement, {
className: cx(classes.root, {
[classes.trackStandBy]: !readOnly && !disabled && !isSingle && !isDraggingTrack,
[classes.trackDragging]: !readOnly && !disabled && !isSingle && isDraggingTrack,
[classes.rootDisabled]: !!disabled
}, className),
id,
name,
status: statusArrayToFormStatus(validationStatus),
disabled,
required,
readOnly,
onMouseDown: onMouseDownHandler,
onMouseUp: onMouseUpHandler,
onBlur: onBlurHandler,
...others,
children: [
/* @__PURE__ */ jsx(HvLabelContainer, {
label,
inputId: sliderInputId,
labelId: setId(elementId, "label"),
descriptionId: setId(elementId, "description"),
classes: {
root: cx(classes.labelContainer, {
[classes.labelIncluded]: hasLabel,
[classes.onlyInput]: !hasLabel
}),
label: classes.label
},
children: !hideInput && /* @__PURE__ */ jsx(HvSliderInput, {
id: sliderInputId,
label,
values: knobsValues,
onChange: onInputChangeHandler,
status: validationStatus,
disabled,
readOnly,
markDigits,
inputProps
})
}),
/* @__PURE__ */ jsx("div", {
className: cx(classes.sliderBase, classes.sliderContainer),
children: /* @__PURE__ */ jsx(Slider, {
ref,
range: !isSingle,
handleRender: createKnob,
className: cx(classes.sliderRoot, { [classes.rootRange]: !isSingle }),
min: 0,
max: divisionQuantity,
step: 1,
marks,
dotStyle: disabled ? sliderStyles.dotDisabled : sliderStyles.dot,
onChange: (singleValue) => onChangeHandler(Array().concat(singleValue)),
onBeforeChange: (singleValue) => onBeforeChangeHandler(Array().concat(singleValue)),
onAfterChange: (singleValue) => onAfterChangeHandler(Array().concat(singleValue)),
value: knobsPositions.length === 0 ? void 0 : isSingle ? knobsPositions[0] : [...knobsPositions],
allowCross: false,
disabled,
count: rangesCount,
railStyle: sliderStyles.rail,
handleStyle: knobStyles.knobInner,
trackStyle: trackStyles,
draggableTrack: !readOnly && !isSingle,
...sliderProps
})
}),
canShowError && /* @__PURE__ */ jsx(HvWarningText, {
id: setId(elementId, "error"),
className: classes.error,
disableBorder: true,
children: validationMessage
})
]
});
});
//#endregion
export { HvSlider };