@skyscanner/backpack-web 41.10.0 → 41.11.1-beta.1

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 (52) hide show
  1. package/bpk-component-accordion/src/BpkAccordion.js +2 -1
  2. package/bpk-component-accordion/src/BpkAccordionItem.js +2 -1
  3. package/bpk-component-aria-live/src/BpkAriaLive.js +2 -1
  4. package/bpk-component-autosuggest/src/BpkAutosuggestV2/BpkAutosuggest.js +2 -1
  5. package/bpk-component-bottom-sheet/src/BpkBottomSheet.js +14 -5
  6. package/bpk-component-breakpoint/src/BpkBreakpoint.js +2 -2
  7. package/bpk-component-breakpoint/src/useMediaQuery.js +27 -14
  8. package/bpk-component-bubble/src/BpkBubble.js +2 -1
  9. package/bpk-component-calendar/src/BpkCalendarDate.js +2 -1
  10. package/bpk-component-calendar/src/BpkCalendarGrid.js +2 -1
  11. package/bpk-component-calendar/src/BpkCalendarGridHeader.js +2 -1
  12. package/bpk-component-calendar/src/BpkCalendarGridTransition.js +2 -1
  13. package/bpk-component-calendar/src/BpkCalendarNav.js +2 -1
  14. package/bpk-component-calendar/src/BpkCalendarWeek.js +2 -1
  15. package/bpk-component-card-list/src/BpkCardList.js +3 -0
  16. package/bpk-component-card-list/src/BpkCardListRowRail/BpkCardListCarousel.js +15 -30
  17. package/bpk-component-card-list/src/BpkCardListRowRail/BpkCardListCarousel.module.css +1 -1
  18. package/bpk-component-card-list/src/BpkCardListRowRail/BpkCardListRowRailContainer.js +14 -1
  19. package/bpk-component-card-list/src/BpkCardListRowRail/constants.d.ts +1 -1
  20. package/bpk-component-card-list/src/BpkCardListRowRail/constants.js +1 -1
  21. package/bpk-component-card-list/src/BpkCardListRowRail/utils.d.ts +25 -8
  22. package/bpk-component-card-list/src/BpkCardListRowRail/utils.js +118 -35
  23. package/bpk-component-card-list/src/common-types.d.ts +3 -0
  24. package/bpk-component-chip/src/BpkDismissibleChip.js +3 -2
  25. package/bpk-component-chip/src/BpkDropdownChip.js +2 -0
  26. package/bpk-component-chip/src/BpkIconChip.js +2 -0
  27. package/bpk-component-chip/src/BpkSelectableChip.js +2 -1
  28. package/bpk-component-chip-group/src/BpkMultiSelectChipGroup.js +2 -1
  29. package/bpk-component-chip-group/src/BpkStickyChip.js +2 -1
  30. package/bpk-component-content-cards/src/BpkContentCard.module.css +1 -1
  31. package/bpk-component-input/src/withOpenEvents.d.ts +11 -12
  32. package/bpk-component-input/src/withOpenEvents.js +3 -3
  33. package/bpk-component-map/src/BpkBasicMapMarker.js +3 -0
  34. package/bpk-component-map/src/BpkPriceMarker.js +2 -0
  35. package/bpk-component-nudger/src/BpkNudger.js +2 -2
  36. package/bpk-component-panel/index.d.ts +2 -0
  37. package/bpk-component-panel/index.js +1 -0
  38. package/bpk-component-panel/src/BpkPanel.d.ts +14 -1
  39. package/bpk-component-panel/src/BpkPanel.js +4 -1
  40. package/bpk-component-panel/src/BpkPanel.module.css +1 -1
  41. package/bpk-component-price-range/src/BpkPriceMarker.js +2 -1
  42. package/bpk-component-price-range/src/BpkPriceRange.js +2 -1
  43. package/bpk-component-scrollable-calendar/src/BpkScrollableCalendarGrid.js +2 -1
  44. package/bpk-component-scrollable-calendar/src/BpkScrollableCalendarGridList.js +2 -1
  45. package/bpk-component-tooltip/src/BpkTooltip.js +2 -1
  46. package/bpk-mixins/_index.scss +1 -0
  47. package/bpk-mixins/_surfaces.scss +52 -0
  48. package/bpk-react-utils/index.d.ts +13 -1
  49. package/bpk-react-utils/index.js +4 -2
  50. package/bpk-react-utils/src/surfaceColors.d.ts +11 -0
  51. package/bpk-react-utils/src/surfaceColors.js +29 -0
  52. package/package.json +1 -1
@@ -17,7 +17,7 @@
17
17
  */
18
18
 
19
19
  import { createContext } from 'react';
20
- import { cssModules } from "../../bpk-react-utils";
20
+ import { cssModules, getDataComponentAttribute } from "../../bpk-react-utils";
21
21
  import STYLES from "./BpkAccordion.module.css";
22
22
  import { jsx as _jsx } from "react/jsx-runtime";
23
23
  const getClassName = cssModules(STYLES);
@@ -41,6 +41,7 @@ const BpkAccordion = props => {
41
41
  },
42
42
  children: /*#__PURE__*/_jsx("div", {
43
43
  className: classNames,
44
+ ...getDataComponentAttribute('Accordion'),
44
45
  ...rest,
45
46
  children: children
46
47
  })
@@ -22,7 +22,7 @@ import AnimateHeight from "../../bpk-animate-height";
22
22
  import { withButtonAlignment } from "../../bpk-component-icon";
23
23
  import ChevronDownIcon from "../../bpk-component-icon/sm/chevron-down";
24
24
  import BpkText, { TEXT_STYLES } from "../../bpk-component-text";
25
- import { cssModules } from "../../bpk-react-utils";
25
+ import { cssModules, getDataComponentAttribute } from "../../bpk-react-utils";
26
26
  import { BpkAccordionContext } from "./BpkAccordion";
27
27
  import STYLES from "./BpkAccordionItem.module.css";
28
28
  import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
@@ -88,6 +88,7 @@ const BpkAccordionItem = props => {
88
88
  _jsxs("div", {
89
89
  id: id,
90
90
  className: itemClassNames.join(' '),
91
+ ...getDataComponentAttribute('AccordionItem'),
91
92
  ...rest,
92
93
  children: [/*#__PURE__*/_jsx("div", {
93
94
  className: titleClassNames.join(' '),
@@ -16,7 +16,7 @@
16
16
  * limitations under the License.
17
17
  */
18
18
 
19
- import { cssModules } from "../../bpk-react-utils";
19
+ import { cssModules, getDataComponentAttribute } from "../../bpk-react-utils";
20
20
  import STYLES from "./BpkAriaLive.module.css";
21
21
  import { jsx as _jsx } from "react/jsx-runtime";
22
22
  const getClassName = cssModules(STYLES);
@@ -35,6 +35,7 @@ const BpkAriaLive = ({
35
35
  return /*#__PURE__*/_jsx("div", {
36
36
  "aria-live": politenessSetting,
37
37
  className: classNames,
38
+ ...getDataComponentAttribute('AriaLive'),
38
39
  ...rest
39
40
  });
40
41
  };
@@ -21,7 +21,7 @@ import { useFloating, offset, shift, size, arrow as floatingArrow, FloatingArrow
21
21
  import { useCombobox } from 'downshift';
22
22
  import { surfaceHighlightDay } from '@skyscanner/bpk-foundations-web/tokens/base.es6';
23
23
  import BpkInput from "../../../bpk-component-input";
24
- import { cssModules } from "../../../bpk-react-utils";
24
+ import { cssModules, getDataComponentAttribute } from "../../../bpk-react-utils";
25
25
  import STYLES from "./BpkAutosuggest.module.css";
26
26
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
27
27
  const getClassName = cssModules(STYLES);
@@ -536,6 +536,7 @@ const BpkAutosuggest = /*#__PURE__*/forwardRef(({
536
536
  return /*#__PURE__*/_jsxs("div", {
537
537
  ref: containerWrapperRef,
538
538
  className: getClassName(theme.container, suggestionsCount && theme.containerOpen),
539
+ ...getDataComponentAttribute('Autosuggest'),
539
540
  children: [renderInput(), showSuggestions && (isDesktop ? /*#__PURE__*/_jsx(FloatingPortal, {
540
541
  children: /*#__PURE__*/_jsxs("div", {
541
542
  ref: refs.setFloating,
@@ -23,9 +23,10 @@ import BpkCloseButton from "../../bpk-component-close-button";
23
23
  import BpkLink from "../../bpk-component-link";
24
24
  import BpkNavigationBar from "../../bpk-component-navigation-bar";
25
25
  import { TEXT_STYLES } from "../../bpk-component-text/src/BpkText";
26
+ import BpkVisuallyHidden from "../../bpk-component-visually-hidden";
26
27
  import { BpkDialogWrapper, cssModules, getDataComponentAttribute } from "../../bpk-react-utils";
27
28
  import STYLES from "./BpkBottomSheet.module.css";
28
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
29
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
29
30
  const getClassName = cssModules(STYLES);
30
31
  export const PADDING_TYPE = {
31
32
  none: 'none',
@@ -93,6 +94,8 @@ const BpkBottomSheet = ({
93
94
  }, timeoutDuration);
94
95
  }, [isAboveMobile, onClose]);
95
96
  const headingId = `bpk-bottom-sheet-heading-${id}`;
97
+ const hiddenTitleId = `bpk-bottom-sheet-title-hidden-${id}`;
98
+ const showHiddenTitle = !title && 'ariaLabel' in ariaProps && ariaProps.ariaLabel;
96
99
  const dialogClassName = getClassName('bpk-bottom-sheet', wide && 'bpk-bottom-sheet--wide');
97
100
  const contentStyle = getContentStyles(paddingStyles);
98
101
  return /*#__PURE__*/_jsx(BpkDialogWrapper, {
@@ -115,10 +118,10 @@ const BpkBottomSheet = ({
115
118
  },
116
119
  ...getDataComponentAttribute('BottomSheet'),
117
120
  children: /*#__PURE__*/_jsxs(_Fragment, {
118
- children: [/*#__PURE__*/_jsx("header", {
121
+ children: [/*#__PURE__*/_jsxs("header", {
119
122
  className: getClassName('bpk-bottom-sheet--header-wrapper'),
120
- children: /*#__PURE__*/_jsx(BpkNavigationBar, {
121
- id: headingId,
123
+ children: [/*#__PURE__*/_jsx(BpkNavigationBar, {
124
+ id: showHiddenTitle ? hiddenTitleId : headingId,
122
125
  title: title,
123
126
  titleTextStyle: TEXT_STYLES.label1,
124
127
  titleTagName: title ? 'h2' : 'span',
@@ -132,7 +135,13 @@ const BpkBottomSheet = ({
132
135
  onClick: onAction,
133
136
  children: actionText
134
137
  }) : null
135
- })
138
+ }), showHiddenTitle && /*#__PURE__*/_jsx(BpkVisuallyHidden, {
139
+ as: "h2",
140
+ children: /*#__PURE__*/_jsx("span", {
141
+ id: hiddenTitleId,
142
+ children: ariaProps.ariaLabel
143
+ })
144
+ })]
136
145
  }), /*#__PURE__*/_jsx("div", {
137
146
  className: contentStyle,
138
147
  children: children
@@ -42,7 +42,7 @@ const useLegacyWarning = (query, legacy, isClient) => useMemo(() => {
42
42
  const BpkBreakpoint = ({
43
43
  children,
44
44
  legacy = false,
45
- matchSSR = false,
45
+ matchSSR,
46
46
  query
47
47
  }) => {
48
48
  /**
@@ -64,7 +64,7 @@ const BpkBreakpoint = ({
64
64
  return matches ? children : null;
65
65
  }
66
66
  if (typeof children === 'function') {
67
- return children(matchSSR);
67
+ return children(!!matchSSR);
68
68
  }
69
69
  return matchSSR ? children : null;
70
70
  };
@@ -17,28 +17,41 @@
17
17
  */
18
18
 
19
19
  import { useEffect, useState } from 'react';
20
- const useMediaQuery = (query, matchSSR = false) => {
20
+ const useMediaQuery = (query, matchSSR) => {
21
21
  const isClient = typeof window !== 'undefined' && !!window.matchMedia;
22
+
23
+ // When matchSSR is provided, we need hydration protection
24
+ const needsHydrationProtection = matchSSR !== undefined;
25
+ const [isHydrated, setIsHydrated] = useState(!needsHydrationProtection);
22
26
  const [matches, setMatches] = useState(() => {
23
- // When matchSSR=true: use matchSSR value to match server-rendered HTML
24
- // This prevents hydration errors when User-Agent (server) != viewport size (client)
25
- if (!isClient || matchSSR) {
27
+ if (!isClient) {
28
+ return matchSSR ?? false;
29
+ }
30
+ if (needsHydrationProtection) {
26
31
  return matchSSR;
27
32
  }
28
33
  return window.matchMedia(query).matches;
29
34
  });
35
+
36
+ // Mark hydration as complete
30
37
  useEffect(() => {
31
- if (isClient) {
32
- const media = window.matchMedia(query);
33
- setMatches(media.matches);
34
- const listener = () => {
35
- setMatches(media.matches);
36
- };
37
- media.addEventListener('change', listener);
38
- return () => media.removeEventListener('change', listener);
38
+ if (needsHydrationProtection && !isHydrated) {
39
+ setIsHydrated(true);
39
40
  }
40
- return () => {};
41
- }, [query, isClient]);
41
+ }, [needsHydrationProtection, isHydrated]);
42
+ useEffect(() => {
43
+ // Wait for hydration to complete
44
+ if (!isClient || !isHydrated) {
45
+ return () => {};
46
+ }
47
+ const media = window.matchMedia(query);
48
+ setMatches(media.matches);
49
+ const listener = () => {
50
+ setMatches(media.matches);
51
+ };
52
+ media.addEventListener('change', listener);
53
+ return () => media.removeEventListener('change', listener);
54
+ }, [query, isClient, isHydrated]);
42
55
  return matches;
43
56
  };
44
57
  export default useMediaQuery;
@@ -17,7 +17,7 @@
17
17
  */
18
18
 
19
19
  import BpkText, { TEXT_COLORS, TEXT_STYLES } from "../../bpk-component-text";
20
- import { cssModules } from "../../bpk-react-utils";
20
+ import { cssModules, getDataComponentAttribute } from "../../bpk-react-utils";
21
21
  import BpkBubbleArrow from "../icons/BpkBubbleArrow";
22
22
  import STYLES from "./BpkBubble.module.css";
23
23
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
@@ -26,6 +26,7 @@ const BpkBubble = ({
26
26
  children
27
27
  }) => /*#__PURE__*/_jsxs("span", {
28
28
  className: getClassName('bpk-bubble'),
29
+ ...getDataComponentAttribute('Bubble'),
29
30
  children: [/*#__PURE__*/_jsx(BpkText, {
30
31
  textStyle: TEXT_STYLES.label3,
31
32
  tagName: "span",
@@ -17,7 +17,7 @@
17
17
  */
18
18
 
19
19
  import { PureComponent } from 'react';
20
- import { cssModules } from "../../bpk-react-utils";
20
+ import { cssModules, getDataComponentAttribute } from "../../bpk-react-utils";
21
21
  import STYLES from "./BpkCalendarDate.module.css";
22
22
  import { jsx as _jsx } from "react/jsx-runtime";
23
23
  const getClassName = cssModules(STYLES);
@@ -125,6 +125,7 @@ class BpkCalendarDate extends PureComponent {
125
125
  type: "button",
126
126
  style: style,
127
127
  className: classNames.join(' '),
128
+ ...getDataComponentAttribute('CalendarDate'),
128
129
  "aria-label": `${date.getDate()}`,
129
130
  disabled: isBlocked,
130
131
  tabIndex: isKeyboardFocusable && isFocused ? 0 : -1,
@@ -17,7 +17,7 @@
17
17
  */
18
18
 
19
19
  import { Component } from 'react';
20
- import { cssModules, isDeviceIos } from "../../bpk-react-utils";
20
+ import { cssModules, isDeviceIos, getDataComponentAttribute } from "../../bpk-react-utils";
21
21
  import { addCalendarGridTransition } from "./BpkCalendarGridTransition";
22
22
  import BpkCalendarWeek from "./BpkCalendarWeek";
23
23
  import { CALENDAR_SELECTION_TYPE } from "./custom-proptypes";
@@ -105,6 +105,7 @@ class BpkCalendarGrid extends Component {
105
105
  const classNames = getClassName('bpk-calendar-grid', className);
106
106
  return /*#__PURE__*/_jsx("div", {
107
107
  className: classNames,
108
+ ...getDataComponentAttribute('CalendarGrid'),
108
109
  "aria-hidden": !isKeyboardFocusable,
109
110
  role: "grid",
110
111
  "aria-label": formatMonth(month),
@@ -17,7 +17,7 @@
17
17
  */
18
18
 
19
19
  import { PureComponent } from 'react';
20
- import { cssModules } from "../../bpk-react-utils";
20
+ import { cssModules, getDataComponentAttribute } from "../../bpk-react-utils";
21
21
  import { orderDaysOfWeek } from "./date-utils";
22
22
  import STYLES from "./BpkCalendarGridHeader.module.css";
23
23
  import { jsx as _jsx } from "react/jsx-runtime";
@@ -57,6 +57,7 @@ class BpkCalendarGridHeader extends PureComponent {
57
57
  }
58
58
  return /*#__PURE__*/_jsx("header", {
59
59
  className: classNames.join(' '),
60
+ ...getDataComponentAttribute('CalendarGridHeader'),
60
61
  "aria-hidden": true,
61
62
  children: /*#__PURE__*/_jsx("ol", {
62
63
  className: getClassName('bpk-calendar-header__week'),
@@ -17,7 +17,7 @@
17
17
  */
18
18
 
19
19
  import { Component, createElement as _createElement } from 'react';
20
- import { cssModules, isRTL } from "../../bpk-react-utils";
20
+ import { cssModules, isRTL, getDataComponentAttribute } from "../../bpk-react-utils";
21
21
  import { addMonths, isSameMonth, formatIsoMonth, differenceInCalendarMonths, dateToBoundaries, startOfDay, setMonthYear, isWithinRange, getMonthRange } from "./date-utils";
22
22
  import { getCalendarGridWidth, getTransformStyles, isTransitionEndSupported } from "./utils";
23
23
  import STYLES from "./BpkCalendarGridTransition.module.css";
@@ -132,6 +132,7 @@ class BpkCalendarGridTransition extends Component {
132
132
  className: stripClassNames,
133
133
  style: getTransformStyles(transitionValue),
134
134
  onTransitionEnd: this.onMonthTransitionEnd,
135
+ ...getDataComponentAttribute('CalendarGridTransition'),
135
136
  children: this.state.months.map((m, index) => min && max && isWithinRange(m, {
136
137
  start: min,
137
138
  end: max
@@ -22,7 +22,7 @@ import ArrowLeftIcon from "../../bpk-component-icon/lg/arrow-left";
22
22
  import ArrowRightIcon from "../../bpk-component-icon/lg/arrow-right";
23
23
  // @ts-expect-error Untyped import. See `decisions/imports-ts-suppressions.md`.
24
24
  import BpkSelect from "../../bpk-component-select";
25
- import { cssModules } from "../../bpk-react-utils";
25
+ import { cssModules, getDataComponentAttribute } from "../../bpk-react-utils";
26
26
  import { addMonths, formatIsoMonth, getMonthRange, getMonthsInRange, isWithinRange, parseIsoDate, startOfMonth } from "./date-utils";
27
27
  import STYLES from "./BpkCalendarNav.module.css";
28
28
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
@@ -69,6 +69,7 @@ const BpkCalendarNav = ({
69
69
  const nextMonth = addMonths(baseMonth, 1);
70
70
  return /*#__PURE__*/_jsx("div", {
71
71
  className: getClassName('bpk-calendar-nav'),
72
+ ...getDataComponentAttribute('CalendarNav'),
72
73
  children: /*#__PURE__*/_jsxs("div", {
73
74
  style: {
74
75
  display: 'table-row'
@@ -17,7 +17,7 @@
17
17
  */
18
18
 
19
19
  import { Component } from 'react';
20
- import { cssModules } from "../../bpk-react-utils";
20
+ import { cssModules, getDataComponentAttribute } from "../../bpk-react-utils";
21
21
  import { SELECTION_TYPES } from "./BpkCalendarDate";
22
22
  import { CALENDAR_SELECTION_TYPE } from "./custom-proptypes";
23
23
  import { isSameDay, isSameWeek, isSameMonth, isToday, isWithinRange, startOfMonth, endOfMonth } from "./date-utils";
@@ -267,6 +267,7 @@ class BpkCalendarWeek extends Component {
267
267
  }
268
268
  return /*#__PURE__*/_jsx("div", {
269
269
  className: getClassName('bpk-calendar-week'),
270
+ ...getDataComponentAttribute('CalendarWeek'),
270
271
  role: "row",
271
272
  children: this.props.dates.map(date => {
272
273
  const isBlocked = minDate && maxDate ? !isWithinRange(date.val, {
@@ -38,6 +38,7 @@ const BpkCardList = props => {
38
38
  chipGroup,
39
39
  description,
40
40
  expandText,
41
+ initiallyInViewCardIndex = 0,
41
42
  initiallyShownCardsDesktop = DEFAULT_ITEMS_DESKTOP,
42
43
  initiallyShownCardsMobile = DEFAULT_ITEMS_MOBILE,
43
44
  layoutDesktop,
@@ -78,6 +79,7 @@ const BpkCardList = props => {
78
79
  initiallyShownCards: initiallyShownCardsMobile,
79
80
  layout: layoutMobile,
80
81
  accessibilityLabels: accessibilityLabels,
82
+ initiallyInViewCardIndex: initiallyInViewCardIndex,
81
83
  isMobile: true,
82
84
  children: cardList
83
85
  }), layoutMobile === LAYOUTS.stack && /*#__PURE__*/_jsx(BpkCardListGridStack, {
@@ -97,6 +99,7 @@ const BpkCardList = props => {
97
99
  initiallyShownCards: initiallyShownCardsDesktop,
98
100
  layout: layoutDesktop,
99
101
  accessibilityLabels: accessibilityLabels,
102
+ initiallyInViewCardIndex: initiallyInViewCardIndex,
100
103
  children: cardList
101
104
  }), layoutDesktop === LAYOUTS.grid && accessoryDesktop !== ACCESSORY_DESKTOP_TYPES.pagination && /*#__PURE__*/_jsx(BpkCardListGridStack, {
102
105
  accessory: accessoryDesktop,
@@ -20,7 +20,7 @@ import { useRef, useState, useEffect, isValidElement, Children, useMemo } from '
20
20
  import throttle from 'lodash/throttle';
21
21
  import { cssModules, getDataComponentAttribute } from "../../../bpk-react-utils";
22
22
  import { RENDER_BUFFER_SIZE } from "./constants";
23
- import { lockScroll, setA11yTabIndex, useScrollToCard, useIntersectionObserver } from "./utils";
23
+ import { setA11yTabIndex, useIntersectionObserver, usePageScrollSync } from "./utils";
24
24
  import STYLES from "./BpkCardListCarousel.module.css";
25
25
  import { jsx as _jsx } from "react/jsx-runtime";
26
26
  const getClassName = cssModules(STYLES);
@@ -30,6 +30,7 @@ const BpkCardListCarousel = props => {
30
30
  carouselLabel = (initiallyShownCards, childrenLength) => `Entering Carousel with ${initiallyShownCards} slides shown at a time, ${childrenLength} slides in total. Please use Pagination below with the Previous and Next buttons to navigate, or the slide dot buttons at the end to jump to slides.`,
31
31
  children,
32
32
  currentIndex,
33
+ initialPageIndex,
33
34
  initiallyShownCards,
34
35
  isMobile = false,
35
36
  layout,
@@ -47,13 +48,20 @@ const BpkCardListCarousel = props => {
47
48
  const firstCardWidthRef = useRef(null);
48
49
  const firstCardHeightRef = useRef(null);
49
50
  const [visibilityList, setVisibilityList] = useState(Array(childrenLength).fill(0));
50
- const stateScrollingLockRef = useRef(false);
51
- const openSetStateLockTimeoutRef = useRef(null);
52
51
  const observerVisibility = useIntersectionObserver({
53
52
  root,
54
53
  threshold: 0.5
55
54
  }, setVisibilityList);
56
- useScrollToCard(currentIndex * initiallyShownCards, root, cardRefs, stateScrollingLockRef);
55
+ usePageScrollSync({
56
+ currentIndex,
57
+ setCurrentIndex,
58
+ initiallyShownCards,
59
+ cardRefs,
60
+ visibilityList,
61
+ container: root,
62
+ enabled: !isMobile,
63
+ initialPageIndex
64
+ });
57
65
 
58
66
  // Similar to Virtual Scrolling to improve performance
59
67
  const firstVisibleIndex = Math.max(0, visibilityList.indexOf(1));
@@ -87,22 +95,6 @@ const BpkCardListCarousel = props => {
87
95
  }
88
96
  }
89
97
  }), [childrenLength, observerVisibility, visibilityList, firstCardWidthRef, firstCardHeightRef]);
90
- useEffect(() => {
91
- const container = root;
92
- if (isMobile || !container) return undefined;
93
- const lockScrollDuringInteraction = () => {
94
- lockScroll(stateScrollingLockRef, openSetStateLockTimeoutRef);
95
- };
96
- container.addEventListener('wheel', lockScrollDuringInteraction);
97
- container.addEventListener('touchmove', lockScrollDuringInteraction);
98
- return () => {
99
- container.removeEventListener('touchmove', lockScrollDuringInteraction);
100
- container.removeEventListener('wheel', lockScrollDuringInteraction);
101
- if (openSetStateLockTimeoutRef.current) {
102
- clearTimeout(openSetStateLockTimeoutRef.current);
103
- }
104
- };
105
- }, [root]);
106
98
  useEffect(() => {
107
99
  // update hasBeenVisibleRef to include the range of cards that should be visible
108
100
  const start = currentIndex * initiallyShownCards;
@@ -111,15 +103,6 @@ const BpkCardListCarousel = props => {
111
103
  hasBeenVisibleRef.current.add(i);
112
104
  }
113
105
  }, [currentIndex, initiallyShownCards, childrenLength, dynamicRenderBufferSize]);
114
- useEffect(() => {
115
- const firstVisible = visibilityList.indexOf(1);
116
- if (firstVisible >= 0) {
117
- const newIndex = Math.floor(firstVisible / initiallyShownCards);
118
- if (newIndex !== currentIndex) {
119
- setCurrentIndex(newIndex);
120
- }
121
- }
122
- }, [initiallyShownCards]);
123
106
  useEffect(() => {
124
107
  const handleResize = throttle(() => {
125
108
  firstCardWidthRef.current = null;
@@ -149,8 +132,9 @@ const BpkCardListCarousel = props => {
149
132
  if (firstCardHeightRef.current) {
150
133
  cardDimensionStyle.height = `${firstCardHeightRef.current}px`;
151
134
  }
135
+ const isPageStart = index % initiallyShownCards === 0;
152
136
  const commonProps = {
153
- className: getClassName(`bpk-card-list-row-rail__${layout}__card`),
137
+ className: getClassName(`bpk-card-list-row-rail__${layout}__card`, isPageStart && 'bpk-card-list-row-rail__card--page-start'),
154
138
  style: shownNumberStyle,
155
139
  key: `carousel-card-${index.toString()}`,
156
140
  role: 'group'
@@ -161,6 +145,7 @@ const BpkCardListCarousel = props => {
161
145
  if (!shouldRenderCard) {
162
146
  return /*#__PURE__*/_jsx("div", {
163
147
  ...commonProps,
148
+ ref: cardRefFns[index],
164
149
  style: {
165
150
  ...commonProps.style,
166
151
  ...cardDimensionStyle,
@@ -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{--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)}}.bpk-card-list-row-rail__card-slot{display:grid;height:100%}
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:scroll;box-sizing:border-box;gap:var(--carousel-card-gap);margin-block:-1.5rem;margin-inline:-0.5rem;-webkit-overflow-scrolling:touch;padding-block:1.5rem;padding-inline:.5rem;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(min-width: 32.0625rem){.bpk-card-list-row-rail__row__card,.bpk-card-list-row-rail__rail__card{scroll-snap-align:none}}@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))}}@media(min-width: 32.0625rem){.bpk-card-list-row-rail__card--page-start{scroll-snap-align:start;scroll-snap-stop:always}}.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)}}.bpk-card-list-row-rail__card-slot{display:grid;height:100%}
@@ -28,6 +28,7 @@ const BpkCardListRowRailContainer = props => {
28
28
  accessibilityLabels,
29
29
  accessory,
30
30
  children,
31
+ initiallyInViewCardIndex,
31
32
  initiallyShownCards,
32
33
  isMobile = false,
33
34
  layout
@@ -35,7 +36,18 @@ const BpkCardListRowRailContainer = props => {
35
36
  const childrenCount = Children.count(children);
36
37
  const totalIndicators = Math.ceil(childrenCount / initiallyShownCards);
37
38
  const showAccessory = childrenCount > initiallyShownCards;
38
- const [currentIndex, setCurrentIndex] = useState(0);
39
+
40
+ // Calculate initial page from card index
41
+ const [initialPageIndex] = useState(() => {
42
+ if (initiallyInViewCardIndex < 0) {
43
+ return 0;
44
+ }
45
+ if (initiallyInViewCardIndex >= childrenCount) {
46
+ return Math.max(0, totalIndicators - 1);
47
+ }
48
+ return Math.floor(initiallyInViewCardIndex / initiallyShownCards);
49
+ });
50
+ const [currentIndex, setCurrentIndex] = useState(initialPageIndex);
39
51
  const accessoryContent = layout === LAYOUTS.row && accessory === ACCESSORY_DESKTOP_TYPES.pagination ? /*#__PURE__*/_jsx(BpkPageIndicator, {
40
52
  currentIndex: currentIndex,
41
53
  totalIndicators: totalIndicators,
@@ -57,6 +69,7 @@ const BpkCardListRowRailContainer = props => {
57
69
  isMobile: isMobile,
58
70
  carouselLabel: accessibilityLabels?.carouselLabel,
59
71
  slideLabel: accessibilityLabels?.slideLabel,
72
+ initialPageIndex: initialPageIndex,
60
73
  children: children
61
74
  }), accessoryContent && showAccessory && /*#__PURE__*/_jsx("div", {
62
75
  role: "region",
@@ -1,2 +1,2 @@
1
- export declare const RELEASE_LOCK_DELAY = 20;
1
+ export declare const RELEASE_LOCK_DELAY = 200;
2
2
  export declare const RENDER_BUFFER_SIZE = 3;
@@ -16,5 +16,5 @@
16
16
  * limitations under the License.
17
17
  */
18
18
 
19
- export const RELEASE_LOCK_DELAY = 20;
19
+ export const RELEASE_LOCK_DELAY = 200;
20
20
  export const RENDER_BUFFER_SIZE = 3;
@@ -1,9 +1,4 @@
1
- /**
2
- * Typically used to prevent useScrollToCard() from being called, to prevent scrollings caused by state changes, so as to avoid cyclic dependencies.
3
- * @param stateScrollingLockRef - Ref to the state that indicates if scrollIntoView should be locked
4
- * @param openSetStateLockTimeoutRef - Ref to the timeout that releases the lock after a delay. Should be renewed every time another scroll action is triggered, with a new lock is added.
5
- */
6
- export declare const lockScroll: (stateScrollingLockRef: React.MutableRefObject<boolean>, openSetStateLockTimeoutRef: React.MutableRefObject<NodeJS.Timeout | null>) => void;
1
+ import type { Dispatch, SetStateAction, RefObject } from 'react';
7
2
  /**
8
3
  * Only sets the tabIndex of focusable elements as 0 if the card is visible, otherwise sets it to -1, including all its children.
9
4
  * For example, if there is a button inside a card which is not shown, it cannot be focused as well.
@@ -11,5 +6,27 @@ export declare const lockScroll: (stateScrollingLockRef: React.MutableRefObject<
11
6
  * @param index - Current container index
12
7
  */
13
8
  export declare const setA11yTabIndex: (el: HTMLDivElement | null, index: number, visibilityList: number[]) => void;
14
- export declare const useScrollToCard: (currentIndex: number, container: HTMLElement | null, cardRefs: React.MutableRefObject<Array<HTMLDivElement | null>>, stateScrollingLockRef: React.MutableRefObject<boolean>) => void;
15
- export declare const useIntersectionObserver: ({ root, threshold }: IntersectionObserverInit, setVisibilityList: React.Dispatch<React.SetStateAction<number[]>>) => (element: HTMLElement | null, index: number) => void;
9
+ export declare const useIntersectionObserver: ({ root, threshold }: IntersectionObserverInit, setVisibilityList: Dispatch<SetStateAction<number[]>>) => (element: HTMLElement | null, index: number) => void;
10
+ type UsePageScrollSyncOptions = {
11
+ currentIndex: number;
12
+ setCurrentIndex: Dispatch<SetStateAction<number>>;
13
+ initiallyShownCards: number;
14
+ cardRefs: RefObject<Array<HTMLDivElement | null>>;
15
+ visibilityList: number[];
16
+ container: HTMLElement | null;
17
+ enabled: boolean;
18
+ initialPageIndex: number;
19
+ };
20
+ /**
21
+ * Provides bidirectional synchronisation between page index state and scroll position.
22
+ *
23
+ * - **State → Scroll**: When `currentIndex` changes (e.g. via pagination buttons),
24
+ * the container scrolls to bring the corresponding page into view.
25
+ * - **Scroll → State**: When the user scrolls (via wheel or touch), `currentIndex`
26
+ * is updated to reflect the first visible page.
27
+ *
28
+ * The hook uses a lock mechanism to prevent conflicts between programmatic and
29
+ * user-initiated scrolling.
30
+ */
31
+ export declare const usePageScrollSync: ({ cardRefs, container, currentIndex, enabled, initialPageIndex, initiallyShownCards, setCurrentIndex, visibilityList, }: UsePageScrollSyncOptions) => void;
32
+ export {};