/
modal.tsx
194 lines (179 loc) · 5.17 KB
/
modal.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
import { FocusLockProps } from "@chakra-ui/focus-lock"
import { Portal, PortalProps } from "@chakra-ui/portal"
import { createContext } from "@chakra-ui/react-context"
import {
SystemStyleObject,
ThemingProps,
useMultiStyleConfig,
} from "@chakra-ui/system"
import { AnimatePresence } from "framer-motion"
import { useModal, UseModalProps, UseModalReturn } from "./use-modal"
interface FocusableElement {
focus(options?: FocusOptions): void
}
const [ModalStylesProvider, useModalStyles] = createContext<
Record<string, SystemStyleObject>
>({
name: `ModalStylesContext`,
errorMessage: `useModalStyles returned is 'undefined'. Seems you forgot to wrap the components in "<Modal />" `,
})
export { ModalContextProvider, useModalContext, useModalStyles }
interface ModalOptions extends Pick<FocusLockProps, "lockFocusAcrossFrames"> {
/**
* If `false`, focus lock will be disabled completely.
*
* This is useful in situations where you still need to interact with
* other surrounding elements.
*
* 🚨Warning: We don't recommend doing this because it hurts the
* accessibility of the modal, based on WAI-ARIA specifications.
*
* @default true
*/
trapFocus?: boolean
/**
* If `true`, the modal will autofocus the first enabled and interactive
* element within the `ModalContent`
*
* @default true
*/
autoFocus?: boolean
/**
* The `ref` of element to receive focus when the modal opens.
*/
initialFocusRef?: React.RefObject<FocusableElement>
/**
* The `ref` of element to receive focus when the modal closes.
*/
finalFocusRef?: React.RefObject<FocusableElement>
/**
* If `true`, the modal will return focus to the element that triggered it when it closes.
* @default true
*/
returnFocusOnClose?: boolean
/**
* If `true`, scrolling will be disabled on the `body` when the modal opens.
* @default true
*/
blockScrollOnMount?: boolean
/**
* Handle zoom/pinch gestures on iOS devices when scroll locking is enabled.
* @default false.
*/
allowPinchZoom?: boolean
/**
* If `true`, a `padding-right` will be applied to the body element
* that's equal to the width of the scrollbar.
*
* This can help prevent some unpleasant flickering effect
* and content adjustment when the modal opens
*/
preserveScrollBarGap?: boolean
}
type ScrollBehavior = "inside" | "outside"
type MotionPreset = "slideInBottom" | "slideInRight" | "scale" | "none"
export interface ModalProps
extends UseModalProps,
ModalOptions,
ThemingProps<"Modal"> {
children: React.ReactNode
/**
* If `true`, the modal will be centered on screen.
* @default false
*/
isCentered?: boolean
/**
* Where scroll behavior should originate.
* - If set to `inside`, scroll only occurs within the `ModalBody`.
* - If set to `outside`, the entire `ModalContent` will scroll within the viewport.
*
* @default "outside"
*/
scrollBehavior?: ScrollBehavior
/**
* Props to be forwarded to the portal component
*/
portalProps?: Pick<PortalProps, "appendToParentPortal" | "containerRef">
/**
* The transition that should be used for the modal
* @default "scale"
*/
motionPreset?: MotionPreset
/**
* Fires when all exiting nodes have completed animating out
*/
onCloseComplete?: () => void
}
interface ModalContext extends ModalOptions, UseModalReturn {
/**
* The transition that should be used for the modal
*/
motionPreset?: MotionPreset
}
const [ModalContextProvider, useModalContext] = createContext<ModalContext>({
strict: true,
name: "ModalContext",
errorMessage:
"useModalContext: `context` is undefined. Seems you forgot to wrap modal components in `<Modal />`",
})
/**
* Modal provides context, theming, and accessibility properties
* to all other modal components.
*
* It doesn't render any DOM node.
*
* @see Docs https://chakra-ui.com/docs/components/modal
* @see WAI-ARIA https://www.w3.org/WAI/ARIA/apg/patterns/dialogmodal/
*/
export const Modal: React.FC<ModalProps> = (props) => {
const {
portalProps,
children,
autoFocus,
trapFocus,
initialFocusRef,
finalFocusRef,
returnFocusOnClose,
blockScrollOnMount,
allowPinchZoom,
preserveScrollBarGap,
motionPreset,
lockFocusAcrossFrames,
onCloseComplete,
} = props
const styles = useMultiStyleConfig("Modal", props)
const modal = useModal(props)
const context = {
...modal,
autoFocus,
trapFocus,
initialFocusRef,
finalFocusRef,
returnFocusOnClose,
blockScrollOnMount,
allowPinchZoom,
preserveScrollBarGap,
motionPreset,
lockFocusAcrossFrames,
}
return (
<ModalContextProvider value={context}>
<ModalStylesProvider value={styles}>
<AnimatePresence onExitComplete={onCloseComplete}>
{context.isOpen && <Portal {...portalProps}>{children}</Portal>}
</AnimatePresence>
</ModalStylesProvider>
</ModalContextProvider>
)
}
Modal.defaultProps = {
lockFocusAcrossFrames: true,
returnFocusOnClose: true,
scrollBehavior: "outside",
trapFocus: true,
autoFocus: true,
blockScrollOnMount: true,
allowPinchZoom: false,
motionPreset: "scale",
}
Modal.displayName = "Modal"