@simplybusiness/mobius 4.4.6 → 4.5.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 (112) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/cjs/components/Drawer/Content.js +2 -9
  3. package/dist/cjs/components/Drawer/Content.js.map +1 -1
  4. package/dist/cjs/components/Drawer/Drawer.js +28 -134
  5. package/dist/cjs/components/Drawer/Drawer.js.map +1 -1
  6. package/dist/cjs/components/Drawer/DrawerContext.js +2 -2
  7. package/dist/cjs/components/Drawer/DrawerContext.js.map +1 -1
  8. package/dist/cjs/components/Drawer/Header.js +6 -2
  9. package/dist/cjs/components/Drawer/Header.js.map +1 -1
  10. package/dist/cjs/components/Drawer/index.js +12 -3
  11. package/dist/cjs/components/Drawer/index.js.map +1 -1
  12. package/dist/cjs/components/Drawer/useDrawer.js +2 -2
  13. package/dist/cjs/components/Drawer/useDrawer.js.map +1 -1
  14. package/dist/cjs/components/Modal/Content.js +2 -9
  15. package/dist/cjs/components/Modal/Content.js.map +1 -1
  16. package/dist/cjs/components/Modal/Header.js +6 -2
  17. package/dist/cjs/components/Modal/Header.js.map +1 -1
  18. package/dist/cjs/components/Modal/Modal.js +27 -133
  19. package/dist/cjs/components/Modal/Modal.js.map +1 -1
  20. package/dist/cjs/components/Modal/ModalContext.js +17 -0
  21. package/dist/cjs/components/Modal/ModalContext.js.map +1 -0
  22. package/dist/cjs/components/Modal/index.js +12 -3
  23. package/dist/cjs/components/Modal/index.js.map +1 -1
  24. package/dist/cjs/components/Modal/useModal.js +21 -0
  25. package/dist/cjs/components/Modal/useModal.js.map +1 -0
  26. package/dist/cjs/hooks/index.js +1 -0
  27. package/dist/cjs/hooks/index.js.map +1 -1
  28. package/dist/cjs/hooks/useDialog/index.js +20 -0
  29. package/dist/cjs/hooks/useDialog/index.js.map +1 -0
  30. package/dist/cjs/hooks/useDialog/useDialog.js +94 -0
  31. package/dist/cjs/hooks/useDialog/useDialog.js.map +1 -0
  32. package/dist/cjs/hooks/useDialogPolyfill/index.js +20 -0
  33. package/dist/cjs/hooks/useDialogPolyfill/index.js.map +1 -0
  34. package/dist/cjs/hooks/useDialogPolyfill/useDialogPolyfill.js +77 -0
  35. package/dist/cjs/hooks/useDialogPolyfill/useDialogPolyfill.js.map +1 -0
  36. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  37. package/dist/esm/components/Drawer/Content.js +3 -10
  38. package/dist/esm/components/Drawer/Content.js.map +1 -1
  39. package/dist/esm/components/Drawer/Drawer.js +30 -95
  40. package/dist/esm/components/Drawer/Drawer.js.map +1 -1
  41. package/dist/esm/components/Drawer/DrawerContext.js +2 -2
  42. package/dist/esm/components/Drawer/DrawerContext.js.map +1 -1
  43. package/dist/esm/components/Drawer/Header.js +6 -2
  44. package/dist/esm/components/Drawer/Header.js.map +1 -1
  45. package/dist/esm/components/Drawer/index.js +2 -1
  46. package/dist/esm/components/Drawer/index.js.map +1 -1
  47. package/dist/esm/components/Drawer/types.js.map +1 -1
  48. package/dist/esm/components/Drawer/useDrawer.js +2 -2
  49. package/dist/esm/components/Drawer/useDrawer.js.map +1 -1
  50. package/dist/esm/components/Modal/Content.js +3 -10
  51. package/dist/esm/components/Modal/Content.js.map +1 -1
  52. package/dist/esm/components/Modal/Header.js +6 -2
  53. package/dist/esm/components/Modal/Header.js.map +1 -1
  54. package/dist/esm/components/Modal/Modal.js +28 -93
  55. package/dist/esm/components/Modal/Modal.js.map +1 -1
  56. package/dist/esm/components/Modal/ModalContext.js +7 -0
  57. package/dist/esm/components/Modal/ModalContext.js.map +1 -0
  58. package/dist/esm/components/Modal/index.js +2 -1
  59. package/dist/esm/components/Modal/index.js.map +1 -1
  60. package/dist/esm/components/Modal/types.js.map +1 -1
  61. package/dist/esm/components/Modal/useModal.js +11 -0
  62. package/dist/esm/components/Modal/useModal.js.map +1 -0
  63. package/dist/esm/hooks/index.js +1 -0
  64. package/dist/esm/hooks/index.js.map +1 -1
  65. package/dist/esm/hooks/useDialog/index.js +3 -0
  66. package/dist/esm/hooks/useDialog/index.js.map +1 -0
  67. package/dist/esm/hooks/useDialog/useDialog.js +84 -0
  68. package/dist/esm/hooks/useDialog/useDialog.js.map +1 -0
  69. package/dist/esm/hooks/useDialogPolyfill/index.js +3 -0
  70. package/dist/esm/hooks/useDialogPolyfill/index.js.map +1 -0
  71. package/dist/esm/hooks/useDialogPolyfill/useDialogPolyfill.js +27 -0
  72. package/dist/esm/hooks/useDialogPolyfill/useDialogPolyfill.js.map +1 -0
  73. package/dist/types/components/Drawer/Content.d.ts +0 -2
  74. package/dist/types/components/Drawer/DrawerContext.d.ts +2 -0
  75. package/dist/types/components/Drawer/Header.d.ts +0 -2
  76. package/dist/types/components/Drawer/index.d.ts +4 -3
  77. package/dist/types/components/Drawer/types.d.ts +5 -1
  78. package/dist/types/components/Drawer/useDrawer.d.ts +4 -0
  79. package/dist/types/components/Modal/Content.d.ts +0 -2
  80. package/dist/types/components/Modal/Header.d.ts +0 -2
  81. package/dist/types/components/Modal/ModalContext.d.ts +2 -0
  82. package/dist/types/components/Modal/index.d.ts +4 -3
  83. package/dist/types/components/Modal/types.d.ts +5 -1
  84. package/dist/types/components/Modal/useModal.d.ts +4 -0
  85. package/dist/types/hooks/index.d.ts +1 -0
  86. package/dist/types/hooks/useDialog/index.d.ts +1 -0
  87. package/dist/types/hooks/useDialog/useDialog.d.ts +16 -0
  88. package/dist/types/hooks/useDialogPolyfill/index.d.ts +1 -0
  89. package/dist/types/hooks/useDialogPolyfill/useDialogPolyfill.d.ts +2 -0
  90. package/package.json +2 -1
  91. package/src/components/Drawer/Content.tsx +4 -26
  92. package/src/components/Drawer/Drawer.mdx +62 -0
  93. package/src/components/Drawer/Drawer.stories.tsx +1 -1
  94. package/src/components/Drawer/Drawer.tsx +31 -113
  95. package/src/components/Drawer/DrawerContext.tsx +7 -0
  96. package/src/components/Drawer/Header.tsx +19 -20
  97. package/src/components/Drawer/index.tsx +4 -2
  98. package/src/components/Drawer/types.ts +6 -1
  99. package/src/components/Drawer/useDrawer.ts +8 -0
  100. package/src/components/Modal/Content.tsx +4 -26
  101. package/src/components/Modal/Header.tsx +19 -20
  102. package/src/components/Modal/Modal.mdx +57 -0
  103. package/src/components/Modal/Modal.tsx +29 -111
  104. package/src/components/Modal/ModalContext.tsx +7 -0
  105. package/src/components/Modal/index.tsx +4 -2
  106. package/src/components/Modal/types.ts +6 -1
  107. package/src/components/Modal/useModal.ts +8 -0
  108. package/src/hooks/index.tsx +1 -0
  109. package/src/hooks/useDialog/index.ts +1 -0
  110. package/src/hooks/useDialog/useDialog.ts +98 -0
  111. package/src/hooks/useDialogPolyfill/index.ts +1 -0
  112. package/src/hooks/useDialogPolyfill/useDialogPolyfill.ts +32 -0
@@ -1,27 +1,17 @@
1
1
  "use client";
2
2
 
3
3
  import classNames from "classnames/dedupe";
4
- import {
5
- Children,
6
- Ref,
7
- SyntheticEvent,
8
- cloneElement,
9
- forwardRef,
10
- isValidElement,
11
- useCallback,
12
- useEffect,
13
- useRef,
14
- } from "react";
15
- import { useBodyScrollLock } from "../../hooks/useBodyScrollLock";
16
- import { supportsDialog } from "../../utils/polyfill-tests";
4
+ import { Ref, forwardRef, useMemo, useRef } from "react";
17
5
  import { VisuallyHidden } from "../VisuallyHidden";
18
6
  import { DrawerProps } from "./types";
19
- import { mergeRefs } from "../../utils";
7
+ import { mergeRefs, supportsDialog } from "../../utils";
8
+ import { DrawerContext } from "./DrawerContext";
9
+ import { useDialog } from "../../hooks";
20
10
 
21
11
  export type DialogElementType = HTMLDialogElement;
22
12
  export type DialogRef = Ref<DialogElementType>;
23
13
 
24
- const TRANSITION_CLASS_NAME = "--transition";
14
+ const TRANSITION_CSS_VARIABLE = "--drawer-transition-duration";
25
15
 
26
16
  const Drawer = forwardRef((props: DrawerProps, ref: DialogRef) => {
27
17
  const {
@@ -34,122 +24,50 @@ const Drawer = forwardRef((props: DrawerProps, ref: DialogRef) => {
34
24
  onClose,
35
25
  children,
36
26
  } = props;
37
- const hasOpened = useRef<boolean>(false);
38
- const modalRef = useRef<HTMLDialogElement | null>(null);
39
- const hasDialogSupport = supportsDialog();
40
-
41
- // Fire onOpen once
42
- if (onOpen && !hasOpened.current) {
43
- onOpen();
44
- hasOpened.current = true;
45
- }
46
-
47
- useBodyScrollLock({ enabled: isOpen });
48
-
49
- // Add close handler, to enable closing transitions
50
- const handleClose = useCallback(
51
- (event?: SyntheticEvent<HTMLElement, Event>) => {
52
- // `transitionend` may be called from another component but `event` may be undefined.
53
- // This fixes an issue where closing `<Accordion>` inside `<Drawer>` will close both
54
- if (!event) return;
55
-
56
- if (event) {
57
- event.preventDefault();
58
- event.stopPropagation();
59
- }
60
-
61
- // Name the callback function, so we can add and remove event listener
62
- const transitionCallback = (e: Event) => {
63
- // Close drawer only if the transition is on the dialog element
64
- // As it can be on a child element (ie `<Button>` inside the drawer)
65
- if (e.target === modalRef.current) {
66
- // eslint-disable-next-line @typescript-eslint/no-use-before-define
67
- doClose();
68
- }
69
- };
70
-
71
- const doClose = () => {
72
- modalRef.current?.close();
73
- onClose?.();
74
- modalRef.current?.removeEventListener(
75
- "transitionend",
76
- transitionCallback,
77
- );
78
- };
79
-
80
- // Delay close to allow backdrop exit transition
81
- if (hasDialogSupport) {
82
- modalRef.current?.classList.remove(TRANSITION_CLASS_NAME);
83
- modalRef.current?.addEventListener("transitionend", transitionCallback);
84
- } else {
85
- doClose();
86
- }
27
+ const shouldTransition = supportsDialog();
28
+ const dialogRef = useRef<HTMLDialogElement | null>(null);
29
+ const { close } = useDialog({
30
+ ref: dialogRef,
31
+ isOpen,
32
+ onOpen,
33
+ onClose,
34
+ transition: {
35
+ isEnabled: true,
36
+ CSSVariable: TRANSITION_CSS_VARIABLE,
87
37
  },
88
- [onClose, hasDialogSupport],
89
- );
38
+ });
90
39
 
91
- const modalClasses = classNames(
40
+ const dialogClasses = classNames(
92
41
  "mobius",
93
42
  "mobius/Drawer",
94
43
  `--${direction}`,
95
44
  className,
96
45
  {
97
- "--should-transition": hasDialogSupport,
46
+ "--should-transition": shouldTransition,
98
47
  },
99
48
  );
100
49
 
101
- // Add polyfill for HTML Dialog in old browsers
102
- useEffect(() => {
103
- async function toggleModal() {
104
- if (
105
- !hasDialogSupport &&
106
- typeof window !== "undefined" &&
107
- modalRef.current !== null
108
- ) {
109
- // eslint-disable-next-line import/no-extraneous-dependencies
110
- const { default: dialogPolyfill } = await import("dialog-polyfill");
111
- try {
112
- dialogPolyfill.registerDialog(modalRef.current);
113
- } catch (error) {
114
- // In iOS 15 <= 15.2 this intermittently fails with
115
- // TypeError: null is not an object (evaluating 'element.showModal')
116
- // Checking showModal presence through hasOwnProperty is falsy natively, truthy with polyfill 🤷🏼‍♂️
117
- console.error("Failed to load dialog-polyfill", error);
118
- }
119
- }
120
-
121
- if (isOpen && !modalRef.current?.open) {
122
- modalRef.current?.showModal();
123
- modalRef.current?.classList.add(TRANSITION_CLASS_NAME);
124
- onOpen?.();
125
- } else if (!isOpen && modalRef.current?.open) {
126
- handleClose();
127
- }
128
- }
129
-
130
- toggleModal();
131
- }, [isOpen, onOpen, handleClose, hasDialogSupport]);
50
+ const contextValue = useMemo(
51
+ () => ({
52
+ onClose: close,
53
+ closeLabel,
54
+ }),
55
+ [close, closeLabel],
56
+ );
132
57
 
133
58
  return (
134
59
  <dialog
135
- ref={mergeRefs([modalRef, ref])}
136
- onCancel={handleClose}
137
- className={modalClasses}
60
+ ref={mergeRefs([dialogRef, ref])}
61
+ onCancel={close}
62
+ className={dialogClasses}
138
63
  aria-describedby="screen-reader-announce"
139
64
  >
140
65
  <VisuallyHidden>
141
66
  <div id="screen-reader-announce">{announce}</div>
142
67
  </VisuallyHidden>
143
- {Children.map(children, child => {
144
- if (isValidElement(child)) {
145
- return cloneElement(child, {
146
- onClose: handleClose,
147
- closeLabel,
148
- } as any);
149
- }
150
-
151
- return child;
152
- })}
68
+ <DrawerContext.Provider value={contextValue}>
69
+ {children}
70
+ </DrawerContext.Provider>
153
71
  </dialog>
154
72
  );
155
73
  });
@@ -0,0 +1,7 @@
1
+ import { createContext } from "react";
2
+ import { DrawerContextProps } from "./types";
3
+
4
+ export const DrawerContext = createContext<DrawerContextProps>({
5
+ onClose: () => {},
6
+ closeLabel: undefined,
7
+ });
@@ -4,6 +4,7 @@ import { cross } from "@simplybusiness/icons";
4
4
  import { DOMProps } from "../../types/dom";
5
5
  import { Button } from "../Button";
6
6
  import { Icon } from "../Icon";
7
+ import { useDrawer } from "./useDrawer";
7
8
 
8
9
  export type HeaderElementType = HTMLDivElement;
9
10
  export type HeaderRef = Ref<HTMLElement>;
@@ -11,28 +12,26 @@ export type HeaderRef = Ref<HTMLElement>;
11
12
  export interface HeaderProps
12
13
  extends DOMProps,
13
14
  RefAttributes<HeaderElementType>,
14
- PropsWithChildren {
15
- onClose?: () => void;
16
- closeLabel?: string;
17
- }
15
+ PropsWithChildren {}
18
16
 
19
17
  const Header = forwardRef(
20
- (
21
- { children, onClose, closeLabel, ...otherProps }: HeaderProps,
22
- ref: HeaderRef,
23
- ) => (
24
- <header ref={ref} {...otherProps} className="mobius/DrawerHeader">
25
- {children}
26
- <Button
27
- aria-label="Close"
28
- variant="basic"
29
- onPress={onClose}
30
- className="mobius/DrawerClose"
31
- >
32
- <Icon icon={cross} /> {closeLabel}
33
- </Button>
34
- </header>
35
- ),
18
+ ({ children, ...otherProps }: HeaderProps, ref: HeaderRef) => {
19
+ const { onClose, closeLabel } = useDrawer();
20
+
21
+ return (
22
+ <header ref={ref} {...otherProps} className="mobius/DrawerHeader">
23
+ {children}
24
+ <Button
25
+ aria-label="Close"
26
+ variant="basic"
27
+ onPress={onClose}
28
+ className="mobius/DrawerClose"
29
+ >
30
+ <Icon icon={cross} /> {closeLabel}
31
+ </Button>
32
+ </header>
33
+ );
34
+ },
36
35
  );
37
36
 
38
37
  Header.displayName = "Header";
@@ -4,7 +4,8 @@ import {
4
4
  ContentProps as DrawerContentProps,
5
5
  } from "./Content";
6
6
  import { Drawer as DrawerComponent } from "./Drawer";
7
- import { DrawerProps } from "./types";
7
+ import { DrawerProps, DrawerContextProps } from "./types";
8
+ import { useDrawer } from "./useDrawer";
8
9
  import {
9
10
  HeaderElementType as DrawerHeaderElementType,
10
11
  HeaderProps as DrawerHeaderProps,
@@ -17,9 +18,10 @@ const Drawer = Object.assign(DrawerComponent, {
17
18
  });
18
19
 
19
20
  Drawer.displayName = "Drawer";
20
- export { Drawer };
21
+ export { Drawer, useDrawer };
21
22
 
22
23
  export type {
24
+ DrawerContextProps,
23
25
  DrawerContentDivElementType,
24
26
  DrawerContentProps,
25
27
  DrawerHeaderElementType,
@@ -1,4 +1,4 @@
1
- import { ReactNode } from "react";
1
+ import { ReactNode, SyntheticEvent } from "react";
2
2
 
3
3
  export interface DrawerProps {
4
4
  isOpen: boolean;
@@ -16,3 +16,8 @@ export interface DrawerProps {
16
16
  onClose?: () => void;
17
17
  children?: ReactNode;
18
18
  }
19
+
20
+ export type DrawerContextProps = {
21
+ onClose: (event?: SyntheticEvent<HTMLElement, Event>) => void;
22
+ closeLabel: string | undefined;
23
+ };
@@ -0,0 +1,8 @@
1
+ import { useContext } from "react";
2
+ import { DrawerContext } from "./DrawerContext";
3
+
4
+ export const useDrawer = () => {
5
+ const { onClose, closeLabel } = useContext(DrawerContext);
6
+
7
+ return { onClose, closeLabel };
8
+ };
@@ -1,12 +1,4 @@
1
- import {
2
- Children,
3
- PropsWithChildren,
4
- Ref,
5
- RefAttributes,
6
- cloneElement,
7
- forwardRef,
8
- isValidElement,
9
- } from "react";
1
+ import { PropsWithChildren, Ref, RefAttributes, forwardRef } from "react";
10
2
  import { DOMProps } from "../../types/dom";
11
3
 
12
4
  export type DivRef = Ref<HTMLDivElement>;
@@ -14,26 +6,12 @@ export type DivElementType = HTMLDivElement;
14
6
  export interface ContentProps
15
7
  extends DOMProps,
16
8
  RefAttributes<DivElementType>,
17
- PropsWithChildren {
18
- onClose?: () => void;
19
- closeLabel?: string;
20
- }
9
+ PropsWithChildren {}
21
10
 
22
11
  const Content = forwardRef(
23
- (
24
- { children, onClose, closeLabel: _, ...otherProps }: ContentProps,
25
- ref: DivRef,
26
- ) => (
12
+ ({ children, ...otherProps }: ContentProps, ref: DivRef) => (
27
13
  <div ref={ref} {...otherProps} className="mobius/ModalContent">
28
- {Children.map(children, child => {
29
- if (isValidElement(child)) {
30
- return cloneElement(child, {
31
- onClose,
32
- } as any);
33
- }
34
-
35
- return child;
36
- })}
14
+ {children}
37
15
  </div>
38
16
  ),
39
17
  );
@@ -4,6 +4,7 @@ import { cross } from "@simplybusiness/icons";
4
4
  import { DOMProps } from "../../types/dom";
5
5
  import { Button } from "../Button";
6
6
  import { Icon } from "../Icon";
7
+ import { useModal } from "./useModal";
7
8
 
8
9
  export type HeaderElementType = HTMLDivElement;
9
10
  export type HeaderRef = Ref<HTMLElement>;
@@ -11,28 +12,26 @@ export type HeaderRef = Ref<HTMLElement>;
11
12
  export interface HeaderProps
12
13
  extends DOMProps,
13
14
  RefAttributes<HeaderElementType>,
14
- PropsWithChildren {
15
- onClose?: () => void;
16
- closeLabel?: string;
17
- }
15
+ PropsWithChildren {}
18
16
 
19
17
  const Header = forwardRef(
20
- (
21
- { children, onClose, closeLabel, ...otherProps }: HeaderProps,
22
- ref: HeaderRef,
23
- ) => (
24
- <header ref={ref} {...otherProps} className="mobius/ModalHeader">
25
- {children}
26
- <Button
27
- aria-label="Close"
28
- variant="basic"
29
- onPress={onClose}
30
- className="mobius/ModalClose"
31
- >
32
- <Icon icon={cross} /> {closeLabel}
33
- </Button>
34
- </header>
35
- ),
18
+ ({ children, ...otherProps }: HeaderProps, ref: HeaderRef) => {
19
+ const { onClose, closeLabel } = useModal();
20
+
21
+ return (
22
+ <header ref={ref} {...otherProps} className="mobius/ModalHeader">
23
+ {children}
24
+ <Button
25
+ aria-label="Close"
26
+ variant="basic"
27
+ onPress={onClose}
28
+ className="mobius/ModalClose"
29
+ >
30
+ <Icon icon={cross} /> {closeLabel}
31
+ </Button>
32
+ </header>
33
+ );
34
+ },
36
35
  );
37
36
 
38
37
  Header.displayName = "Header";
@@ -116,6 +116,63 @@ const WithAnimationDemo = () => {
116
116
  };
117
117
  ```
118
118
 
119
+ ### Closing Modal from a child component
120
+
121
+ As of `@simplybusiness/mobius` version 4.5.0 `Modal` component can be closed from a child component using the `useModal()` hook.
122
+
123
+ The hook provides `onClose` method and `closeLabel` string that can be used to close the modal.
124
+
125
+ `onClose` method will ensure that the modal is closed using its CSS transition, as well as `onClose` prop firing, if provided.
126
+
127
+ If you previously used `onClose` and/or `closeLabel` props provided by default to all children props, you will need to replace these with `useModal()`.
128
+
129
+ Parent component for both old and new implementations remains the same:
130
+
131
+ ```jsx
132
+ import { Modal, Button } from "@simplybusiness/mobius";
133
+
134
+ const ParentComponent = () => {
135
+ const [open, setOpen] = useState(false);
136
+
137
+ return (
138
+ <>
139
+ <Button onClick={() => setOpen(true)}>Open</Button>
140
+
141
+ <Modal isOpen={open} onClose={() => setOpen(false)} animation="slideUp">
142
+ <Modal.Header>Title</Modal.Header>
143
+ <Modal.Content>Content</Modal.Content>
144
+ </Modal>
145
+ </>
146
+ );
147
+ };
148
+ ```
149
+
150
+ Old implementation for child component:
151
+
152
+ ```jsx
153
+ import { Button } from "@simplybusiness/mobius";
154
+
155
+ const ChildComponent = ({ onClose, closeLabel }) => (
156
+ <Button onClick={onClose}>{closeLabel}</Button>
157
+ );
158
+ ```
159
+
160
+ New implementation for child component:
161
+
162
+ ```jsx
163
+ import { useModal, Button } from "@simplybusiness/mobius";
164
+
165
+ const ChildComponent = () => {
166
+ const { onClose, closeLabel } = useModal();
167
+
168
+ return <Button onClick={onClose}>{closeLabel}</Button>;
169
+ };
170
+ ```
171
+
172
+ The motivation behind this change is to provide a more consistent and predictable API for the `Modal` component.
173
+
174
+ This particularly applies to components that already have props named `onClose` and `closeLabel`, which may lead to unexpected side effects.
175
+
119
176
  ### Nested Modals
120
177
 
121
178
  <Story of={ModalStories.WithNested} />
@@ -1,26 +1,17 @@
1
1
  "use client";
2
2
 
3
3
  import classNames from "classnames/dedupe";
4
- import {
5
- Children,
6
- Ref,
7
- SyntheticEvent,
8
- cloneElement,
9
- forwardRef,
10
- isValidElement,
11
- useCallback,
12
- useEffect,
13
- useRef,
14
- } from "react";
15
- import { useBodyScrollLock } from "../../hooks/useBodyScrollLock";
4
+ import { Ref, forwardRef, useMemo, useRef } from "react";
16
5
  import { supportsDialog } from "../../utils/polyfill-tests";
17
6
  import { ModalProps } from "./types";
18
7
  import { mergeRefs } from "../../utils";
8
+ import { ModalContext } from "./ModalContext";
9
+ import { useDialog } from "../../hooks";
19
10
 
20
11
  export type ModalElementType = HTMLDialogElement;
21
12
  export type ModalRef = Ref<ModalElementType>;
22
13
 
23
- const TRANSITION_CLASS_NAME = "--transition";
14
+ const TRANSITION_CSS_VARIABLE = "--modal-transition-duration";
24
15
 
25
16
  const Modal = forwardRef((props: ModalProps, ref: ModalRef) => {
26
17
  const {
@@ -56,66 +47,25 @@ const Modal = forwardRef((props: ModalProps, ref: ModalRef) => {
56
47
  }
57
48
  }
58
49
 
59
- const hasOpened = useRef<boolean>(false);
60
- const modalRef = useRef<HTMLDialogElement | null>(null);
61
- const hasDialogSupport = supportsDialog();
62
-
63
- // Fire onOpen once
64
- if (onOpen && !hasOpened.current) {
65
- onOpen();
66
- hasOpened.current = true;
67
- }
68
-
69
- useBodyScrollLock({ enabled: isOpen });
70
-
71
- // Add close handler, to enable closing animations
72
- const handleClose = useCallback(
73
- (event?: SyntheticEvent<HTMLElement, Event>) => {
74
- // `transitionend` may be called from another component but `event` may be undefined.
75
- // This fixes an issue where closing `<Accordion>` inside `<Modal>` will close both
76
- if (!event) return;
77
-
78
- if (event) {
79
- event.preventDefault();
80
- event.stopPropagation();
81
- }
82
-
83
- // Name the callback function, so we can add and remove event listener
84
- const transitionCallback = (e: Event) => {
85
- // Close modal only if the transition is on the dialog element
86
- // As it can be on a child element (ie `<Button>` inside the drawer)
87
- if (e.target === modalRef.current) {
88
- // eslint-disable-next-line @typescript-eslint/no-use-before-define
89
- doClose();
90
- }
91
- };
92
-
93
- const doClose = () => {
94
- modalRef.current?.close();
95
- onClose?.();
96
- modalRef.current?.removeEventListener(
97
- "transitionend",
98
- transitionCallback,
99
- );
100
- };
101
-
102
- // Delay close to allow backdrop exit transition
103
- if (hasDialogSupport && animation) {
104
- modalRef.current?.classList.remove(TRANSITION_CLASS_NAME);
105
- modalRef.current?.addEventListener("transitionend", transitionCallback);
106
- } else {
107
- doClose();
108
- }
50
+ const shouldTransition = supportsDialog();
51
+ const dialogRef = useRef<HTMLDialogElement | null>(null);
52
+ const { close } = useDialog({
53
+ ref: dialogRef,
54
+ isOpen,
55
+ onOpen,
56
+ onClose,
57
+ transition: {
58
+ isEnabled: !!animation,
59
+ CSSVariable: TRANSITION_CSS_VARIABLE,
109
60
  },
110
- [onClose, hasDialogSupport, animation],
111
- );
61
+ });
112
62
 
113
63
  const modalClasses = classNames(
114
64
  "mobius",
115
65
  "mobius/Modal",
116
66
  {
117
- "--no-dialog-support": !hasDialogSupport, // This class is used to correctly position modal in x/y middle on iOS <= 15.2
118
- "--should-transition": hasDialogSupport && animation,
67
+ "--no-dialog-support": !shouldTransition, // This class is used to correctly position modal in x/y middle on iOS <= 15.2
68
+ "--should-transition": shouldTransition && animation,
119
69
  "--slide-up": animation === "slideUp",
120
70
  "--fade": animation === "fade",
121
71
  "--is-fullscreen": isFullScreen,
@@ -123,55 +73,23 @@ const Modal = forwardRef((props: ModalProps, ref: ModalRef) => {
123
73
  className,
124
74
  );
125
75
 
126
- // Add polyfill for HTML Dialog in old browsers
127
- useEffect(() => {
128
- async function toggleModal() {
129
- if (
130
- !hasDialogSupport &&
131
- typeof window !== "undefined" &&
132
- modalRef.current !== null
133
- ) {
134
- // eslint-disable-next-line import/no-extraneous-dependencies
135
- const { default: dialogPolyfill } = await import("dialog-polyfill");
136
-
137
- try {
138
- dialogPolyfill.registerDialog(modalRef.current);
139
- } catch (error) {
140
- // In iOS 15 <= 15.2 this intermittently fails with
141
- // TypeError: null is not an object (evaluating 'element.showModal')
142
- // Checking showModal presence through hasOwnProperty is falsy natively, truthy with polyfill 🤷🏼‍♂️
143
- console.error("Failed to load dialog-polyfill", error);
144
- }
145
- }
146
-
147
- if (isOpen && !modalRef.current?.open) {
148
- modalRef.current?.showModal();
149
- modalRef.current?.classList.add(TRANSITION_CLASS_NAME);
150
- onOpen?.();
151
- } else if (!isOpen && modalRef.current?.open) {
152
- handleClose();
153
- }
154
- }
155
-
156
- toggleModal();
157
- }, [isOpen, onOpen, handleClose, hasDialogSupport]);
76
+ const contextValue = useMemo(
77
+ () => ({
78
+ onClose: close,
79
+ closeLabel,
80
+ }),
81
+ [close, closeLabel],
82
+ );
158
83
 
159
84
  return (
160
85
  <dialog
161
- ref={mergeRefs([modalRef, ref])}
162
- onCancel={handleClose}
86
+ ref={mergeRefs([dialogRef, ref])}
87
+ onCancel={close}
163
88
  className={modalClasses}
164
89
  >
165
- {Children.map(children, child => {
166
- if (isValidElement(child)) {
167
- return cloneElement(child, {
168
- onClose: handleClose,
169
- closeLabel,
170
- } as any);
171
- }
172
-
173
- return child;
174
- })}
90
+ <ModalContext.Provider value={contextValue}>
91
+ {children}
92
+ </ModalContext.Provider>
175
93
  </dialog>
176
94
  );
177
95
  });
@@ -0,0 +1,7 @@
1
+ import { createContext } from "react";
2
+ import { ModalContextProps } from "./types";
3
+
4
+ export const ModalContext = createContext<ModalContextProps>({
5
+ onClose: () => {},
6
+ closeLabel: undefined,
7
+ });
@@ -9,7 +9,8 @@ import {
9
9
  HeaderProps as ModalHeaderProps,
10
10
  } from "./Header";
11
11
  import { Modal as ModalComponent } from "./Modal";
12
- import { ModalProps } from "./types";
12
+ import { ModalProps, ModalContextProps } from "./types";
13
+ import { useModal } from "./useModal";
13
14
 
14
15
  const Modal = Object.assign(ModalComponent, {
15
16
  Header,
@@ -17,9 +18,10 @@ const Modal = Object.assign(ModalComponent, {
17
18
  });
18
19
 
19
20
  Modal.displayName = "Modal";
20
- export { Modal };
21
+ export { Modal, useModal };
21
22
 
22
23
  export type {
24
+ ModalContextProps,
23
25
  ModalContentDivElementType,
24
26
  ModalContentProps,
25
27
  ModalHeaderElementType,
@@ -1,4 +1,4 @@
1
- import { ReactNode } from "react";
1
+ import { ReactNode, SyntheticEvent } from "react";
2
2
 
3
3
  export interface ModalProps {
4
4
  isOpen: boolean;
@@ -30,3 +30,8 @@ export interface ModalProps {
30
30
  */
31
31
  parentSelector?: () => HTMLElement;
32
32
  }
33
+
34
+ export type ModalContextProps = {
35
+ onClose: (event?: SyntheticEvent<HTMLElement, Event>) => void;
36
+ closeLabel: string | undefined;
37
+ };