@carbon/react 1.90.0 → 1.91.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/.playwright/INTERNAL_AVT_REPORT_DO_NOT_USE.json +921 -921
  2. package/es/components/CodeSnippet/CodeSnippet.d.ts +1 -1
  3. package/es/components/CodeSnippet/CodeSnippet.js +1 -1
  4. package/es/components/ComboBox/ComboBox.js +1 -12
  5. package/es/components/ComboButton/index.js +1 -1
  6. package/es/components/ComposedModal/ComposedModal.js +1 -1
  7. package/es/components/Copy/Copy.d.ts +1 -1
  8. package/es/components/Copy/Copy.js +1 -1
  9. package/es/components/CopyButton/CopyButton.d.ts +1 -1
  10. package/es/components/CopyButton/CopyButton.js +1 -1
  11. package/es/components/DataTable/DataTable.d.ts +60 -15
  12. package/es/components/DataTable/DataTable.js +106 -179
  13. package/es/components/DataTable/Table.d.ts +2 -2
  14. package/es/components/DataTable/Table.js +1 -1
  15. package/es/components/DataTable/TableExpandHeader.d.ts +1 -1
  16. package/es/components/DataTable/TableExpandHeader.js +1 -1
  17. package/es/components/DatePicker/DatePicker.d.ts +0 -12
  18. package/es/components/DatePicker/DatePicker.js +3 -3
  19. package/es/components/DatePicker/plugins/rangePlugin.d.ts +19 -2
  20. package/es/components/DatePicker/plugins/rangePlugin.js +18 -14
  21. package/es/components/Dropdown/Dropdown.js +1 -12
  22. package/es/components/FeatureFlags/index.js +1 -0
  23. package/es/components/IconButton/index.js +1 -1
  24. package/es/components/Menu/MenuItem.d.ts +1 -1
  25. package/es/components/Menu/MenuItem.js +5 -5
  26. package/es/components/Modal/Modal.js +1 -1
  27. package/es/components/MultiSelect/FilterableMultiSelect.js +1 -1
  28. package/es/components/MultiSelect/MultiSelect.js +1 -12
  29. package/es/components/Notification/Notification.d.ts +6 -6
  30. package/es/components/Notification/Notification.js +6 -6
  31. package/es/components/OverflowMenu/OverflowMenu.js +1 -1
  32. package/es/components/OverflowMenu/next/index.js +1 -1
  33. package/es/components/Popover/index.js +1 -1
  34. package/es/components/Search/Search.d.ts +4 -2
  35. package/es/components/Search/Search.js +5 -4
  36. package/es/components/Slider/Slider.d.ts +144 -188
  37. package/es/components/Slider/Slider.js +787 -710
  38. package/es/components/Slider/index.d.ts +2 -2
  39. package/es/components/Tabs/Tabs.d.ts +4 -0
  40. package/es/components/TextArea/TextArea.js +13 -6
  41. package/es/components/TextInput/ControlledPasswordInput.js +2 -2
  42. package/es/components/TextInput/PasswordInput.js +2 -2
  43. package/es/components/TextInput/TextInput.js +2 -2
  44. package/es/components/TextInput/util.d.ts +17 -5
  45. package/es/components/TextInput/util.js +2 -7
  46. package/es/components/UIShell/HeaderPanel.d.ts +1 -1
  47. package/es/index.d.ts +12 -13
  48. package/es/index.js +25 -24
  49. package/es/internal/defaultItemToString.d.ts +7 -0
  50. package/es/internal/defaultItemToString.js +17 -0
  51. package/es/internal/index.d.ts +1 -0
  52. package/es/prop-types/deprecateValuesWithin.d.ts +8 -1
  53. package/es/prop-types/deprecateValuesWithin.js +6 -6
  54. package/es/prop-types/requiredIfGivenPropIsTruthy.d.ts +8 -7
  55. package/es/prop-types/requiredIfGivenPropIsTruthy.js +10 -10
  56. package/lib/components/CodeSnippet/CodeSnippet.d.ts +1 -1
  57. package/lib/components/CodeSnippet/CodeSnippet.js +1 -1
  58. package/lib/components/ComboBox/ComboBox.js +3 -14
  59. package/lib/components/ComboButton/index.js +1 -1
  60. package/lib/components/ComposedModal/ComposedModal.js +1 -1
  61. package/lib/components/Copy/Copy.d.ts +1 -1
  62. package/lib/components/Copy/Copy.js +1 -1
  63. package/lib/components/CopyButton/CopyButton.d.ts +1 -1
  64. package/lib/components/CopyButton/CopyButton.js +1 -1
  65. package/lib/components/DataTable/DataTable.d.ts +60 -15
  66. package/lib/components/DataTable/DataTable.js +106 -179
  67. package/lib/components/DataTable/Table.d.ts +2 -2
  68. package/lib/components/DataTable/Table.js +1 -1
  69. package/lib/components/DataTable/TableExpandHeader.d.ts +1 -1
  70. package/lib/components/DataTable/TableExpandHeader.js +3 -3
  71. package/lib/components/DatePicker/DatePicker.d.ts +0 -12
  72. package/lib/components/DatePicker/DatePicker.js +2 -2
  73. package/lib/components/DatePicker/plugins/rangePlugin.d.ts +19 -2
  74. package/lib/components/DatePicker/plugins/rangePlugin.js +18 -16
  75. package/lib/components/Dropdown/Dropdown.js +3 -14
  76. package/lib/components/FeatureFlags/index.js +1 -0
  77. package/lib/components/IconButton/index.js +1 -1
  78. package/lib/components/Menu/MenuItem.d.ts +1 -1
  79. package/lib/components/Menu/MenuItem.js +6 -6
  80. package/lib/components/Modal/Modal.js +1 -1
  81. package/lib/components/MultiSelect/FilterableMultiSelect.js +8 -8
  82. package/lib/components/MultiSelect/MultiSelect.js +2 -13
  83. package/lib/components/Notification/Notification.d.ts +6 -6
  84. package/lib/components/Notification/Notification.js +6 -6
  85. package/lib/components/OverflowMenu/OverflowMenu.js +1 -1
  86. package/lib/components/OverflowMenu/next/index.js +1 -1
  87. package/lib/components/Popover/index.js +1 -1
  88. package/lib/components/Search/Search.d.ts +4 -2
  89. package/lib/components/Search/Search.js +5 -4
  90. package/lib/components/Slider/Slider.d.ts +144 -188
  91. package/lib/components/Slider/Slider.js +784 -709
  92. package/lib/components/Slider/index.d.ts +2 -2
  93. package/lib/components/Tabs/Tabs.d.ts +4 -0
  94. package/lib/components/TextArea/TextArea.js +13 -6
  95. package/lib/components/TextInput/ControlledPasswordInput.js +1 -1
  96. package/lib/components/TextInput/PasswordInput.js +1 -1
  97. package/lib/components/TextInput/TextInput.js +1 -1
  98. package/lib/components/TextInput/util.d.ts +17 -5
  99. package/lib/components/TextInput/util.js +2 -7
  100. package/lib/components/UIShell/HeaderPanel.d.ts +1 -1
  101. package/lib/index.d.ts +12 -13
  102. package/lib/index.js +51 -28
  103. package/lib/internal/defaultItemToString.d.ts +7 -0
  104. package/lib/internal/defaultItemToString.js +19 -0
  105. package/lib/internal/index.d.ts +1 -0
  106. package/lib/prop-types/deprecateValuesWithin.d.ts +8 -1
  107. package/lib/prop-types/deprecateValuesWithin.js +6 -8
  108. package/lib/prop-types/requiredIfGivenPropIsTruthy.d.ts +8 -7
  109. package/lib/prop-types/requiredIfGivenPropIsTruthy.js +10 -12
  110. package/package.json +8 -7
  111. package/es/components/MultiSelect/tools/itemToString.d.ts +0 -1
  112. package/es/components/MultiSelect/tools/itemToString.js +0 -21
  113. package/es/components/Slider/index.js +0 -14
  114. package/es/internal/createClassWrapper.js +0 -23
  115. package/lib/components/MultiSelect/tools/itemToString.d.ts +0 -1
  116. package/lib/components/MultiSelect/tools/itemToString.js +0 -23
  117. package/lib/components/Slider/index.js +0 -20
  118. package/lib/internal/createClassWrapper.js +0 -25
@@ -5,15 +5,14 @@
5
5
  * LICENSE file in the root directory of this source tree.
6
6
  */
7
7
 
8
- import { defineProperty as _defineProperty, extends as _extends } from '../../_virtual/_rollupPluginBabelHelpers.js';
9
- import React, { PureComponent, createRef } from 'react';
8
+ import { extends as _extends } from '../../_virtual/_rollupPluginBabelHelpers.js';
9
+ import React, { useReducer, useRef, useEffect, useMemo } from 'react';
10
10
  import PropTypes from 'prop-types';
11
11
  import cx from 'classnames';
12
- import { ArrowDown, ArrowLeft, ArrowUp, ArrowRight, Enter } from '../../internal/keyboard/keys.js';
12
+ import { Enter, ArrowDown, ArrowLeft, ArrowUp, ArrowRight } from '../../internal/keyboard/keys.js';
13
13
  import { matches } from '../../internal/keyboard/match.js';
14
14
  import { PrefixContext } from '../../internal/usePrefix.js';
15
15
  import { deprecate } from '../../prop-types/deprecate.js';
16
- import { FeatureFlagContext } from '../FeatureFlags/index.js';
17
16
  import { WarningFilled, WarningAltFilled } from '@carbon/icons-react';
18
17
  import '../Text/index.js';
19
18
  import '../Tooltip/DefinitionTooltip.js';
@@ -76,752 +75,834 @@ var HandlePosition = /*#__PURE__*/function (HandlePosition) {
76
75
  HandlePosition["LOWER"] = "lower";
77
76
  HandlePosition["UPPER"] = "upper";
78
77
  return HandlePosition;
79
- }(HandlePosition || {});
80
- class Slider extends PureComponent {
81
- constructor(props) {
82
- super(props);
83
- _defineProperty(this, "state", {
84
- value: this.props.value,
85
- valueUpper: this.props.unstable_valueUpper,
86
- left: 0,
87
- leftUpper: 0,
88
- needsOnRelease: false,
89
- isValid: true,
90
- isValidUpper: true,
91
- activeHandle: undefined,
92
- correctedValue: null,
93
- correctedPosition: null,
94
- isRtl: false
95
- });
96
- _defineProperty(this, "thumbRef", void 0);
97
- _defineProperty(this, "thumbRefUpper", void 0);
98
- _defineProperty(this, "filledTrackRef", void 0);
99
- _defineProperty(this, "element", null);
100
- _defineProperty(this, "inputId", '');
101
- _defineProperty(this, "track", void 0);
102
- _defineProperty(this, "handleDrag", event => {
103
- if (event instanceof globalThis.MouseEvent || event instanceof globalThis.TouchEvent) {
104
- this.onDrag(event);
105
- }
106
- });
107
- /**
108
- * Sets up "drag" event handlers and calls `this.onDrag` in case dragging
109
- * started on somewhere other than the thumb without a corresponding "move"
110
- * event.
111
- */
112
- _defineProperty(this, "onDragStart", evt => {
113
- // Do nothing if component is disabled
114
- if (this.props.disabled || this.props.readOnly) {
115
- return;
116
- }
117
-
118
- // We're going to force focus on one of the handles later on here, b/c we're
119
- // firing on a mousedown event, we need to call event.preventDefault() to
120
- // keep the focus from leaving the HTMLElement.
121
- // @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#notes
122
- evt.preventDefault();
123
-
124
- // Add drag stop handlers
125
- DRAG_STOP_EVENT_TYPES.forEach(element => {
126
- this.element?.ownerDocument.addEventListener(element, this.onDragStop);
127
- });
128
-
129
- // Add drag handlers
130
- DRAG_EVENT_TYPES.forEach(element => {
131
- this.element?.ownerDocument.addEventListener(element, this.handleDrag);
132
- });
133
- const clientX = this.getClientXFromEvent(evt.nativeEvent);
134
- let activeHandle;
135
- if (this.hasTwoHandles()) {
136
- if (evt.target == this.thumbRef.current) {
137
- activeHandle = HandlePosition.LOWER;
138
- } else if (evt.target == this.thumbRefUpper.current) {
139
- activeHandle = HandlePosition.UPPER;
140
- } else if (clientX) {
141
- const distanceToLower = this.calcDistanceToHandle(HandlePosition.LOWER, clientX);
142
- const distanceToUpper = this.calcDistanceToHandle(HandlePosition.UPPER, clientX);
143
- if (distanceToLower <= distanceToUpper) {
144
- activeHandle = HandlePosition.LOWER;
145
- } else {
146
- activeHandle = HandlePosition.UPPER;
147
- }
148
- }
149
- }
78
+ }(HandlePosition || {}); // TODO: Delete this type and directory type the properties in the function.
79
+ const Slider = props => {
80
+ // TODO: Move destructured `props` from the IIFE to here.
150
81
 
151
- // Force focus to the appropriate handle.
152
- const focusOptions = {
153
- preventScroll: true
154
- };
155
- if (this.hasTwoHandles()) {
156
- if (this.thumbRef.current && activeHandle === HandlePosition.LOWER) {
157
- this.thumbRef.current.focus(focusOptions);
158
- } else if (this.thumbRefUpper.current && activeHandle === HandlePosition.UPPER) {
159
- this.thumbRefUpper.current.focus(focusOptions);
160
- }
161
- } else if (this.thumbRef.current) {
162
- this.thumbRef.current.focus(focusOptions);
163
- }
164
- this.setState({
165
- activeHandle
166
- });
82
+ const initialState = {
83
+ value: props.value,
84
+ valueUpper: props.unstable_valueUpper,
85
+ left: 0,
86
+ leftUpper: 0,
87
+ needsOnRelease: false,
88
+ isValid: true,
89
+ isValidUpper: true,
90
+ activeHandle: undefined,
91
+ correctedValue: null,
92
+ correctedPosition: null,
93
+ isRtl: false
94
+ };
167
95
 
168
- // Perform first recalculation since we probably didn't click exactly in the
169
- // middle of the thumb.
170
- this.onDrag(evt.nativeEvent, activeHandle);
171
- });
172
- /**
173
- * Removes "drag" and "drag stop" event handlers and calls sets the flag
174
- * indicating that the `onRelease` callback should be called.
175
- */
176
- _defineProperty(this, "onDragStop", () => {
177
- // Do nothing if component is disabled
178
- if (this.props.disabled || this.props.readOnly) {
179
- return;
180
- }
96
+ // TODO: Investigate using generics on the hook.
97
+ const [state, setState] = useReducer((prev, args) => ({
98
+ ...prev,
99
+ ...args
100
+ }), initialState);
181
101
 
182
- // Remove drag stop handlers
183
- DRAG_STOP_EVENT_TYPES.forEach(element => {
184
- this.element?.ownerDocument.removeEventListener(element, this.onDragStop);
185
- });
102
+ // TODO: Investigate getting rid of these references.
103
+ const stateRef = useRef(state);
104
+ useEffect(() => {
105
+ stateRef.current = state;
106
+ }, [state]);
107
+ const propsRef = useRef(props);
108
+ useEffect(() => {
109
+ propsRef.current = props;
110
+ }, [props]);
111
+ const thumbRef = useRef(null);
112
+ const thumbRefUpper = useRef(null);
113
+ const filledTrackRef = useRef(null);
114
+ const elementRef = useRef(null);
115
+ const trackRef = useRef(null);
116
+ const inputIdRef = useRef('');
186
117
 
187
- // Remove drag handlers
188
- DRAG_EVENT_TYPES.forEach(element => {
189
- this.element?.ownerDocument.removeEventListener(element, this.handleDrag);
190
- });
118
+ // TODO: Delete this function and set its return value as the value of
119
+ // `twoHandles`.
120
+ const hasTwoHandles = () => {
121
+ return typeof state.valueUpper !== 'undefined';
122
+ };
123
+ const twoHandles = hasTwoHandles();
191
124
 
192
- // Set needsOnRelease flag so event fires on next update.
193
- this.setState({
194
- needsOnRelease: true,
195
- isValid: true,
196
- isValidUpper: true
197
- });
198
- });
199
- /**
200
- * Handles a "drag" event by recalculating the value/thumb and setting state
201
- * accordingly.
202
- *
203
- * @param evt The event.
204
- * @param activeHandle The first drag event call, we may have an explicit
205
- * activeHandle value, which is to be used before state is used.
206
- */
207
- _defineProperty(this, "_onDrag", (evt, activeHandle) => {
208
- activeHandle = activeHandle ?? this.state.activeHandle;
209
- // Do nothing if component is disabled, or we have no event.
210
- if (this.props.disabled || this.props.readOnly || !evt) {
211
- return;
212
- }
213
- const clientX = this.getClientXFromEvent(evt);
214
- const {
215
- value,
216
- left
217
- } = this.calcValue({
218
- clientX,
219
- value: this.state.value
220
- });
221
- // If we're set to two handles, negotiate which drag handle is closest to
222
- // the users' interaction.
223
- if (this.hasTwoHandles() && activeHandle) {
224
- this.setValueLeftForHandle(activeHandle, {
225
- value: this.nearestStepValue(value),
125
+ /**
126
+ * Sets up initial slider position and value in response to component mount.
127
+ */
128
+ useEffect(() => {
129
+ if (elementRef.current) {
130
+ const isRtl = document?.dir === 'rtl';
131
+ if (hasTwoHandles()) {
132
+ const {
133
+ value,
226
134
  left
135
+ } = calcValue({
136
+ value: stateRef.current.value,
137
+ useRawValue: true
227
138
  });
228
- } else {
229
- this.setState({
230
- value: this.nearestStepValue(value),
139
+ const {
140
+ value: valueUpper,
141
+ left: leftUpper
142
+ } = calcValue({
143
+ value: stateRef.current.valueUpper,
144
+ useRawValue: true
145
+ });
146
+ setState({
147
+ isRtl,
148
+ value,
231
149
  left,
232
- isValid: true
150
+ valueUpper,
151
+ leftUpper
233
152
  });
234
- }
235
- this.setState({
236
- correctedValue: null,
237
- correctedPosition: null
238
- });
239
- });
240
- /**
241
- * Throttles calls to `this._onDrag` by limiting events to being processed at
242
- * most once every `EVENT_THROTTLE` milliseconds.
243
- */
244
- _defineProperty(this, "onDrag", throttle(this._onDrag, EVENT_THROTTLE, {
245
- leading: true,
246
- trailing: false
247
- }));
248
- /**
249
- * Handles a `keydown` event by recalculating the value/thumb and setting
250
- * state accordingly.
251
- */
252
- _defineProperty(this, "onKeyDown", evt => {
253
- // Do nothing if component is disabled, or we don't have a valid event
254
- if (this.props.disabled || this.props.readOnly) {
255
- return;
256
- }
257
- const {
258
- step = 1,
259
- stepMultiplier = 4
260
- } = this.props;
261
- let delta = 0;
262
- if (matches(evt, [ArrowDown, ArrowLeft])) {
263
- delta = -step;
264
- } else if (matches(evt, [ArrowUp, ArrowRight])) {
265
- delta = step;
266
153
  } else {
267
- // Ignore keys we don't want to handle
268
- return;
269
- }
270
-
271
- // If shift was held, account for the stepMultiplier
272
- if (evt.shiftKey) {
273
- delta *= stepMultiplier;
274
- }
275
- if (this.hasTwoHandles() && this.state.activeHandle) {
276
- const currentValue = this.state.activeHandle === HandlePosition.LOWER ? this.state.value : this.state.valueUpper;
277
154
  const {
278
155
  value,
279
156
  left
280
- } = this.calcValue({
281
- value: this.calcValueForDelta(currentValue ?? this.props.min, delta, this.props.step)
282
- });
283
- this.setValueLeftForHandle(this.state.activeHandle, {
284
- value: this.nearestStepValue(value),
285
- left
157
+ } = calcValue({
158
+ value: stateRef.current.value,
159
+ useRawValue: true
286
160
  });
287
- } else {
288
- const {
161
+ setState({
162
+ isRtl,
289
163
  value,
290
164
  left
291
- } = this.calcValue({
292
- // Ensures custom value from `<input>` won't cause skipping next stepping
293
- // point with right arrow key, e.g. Typing 51 in `<input>`, moving focus
294
- // onto the thumb and the hitting right arrow key should yield 52 instead
295
- // of 54.
296
- value: this.calcValueForDelta(this.state.value, delta, this.props.step)
297
- });
298
- this.setState({
299
- value: this.nearestStepValue(value),
300
- left,
301
- isValid: true
302
165
  });
303
166
  }
304
- this.setState({
305
- correctedValue: null,
306
- correctedPosition: null
307
- });
308
- });
309
- /**
310
- * Provides the two-way binding for the input field of the Slider. It also
311
- * Handles a change to the input field by recalculating the value/thumb and
312
- * setting state accordingly.
313
- */
314
- _defineProperty(this, "onChange", evt => {
315
- // Do nothing if component is disabled
316
- if (this.props.disabled || this.props.readOnly) {
317
- return;
318
- }
319
-
320
- // Do nothing if we have no valid event, target, or value
321
- if (!evt || !('target' in evt) || typeof evt.target.value !== 'string') {
322
- return;
323
- }
167
+ }
168
+ return () => {
169
+ DRAG_STOP_EVENT_TYPES.forEach(element => elementRef.current?.ownerDocument.removeEventListener(element, onDragStop));
170
+ DRAG_EVENT_TYPES.forEach(element => elementRef.current?.ownerDocument.removeEventListener(element, handleDrag));
171
+ };
172
+ // eslint-disable-next-line react-hooks/exhaustive-deps
173
+ }, []);
174
+ useEffect(() => {
175
+ // TODO: Uncomment this code and delete all of the `filledTrackRef.current`
176
+ // checks.
177
+ // const el = filledTrackRef.current;
178
+ //
179
+ // if (!el) return;
324
180
 
325
- // Avoid calling calcValue for invalid numbers, but still update the state.
326
- const activeHandle = evt.target.dataset.handlePosition ?? HandlePosition.LOWER;
327
- const targetValue = Number.parseFloat(evt.target.value);
328
- if (this.hasTwoHandles()) {
329
- if (isNaN(targetValue)) {
330
- this.setValueForHandle(activeHandle, evt.target.value);
331
- } else if (this.isValidValueForPosition({
332
- handle: activeHandle,
333
- value: targetValue,
334
- min: this.props.min,
335
- max: this.props.max
336
- })) {
337
- this.processNewInputValue(evt.target);
338
- } else {
339
- this.setValueForHandle(activeHandle, targetValue);
340
- }
341
- } else {
342
- if (isNaN(targetValue)) {
343
- this.setState({
344
- value: evt.target.value
345
- });
346
- } else if (this.isValidValue({
347
- value: targetValue,
348
- min: this.props.min,
349
- max: this.props.max
350
- })) {
351
- this.processNewInputValue(evt.target);
352
- } else {
353
- this.setState({
354
- value: targetValue
355
- });
356
- }
357
- }
358
- });
359
- /**
360
- * Checks for validity of input value after clicking out of the input. It also
361
- * Handles state change to isValid state.
362
- */
363
- _defineProperty(this, "onBlur", evt => {
364
- // Do nothing if we have no valid event, target, or value
365
- if (!evt || !('target' in evt) || typeof evt.target.value !== 'string') {
366
- return;
181
+ // Fire onChange event handler if present, if there's a usable value, and
182
+ // if the value is different from the last one
183
+ if (hasTwoHandles()) {
184
+ if (filledTrackRef.current) {
185
+ 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})`;
367
186
  }
368
- const {
369
- value: targetValue
370
- } = evt.target;
371
- this.processNewInputValue(evt.target);
372
- this.props.onBlur?.({
373
- value: targetValue,
374
- handlePosition: evt.target.dataset.handlePosition
375
- });
376
- });
377
- _defineProperty(this, "onInputKeyDown", evt => {
378
- // Do nothing if component is disabled, or we don't have a valid event.
379
- if (this.props.disabled || this.props.readOnly || !(evt.target instanceof HTMLInputElement)) {
380
- return;
187
+ } else {
188
+ if (filledTrackRef.current) {
189
+ filledTrackRef.current.style.transform = state.isRtl ? `translate(100%, -50%) scaleX(-${state.left / 100})` : `translate(0%, -50%) scaleX(${state.left / 100})`;
381
190
  }
191
+ }
192
+ // TODO: Investigate whether the missing dependency should be added.
193
+ //
194
+ // eslint-disable-next-line react-hooks/exhaustive-deps
195
+ }, [state.left, state.leftUpper, state.isRtl]);
382
196
 
383
- // Do nothing if we have no valid event, target, or value.
384
- if (!evt || !('target' in evt) || typeof evt.target.value !== 'string') {
385
- return;
386
- }
387
- if (matches(evt, [Enter])) {
388
- this.processNewInputValue(evt.target);
389
- }
390
- });
391
- _defineProperty(this, "processNewInputValue", input => {
392
- this.setState({
393
- correctedValue: null,
394
- correctedPosition: null
197
+ // Fire onChange when value(s) change
198
+ const prevValsRef = useRef(null);
199
+ useEffect(() => {
200
+ const prev = prevValsRef.current;
201
+ if (prev && (prev.value !== state.value || prev.valueUpper !== state.valueUpper) && typeof props.onChange === 'function') {
202
+ props.onChange({
203
+ value: state.value,
204
+ valueUpper: state.valueUpper
395
205
  });
396
- const targetValue = Number.parseFloat(input.value);
397
- const validity = !isNaN(targetValue);
398
-
399
- // When there are two handles, we'll also have the data-handle-position
400
- // attribute to consider the other value before settling on the validity to
401
- // set.
402
- const handlePosition = input.dataset.handlePosition;
403
- if (handlePosition === HandlePosition.LOWER) {
404
- this.setState({
405
- isValid: validity
406
- });
407
- } else if (handlePosition === HandlePosition.UPPER) {
408
- this.setState({
409
- isValidUpper: validity
410
- });
411
- }
412
- this.setState({
413
- isValid: validity
206
+ }
207
+ prevValsRef.current = {
208
+ value: state.value,
209
+ valueUpper: state.valueUpper
210
+ };
211
+ // TODO: Investigate whether the missing dependency should be added.
212
+ //
213
+ // eslint-disable-next-line react-hooks/exhaustive-deps
214
+ }, [state.value, state.valueUpper, props.onChange]);
215
+ useEffect(() => {
216
+ // Fire onRelease event handler if present and if needed
217
+ if (state.needsOnRelease && typeof props.onRelease === 'function') {
218
+ props.onRelease({
219
+ value: state.value,
220
+ valueUpper: state.valueUpper
414
221
  });
415
- if (validity) {
416
- const adjustedValue = handlePosition ? this.getAdjustedValueForPosition({
417
- handle: handlePosition,
418
- value: targetValue,
419
- min: this.props.min,
420
- max: this.props.max
421
- }) : this.getAdjustedValue({
422
- value: targetValue,
423
- min: this.props.min,
424
- max: this.props.max
425
- });
426
- if (adjustedValue !== targetValue) {
427
- this.setState({
428
- correctedValue: targetValue.toString(),
429
- correctedPosition: handlePosition
430
- });
431
- } else {
432
- this.setState({
433
- correctedValue: null,
434
- correctedPosition: null
435
- });
436
- }
222
+ // Reset the flag
223
+ setState({
224
+ needsOnRelease: false
225
+ });
226
+ }
227
+ // TODO: Investigate whether the missing dependency should be added.
228
+ //
229
+ // eslint-disable-next-line react-hooks/exhaustive-deps
230
+ }, [state.needsOnRelease, state.value, state.valueUpper, props.onRelease]);
231
+ const prevSyncKeysRef = useRef(null);
232
+ useEffect(() => {
233
+ const prev = prevSyncKeysRef.current;
234
+ const next = [props.value, props.unstable_valueUpper, props.max, props.min];
235
+
236
+ // If value from props does not change, do nothing here.
237
+ // Otherwise, do prop -> state sync without "value capping".
238
+ if (!prev || prev[0] !== next[0] || prev[1] !== next[1] || prev[2] !== next[2] || prev[3] !== next[3]) {
239
+ setState(calcValue({
240
+ value: props.value,
241
+ useRawValue: true
242
+ }));
243
+ if (typeof props.unstable_valueUpper !== 'undefined') {
437
244
  const {
438
- value,
439
- left
440
- } = this.calcValue({
441
- value: adjustedValue,
245
+ value: valueUpper,
246
+ left: leftUpper
247
+ } = calcValue({
248
+ value: props.unstable_valueUpper,
442
249
  useRawValue: true
443
250
  });
444
- if (handlePosition) {
445
- this.setValueLeftForHandle(handlePosition, {
446
- value: this.nearestStepValue(value),
447
- left
448
- });
449
- } else {
450
- this.setState({
451
- value,
452
- left
453
- });
454
- }
251
+ setState({
252
+ valueUpper,
253
+ leftUpper
254
+ });
255
+ } else {
256
+ setState({
257
+ valueUpper: undefined,
258
+ leftUpper: undefined
259
+ });
455
260
  }
261
+ prevSyncKeysRef.current = next;
262
+ }
263
+ // TODO: Investigate whether the missing dependency should be added.
264
+ //
265
+ // eslint-disable-next-line react-hooks/exhaustive-deps
266
+ }, [props.value, props.unstable_valueUpper, props.max, props.min]);
267
+
268
+ /**
269
+ * Rounds a given value to the nearest step defined by the slider's `step`
270
+ * prop.
271
+ *
272
+ * @param value - The value to adjust to the nearest step. Defaults to `0`.
273
+ * @returns The value rounded to the precision determined by the step.
274
+ */
275
+ const nearestStepValue = (value = 0) => {
276
+ // TODO: Use a nullish coalescing operator.
277
+ const decimals = (props.step?.toString().split('.')[1] || '').length;
278
+ return Number(value.toFixed(decimals));
279
+ };
280
+ const handleDrag = event => {
281
+ if (event instanceof globalThis.MouseEvent || event instanceof globalThis.TouchEvent) {
282
+ onDrag(event);
283
+ }
284
+ };
285
+
286
+ /**
287
+ * Sets up "drag" event handlers and calls `onDrag` in case dragging
288
+ * started on somewhere other than the thumb without a corresponding "move"
289
+ * event.
290
+ */
291
+ const onDragStart = evt => {
292
+ // Do nothing if component is disabled
293
+ if (props.disabled || props.readOnly) {
294
+ return;
295
+ }
296
+
297
+ // We're going to force focus on one of the handles later on here, b/c we're
298
+ // firing on a mousedown event, we need to call event.preventDefault() to
299
+ // keep the focus from leaving the HTMLElement.
300
+ // @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus#notes
301
+ evt.preventDefault();
302
+
303
+ // TODO: Abstract `elementRef.current?.ownerDocument` to a variable that can
304
+ // be used here and everywhere else in this file.
305
+
306
+ // Add drag stop handlers
307
+ DRAG_STOP_EVENT_TYPES.forEach(element => {
308
+ elementRef.current?.ownerDocument.addEventListener(element, onDragStop);
456
309
  });
457
- _defineProperty(this, "calcLeftPercent", ({
458
- clientX,
459
- value,
460
- range
461
- }) => {
462
- const boundingRect = this.element?.getBoundingClientRect?.();
463
- let width = boundingRect ? boundingRect.right - boundingRect.left : 0;
464
310
 
465
- // Enforce a minimum width of at least 1 for calculations
466
- if (width <= 0) {
467
- width = 1;
311
+ // Add drag handlers
312
+ DRAG_EVENT_TYPES.forEach(element => {
313
+ elementRef.current?.ownerDocument.addEventListener(element, handleDrag);
314
+ });
315
+ const clientX = getClientXFromEvent(evt.nativeEvent);
316
+ let activeHandle;
317
+ if (hasTwoHandles()) {
318
+ if (evt.target == thumbRef.current) {
319
+ activeHandle = HandlePosition.LOWER;
320
+ } else if (evt.target == thumbRefUpper.current) {
321
+ activeHandle = HandlePosition.UPPER;
322
+ } else if (clientX) {
323
+ const distanceToLower = calcDistanceToHandle(HandlePosition.LOWER, clientX);
324
+ const distanceToUpper = calcDistanceToHandle(HandlePosition.UPPER, clientX);
325
+ if (distanceToLower <= distanceToUpper) {
326
+ activeHandle = HandlePosition.LOWER;
327
+ } else {
328
+ activeHandle = HandlePosition.UPPER;
329
+ }
468
330
  }
331
+ }
469
332
 
470
- // If a clientX is specified, use it to calculate the leftPercent. If not,
471
- // use the provided value to calculate it instead.
472
- if (clientX) {
473
- const leftOffset = this.state.isRtl ? (boundingRect?.right ?? 0) - clientX : clientX - (boundingRect?.left ?? 0);
474
- return leftOffset / width;
475
- } else if (value !== null && typeof value !== 'undefined' && range) {
476
- // Prevent NaN calculation if the range is 0.
477
- return range === 0 ? 0 : (value - this.props.min) / range;
333
+ // Force focus to the appropriate handle.
334
+ const focusOptions = {
335
+ preventScroll: true
336
+ };
337
+ if (hasTwoHandles()) {
338
+ if (thumbRef.current && activeHandle === HandlePosition.LOWER) {
339
+ thumbRef.current.focus(focusOptions);
340
+ } else if (thumbRefUpper.current && activeHandle === HandlePosition.UPPER) {
341
+ thumbRefUpper.current.focus(focusOptions);
478
342
  }
479
- // We should never end up in this scenario, but in case we do, and to
480
- // re-assure Typescript, return 0.
481
- return 0;
343
+ } else if (thumbRef.current) {
344
+ thumbRef.current.focus(focusOptions);
345
+ }
346
+ setState({
347
+ activeHandle
482
348
  });
483
- /**
484
- * Calculates the discrete value (snapped to the nearest step) along
485
- * with the corresponding handle position percentage.
486
- */
487
- _defineProperty(this, "calcDiscreteValueAndPercent", ({
488
- leftPercent
489
- }) => {
490
- const {
491
- step = 1,
492
- min,
493
- max
494
- } = this.props;
495
- const numSteps = Math.floor((max - min) / step) + ((max - min) % step === 0 ? 1 : 2);
496
- /** Index of the step that corresponds to `leftPercent`. */
497
- const stepIndex = Math.round(leftPercent * (numSteps - 1));
498
- const discreteValue = stepIndex === numSteps - 1 ? max : min + step * stepIndex;
499
- /** Percentage corresponding to the step index. */
500
- const discretePercent = stepIndex / (numSteps - 1);
501
- return {
502
- discreteValue,
503
- discretePercent
504
- };
349
+
350
+ // Perform first recalculation since we probably didn't click exactly in the
351
+ // middle of the thumb.
352
+ onDrag(evt.nativeEvent, activeHandle);
353
+ };
354
+
355
+ /**
356
+ * Removes "drag" and "drag stop" event handlers and calls sets the flag
357
+ * indicating that the `onRelease` callback should be called.
358
+ */
359
+ const onDragStop = () => {
360
+ // Do nothing if component is disabled
361
+ if (props.disabled || props.readOnly) {
362
+ return;
363
+ }
364
+
365
+ // TODO: Rename parameters in `DRAG_*` loops to `type`.
366
+ // Remove drag stop handlers
367
+ DRAG_STOP_EVENT_TYPES.forEach(element => {
368
+ elementRef.current?.ownerDocument.removeEventListener(element, onDragStop);
505
369
  });
506
- /**
507
- * Calculates the slider's value and handle position based on either a
508
- * mouse/touch event or an explicit value.
509
- */
510
- _defineProperty(this, "calcValue", ({
511
- clientX,
370
+
371
+ // Remove drag handlers
372
+ DRAG_EVENT_TYPES.forEach(element => {
373
+ elementRef.current?.ownerDocument.removeEventListener(element, handleDrag);
374
+ });
375
+
376
+ // Set needsOnRelease flag so event fires on next update.
377
+ setState({
378
+ needsOnRelease: true,
379
+ isValid: true,
380
+ isValidUpper: true
381
+ });
382
+ };
383
+
384
+ // TODO: Rename this reference.
385
+ /**
386
+ * Handles a "drag" event by recalculating the value/thumb and setting state
387
+ * accordingly.
388
+ *
389
+ * @param evt The event.
390
+ * @param activeHandle The first drag event call, we may have an explicit
391
+ * activeHandle value, which is to be used before state is used.
392
+ */
393
+ const _onDragRef = useRef(null);
394
+ _onDragRef.current = (evt, activeHandle) => {
395
+ activeHandle = activeHandle ?? stateRef.current.activeHandle;
396
+ // Do nothing if component is disabled, or we have no event.
397
+ if (propsRef.current.disabled || propsRef.current.readOnly || !evt) {
398
+ return;
399
+ }
400
+ const clientX = getClientXFromEvent(evt);
401
+ const {
512
402
  value,
513
- useRawValue
514
- }) => {
515
- const range = this.props.max - this.props.min;
516
- const leftPercentRaw = this.calcLeftPercent({
517
- clientX,
518
- value,
519
- range
403
+ left
404
+ } = calcValue({
405
+ clientX,
406
+ value: stateRef.current.value
407
+ });
408
+ // If we're set to two handles, negotiate which drag handle is closest to
409
+ // the users' interaction.
410
+ if (hasTwoHandles() && activeHandle) {
411
+ setValueLeftForHandle(activeHandle, {
412
+ value: nearestStepValue(value),
413
+ left
520
414
  });
521
- /** `leftPercentRaw` clamped between 0 and 1. */
522
- const leftPercent = clamp(leftPercentRaw, 0, 1);
523
- if (useRawValue) {
524
- return {
525
- value,
526
- left: leftPercent * 100
527
- };
528
- }
415
+ } else {
416
+ setState({
417
+ value: nearestStepValue(value),
418
+ left,
419
+ isValid: true
420
+ });
421
+ }
422
+ // TODO: Investigate if it would be better to not call `setState`
423
+ // back-to-back here and in other places.
424
+ setState({
425
+ correctedValue: null,
426
+ correctedPosition: null
427
+ });
428
+ };
429
+
430
+ /**
431
+ * Throttles calls to `_onDrag` by limiting events to being processed at
432
+ * most once every `EVENT_THROTTLE` milliseconds.
433
+ */
434
+ const onDrag = useMemo(() => throttle((evt, activeHandle) => {
435
+ _onDragRef.current?.(evt, activeHandle);
436
+ }, EVENT_THROTTLE, {
437
+ leading: true,
438
+ trailing: false
439
+ }), []);
529
440
 
530
- // Use the discrete value and percentage for snapping.
441
+ /**
442
+ * Handles a `keydown` event by recalculating the value/thumb and setting
443
+ * state accordingly.
444
+ */
445
+ const onKeyDown = evt => {
446
+ // Do nothing if component is disabled, or we don't have a valid event
447
+ if (props.disabled || props.readOnly) {
448
+ return;
449
+ }
450
+ const {
451
+ step = 1,
452
+ stepMultiplier = 4
453
+ } = props;
454
+ let delta = 0;
455
+ if (matches(evt, [ArrowDown, ArrowLeft])) {
456
+ delta = -step;
457
+ } else if (matches(evt, [ArrowUp, ArrowRight])) {
458
+ delta = step;
459
+ } else {
460
+ // Ignore keys we don't want to handle
461
+ return;
462
+ }
463
+
464
+ // If shift was held, account for the stepMultiplier
465
+ if (evt.shiftKey) {
466
+ delta *= stepMultiplier;
467
+ }
468
+ if (hasTwoHandles() && state.activeHandle) {
469
+ const currentValue = state.activeHandle === HandlePosition.LOWER ? state.value : state.valueUpper;
531
470
  const {
532
- discreteValue,
533
- discretePercent
534
- } = this.calcDiscreteValueAndPercent({
535
- leftPercent
471
+ value,
472
+ left
473
+ } = calcValue({
474
+ value: calcValueForDelta(currentValue ?? props.min, delta, props.step)
536
475
  });
537
- return {
538
- value: discreteValue,
539
- left: discretePercent * 100
540
- };
541
- });
542
- _defineProperty(this, "calcDistanceToHandle", (handle, clientX) => {
543
- const handleBoundingRect = this.getHandleBoundingRect(handle);
544
- // x co-ordinate of the midpoint.
545
- const handleX = handleBoundingRect.left + handleBoundingRect.width / 2;
546
- return Math.abs(handleX - clientX);
547
- });
548
- /**
549
- * Calculates a new slider value based on the current value, a change delta,
550
- * and a step.
551
- *
552
- * @param currentValue - The starting value from which the slider is moving.
553
- * @param delta - The amount to adjust the current value by.
554
- * @param step - The step. Defaults to `1`.
555
- * @returns The new slider value, rounded to the same number of decimal places
556
- * as the step.
557
- */
558
- _defineProperty(this, "calcValueForDelta", (currentValue, delta, step = 1) => {
559
- const base = delta > 0 ? Math.floor(currentValue / step) * step : currentValue;
560
- const newValue = base + delta;
561
- const decimals = (step.toString().split('.')[1] || '').length;
562
- return Number(newValue.toFixed(decimals));
563
- });
564
- /**
565
- * Sets state relevant to the given handle position.
566
- *
567
- * Guards against setting either lower or upper values beyond its counterpart.
568
- */
569
- _defineProperty(this, "setValueLeftForHandle", (handle, {
570
- value: newValue,
571
- left: newLeft
572
- }) => {
476
+ setValueLeftForHandle(state.activeHandle, {
477
+ value: nearestStepValue(value),
478
+ left
479
+ });
480
+ } else {
573
481
  const {
574
482
  value,
575
- valueUpper,
483
+ left
484
+ } = calcValue({
485
+ // Ensures custom value from `<input>` won't cause skipping next stepping
486
+ // point with right arrow key, e.g. Typing 51 in `<input>`, moving focus
487
+ // onto the thumb and the hitting right arrow key should yield 52 instead
488
+ // of 54.
489
+ value: calcValueForDelta(state.value, delta, props.step)
490
+ });
491
+ setState({
492
+ value: nearestStepValue(value),
576
493
  left,
577
- leftUpper
578
- } = this.state;
579
- if (handle === HandlePosition.LOWER) {
580
- // Don't allow higher than the upper handle.
581
- this.setState({
582
- value: valueUpper && newValue > valueUpper ? valueUpper : newValue,
583
- left: valueUpper && newValue > valueUpper ? leftUpper : newLeft,
584
- isValid: true
494
+ isValid: true
495
+ });
496
+ }
497
+ setState({
498
+ correctedValue: null,
499
+ correctedPosition: null
500
+ });
501
+ };
502
+
503
+ /**
504
+ * Provides the two-way binding for the input field of the Slider. It also
505
+ * Handles a change to the input field by recalculating the value/thumb and
506
+ * setting state accordingly.
507
+ */
508
+ const onChangeInput = evt => {
509
+ // Do nothing if component is disabled
510
+ if (props.disabled || props.readOnly) {
511
+ return;
512
+ }
513
+
514
+ // Do nothing if we have no valid event, target, or value
515
+ if (!evt || !('target' in evt) || typeof evt.target.value !== 'string') {
516
+ return;
517
+ }
518
+
519
+ // Avoid calling calcValue for invalid numbers, but still update the state.
520
+ const activeHandle = evt.target.dataset.handlePosition ?? HandlePosition.LOWER;
521
+ const targetValue = Number.parseFloat(evt.target.value);
522
+ if (hasTwoHandles()) {
523
+ if (isNaN(targetValue)) {
524
+ setValueForHandle(activeHandle, evt.target.value);
525
+ } else if (isValidValueForPosition({
526
+ handle: activeHandle,
527
+ value: targetValue,
528
+ min: props.min,
529
+ max: props.max
530
+ })) {
531
+ processNewInputValue(evt.target);
532
+ } else {
533
+ setValueForHandle(activeHandle, targetValue);
534
+ }
535
+ } else {
536
+ if (isNaN(targetValue)) {
537
+ // TODO: Address this error
538
+ //
539
+ // @ts-expect-error - Passing a string to something that expects a
540
+ // number.
541
+ setState({
542
+ value: evt.target.value
585
543
  });
544
+ } else if (isValidValue({
545
+ value: targetValue,
546
+ min: props.min,
547
+ max: props.max
548
+ })) {
549
+ processNewInputValue(evt.target);
586
550
  } else {
587
- this.setState({
588
- valueUpper: value && newValue < value ? value : newValue,
589
- leftUpper: value && newValue < value ? left : newLeft,
590
- isValidUpper: true
551
+ setState({
552
+ value: targetValue
591
553
  });
592
554
  }
555
+ }
556
+ };
557
+
558
+ /**
559
+ * Checks for validity of input value after clicking out of the input. It also
560
+ * Handles state change to isValid state.
561
+ */
562
+ const onBlurInput = evt => {
563
+ // Do nothing if we have no valid event, target, or value
564
+ if (!evt || !('target' in evt) || typeof evt.target.value !== 'string') {
565
+ return;
566
+ }
567
+ const {
568
+ value: targetValue
569
+ } = evt.target;
570
+ processNewInputValue(evt.target);
571
+ props.onBlur?.({
572
+ value: targetValue,
573
+ handlePosition: evt.target.dataset.handlePosition
593
574
  });
594
- _defineProperty(this, "setValueForHandle", (handle, value) => {
595
- if (handle === HandlePosition.LOWER) {
596
- this.setState({
597
- value,
598
- isValid: true
575
+ };
576
+ const onInputKeyDown = evt => {
577
+ // Do nothing if component is disabled, or we don't have a valid event.
578
+ if (props.disabled || props.readOnly || !(evt.target instanceof HTMLInputElement)) {
579
+ return;
580
+ }
581
+
582
+ // Do nothing if we have no valid event, target, or value.
583
+ if (!evt || !('target' in evt) || typeof evt.target.value !== 'string') {
584
+ return;
585
+ }
586
+ if (matches(evt, [Enter])) {
587
+ processNewInputValue(evt.target);
588
+ }
589
+ };
590
+ const processNewInputValue = input => {
591
+ setState({
592
+ correctedValue: null,
593
+ correctedPosition: null
594
+ });
595
+ const targetValue = Number.parseFloat(input.value);
596
+ const validity = !isNaN(targetValue);
597
+
598
+ // When there are two handles, we'll also have the data-handle-position
599
+ // attribute to consider the other value before settling on the validity to
600
+ // set.
601
+ const handlePosition = input.dataset.handlePosition;
602
+ if (handlePosition === HandlePosition.LOWER) {
603
+ setState({
604
+ isValid: validity
605
+ });
606
+ } else if (handlePosition === HandlePosition.UPPER) {
607
+ setState({
608
+ isValidUpper: validity
609
+ });
610
+ }
611
+ setState({
612
+ isValid: validity
613
+ });
614
+ if (validity) {
615
+ const adjustedValue = handlePosition ? getAdjustedValueForPosition({
616
+ handle: handlePosition,
617
+ value: targetValue,
618
+ min: props.min,
619
+ max: props.max
620
+ }) : getAdjustedValue({
621
+ value: targetValue,
622
+ min: props.min,
623
+ max: props.max
624
+ });
625
+ if (adjustedValue !== targetValue) {
626
+ setState({
627
+ correctedValue: targetValue.toString(),
628
+ correctedPosition: handlePosition ?? null
599
629
  });
600
630
  } else {
601
- this.setState({
602
- valueUpper: value,
603
- isValidUpper: true
631
+ setState({
632
+ correctedValue: null,
633
+ correctedPosition: null
604
634
  });
605
635
  }
606
- });
607
- _defineProperty(this, "isValidValueForPosition", ({
608
- handle,
609
- value: newValue,
610
- min,
611
- max
612
- }) => {
613
636
  const {
614
637
  value,
615
- valueUpper
616
- } = this.state;
617
- if (!this.isValidValue({
618
- value: newValue,
619
- min,
620
- max
621
- })) {
622
- return false;
623
- }
624
- if (handle === HandlePosition.LOWER) {
625
- return !valueUpper || newValue <= valueUpper;
626
- } else if (handle === HandlePosition.UPPER) {
627
- return !value || newValue >= value;
638
+ left
639
+ } = calcValue({
640
+ value: adjustedValue,
641
+ useRawValue: true
642
+ });
643
+ if (handlePosition) {
644
+ setValueLeftForHandle(handlePosition, {
645
+ value: nearestStepValue(value),
646
+ left
647
+ });
648
+ } else {
649
+ setState({
650
+ value,
651
+ left
652
+ });
628
653
  }
629
- return false;
630
- });
631
- _defineProperty(this, "isValidValue", ({
632
- value,
633
- min,
634
- max
635
- }) => {
636
- return !(value < min || value > max);
637
- });
638
- _defineProperty(this, "getAdjustedValueForPosition", ({
639
- handle,
640
- value: newValue,
654
+ }
655
+ };
656
+ const calcLeftPercent = ({
657
+ clientX,
658
+ value,
659
+ range
660
+ }) => {
661
+ // TODO: Delete the optional chaining operator after `getBoundingClientRect`.
662
+ const boundingRect = elementRef.current?.getBoundingClientRect?.();
663
+ let width = boundingRect ? boundingRect.right - boundingRect.left : 0;
664
+
665
+ // Enforce a minimum width of at least 1 for calculations
666
+ if (width <= 0) {
667
+ width = 1;
668
+ }
669
+
670
+ // If a clientX is specified, use it to calculate the leftPercent. If not,
671
+ // use the provided value to calculate it instead.
672
+ if (clientX) {
673
+ const leftOffset = state.isRtl ? (boundingRect?.right ?? 0) - clientX : clientX - (boundingRect?.left ?? 0);
674
+ return leftOffset / width;
675
+ } else if (value !== null && typeof value !== 'undefined' && range) {
676
+ // Prevent NaN calculation if the range is 0.
677
+ return range === 0 ? 0 : (value - props.min) / range;
678
+ }
679
+ // We should never end up in this scenario, but in case we do, and to
680
+ // re-assure Typescript, return 0.
681
+ return 0;
682
+ };
683
+
684
+ /**
685
+ * Calculates the discrete value (snapped to the nearest step) along
686
+ * with the corresponding handle position percentage.
687
+ */
688
+ const calcDiscreteValueAndPercent = ({
689
+ leftPercent
690
+ }) => {
691
+ const {
692
+ step = 1,
641
693
  min,
642
694
  max
643
- }) => {
644
- const {
645
- value,
646
- valueUpper
647
- } = this.state;
648
- newValue = this.getAdjustedValue({
649
- value: newValue,
650
- min,
651
- max
652
- });
695
+ } = props;
696
+ const numSteps = Math.floor((max - min) / step) + ((max - min) % step === 0 ? 1 : 2);
697
+ /** Index of the step that corresponds to `leftPercent`. */
698
+ const stepIndex = Math.round(leftPercent * (numSteps - 1));
699
+ const discreteValue = stepIndex === numSteps - 1 ? max : min + step * stepIndex;
700
+ /** Percentage corresponding to the step index. */
701
+ const discretePercent = stepIndex / (numSteps - 1);
702
+ return {
703
+ discreteValue,
704
+ discretePercent
705
+ };
706
+ };
653
707
 
654
- // Next adjust to the opposite handle.
655
- if (handle === HandlePosition.LOWER && valueUpper) {
656
- newValue = newValue > valueUpper ? valueUpper : newValue;
657
- } else if (handle === HandlePosition.UPPER && value) {
658
- newValue = newValue < value ? value : newValue;
659
- }
660
- return newValue;
661
- });
662
- _defineProperty(this, "getAdjustedValue", ({
708
+ /**
709
+ * Calculates the slider's value and handle position based on either a
710
+ * mouse/touch event or an explicit value.
711
+ */
712
+ const calcValue = ({
713
+ clientX,
714
+ value,
715
+ useRawValue
716
+ }) => {
717
+ const range = props.max - props.min;
718
+ const leftPercentRaw = calcLeftPercent({
719
+ clientX,
663
720
  value,
664
- min,
665
- max
666
- }) => {
667
- if (value < min) {
668
- value = min;
669
- }
670
- if (value > max) {
671
- value = max;
672
- }
673
- return value;
721
+ range
674
722
  });
675
- /**
676
- * Get the bounding rect for the requested handles' DOM element.
677
- *
678
- * If the bounding rect is not available, a new, empty DOMRect is returned.
679
- */
680
- _defineProperty(this, "getHandleBoundingRect", handle => {
681
- let boundingRect;
682
- if (handle === HandlePosition.LOWER) {
683
- boundingRect = this.thumbRef.current?.getBoundingClientRect();
684
- } else {
685
- boundingRect = this.thumbRefUpper.current?.getBoundingClientRect();
686
- }
687
- return boundingRect ?? new DOMRect();
723
+ /** `leftPercentRaw` clamped between 0 and 1. */
724
+ const leftPercent = clamp(leftPercentRaw, 0, 1);
725
+ if (useRawValue) {
726
+ return {
727
+ value,
728
+ left: leftPercent * 100
729
+ };
730
+ }
731
+
732
+ // Use the discrete value and percentage for snapping.
733
+ const {
734
+ discreteValue,
735
+ discretePercent
736
+ } = calcDiscreteValueAndPercent({
737
+ leftPercent
688
738
  });
689
- this.thumbRef = /*#__PURE__*/createRef();
690
- this.thumbRefUpper = /*#__PURE__*/createRef();
691
- this.filledTrackRef = /*#__PURE__*/createRef();
692
- }
739
+ return {
740
+ value: discreteValue,
741
+ left: discretePercent * 100
742
+ };
743
+ };
744
+ const calcDistanceToHandle = (handle, clientX) => {
745
+ const handleBoundingRect = getHandleBoundingRect(handle);
746
+ /** x-coordinate of the midpoint. */
747
+ const handleX = handleBoundingRect.left + handleBoundingRect.width / 2;
748
+ return Math.abs(handleX - clientX);
749
+ };
693
750
 
694
751
  /**
695
- * Sets up initial slider position and value in response to component mount.
752
+ * Calculates a new slider value based on the current value, a change delta,
753
+ * and a step.
754
+ *
755
+ * @param currentValue - The starting value from which the slider is moving.
756
+ * @param delta - The amount to adjust the current value by.
757
+ * @param step - The step. Defaults to `1`.
758
+ * @returns The new slider value, rounded to the same number of decimal places
759
+ * as the step.
696
760
  */
697
- componentDidMount() {
698
- if (this.element) {
699
- const isRtl = document?.dir === 'rtl';
700
- if (this.hasTwoHandles()) {
701
- const {
702
- value,
703
- left
704
- } = this.calcValue({
705
- value: this.state.value,
706
- useRawValue: true
707
- });
708
- const {
709
- value: valueUpper,
710
- left: leftUpper
711
- } = this.calcValue({
712
- value: this.state.valueUpper,
713
- useRawValue: true
714
- });
715
- this.setState({
716
- isRtl,
717
- value,
718
- left,
719
- valueUpper,
720
- leftUpper
721
- });
722
- if (this.filledTrackRef.current) {
723
- 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})`;
724
- }
725
- } else {
726
- const {
727
- value,
728
- left
729
- } = this.calcValue({
730
- value: this.state.value,
731
- useRawValue: true
732
- });
733
- this.setState({
734
- isRtl,
735
- value,
736
- left
737
- });
738
- if (this.filledTrackRef.current) {
739
- this.filledTrackRef.current.style.transform = this.state.isRtl ? `translate(100%, -50%) scaleX(-${this.state.left / 100})` : `translate(0%, -50%) scaleX(${this.state.left / 100})`;
740
- }
741
- }
742
- }
743
- }
761
+ const calcValueForDelta = (currentValue, delta, step = 1) => {
762
+ const base = delta > 0 ? Math.floor(currentValue / step) * step : currentValue;
763
+ const newValue = base + delta;
764
+ // TODO: Why is the logical OR needed here?
765
+ const decimals = (step.toString().split('.')[1] || '').length;
766
+ return Number(newValue.toFixed(decimals));
767
+ };
744
768
 
745
769
  /**
746
- * Handles firing of `onChange` and `onRelease` callbacks to parent in
747
- * response to state changes.
770
+ * Sets state relevant to the given handle position.
748
771
  *
749
- * @param {*} prevProps prevProps
750
- * @param {*} prevState The previous Slider state, used to see if callbacks
751
- * should be called.
772
+ * Guards against setting either lower or upper values beyond its counterpart.
752
773
  */
753
- componentDidUpdate(prevProps, prevState) {
754
- // Fire onChange event handler if present, if there's a usable value, and
755
- // if the value is different from the last one
756
- if (this.hasTwoHandles()) {
757
- if (this.filledTrackRef.current) {
758
- 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})`;
759
- }
774
+ const setValueLeftForHandle = (handle, {
775
+ value: newValue,
776
+ left: newLeft
777
+ }) => {
778
+ const {
779
+ value,
780
+ valueUpper,
781
+ left,
782
+ leftUpper
783
+ } = state;
784
+ if (handle === HandlePosition.LOWER) {
785
+ // Don't allow higher than the upper handle.
786
+ setState({
787
+ value: valueUpper && newValue > valueUpper ? valueUpper : newValue,
788
+ left: valueUpper && newValue > valueUpper ? leftUpper : newLeft,
789
+ isValid: true
790
+ });
760
791
  } else {
761
- if (this.filledTrackRef.current) {
762
- this.filledTrackRef.current.style.transform = this.state.isRtl ? `translate(100%, -50%) scaleX(-${this.state.left / 100})` : `translate(0%, -50%) scaleX(${this.state.left / 100})`;
763
- }
764
- }
765
- if ((prevState.value !== this.state.value || prevState.valueUpper !== this.state.valueUpper) && typeof this.props.onChange === 'function') {
766
- this.props.onChange({
767
- value: this.state.value,
768
- valueUpper: this.state.valueUpper
792
+ setState({
793
+ valueUpper: value && newValue < value ? value : newValue,
794
+ leftUpper: value && newValue < value ? left : newLeft,
795
+ isValidUpper: true
769
796
  });
770
797
  }
771
-
772
- // Fire onRelease event handler if present and if needed
773
- if (this.state.needsOnRelease && typeof this.props.onRelease === 'function') {
774
- this.props.onRelease({
775
- value: this.state.value,
776
- valueUpper: this.state.valueUpper
798
+ };
799
+ const setValueForHandle = (handle, value) => {
800
+ if (handle === HandlePosition.LOWER) {
801
+ setState({
802
+ // TODO: Address this error
803
+ //
804
+ // @ts-expect-error - Passing a string to something that expects a
805
+ // number.
806
+ value,
807
+ isValid: true
777
808
  });
778
- // Reset the flag
779
- this.setState({
780
- needsOnRelease: false
809
+ } else {
810
+ setState({
811
+ // TODO: Address this error
812
+ //
813
+ // @ts-expect-error - Passing a string to something that expects a
814
+ // number.
815
+ valueUpper: value,
816
+ isValidUpper: true
781
817
  });
782
818
  }
819
+ };
820
+ const isValidValueForPosition = ({
821
+ handle,
822
+ value: newValue,
823
+ min,
824
+ max
825
+ }) => {
826
+ const {
827
+ value,
828
+ valueUpper
829
+ } = state;
830
+ if (!isValidValue({
831
+ value: newValue,
832
+ min,
833
+ max
834
+ })) {
835
+ return false;
836
+ }
837
+ if (handle === HandlePosition.LOWER) {
838
+ return !valueUpper || newValue <= valueUpper;
839
+ } else if (handle === HandlePosition.UPPER) {
840
+ return !value || newValue >= value;
841
+ }
842
+ return false;
843
+ };
844
+ const isValidValue = ({
845
+ value,
846
+ min,
847
+ max
848
+ }) => {
849
+ return !(value < min || value > max);
850
+ };
851
+ const getAdjustedValueForPosition = ({
852
+ handle,
853
+ value: newValueInput,
854
+ min,
855
+ max
856
+ }) => {
857
+ const {
858
+ value,
859
+ valueUpper
860
+ } = state;
861
+ let newValue = getAdjustedValue({
862
+ value: newValueInput,
863
+ min,
864
+ max
865
+ });
783
866
 
784
- // If value from props does not change, do nothing here.
785
- // Otherwise, do prop -> state sync without "value capping".
786
- if (prevProps.value === this.props.value && prevProps.unstable_valueUpper === this.props.unstable_valueUpper && prevProps.max === this.props.max && prevProps.min === this.props.min) {
787
- return;
867
+ // TODO: Just return the value.
868
+ // Next adjust to the opposite handle.
869
+ if (handle === HandlePosition.LOWER && valueUpper) {
870
+ newValue = newValue > valueUpper ? valueUpper : newValue;
871
+ } else if (handle === HandlePosition.UPPER && value) {
872
+ newValue = newValue < value ? value : newValue;
788
873
  }
789
- this.setState(this.calcValue({
790
- value: this.props.value,
791
- useRawValue: true
792
- }));
793
- if (typeof this.props.unstable_valueUpper !== 'undefined') {
794
- const {
795
- value: valueUpper,
796
- left: leftUpper
797
- } = this.calcValue({
798
- value: this.props.unstable_valueUpper,
799
- useRawValue: true
800
- });
801
- this.setState({
802
- valueUpper,
803
- leftUpper
804
- });
805
- } else {
806
- this.setState({
807
- valueUpper: undefined,
808
- leftUpper: undefined
809
- });
874
+ return newValue;
875
+ };
876
+ const getAdjustedValue = ({
877
+ value,
878
+ min,
879
+ max
880
+ }) => {
881
+ // TODO: Just return the value.
882
+ if (value < min) {
883
+ value = min;
810
884
  }
811
- }
885
+ if (value > max) {
886
+ value = max;
887
+ }
888
+ return value;
889
+ };
812
890
 
813
891
  /**
814
- * Rounds a given value to the nearest step defined by the slider's `step`
815
- * prop.
892
+ * Get the bounding rect for the requested handles' DOM element.
816
893
  *
817
- * @param value - The value to adjust to the nearest step. Defaults to `0`.
818
- * @returns The value rounded to the precision determined by the step.
894
+ * If the bounding rect is not available, a new, empty DOMRect is returned.
819
895
  */
820
- nearestStepValue(value = 0) {
821
- const decimals = (this.props.step?.toString().split('.')[1] || '').length;
822
- return Number(value.toFixed(decimals));
823
- }
824
- getClientXFromEvent(event) {
896
+ const getHandleBoundingRect = handle => {
897
+ let boundingRect;
898
+ if (handle === HandlePosition.LOWER) {
899
+ boundingRect = thumbRef.current?.getBoundingClientRect();
900
+ } else {
901
+ boundingRect = thumbRefUpper.current?.getBoundingClientRect();
902
+ }
903
+ return boundingRect ?? new DOMRect();
904
+ };
905
+ const getClientXFromEvent = event => {
825
906
  let clientX;
826
907
  if ('clientX' in event) {
827
908
  clientX = event.clientX;
@@ -829,17 +910,12 @@ class Slider extends PureComponent {
829
910
  clientX = event.touches[0].clientX;
830
911
  }
831
912
  return clientX;
832
- }
833
- hasTwoHandles() {
834
- return typeof this.state.valueUpper !== 'undefined';
835
- }
836
-
837
- // syncs invalid state and prop
838
- static getDerivedStateFromProps(props, state) {
913
+ };
914
+ useEffect(() => {
839
915
  const {
840
916
  isValid,
841
917
  isValidUpper
842
- } = state;
918
+ } = stateRef.current;
843
919
  const derivedState = {};
844
920
 
845
921
  // Will override state in favor of invalid prop
@@ -850,16 +926,20 @@ class Slider extends PureComponent {
850
926
  if (isValid === false) derivedState.isValid = true;
851
927
  if (isValidUpper === false) derivedState.isValidUpper = true;
852
928
  }
853
- return Object.keys(derivedState).length ? derivedState : null;
854
- }
855
- render() {
856
- var _Fragment, _Fragment2, _Fragment3, _Fragment4;
929
+ if (Object.keys(derivedState).length) {
930
+ setState(derivedState);
931
+ }
932
+ }, [props.invalid]);
933
+
934
+ // TODO: Delete this IIFE. It was added to maintain whitespace and to make it clear
935
+ // what exactly has changed.
936
+ return ((_Fragment, _Fragment2, _Fragment3, _Fragment4) => {
857
937
  const {
858
938
  ariaLabelInput,
859
939
  unstable_ariaLabelInputUpper: ariaLabelInputUpper,
860
940
  className,
861
941
  hideTextInput = false,
862
- id = this.inputId = this.inputId ||
942
+ id = inputIdRef.current = inputIdRef.current ||
863
943
  // TODO:
864
944
  // 1. Why isn't `inputId` just set to this value instead of an empty
865
945
  // string?
@@ -889,8 +969,7 @@ class Slider extends PureComponent {
889
969
  warnText,
890
970
  translateWithId: t = translateWithId,
891
971
  ...other
892
- } = this.props;
893
- const twoHandles = this.hasTwoHandles();
972
+ } = props;
894
973
  delete other.onRelease;
895
974
  delete other.invalid;
896
975
  delete other.unstable_valueUpper;
@@ -902,7 +981,7 @@ class Slider extends PureComponent {
902
981
  correctedValue,
903
982
  correctedPosition,
904
983
  isRtl
905
- } = this.state;
984
+ } = state;
906
985
  const showWarning = !readOnly && warn ||
907
986
  // TODO: https://github.com/carbon-design-system/carbon/issues/18991#issuecomment-2795709637
908
987
  // eslint-disable-next-line valid-typeof , no-constant-binary-expression -- https://github.com/carbon-design-system/carbon/issues/20071
@@ -961,12 +1040,12 @@ class Slider extends PureComponent {
961
1040
  }]);
962
1041
  const lowerThumbWrapperProps = {
963
1042
  style: {
964
- insetInlineStart: `${this.state.left}%`
1043
+ insetInlineStart: `${state.left}%`
965
1044
  }
966
1045
  };
967
1046
  const upperThumbWrapperProps = {
968
1047
  style: {
969
- insetInlineStart: `${this.state.leftUpper}%`
1048
+ insetInlineStart: `${state.leftUpper}%`
970
1049
  }
971
1050
  };
972
1051
  return /*#__PURE__*/React.createElement("div", {
@@ -992,10 +1071,10 @@ class Slider extends PureComponent {
992
1071
  min: min,
993
1072
  max: max,
994
1073
  step: step,
995
- onChange: this.onChange,
996
- onBlur: this.onBlur,
997
- onKeyUp: this.props.onInputKeyUp,
998
- onKeyDown: this.onInputKeyDown,
1074
+ onChange: onChangeInput,
1075
+ onBlur: onBlurInput,
1076
+ onKeyUp: props.onInputKeyUp,
1077
+ onKeyDown: onInputKeyDown,
999
1078
  "data-invalid": !isValid && !readOnly ? true : null,
1000
1079
  "data-handle-position": HandlePosition.LOWER,
1001
1080
  "aria-invalid": !isValid && !readOnly ? true : undefined,
@@ -1009,11 +1088,11 @@ class Slider extends PureComponent {
1009
1088
  }, formatLabel(min, minLabel)), /*#__PURE__*/React.createElement("div", _extends({
1010
1089
  className: sliderClasses,
1011
1090
  ref: node => {
1012
- this.element = node;
1091
+ elementRef.current = node;
1013
1092
  },
1014
- onMouseDown: this.onDragStart,
1015
- onTouchStart: this.onDragStart,
1016
- onKeyDown: this.onKeyDown,
1093
+ onMouseDown: onDragStart,
1094
+ onTouchStart: onDragStart,
1095
+ onKeyDown: onKeyDown,
1017
1096
  role: "presentation",
1018
1097
  tabIndex: -1,
1019
1098
  "data-invalid": (twoHandles ? !isValid || !isValidUpper : !isValid) && !readOnly ? true : null
@@ -1033,8 +1112,8 @@ class Slider extends PureComponent {
1033
1112
  "aria-valuenow": value,
1034
1113
  "aria-labelledby": twoHandles ? undefined : labelId,
1035
1114
  "aria-label": twoHandles ? ariaLabelInput : undefined,
1036
- ref: this.thumbRef,
1037
- onFocus: () => this.setState({
1115
+ ref: thumbRef,
1116
+ onFocus: () => setState({
1038
1117
  activeHandle: HandlePosition.LOWER
1039
1118
  })
1040
1119
  }, twoHandles && !isRtl ? _Fragment || (_Fragment = /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(LowerHandle, {
@@ -1058,8 +1137,8 @@ class Slider extends PureComponent {
1058
1137
  "aria-valuemin": value,
1059
1138
  "aria-valuenow": valueUpper,
1060
1139
  "aria-label": ariaLabelInputUpper,
1061
- ref: this.thumbRefUpper,
1062
- onFocus: () => this.setState({
1140
+ ref: thumbRefUpper,
1141
+ onFocus: () => setState({
1063
1142
  activeHandle: HandlePosition.UPPER
1064
1143
  })
1065
1144
  }, twoHandles && !isRtl ? _Fragment3 || (_Fragment3 = /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(UpperHandle, {
@@ -1073,11 +1152,11 @@ class Slider extends PureComponent {
1073
1152
  }))) : undefined)) : null, /*#__PURE__*/React.createElement("div", {
1074
1153
  className: `${prefix}--slider__track`,
1075
1154
  ref: node => {
1076
- this.track = node;
1155
+ trackRef.current = node;
1077
1156
  }
1078
1157
  }), /*#__PURE__*/React.createElement("div", {
1079
1158
  className: `${prefix}--slider__filled-track`,
1080
- ref: this.filledTrackRef
1159
+ ref: filledTrackRef
1081
1160
  })), /*#__PURE__*/React.createElement(Text, {
1082
1161
  className: `${prefix}--slider__range-label`
1083
1162
  }, formatLabel(max, maxLabel)), /*#__PURE__*/React.createElement("div", {
@@ -1095,10 +1174,10 @@ class Slider extends PureComponent {
1095
1174
  min: min,
1096
1175
  max: max,
1097
1176
  step: step,
1098
- onChange: this.onChange,
1099
- onBlur: this.onBlur,
1100
- onKeyDown: this.onInputKeyDown,
1101
- onKeyUp: this.props.onInputKeyUp,
1177
+ onChange: onChangeInput,
1178
+ onBlur: onBlurInput,
1179
+ onKeyDown: onInputKeyDown,
1180
+ onKeyUp: props.onInputKeyUp,
1102
1181
  "data-invalid": (twoHandles ? !isValidUpper : !isValid) && !readOnly ? true : null,
1103
1182
  "data-handle-position": twoHandles ? HandlePosition.UPPER : null,
1104
1183
  "aria-invalid": (twoHandles ? !isValidUpper : !isValid) && !readOnly ? true : undefined,
@@ -1121,10 +1200,8 @@ class Slider extends PureComponent {
1121
1200
  correctedValue
1122
1201
  })));
1123
1202
  });
1124
- }
1125
- }
1126
- _defineProperty(Slider, "contextType", FeatureFlagContext);
1127
- _defineProperty(Slider, "translationIds", Object.values(translationIds));
1203
+ })();
1204
+ };
1128
1205
  Slider.propTypes = {
1129
1206
  /**
1130
1207
  * The `ariaLabel` for the `<input>`.
@@ -1265,4 +1342,4 @@ Slider.propTypes = {
1265
1342
  warnText: PropTypes.node
1266
1343
  };
1267
1344
 
1268
- export { Slider as default };
1345
+ export { Slider };