UNPKG

@hitachivantara/uikit-react-core

Version:
321 lines (320 loc) 12.8 kB
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 };