@skyscanner/backpack-web 41.5.0 → 41.6.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.
@@ -15,13 +15,22 @@
15
15
  * See the License for the specific language governing permissions and
16
16
  * limitations under the License.
17
17
  */
18
- import { forwardRef, useRef, useEffect } from 'react';
18
+ import { forwardRef, useRef, useEffect, useCallback } from 'react';
19
19
  import { useComposedRefs } from '@radix-ui/react-compose-refs';
20
20
  import * as Slider from '@radix-ui/react-slider';
21
21
  import { cssModules, isRTL, setNativeValue } from "../../bpk-react-utils";
22
22
  import STYLES from "./BpkSlider.module.css";
23
23
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
24
24
  const getClassName = cssModules(STYLES);
25
+
26
+ // Pure utility function to normalize slider values for callbacks
27
+ // Returns single number for single-thumb slider, array for range slider
28
+ const processSliderValues = (sliderValues, callback) => {
29
+ const val = sliderValues.length === 1 ? sliderValues[0] : sliderValues;
30
+ if (callback) {
31
+ callback(val);
32
+ }
33
+ };
25
34
  const BpkSlider = ({
26
35
  ariaLabel,
27
36
  ariaValuetext,
@@ -37,19 +46,72 @@ const BpkSlider = ({
37
46
  }) => {
38
47
  const invert = isRTL();
39
48
  const currentValue = Array.isArray(value) ? value : [value];
40
- const processSliderValues = (sliderValues, callback) => {
41
- const val = sliderValues.length === 1 ? sliderValues[0] : sliderValues;
42
- if (callback) {
43
- callback(val);
49
+
50
+ // Track the latest value and callback for the Chrome workaround
51
+ // Using refs to avoid re-registering document event listeners when these change
52
+ const latestValueRef = useRef(currentValue);
53
+ const onAfterChangeRef = useRef(onAfterChange);
54
+ const isDraggingRef = useRef(false);
55
+ const hasCommittedRef = useRef(false);
56
+ // Store cleanup function to prevent memory leaks if component unmounts during drag
57
+ const cleanupRef = useRef(null);
58
+
59
+ // Keep refs updated
60
+ useEffect(() => {
61
+ latestValueRef.current = currentValue;
62
+ }, [currentValue]);
63
+ useEffect(() => {
64
+ onAfterChangeRef.current = onAfterChange;
65
+ }, [onAfterChange]);
66
+
67
+ // Cleanup on unmount to prevent memory leaks
68
+ useEffect(() => () => {
69
+ if (cleanupRef.current) {
70
+ cleanupRef.current();
44
71
  }
45
- };
72
+ }, []);
46
73
  const thumbRefs = [useRef(null), useRef(null)];
47
- const handleOnChange = sliderValues => {
74
+ const handleOnChange = useCallback(sliderValues => {
75
+ latestValueRef.current = sliderValues;
48
76
  processSliderValues(sliderValues, onChange);
49
- };
50
- const handleOnAfterChange = sliderValues => {
77
+ }, [onChange]);
78
+ const handleOnAfterChange = useCallback(sliderValues => {
79
+ hasCommittedRef.current = true;
80
+ isDraggingRef.current = false;
51
81
  processSliderValues(sliderValues, onAfterChange);
52
- };
82
+ }, [onAfterChange]);
83
+
84
+ // Chrome workaround: Listen for pointerup/pointercancel on document as safety net
85
+ // This ensures onAfterChange fires even when Radix's onValueCommit doesn't
86
+ // See: https://github.com/radix-ui/primitives/issues/1760
87
+ const handlePointerDown = useCallback(() => {
88
+ // Clean up any previous listener still hanging around (edge case)
89
+ if (cleanupRef.current) {
90
+ cleanupRef.current();
91
+ }
92
+ isDraggingRef.current = true;
93
+ hasCommittedRef.current = false;
94
+ const handlePointerEnd = () => {
95
+ document.removeEventListener('pointerup', handlePointerEnd);
96
+ document.removeEventListener('pointercancel', handlePointerEnd);
97
+ cleanupRef.current = null;
98
+
99
+ // Use requestAnimationFrame to defer the check, allowing Radix's onValueCommit
100
+ // to fire first and set hasCommittedRef.current = true. This prevents the race
101
+ // condition where both handlers could fire onAfterChange for the same interaction.
102
+ requestAnimationFrame(() => {
103
+ if (isDraggingRef.current && !hasCommittedRef.current && onAfterChangeRef.current) {
104
+ // Radix didn't fire onValueCommit, so we fire it manually
105
+ processSliderValues(latestValueRef.current, onAfterChangeRef.current);
106
+ }
107
+ isDraggingRef.current = false;
108
+ hasCommittedRef.current = false;
109
+ });
110
+ };
111
+ cleanupRef.current = handlePointerEnd;
112
+ document.addEventListener('pointerup', handlePointerEnd);
113
+ document.addEventListener('pointercancel', handlePointerEnd);
114
+ }, []);
53
115
  return /*#__PURE__*/_jsxs(Slider.Root, {
54
116
  className: getClassName('bpk-slider'),
55
117
  defaultValue: currentValue,
@@ -58,6 +120,7 @@ const BpkSlider = ({
58
120
  step: step || 1,
59
121
  onValueChange: handleOnChange,
60
122
  onValueCommit: handleOnAfterChange,
123
+ onPointerDown: handlePointerDown,
61
124
  inverted: invert,
62
125
  minStepsBetweenThumbs: minDistance,
63
126
  ...rest,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skyscanner/backpack-web",
3
- "version": "41.5.0",
3
+ "version": "41.6.0",
4
4
  "description": "Backpack Design System web library",
5
5
  "repository": {
6
6
  "type": "git",