@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.
- package/CHANGELOG.md +12 -0
- package/dist/cjs/components/Drawer/Content.js +2 -9
- package/dist/cjs/components/Drawer/Content.js.map +1 -1
- package/dist/cjs/components/Drawer/Drawer.js +28 -134
- package/dist/cjs/components/Drawer/Drawer.js.map +1 -1
- package/dist/cjs/components/Drawer/DrawerContext.js +2 -2
- package/dist/cjs/components/Drawer/DrawerContext.js.map +1 -1
- package/dist/cjs/components/Drawer/Header.js +6 -2
- package/dist/cjs/components/Drawer/Header.js.map +1 -1
- package/dist/cjs/components/Drawer/index.js +12 -3
- package/dist/cjs/components/Drawer/index.js.map +1 -1
- package/dist/cjs/components/Drawer/useDrawer.js +2 -2
- package/dist/cjs/components/Drawer/useDrawer.js.map +1 -1
- package/dist/cjs/components/Modal/Content.js +2 -9
- package/dist/cjs/components/Modal/Content.js.map +1 -1
- package/dist/cjs/components/Modal/Header.js +6 -2
- package/dist/cjs/components/Modal/Header.js.map +1 -1
- package/dist/cjs/components/Modal/Modal.js +27 -133
- package/dist/cjs/components/Modal/Modal.js.map +1 -1
- package/dist/cjs/components/Modal/ModalContext.js +17 -0
- package/dist/cjs/components/Modal/ModalContext.js.map +1 -0
- package/dist/cjs/components/Modal/index.js +12 -3
- package/dist/cjs/components/Modal/index.js.map +1 -1
- package/dist/cjs/components/Modal/useModal.js +21 -0
- package/dist/cjs/components/Modal/useModal.js.map +1 -0
- package/dist/cjs/hooks/index.js +1 -0
- package/dist/cjs/hooks/index.js.map +1 -1
- package/dist/cjs/hooks/useDialog/index.js +20 -0
- package/dist/cjs/hooks/useDialog/index.js.map +1 -0
- package/dist/cjs/hooks/useDialog/useDialog.js +94 -0
- package/dist/cjs/hooks/useDialog/useDialog.js.map +1 -0
- package/dist/cjs/hooks/useDialogPolyfill/index.js +20 -0
- package/dist/cjs/hooks/useDialogPolyfill/index.js.map +1 -0
- package/dist/cjs/hooks/useDialogPolyfill/useDialogPolyfill.js +77 -0
- package/dist/cjs/hooks/useDialogPolyfill/useDialogPolyfill.js.map +1 -0
- package/dist/cjs/tsconfig.tsbuildinfo +1 -1
- package/dist/esm/components/Drawer/Content.js +3 -10
- package/dist/esm/components/Drawer/Content.js.map +1 -1
- package/dist/esm/components/Drawer/Drawer.js +30 -95
- package/dist/esm/components/Drawer/Drawer.js.map +1 -1
- package/dist/esm/components/Drawer/DrawerContext.js +2 -2
- package/dist/esm/components/Drawer/DrawerContext.js.map +1 -1
- package/dist/esm/components/Drawer/Header.js +6 -2
- package/dist/esm/components/Drawer/Header.js.map +1 -1
- package/dist/esm/components/Drawer/index.js +2 -1
- package/dist/esm/components/Drawer/index.js.map +1 -1
- package/dist/esm/components/Drawer/types.js.map +1 -1
- package/dist/esm/components/Drawer/useDrawer.js +2 -2
- package/dist/esm/components/Drawer/useDrawer.js.map +1 -1
- package/dist/esm/components/Modal/Content.js +3 -10
- package/dist/esm/components/Modal/Content.js.map +1 -1
- package/dist/esm/components/Modal/Header.js +6 -2
- package/dist/esm/components/Modal/Header.js.map +1 -1
- package/dist/esm/components/Modal/Modal.js +28 -93
- package/dist/esm/components/Modal/Modal.js.map +1 -1
- package/dist/esm/components/Modal/ModalContext.js +7 -0
- package/dist/esm/components/Modal/ModalContext.js.map +1 -0
- package/dist/esm/components/Modal/index.js +2 -1
- package/dist/esm/components/Modal/index.js.map +1 -1
- package/dist/esm/components/Modal/types.js.map +1 -1
- package/dist/esm/components/Modal/useModal.js +11 -0
- package/dist/esm/components/Modal/useModal.js.map +1 -0
- package/dist/esm/hooks/index.js +1 -0
- package/dist/esm/hooks/index.js.map +1 -1
- package/dist/esm/hooks/useDialog/index.js +3 -0
- package/dist/esm/hooks/useDialog/index.js.map +1 -0
- package/dist/esm/hooks/useDialog/useDialog.js +84 -0
- package/dist/esm/hooks/useDialog/useDialog.js.map +1 -0
- package/dist/esm/hooks/useDialogPolyfill/index.js +3 -0
- package/dist/esm/hooks/useDialogPolyfill/index.js.map +1 -0
- package/dist/esm/hooks/useDialogPolyfill/useDialogPolyfill.js +27 -0
- package/dist/esm/hooks/useDialogPolyfill/useDialogPolyfill.js.map +1 -0
- package/dist/types/components/Drawer/Content.d.ts +0 -2
- package/dist/types/components/Drawer/DrawerContext.d.ts +2 -0
- package/dist/types/components/Drawer/Header.d.ts +0 -2
- package/dist/types/components/Drawer/index.d.ts +4 -3
- package/dist/types/components/Drawer/types.d.ts +5 -1
- package/dist/types/components/Drawer/useDrawer.d.ts +4 -0
- package/dist/types/components/Modal/Content.d.ts +0 -2
- package/dist/types/components/Modal/Header.d.ts +0 -2
- package/dist/types/components/Modal/ModalContext.d.ts +2 -0
- package/dist/types/components/Modal/index.d.ts +4 -3
- package/dist/types/components/Modal/types.d.ts +5 -1
- package/dist/types/components/Modal/useModal.d.ts +4 -0
- package/dist/types/hooks/index.d.ts +1 -0
- package/dist/types/hooks/useDialog/index.d.ts +1 -0
- package/dist/types/hooks/useDialog/useDialog.d.ts +16 -0
- package/dist/types/hooks/useDialogPolyfill/index.d.ts +1 -0
- package/dist/types/hooks/useDialogPolyfill/useDialogPolyfill.d.ts +2 -0
- package/package.json +2 -1
- package/src/components/Drawer/Content.tsx +4 -26
- package/src/components/Drawer/Drawer.mdx +62 -0
- package/src/components/Drawer/Drawer.stories.tsx +1 -1
- package/src/components/Drawer/Drawer.tsx +31 -113
- package/src/components/Drawer/DrawerContext.tsx +7 -0
- package/src/components/Drawer/Header.tsx +19 -20
- package/src/components/Drawer/index.tsx +4 -2
- package/src/components/Drawer/types.ts +6 -1
- package/src/components/Drawer/useDrawer.ts +8 -0
- package/src/components/Modal/Content.tsx +4 -26
- package/src/components/Modal/Header.tsx +19 -20
- package/src/components/Modal/Modal.mdx +57 -0
- package/src/components/Modal/Modal.tsx +29 -111
- package/src/components/Modal/ModalContext.tsx +7 -0
- package/src/components/Modal/index.tsx +4 -2
- package/src/components/Modal/types.ts +6 -1
- package/src/components/Modal/useModal.ts +8 -0
- package/src/hooks/index.tsx +1 -0
- package/src/hooks/useDialog/index.ts +1 -0
- package/src/hooks/useDialog/useDialog.ts +98 -0
- package/src/hooks/useDialogPolyfill/index.ts +1 -0
- 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
|
|
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
|
|
38
|
-
const
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
89
|
-
);
|
|
38
|
+
});
|
|
90
39
|
|
|
91
|
-
const
|
|
40
|
+
const dialogClasses = classNames(
|
|
92
41
|
"mobius",
|
|
93
42
|
"mobius/Drawer",
|
|
94
43
|
`--${direction}`,
|
|
95
44
|
className,
|
|
96
45
|
{
|
|
97
|
-
"--should-transition":
|
|
46
|
+
"--should-transition": shouldTransition,
|
|
98
47
|
},
|
|
99
48
|
);
|
|
100
49
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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([
|
|
136
|
-
onCancel={
|
|
137
|
-
className={
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
});
|
|
@@ -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
|
-
{
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
+
};
|
|
@@ -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
|
-
{
|
|
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
|
-
{
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
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
|
|
60
|
-
const
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
111
|
-
);
|
|
61
|
+
});
|
|
112
62
|
|
|
113
63
|
const modalClasses = classNames(
|
|
114
64
|
"mobius",
|
|
115
65
|
"mobius/Modal",
|
|
116
66
|
{
|
|
117
|
-
"--no-dialog-support": !
|
|
118
|
-
"--should-transition":
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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([
|
|
162
|
-
onCancel={
|
|
86
|
+
ref={mergeRefs([dialogRef, ref])}
|
|
87
|
+
onCancel={close}
|
|
163
88
|
className={modalClasses}
|
|
164
89
|
>
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
});
|
|
@@ -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
|
+
};
|