@bronzelabs/oakma-ui 0.0.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/.prettierrc.cjs +25 -0
- package/.storybook/components/ActionButton.tsx +44 -0
- package/.storybook/components/DummyIcons.tsx +47 -0
- package/.storybook/components/index.ts +2 -0
- package/.storybook/docs/blocks/ImportStatement.tsx +52 -0
- package/.storybook/docs/blocks/index.ts +1 -0
- package/.storybook/docs/page.tsx +41 -0
- package/.storybook/main.ts +21 -0
- package/.storybook/postcss.config.cjs +8 -0
- package/.storybook/preview-body.html +20 -0
- package/.storybook/preview-head.html +6 -0
- package/.storybook/preview.tsx +30 -0
- package/.storybook/tailwind.css +6 -0
- package/.storybook/utils/index.ts +2 -0
- package/.storybook/utils/renderAsReact.tsx +30 -0
- package/.storybook/utils/renderDocsWithProps.tsx +22 -0
- package/@types/markdown.d.ts +4 -0
- package/README.md +3 -0
- package/eslint.config.js +91 -0
- package/package.json +63 -0
- package/postcss.config.cjs +8 -0
- package/scripts/release.sh +76 -0
- package/src/components/Button/Button.stories.tsx +314 -0
- package/src/components/Button/Button.tsx +132 -0
- package/src/components/Button/index.ts +2 -0
- package/src/components/Button/types.ts +19 -0
- package/src/components/Checkbox/Checkbox.stories.tsx +152 -0
- package/src/components/Checkbox/Checkbox.tsx +90 -0
- package/src/components/Checkbox/index.ts +2 -0
- package/src/components/Checkbox/types.ts +6 -0
- package/src/components/Chip/Chip.stories.tsx +146 -0
- package/src/components/Chip/Chip.tsx +59 -0
- package/src/components/Chip/index.ts +2 -0
- package/src/components/Chip/types.ts +6 -0
- package/src/components/Drawer/Drawer.docs.md +88 -0
- package/src/components/Drawer/Drawer.stories.tsx +239 -0
- package/src/components/Drawer/Drawer.tsx +194 -0
- package/src/components/Drawer/index.ts +3 -0
- package/src/components/Drawer/types.ts +3 -0
- package/src/components/Dropdown/AsyncDropdown.tsx +105 -0
- package/src/components/Dropdown/Dropdown.docs.md +33 -0
- package/src/components/Dropdown/Dropdown.stories.tsx +419 -0
- package/src/components/Dropdown/Dropdown.tsx +104 -0
- package/src/components/Dropdown/MultiValue.tsx +19 -0
- package/src/components/Dropdown/ValueContainer.tsx +114 -0
- package/src/components/Dropdown/index.ts +4 -0
- package/src/components/Dropdown/types.ts +29 -0
- package/src/components/Dropdown/useDropdown.tsx +257 -0
- package/src/components/Logo/Logo.stories.tsx +130 -0
- package/src/components/Logo/Logo.tsx +80 -0
- package/src/components/Logo/index.ts +2 -0
- package/src/components/Modal/Modal.docs.md +94 -0
- package/src/components/Modal/Modal.stories.tsx +318 -0
- package/src/components/Modal/Modal.tsx +217 -0
- package/src/components/Modal/index.ts +1 -0
- package/src/components/MultiSelect/AsyncMultiSelect.tsx +47 -0
- package/src/components/MultiSelect/MultiSelect.docs.md +37 -0
- package/src/components/MultiSelect/MultiSelect.stories.tsx +493 -0
- package/src/components/MultiSelect/MultiSelect.tsx +81 -0
- package/src/components/MultiSelect/index.ts +2 -0
- package/src/components/Notification/Notification.stories.tsx +158 -0
- package/src/components/Notification/Notification.tsx +110 -0
- package/src/components/Notification/index.ts +1 -0
- package/src/components/Notification/types.ts +11 -0
- package/src/components/Notifications/Notifications.docs.md +103 -0
- package/src/components/Notifications/Notifications.stories.tsx +159 -0
- package/src/components/Notifications/Notifications.tsx +90 -0
- package/src/components/Notifications/NotificationsContext.tsx +90 -0
- package/src/components/Notifications/index.ts +7 -0
- package/src/components/Select/Select.stories.tsx +234 -0
- package/src/components/Select/Select.tsx +129 -0
- package/src/components/Select/index.ts +2 -0
- package/src/components/Select/types.ts +1 -0
- package/src/components/Spinner/Spinner.stories.tsx +55 -0
- package/src/components/Spinner/Spinner.tsx +48 -0
- package/src/components/Spinner/index.ts +2 -0
- package/src/components/Spinner/types.ts +8 -0
- package/src/components/TextArea/TextArea.stories.tsx +243 -0
- package/src/components/TextArea/TextArea.tsx +133 -0
- package/src/components/TextArea/index.ts +2 -0
- package/src/components/TextArea/types.ts +4 -0
- package/src/components/TextField/Container.tsx +68 -0
- package/src/components/TextField/ErrorMessage.tsx +37 -0
- package/src/components/TextField/Icon.tsx +77 -0
- package/src/components/TextField/Label.tsx +56 -0
- package/src/components/TextField/NotchBorder.tsx +67 -0
- package/src/components/TextField/index.ts +14 -0
- package/src/components/TextField/types.ts +15 -0
- package/src/components/TextField/useInputKeyboardFocus.tsx +63 -0
- package/src/components/TextInput/TextInput.stories.tsx +384 -0
- package/src/components/TextInput/TextInput.tsx +255 -0
- package/src/components/TextInput/index.ts +2 -0
- package/src/components/TextInput/types.ts +4 -0
- package/src/components/Toggle/Toggle.stories.tsx +142 -0
- package/src/components/Toggle/Toggle.tsx +69 -0
- package/src/components/Toggle/index.ts +1 -0
- package/src/hooks/index.ts +6 -0
- package/src/hooks/useCombinedRefs.ts +37 -0
- package/src/hooks/useEventListener.ts +87 -0
- package/src/hooks/useFocusTrap/createAriaHider.ts +62 -0
- package/src/hooks/useFocusTrap/index.ts +1 -0
- package/src/hooks/useFocusTrap/scopeTab.ts +46 -0
- package/src/hooks/useFocusTrap/tabbable.ts +107 -0
- package/src/hooks/useFocusTrap/useFocusTrap.ts +97 -0
- package/src/hooks/useIsomorphicLayoutEffect.ts +14 -0
- package/src/hooks/useLockBodyScroll.ts +24 -0
- package/src/hooks/useOnClickOutside.ts +53 -0
- package/src/index.ts +22 -0
- package/src/tailwind.css +4 -0
- package/src/types/helpers.ts +11 -0
- package/src/types/polymorphic.ts +39 -0
- package/src/utils/animation/variants.ts +21 -0
- package/src/utils/array/index.ts +1 -0
- package/src/utils/array/uniqBy.ts +12 -0
- package/src/utils/common/index.ts +1 -0
- package/src/utils/common/isFunction.ts +17 -0
- package/src/utils/react/extractDisplayName.ts +15 -0
- package/src/utils/react/index.ts +1 -0
- package/tsconfig.json +16 -0
- package/tsconfig.production.json +19 -0
- package/tsup.config.ts +16 -0
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
|
|
3
|
+
// Components
|
|
4
|
+
import Modal from "./Modal"
|
|
5
|
+
import Button from "../Button"
|
|
6
|
+
import TextInput from "../TextInput"
|
|
7
|
+
|
|
8
|
+
// Types
|
|
9
|
+
import type { StoryFn, Meta } from "@storybook/react-webpack5"
|
|
10
|
+
|
|
11
|
+
// Utils
|
|
12
|
+
import { StorybookActionButton } from "../../../.storybook/components"
|
|
13
|
+
import { renderDocsWithProps } from "../../../.storybook/utils"
|
|
14
|
+
import extraDocs from "./Modal.docs.md"
|
|
15
|
+
|
|
16
|
+
const lorem = `Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.`
|
|
17
|
+
|
|
18
|
+
/*
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const meta = {
|
|
26
|
+
title: "Components/Modal",
|
|
27
|
+
component: Modal,
|
|
28
|
+
parameters: {
|
|
29
|
+
controls: {
|
|
30
|
+
exclude: ["ref", "children"],
|
|
31
|
+
},
|
|
32
|
+
docs: {
|
|
33
|
+
description: {
|
|
34
|
+
component: "A component that can be used to display content in a modal.",
|
|
35
|
+
},
|
|
36
|
+
page: renderDocsWithProps({ extraDocs }),
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
argTypes: {
|
|
40
|
+
isOpen: {
|
|
41
|
+
description: "Controls whether the Modal is visible.",
|
|
42
|
+
control: { type: "boolean" },
|
|
43
|
+
table: {
|
|
44
|
+
type: { summary: "boolean" },
|
|
45
|
+
defaultValue: { summary: "false" },
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
onClose: {
|
|
49
|
+
description:
|
|
50
|
+
"Callback fired when the Modal should close (close button, outside click, or Escape key).",
|
|
51
|
+
control: false,
|
|
52
|
+
table: {
|
|
53
|
+
type: { summary: "() => void" },
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
withBackdrop: {
|
|
57
|
+
description: "Renders a blurred backdrop behind the Modal.",
|
|
58
|
+
control: { type: "boolean" },
|
|
59
|
+
table: {
|
|
60
|
+
type: { summary: "boolean" },
|
|
61
|
+
defaultValue: { summary: "true" },
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
withCloseButton: {
|
|
65
|
+
description: "Renders an X button in the top-right corner of the Modal.",
|
|
66
|
+
control: { type: "boolean" },
|
|
67
|
+
table: {
|
|
68
|
+
type: { summary: "boolean" },
|
|
69
|
+
defaultValue: { summary: "true" },
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
closeOnClickOutside: {
|
|
73
|
+
description: "Calls `onClose` when the user clicks outside the Modal.",
|
|
74
|
+
control: { type: "boolean" },
|
|
75
|
+
table: {
|
|
76
|
+
type: { summary: "boolean" },
|
|
77
|
+
defaultValue: { summary: "true" },
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
closeOnEscape: {
|
|
81
|
+
description: "Calls `onClose` when the user presses the Escape key.",
|
|
82
|
+
control: { type: "boolean" },
|
|
83
|
+
table: {
|
|
84
|
+
type: { summary: "boolean" },
|
|
85
|
+
defaultValue: { summary: "true" },
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
lockScroll: {
|
|
89
|
+
description: "Locks body scroll while the Modal is open.",
|
|
90
|
+
control: { type: "boolean" },
|
|
91
|
+
table: {
|
|
92
|
+
type: { summary: "boolean" },
|
|
93
|
+
defaultValue: { summary: "true" },
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
trapFocus: {
|
|
97
|
+
description: "Traps keyboard focus inside the Modal while it is open.",
|
|
98
|
+
control: { type: "boolean" },
|
|
99
|
+
table: {
|
|
100
|
+
type: { summary: "boolean" },
|
|
101
|
+
defaultValue: { summary: "true" },
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
portal: {
|
|
105
|
+
description:
|
|
106
|
+
"DOM element or DocumentFragment to render the Modal portal into. Defaults to `document.body`.",
|
|
107
|
+
control: false,
|
|
108
|
+
table: {
|
|
109
|
+
type: { summary: "Element | DocumentFragment" },
|
|
110
|
+
defaultValue: { summary: "document.body" },
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
header: {
|
|
114
|
+
description: "Replaces the default title/subtitle area with a custom React node.",
|
|
115
|
+
control: false,
|
|
116
|
+
table: {
|
|
117
|
+
type: { summary: "ReactNode" },
|
|
118
|
+
defaultValue: { summary: "undefined" },
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
footer: {
|
|
122
|
+
description: "Renders a sticky footer area at the bottom of the Modal.",
|
|
123
|
+
control: false,
|
|
124
|
+
table: {
|
|
125
|
+
type: { summary: "ReactNode" },
|
|
126
|
+
defaultValue: { summary: "undefined" },
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
contentClassName: {
|
|
130
|
+
description: "Additional CSS class applied to the scrollable content wrapper.",
|
|
131
|
+
control: { type: "text" },
|
|
132
|
+
table: {
|
|
133
|
+
type: { summary: "string" },
|
|
134
|
+
defaultValue: { summary: "undefined" },
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
title: {
|
|
138
|
+
description:
|
|
139
|
+
"@deprecated — not currently rendered. Use `header` to display a custom heading.",
|
|
140
|
+
control: { type: "text" },
|
|
141
|
+
table: {
|
|
142
|
+
type: { summary: "string" },
|
|
143
|
+
defaultValue: { summary: "undefined" },
|
|
144
|
+
category: "Deprecated",
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
subTitle: {
|
|
148
|
+
description:
|
|
149
|
+
"@deprecated — not currently rendered. Use `header` to display a custom subheading.",
|
|
150
|
+
control: { type: "text" },
|
|
151
|
+
table: {
|
|
152
|
+
type: { summary: "string" },
|
|
153
|
+
defaultValue: { summary: "undefined" },
|
|
154
|
+
category: "Deprecated",
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
args: {
|
|
159
|
+
withBackdrop: true,
|
|
160
|
+
withCloseButton: true,
|
|
161
|
+
closeOnClickOutside: true,
|
|
162
|
+
closeOnEscape: true,
|
|
163
|
+
lockScroll: true,
|
|
164
|
+
trapFocus: true,
|
|
165
|
+
children: <p>{lorem}</p>,
|
|
166
|
+
},
|
|
167
|
+
decorators: [
|
|
168
|
+
(Story, { args: { onClose, ...rest } }) => {
|
|
169
|
+
const [isOpen, setIsOpen] = React.useState(false)
|
|
170
|
+
|
|
171
|
+
return (
|
|
172
|
+
<>
|
|
173
|
+
<StorybookActionButton onClick={() => setIsOpen(true)}>Open modal</StorybookActionButton>
|
|
174
|
+
{Story({
|
|
175
|
+
args: {
|
|
176
|
+
isOpen: isOpen,
|
|
177
|
+
onClose: () => {
|
|
178
|
+
setIsOpen(false)
|
|
179
|
+
onClose?.()
|
|
180
|
+
},
|
|
181
|
+
...rest,
|
|
182
|
+
},
|
|
183
|
+
})}
|
|
184
|
+
</>
|
|
185
|
+
)
|
|
186
|
+
},
|
|
187
|
+
],
|
|
188
|
+
} satisfies Meta<typeof Modal>
|
|
189
|
+
|
|
190
|
+
// Templates
|
|
191
|
+
const DummyContent = () => (
|
|
192
|
+
<div className="flex flex-col pb-6 text-center">
|
|
193
|
+
<h2 className="mb-6 font-lora text-2xl font-medium text-neutral-100">Change account photo</h2>
|
|
194
|
+
<div className="mb-3 flex flex-col items-center gap-3 rounded-3xl border border-stroke-dark p-8">
|
|
195
|
+
<h3 className="font-inter text-lg font-medium text-neutral-100">Upload your photo</h3>
|
|
196
|
+
<Button size="sm" variant="secondary">
|
|
197
|
+
Select File
|
|
198
|
+
</Button>
|
|
199
|
+
</div>
|
|
200
|
+
<p className="text-xs text-neutral-50">
|
|
201
|
+
It should be smaller than 2MB, and it should show your face.
|
|
202
|
+
</p>
|
|
203
|
+
</div>
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
const Template: StoryFn<typeof Modal> = args => <Modal {...args} />
|
|
207
|
+
|
|
208
|
+
const CustomFooterTemplate: StoryFn<typeof Modal> = args => (
|
|
209
|
+
<Modal
|
|
210
|
+
{...args}
|
|
211
|
+
footer={
|
|
212
|
+
<div className="flex gap-1.5">
|
|
213
|
+
<Button className="w-full" size="md" variant="secondary" onClick={args.onClose}>
|
|
214
|
+
Accept
|
|
215
|
+
</Button>
|
|
216
|
+
<Button className="w-full" size="md" variant="danger" onClick={args.onClose} data-autofocus>
|
|
217
|
+
Decline
|
|
218
|
+
</Button>
|
|
219
|
+
</div>
|
|
220
|
+
}
|
|
221
|
+
/>
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
const WithFormTemplate: StoryFn<typeof Modal> = args => (
|
|
225
|
+
<Modal {...args}>
|
|
226
|
+
<form
|
|
227
|
+
className="grid gap-6"
|
|
228
|
+
onSubmit={e => {
|
|
229
|
+
e.preventDefault()
|
|
230
|
+
args.onClose()
|
|
231
|
+
alert("submitted")
|
|
232
|
+
}}
|
|
233
|
+
>
|
|
234
|
+
<TextInput id="example" type="email" label="Email" placeholder="Enter your email" required />
|
|
235
|
+
<Button className="w-full">Finish</Button>
|
|
236
|
+
</form>
|
|
237
|
+
</Modal>
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
// Stories
|
|
241
|
+
const Default = Template.bind({})
|
|
242
|
+
const CustomHeader = Template.bind({})
|
|
243
|
+
const CustomFooter = CustomFooterTemplate.bind({})
|
|
244
|
+
const Wide = Template.bind({})
|
|
245
|
+
const Long = Template.bind({})
|
|
246
|
+
const WithForm = WithFormTemplate.bind({})
|
|
247
|
+
|
|
248
|
+
// Args
|
|
249
|
+
Default.args = {
|
|
250
|
+
children: <DummyContent />,
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
CustomHeader.args = {
|
|
254
|
+
header: (
|
|
255
|
+
<header
|
|
256
|
+
style={{
|
|
257
|
+
display: "flex",
|
|
258
|
+
justifyContent: "center",
|
|
259
|
+
padding: 24,
|
|
260
|
+
background:
|
|
261
|
+
"linear-gradient(90deg, rgb(2 0 36 / 1) 0%, rgb(9 9 121 / 1) 35%, rgb(0 212 255 / 1) 100%)",
|
|
262
|
+
}}
|
|
263
|
+
>
|
|
264
|
+
<h5 style={{ margin: 0, color: "white" }}>This is a custom header</h5>
|
|
265
|
+
</header>
|
|
266
|
+
),
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
Wide.args = {
|
|
270
|
+
children: <DummyContent />,
|
|
271
|
+
className: "sm:w-[600px]",
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
Long.args = {
|
|
275
|
+
title: "Title",
|
|
276
|
+
subTitle: "Subtitle",
|
|
277
|
+
children: (
|
|
278
|
+
<div className="grid gap-3">
|
|
279
|
+
<p>{lorem}</p>
|
|
280
|
+
<p>{lorem}</p>
|
|
281
|
+
<p>{lorem}</p>
|
|
282
|
+
<p>{lorem}</p>
|
|
283
|
+
<p>{lorem}</p>
|
|
284
|
+
<p>{lorem}</p>
|
|
285
|
+
</div>
|
|
286
|
+
),
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Parameters
|
|
290
|
+
CustomHeader.parameters = {
|
|
291
|
+
docs: {
|
|
292
|
+
description: {
|
|
293
|
+
story:
|
|
294
|
+
"The `header` prop can be used to render a custom header. (e.g. `header={<h1>Custom header</h1>}`)",
|
|
295
|
+
},
|
|
296
|
+
},
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
CustomFooter.parameters = {
|
|
300
|
+
docs: {
|
|
301
|
+
description: {
|
|
302
|
+
story:
|
|
303
|
+
"The `footer` prop can be used to render a custom footer. (e.g. `footer={<h1>Custom footer</h1>}`)",
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
Wide.parameters = {
|
|
309
|
+
docs: {
|
|
310
|
+
description: {
|
|
311
|
+
story:
|
|
312
|
+
"To set a custom width for the Modal pass a tailwind width class to the className prop (e.g. `w-96` or `w-[64rem]`).",
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export { Default, CustomHeader, CustomFooter, Wide, Long, WithForm }
|
|
318
|
+
export default meta
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import React, { useEffect } from "react"
|
|
4
|
+
import { createPortal } from "react-dom"
|
|
5
|
+
|
|
6
|
+
// Components
|
|
7
|
+
import { XMarkIcon } from "@heroicons/react/20/solid"
|
|
8
|
+
|
|
9
|
+
// Utils
|
|
10
|
+
import { motion, AnimatePresence, Variants, HTMLMotionProps } from "motion/react"
|
|
11
|
+
import clsx from "clsx"
|
|
12
|
+
import {
|
|
13
|
+
useCombinedRefs,
|
|
14
|
+
useEventListener,
|
|
15
|
+
useLockBodyScroll,
|
|
16
|
+
useOnClickOutside,
|
|
17
|
+
useFocusTrap,
|
|
18
|
+
} from "../../hooks"
|
|
19
|
+
import { backdropVariants } from "../../utils/animation/variants"
|
|
20
|
+
|
|
21
|
+
// Variants
|
|
22
|
+
const modalVariants: Variants = {
|
|
23
|
+
hidden: {
|
|
24
|
+
opacity: 0,
|
|
25
|
+
x: "-50%",
|
|
26
|
+
y: "-45%",
|
|
27
|
+
transition: {
|
|
28
|
+
duration: 0.15,
|
|
29
|
+
ease: [0.4, 0, 0.2, 1],
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
visible: {
|
|
33
|
+
opacity: 1,
|
|
34
|
+
x: "-50%",
|
|
35
|
+
y: "-50%",
|
|
36
|
+
transition: {
|
|
37
|
+
duration: 0.15,
|
|
38
|
+
ease: [0.4, 0, 0.2, 1],
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Params
|
|
44
|
+
interface ModalProps extends HTMLMotionProps<"div"> {
|
|
45
|
+
/** */
|
|
46
|
+
isOpen?: boolean
|
|
47
|
+
/** Callback function that's called when the Modal is closed. */
|
|
48
|
+
onClose: () => void
|
|
49
|
+
/** */
|
|
50
|
+
withBackdrop?: boolean
|
|
51
|
+
/** */
|
|
52
|
+
withCloseButton?: boolean
|
|
53
|
+
/** */
|
|
54
|
+
closeOnClickOutside?: boolean
|
|
55
|
+
/** */
|
|
56
|
+
closeOnEscape?: boolean
|
|
57
|
+
/** */
|
|
58
|
+
lockScroll?: boolean
|
|
59
|
+
/** */
|
|
60
|
+
trapFocus?: boolean
|
|
61
|
+
/** */
|
|
62
|
+
portal?: Element | DocumentFragment
|
|
63
|
+
/** @deprecated Not currently in use */
|
|
64
|
+
/** Title to use at the top of the Modal. */
|
|
65
|
+
title?: string
|
|
66
|
+
/** @deprecated Not currently in use */
|
|
67
|
+
/** Subtitle to use at the top of the Modal. */
|
|
68
|
+
subTitle?: string
|
|
69
|
+
/** A React Node that's displayed in place of the default `title` and `subTitle`. */
|
|
70
|
+
header?: React.ReactNode
|
|
71
|
+
/** A React Node that's displayed at the bottom of the Modal. */
|
|
72
|
+
footer?: React.ReactNode
|
|
73
|
+
/** */
|
|
74
|
+
children?: React.ReactNode
|
|
75
|
+
/** */
|
|
76
|
+
contentClassName?: string
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const MODAL_EVENT = "oakma-modal-state"
|
|
80
|
+
|
|
81
|
+
/*
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
*/
|
|
88
|
+
|
|
89
|
+
const Modal: React.FC<ModalProps> = ({
|
|
90
|
+
isOpen = false,
|
|
91
|
+
withBackdrop = true,
|
|
92
|
+
withCloseButton = true,
|
|
93
|
+
closeOnClickOutside = true,
|
|
94
|
+
closeOnEscape = true,
|
|
95
|
+
lockScroll = true,
|
|
96
|
+
trapFocus = true,
|
|
97
|
+
portal,
|
|
98
|
+
onClose,
|
|
99
|
+
title, // eslint-disable-line
|
|
100
|
+
subTitle, // eslint-disable-line
|
|
101
|
+
className,
|
|
102
|
+
children,
|
|
103
|
+
header,
|
|
104
|
+
footer,
|
|
105
|
+
contentClassName,
|
|
106
|
+
ref,
|
|
107
|
+
...rest
|
|
108
|
+
}) => {
|
|
109
|
+
// Refs
|
|
110
|
+
const focusRef = useFocusTrap(trapFocus && isOpen)
|
|
111
|
+
const combinedRefs = useCombinedRefs<HTMLDivElement | null>(ref, focusRef)
|
|
112
|
+
|
|
113
|
+
// Events
|
|
114
|
+
useOnClickOutside<HTMLDivElement | null>(combinedRefs, () => {
|
|
115
|
+
if (closeOnClickOutside) onClose()
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
useEventListener("keydown", e => {
|
|
119
|
+
if (closeOnEscape && e.key === "Escape") onClose()
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
useLockBodyScroll(lockScroll && isOpen)
|
|
123
|
+
|
|
124
|
+
// Effects
|
|
125
|
+
useEffect(() => {
|
|
126
|
+
if (typeof window !== "undefined") {
|
|
127
|
+
window.dispatchEvent(new CustomEvent(MODAL_EVENT, { detail: { open: isOpen } }))
|
|
128
|
+
}
|
|
129
|
+
return () => {
|
|
130
|
+
if (typeof window !== "undefined") {
|
|
131
|
+
window.dispatchEvent(new CustomEvent(MODAL_EVENT, { detail: { open: false } }))
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}, [isOpen])
|
|
135
|
+
|
|
136
|
+
if (typeof window === "undefined") {
|
|
137
|
+
return null
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return createPortal(
|
|
141
|
+
<AnimatePresence>
|
|
142
|
+
{isOpen && (
|
|
143
|
+
<div className="fixed inset-0 z-9998">
|
|
144
|
+
{withBackdrop && (
|
|
145
|
+
<motion.div
|
|
146
|
+
className="fixed inset-0 opacity-30 backdrop-blur-xs"
|
|
147
|
+
initial="hidden"
|
|
148
|
+
animate="visible"
|
|
149
|
+
exit="hidden"
|
|
150
|
+
variants={backdropVariants}
|
|
151
|
+
role="presentation"
|
|
152
|
+
style={{ backgroundColor: "rgba(15 41 41 / 0.3)" }}
|
|
153
|
+
/>
|
|
154
|
+
)}
|
|
155
|
+
|
|
156
|
+
<motion.div
|
|
157
|
+
className={clsx(
|
|
158
|
+
"fixed top-1/2 left-1/2 flex max-h-[90vh] w-[calc(100vw-3rem)] shrink-0 flex-col gap-4 overflow-hidden rounded-5xl bg-white shadow-md sm:w-96",
|
|
159
|
+
className,
|
|
160
|
+
)}
|
|
161
|
+
ref={combinedRefs}
|
|
162
|
+
role="dialog"
|
|
163
|
+
aria-modal
|
|
164
|
+
initial="hidden"
|
|
165
|
+
animate="visible"
|
|
166
|
+
exit="hidden"
|
|
167
|
+
variants={modalVariants}
|
|
168
|
+
{...rest}
|
|
169
|
+
>
|
|
170
|
+
<header
|
|
171
|
+
className={clsx(
|
|
172
|
+
"sticky top-0 grid w-full grid-cols-[auto_min-content] items-start gap-4 bg-transparent pt-4 pl-6",
|
|
173
|
+
withCloseButton ? "pr-4" : "p-6",
|
|
174
|
+
)}
|
|
175
|
+
>
|
|
176
|
+
{header && <>{header}</>}
|
|
177
|
+
|
|
178
|
+
{/* {!header && !!title && (
|
|
179
|
+
<div className="flex flex-col pt-1">
|
|
180
|
+
<h2 className="font-lora text-2xl font-medium text-neutral-100">{title}</h2>
|
|
181
|
+
{subTitle && <p className="text-neutral-50">{subTitle}</p>}
|
|
182
|
+
</div>
|
|
183
|
+
)} */}
|
|
184
|
+
|
|
185
|
+
{withCloseButton && (
|
|
186
|
+
<button
|
|
187
|
+
className="col-start-2 -mt-0.5 -mr-0.5 flex size-6 cursor-pointer items-center justify-center justify-self-end rounded-full text-neutral-50 transition-colors hover:bg-neutral-50/10 hover:text-neutral-100 focus-visible:bg-neutral-50/10 focus-visible:text-neutral-100 active:bg-neutral-50/20"
|
|
188
|
+
onClick={onClose}
|
|
189
|
+
title="Close modal"
|
|
190
|
+
>
|
|
191
|
+
<XMarkIcon className="size-6" />
|
|
192
|
+
</button>
|
|
193
|
+
)}
|
|
194
|
+
</header>
|
|
195
|
+
|
|
196
|
+
<div
|
|
197
|
+
className={clsx(
|
|
198
|
+
"size-full overflow-scroll px-6",
|
|
199
|
+
!footer && "pb-6",
|
|
200
|
+
contentClassName,
|
|
201
|
+
)}
|
|
202
|
+
>
|
|
203
|
+
{children}
|
|
204
|
+
</div>
|
|
205
|
+
|
|
206
|
+
{footer && <footer className="bg-transparent px-6 pb-4">{footer}</footer>}
|
|
207
|
+
</motion.div>
|
|
208
|
+
</div>
|
|
209
|
+
)}
|
|
210
|
+
</AnimatePresence>,
|
|
211
|
+
portal || document.body,
|
|
212
|
+
)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
Modal.displayName = "Modal"
|
|
216
|
+
export default Modal
|
|
217
|
+
export type { ModalProps }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default, type ModalProps } from "./Modal"
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import React from "react"
|
|
4
|
+
|
|
5
|
+
// Components
|
|
6
|
+
import { AsyncDropdown, type AsyncDropdownProps } from "../Dropdown"
|
|
7
|
+
import { MultiSelectOption } from "./MultiSelect"
|
|
8
|
+
|
|
9
|
+
// Props
|
|
10
|
+
interface AsyncMultiSelectProps extends Omit<AsyncDropdownProps, "isMulti"> {
|
|
11
|
+
cy?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const multiSelectComponents = { Option: MultiSelectOption }
|
|
15
|
+
|
|
16
|
+
/*
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const AsyncMultiSelect: React.FC<AsyncMultiSelectProps> = ({
|
|
25
|
+
tabSelectsValue = false,
|
|
26
|
+
closeMenuOnSelect = false,
|
|
27
|
+
hideSelectedOptions = false,
|
|
28
|
+
isClearable = false,
|
|
29
|
+
...rest
|
|
30
|
+
}) => {
|
|
31
|
+
return (
|
|
32
|
+
<AsyncDropdown
|
|
33
|
+
closeMenuOnSelect={closeMenuOnSelect}
|
|
34
|
+
hideSelectedOptions={hideSelectedOptions}
|
|
35
|
+
tabSelectsValue={tabSelectsValue}
|
|
36
|
+
// @ts-expect-error - The `isMulti` prop is not supported when used by external consumers, however it is required internally to render the correct components and behavior.
|
|
37
|
+
isMulti
|
|
38
|
+
isClearable={isClearable}
|
|
39
|
+
components={multiSelectComponents}
|
|
40
|
+
{...rest}
|
|
41
|
+
/>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
AsyncMultiSelect.displayName = "AsyncMultiSelect"
|
|
46
|
+
export default AsyncMultiSelect
|
|
47
|
+
export type { AsyncMultiSelectProps }
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
## Usage with `react-hook-form`
|
|
2
|
+
|
|
3
|
+
Due to the nature of the component should, you want to use the MultiSelect within a form managed by `react-hook-form`, you'll need to use their `Controller` component:
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
"use client"
|
|
7
|
+
|
|
8
|
+
import React from "react"
|
|
9
|
+
import { useForm, Controller } from "react-hook-form"
|
|
10
|
+
|
|
11
|
+
export default function MyForm() {
|
|
12
|
+
const { control, handleSubmit } = useForm()
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<form onSubmit={handleSubmit(data => console.log(data))}>
|
|
16
|
+
<Controller
|
|
17
|
+
name="controlled-multi-select"
|
|
18
|
+
control={control}
|
|
19
|
+
render={({ field }) => {
|
|
20
|
+
return <MultiSelect {...multiSelectProps} {...field} />
|
|
21
|
+
}}
|
|
22
|
+
/>
|
|
23
|
+
</form>
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Doing this will mean that the result from the component will be correctly passed to `rhf`'s submit handler.
|
|
29
|
+
|
|
30
|
+
```json
|
|
31
|
+
// example result from multi select
|
|
32
|
+
[
|
|
33
|
+
{ "label": "Item 1", "value": "item-1" },
|
|
34
|
+
{ "label": "Item 2", "value": "item-2" },
|
|
35
|
+
{ "label": "Item 3", "value": "item-3" }
|
|
36
|
+
]
|
|
37
|
+
```
|