@faststore/components 3.42.0 → 3.45.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 (75) hide show
  1. package/dist/cjs/hooks/UIProvider.d.ts +10 -1
  2. package/dist/cjs/hooks/UIProvider.js +24 -0
  3. package/dist/cjs/hooks/UIProvider.js.map +1 -1
  4. package/dist/cjs/hooks/index.d.ts +2 -1
  5. package/dist/cjs/hooks/index.js +5 -3
  6. package/dist/cjs/hooks/index.js.map +1 -1
  7. package/dist/cjs/hooks/useOnClickOutside.d.ts +4 -0
  8. package/dist/cjs/hooks/useOnClickOutside.js +33 -0
  9. package/dist/cjs/hooks/useOnClickOutside.js.map +1 -0
  10. package/dist/cjs/index.d.ts +2 -0
  11. package/dist/cjs/index.js +4 -2
  12. package/dist/cjs/index.js.map +1 -1
  13. package/dist/cjs/molecules/Accordion/AccordionButton.js +1 -1
  14. package/dist/cjs/molecules/Accordion/AccordionButton.js.map +1 -1
  15. package/dist/cjs/molecules/Modal/Modal.d.ts +14 -6
  16. package/dist/cjs/molecules/Modal/Modal.js +13 -4
  17. package/dist/cjs/molecules/Modal/Modal.js.map +1 -1
  18. package/dist/cjs/molecules/Popover/Popover.d.ts +67 -0
  19. package/dist/cjs/molecules/Popover/Popover.js +65 -0
  20. package/dist/cjs/molecules/Popover/Popover.js.map +1 -0
  21. package/dist/cjs/molecules/Popover/index.d.ts +2 -0
  22. package/dist/cjs/molecules/Popover/index.js +9 -0
  23. package/dist/cjs/molecules/Popover/index.js.map +1 -0
  24. package/dist/cjs/molecules/RegionBar/RegionBar.d.ts +13 -4
  25. package/dist/cjs/molecules/RegionBar/RegionBar.js +5 -3
  26. package/dist/cjs/molecules/RegionBar/RegionBar.js.map +1 -1
  27. package/dist/cjs/organisms/RegionModal/RegionModal.d.ts +10 -1
  28. package/dist/cjs/organisms/RegionModal/RegionModal.js +15 -8
  29. package/dist/cjs/organisms/RegionModal/RegionModal.js.map +1 -1
  30. package/dist/cjs/organisms/ShippingSimulation/ShippingSimulation.d.ts +2 -2
  31. package/dist/cjs/organisms/SlideOver/SlideOverHeader.d.ts +1 -1
  32. package/dist/esm/hooks/UIProvider.d.ts +10 -1
  33. package/dist/esm/hooks/UIProvider.js +24 -0
  34. package/dist/esm/hooks/UIProvider.js.map +1 -1
  35. package/dist/esm/hooks/index.d.ts +2 -1
  36. package/dist/esm/hooks/index.js +2 -1
  37. package/dist/esm/hooks/index.js.map +1 -1
  38. package/dist/esm/hooks/useOnClickOutside.d.ts +4 -0
  39. package/dist/esm/hooks/useOnClickOutside.js +29 -0
  40. package/dist/esm/hooks/useOnClickOutside.js.map +1 -0
  41. package/dist/esm/index.d.ts +2 -0
  42. package/dist/esm/index.js +1 -0
  43. package/dist/esm/index.js.map +1 -1
  44. package/dist/esm/molecules/Accordion/AccordionButton.js +1 -1
  45. package/dist/esm/molecules/Accordion/AccordionButton.js.map +1 -1
  46. package/dist/esm/molecules/Modal/Modal.d.ts +14 -6
  47. package/dist/esm/molecules/Modal/Modal.js +13 -4
  48. package/dist/esm/molecules/Modal/Modal.js.map +1 -1
  49. package/dist/esm/molecules/Popover/Popover.d.ts +67 -0
  50. package/dist/esm/molecules/Popover/Popover.js +62 -0
  51. package/dist/esm/molecules/Popover/Popover.js.map +1 -0
  52. package/dist/esm/molecules/Popover/index.d.ts +2 -0
  53. package/dist/esm/molecules/Popover/index.js +2 -0
  54. package/dist/esm/molecules/Popover/index.js.map +1 -0
  55. package/dist/esm/molecules/RegionBar/RegionBar.d.ts +13 -4
  56. package/dist/esm/molecules/RegionBar/RegionBar.js +5 -3
  57. package/dist/esm/molecules/RegionBar/RegionBar.js.map +1 -1
  58. package/dist/esm/organisms/RegionModal/RegionModal.d.ts +10 -1
  59. package/dist/esm/organisms/RegionModal/RegionModal.js +15 -8
  60. package/dist/esm/organisms/RegionModal/RegionModal.js.map +1 -1
  61. package/dist/esm/organisms/ShippingSimulation/ShippingSimulation.d.ts +2 -2
  62. package/dist/esm/organisms/SlideOver/SlideOverHeader.d.ts +1 -1
  63. package/package.json +2 -2
  64. package/src/hooks/UIProvider.tsx +48 -1
  65. package/src/hooks/index.ts +2 -1
  66. package/src/hooks/useOnClickOutside.ts +40 -0
  67. package/src/index.ts +2 -0
  68. package/src/molecules/Accordion/AccordionButton.tsx +1 -1
  69. package/src/molecules/Modal/Modal.tsx +29 -8
  70. package/src/molecules/Popover/Popover.tsx +209 -0
  71. package/src/molecules/Popover/index.tsx +2 -0
  72. package/src/molecules/RegionBar/RegionBar.tsx +21 -5
  73. package/src/organisms/RegionModal/RegionModal.tsx +29 -7
  74. package/src/organisms/ShippingSimulation/ShippingSimulation.tsx +2 -2
  75. package/src/organisms/SlideOver/SlideOverHeader.tsx +1 -1
@@ -43,6 +43,10 @@ export interface RegionModalProps extends Omit<ModalProps, 'children'> {
43
43
  * Postal code input's label.
44
44
  */
45
45
  inputLabel?: string;
46
+ /**
47
+ * The text displayed on the InputField Button. Suggestion: maximum 9 characters.
48
+ */
49
+ inputButtonActionText?: string;
46
50
  /**
47
51
  * Enables fadeOut effect on modal after onSubmit function
48
52
  */
@@ -67,6 +71,11 @@ export interface RegionModalProps extends Omit<ModalProps, 'children'> {
67
71
  * Callback function when the input clear button is clicked.
68
72
  */
69
73
  onClear?: () => void;
74
+ /**
75
+ * Determines if the modal can be dismissed using the close button or the Escape key.
76
+ * @default true
77
+ */
78
+ dismissible?: boolean;
70
79
  }
71
- declare function RegionModal({ testId, title, description, closeButtonAriaLabel, idkPostalCodeLinkProps, errorMessage, inputRef, inputValue, inputLabel, fadeOutOnSubmit, overlayProps, onClose, onInput, onSubmit, onClear, ...otherProps }: RegionModalProps): React.JSX.Element;
80
+ declare function RegionModal({ testId, title, description, closeButtonAriaLabel, idkPostalCodeLinkProps, errorMessage, inputRef, inputValue, inputLabel, inputButtonActionText, fadeOutOnSubmit, overlayProps, onClose, onInput, onSubmit, onClear, dismissible, ...otherProps }: RegionModalProps): React.JSX.Element;
72
81
  export default RegionModal;
@@ -1,19 +1,26 @@
1
1
  import React from 'react';
2
2
  import { InputField, Link, Modal, ModalBody, ModalHeader } from '../..';
3
- function RegionModal({ testId = 'fs-region-modal', title = 'Set your location', description = 'Prices, offers and availability may vary according to your location.', closeButtonAriaLabel = 'Close Region Modal', idkPostalCodeLinkProps, errorMessage, inputRef, inputValue, inputLabel = 'Postal Code', fadeOutOnSubmit, overlayProps, onClose, onInput, onSubmit, onClear, ...otherProps }) {
4
- return (React.createElement(Modal, { "data-fs-region-modal": true, testId: testId, overlayProps: overlayProps, title: "Region modal", "aria-label": "Region modal", ...otherProps }, ({ fadeOut }) => (React.createElement(React.Fragment, null,
5
- React.createElement(ModalHeader, { onClose: () => {
6
- fadeOut();
7
- onClose?.();
8
- }, title: title, description: description, closeBtnProps: {
3
+ function RegionModal({ testId = 'fs-region-modal', title = 'Set your location', description = 'Offers and availability vary by location.', closeButtonAriaLabel = 'Close Region Modal', idkPostalCodeLinkProps, errorMessage, inputRef, inputValue, inputLabel = 'Postal Code', inputButtonActionText = 'Apply', fadeOutOnSubmit, overlayProps, onClose, onInput, onSubmit, onClear, dismissible = true, ...otherProps }) {
4
+ return (React.createElement(Modal, { "data-fs-region-modal": true, testId: testId, overlayProps: overlayProps, title: "Region modal", "aria-label": "Region modal", disableEscapeKeyDown: !dismissible, onEntered: () => {
5
+ if (inputRef?.current) {
6
+ inputRef.current.focus();
7
+ }
8
+ }, ...otherProps }, ({ fadeOut }) => (React.createElement(React.Fragment, null,
9
+ React.createElement(ModalHeader, { ...(dismissible && {
10
+ onClose: () => {
11
+ fadeOut();
12
+ onClear?.();
13
+ onClose?.();
14
+ },
15
+ }), title: title, description: description, closeBtnProps: {
9
16
  'aria-label': closeButtonAriaLabel,
10
17
  } }),
11
18
  React.createElement(ModalBody, null,
12
- React.createElement(InputField, { "data-fs-region-modal-input": true, id: `${testId}-input-field`, inputRef: inputRef, label: inputLabel, actionable: true, value: inputValue, onInput: (event) => onInput?.(event), onSubmit: () => {
19
+ React.createElement(InputField, { "data-fs-region-modal-input": true, id: `${testId}-input-field`, inputRef: inputRef, label: inputLabel, actionable: true, value: inputValue, buttonActionText: inputButtonActionText, onInput: (event) => onInput?.(event), onSubmit: () => {
13
20
  onSubmit?.();
14
21
  fadeOutOnSubmit ? fadeOut() : null;
15
22
  }, onClear: () => onClear?.(), error: errorMessage }),
16
- React.createElement(Link, { "data-fs-region-modal-link": true, ...idkPostalCodeLinkProps }))))));
23
+ idkPostalCodeLinkProps && (React.createElement(Link, { "data-fs-region-modal-link": true, ...idkPostalCodeLinkProps })))))));
17
24
  }
18
25
  export default RegionModal;
19
26
  //# sourceMappingURL=RegionModal.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"RegionModal.js","sourceRoot":"","sources":["../../../../src/organisms/RegionModal/RegionModal.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAGzB,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,OAAO,CAAA;AA4EvE,SAAS,WAAW,CAAC,EACnB,MAAM,GAAG,iBAAiB,EAC1B,KAAK,GAAG,mBAAmB,EAC3B,WAAW,GAAG,sEAAsE,EACpF,oBAAoB,GAAG,oBAAoB,EAC3C,sBAAsB,EACtB,YAAY,EACZ,QAAQ,EACR,UAAU,EACV,UAAU,GAAG,aAAa,EAC1B,eAAe,EACf,YAAY,EACZ,OAAO,EACP,OAAO,EACP,QAAQ,EACR,OAAO,EACP,GAAG,UAAU,EACI;IACjB,OAAO,CACL,oBAAC,KAAK,kCAEJ,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,YAAY,EAC1B,KAAK,EAAC,cAAc,gBACT,cAAc,KACrB,UAAU,IAEb,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,CAChB;QACE,oBAAC,WAAW,IACV,OAAO,EAAE,GAAG,EAAE;gBACZ,OAAO,EAAE,CAAA;gBACT,OAAO,EAAE,EAAE,CAAA;YACb,CAAC,EACD,KAAK,EAAE,KAAK,EACZ,WAAW,EAAE,WAAW,EACxB,aAAa,EAAE;gBACb,YAAY,EAAE,oBAAoB;aACnC,GACD;QACF,oBAAC,SAAS;YACR,oBAAC,UAAU,wCAET,EAAE,EAAE,GAAG,MAAM,cAAc,EAC3B,QAAQ,EAAE,QAAQ,EAClB,KAAK,EAAE,UAAU,EACjB,UAAU,QACV,KAAK,EAAE,UAAU,EACjB,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,CAAC,EACpC,QAAQ,EAAE,GAAG,EAAE;oBACb,QAAQ,EAAE,EAAE,CAAA;oBACZ,eAAe,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,IAAI,CAAA;gBACpC,CAAC,EACD,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,EAAE,EAAE,EAC1B,KAAK,EAAE,YAAY,GACnB;YAEF,oBAAC,IAAI,0CAA+B,sBAAsB,GAAI,CACpD,CACX,CACJ,CACK,CACT,CAAA;AACH,CAAC;AAED,eAAe,WAAW,CAAA"}
1
+ {"version":3,"file":"RegionModal.js","sourceRoot":"","sources":["../../../../src/organisms/RegionModal/RegionModal.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAA;AAGzB,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,OAAO,CAAA;AAqFvE,SAAS,WAAW,CAAC,EACnB,MAAM,GAAG,iBAAiB,EAC1B,KAAK,GAAG,mBAAmB,EAC3B,WAAW,GAAG,2CAA2C,EACzD,oBAAoB,GAAG,oBAAoB,EAC3C,sBAAsB,EACtB,YAAY,EACZ,QAAQ,EACR,UAAU,EACV,UAAU,GAAG,aAAa,EAC1B,qBAAqB,GAAG,OAAO,EAC/B,eAAe,EACf,YAAY,EACZ,OAAO,EACP,OAAO,EACP,QAAQ,EACR,OAAO,EACP,WAAW,GAAG,IAAI,EAClB,GAAG,UAAU,EACI;IACjB,OAAO,CACL,oBAAC,KAAK,kCAEJ,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,YAAY,EAC1B,KAAK,EAAC,cAAc,gBACT,cAAc,EACzB,oBAAoB,EAAE,CAAC,WAAW,EAClC,SAAS,EAAE,GAAG,EAAE;YACd,IAAI,QAAQ,EAAE,OAAO,EAAE,CAAC;gBACtB,QAAQ,CAAC,OAAO,CAAC,KAAK,EAAE,CAAA;YAC1B,CAAC;QACH,CAAC,KACG,UAAU,IAEb,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,CAChB;QACE,oBAAC,WAAW,OACN,CAAC,WAAW,IAAI;gBAClB,OAAO,EAAE,GAAG,EAAE;oBACZ,OAAO,EAAE,CAAA;oBACT,OAAO,EAAE,EAAE,CAAA;oBACX,OAAO,EAAE,EAAE,CAAA;gBACb,CAAC;aACF,CAAC,EACF,KAAK,EAAE,KAAK,EACZ,WAAW,EAAE,WAAW,EACxB,aAAa,EAAE;gBACb,YAAY,EAAE,oBAAoB;aACnC,GACD;QACF,oBAAC,SAAS;YACR,oBAAC,UAAU,wCAET,EAAE,EAAE,GAAG,MAAM,cAAc,EAC3B,QAAQ,EAAE,QAAQ,EAClB,KAAK,EAAE,UAAU,EACjB,UAAU,QACV,KAAK,EAAE,UAAU,EACjB,gBAAgB,EAAE,qBAAqB,EACvC,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,CAAC,EACpC,QAAQ,EAAE,GAAG,EAAE;oBACb,QAAQ,EAAE,EAAE,CAAA;oBACZ,eAAe,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,IAAI,CAAA;gBACpC,CAAC,EACD,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,EAAE,EAAE,EAC1B,KAAK,EAAE,YAAY,GACnB;YACD,sBAAsB,IAAI,CACzB,oBAAC,IAAI,0CAA+B,sBAAsB,GAAI,CAC/D,CACS,CACX,CACJ,CACK,CACT,CAAA;AACH,CAAC;AAED,eAAe,WAAW,CAAA"}
@@ -54,9 +54,9 @@ interface Address {
54
54
  */
55
55
  reference?: string;
56
56
  /**
57
- * Address geoCoordinates
57
+ * Address geoCoordinates. [longitude, latitude]
58
58
  */
59
- geoCoordinates?: [number];
59
+ geoCoordinates?: [number, number];
60
60
  }
61
61
  export interface ShippingSimulationProps extends HTMLAttributes<HTMLDivElement> {
62
62
  /**
@@ -5,7 +5,7 @@ export interface SlideOverHeaderProps extends Omit<HTMLAttributes<HTMLDivElement
5
5
  /**
6
6
  * A react component to be used as the title in the header.
7
7
  */
8
- children: ReactNode;
8
+ children?: ReactNode;
9
9
  /**
10
10
  * Props for the Close Button component.
11
11
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@faststore/components",
3
- "version": "3.42.0",
3
+ "version": "3.45.0",
4
4
  "main": "dist/cjs/index.js",
5
5
  "module": "dist/esm/index.js",
6
6
  "typings": "dist/esm/index.d.ts",
@@ -56,5 +56,5 @@
56
56
  "volta": {
57
57
  "extends": "../../package.json"
58
58
  },
59
- "gitHead": "d8814aed0849211cb76d06b880c6d556bf1ac898"
59
+ "gitHead": "7a888c79aa95c02416c95820f1a912f829fa379c"
60
60
  }
@@ -1,4 +1,4 @@
1
- import type { PropsWithChildren, ReactNode } from 'react'
1
+ import type { PropsWithChildren, ReactNode, RefObject } from 'react'
2
2
  import React, { createContext, useContext, useMemo, useReducer } from 'react'
3
3
 
4
4
  export interface Toast {
@@ -8,6 +8,11 @@ export interface Toast {
8
8
  icon?: ReactNode
9
9
  }
10
10
 
11
+ export interface Popover {
12
+ isOpen: boolean
13
+ triggerRef?: RefObject<HTMLElement>
14
+ }
15
+
11
16
  interface State {
12
17
  /** Cart sidebar */
13
18
  cart: boolean
@@ -17,7 +22,10 @@ interface State {
17
22
  navbar: boolean
18
23
  /** Search page filter slider */
19
24
  filter: boolean
25
+ /** Toast notifications */
20
26
  toasts: Toast[]
27
+ /** Region Popover */
28
+ popover: Popover
21
29
  }
22
30
 
23
31
  type UIElement = 'navbar' | 'cart' | 'modal' | 'filter'
@@ -38,6 +46,16 @@ type Action =
38
46
  | {
39
47
  type: 'popToast'
40
48
  }
49
+ | {
50
+ type: 'openPopover'
51
+ payload: {
52
+ isOpen: boolean
53
+ triggerRef?: RefObject<HTMLElement>
54
+ }
55
+ }
56
+ | {
57
+ type: 'closePopover'
58
+ }
41
59
 
42
60
  const reducer = (state: State, action: Action): State => {
43
61
  const { type } = action
@@ -89,6 +107,26 @@ const reducer = (state: State, action: Action): State => {
89
107
  }
90
108
  }
91
109
 
110
+ case 'openPopover': {
111
+ return {
112
+ ...state,
113
+ popover: {
114
+ isOpen: true,
115
+ triggerRef: action.payload.triggerRef,
116
+ },
117
+ }
118
+ }
119
+
120
+ case 'closePopover': {
121
+ return {
122
+ ...state,
123
+ popover: {
124
+ isOpen: false,
125
+ triggerRef: undefined,
126
+ },
127
+ }
128
+ }
129
+
92
130
  default:
93
131
  throw new Error(`Action ${type} not implemented`)
94
132
  }
@@ -100,6 +138,10 @@ const initializer = (): State => ({
100
138
  navbar: false,
101
139
  filter: false,
102
140
  toasts: [],
141
+ popover: {
142
+ isOpen: false,
143
+ triggerRef: undefined,
144
+ },
103
145
  })
104
146
 
105
147
  interface Context extends State {
@@ -113,6 +155,8 @@ interface Context extends State {
113
155
  closeModal: () => void
114
156
  pushToast: (data: Toast) => void
115
157
  popToast: () => void
158
+ openPopover: (popover: Popover) => void
159
+ closePopover: () => void
116
160
  }
117
161
 
118
162
  const UIContext = createContext<Context | undefined>(undefined)
@@ -133,6 +177,9 @@ function UIProvider({ children }: PropsWithChildren<unknown>) {
133
177
  pushToast: (toast: Toast) =>
134
178
  dispatch({ type: 'pushToast', payload: toast }),
135
179
  popToast: () => dispatch({ type: 'popToast' }),
180
+ openPopover: (popover: Popover) =>
181
+ dispatch({ type: 'openPopover', payload: popover }),
182
+ closePopover: () => dispatch({ type: 'closePopover' }),
136
183
  }),
137
184
  []
138
185
  )
@@ -1,6 +1,6 @@
1
1
  export { default as UIProvider, Toast as ToastProps, useUI } from './UIProvider'
2
2
  export { useFadeEffect } from './useFadeEffect'
3
- export { useTrapFocus } from './useTrapFocus'
3
+ export { useOnClickOutside } from './useOnClickOutside'
4
4
  export { useSearch } from './useSearch'
5
5
  export { useSKUMatrix } from './useSKUMatrix'
6
6
  export { useScrollDirection } from './useScrollDirection'
@@ -12,3 +12,4 @@ export type {
12
12
  SlideDirection,
13
13
  } from './useSlider'
14
14
  export { useSlideVisibility } from './useSlideVisibility'
15
+ export { useTrapFocus } from './useTrapFocus'
@@ -0,0 +1,40 @@
1
+ import type { RefObject } from 'react'
2
+ import { useEffect } from 'react'
3
+
4
+ type Handler = (event: any) => void
5
+
6
+ export function useOnClickOutside<T extends HTMLElement = HTMLElement>(
7
+ ref: RefObject<T> | undefined,
8
+ handler: Handler
9
+ ) {
10
+ useEffect(
11
+ () => {
12
+ if (!ref?.current) return
13
+
14
+ const listener: Handler = (event) => {
15
+ if (!ref?.current || ref.current.contains(event.target)) {
16
+ return
17
+ }
18
+
19
+ handler(event)
20
+ }
21
+
22
+ document.addEventListener('mousedown', listener)
23
+ document.addEventListener('touchstart', listener)
24
+
25
+ return () => {
26
+ document.removeEventListener('mousedown', listener)
27
+ document.removeEventListener('touchstart', listener)
28
+ }
29
+ },
30
+ /**
31
+ * Add ref and handler to effect dependencies.
32
+ * It's worth noting that because passed in handler is a new
33
+ * function on every render that will cause this effect
34
+ * callback/cleanup to run every render. It's not a big deal
35
+ * but to optimize you can wrap handler in useCallback before
36
+ * passing it into this hook.
37
+ */
38
+ [ref, handler]
39
+ )
40
+ }
package/src/index.ts CHANGED
@@ -131,6 +131,8 @@ export type {
131
131
  } from './molecules/NavbarLinks'
132
132
  export { default as OrderSummary } from './molecules/OrderSummary'
133
133
  export type { OrderSummaryProps } from './molecules/OrderSummary'
134
+ export { default as Popover } from './molecules/Popover'
135
+ export type { PopoverProps } from './molecules/Popover'
134
136
  export {
135
137
  default as ProductCard,
136
138
  ProductCardImage,
@@ -78,7 +78,7 @@ const AccordionButton = forwardRef<HTMLButtonElement, AccordionButtonProps>(
78
78
  ref={ref}
79
79
  id={button}
80
80
  variant="tertiary"
81
- data-fs-accordion-button
81
+ data-fs-accordion-button={indices.has(index) ? 'expanded' : 'collapsed'}
82
82
  aria-expanded={indices.has(index)}
83
83
  icon={indices.has(index) ? expandedIcon : collapsedIcon}
84
84
  iconPosition="right"
@@ -20,7 +20,8 @@ export type ModalChildrenProps = {
20
20
 
21
21
  type ModalChildrenFunction = (props: ModalChildrenProps) => ReactNode
22
22
 
23
- export interface ModalProps extends Omit<ModalContentProps, 'children'> {
23
+ export interface ModalProps
24
+ extends Omit<ModalContentProps, 'children' | 'onEntered'> {
24
25
  /**
25
26
  * ID to find this component in testing tools (e.g.: cypress, testing library, and jest).
26
27
  */
@@ -31,21 +32,29 @@ export interface ModalProps extends Omit<ModalContentProps, 'children'> {
31
32
  */
32
33
  'aria-labelledby'?: AriaAttributes['aria-label']
33
34
  /**
34
- * A boolean value that represents the state of the Modal
35
+ * A boolean value that represents the state of the Modal.
35
36
  */
36
37
  isOpen?: boolean
37
38
  /**
38
- * Event emitted when the modal is closed
39
+ * Event emitted when the modal is closed.
39
40
  */
40
41
  onDismiss?: () => void
41
42
  /**
42
- * Props forwarded to the `Overlay` component
43
+ * Callback function when the modal is opened.
44
+ */
45
+ onEntered?: () => void
46
+ /**
47
+ * Props forwarded to the `Overlay` component.
43
48
  */
44
49
  overlayProps?: OverlayProps
45
50
  /**
46
- * Children or function as a children
51
+ * Children or function as a children.
47
52
  */
48
53
  children: ModalChildrenFunction | ReactNode
54
+ /**
55
+ * Disable being closed using the Escape key.
56
+ */
57
+ disableEscapeKeyDown?: boolean
49
58
  }
50
59
 
51
60
  /*
@@ -60,13 +69,15 @@ const Modal = ({
60
69
  isOpen = true,
61
70
  onDismiss,
62
71
  overlayProps,
72
+ disableEscapeKeyDown = false,
73
+ onEntered,
63
74
  ...otherProps
64
75
  }: ModalProps) => {
65
76
  const { closeModal } = useUI()
66
77
  const { fade, fadeOut, fadeIn } = useFadeEffect()
67
78
 
68
79
  const handleBackdropClick = (event: MouseEvent) => {
69
- if (event.defaultPrevented) {
80
+ if (disableEscapeKeyDown || event.defaultPrevented) {
70
81
  return
71
82
  }
72
83
 
@@ -76,7 +87,11 @@ const Modal = ({
76
87
  }
77
88
 
78
89
  const handleBackdropKeyDown = (event: KeyboardEvent) => {
79
- if (event.key !== 'Escape' || event.defaultPrevented) {
90
+ if (
91
+ disableEscapeKeyDown ||
92
+ event.key !== 'Escape' ||
93
+ event.defaultPrevented
94
+ ) {
80
95
  return
81
96
  }
82
97
 
@@ -93,7 +108,13 @@ const Modal = ({
93
108
  {...overlayProps}
94
109
  >
95
110
  <ModalContent
96
- onTransitionEnd={() => fade === 'out' && closeModal()}
111
+ onTransitionEnd={() => {
112
+ if (fade === 'out') {
113
+ closeModal()
114
+ } else if (fade === 'in' && onEntered) {
115
+ onEntered()
116
+ }
117
+ }}
97
118
  data-fs-modal
98
119
  data-fs-modal-state={fade}
99
120
  testId={testId}
@@ -0,0 +1,209 @@
1
+ import React, {
2
+ forwardRef,
3
+ useCallback,
4
+ useEffect,
5
+ useRef,
6
+ useState,
7
+ type HTMLAttributes,
8
+ type KeyboardEvent,
9
+ type ReactNode,
10
+ type RefObject,
11
+ } from 'react'
12
+ import Icon from '../../atoms/Icon'
13
+ import IconButton from '../IconButton'
14
+
15
+ import { useOnClickOutside, useUI } from '../../hooks'
16
+
17
+ /**
18
+ * Specifies Popover position.
19
+ */
20
+ export type Side = 'bottom' | 'top'
21
+
22
+ /**
23
+ * Specifies tooltip alignment.
24
+ */
25
+ export type Alignment = 'start' | 'center' | 'end'
26
+
27
+ /**
28
+ * Combines side + alignment (e.g., "top-start").
29
+ */
30
+ export type Placement = `${Side}-${Alignment}`
31
+
32
+ export interface PopoverProps
33
+ extends Omit<HTMLAttributes<HTMLDivElement>, 'content'> {
34
+ /**
35
+ * The Popover header's title.
36
+ */
37
+ title?: string
38
+ /**
39
+ * Content of the Popover.
40
+ */
41
+ content: ReactNode
42
+ /**
43
+ * Defines the side or side-alignment (e.g., "bottom-start", "bottom-center") of the Popover.
44
+ */
45
+ placement?: Placement
46
+ /**
47
+ * If the Popover can be closed by a button.
48
+ */
49
+ dismissible?: boolean
50
+ /**
51
+ * Called when the Popover is dismissed.
52
+ */
53
+ onDismiss?: () => void
54
+ /**
55
+ * Callback when the Popover is fully rendered and positioned.
56
+ */
57
+ onEntered?: () => void
58
+ /**
59
+ * Close button aria-label.
60
+ */
61
+ closeButtonAriaLabel?: string
62
+ /**
63
+ * Controls whether the Popover is open.
64
+ */
65
+ isOpen: boolean
66
+ /**
67
+ * ID to find this component in testing tools (e.g.: cypress, testing library, and jest).
68
+ */
69
+ testId?: string
70
+ /**
71
+ * Offset value for top position (e.g.: 12).
72
+ * @default '8'
73
+ */
74
+ offsetTop?: number
75
+ /**
76
+ * Offset value for left position (e.g.: 12).
77
+ * @default '0'
78
+ */
79
+ offsetLeft?: number
80
+ /**
81
+ * Reference to the trigger element that opens the Popover.
82
+ */
83
+ triggerRef?: RefObject<HTMLElement>
84
+ }
85
+
86
+ const calculatePosition = (
87
+ rect: DOMRect,
88
+ placement: Placement,
89
+ offsetTop: number,
90
+ offsetLeft: number
91
+ ) => {
92
+ const { top, left, height } = rect
93
+
94
+ switch (true) {
95
+ case placement.startsWith('top'):
96
+ return {
97
+ top: top + height + window.scrollY - offsetTop,
98
+ left: left + window.scrollX + offsetLeft,
99
+ }
100
+ case placement.startsWith('bottom'):
101
+ return {
102
+ top: top + height + window.scrollY + offsetTop,
103
+ left: left + window.scrollX + offsetLeft,
104
+ }
105
+ default:
106
+ return { top: 0, left: 0 }
107
+ }
108
+ }
109
+
110
+ const Popover = forwardRef<HTMLDivElement, PopoverProps>(function Popover(
111
+ {
112
+ title,
113
+ content,
114
+ placement = 'bottom-start',
115
+ dismissible = false,
116
+ onDismiss,
117
+ isOpen,
118
+ triggerRef: propTriggerRef,
119
+ offsetTop = 8,
120
+ offsetLeft = 0,
121
+ closeButtonAriaLabel = 'Close Popover',
122
+ testId = 'fs-popover',
123
+ style,
124
+ onEntered,
125
+ ...otherProps
126
+ },
127
+ ref
128
+ ) {
129
+ // Use forwarded ref or internal ref for fallback
130
+ const popoverRef = ref || useRef<HTMLDivElement>(null)
131
+
132
+ const [popoverPosition, setPopoverPosition] = useState({ top: 0, left: 0 })
133
+ const { popover, closePopover } = useUI()
134
+
135
+ const contextTriggerRef = popover.triggerRef
136
+
137
+ // Use the propTriggerRef if provided, otherwise fallback to contextTriggerRef
138
+ const triggerRef = propTriggerRef || contextTriggerRef
139
+
140
+ useEffect(() => {
141
+ if (!isOpen || !triggerRef?.current) return
142
+
143
+ // Set the position according to the trigger element and placement
144
+ const rect = triggerRef.current.getBoundingClientRect()
145
+
146
+ setPopoverPosition(
147
+ calculatePosition(rect, placement, offsetTop, offsetLeft)
148
+ )
149
+
150
+ // Trigger the onEntered callback after positioning
151
+ if (onEntered) {
152
+ onEntered()
153
+ }
154
+ }, [isOpen, triggerRef, offsetTop, offsetLeft, placement])
155
+
156
+ const handleDismiss = useCallback(() => {
157
+ closePopover()
158
+ onDismiss?.()
159
+ }, [closePopover, onDismiss])
160
+
161
+ useOnClickOutside(
162
+ isOpen ? (popoverRef as RefObject<HTMLDivElement>) : undefined,
163
+ handleDismiss
164
+ )
165
+
166
+ const handleKeyDown = useCallback(
167
+ (event: KeyboardEvent<HTMLDivElement>) => {
168
+ if (event.key === 'Escape') {
169
+ handleDismiss()
170
+ }
171
+ },
172
+ [handleDismiss]
173
+ )
174
+
175
+ if (!isOpen) {
176
+ return null
177
+ }
178
+
179
+ return (
180
+ <div
181
+ data-fs-popover
182
+ role="dialog"
183
+ ref={popoverRef}
184
+ data-fs-popover-placement={placement}
185
+ onKeyDown={handleKeyDown}
186
+ data-testid={testId}
187
+ style={{ position: 'absolute', ...popoverPosition, ...style }}
188
+ {...otherProps}
189
+ >
190
+ <header data-fs-popover-header>
191
+ {title && <h3 data-fs-popover-header-title>{title}</h3>}
192
+ {dismissible && (
193
+ <IconButton
194
+ data-fs-popover-header-dismiss-button
195
+ size="small"
196
+ variant="tertiary"
197
+ icon={<Icon name="X" width={20} height={20} />}
198
+ aria-label={closeButtonAriaLabel}
199
+ onClick={handleDismiss}
200
+ />
201
+ )}
202
+ </header>
203
+ <div data-fs-popover-content>{content}</div>
204
+ <span data-fs-popover-indicator aria-hidden="true" />
205
+ </div>
206
+ )
207
+ })
208
+
209
+ export default Popover
@@ -0,0 +1,2 @@
1
+ export { default } from './Popover'
2
+ export type { PopoverProps } from './Popover'
@@ -1,11 +1,17 @@
1
- import type { HTMLAttributes, ReactNode } from 'react'
1
+ import type { HTMLAttributes, ReactNode, RefAttributes } from 'react'
2
2
  import React, { forwardRef } from 'react'
3
3
 
4
4
  import { Button } from '../../'
5
5
 
6
- export interface RegionBarProps extends HTMLAttributes<HTMLDivElement> {
6
+ export interface RegionBarProps
7
+ extends HTMLAttributes<HTMLDivElement>,
8
+ RefAttributes<HTMLDivElement> {
7
9
  /**
8
- * Postal code string to be display in the component
10
+ * City to be displayed in the component.
11
+ */
12
+ city?: string
13
+ /**
14
+ * Postal code string to be display in the component.
9
15
  */
10
16
  postalCode?: string
11
17
  /**
@@ -28,16 +34,23 @@ export interface RegionBarProps extends HTMLAttributes<HTMLDivElement> {
28
34
  * A React component that will be rendered as an icon.
29
35
  */
30
36
  buttonIcon?: ReactNode
37
+ /**
38
+ * Boolean to control whether postal code should be visible or not.
39
+ * @default true
40
+ */
41
+ shouldDisplayPostalCode?: boolean
31
42
  }
32
43
 
33
44
  const RegionBar = forwardRef<HTMLDivElement, RegionBarProps>(function RegionBar(
34
45
  {
46
+ city,
35
47
  postalCode,
36
48
  icon,
37
49
  label,
38
50
  editLabel,
39
51
  buttonIcon,
40
52
  onButtonClick,
53
+ shouldDisplayPostalCode = true,
41
54
  ...otherProps
42
55
  },
43
56
  ref
@@ -51,9 +64,12 @@ const RegionBar = forwardRef<HTMLDivElement, RegionBarProps>(function RegionBar(
51
64
  icon={buttonIcon}
52
65
  >
53
66
  {!!icon && icon}
54
- {postalCode ? (
67
+ {city && postalCode ? (
55
68
  <>
56
- <span data-fs-region-bar-postal-code>{postalCode}</span>
69
+ <span data-fs-region-bar-postal-code>
70
+ {city}
71
+ {shouldDisplayPostalCode && `, ${postalCode}`}
72
+ </span>
57
73
  {!!editLabel && <span data-fs-region-bar-cta>{editLabel}</span>}
58
74
  </>
59
75
  ) : (