UNPKG

@carbon/react

Version:

React components for the Carbon Design System

1,362 lines (1,314 loc) 46 kB
/** * Copyright IBM Corp. 2016, 2023 * * This source code is licensed under the Apache-2.0 license found in the * LICENSE file in the root directory of this source tree. */ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var _rollupPluginBabelHelpers = require('../../_virtual/_rollupPluginBabelHelpers.js'); var React = require('react'); var PropTypes = require('prop-types'); var cx = require('classnames'); var keys = require('../../internal/keyboard/keys.js'); var match = require('../../internal/keyboard/match.js'); var usePrefix = require('../../internal/usePrefix.js'); var deprecate = require('../../prop-types/deprecate.js'); var iconsReact = require('@carbon/icons-react'); var Text = require('../Text/Text.js'); require('../Text/TextDirection.js'); require('../Tooltip/DefinitionTooltip.js'); var Tooltip = require('../Tooltip/Tooltip.js'); var SliderHandles = require('./SliderHandles.js'); var clamp = require('../../internal/clamp.js'); var useNormalizedInputProps = require('../../internal/useNormalizedInputProps.js'); var throttle = require('../../node_modules/es-toolkit/dist/compat/function/throttle.js'); const ThumbWrapper = ({ hasTooltip, className, style, children, ...rest }) => { if (hasTooltip) { return /*#__PURE__*/React.createElement(Tooltip.Tooltip, _rollupPluginBabelHelpers.extends({ className: className, style: style }, rest), children); } else { return /*#__PURE__*/React.createElement("div", { className: className, style: style }, children); } }; const translationIds = { 'carbon.slider.auto-correct-announcement': 'carbon.slider.auto-correct-announcement' }; const defaultTranslations = { [translationIds['carbon.slider.auto-correct-announcement']]: 'The inputted value "{correctedValue}" was corrected to the nearest allowed digit.' }; const defaultTranslateWithId = (messageId, args) => { const template = defaultTranslations[messageId]; if (args?.correctedValue) { return template.replace('{correctedValue}', args.correctedValue); } return template; }; const defaultFormatLabel = (value, label) => { return `${value}${label ?? ''}`; }; // TODO: Assuming a 16ms throttle corresponds to 60 FPS, should it be halved, // since many systems can handle 120 FPS? If it doesn't correspond to 60 FPS, // what does it correspond to? /** * Minimum time between processed "drag" events in milliseconds. */ const EVENT_THROTTLE = 16; const DRAG_EVENT_TYPES = new Set(['mousemove', 'touchmove']); const DRAG_STOP_EVENT_TYPES = new Set(['mouseup', 'touchend', 'touchcancel']); var HandlePosition = /*#__PURE__*/function (HandlePosition) { HandlePosition["LOWER"] = "lower"; HandlePosition["UPPER"] = "upper"; return HandlePosition; }(HandlePosition || {}); // TODO: Delete this type and directory type the properties in the function. const Slider = props => { var _Fragment, _Fragment2, _Fragment3, _Fragment4; // TODO: Move destructured `props` from the IIFE to here. const initialState = { value: props.value, valueUpper: props.unstable_valueUpper, left: 0, leftUpper: 0, needsOnRelease: false, isValid: true, isValidUpper: true, activeHandle: undefined, correctedValue: null, correctedPosition: null, isRtl: false }; // TODO: Investigate using generics on the hook. const [state, setState] = React.useReducer((prev, args) => ({ ...prev, ...args }), initialState); // TODO: Investigate getting rid of these references. const stateRef = React.useRef(state); React.useEffect(() => { stateRef.current = state; }, [state]); const propsRef = React.useRef(props); React.useEffect(() => { propsRef.current = props; }, [props]); const thumbRef = React.useRef(null); const thumbRefUpper = React.useRef(null); const filledTrackRef = React.useRef(null); const elementRef = React.useRef(null); const trackRef = React.useRef(null); const inputIdRef = React.useRef(''); // TODO: Delete this function and set its return value as the value of // `twoHandles`. const hasTwoHandles = () => { return typeof state.valueUpper !== 'undefined'; }; const twoHandles = hasTwoHandles(); /** * Sets up initial slider position and value in response to component mount. */ React.useEffect(() => { if (elementRef.current) { const isRtl = document?.dir === 'rtl'; if (hasTwoHandles()) { const { value, left } = calcValue({ value: stateRef.current.value, useRawValue: true }); const { value: valueUpper, left: leftUpper } = calcValue({ value: stateRef.current.valueUpper, useRawValue: true }); setState({ isRtl, value, left, valueUpper, leftUpper }); } else { const { value, left } = calcValue({ value: stateRef.current.value, useRawValue: true }); setState({ isRtl, value, left }); } } return () => { DRAG_STOP_EVENT_TYPES.forEach(element => elementRef.current?.ownerDocument.removeEventListener(element, onDragStop)); DRAG_EVENT_TYPES.forEach(element => elementRef.current?.ownerDocument.removeEventListener(element, handleDrag)); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); React.useEffect(() => { // TODO: Uncomment this code and delete all of the `filledTrackRef.current` // checks. // const el = filledTrackRef.current; // // if (!el) return; // Fire onChange event handler if present, if there's a usable value, and // if the value is different from the last one if (hasTwoHandles()) { if (filledTrackRef.current) { filledTrackRef.current.style.transform = state.isRtl ? `translate(${100 - state.leftUpper}%, -50%) scaleX(${(state.leftUpper - state.left) / 100})` : `translate(${state.left}%, -50%) scaleX(${(state.leftUpper - state.left) / 100})`; } } else { if (filledTrackRef.current) { filledTrackRef.current.style.transform = state.isRtl ? `translate(100%, -50%) scaleX(-${state.left / 100})` : `translate(0%, -50%) scaleX(${state.left / 100})`; } } // TODO: Investigate whether the missing dependency should be added. // // eslint-disable-next-line react-hooks/exhaustive-deps }, [state.left, state.leftUpper, state.isRtl]); // Fire onChange when value(s) change const prevValsRef = React.useRef(null); React.useEffect(() => { const prev = prevValsRef.current; if (prev && (prev.value !== state.value || prev.valueUpper !== state.valueUpper) && typeof props.onChange === 'function') { props.onChange({ value: state.value, valueUpper: state.valueUpper }); } prevValsRef.current = { value: state.value, valueUpper: state.valueUpper }; // TODO: Investigate whether the missing dependency should be added. // // eslint-disable-next-line react-hooks/exhaustive-deps }, [state.value, state.valueUpper, props.onChange]); React.useEffect(() => { // Fire onRelease event handler if present and if needed if (state.needsOnRelease && typeof props.onRelease === 'function') { props.onRelease({ value: state.value, valueUpper: state.valueUpper }); // Reset the flag setState({ needsOnRelease: false }); } // TODO: Investigate whether the missing dependency should be added. // // eslint-disable-next-line react-hooks/exhaustive-deps }, [state.needsOnRelease, state.value, state.valueUpper, props.onRelease]); const prevSyncKeysRef = React.useRef(null); React.useEffect(() => { const prev = prevSyncKeysRef.current; const next = [props.value, props.unstable_valueUpper, props.max, props.min]; // If value from props does not change, do nothing here. // Otherwise, do prop -> state sync without "value capping". if (!prev || prev[0] !== next[0] || prev[1] !== next[1] || prev[2] !== next[2] || prev[3] !== next[3]) { setState(calcValue({ value: props.value, useRawValue: true })); if (typeof props.unstable_valueUpper !== 'undefined') { const { value: valueUpper, left: leftUpper } = calcValue({ value: props.unstable_valueUpper, useRawValue: true }); setState({ valueUpper, leftUpper }); } else { setState({ valueUpper: undefined, leftUpper: undefined }); } prevSyncKeysRef.current = next; } // TODO: Investigate whether the missing dependency should be added. // // eslint-disable-next-line react-hooks/exhaustive-deps }, [props.value, props.unstable_valueUpper, props.max, props.min]); /** * Rounds a given value to the nearest step defined by the slider's `step` * prop. * * @param value - The value to adjust to the nearest step. Defaults to `0`. * @returns The value rounded to the precision determined by the step. */ const nearestStepValue = (value = 0) => { // TODO: Use a nullish coalescing operator. const decimals = (props.step?.toString().split('.')[1] || '').length; return Number(value.toFixed(decimals)); }; const handleDrag = event => { if (event instanceof globalThis.MouseEvent || event instanceof globalThis.TouchEvent) { onDrag(event); } }; /** * Sets up "drag" event handlers and calls `onDrag` in case dragging * started on somewhere other than the thumb without a corresponding "move" * event. */ const onDragStart = evt => { // Do nothing if component is disabled if (props.disabled || props.readOnly) { return; } // We're going to force focus on one of the handles later on here, b/c we're // firing on a mousedown event, we need to call event.preventDefault() to // keep the focus from leaving the HTMLElement. // @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#notes evt.preventDefault(); // TODO: Abstract `elementRef.current?.ownerDocument` to a variable that can // be used here and everywhere else in this file. // Add drag stop handlers DRAG_STOP_EVENT_TYPES.forEach(element => { elementRef.current?.ownerDocument.addEventListener(element, onDragStop); }); // Add drag handlers DRAG_EVENT_TYPES.forEach(element => { elementRef.current?.ownerDocument.addEventListener(element, handleDrag); }); const clientX = getClientXFromEvent(evt.nativeEvent); let activeHandle; if (hasTwoHandles()) { if (evt.target == thumbRef.current) { activeHandle = HandlePosition.LOWER; } else if (evt.target == thumbRefUpper.current) { activeHandle = HandlePosition.UPPER; } else if (clientX) { const distanceToLower = calcDistanceToHandle(HandlePosition.LOWER, clientX); const distanceToUpper = calcDistanceToHandle(HandlePosition.UPPER, clientX); if (distanceToLower <= distanceToUpper) { activeHandle = HandlePosition.LOWER; } else { activeHandle = HandlePosition.UPPER; } } } // Force focus to the appropriate handle. const focusOptions = { preventScroll: true }; if (hasTwoHandles()) { if (thumbRef.current && activeHandle === HandlePosition.LOWER) { thumbRef.current.focus(focusOptions); } else if (thumbRefUpper.current && activeHandle === HandlePosition.UPPER) { thumbRefUpper.current.focus(focusOptions); } } else if (thumbRef.current) { thumbRef.current.focus(focusOptions); } setState({ activeHandle }); // Perform first recalculation since we probably didn't click exactly in the // middle of the thumb. onDrag(evt.nativeEvent, activeHandle); }; /** * Removes "drag" and "drag stop" event handlers and calls sets the flag * indicating that the `onRelease` callback should be called. */ const onDragStop = () => { // Do nothing if component is disabled if (props.disabled || props.readOnly) { return; } // TODO: Rename parameters in `DRAG_*` loops to `type`. // Remove drag stop handlers DRAG_STOP_EVENT_TYPES.forEach(element => { elementRef.current?.ownerDocument.removeEventListener(element, onDragStop); }); // Remove drag handlers DRAG_EVENT_TYPES.forEach(element => { elementRef.current?.ownerDocument.removeEventListener(element, handleDrag); }); // Set needsOnRelease flag so event fires on next update. setState({ needsOnRelease: true, isValid: true, isValidUpper: true }); }; // TODO: Rename this reference. /** * Handles a "drag" event by recalculating the value/thumb and setting state * accordingly. * * @param evt The event. * @param activeHandle The first drag event call, we may have an explicit * activeHandle value, which is to be used before state is used. */ const _onDragRef = React.useRef(null); _onDragRef.current = (evt, activeHandle) => { activeHandle = activeHandle ?? stateRef.current.activeHandle; // Do nothing if component is disabled, or we have no event. if (propsRef.current.disabled || propsRef.current.readOnly || !evt) { return; } const clientX = getClientXFromEvent(evt); const { value, left } = calcValue({ clientX, value: stateRef.current.value }); // If we're set to two handles, negotiate which drag handle is closest to // the users' interaction. if (hasTwoHandles() && activeHandle) { setValueLeftForHandle(activeHandle, { value: nearestStepValue(value), left }); } else { setState({ value: nearestStepValue(value), left, isValid: true }); } // TODO: Investigate if it would be better to not call `setState` // back-to-back here and in other places. setState({ correctedValue: null, correctedPosition: null }); }; /** * Throttles calls to `_onDrag` by limiting events to being processed at * most once every `EVENT_THROTTLE` milliseconds. */ const onDrag = React.useMemo(() => throttle.throttle((evt, activeHandle) => { _onDragRef.current?.(evt, activeHandle); }, EVENT_THROTTLE, { leading: true, trailing: false }), []); /** * Handles a `keydown` event by recalculating the value/thumb and setting * state accordingly. */ const onKeyDown = evt => { // Do nothing if component is disabled, or we don't have a valid event if (props.disabled || props.readOnly) { return; } const { step = 1, stepMultiplier = 4 } = props; let delta = 0; if (match.matches(evt, [keys.ArrowDown, keys.ArrowLeft])) { delta = -step; } else if (match.matches(evt, [keys.ArrowUp, keys.ArrowRight])) { delta = step; } else { // Ignore keys we don't want to handle return; } // If shift was held, account for the stepMultiplier if (evt.shiftKey) { delta *= stepMultiplier; } if (hasTwoHandles() && state.activeHandle) { const currentValue = state.activeHandle === HandlePosition.LOWER ? state.value : state.valueUpper; const { value, left } = calcValue({ value: calcValueForDelta(currentValue ?? props.min, delta, props.step) }); setValueLeftForHandle(state.activeHandle, { value: nearestStepValue(value), left }); } else { const { value, left } = calcValue({ // Ensures custom value from `<input>` won't cause skipping next stepping // point with right arrow key, e.g. Typing 51 in `<input>`, moving focus // onto the thumb and the hitting right arrow key should yield 52 instead // of 54. value: calcValueForDelta(state.value, delta, props.step) }); setState({ value: nearestStepValue(value), left, isValid: true }); } setState({ correctedValue: null, correctedPosition: null }); }; /** * Provides the two-way binding for the input field of the Slider. It also * Handles a change to the input field by recalculating the value/thumb and * setting state accordingly. */ const onChangeInput = evt => { // Do nothing if component is disabled if (props.disabled || props.readOnly) { return; } // Do nothing if we have no valid event, target, or value if (!evt || !('target' in evt) || typeof evt.target.value !== 'string') { return; } // Avoid calling calcValue for invalid numbers, but still update the state. const activeHandle = evt.target.dataset.handlePosition ?? HandlePosition.LOWER; const targetValue = Number.parseFloat(evt.target.value); if (hasTwoHandles()) { if (isNaN(targetValue)) { setValueForHandle(activeHandle, evt.target.value); } else if (isValidValueForPosition({ handle: activeHandle, value: targetValue, min: props.min, max: props.max })) { processNewInputValue(evt.target); } else { setValueForHandle(activeHandle, targetValue); } } else { if (isNaN(targetValue)) { // TODO: Address this error // // @ts-expect-error - Passing a string to something that expects a // number. setState({ value: evt.target.value }); } else if (isValidValue({ value: targetValue, min: props.min, max: props.max })) { processNewInputValue(evt.target); } else { setState({ value: targetValue }); } } }; /** * Checks for validity of input value after clicking out of the input. It also * Handles state change to isValid state. */ const onBlurInput = evt => { // Do nothing if we have no valid event, target, or value if (!evt || !('target' in evt) || typeof evt.target.value !== 'string') { return; } const { value: targetValue } = evt.target; processNewInputValue(evt.target); props.onBlur?.({ value: targetValue, handlePosition: evt.target.dataset.handlePosition }); }; const onInputKeyDown = evt => { // Do nothing if component is disabled, or we don't have a valid event. if (props.disabled || props.readOnly || !(evt.target instanceof HTMLInputElement)) { return; } // Do nothing if we have no valid event, target, or value. if (!evt || !('target' in evt) || typeof evt.target.value !== 'string') { return; } if (match.matches(evt, [keys.Enter])) { processNewInputValue(evt.target); } }; const processNewInputValue = input => { setState({ correctedValue: null, correctedPosition: null }); const targetValue = Number.parseFloat(input.value); const validity = !isNaN(targetValue); // When there are two handles, we'll also have the data-handle-position // attribute to consider the other value before settling on the validity to // set. const handlePosition = input.dataset.handlePosition; if (handlePosition === HandlePosition.LOWER) { setState({ isValid: validity }); } else if (handlePosition === HandlePosition.UPPER) { setState({ isValidUpper: validity }); } setState({ isValid: validity }); if (validity) { const adjustedValue = handlePosition ? getAdjustedValueForPosition({ handle: handlePosition, value: targetValue, min: props.min, max: props.max }) : getAdjustedValue({ value: targetValue, min: props.min, max: props.max }); if (adjustedValue !== targetValue) { setState({ correctedValue: targetValue.toString(), correctedPosition: handlePosition ?? null }); } else { setState({ correctedValue: null, correctedPosition: null }); } const { value, left } = calcValue({ value: adjustedValue, useRawValue: true }); if (handlePosition) { setValueLeftForHandle(handlePosition, { value: nearestStepValue(value), left }); } else { setState({ value, left }); } } }; const calcLeftPercent = ({ clientX, value, range }) => { // TODO: Delete the optional chaining operator after `getBoundingClientRect`. const boundingRect = elementRef.current?.getBoundingClientRect?.(); let width = boundingRect ? boundingRect.right - boundingRect.left : 0; // Enforce a minimum width of at least 1 for calculations if (width <= 0) { width = 1; } // If a clientX is specified, use it to calculate the leftPercent. If not, // use the provided value to calculate it instead. if (clientX) { const leftOffset = state.isRtl ? (boundingRect?.right ?? 0) - clientX : clientX - (boundingRect?.left ?? 0); return leftOffset / width; } else if (value !== null && typeof value !== 'undefined' && range) { // Prevent NaN calculation if the range is 0. return range === 0 ? 0 : (value - props.min) / range; } // We should never end up in this scenario, but in case we do, and to // re-assure Typescript, return 0. return 0; }; /** * Calculates the discrete value (snapped to the nearest step) along * with the corresponding handle position percentage. */ const calcDiscreteValueAndPercent = ({ leftPercent }) => { const { step = 1, min, max } = props; const numSteps = Math.floor((max - min) / step) + 1; /** Index of the step that corresponds to `leftPercent`. */ const stepIndex = Math.round(leftPercent * (numSteps - 1)); const discreteValue = stepIndex === numSteps - 1 ? max : min + step * stepIndex; /** Percentage corresponding to the step index. */ const discretePercent = stepIndex / (numSteps - 1); return { discreteValue, discretePercent }; }; /** * Calculates the slider's value and handle position based on either a * mouse/touch event or an explicit value. */ const calcValue = ({ clientX, value, useRawValue }) => { const range = props.max - props.min; const leftPercentRaw = calcLeftPercent({ clientX, value, range }); /** `leftPercentRaw` clamped between 0 and 1. */ const leftPercent = clamp.clamp(leftPercentRaw, 0, 1); if (useRawValue) { return { value, left: leftPercent * 100 }; } // Use the discrete value and percentage for snapping. const { discreteValue, discretePercent } = calcDiscreteValueAndPercent({ leftPercent }); return { value: discreteValue, left: discretePercent * 100 }; }; const calcDistanceToHandle = (handle, clientX) => { const handleBoundingRect = getHandleBoundingRect(handle); /** x-coordinate of the midpoint. */ const handleX = handleBoundingRect.left + handleBoundingRect.width / 2; return Math.abs(handleX - clientX); }; /** * Calculates a new slider value based on the current value, a change delta, * and a step. * * @param currentValue - The starting value from which the slider is moving. * @param delta - The amount to adjust the current value by. * @param step - The step. Defaults to `1`. * @returns The new slider value, rounded to the same number of decimal places * as the step. */ const calcValueForDelta = (currentValue, delta, step = 1) => { const base = delta > 0 ? Math.round(currentValue / step) * step : currentValue; const newValue = base + delta; // TODO: Why is the logical OR needed here? const decimals = (step.toString().split('.')[1] || '').length; return Number(newValue.toFixed(decimals)); }; /** * Sets state relevant to the given handle position. * * Guards against setting either lower or upper values beyond its counterpart. */ const setValueLeftForHandle = (handle, { value: newValue, left: newLeft }) => { const { value, valueUpper, left, leftUpper } = state; if (handle === HandlePosition.LOWER) { // Don't allow higher than the upper handle. setState({ value: valueUpper && newValue > valueUpper ? valueUpper : newValue, left: valueUpper && newValue > valueUpper ? leftUpper : newLeft, isValid: true }); } else { setState({ valueUpper: value && newValue < value ? value : newValue, leftUpper: value && newValue < value ? left : newLeft, isValidUpper: true }); } }; const setValueForHandle = (handle, value) => { if (handle === HandlePosition.LOWER) { setState({ // TODO: Address this error // // @ts-expect-error - Passing a string to something that expects a // number. value, isValid: true }); } else { setState({ // TODO: Address this error // // @ts-expect-error - Passing a string to something that expects a // number. valueUpper: value, isValidUpper: true }); } }; const isValidValueForPosition = ({ handle, value: newValue, min, max }) => { const { value, valueUpper } = state; if (!isValidValue({ value: newValue, min, max })) { return false; } if (handle === HandlePosition.LOWER) { return !valueUpper || newValue <= valueUpper; } else if (handle === HandlePosition.UPPER) { return !value || newValue >= value; } return false; }; const isValidValue = ({ value, min, max }) => { return !(value < min || value > max); }; const getAdjustedValueForPosition = ({ handle, value: newValueInput, min, max }) => { const { value, valueUpper } = state; let newValue = getAdjustedValue({ value: newValueInput, min, max }); // TODO: Just return the value. // Next adjust to the opposite handle. if (handle === HandlePosition.LOWER && valueUpper) { newValue = newValue > valueUpper ? valueUpper : newValue; } else if (handle === HandlePosition.UPPER && value) { newValue = newValue < value ? value : newValue; } return newValue; }; const getAdjustedValue = ({ value, min, max }) => { // TODO: Just return the value. if (value < min) { value = min; } if (value > max) { value = max; } return value; }; /** * Get the bounding rect for the requested handles' DOM element. * * If the bounding rect is not available, a new, empty DOMRect is returned. */ const getHandleBoundingRect = handle => { let boundingRect; if (handle === HandlePosition.LOWER) { boundingRect = thumbRef.current?.getBoundingClientRect(); } else { boundingRect = thumbRefUpper.current?.getBoundingClientRect(); } return boundingRect ?? new DOMRect(); }; const getClientXFromEvent = event => { let clientX; if ('clientX' in event) { clientX = event.clientX; } else if ('touches' in event && 0 in event.touches && 'clientX' in event.touches[0]) { clientX = event.touches[0].clientX; } return clientX; }; React.useEffect(() => { const { isValid, isValidUpper } = stateRef.current; const derivedState = {}; // Will override state in favor of invalid prop if (props.invalid === true) { if (isValid === true) derivedState.isValid = false; if (isValidUpper === true) derivedState.isValidUpper = false; } else if (props.invalid === false) { if (isValid === false) derivedState.isValid = true; if (isValidUpper === false) derivedState.isValidUpper = true; } if (Object.keys(derivedState).length) { setState(derivedState); } }, [props.invalid]); const { ariaLabelInput, unstable_ariaLabelInputUpper: ariaLabelInputUpper, className, hideTextInput = false, id = inputIdRef.current = inputIdRef.current || // TODO: // 1. Why isn't `inputId` just set to this value instead of an empty // string? // 2. Why this value instead of something else, like // `crypto.randomUUID()` or `useId()`? `__carbon-slider_${Math.random().toString(36).substr(2)}`, min, minLabel, max, maxLabel, formatLabel = defaultFormatLabel, labelText, hideLabel, step = 1, // TODO: Other properties are deleted below. Why isn't this one? // eslint-disable-next-line @typescript-eslint/no-unused-vars -- https://github.com/carbon-design-system/carbon/issues/20452 stepMultiplier: _stepMultiplier, inputType = 'number', invalidText, required, disabled = false, name, unstable_nameUpper: nameUpper, light, readOnly = false, warn = false, warnText, translateWithId: t = defaultTranslateWithId, ...other } = props; const { value, valueUpper, isValid, isValidUpper, correctedValue, correctedPosition, isRtl } = state; const normalizedProps = useNormalizedInputProps.useNormalizedInputProps({ id, disabled, readOnly, invalid: !isValid, warn }); const normalizedUpperProps = useNormalizedInputProps.useNormalizedInputProps({ id, disabled, readOnly, invalid: !isValidUpper, warn }); // TODO: Delete this IIFE. It was added to maintain whitespace and to make it clear // what exactly has changed. return (() => { delete other.onRelease; delete other.invalid; delete other.unstable_valueUpper; const showWarning = normalizedProps.warn || // TODO: https://github.com/carbon-design-system/carbon/issues/18991#issuecomment-2795709637 // eslint-disable-next-line valid-typeof , no-constant-binary-expression -- https://github.com/carbon-design-system/carbon/issues/20452 typeof correctedValue !== null && correctedPosition === HandlePosition.LOWER && isValid; const showWarningUpper = normalizedUpperProps.warn || // TODO: https://github.com/carbon-design-system/carbon/issues/18991#issuecomment-2795709637 // eslint-disable-next-line valid-typeof, no-constant-binary-expression -- https://github.com/carbon-design-system/carbon/issues/20452 typeof correctedValue !== null && correctedPosition === (twoHandles ? HandlePosition.UPPER : HandlePosition.LOWER) && (twoHandles ? isValidUpper : isValid); return /*#__PURE__*/React.createElement(usePrefix.PrefixContext.Consumer, null, prefix => { const labelId = `${id}-label`; const labelClasses = cx(`${prefix}--label`, { [`${prefix}--visually-hidden`]: hideLabel, [`${prefix}--label--disabled`]: disabled }); const containerClasses = cx(`${prefix}--slider-container`, { [`${prefix}--slider-container--two-handles`]: twoHandles, [`${prefix}--slider-container--disabled`]: disabled, [`${prefix}--slider-container--readonly`]: readOnly, [`${prefix}--slider-container--rtl`]: isRtl }); const sliderClasses = cx(`${prefix}--slider`, { [`${prefix}--slider--disabled`]: disabled, [`${prefix}--slider--readonly`]: readOnly }); const fixedInputClasses = [`${prefix}--text-input`, `${prefix}--slider-text-input`]; const conditionalInputClasses = { [`${prefix}--text-input--light`]: light }; const lowerInputClasses = cx([...fixedInputClasses, `${prefix}--slider-text-input--lower`, conditionalInputClasses, { [`${prefix}--text-input--invalid`]: normalizedProps.invalid, [`${prefix}--slider-text-input--warn`]: showWarning }]); const upperInputClasses = cx([...fixedInputClasses, `${prefix}--slider-text-input--upper`, conditionalInputClasses, { [`${prefix}--text-input--invalid`]: twoHandles ? normalizedUpperProps.invalid : normalizedProps.invalid, [`${prefix}--slider-text-input--warn`]: showWarningUpper }]); const lowerInputWrapperClasses = cx([`${prefix}--text-input-wrapper`, `${prefix}--slider-text-input-wrapper`, `${prefix}--slider-text-input-wrapper--lower`, { [`${prefix}--text-input-wrapper--readonly`]: readOnly, [`${prefix}--slider-text-input-wrapper--hidden`]: hideTextInput }]); const upperInputWrapperClasses = cx([`${prefix}--text-input-wrapper`, `${prefix}--slider-text-input-wrapper`, `${prefix}--slider-text-input-wrapper--upper`, { [`${prefix}--text-input-wrapper--readonly`]: readOnly, [`${prefix}--slider-text-input-wrapper--hidden`]: hideTextInput }]); const lowerThumbClasses = cx(`${prefix}--slider__thumb`, { [`${prefix}--slider__thumb--lower`]: twoHandles }); const upperThumbClasses = cx(`${prefix}--slider__thumb`, { [`${prefix}--slider__thumb--upper`]: twoHandles }); const lowerThumbWrapperClasses = cx([`${prefix}--icon-tooltip`, `${prefix}--slider__thumb-wrapper`, { [`${prefix}--slider__thumb-wrapper--lower`]: twoHandles }]); const upperThumbWrapperClasses = cx([`${prefix}--icon-tooltip`, `${prefix}--slider__thumb-wrapper`, { [`${prefix}--slider__thumb-wrapper--upper`]: twoHandles }]); const lowerThumbWrapperProps = { style: { insetInlineStart: `${state.left}%` } }; const upperThumbWrapperProps = { style: { insetInlineStart: `${state.leftUpper}%` } }; return /*#__PURE__*/React.createElement("div", { className: cx(`${prefix}--form-item`, className) }, /*#__PURE__*/React.createElement(Text.Text, { as: "label", htmlFor: twoHandles ? undefined : id, className: labelClasses, id: labelId }, labelText), /*#__PURE__*/React.createElement("div", { className: containerClasses }, twoHandles ? /*#__PURE__*/React.createElement("div", { className: lowerInputWrapperClasses }, /*#__PURE__*/React.createElement("input", { type: hideTextInput ? 'hidden' : inputType, id: `${id}-lower-input-for-slider`, name: name, className: lowerInputClasses, value: value, "aria-label": ariaLabelInput, disabled: disabled, required: required, min: min, max: max, step: step, onChange: onChangeInput, onBlur: onBlurInput, onKeyUp: props.onInputKeyUp, onKeyDown: onInputKeyDown, "data-invalid": normalizedProps.invalid ? true : null, "data-handle-position": HandlePosition.LOWER, "aria-invalid": normalizedProps.invalid ? true : undefined, readOnly: readOnly }), normalizedProps.invalid && /*#__PURE__*/React.createElement(iconsReact.WarningFilled, { className: `${prefix}--slider__invalid-icon` }), showWarning && /*#__PURE__*/React.createElement(iconsReact.WarningAltFilled, { className: `${prefix}--slider__invalid-icon ${prefix}--slider__invalid-icon--warning` })) : null, /*#__PURE__*/React.createElement(Text.Text, { className: `${prefix}--slider__range-label` }, formatLabel(min, minLabel)), /*#__PURE__*/React.createElement("div", _rollupPluginBabelHelpers.extends({ className: sliderClasses, ref: node => { elementRef.current = node; }, onMouseDown: onDragStart, onTouchStart: onDragStart, onKeyDown: onKeyDown, role: "presentation", tabIndex: -1, "data-invalid": (twoHandles ? normalizedProps.invalid || normalizedUpperProps.invalid : normalizedProps.invalid) ? true : null }, other), /*#__PURE__*/React.createElement(ThumbWrapper, _rollupPluginBabelHelpers.extends({ hasTooltip: hideTextInput, className: lowerThumbWrapperClasses, label: formatLabel(value, undefined), align: "top" }, lowerThumbWrapperProps), /*#__PURE__*/React.createElement("div", { className: lowerThumbClasses, role: "slider", id: twoHandles ? undefined : id, tabIndex: readOnly || disabled ? undefined : 0, "aria-valuetext": formatLabel(value, undefined), "aria-valuemax": twoHandles ? valueUpper : max, "aria-valuemin": min, "aria-valuenow": value, "aria-labelledby": twoHandles ? undefined : labelId, "aria-label": twoHandles ? ariaLabelInput : undefined, ref: thumbRef, onFocus: () => setState({ activeHandle: HandlePosition.LOWER }) }, twoHandles && !isRtl ? _Fragment || (_Fragment = /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(SliderHandles.LowerHandle, { "aria-label": ariaLabelInput }), /*#__PURE__*/React.createElement(SliderHandles.LowerHandleFocus, { "aria-label": ariaLabelInput }))) : twoHandles && isRtl ? _Fragment2 || (_Fragment2 = /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(SliderHandles.UpperHandle, { "aria-label": ariaLabelInputUpper }), /*#__PURE__*/React.createElement(SliderHandles.UpperHandleFocus, { "aria-label": ariaLabelInputUpper }))) : undefined)), twoHandles ? /*#__PURE__*/React.createElement(ThumbWrapper, _rollupPluginBabelHelpers.extends({ hasTooltip: hideTextInput, className: upperThumbWrapperClasses, label: formatLabel(valueUpper ?? 0, undefined), align: "top" }, upperThumbWrapperProps), /*#__PURE__*/React.createElement("div", { className: upperThumbClasses, role: "slider", tabIndex: readOnly || disabled ? undefined : 0, "aria-valuemax": max, "aria-valuemin": value, "aria-valuenow": valueUpper, "aria-label": ariaLabelInputUpper, ref: thumbRefUpper, onFocus: () => setState({ activeHandle: HandlePosition.UPPER }) }, twoHandles && !isRtl ? _Fragment3 || (_Fragment3 = /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(SliderHandles.UpperHandle, { "aria-label": ariaLabelInputUpper }), /*#__PURE__*/React.createElement(SliderHandles.UpperHandleFocus, { "aria-label": ariaLabelInputUpper }))) : twoHandles && isRtl ? _Fragment4 || (_Fragment4 = /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(SliderHandles.LowerHandle, { "aria-label": ariaLabelInput }), /*#__PURE__*/React.createElement(SliderHandles.LowerHandleFocus, { "aria-label": ariaLabelInput }))) : undefined)) : null, /*#__PURE__*/React.createElement("div", { className: `${prefix}--slider__track`, ref: node => { trackRef.current = node; } }), /*#__PURE__*/React.createElement("div", { className: `${prefix}--slider__filled-track`, ref: filledTrackRef })), /*#__PURE__*/React.createElement(Text.Text, { className: `${prefix}--slider__range-label` }, formatLabel(max, maxLabel)), /*#__PURE__*/React.createElement("div", { className: upperInputWrapperClasses }, /*#__PURE__*/React.createElement("input", { type: hideTextInput ? 'hidden' : inputType, id: `${id}-${twoHandles ? 'upper-' : ''}input-for-slider`, name: twoHandles ? nameUpper : name, className: upperInputClasses, value: twoHandles ? valueUpper : value, "aria-labelledby": !ariaLabelInput && !twoHandles ? labelId : undefined, "aria-label": twoHandles ? ariaLabelInputUpper : ariaLabelInput ? ariaLabelInput : undefined, disabled: disabled, required: required, min: min, max: max, step: step, onChange: onChangeInput, onBlur: onBlurInput, onKeyDown: onInputKeyDown, onKeyUp: props.onInputKeyUp, "data-invalid": (twoHandles ? normalizedUpperProps.invalid : normalizedProps.invalid) ? true : null, "data-handle-position": twoHandles ? HandlePosition.UPPER : null, "aria-invalid": (twoHandles ? normalizedUpperProps.invalid : normalizedProps.invalid) ? true : undefined, readOnly: readOnly }), (twoHandles ? normalizedUpperProps.invalid : normalizedProps.invalid) && /*#__PURE__*/React.createElement(iconsReact.WarningFilled, { className: `${prefix}--slider__invalid-icon` }), showWarningUpper && /*#__PURE__*/React.createElement(iconsReact.WarningAltFilled, { className: `${prefix}--slider__invalid-icon ${prefix}--slider__invalid-icon--warning` }))), (normalizedProps.invalid || normalizedUpperProps.invalid) && /*#__PURE__*/React.createElement(Text.Text, { as: "div", className: cx(`${prefix}--slider__validation-msg`, `${prefix}--slider__validation-msg--invalid`, `${prefix}--form-requirement`) }, invalidText), (normalizedProps.warn || normalizedUpperProps.warn) && /*#__PURE__*/React.createElement(Text.Text, { as: "div", className: cx(`${prefix}--slider__validation-msg`, `${prefix}--form-requirement`) }, warnText), correctedValue && /*#__PURE__*/React.createElement(Text.Text, { as: "div", role: "alert", className: cx(`${prefix}--slider__status-msg`, `${prefix}--form-requirement`) }, t(translationIds['carbon.slider.auto-correct-announcement'], { correctedValue }))); }); })(); }; Slider.propTypes = { /** * The `ariaLabel` for the `<input>`. */ ariaLabelInput: PropTypes.string, /** * The child nodes. */ children: PropTypes.node, /** * The CSS class name for the slider. */ className: PropTypes.string, /** * `true` to disable this slider. */ disabled: PropTypes.bool, /** * The callback to format the label associated with the minimum/maximum value. */ formatLabel: PropTypes.func, /** * `true` to hide the number input box. */ hideTextInput: PropTypes.bool, /** * The ID of the `<input>`. */ id: PropTypes.string, /** * The `type` attribute of the `<input>`. */ inputType: PropTypes.string, /** * `Specify whether the Slider is currently invalid */ invalid: PropTypes.bool, /** * Provide the text that is displayed when the Slider is in an invalid state */ invalidText: PropTypes.node, /** * The label for the slider. */ labelText: PropTypes.node, /** * Specify whether you want the underlying label to be visually hidden */ hideLabel: PropTypes.bool, /** * `true` to use the light version. */ light: deprecate.deprecate(PropTypes.bool, 'The `light` prop for `Slider` is no longer needed and has ' + 'been deprecated in v11 in favor of the new `Layer` component. It will be moved in the next major release.'), /** * The maximum value. */ max: PropTypes.number.isRequired, /** * The label associated with the maximum value. */ maxLabel: PropTypes.string, /** * The minimum value. */ min: PropTypes.number.isRequired, /** * The label associated with the minimum value. */ minLabel: PropTypes.string, /** * The `name` attribute of the `<input>`. */ name: PropTypes.string, /** * Provide an optional function to be called when the input element * loses focus */ onBlur: PropTypes.func, /** * The callback to get notified of change in value. */ onChange: PropTypes.func, /** * Provide an optional function to be called when a key is pressed in the number input. */ onInputKeyUp: PropTypes.func, /** * The callback to get notified of value on handle release. */ onRelease: PropTypes.func, /** * Whether the slider should be read-only */ readOnly: PropTypes.bool, /** * `true` to specify if the control is required. */ required: PropTypes.bool, /** * A value determining how much the value should increase/decrease by moving the thumb by mouse. If a value other than 1 is provided and the input is *not* hidden, the new step requirement should be added to a visible label. Values outside the `step` increment will be considered invalid. */ step: PropTypes.number, /** * A value determining how much the value should increase/decrease by Shift+arrow keys, * which will be `(max - min) / stepMultiplier`. */ stepMultiplier: PropTypes.number, /** * Supply a method to translate internal strings with your i18n tool of * choice. Translation keys are available on the `translationIds` field for * this component. */ translateWithId: PropTypes.func, /** * The `ariaLabel` for the upper bound `<input>` when there are two handles. */ unstable_ariaLabelInputUpper: PropTypes.string, /** * The `name` attribute of the upper bound `<input>` when there are two handles. */ unstable_nameUpper: PropTypes.string, /** * The upper bound when there are two handles. */ unstable_valueUpper: PropTypes.number, /** * The value of the slider. When there are two handles, value is the lower * bound. */ value: PropTypes.number.isRequired, /** * `Specify whether the Slider is in a warn state */ warn: PropTypes.bool, /** * Provide the text that is displayed when the Slider is in a warn state */ warnText: PropTypes.node }; exports.default = Slider;