@carbon/react
Version:
React components for the Carbon Design System
1,274 lines (1,243 loc) • 44.8 kB
JavaScript
/**
* 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.
*/
import { defineProperty as _defineProperty, extends as _extends } from '../../_virtual/_rollupPluginBabelHelpers.js';
import React, { PureComponent, createRef } from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';
import { ArrowDown, ArrowLeft, ArrowUp, ArrowRight, Enter } from '../../internal/keyboard/keys.js';
import { matches } from '../../internal/keyboard/match.js';
import { PrefixContext } from '../../internal/usePrefix.js';
import { deprecate } from '../../prop-types/deprecate.js';
import { FeatureFlagContext } from '../FeatureFlags/index.js';
import { WarningFilled, WarningAltFilled } from '@carbon/icons-react';
import '../Text/index.js';
import '../Tooltip/DefinitionTooltip.js';
import { Tooltip } from '../Tooltip/Tooltip.js';
import { LowerHandle, LowerHandleFocus, UpperHandle, UpperHandleFocus } from './SliderHandles.js';
import { clamp } from '../../internal/clamp.js';
import { throttle } from '../../node_modules/es-toolkit/dist/compat/function/throttle.js';
import { Text } from '../Text/Text.js';
const ThumbWrapper = ({
hasTooltip,
className,
style,
children,
...rest
}) => {
if (hasTooltip) {
return (
/*#__PURE__*/
// eslint-disable-next-line react/forbid-component-props
React.createElement(Tooltip, _extends({
className: className,
style: style
}, rest), children)
);
} else {
return (
/*#__PURE__*/
// eslint-disable-next-line react/forbid-dom-props
React.createElement("div", {
className: className,
style: style
}, children)
);
}
};
const translationIds = {
autoCorrectAnnouncement: 'carbon.slider.auto-correct-announcement'
};
/**
* Message ids that will be passed to translateWithId().
*/
function translateWithId(translationId, translationState) {
if (translationState?.correctedValue) {
const {
correctedValue
} = translationState;
return `The inputted value "${correctedValue}" was corrected to the nearest allowed digit.`;
}
return '';
}
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 || {});
class Slider extends PureComponent {
constructor(props) {
super(props);
_defineProperty(this, "state", {
value: this.props.value,
valueUpper: this.props.unstable_valueUpper,
left: 0,
leftUpper: 0,
needsOnRelease: false,
isValid: true,
isValidUpper: true,
activeHandle: undefined,
correctedValue: null,
correctedPosition: null,
isRtl: false
});
_defineProperty(this, "thumbRef", void 0);
_defineProperty(this, "thumbRefUpper", void 0);
_defineProperty(this, "filledTrackRef", void 0);
_defineProperty(this, "element", null);
_defineProperty(this, "inputId", '');
_defineProperty(this, "track", void 0);
_defineProperty(this, "handleDrag", event => {
if (event instanceof globalThis.MouseEvent || event instanceof globalThis.TouchEvent) {
this.onDrag(event);
}
});
/**
* Sets up "drag" event handlers and calls `this.onDrag` in case dragging
* started on somewhere other than the thumb without a corresponding "move"
* event.
*/
_defineProperty(this, "onDragStart", evt => {
// Do nothing if component is disabled
if (this.props.disabled || this.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();
// Add drag stop handlers
DRAG_STOP_EVENT_TYPES.forEach(element => {
this.element?.ownerDocument.addEventListener(element, this.onDragStop);
});
// Add drag handlers
DRAG_EVENT_TYPES.forEach(element => {
this.element?.ownerDocument.addEventListener(element, this.handleDrag);
});
const clientX = this.getClientXFromEvent(evt.nativeEvent);
let activeHandle;
if (this.hasTwoHandles()) {
if (evt.target == this.thumbRef.current) {
activeHandle = HandlePosition.LOWER;
} else if (evt.target == this.thumbRefUpper.current) {
activeHandle = HandlePosition.UPPER;
} else if (clientX) {
const distanceToLower = this.calcDistanceToHandle(HandlePosition.LOWER, clientX);
const distanceToUpper = this.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 (this.hasTwoHandles()) {
if (this.thumbRef.current && activeHandle === HandlePosition.LOWER) {
this.thumbRef.current.focus(focusOptions);
} else if (this.thumbRefUpper.current && activeHandle === HandlePosition.UPPER) {
this.thumbRefUpper.current.focus(focusOptions);
}
} else if (this.thumbRef.current) {
this.thumbRef.current.focus(focusOptions);
}
this.setState({
activeHandle
});
// Perform first recalculation since we probably didn't click exactly in the
// middle of the thumb.
this.onDrag(evt.nativeEvent, activeHandle);
});
/**
* Removes "drag" and "drag stop" event handlers and calls sets the flag
* indicating that the `onRelease` callback should be called.
*/
_defineProperty(this, "onDragStop", () => {
// Do nothing if component is disabled
if (this.props.disabled || this.props.readOnly) {
return;
}
// Remove drag stop handlers
DRAG_STOP_EVENT_TYPES.forEach(element => {
this.element?.ownerDocument.removeEventListener(element, this.onDragStop);
});
// Remove drag handlers
DRAG_EVENT_TYPES.forEach(element => {
this.element?.ownerDocument.removeEventListener(element, this.handleDrag);
});
// Set needsOnRelease flag so event fires on next update.
this.setState({
needsOnRelease: true,
isValid: true,
isValidUpper: true
});
});
/**
* 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.
*/
_defineProperty(this, "_onDrag", (evt, activeHandle) => {
activeHandle = activeHandle ?? this.state.activeHandle;
// Do nothing if component is disabled, or we have no event.
if (this.props.disabled || this.props.readOnly || !evt) {
return;
}
const clientX = this.getClientXFromEvent(evt);
const {
value,
left
} = this.calcValue({
clientX,
value: this.state.value
});
// If we're set to two handles, negotiate which drag handle is closest to
// the users' interaction.
if (this.hasTwoHandles() && activeHandle) {
this.setValueLeftForHandle(activeHandle, {
value: this.nearestStepValue(value),
left
});
} else {
this.setState({
value: this.nearestStepValue(value),
left,
isValid: true
});
}
this.setState({
correctedValue: null,
correctedPosition: null
});
});
/**
* Throttles calls to `this._onDrag` by limiting events to being processed at
* most once every `EVENT_THROTTLE` milliseconds.
*/
_defineProperty(this, "onDrag", throttle(this._onDrag, EVENT_THROTTLE, {
leading: true,
trailing: false
}));
/**
* Handles a `keydown` event by recalculating the value/thumb and setting
* state accordingly.
*/
_defineProperty(this, "onKeyDown", evt => {
// Do nothing if component is disabled, or we don't have a valid event
if (this.props.disabled || this.props.readOnly) {
return;
}
const {
step = 1,
stepMultiplier = 4
} = this.props;
let delta = 0;
if (matches(evt, [ArrowDown, ArrowLeft])) {
delta = -step;
} else if (matches(evt, [ArrowUp, 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 (this.hasTwoHandles() && this.state.activeHandle) {
const currentValue = this.state.activeHandle === HandlePosition.LOWER ? this.state.value : this.state.valueUpper;
const {
value,
left
} = this.calcValue({
value: this.calcValueForDelta(currentValue ?? this.props.min, delta, this.props.step)
});
this.setValueLeftForHandle(this.state.activeHandle, {
value: this.nearestStepValue(value),
left
});
} else {
const {
value,
left
} = this.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: this.calcValueForDelta(this.state.value, delta, this.props.step)
});
this.setState({
value: this.nearestStepValue(value),
left,
isValid: true
});
}
this.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.
*/
_defineProperty(this, "onChange", evt => {
// Do nothing if component is disabled
if (this.props.disabled || this.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 (this.hasTwoHandles()) {
if (isNaN(targetValue)) {
this.setValueForHandle(activeHandle, evt.target.value);
} else if (this.isValidValueForPosition({
handle: activeHandle,
value: targetValue,
min: this.props.min,
max: this.props.max
})) {
this.processNewInputValue(evt.target);
} else {
this.setValueForHandle(activeHandle, targetValue);
}
} else {
if (isNaN(targetValue)) {
this.setState({
value: evt.target.value
});
} else if (this.isValidValue({
value: targetValue,
min: this.props.min,
max: this.props.max
})) {
this.processNewInputValue(evt.target);
} else {
this.setState({
value: targetValue
});
}
}
});
/**
* Checks for validity of input value after clicking out of the input. It also
* Handles state change to isValid state.
*/
_defineProperty(this, "onBlur", 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;
this.processNewInputValue(evt.target);
this.props.onBlur?.({
value: targetValue,
handlePosition: evt.target.dataset.handlePosition
});
});
_defineProperty(this, "onInputKeyDown", evt => {
// Do nothing if component is disabled, or we don't have a valid event.
if (this.props.disabled || this.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 (matches(evt, [Enter])) {
this.processNewInputValue(evt.target);
}
});
_defineProperty(this, "processNewInputValue", input => {
this.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) {
this.setState({
isValid: validity
});
} else if (handlePosition === HandlePosition.UPPER) {
this.setState({
isValidUpper: validity
});
}
this.setState({
isValid: validity
});
if (validity) {
const adjustedValue = handlePosition ? this.getAdjustedValueForPosition({
handle: handlePosition,
value: targetValue,
min: this.props.min,
max: this.props.max
}) : this.getAdjustedValue({
value: targetValue,
min: this.props.min,
max: this.props.max
});
if (adjustedValue !== targetValue) {
this.setState({
correctedValue: targetValue.toString(),
correctedPosition: handlePosition
});
} else {
this.setState({
correctedValue: null,
correctedPosition: null
});
}
const {
value,
left
} = this.calcValue({
value: adjustedValue,
useRawValue: true
});
if (handlePosition) {
this.setValueLeftForHandle(handlePosition, {
value: this.nearestStepValue(value),
left
});
} else {
this.setState({
value,
left
});
}
}
});
_defineProperty(this, "calcLeftPercent", ({
clientX,
value,
range
}) => {
const boundingRect = this.element?.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 = this.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 - this.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.
*/
_defineProperty(this, "calcDiscreteValueAndPercent", ({
leftPercent
}) => {
const {
step = 1,
min,
max
} = this.props;
const numSteps = Math.floor((max - min) / step) + ((max - min) % step === 0 ? 1 : 2);
/** 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.
*/
_defineProperty(this, "calcValue", ({
clientX,
value,
useRawValue
}) => {
const range = this.props.max - this.props.min;
const leftPercentRaw = this.calcLeftPercent({
clientX,
value,
range
});
/** `leftPercentRaw` clamped between 0 and 1. */
const leftPercent = clamp(leftPercentRaw, 0, 1);
if (useRawValue) {
return {
value,
left: leftPercent * 100
};
}
// Use the discrete value and percentage for snapping.
const {
discreteValue,
discretePercent
} = this.calcDiscreteValueAndPercent({
leftPercent
});
return {
value: discreteValue,
left: discretePercent * 100
};
});
_defineProperty(this, "calcDistanceToHandle", (handle, clientX) => {
const handleBoundingRect = this.getHandleBoundingRect(handle);
// x co-ordinate 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.
*/
_defineProperty(this, "calcValueForDelta", (currentValue, delta, step = 1) => {
const base = delta > 0 ? Math.floor(currentValue / step) * step : currentValue;
const newValue = base + delta;
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.
*/
_defineProperty(this, "setValueLeftForHandle", (handle, {
value: newValue,
left: newLeft
}) => {
const {
value,
valueUpper,
left,
leftUpper
} = this.state;
if (handle === HandlePosition.LOWER) {
// Don't allow higher than the upper handle.
this.setState({
value: valueUpper && newValue > valueUpper ? valueUpper : newValue,
left: valueUpper && newValue > valueUpper ? leftUpper : newLeft,
isValid: true
});
} else {
this.setState({
valueUpper: value && newValue < value ? value : newValue,
leftUpper: value && newValue < value ? left : newLeft,
isValidUpper: true
});
}
});
_defineProperty(this, "setValueForHandle", (handle, value) => {
if (handle === HandlePosition.LOWER) {
this.setState({
value,
isValid: true
});
} else {
this.setState({
valueUpper: value,
isValidUpper: true
});
}
});
_defineProperty(this, "isValidValueForPosition", ({
handle,
value: newValue,
min,
max
}) => {
const {
value,
valueUpper
} = this.state;
if (!this.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;
});
_defineProperty(this, "isValidValue", ({
value,
min,
max
}) => {
return !(value < min || value > max);
});
_defineProperty(this, "getAdjustedValueForPosition", ({
handle,
value: newValue,
min,
max
}) => {
const {
value,
valueUpper
} = this.state;
newValue = this.getAdjustedValue({
value: newValue,
min,
max
});
// 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;
});
_defineProperty(this, "getAdjustedValue", ({
value,
min,
max
}) => {
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.
*/
_defineProperty(this, "getHandleBoundingRect", handle => {
let boundingRect;
if (handle === HandlePosition.LOWER) {
boundingRect = this.thumbRef.current?.getBoundingClientRect();
} else {
boundingRect = this.thumbRefUpper.current?.getBoundingClientRect();
}
return boundingRect ?? new DOMRect();
});
this.thumbRef = /*#__PURE__*/createRef();
this.thumbRefUpper = /*#__PURE__*/createRef();
this.filledTrackRef = /*#__PURE__*/createRef();
}
/**
* Sets up initial slider position and value in response to component mount.
*/
componentDidMount() {
if (this.element) {
const isRtl = document?.dir === 'rtl';
if (this.hasTwoHandles()) {
const {
value,
left
} = this.calcValue({
value: this.state.value,
useRawValue: true
});
const {
value: valueUpper,
left: leftUpper
} = this.calcValue({
value: this.state.valueUpper,
useRawValue: true
});
this.setState({
isRtl,
value,
left,
valueUpper,
leftUpper
});
if (this.filledTrackRef.current) {
this.filledTrackRef.current.style.transform = this.state.isRtl ? `translate(${100 - this.state.leftUpper}%, -50%) scaleX(${(this.state.leftUpper - this.state.left) / 100})` : `translate(${this.state.left}%, -50%) scaleX(${(this.state.leftUpper - this.state.left) / 100})`;
}
} else {
const {
value,
left
} = this.calcValue({
value: this.state.value,
useRawValue: true
});
this.setState({
isRtl,
value,
left
});
if (this.filledTrackRef.current) {
this.filledTrackRef.current.style.transform = this.state.isRtl ? `translate(100%, -50%) scaleX(-${this.state.left / 100})` : `translate(0%, -50%) scaleX(${this.state.left / 100})`;
}
}
}
}
/**
* Handles firing of `onChange` and `onRelease` callbacks to parent in
* response to state changes.
*
* @param {*} prevProps prevProps
* @param {*} prevState The previous Slider state, used to see if callbacks
* should be called.
*/
componentDidUpdate(prevProps, prevState) {
// Fire onChange event handler if present, if there's a usable value, and
// if the value is different from the last one
if (this.hasTwoHandles()) {
if (this.filledTrackRef.current) {
this.filledTrackRef.current.style.transform = this.state.isRtl ? `translate(${100 - this.state.leftUpper}%, -50%) scaleX(${(this.state.leftUpper - this.state.left) / 100})` : `translate(${this.state.left}%, -50%) scaleX(${(this.state.leftUpper - this.state.left) / 100})`;
}
} else {
if (this.filledTrackRef.current) {
this.filledTrackRef.current.style.transform = this.state.isRtl ? `translate(100%, -50%) scaleX(-${this.state.left / 100})` : `translate(0%, -50%) scaleX(${this.state.left / 100})`;
}
}
if ((prevState.value !== this.state.value || prevState.valueUpper !== this.state.valueUpper) && typeof this.props.onChange === 'function') {
this.props.onChange({
value: this.state.value,
valueUpper: this.state.valueUpper
});
}
// Fire onRelease event handler if present and if needed
if (this.state.needsOnRelease && typeof this.props.onRelease === 'function') {
this.props.onRelease({
value: this.state.value,
valueUpper: this.state.valueUpper
});
// Reset the flag
this.setState({
needsOnRelease: false
});
}
// If value from props does not change, do nothing here.
// Otherwise, do prop -> state sync without "value capping".
if (prevProps.value === this.props.value && prevProps.unstable_valueUpper === this.props.unstable_valueUpper && prevProps.max === this.props.max && prevProps.min === this.props.min) {
return;
}
this.setState(this.calcValue({
value: this.props.value,
useRawValue: true
}));
if (typeof this.props.unstable_valueUpper !== 'undefined') {
const {
value: valueUpper,
left: leftUpper
} = this.calcValue({
value: this.props.unstable_valueUpper,
useRawValue: true
});
this.setState({
valueUpper,
leftUpper
});
} else {
this.setState({
valueUpper: undefined,
leftUpper: undefined
});
}
}
/**
* 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.
*/
nearestStepValue(value = 0) {
const decimals = (this.props.step?.toString().split('.')[1] || '').length;
return Number(value.toFixed(decimals));
}
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;
}
hasTwoHandles() {
return typeof this.state.valueUpper !== 'undefined';
}
// syncs invalid state and prop
static getDerivedStateFromProps(props, state) {
const {
isValid,
isValidUpper
} = state;
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;
}
return Object.keys(derivedState).length ? derivedState : null;
}
render() {
var _Fragment, _Fragment2, _Fragment3, _Fragment4;
const {
ariaLabelInput,
unstable_ariaLabelInputUpper: ariaLabelInputUpper,
className,
hideTextInput = false,
id = this.inputId = this.inputId ||
// 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?
stepMultiplier: _stepMultiplier,
inputType = 'number',
invalidText,
required,
disabled = false,
name,
unstable_nameUpper: nameUpper,
light,
readOnly = false,
warn,
warnText,
translateWithId: t = translateWithId,
...other
} = this.props;
const twoHandles = this.hasTwoHandles();
delete other.onRelease;
delete other.invalid;
delete other.unstable_valueUpper;
const {
value,
valueUpper,
isValid,
isValidUpper,
correctedValue,
correctedPosition,
isRtl
} = this.state;
const showWarning = !readOnly && warn ||
// TODO: https://github.com/carbon-design-system/carbon/issues/18991#issuecomment-2795709637
typeof correctedValue !== null && correctedPosition === HandlePosition.LOWER && isValid;
const showWarningUpper = !readOnly && warn ||
// TODO: https://github.com/carbon-design-system/carbon/issues/18991#issuecomment-2795709637
typeof correctedValue !== null && correctedPosition === (twoHandles ? HandlePosition.UPPER : HandlePosition.LOWER) && (twoHandles ? isValidUpper : isValid);
return /*#__PURE__*/React.createElement(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`]: !readOnly && !isValid,
[`${prefix}--slider-text-input--warn`]: showWarning
}]);
const upperInputClasses = cx([...fixedInputClasses, `${prefix}--slider-text-input--upper`, conditionalInputClasses, {
[`${prefix}--text-input--invalid`]: !readOnly && (twoHandles ? !isValidUpper : !isValid),
[`${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: `${this.state.left}%`
}
};
const upperThumbWrapperProps = {
style: {
insetInlineStart: `${this.state.leftUpper}%`
}
};
return /*#__PURE__*/React.createElement("div", {
className: cx(`${prefix}--form-item`, className)
}, /*#__PURE__*/React.createElement(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: this.onChange,
onBlur: this.onBlur,
onKeyUp: this.props.onInputKeyUp,
onKeyDown: this.onInputKeyDown,
"data-invalid": !isValid && !readOnly ? true : null,
"data-handle-position": HandlePosition.LOWER,
"aria-invalid": !isValid && !readOnly ? true : undefined,
readOnly: readOnly
}), !readOnly && !isValid && /*#__PURE__*/React.createElement(WarningFilled, {
className: `${prefix}--slider__invalid-icon`
}), showWarning && /*#__PURE__*/React.createElement(WarningAltFilled, {
className: `${prefix}--slider__invalid-icon ${prefix}--slider__invalid-icon--warning`
})) : null, /*#__PURE__*/React.createElement(Text, {
className: `${prefix}--slider__range-label`
}, formatLabel(min, minLabel)), /*#__PURE__*/React.createElement("div", _extends({
className: sliderClasses,
ref: node => {
this.element = node;
},
onMouseDown: this.onDragStart,
onTouchStart: this.onDragStart,
onKeyDown: this.onKeyDown,
role: "presentation",
tabIndex: -1,
"data-invalid": (twoHandles ? !isValid || !isValidUpper : !isValid) && !readOnly ? true : null
}, other), /*#__PURE__*/React.createElement(ThumbWrapper, _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: this.thumbRef,
onFocus: () => this.setState({
activeHandle: HandlePosition.LOWER
})
}, twoHandles && !isRtl ? _Fragment || (_Fragment = /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(LowerHandle, {
"aria-label": ariaLabelInput
}), /*#__PURE__*/React.createElement(LowerHandleFocus, {
"aria-label": ariaLabelInput
}))) : twoHandles && isRtl ? _Fragment2 || (_Fragment2 = /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(UpperHandle, {
"aria-label": ariaLabelInputUpper
}), /*#__PURE__*/React.createElement(UpperHandleFocus, {
"aria-label": ariaLabelInputUpper
}))) : undefined)), twoHandles ? /*#__PURE__*/React.createElement(ThumbWrapper, _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: this.thumbRefUpper,
onFocus: () => this.setState({
activeHandle: HandlePosition.UPPER
})
}, twoHandles && !isRtl ? _Fragment3 || (_Fragment3 = /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(UpperHandle, {
"aria-label": ariaLabelInputUpper
}), /*#__PURE__*/React.createElement(UpperHandleFocus, {
"aria-label": ariaLabelInputUpper
}))) : twoHandles && isRtl ? _Fragment4 || (_Fragment4 = /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(LowerHandle, {
"aria-label": ariaLabelInput
}), /*#__PURE__*/React.createElement(LowerHandleFocus, {
"aria-label": ariaLabelInput
}))) : undefined)) : null, /*#__PURE__*/React.createElement("div", {
className: `${prefix}--slider__track`,
ref: node => {
this.track = node;
}
}), /*#__PURE__*/React.createElement("div", {
className: `${prefix}--slider__filled-track`,
ref: this.filledTrackRef
})), /*#__PURE__*/React.createElement(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: this.onChange,
onBlur: this.onBlur,
onKeyDown: this.onInputKeyDown,
onKeyUp: this.props.onInputKeyUp,
"data-invalid": (twoHandles ? !isValidUpper : !isValid) && !readOnly ? true : null,
"data-handle-position": twoHandles ? HandlePosition.UPPER : null,
"aria-invalid": (twoHandles ? !isValidUpper : !isValid) && !readOnly ? true : undefined,
readOnly: readOnly
}), !readOnly && (twoHandles ? !isValidUpper : !isValid) && /*#__PURE__*/React.createElement(WarningFilled, {
className: `${prefix}--slider__invalid-icon`
}), showWarningUpper && /*#__PURE__*/React.createElement(WarningAltFilled, {
className: `${prefix}--slider__invalid-icon ${prefix}--slider__invalid-icon--warning`
}))), !readOnly && (!isValid || !isValidUpper) && /*#__PURE__*/React.createElement(Text, {
as: "div",
className: cx(`${prefix}--slider__validation-msg`, `${prefix}--slider__validation-msg--invalid`, `${prefix}--form-requirement`)
}, invalidText), !readOnly && warn && isValid && isValidUpper && /*#__PURE__*/React.createElement(Text, {
as: "div",
className: cx(`${prefix}--slider__validation-msg`, `${prefix}--form-requirement`)
}, warnText), correctedValue && /*#__PURE__*/React.createElement(Text, {
as: "div",
role: "alert",
className: cx(`${prefix}--slider__status-msg`, `${prefix}--form-requirement`)
}, t(translationIds.autoCorrectAnnouncement, {
correctedValue
})));
});
}
}
_defineProperty(Slider, "contextType", FeatureFlagContext);
_defineProperty(Slider, "translationIds", Object.values(translationIds));
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(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
};
export { Slider as default };