@skyscanner/backpack-web 41.4.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,4 +15,4 @@
15
15
  * See the License for the specific language governing permissions and
16
16
  * limitations under the License.
17
17
  */
18
- .bpk-bubble{position:relative;display:inline-flex;width:auto;height:1.25rem;padding:0 .5rem;flex-direction:column;justify-content:center;border-radius:.25rem;background-color:#e70866;font-family:"Larken","Noto Sans Arabic","Noto Sans Hebrew","Noto Serif","Noto Serif Devanagari","Noto Serif Thai","Noto Sans SC","Noto Sans TC","Noto Sans JP","Noto Sans KR",sans-serif;text-align:center;white-space:nowrap;font-size:.75rem;line-height:1rem;font-weight:400}.bpk-bubble__arrow{position:absolute;bottom:-5px;left:50%;transform:translateX(-50%);color:#e70866}
18
+ .bpk-bubble{position:relative;display:inline-flex;width:auto;height:1.25rem;padding:0 .5rem;flex-direction:column;justify-content:center;border-radius:.25rem;background-color:#e70866;font-family:var(--bpk-larken-font-stack, "Larken", "Noto Sans Arabic", "Noto Serif Hebrew", "Noto Serif", "Noto Serif Devanagari", "Noto Serif Thai", "Noto Serif SC", "Noto Serif TC", "Noto Serif JP", "Noto Serif KR", sans-serif);text-align:center;white-space:nowrap;font-size:.75rem;line-height:1rem;font-weight:400}.bpk-bubble__arrow{position:absolute;bottom:-5px;left:50%;transform:translateX(-50%);color:#e70866}
@@ -15,4 +15,4 @@
15
15
  * See the License for the specific language governing permissions and
16
16
  * limitations under the License.
17
17
  */
18
- .bpk-card-list-row-rail__row,.bpk-card-list-row-rail__rail{display:flex;overflow-x:hidden;box-sizing:border-box;gap:.25rem;margin-inline:-0.5rem;scroll-snap-stop:always;scroll-snap-type:x mandatory;scrollbar-width:none}@media(max-width: 32rem){.bpk-card-list-row-rail__row,.bpk-card-list-row-rail__rail{overflow-x:scroll}}.bpk-card-list-row-rail__row::-webkit-scrollbar,.bpk-card-list-row-rail__rail::-webkit-scrollbar{display:none}.bpk-card-list-row-rail__row__card,.bpk-card-list-row-rail__rail__card{position:relative;padding:0 .5rem;flex:0 0 calc((100% - .5rem*(var(--initially-shown-cards, 3) - 1))/var(--initially-shown-cards, 3));overflow:visible;box-sizing:border-box;scroll-snap-align:start}@media(max-width: 32rem){.bpk-card-list-row-rail__row__card,.bpk-card-list-row-rail__rail__card{flex-basis:calc((100% - .5rem*(var(--initially-shown-cards, 3) - 1))/max(1,var(--initially-shown-cards, 3) - .8))}}.bpk-card-list-row-rail__row__card{flex-basis:calc((100% - .25rem*(var(--initially-shown-cards, 3) - 1))/var(--initially-shown-cards, 3))}.bpk-card-list-row-rail__rail{-webkit-overflow-scrolling:touch}
18
+ .bpk-card-list-row-rail__row,.bpk-card-list-row-rail__rail{--spacing-offset: 0.5rem;--carousel-card-gap: 1.25rem;display:flex;overflow-x:hidden;box-sizing:border-box;gap:var(--carousel-card-gap);margin-block:-1.5rem;margin-inline:-0.5rem;padding-block:1.5rem;padding-inline:.5rem;scroll-snap-stop:always;scroll-snap-type:x mandatory;scrollbar-width:none}@media(max-width: 32rem){.bpk-card-list-row-rail__row,.bpk-card-list-row-rail__rail{--spacing-offset: 1rem;--carousel-card-gap: 1rem;overflow-x:scroll}}.bpk-card-list-row-rail__row::-webkit-scrollbar,.bpk-card-list-row-rail__rail::-webkit-scrollbar{display:none}.bpk-card-list-row-rail__row__card,.bpk-card-list-row-rail__rail__card{position:relative;flex:0 0 calc((100% - (var(--carousel-card-gap)*(var(--initially-shown-cards, 3) - 1) + var(--spacing-offset)*2/var(--initially-shown-cards, 3)))/var(--initially-shown-cards, 3));overflow:visible;box-sizing:border-box;scroll-margin-inline:var(--spacing-offset);scroll-snap-align:start}@media(max-width: 32rem){.bpk-card-list-row-rail__row__card,.bpk-card-list-row-rail__rail__card{flex:0 0 calc((100% - var(--carousel-card-gap)*(var(--initially-shown-cards, 3) - 1))/max(1,var(--initially-shown-cards, 3) - .8))}}.bpk-card-list-row-rail__rail{-webkit-overflow-scrolling:touch}@media(max-width: 32rem){.bpk-card-list-row-rail__rail{margin-inline:calc(-1*var(--spacing-offset));padding-inline:var(--spacing-offset)}}
@@ -19,7 +19,6 @@
19
19
  import { cloneElement } from 'react';
20
20
  // @ts-expect-error Untyped import. See `decisions/imports-ts-suppressions.md`.
21
21
  import BpkFormValidation from "../../bpk-component-form-validation";
22
- // @ts-expect-error Untyped import. See `decisions/imports-ts-suppressions.md`.
23
22
  import BpkLabel from "../../bpk-component-label";
24
23
  import { cssModules } from "../../bpk-react-utils";
25
24
  import STYLES from "./BpkFieldset.module.css";
@@ -0,0 +1,3 @@
1
+ import BpkLabel from './src/BpkLabel';
2
+ export type { Props as BpkLabelProps } from './src/BpkLabel';
3
+ export default BpkLabel;
@@ -14,5 +14,7 @@
14
14
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
15
  * See the License for the specific language governing permissions and
16
16
  * limitations under the License.
17
- */import BpkLabel from "./src/BpkLabel";
17
+ */
18
+
19
+ import BpkLabel from "./src/BpkLabel";
18
20
  export default BpkLabel;
@@ -0,0 +1,11 @@
1
+ import type { ComponentPropsWithoutRef, ReactNode } from 'react';
2
+ export type Props = {
3
+ children: ReactNode;
4
+ className?: string;
5
+ disabled?: boolean;
6
+ valid?: boolean | null;
7
+ required?: boolean;
8
+ white?: boolean;
9
+ } & ComponentPropsWithoutRef<'label'>;
10
+ declare const BpkLabel: ({ children, className, disabled, required, valid, white, ...rest }: Props) => import("react/jsx-runtime").JSX.Element;
11
+ export default BpkLabel;
@@ -14,14 +14,15 @@
14
14
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
15
  * See the License for the specific language governing permissions and
16
16
  * limitations under the License.
17
- */import PropTypes from 'prop-types';
17
+ */
18
+
18
19
  import { cssModules } from "../../bpk-react-utils";
19
20
  import STYLES from "./BpkLabel.module.css";
20
21
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
21
22
  const getClassName = cssModules(STYLES);
22
23
  const BpkLabel = ({
23
24
  children,
24
- className = null,
25
+ className,
25
26
  disabled = false,
26
27
  required = false,
27
28
  valid = null,
@@ -30,25 +31,13 @@ const BpkLabel = ({
30
31
  }) => {
31
32
  const invalid = valid === false;
32
33
  const classNames = getClassName('bpk-label', white && 'bpk-label--white', invalid && 'bpk-label--invalid', disabled && 'bpk-label--disabled', white && disabled && 'bpk-label--disabled--white', className);
33
- return (
34
- /*#__PURE__*/
35
- // $FlowFixMe[cannot-spread-inexact] - inexact rest. See 'decisions/flowfixme.md'.
36
- _jsxs("label", {
37
- className: classNames,
38
- ...rest,
39
- children: [children, !disabled && required && /*#__PURE__*/_jsx("span", {
40
- className: getClassName('bpk-label__asterisk'),
41
- children: "*"
42
- })]
43
- })
44
- );
45
- };
46
- BpkLabel.propTypes = {
47
- children: PropTypes.node.isRequired,
48
- className: PropTypes.string,
49
- disabled: PropTypes.bool,
50
- valid: PropTypes.bool,
51
- required: PropTypes.bool,
52
- white: PropTypes.bool
34
+ return /*#__PURE__*/_jsxs("label", {
35
+ className: classNames,
36
+ ...rest,
37
+ children: [children, !disabled && required && /*#__PURE__*/_jsx("span", {
38
+ className: getClassName('bpk-label__asterisk'),
39
+ children: "*"
40
+ })]
41
+ });
53
42
  };
54
43
  export default BpkLabel;
@@ -82,6 +82,7 @@ const BpkNavigationTabGroup = ({
82
82
  children: tabs.map((tab, index) => {
83
83
  const selected = index === selectedTab;
84
84
  const {
85
+ badgeText,
85
86
  icon,
86
87
  text,
87
88
  ...tabWrapItem
@@ -103,10 +104,10 @@ const BpkNavigationTabGroup = ({
103
104
  textStyle: TEXT_STYLES.label2,
104
105
  children: text
105
106
  })]
106
- }), tab.badgeText && /*#__PURE__*/_jsx("span", {
107
+ }), badgeText && /*#__PURE__*/_jsx("span", {
107
108
  className: getClassName('bpk-navigation-tab-bubble-wrapper'),
108
109
  children: /*#__PURE__*/_jsx(BpkBubble, {
109
- children: tab.badgeText
110
+ children: badgeText
110
111
  })
111
112
  })]
112
113
  })
@@ -21,7 +21,6 @@ import BpkButton, { BUTTON_TYPES } from "../../bpk-component-button";
21
21
  import { withButtonAlignment } from "../../bpk-component-icon";
22
22
  import MinusIcon from "../../bpk-component-icon/sm/minus";
23
23
  import PlusIcon from "../../bpk-component-icon/sm/plus";
24
- // @ts-expect-error Untyped import. See `decisions/imports-ts-suppressions.md`.
25
24
  import BpkLabel from "../../bpk-component-label";
26
25
  import BpkText, { TEXT_STYLES } from "../../bpk-component-text";
27
26
  import { cssModules, setNativeValue } from "../../bpk-react-utils";
@@ -1,3 +1,4 @@
1
- import BpkSegmentedControl, { type Props as BpkSegmentControlProps } from './src/BpkSegmentedControl';
2
- export type { BpkSegmentControlProps };
1
+ import BpkSegmentedControl, { useSegmentedControlPanels, type Props as BpkSegmentControlProps, type TabPanelProps } from './src/BpkSegmentedControl';
2
+ export type { BpkSegmentControlProps, TabPanelProps };
3
+ export { useSegmentedControlPanels };
3
4
  export default BpkSegmentedControl;
@@ -16,5 +16,6 @@
16
16
  * limitations under the License.
17
17
  */
18
18
 
19
- import BpkSegmentedControl from "./src/BpkSegmentedControl";
19
+ import BpkSegmentedControl, { useSegmentedControlPanels } from "./src/BpkSegmentedControl";
20
+ export { useSegmentedControlPanels };
20
21
  export default BpkSegmentedControl;
@@ -6,16 +6,51 @@ export declare const SEGMENT_TYPES: {
6
6
  SurfaceContrast: string;
7
7
  };
8
8
  export type SegmentTypes = (typeof SEGMENT_TYPES)[keyof typeof SEGMENT_TYPES];
9
+ export type TabPanelProps = {
10
+ id: string;
11
+ role: 'tabpanel';
12
+ 'aria-labelledby': string;
13
+ hidden: boolean;
14
+ tabIndex: 0;
15
+ };
16
+ /**
17
+ * Custom hook to manage segmented control and its panels with automatic ID generation.
18
+ * Simplifies the API by eliminating the need to manually track IDs.
19
+ *
20
+ * Note: For optimal performance, memoize the buttonContents array in the parent component
21
+ * to prevent unnecessary recalculations (e.g., using useMemo or defining outside render).
22
+ *
23
+ * @param {Array<string | ReactNode>} buttonContents - Array of button content (strings or ReactNodes)
24
+ * @param {number} selectedIndex - Currently selected tab index
25
+ * @returns {Object} Object with controlProps (for BpkSegmentedControl) and getPanelProps function
26
+ */
27
+ export declare const useSegmentedControlPanels: (buttonContents: string[] | ReactNode[], selectedIndex: number) => {
28
+ controlProps: {
29
+ id: string;
30
+ buttonContents: string[] | ReactNode[];
31
+ selectedIndex: number;
32
+ };
33
+ getPanelProps: (index: number) => TabPanelProps;
34
+ };
9
35
  export type Props = {
10
36
  buttonContents: string[] | ReactNode[];
11
37
  /**
12
38
  * Accessible label for the segmented control group.
13
39
  */
14
40
  label?: string;
41
+ /**
42
+ * ID used to link the segmented control with its tab panels for accessibility.
43
+ * Created using controlProps from useSegmentedControlPanels hook.
44
+ */
45
+ id?: string;
15
46
  type?: SegmentTypes;
47
+ /**
48
+ * Callback fired when a tab is selected. Receives the index of the selected tab.
49
+ */
16
50
  onItemClick: (id: number) => void;
17
51
  selectedIndex: number;
18
52
  shadow?: boolean;
53
+ activationMode?: 'automatic' | 'manual';
19
54
  };
20
- declare const BpkSegmentedControl: ({ buttonContents, label, onItemClick, selectedIndex, shadow, type, }: Props) => import("react/jsx-runtime").JSX.Element;
55
+ declare const BpkSegmentedControl: ({ activationMode, buttonContents, id: providedId, label, onItemClick, selectedIndex, shadow, type, }: Props) => import("react/jsx-runtime").JSX.Element;
21
56
  export default BpkSegmentedControl;
@@ -16,8 +16,8 @@
16
16
  * limitations under the License.
17
17
  */
18
18
 
19
- import { useState } from 'react';
20
- import { cssModules } from "../../bpk-react-utils";
19
+ import { useId, useMemo, useRef, useState } from 'react';
20
+ import { cssModules, isRTL } from "../../bpk-react-utils";
21
21
  import STYLES from "./BpkSegmentedControl.module.css";
22
22
  import { jsx as _jsx } from "react/jsx-runtime";
23
23
  const getClassName = cssModules(STYLES);
@@ -27,40 +27,165 @@ export const SEGMENT_TYPES = {
27
27
  SurfaceDefault: 'surface-default',
28
28
  SurfaceContrast: 'surface-contrast'
29
29
  };
30
+ const getPanelId = (baseId, index) => `${baseId}-panel-${index}`;
31
+ const getTabId = (baseId, index) => `${baseId}-tab-${index}`;
32
+
33
+ /**
34
+ * Helper function to get accessibility props for tab panel elements.
35
+ * Use this to ensure proper ARIA relationships between tabs and their panels.
36
+ *
37
+ * Note: For a simpler API, consider using the useSegmentedControlPanels hook instead,
38
+ * which manages IDs automatically and reduces boilerplate.
39
+ * This function is kept for backward compatibility.
40
+ *
41
+ * @param {string} baseId - The base ID used to generate unique IDs for tabs and panels.
42
+ * @param {number} index - The index of the tab panel.
43
+ * @param {number} selectedIndex - The currently selected tab index.
44
+ * @returns {TabPanelProps} An object containing the necessary props for a tab panel.
45
+ */
46
+ const getTabPanelProps = (baseId, index, selectedIndex) => ({
47
+ id: getPanelId(baseId, index),
48
+ role: 'tabpanel',
49
+ 'aria-labelledby': getTabId(baseId, index),
50
+ hidden: index !== selectedIndex,
51
+ tabIndex: 0
52
+ });
53
+ const getContainerAriaProps = (providedId, label) => {
54
+ const props = {};
55
+ if (providedId) {
56
+ props.role = 'tablist';
57
+ props['aria-orientation'] = 'horizontal';
58
+ }
59
+ if (label) {
60
+ props['aria-label'] = label;
61
+ }
62
+ return props;
63
+ };
64
+ const getButtonAriaProps = (providedId, isSelected, panelId) => {
65
+ if (!providedId) {
66
+ return {};
67
+ }
68
+ return {
69
+ role: 'tab',
70
+ 'aria-selected': isSelected,
71
+ 'aria-controls': panelId
72
+ };
73
+ };
74
+ const getTabIndex = (providedId, isSelected) => {
75
+ if (!providedId) {
76
+ return undefined;
77
+ }
78
+ return isSelected ? 0 : -1;
79
+ };
80
+ const getNextIndex = (current, max) => current === max ? 0 : current + 1;
81
+ const getPrevIndex = (current, max) => current === 0 ? max : current - 1;
82
+
83
+ /**
84
+ * Custom hook to manage segmented control and its panels with automatic ID generation.
85
+ * Simplifies the API by eliminating the need to manually track IDs.
86
+ *
87
+ * Note: For optimal performance, memoize the buttonContents array in the parent component
88
+ * to prevent unnecessary recalculations (e.g., using useMemo or defining outside render).
89
+ *
90
+ * @param {Array<string | ReactNode>} buttonContents - Array of button content (strings or ReactNodes)
91
+ * @param {number} selectedIndex - Currently selected tab index
92
+ * @returns {Object} Object with controlProps (for BpkSegmentedControl) and getPanelProps function
93
+ */
94
+ export const useSegmentedControlPanels = (buttonContents, selectedIndex) => {
95
+ const baseId = useId();
96
+ const controlProps = useMemo(() => ({
97
+ id: baseId,
98
+ buttonContents,
99
+ selectedIndex
100
+ }), [baseId, buttonContents, selectedIndex]);
101
+ const getPanelProps = useMemo(() => index => getTabPanelProps(baseId, index, selectedIndex), [baseId, selectedIndex]);
102
+ return {
103
+ controlProps,
104
+ getPanelProps
105
+ };
106
+ };
30
107
  const BpkSegmentedControl = ({
108
+ activationMode = 'automatic',
31
109
  buttonContents,
110
+ id: providedId,
32
111
  label,
33
112
  onItemClick,
34
113
  selectedIndex,
35
114
  shadow = false,
36
115
  type = SEGMENT_TYPES.CanvasDefault
37
116
  }) => {
117
+ const buttonRefs = useRef([]);
118
+ const panelIds = useMemo(() => Array.from({
119
+ length: buttonContents.length
120
+ }, (_, i) => providedId ? getPanelId(providedId, i) : undefined), [providedId, buttonContents.length]);
121
+
122
+ // TODO: Consider removing internal state - component is controlled via selectedIndex prop.
123
+ // Internal state may cause sync issues if selectedIndex changes externally.
38
124
  const [selectedButton, setSelectedButton] = useState(selectedIndex);
39
- const handleButtonClick = id => {
40
- if (id !== selectedButton) {
41
- setSelectedButton(id);
42
- onItemClick(id);
125
+ const handleButtonClick = index => {
126
+ if (index !== selectedButton) {
127
+ setSelectedButton(index);
128
+ onItemClick(index);
129
+ }
130
+ };
131
+ const handleKeyDown = (event, currentIndex) => {
132
+ const lastIndex = buttonContents.length - 1;
133
+ const rtl = isRTL();
134
+ let newIndex = currentIndex;
135
+ switch (event.key) {
136
+ case 'ArrowRight':
137
+ newIndex = rtl ? getPrevIndex(currentIndex, lastIndex) : getNextIndex(currentIndex, lastIndex);
138
+ break;
139
+ case 'ArrowLeft':
140
+ newIndex = rtl ? getNextIndex(currentIndex, lastIndex) : getPrevIndex(currentIndex, lastIndex);
141
+ break;
142
+ case 'Home':
143
+ newIndex = 0;
144
+ break;
145
+ case 'End':
146
+ newIndex = lastIndex;
147
+ break;
148
+ case ' ':
149
+ case 'Enter':
150
+ event.preventDefault();
151
+ if (activationMode === 'manual') {
152
+ setSelectedButton(currentIndex);
153
+ onItemClick(currentIndex);
154
+ }
155
+ return;
156
+ default:
157
+ return;
158
+ }
159
+ event.preventDefault();
160
+ if (activationMode === 'automatic') {
161
+ setSelectedButton(newIndex);
162
+ onItemClick(newIndex);
43
163
  }
164
+ buttonRefs.current[newIndex]?.focus();
44
165
  };
45
166
  const containerStyling = getClassName('bpk-segmented-control-group', shadow && 'bpk-segmented-control-group-shadow');
46
167
  return /*#__PURE__*/_jsx("div", {
47
168
  className: containerStyling,
48
- role: "group",
49
- ...(label ? {
50
- 'aria-label': label
51
- } : {}),
169
+ ...getContainerAriaProps(providedId, label),
52
170
  children: buttonContents.map((content, index) => {
53
171
  const isSelected = index === selectedButton;
54
172
  const rightOfOption = index === selectedButton + 1;
55
173
  const buttonStyling = getClassName('bpk-segmented-control', `bpk-segmented-control--${type}`, isSelected && `bpk-segmented-control--${type}-selected`, rightOfOption && `bpk-segmented-control--${type}-rightOfOption`, shadow && isSelected && `bpk-segmented-control--${type}-selected-shadow`);
174
+ const buttonTabId = providedId ? getTabId(providedId, index) : undefined;
175
+ const tabIndexValue = getTabIndex(providedId, isSelected);
56
176
  return /*#__PURE__*/_jsx("button", {
57
- id: index.toString(),
177
+ ref: el => {
178
+ buttonRefs.current[index] = el;
179
+ },
180
+ id: buttonTabId,
58
181
  type: "button",
59
182
  onClick: () => handleButtonClick(index),
183
+ onKeyDown: event => handleKeyDown(event, index),
60
184
  className: buttonStyling,
61
- "aria-pressed": !!isSelected,
185
+ tabIndex: tabIndexValue,
186
+ ...getButtonAriaProps(providedId, isSelected, panelIds[index]),
62
187
  children: content
63
- }, `index-${index.toString()}`);
188
+ }, buttonTabId || `${index}`);
64
189
  })
65
190
  });
66
191
  };
@@ -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,
@@ -15,4 +15,4 @@
15
15
  * See the License for the specific language governing permissions and
16
16
  * limitations under the License.
17
17
  */
18
- .bpk-text{margin:0}.bpk-text--xs{font-size:.75rem;line-height:1rem;font-weight:400}.bpk-text--sm{font-size:.875rem;line-height:1.25rem;font-weight:400}.bpk-text--base{font-size:1rem;line-height:1.5rem;font-weight:400}.bpk-text--lg{font-size:1.25rem;line-height:1.75rem;font-weight:400}.bpk-text--xl{font-size:1.5rem;line-height:2rem;font-weight:400}.bpk-text--xxl{font-size:2rem;line-height:2.5rem;font-weight:700}.bpk-text--xxxl{font-size:2.5rem;line-height:3rem;font-weight:700}.bpk-text--xxxxl{font-size:3rem;line-height:3.5rem;font-weight:700;letter-spacing:-0.02em}.bpk-text--xxxxxl{font-size:4rem;line-height:4rem;font-weight:700;letter-spacing:-0.02em}.bpk-text--caption{font-size:.75rem;line-height:1rem;font-weight:400}.bpk-text--footnote{font-size:.875rem;line-height:1.25rem;font-weight:400}.bpk-text--label-1{font-size:1rem;line-height:1.5rem;font-weight:700}.bpk-text--label-2{font-size:.875rem;line-height:1.25rem;font-weight:700}.bpk-text--label-3{font-size:.75rem;line-height:1rem;font-weight:700}.bpk-text--body-default{font-size:1rem;line-height:1.5rem;font-weight:400}.bpk-text--body-longform{font-size:1.25rem;line-height:1.75rem;font-weight:400}.bpk-text--subheading{font-size:1.5rem;line-height:2rem;font-weight:400}.bpk-text--heading-1{font-size:2.5rem;line-height:3rem;font-weight:700}.bpk-text--heading-2{font-size:2rem;line-height:2.5rem;font-weight:700}.bpk-text--heading-3{font-size:1.5rem;line-height:1.75rem;font-weight:700}.bpk-text--heading-4{font-size:1.25rem;line-height:1.5rem;font-weight:700}.bpk-text--heading-5{font-size:1rem;line-height:1.25rem;font-weight:700}.bpk-text--hero-1{font-size:7.5rem;line-height:7.5rem;font-weight:900;letter-spacing:-0.04em}.bpk-text--hero-2{font-size:6rem;line-height:6rem;font-weight:900;letter-spacing:-0.04em}.bpk-text--hero-3{font-size:4.75rem;line-height:4.75rem;font-weight:900;letter-spacing:-0.04em}.bpk-text--hero-4{font-size:4rem;line-height:4rem;font-weight:900;letter-spacing:-0.04em}.bpk-text--hero-5{font-size:3rem;line-height:3rem;font-weight:900;letter-spacing:-0.04em}.bpk-text--hero-6{font-size:2.5rem;line-height:2.5rem;font-weight:900;letter-spacing:-0.04em}.bpk-text--editorial-1{font-family:"Larken","Noto Sans Arabic","Noto Sans Hebrew","Noto Serif","Noto Serif Devanagari","Noto Serif Thai","Noto Sans SC","Noto Sans TC","Noto Sans JP","Noto Sans KR",sans-serif;font-size:3rem;line-height:3.5rem;font-weight:300}.bpk-text--editorial-2{font-family:"Larken","Noto Sans Arabic","Noto Sans Hebrew","Noto Serif","Noto Serif Devanagari","Noto Serif Thai","Noto Sans SC","Noto Sans TC","Noto Sans JP","Noto Sans KR",sans-serif;font-size:2rem;line-height:2.5rem;font-weight:300}.bpk-text--editorial-3{font-family:"Larken","Noto Sans Arabic","Noto Sans Hebrew","Noto Serif","Noto Serif Devanagari","Noto Serif Thai","Noto Sans SC","Noto Sans TC","Noto Sans JP","Noto Sans KR",sans-serif;font-size:1.25rem;line-height:1.75rem;font-weight:400}.bpk-text.bpk-text--text-disabled{color:rgba(0,0,0,.2)}.bpk-text.bpk-text--text-disabled-on-dark{color:hsla(0,0%,100%,.5)}.bpk-text.bpk-text--text-error{color:#e70866}.bpk-text.bpk-text--text-hero{color:#0062e3}.bpk-text.bpk-text--text-link{color:#0062e3}.bpk-text.bpk-text--text-on-dark{color:#fff}.bpk-text.bpk-text--text-on-light{color:#161616}.bpk-text.bpk-text--text-primary{color:#161616}.bpk-text.bpk-text--text-primary-inverse{color:#fff}.bpk-text.bpk-text--text-secondary{color:#626971}.bpk-text.bpk-text--text-success{color:#0c838a}
18
+ .bpk-text{margin:0}.bpk-text--xs{font-size:.75rem;line-height:1rem;font-weight:400}.bpk-text--sm{font-size:.875rem;line-height:1.25rem;font-weight:400}.bpk-text--base{font-size:1rem;line-height:1.5rem;font-weight:400}.bpk-text--lg{font-size:1.25rem;line-height:1.75rem;font-weight:400}.bpk-text--xl{font-size:1.5rem;line-height:2rem;font-weight:400}.bpk-text--xxl{font-size:2rem;line-height:2.5rem;font-weight:700}.bpk-text--xxxl{font-size:2.5rem;line-height:3rem;font-weight:700}.bpk-text--xxxxl{font-size:3rem;line-height:3.5rem;font-weight:700;letter-spacing:-0.02em}.bpk-text--xxxxxl{font-size:4rem;line-height:4rem;font-weight:700;letter-spacing:-0.02em}.bpk-text--caption{font-size:.75rem;line-height:1rem;font-weight:400}.bpk-text--footnote{font-size:.875rem;line-height:1.25rem;font-weight:400}.bpk-text--label-1{font-size:1rem;line-height:1.5rem;font-weight:700}.bpk-text--label-2{font-size:.875rem;line-height:1.25rem;font-weight:700}.bpk-text--label-3{font-size:.75rem;line-height:1rem;font-weight:700}.bpk-text--body-default{font-size:1rem;line-height:1.5rem;font-weight:400}.bpk-text--body-longform{font-size:1.25rem;line-height:1.75rem;font-weight:400}.bpk-text--subheading{font-size:1.5rem;line-height:2rem;font-weight:400}.bpk-text--heading-1{font-size:2.5rem;line-height:3rem;font-weight:700}.bpk-text--heading-2{font-size:2rem;line-height:2.5rem;font-weight:700}.bpk-text--heading-3{font-size:1.5rem;line-height:1.75rem;font-weight:700}.bpk-text--heading-4{font-size:1.25rem;line-height:1.5rem;font-weight:700}.bpk-text--heading-5{font-size:1rem;line-height:1.25rem;font-weight:700}.bpk-text--hero-1{font-size:7.5rem;line-height:7.5rem;font-weight:900;letter-spacing:-0.04em}.bpk-text--hero-2{font-size:6rem;line-height:6rem;font-weight:900;letter-spacing:-0.04em}.bpk-text--hero-3{font-size:4.75rem;line-height:4.75rem;font-weight:900;letter-spacing:-0.04em}.bpk-text--hero-4{font-size:4rem;line-height:4rem;font-weight:900;letter-spacing:-0.04em}.bpk-text--hero-5{font-size:3rem;line-height:3rem;font-weight:900;letter-spacing:-0.04em}.bpk-text--hero-6{font-size:2.5rem;line-height:2.5rem;font-weight:900;letter-spacing:-0.04em}.bpk-text--editorial-1{font-family:var(--bpk-larken-font-stack, "Larken", "Noto Sans Arabic", "Noto Serif Hebrew", "Noto Serif", "Noto Serif Devanagari", "Noto Serif Thai", "Noto Serif SC", "Noto Serif TC", "Noto Serif JP", "Noto Serif KR", sans-serif);font-size:3rem;line-height:3.5rem;font-weight:300}.bpk-text--editorial-2{font-family:var(--bpk-larken-font-stack, "Larken", "Noto Sans Arabic", "Noto Serif Hebrew", "Noto Serif", "Noto Serif Devanagari", "Noto Serif Thai", "Noto Serif SC", "Noto Serif TC", "Noto Serif JP", "Noto Serif KR", sans-serif);font-size:2rem;line-height:2.5rem;font-weight:300}.bpk-text--editorial-3{font-family:var(--bpk-larken-font-stack, "Larken", "Noto Sans Arabic", "Noto Serif Hebrew", "Noto Serif", "Noto Serif Devanagari", "Noto Serif Thai", "Noto Serif SC", "Noto Serif TC", "Noto Serif JP", "Noto Serif KR", sans-serif);font-size:1.25rem;line-height:1.75rem;font-weight:400}.bpk-text.bpk-text--text-disabled{color:rgba(0,0,0,.2)}.bpk-text.bpk-text--text-disabled-on-dark{color:hsla(0,0%,100%,.5)}.bpk-text.bpk-text--text-error{color:#e70866}.bpk-text.bpk-text--text-hero{color:#0062e3}.bpk-text.bpk-text--text-link{color:#0062e3}.bpk-text.bpk-text--text-on-dark{color:#fff}.bpk-text.bpk-text--text-on-light{color:#161616}.bpk-text.bpk-text--text-primary{color:#161616}.bpk-text.bpk-text--text-primary-inverse{color:#fff}.bpk-text.bpk-text--text-secondary{color:#626971}.bpk-text.bpk-text--text-success{color:#0c838a}
@@ -17,8 +17,6 @@
17
17
  */
18
18
 
19
19
  import { Component } from 'react';
20
-
21
- // @ts-expect-error Untyped import. See `decisions/imports-ts-suppressions.md`.
22
20
  import BpkLabel from "../../bpk-component-label";
23
21
  // @ts-expect-error Untyped import. See `decisions/imports-ts-suppressions.md`.
24
22
  import BpkSelect from "../../bpk-component-select";
@@ -506,7 +506,7 @@
506
506
  /// }
507
507
 
508
508
  @mixin bpk-editorial-1 {
509
- font-family: tokens.$bpk-font-family-larken;
509
+ font-family: var(--bpk-larken-font-stack, tokens.$bpk-font-family-larken);
510
510
 
511
511
  @include _bpk-text-factory(
512
512
  tokens.$bpk-font-size-xxxxl,
@@ -523,7 +523,7 @@
523
523
  /// }
524
524
 
525
525
  @mixin bpk-editorial-2 {
526
- font-family: tokens.$bpk-font-family-larken;
526
+ font-family: var(--bpk-larken-font-stack, tokens.$bpk-font-family-larken);
527
527
 
528
528
  @include _bpk-text-factory(
529
529
  tokens.$bpk-font-size-xxl,
@@ -540,7 +540,7 @@
540
540
  /// }
541
541
 
542
542
  @mixin bpk-editorial-3 {
543
- font-family: tokens.$bpk-font-family-larken;
543
+ font-family: var(--bpk-larken-font-stack, tokens.$bpk-font-family-larken);
544
544
 
545
545
  @include _bpk-text-factory(
546
546
  tokens.$bpk-font-size-lg,
@@ -16,4 +16,4 @@
16
16
  * See the License for the specific language governing permissions and
17
17
  * limitations under the License.
18
18
  *
19
- *//*! normalize.css v4.1.1 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;line-height:1.15;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block}audio:not([controls]){display:none;height:0}progress{vertical-align:baseline}template,[hidden]{display:none}a{background-color:transparent;-webkit-text-decoration-skip:objects}a:active,a:hover{outline-width:0}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:inherit}b,strong{font-weight:bolder}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background-color:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-0.25em}sup{top:-0.5em}img{border-style:none}svg:not(:root){overflow:hidden}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}figure{margin:1em 40px}hr{box-sizing:content-box;height:0;overflow:visible}button,input,optgroup,select,textarea{font:inherit;margin:0}optgroup{font-weight:bold}button,input{overflow:visible}button,select{text-transform:none}button,html [type=button],[type=reset],[type=submit]{-webkit-appearance:button}button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{border-style:none;padding:0}button:-moz-focusring,[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring{outline:1px dotted ButtonText}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-input-placeholder{color:inherit;opacity:.54}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}html{font-size:100%;box-sizing:border-box}*{box-sizing:inherit}*::before,*::after{box-sizing:inherit}body{color:#161616;font-family:"Skyscanner Relative",-apple-system,BlinkMacSystemFont,"Roboto","Oxygen","Ubuntu","Cantarell","Fira Sans","Droid Sans","Helvetica Neue",sans-serif;font-size:1rem;line-height:1.3rem}body.scaffold-font-size{font-size:13px}body.enable-font-smoothing{-webkit-font-smoothing:antialiased}:focus-visible{outline:.125rem solid #0062e3;outline-offset:.125rem}.hidden,.hide{display:none !important}.visuallyhidden,.visually-hidden{position:absolute;width:1px;height:1px;margin:-1px;padding:0;border:0;white-space:nowrap;overflow:hidden;clip:rect(0 0 0 0)}.visuallyhidden.focusable:active,.visuallyhidden.focusable:focus,.visually-hidden.focusable:active,.visually-hidden.focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}.invisible{visibility:hidden}.clearfix::before,.clearfix::after{content:"";display:table}.clearfix::after{clear:both}
19
+ *//*! normalize.css v4.1.1 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;line-height:1.15;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block}audio:not([controls]){display:none;height:0}progress{vertical-align:baseline}template,[hidden]{display:none}a{background-color:rgba(0,0,0,0);-webkit-text-decoration-skip:objects}a:active,a:hover{outline-width:0}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:inherit}b,strong{font-weight:bolder}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background-color:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-0.25em}sup{top:-0.5em}img{border-style:none}svg:not(:root){overflow:hidden}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}figure{margin:1em 40px}hr{box-sizing:content-box;height:0;overflow:visible}button,input,optgroup,select,textarea{font:inherit;margin:0}optgroup{font-weight:bold}button,input{overflow:visible}button,select{text-transform:none}button,html [type=button],[type=reset],[type=submit]{-webkit-appearance:button}button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{border-style:none;padding:0}button:-moz-focusring,[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring{outline:1px dotted ButtonText}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-input-placeholder{color:inherit;opacity:.54}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}html{font-size:100%;box-sizing:border-box}*{box-sizing:inherit}*::before,*::after{box-sizing:inherit}body{color:#161616;font-family:var(--bpk-base-font-stack, "Skyscanner Relative", "Noto Sans Arabic", "Noto Sans Hebrew", "Noto Sans", "Noto Sans Devanagari", "Noto Sans Thai", "Noto Sans SC", "Noto Sans TC", "Noto Sans JP", "Noto Sans KR", -apple-system, BlinkMacSystemFont, "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif);font-size:1rem;line-height:1.3rem}body.scaffold-font-size{font-size:13px}body.enable-font-smoothing{-webkit-font-smoothing:antialiased}:focus-visible{outline:.125rem solid #0062e3;outline-offset:.125rem}.hidden,.hide{display:none !important}.visuallyhidden,.visually-hidden{position:absolute;width:1px;height:1px;margin:-1px;padding:0;border:0;white-space:nowrap;overflow:hidden;clip:rect(0 0 0 0)}.visuallyhidden.focusable:active,.visuallyhidden.focusable:focus,.visually-hidden.focusable:active,.visually-hidden.focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}.invisible{visibility:hidden}.clearfix::before,.clearfix::after{content:"";display:table}.clearfix::after{clear:both}