@skyscanner/backpack-web 42.2.0 → 42.4.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 (51) hide show
  1. package/bpk-component-chat-bubble/index.d.ts +4 -0
  2. package/bpk-component-chat-bubble/index.js +21 -0
  3. package/bpk-component-chat-bubble/src/BpkChatBubble.d.ts +3 -0
  4. package/bpk-component-chat-bubble/src/BpkChatBubble.js +107 -0
  5. package/bpk-component-chat-bubble/src/BpkChatBubble.module.css +18 -0
  6. package/bpk-component-chat-bubble/src/common-types.d.ts +55 -0
  7. package/bpk-component-chat-bubble/src/common-types.js +29 -0
  8. package/bpk-component-floating-notification/index.d.ts +2 -1
  9. package/bpk-component-floating-notification/index.js +2 -1
  10. package/bpk-component-floating-notification/src/BpkFloatingNotification.d.ts +5 -0
  11. package/bpk-component-floating-notification/src/BpkFloatingNotification.js +8 -2
  12. package/bpk-component-floating-notification/src/BpkFloatingNotification.module.css +1 -1
  13. package/bpk-component-info-banner/src/BpkInfoBanner.module.css +1 -1
  14. package/bpk-component-info-banner/src/BpkInfoBannerInner.js +8 -3
  15. package/bpk-component-info-banner/src/common-types.d.ts +1 -0
  16. package/bpk-component-info-banner/src/common-types.js +2 -1
  17. package/bpk-component-layout/src/BpkBox.d.ts +1 -1
  18. package/bpk-component-layout/src/BpkBox.js +6 -3
  19. package/bpk-component-layout/src/BpkFlex.d.ts +1 -1
  20. package/bpk-component-layout/src/BpkFlex.js +8 -3
  21. package/bpk-component-layout/src/BpkGrid.d.ts +1 -1
  22. package/bpk-component-layout/src/BpkGrid.js +8 -3
  23. package/bpk-component-layout/src/BpkGridItem.d.ts +1 -1
  24. package/bpk-component-layout/src/BpkGridItem.js +14 -5
  25. package/bpk-component-layout/src/BpkStack.d.ts +3 -3
  26. package/bpk-component-layout/src/BpkStack.js +16 -9
  27. package/bpk-component-layout/src/commonProps.d.ts +10 -3
  28. package/bpk-component-layout/src/theme.js +239 -3
  29. package/bpk-component-layout/src/tokenUtils.d.ts +1 -1
  30. package/bpk-component-layout/src/tokenUtils.js +9 -3
  31. package/bpk-component-layout/src/types.d.ts +9 -8
  32. package/bpk-component-modal/index.d.ts +2 -1
  33. package/bpk-component-modal/index.js +2 -1
  34. package/bpk-component-modal/src/BpkModalV3/{BpkModalV3Body.module.css → BpkModalV3Body/BpkModalV3Body.module.css} +1 -1
  35. package/bpk-component-modal/src/BpkModalV3/BpkModalV3Content/BpkModalV3Content.module.css +18 -0
  36. package/bpk-component-modal/src/BpkModalV3/BpkModalV3Root/BpkModalV3Root.d.ts +2 -2
  37. package/bpk-component-modal/src/BpkModalV3/BpkModalV3Root/BpkModalV3Root.js +39 -16
  38. package/bpk-component-modal/src/BpkModalV3/BpkModalV3Root/useBodyLock.d.ts +9 -0
  39. package/bpk-component-modal/src/BpkModalV3/BpkModalV3Root/useBodyLock.js +69 -0
  40. package/bpk-component-modal/src/BpkModalV3/BpkModalV3Scrim/BpkModalV3Scrim.js +1 -1
  41. package/bpk-component-modal/src/BpkModalV3/{BpkModalV3Scrim.module.css → BpkModalV3Scrim/BpkModalV3Scrim.module.css} +1 -1
  42. package/bpk-component-modal/src/BpkModalV3/common-types.d.ts +7 -1
  43. package/bpk-component-modal/src/BpkModalV3/common-types.js +24 -1
  44. package/package.json +1 -1
  45. package/bpk-component-modal/src/BpkModalV3/BpkModalV3Content.module.css +0 -18
  46. /package/bpk-component-chatbot-input/src/{InputField.module.css → InputField/InputField.module.css} +0 -0
  47. /package/bpk-component-chatbot-input/src/{TextAreaField.module.css → TextAreaField/TextAreaField.module.css} +0 -0
  48. /package/bpk-component-modal/src/BpkModalV3/{BpkModalV3CloseTrigger.module.css → BpkModalV3CloseTrigger/BpkModalV3CloseTrigger.module.css} +0 -0
  49. /package/bpk-component-modal/src/BpkModalV3/{BpkModalV3Header.module.css → BpkModalV3Header/BpkModalV3Header.module.css} +0 -0
  50. /package/bpk-component-modal/src/BpkModalV3/{BpkModalV3HeroImage.module.css → BpkModalV3HeroImage/BpkModalV3HeroImage.module.css} +0 -0
  51. /package/bpk-component-modal/src/BpkModalV3/{BpkModalV3Title.module.css → BpkModalV3Title/BpkModalV3Title.module.css} +0 -0
@@ -16,46 +16,53 @@
16
16
  * limitations under the License.
17
17
  */
18
18
 
19
+ import { forwardRef } from 'react';
19
20
  import { Stack, VStack, HStack } from '@chakra-ui/react';
20
21
  import { getDataComponentAttribute } from "../../bpk-react-utils";
21
22
  import { processBpkComponentProps } from "./tokenUtils";
22
23
  import { jsx as _jsx } from "react/jsx-runtime";
23
- export const BpkStack = ({
24
+ export const BpkStack = /*#__PURE__*/forwardRef(({
24
25
  children,
25
26
  ...props
26
- }) => {
27
+ }, ref) => {
27
28
  const processedProps = processBpkComponentProps(props, {
28
29
  component: 'BpkStack'
29
30
  });
30
31
  return /*#__PURE__*/_jsx(Stack, {
32
+ ref: ref,
31
33
  ...getDataComponentAttribute('Stack'),
32
34
  ...processedProps,
33
35
  children: children
34
36
  });
35
- };
36
- export const BpkHStack = ({
37
+ });
38
+ BpkStack.displayName = 'BpkStack';
39
+ export const BpkHStack = /*#__PURE__*/forwardRef(({
37
40
  children,
38
41
  ...props
39
- }) => {
42
+ }, ref) => {
40
43
  const processedProps = processBpkComponentProps(props, {
41
44
  component: 'BpkStack'
42
45
  });
43
46
  return /*#__PURE__*/_jsx(HStack, {
47
+ ref: ref,
44
48
  ...getDataComponentAttribute('HStack'),
45
49
  ...processedProps,
46
50
  children: children
47
51
  });
48
- };
49
- export const BpkVStack = ({
52
+ });
53
+ BpkHStack.displayName = 'BpkHStack';
54
+ export const BpkVStack = /*#__PURE__*/forwardRef(({
50
55
  children,
51
56
  ...props
52
- }) => {
57
+ }, ref) => {
53
58
  const processedProps = processBpkComponentProps(props, {
54
59
  component: 'BpkStack'
55
60
  });
56
61
  return /*#__PURE__*/_jsx(VStack, {
62
+ ref: ref,
57
63
  ...getDataComponentAttribute('VStack'),
58
64
  ...processedProps,
59
65
  children: children
60
66
  });
61
- };
67
+ });
68
+ BpkVStack.displayName = 'BpkVStack';
@@ -1,4 +1,6 @@
1
+ import type { AriaRole, KeyboardEventHandler, MouseEventHandler } from 'react';
1
2
  import type { BpkSpacingValue, BpkSizeValue, BpkPositionValue, BpkResponsiveValue } from './tokens';
3
+ import type { TextStyle } from '../../bpk-component-text/src/BpkText';
2
4
  /**
3
5
  * Common spacing-related props shared by all Backpack layout components
4
6
  * All spacing props must use Backpack spacing tokens or percentages
@@ -39,13 +41,18 @@ export interface BpkSpacingProps {
39
41
  * layout components purely structural.
40
42
  *
41
43
  * NOTE:
42
- * - Layout components other than BpkBox do not expose event handlers.
43
- * - BpkBox reintroduces a minimal set of events (onClick, onFocus, onBlur)
44
- * on its own props type.
44
+ * - Layout components expose onClick, tabIndex and role to support interactive
45
+ * container patterns (e.g. clickable cards, landmark regions).
46
+ * - BpkBox additionally exposes onFocus and onBlur on its own props type.
45
47
  */
46
48
  export interface BpkCommonLayoutProps extends BpkSpacingProps {
47
49
  className?: never;
48
50
  style?: never;
51
+ tabIndex?: number;
52
+ role?: AriaRole;
53
+ onClick?: MouseEventHandler<HTMLElement>;
54
+ onKeyDown?: KeyboardEventHandler<HTMLElement>;
55
+ textStyle?: BpkResponsiveValue<TextStyle>;
49
56
  'data-testid'?: string;
50
57
  'data-cy'?: string;
51
58
  color?: never;
@@ -17,11 +17,10 @@
17
17
  */
18
18
 
19
19
  import { defineConfig } from '@chakra-ui/react';
20
+
20
21
  // Import tokens from Backpack foundations
21
22
  // Note: Some tokens may not be in TypeScript definitions but exist at runtime
22
-
23
- const bpkTokens = require('@skyscanner/bpk-foundations-web/tokens/base.es6');
24
-
23
+ import * as bpkTokens from '@skyscanner/bpk-foundations-web/tokens/base.es6';
25
24
  // NOTE:
26
25
  // We intentionally do not use the raw breakpoint *values* from foundations here.
27
26
  // Foundations exports breakpoint values such as `breakpointMobile = "32rem"` which
@@ -210,6 +209,242 @@ export function getBorderRadiusValue(token) {
210
209
  export function getShadowValue(token) {
211
210
  return shadowMap[token];
212
211
  }
212
+
213
+ /**
214
+ * Maps Backpack text style tokens to Chakra UI textStyles.
215
+ * CSS property values are sourced from @skyscanner/bpk-foundations-web/tokens/base.es6.
216
+ * Each entry mirrors the corresponding SCSS mixin in bpk-mixins/_typography.scss.
217
+ */
218
+ const textStylesMap = {
219
+ xs: {
220
+ value: {
221
+ fontSize: bpkTokens.fontSizeXs,
222
+ lineHeight: bpkTokens.lineHeightXs,
223
+ fontWeight: bpkTokens.fontWeightBook
224
+ }
225
+ },
226
+ sm: {
227
+ value: {
228
+ fontSize: bpkTokens.fontSizeSm,
229
+ lineHeight: bpkTokens.lineHeightSm,
230
+ fontWeight: bpkTokens.fontWeightBook
231
+ }
232
+ },
233
+ base: {
234
+ value: {
235
+ fontSize: bpkTokens.fontSizeBase,
236
+ lineHeight: bpkTokens.lineHeightBase,
237
+ fontWeight: bpkTokens.fontWeightBook
238
+ }
239
+ },
240
+ lg: {
241
+ value: {
242
+ fontSize: bpkTokens.fontSizeLg,
243
+ lineHeight: bpkTokens.lineHeightLg,
244
+ fontWeight: bpkTokens.fontWeightBook
245
+ }
246
+ },
247
+ xl: {
248
+ value: {
249
+ fontSize: bpkTokens.fontSizeXl,
250
+ lineHeight: bpkTokens.lineHeightXl,
251
+ fontWeight: bpkTokens.fontWeightBook
252
+ }
253
+ },
254
+ xxl: {
255
+ value: {
256
+ fontSize: bpkTokens.fontSizeXxl,
257
+ lineHeight: bpkTokens.lineHeightXxl,
258
+ fontWeight: bpkTokens.fontWeightBold
259
+ }
260
+ },
261
+ xxxl: {
262
+ value: {
263
+ fontSize: bpkTokens.fontSizeXxxl,
264
+ lineHeight: bpkTokens.lineHeightXxxl,
265
+ fontWeight: bpkTokens.fontWeightBold
266
+ }
267
+ },
268
+ xxxxl: {
269
+ value: {
270
+ fontSize: bpkTokens.fontSizeXxxxl,
271
+ lineHeight: bpkTokens.lineHeightXxxxl,
272
+ fontWeight: bpkTokens.fontWeightBold,
273
+ letterSpacing: bpkTokens.letterSpacingTight
274
+ }
275
+ },
276
+ xxxxxl: {
277
+ value: {
278
+ fontSize: bpkTokens.fontSizeXxxxxl,
279
+ lineHeight: bpkTokens.lineHeightXxxxxl,
280
+ fontWeight: bpkTokens.fontWeightBold,
281
+ letterSpacing: bpkTokens.letterSpacingTight
282
+ }
283
+ },
284
+ caption: {
285
+ value: {
286
+ fontSize: bpkTokens.fontSizeXs,
287
+ lineHeight: bpkTokens.lineHeightXs,
288
+ fontWeight: bpkTokens.fontWeightBook
289
+ }
290
+ },
291
+ footnote: {
292
+ value: {
293
+ fontSize: bpkTokens.fontSizeSm,
294
+ lineHeight: bpkTokens.lineHeightSm,
295
+ fontWeight: bpkTokens.fontWeightBook
296
+ }
297
+ },
298
+ 'label-1': {
299
+ value: {
300
+ fontSize: bpkTokens.fontSizeBase,
301
+ lineHeight: bpkTokens.lineHeightBase,
302
+ fontWeight: bpkTokens.fontWeightBold
303
+ }
304
+ },
305
+ 'label-2': {
306
+ value: {
307
+ fontSize: bpkTokens.fontSizeSm,
308
+ lineHeight: bpkTokens.lineHeightSm,
309
+ fontWeight: bpkTokens.fontWeightBold
310
+ }
311
+ },
312
+ 'label-3': {
313
+ value: {
314
+ fontSize: bpkTokens.fontSizeXs,
315
+ lineHeight: bpkTokens.lineHeightXs,
316
+ fontWeight: bpkTokens.fontWeightBold
317
+ }
318
+ },
319
+ 'body-default': {
320
+ value: {
321
+ fontSize: bpkTokens.fontSizeBase,
322
+ lineHeight: bpkTokens.lineHeightBase,
323
+ fontWeight: bpkTokens.fontWeightBook
324
+ }
325
+ },
326
+ 'body-longform': {
327
+ value: {
328
+ fontSize: bpkTokens.fontSizeLg,
329
+ lineHeight: bpkTokens.lineHeightLg,
330
+ fontWeight: bpkTokens.fontWeightBook
331
+ }
332
+ },
333
+ subheading: {
334
+ value: {
335
+ fontSize: bpkTokens.fontSizeXl,
336
+ lineHeight: bpkTokens.lineHeightXl,
337
+ fontWeight: bpkTokens.fontWeightBook
338
+ }
339
+ },
340
+ 'heading-1': {
341
+ value: {
342
+ fontSize: bpkTokens.fontSizeXxxl,
343
+ lineHeight: bpkTokens.lineHeightXxxl,
344
+ fontWeight: bpkTokens.fontWeightBold
345
+ }
346
+ },
347
+ 'heading-2': {
348
+ value: {
349
+ fontSize: bpkTokens.fontSizeXxl,
350
+ lineHeight: bpkTokens.lineHeightXxl,
351
+ fontWeight: bpkTokens.fontWeightBold
352
+ }
353
+ },
354
+ 'heading-3': {
355
+ value: {
356
+ fontSize: bpkTokens.fontSizeXl,
357
+ lineHeight: bpkTokens.lineHeightXlTight,
358
+ fontWeight: bpkTokens.fontWeightBold
359
+ }
360
+ },
361
+ 'heading-4': {
362
+ value: {
363
+ fontSize: bpkTokens.fontSizeLg,
364
+ lineHeight: bpkTokens.lineHeightLgTight,
365
+ fontWeight: bpkTokens.fontWeightBold
366
+ }
367
+ },
368
+ 'heading-5': {
369
+ value: {
370
+ fontSize: bpkTokens.fontSizeBase,
371
+ lineHeight: bpkTokens.lineHeightBaseTight,
372
+ fontWeight: bpkTokens.fontWeightBold
373
+ }
374
+ },
375
+ 'hero-1': {
376
+ value: {
377
+ fontSize: bpkTokens.fontSize8Xl,
378
+ lineHeight: bpkTokens.lineHeight8Xl,
379
+ fontWeight: bpkTokens.fontWeightBlack,
380
+ letterSpacing: bpkTokens.letterSpacingHero
381
+ }
382
+ },
383
+ 'hero-2': {
384
+ value: {
385
+ fontSize: bpkTokens.fontSize7Xl,
386
+ lineHeight: bpkTokens.lineHeight7Xl,
387
+ fontWeight: bpkTokens.fontWeightBlack,
388
+ letterSpacing: bpkTokens.letterSpacingHero
389
+ }
390
+ },
391
+ 'hero-3': {
392
+ value: {
393
+ fontSize: bpkTokens.fontSize6Xl,
394
+ lineHeight: bpkTokens.lineHeight6Xl,
395
+ fontWeight: bpkTokens.fontWeightBlack,
396
+ letterSpacing: bpkTokens.letterSpacingHero
397
+ }
398
+ },
399
+ 'hero-4': {
400
+ value: {
401
+ fontSize: bpkTokens.fontSizeXxxxxl,
402
+ lineHeight: bpkTokens.lineHeightXxxxxl,
403
+ fontWeight: bpkTokens.fontWeightBlack,
404
+ letterSpacing: bpkTokens.letterSpacingHero
405
+ }
406
+ },
407
+ 'hero-5': {
408
+ value: {
409
+ fontSize: bpkTokens.fontSizeXxxxl,
410
+ lineHeight: bpkTokens.lineHeightXxxl,
411
+ fontWeight: bpkTokens.fontWeightBlack,
412
+ letterSpacing: bpkTokens.letterSpacingHero
413
+ }
414
+ },
415
+ 'hero-6': {
416
+ value: {
417
+ fontSize: bpkTokens.fontSizeXxxl,
418
+ lineHeight: bpkTokens.lineHeightXxl,
419
+ fontWeight: bpkTokens.fontWeightBlack,
420
+ letterSpacing: bpkTokens.letterSpacingHero
421
+ }
422
+ },
423
+ 'editorial-1': {
424
+ value: {
425
+ fontFamily: `var(--bpk-larken-font-stack, ${bpkTokens.fontFamilyLarken})`,
426
+ fontSize: bpkTokens.fontSizeXxxxl,
427
+ lineHeight: bpkTokens.lineHeightXxxxl,
428
+ fontWeight: bpkTokens.fontWeightLight
429
+ }
430
+ },
431
+ 'editorial-2': {
432
+ value: {
433
+ fontFamily: `var(--bpk-larken-font-stack, ${bpkTokens.fontFamilyLarken})`,
434
+ fontSize: bpkTokens.fontSizeXxl,
435
+ lineHeight: bpkTokens.lineHeightXxl,
436
+ fontWeight: bpkTokens.fontWeightLight
437
+ }
438
+ },
439
+ 'editorial-3': {
440
+ value: {
441
+ fontFamily: `var(--bpk-larken-font-stack, ${bpkTokens.fontFamilyLarken})`,
442
+ fontSize: bpkTokens.fontSizeLg,
443
+ lineHeight: bpkTokens.lineHeightLg,
444
+ fontWeight: bpkTokens.fontWeightBook
445
+ }
446
+ }
447
+ };
213
448
  export function createBpkConfig() {
214
449
  // Convert breakpoint map to Chakra UI format
215
450
  // Breakpoints in Chakra v3 are typically simple strings in the breakpoints object
@@ -227,6 +462,7 @@ export function createBpkConfig() {
227
462
  tokens: {
228
463
  spacing: spacingMap
229
464
  },
465
+ textStyles: textStylesMap,
230
466
  breakpoints: chakraBreakpoints
231
467
  }
232
468
  });
@@ -1,4 +1,4 @@
1
- export type BpkLayoutComponentName = 'BpkBox' | 'BpkFlex' | 'BpkGrid' | 'BpkStack';
1
+ export type BpkLayoutComponentName = 'BpkBox' | 'BpkFlex' | 'BpkGrid' | 'BpkGridItem' | 'BpkStack';
2
2
  /**
3
3
  * Allowlisted, component-scoped prop groups that are eligible for Backpack responsive value
4
4
  * processing (Backpack breakpoint keys -> Chakra breakpoint keys).
@@ -34,6 +34,8 @@ import { BpkBreakpointToChakraKey, isValidSpacingValue, isValidSizeValue, isVali
34
34
  export const BPK_RESPONSIVE_PROP_GROUPS_BY_COMPONENT = {
35
35
  BpkBox: {
36
36
  container: [
37
+ // Typography
38
+ 'textStyle',
37
39
  // Display
38
40
  'display',
39
41
  // Flex container props
@@ -48,25 +50,29 @@ export const BPK_RESPONSIVE_PROP_GROUPS_BY_COMPONENT = {
48
50
  },
49
51
  // Note: BpkFlex maps its public API props to these Chakra keys.
50
52
  BpkFlex: {
51
- container: ['flexDirection', 'justifyContent', 'alignItems', 'flexWrap'],
53
+ container: ['textStyle', 'flexDirection', 'justifyContent', 'alignItems', 'flexWrap'],
52
54
  item: ['flexGrow', 'flexShrink', 'flexBasis']
53
55
  },
54
56
  // Note: BpkGrid maps its public API props to these Chakra keys.
55
57
  BpkGrid: {
56
- container: ['justifyContent', 'alignItems', 'gridTemplateColumns', 'gridTemplateRows', 'gridTemplateAreas', 'gridAutoFlow', 'gridAutoRows', 'gridAutoColumns'],
58
+ container: ['textStyle', 'justifyContent', 'alignItems', 'gridTemplateColumns', 'gridTemplateRows', 'gridTemplateAreas', 'gridAutoFlow', 'gridAutoRows', 'gridAutoColumns'],
57
59
  item: [
58
60
  // Used when placing the grid itself within a parent grid.
59
61
  'gridColumn', 'gridRow']
60
62
  },
63
+ BpkGridItem: {
64
+ container: ['textStyle']
65
+ },
61
66
  // Note: BpkStack uses Chakra Stack option prop names directly.
62
67
  BpkStack: {
63
- container: StackOptionKeys
68
+ container: ['textStyle', ...StackOptionKeys]
64
69
  }
65
70
  };
66
71
  export const BPK_RESPONSIVE_PROP_KEYS_BY_COMPONENT = {
67
72
  BpkBox: [...BPK_RESPONSIVE_PROP_GROUPS_BY_COMPONENT.BpkBox.container, ...(BPK_RESPONSIVE_PROP_GROUPS_BY_COMPONENT.BpkBox.item ?? [])],
68
73
  BpkFlex: [...BPK_RESPONSIVE_PROP_GROUPS_BY_COMPONENT.BpkFlex.container, ...(BPK_RESPONSIVE_PROP_GROUPS_BY_COMPONENT.BpkFlex.item ?? [])],
69
74
  BpkGrid: [...BPK_RESPONSIVE_PROP_GROUPS_BY_COMPONENT.BpkGrid.container, ...(BPK_RESPONSIVE_PROP_GROUPS_BY_COMPONENT.BpkGrid.item ?? [])],
75
+ BpkGridItem: [...BPK_RESPONSIVE_PROP_GROUPS_BY_COMPONENT.BpkGridItem.container],
70
76
  BpkStack: [...BPK_RESPONSIVE_PROP_GROUPS_BY_COMPONENT.BpkStack.container]
71
77
  };
72
78
  function filterToAllowlist(props, allowlist) {
@@ -4,11 +4,11 @@ import type { BpkCommonLayoutProps } from './commonProps';
4
4
  import type { BpkSpacingValue, BpkResponsiveValue, BpkBasisValue } from './tokens';
5
5
  import type { BoxProps, FlexProps, GridProps, GridItemProps, StackProps } from '@chakra-ui/react';
6
6
  /**
7
- * Layout-level event props that should not be exposed on layout components
8
- * by default. BpkBox will reintroduce a minimal subset (onClick, onFocus,
9
- * onBlur) explicitly on its own props type.
7
+ * Layout-level event props that should not be exposed on layout components.
8
+ * onClick is handled via BpkCommonLayoutProps; onFocus/onBlur are reintroduced
9
+ * on BpkBoxProps only.
10
10
  */
11
- type LayoutEventProps = 'onClick' | 'onMouseEnter' | 'onMouseLeave' | 'onMouseOver' | 'onMouseOut' | 'onMouseDown' | 'onMouseUp' | 'onFocus' | 'onBlur' | 'onKeyDown' | 'onKeyUp' | 'onKeyPress';
11
+ type LayoutEventProps = 'onMouseEnter' | 'onMouseLeave' | 'onMouseOver' | 'onMouseOut' | 'onMouseDown' | 'onMouseUp' | 'onFocus' | 'onBlur' | 'onKeyDown' | 'onKeyUp' | 'onKeyPress';
12
12
  /**
13
13
  * Shorthand props from the underlying layout system that we do NOT expose on
14
14
  * Backpack layout components. These mostly mirror longer-form spacing,
@@ -93,13 +93,14 @@ export interface BpkBoxSpecificProps extends Omit<RemoveCommonProps<BoxProps>, B
93
93
  }
94
94
  /**
95
95
  * Props for BpkBox component
96
- * Combines Box-specific props with Backpack common layout props
97
- * and reintroduces a minimal set of interaction props.
96
+ * Combines Box-specific props with Backpack common layout props.
97
+ * onClick is inherited from BpkCommonLayoutProps.
98
+ * onFocus and onBlur are reintroduced here as BpkBox-only interaction props.
99
+ * textStyle maps to Chakra's `textStyle` theme prop for Backpack typography and supports responsive values.
98
100
  */
99
- type BoxEventProps = Pick<BoxProps, 'onClick' | 'onFocus' | 'onBlur'>;
101
+ type BoxEventProps = Pick<BoxProps, 'onFocus' | 'onBlur'>;
100
102
  export interface BpkBoxProps extends BpkCommonLayoutProps, BpkBoxSpecificProps {
101
103
  children?: ReactNode;
102
- onClick?: BoxEventProps['onClick'];
103
104
  onFocus?: BoxEventProps['onFocus'];
104
105
  onBlur?: BoxEventProps['onBlur'];
105
106
  }
@@ -2,9 +2,10 @@ import BpkModal from './src/BpkModal';
2
2
  import { MODAL_STYLING } from './src/BpkModalInner';
3
3
  import { BpkModalV2 } from './src/BpkModalV2/BpkModal';
4
4
  import BpkModalV3 from './src/BpkModalV3/BpkModalV3';
5
+ import { MODAL_V3_TYPES } from './src/BpkModalV3/common-types';
5
6
  import { propTypes, defaultProps } from './src/legacy-prop-types';
6
7
  import themeAttributes from './src/themeAttributes';
7
8
  import type { Props } from './src/BpkModal';
8
9
  export type BpkModalProps = Props;
9
10
  export default BpkModal;
10
- export { propTypes, defaultProps, themeAttributes, BpkModalV2, BpkModalV3, MODAL_STYLING, };
11
+ export { propTypes, defaultProps, themeAttributes, BpkModalV2, BpkModalV3, MODAL_V3_TYPES, MODAL_STYLING, };
@@ -20,8 +20,9 @@ import BpkModal from "./src/BpkModal";
20
20
  import { MODAL_STYLING } from "./src/BpkModalInner";
21
21
  import { BpkModalV2 } from "./src/BpkModalV2/BpkModal";
22
22
  import BpkModalV3 from "./src/BpkModalV3/BpkModalV3";
23
+ import { MODAL_V3_TYPES } from "./src/BpkModalV3/common-types";
23
24
  // @ts-expect-error Untyped import. See `decisions/imports-ts-suppressions.md`.
24
25
  import { propTypes, defaultProps } from "./src/legacy-prop-types";
25
26
  import themeAttributes from "./src/themeAttributes";
26
27
  export default BpkModal;
27
- export { propTypes, defaultProps, themeAttributes, BpkModalV2, BpkModalV3, MODAL_STYLING };
28
+ export { propTypes, defaultProps, themeAttributes, BpkModalV2, BpkModalV3, MODAL_V3_TYPES, MODAL_STYLING };
@@ -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-modal-v3__body{flex:1;overflow-y:auto}
18
+ .bpk-modal-v3__body{flex:1;overflow-y:auto;overscroll-behavior:contain}
@@ -0,0 +1,18 @@
1
+ /*
2
+ * Backpack - Skyscanner's Design System
3
+ *
4
+ * Copyright 2016 Skyscanner Ltd
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */
18
+ .bpk-modal-v3__positioner{position:fixed;display:flex;z-index:1100;justify-content:center;align-items:center;inset:0;pointer-events:none}.bpk-modal-v3__positioner--sheet{align-items:flex-end}.bpk-modal-v3__positioner--full{align-items:stretch}.bpk-modal-v3__positioner--chatbot{align-items:stretch}.bpk-modal-v3__content{position:relative;display:flex;flex-direction:column;background-color:#fff;overflow:clip;pointer-events:none}.bpk-modal-v3__content[data-state=open]{pointer-events:auto}.bpk-modal-v3__content--default{width:100%;max-width:50rem;max-height:90vh;transform:scale(0.9);transition:transform 200ms ease-in-out;border-radius:1.5rem;opacity:0;box-shadow:0px 4px 14px 0px rgba(37,32,31,.25)}@media(max-width: 32rem){.bpk-modal-v3__content--default{max-width:none;height:100%;max-height:none;transform:translateY(100%);border-radius:0;box-shadow:none}.bpk-modal-v3__content--default[data-state=open]{transform:translateY(0)}.bpk-modal-v3__content--default[data-state=closed]{transform:translateY(100%)}}.bpk-modal-v3__content--default[data-state=open]{transform:scale(1);opacity:1}.bpk-modal-v3__content--default[data-state=closed]{transform:scale(0.9);opacity:0}.bpk-modal-v3__content--sheet{width:100%;height:fit-content;max-height:100dvh;transform:translateY(100%);transition:opacity 400ms cubic-bezier(0.5, 0, 0, 1),transform 400ms cubic-bezier(0.5, 0, 0, 1);border-radius:1.5rem 1.5rem 0 0;opacity:0;box-shadow:0px 4px 14px 0px rgba(37,32,31,.25)}.bpk-modal-v3__content--sheet[data-state=open]{transform:translateY(0);opacity:1}.bpk-modal-v3__content--sheet[data-state=closed]{transform:translateY(100%);opacity:0}.bpk-modal-v3__content--full{width:100%;height:100%;transform:translateY(100%);transition:opacity 400ms cubic-bezier(0.5, 0, 0, 1),transform 400ms cubic-bezier(0.5, 0, 0, 1);border-radius:0;opacity:0}.bpk-modal-v3__content--full[data-state=open]{transform:translateY(0);opacity:1}.bpk-modal-v3__content--full[data-state=closed]{transform:translateY(100%);opacity:0}.bpk-modal-v3__content--chatbot{width:100%;height:100%;transform:translateX(100%);transition:transform 400ms cubic-bezier(0.5, 0, 0, 1);border-radius:0;background-color:#eff3f8}html[dir=rtl] .bpk-modal-v3__content--chatbot{transform:translateX(-100%)}.bpk-modal-v3__content--chatbot[data-state=open]{transform:translateX(0)}html[dir=rtl] .bpk-modal-v3__content--chatbot[data-state=open]{transform:translateX(0)}.bpk-modal-v3__content--chatbot[data-state=closed]{transform:translateX(100%)}html[dir=rtl] .bpk-modal-v3__content--chatbot[data-state=closed]{transform:translateX(-100%)}@media(prefers-reduced-motion: reduce){.bpk-modal-v3__content--default{transform:scale(1);transition:none;opacity:1}}@media(prefers-reduced-motion: reduce)and (max-width: 32rem){.bpk-modal-v3__content--default{transform:translateY(0)}}@media(prefers-reduced-motion: reduce){.bpk-modal-v3__content--sheet,.bpk-modal-v3__content--full{transform:translateY(0);transition:none;opacity:1}.bpk-modal-v3__content--chatbot{transform:translateX(0);transition:none;opacity:1}html[dir=rtl] .bpk-modal-v3__content--chatbot{transform:translateX(0)}}
@@ -1,5 +1,5 @@
1
- import type { ReactNode } from 'react';
2
- import type { BpkModalV3Type } from '../common-types';
1
+ import { type ReactNode } from 'react';
2
+ import { type BpkModalV3Type } from '../common-types';
3
3
  type BpkModalV3RootProps = {
4
4
  children: ReactNode;
5
5
  open?: boolean;
@@ -16,29 +16,52 @@
16
16
  * limitations under the License.
17
17
  */
18
18
 
19
+ import { useEffect, useState } from 'react';
19
20
  import { Dialog } from '@ark-ui/react';
21
+ import { durationBase } from '@skyscanner/bpk-foundations-web/tokens/base.es6';
20
22
  import { getDataComponentAttribute } from "../../../../bpk-react-utils";
21
23
  import { ModalTypeProvider } from "../BpkModalV3Context";
24
+ import { MODAL_V3_TYPES } from "../common-types";
25
+ import useBodyLock from "./useBodyLock";
22
26
  import { jsx as _jsx } from "react/jsx-runtime";
23
27
  const BpkModalV3Root = ({
24
28
  children,
25
29
  onOpenChange,
26
30
  open,
27
- type = 'default'
28
- }) => /*#__PURE__*/_jsx(Dialog.Root, {
29
- ...(open !== undefined && {
30
- open
31
- }),
32
- ...(onOpenChange !== undefined && {
33
- onOpenChange
34
- }),
35
- children: /*#__PURE__*/_jsx(ModalTypeProvider, {
36
- value: type,
37
- children: /*#__PURE__*/_jsx("div", {
38
- "data-type": type,
39
- ...getDataComponentAttribute('ModalV3'),
40
- children: children
31
+ type = MODAL_V3_TYPES.default
32
+ }) => {
33
+ const [internalOpen, setInternalOpen] = useState(open ?? false);
34
+ const isOpen = open ?? internalOpen;
35
+ const [bodyLockOpen, setBodyLockOpen] = useState(isOpen);
36
+ useEffect(() => {
37
+ if (isOpen) {
38
+ setBodyLockOpen(true);
39
+ } else {
40
+ const timer = setTimeout(() => setBodyLockOpen(false), parseInt(durationBase, 10));
41
+ return () => clearTimeout(timer);
42
+ }
43
+ return undefined;
44
+ }, [isOpen]);
45
+ useBodyLock(type === MODAL_V3_TYPES.chatbot && bodyLockOpen);
46
+ const handleOpenChange = details => {
47
+ if (open === undefined) {
48
+ setInternalOpen(details.open);
49
+ }
50
+ onOpenChange?.(details);
51
+ };
52
+ return /*#__PURE__*/_jsx(Dialog.Root, {
53
+ ...(open !== undefined && {
54
+ open
55
+ }),
56
+ onOpenChange: handleOpenChange,
57
+ children: /*#__PURE__*/_jsx(ModalTypeProvider, {
58
+ value: type,
59
+ children: /*#__PURE__*/_jsx("div", {
60
+ "data-type": type,
61
+ ...getDataComponentAttribute('ModalV3'),
62
+ children: children
63
+ })
41
64
  })
42
- })
43
- });
65
+ });
66
+ };
44
67
  export default BpkModalV3Root;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Locks body scroll when the modal is open, preventing background scroll and
3
+ * iOS Safari bounce effects. Restores all body styles and scroll position on
4
+ * cleanup.
5
+ * @param {boolean} isLocked - Whether the body scroll should be locked.
6
+ * @returns {void}
7
+ */
8
+ declare const useBodyLock: (isLocked: boolean) => void;
9
+ export default useBodyLock;
@@ -0,0 +1,69 @@
1
+ /*
2
+ * Backpack - Skyscanner's Design System
3
+ *
4
+ * Copyright 2016 Skyscanner Ltd
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */
18
+
19
+ import { useEffect, useRef } from 'react';
20
+
21
+ /**
22
+ * Locks body scroll when the modal is open, preventing background scroll and
23
+ * iOS Safari bounce effects. Restores all body styles and scroll position on
24
+ * cleanup.
25
+ * @param {boolean} isLocked - Whether the body scroll should be locked.
26
+ * @returns {void}
27
+ */
28
+ const useBodyLock = isLocked => {
29
+ const savedScrollYRef = useRef(0);
30
+ const savedBodyStylesRef = useRef(null);
31
+ useEffect(() => {
32
+ if (!isLocked) {
33
+ return undefined;
34
+ }
35
+ const {
36
+ body
37
+ } = document;
38
+ const currentScrollY = window.scrollY;
39
+ savedScrollYRef.current = currentScrollY;
40
+ savedBodyStylesRef.current = {
41
+ overflow: body.style.overflow || '',
42
+ position: body.style.position || '',
43
+ top: body.style.top || '',
44
+ width: body.style.width || '',
45
+ touchAction: body.style.touchAction || '',
46
+ overscrollBehavior: body.style.overscrollBehavior || ''
47
+ };
48
+ body.style.overflow = 'hidden';
49
+ body.style.position = 'fixed';
50
+ body.style.top = `-${currentScrollY}px`;
51
+ body.style.width = '100%';
52
+ body.style.touchAction = 'none';
53
+ body.style.overscrollBehavior = 'contain';
54
+ return () => {
55
+ if (savedBodyStylesRef.current) {
56
+ const saved = savedBodyStylesRef.current;
57
+ body.style.overflow = saved.overflow;
58
+ body.style.position = saved.position;
59
+ body.style.top = saved.top;
60
+ body.style.width = saved.width;
61
+ body.style.touchAction = saved.touchAction;
62
+ body.style.overscrollBehavior = saved.overscrollBehavior;
63
+ savedBodyStylesRef.current = null;
64
+ window.scrollTo(0, savedScrollYRef.current);
65
+ }
66
+ };
67
+ }, [isLocked]);
68
+ };
69
+ export default useBodyLock;
@@ -25,7 +25,7 @@ const getClassName = cssModules(STYLES);
25
25
  const BpkModalV3Scrim = () => {
26
26
  const type = useModalType();
27
27
  return /*#__PURE__*/_jsx(Dialog.Backdrop, {
28
- className: getClassName('bpk-modal-v3__scrim', type === 'full' && 'bpk-modal-v3__scrim--full'),
28
+ className: getClassName('bpk-modal-v3__scrim', type === 'full' && 'bpk-modal-v3__scrim--full', type === 'chatbot' && 'bpk-modal-v3__scrim--chatbot'),
29
29
  ...getDataComponentAttribute('ModalV3Scrim')
30
30
  });
31
31
  };