@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,419 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
|
|
3
|
+
// Components
|
|
4
|
+
import Dropdown, { type DropdownProps } from "./Dropdown"
|
|
5
|
+
import AsyncDropdown from "./AsyncDropdown"
|
|
6
|
+
|
|
7
|
+
// Types
|
|
8
|
+
import type { Meta, StoryFn } from "@storybook/react-webpack5"
|
|
9
|
+
import { TEXT_FIELD_SIZES } from "../TextField"
|
|
10
|
+
|
|
11
|
+
// Utils
|
|
12
|
+
import { DummyPhoneIcon, StorybookActionButton } from "../../../.storybook/components"
|
|
13
|
+
import extraDocs from "./Dropdown.docs.md"
|
|
14
|
+
import { renderDocsWithProps } from "../../../.storybook/utils"
|
|
15
|
+
|
|
16
|
+
const mockOptions = {
|
|
17
|
+
default: [
|
|
18
|
+
{
|
|
19
|
+
label: "Apple 🍎",
|
|
20
|
+
value: "apple",
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
label: "Banana 🍌",
|
|
24
|
+
value: "banana",
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
label: "Watermelon 🍉",
|
|
28
|
+
value: "watermelon",
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
grouped: [
|
|
32
|
+
{
|
|
33
|
+
label: "Sweets",
|
|
34
|
+
options: [
|
|
35
|
+
{
|
|
36
|
+
label: "Chocolate 🍫",
|
|
37
|
+
value: "chocolate",
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
label: "Fruit",
|
|
43
|
+
options: [
|
|
44
|
+
{
|
|
45
|
+
label: "Banana 🍌",
|
|
46
|
+
value: "banana",
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
label: "Watermelon 🍉",
|
|
50
|
+
value: "watermelon",
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
reset: [
|
|
56
|
+
{
|
|
57
|
+
label: "None",
|
|
58
|
+
value: null,
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
label: "Apple 🍎",
|
|
62
|
+
value: "apple",
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
label: "Banana 🍌",
|
|
66
|
+
value: "banana",
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
label: "Watermelon 🍉",
|
|
70
|
+
value: "watermelon",
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
long: [
|
|
74
|
+
{
|
|
75
|
+
label: "Germany 🇩🇪",
|
|
76
|
+
value: "de",
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
label: "The United Kingdom Of Great Britain And Northern Ireland 🇬🇧 ",
|
|
80
|
+
value: "uk",
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
label: "France 🇫🇷",
|
|
84
|
+
value: "fr",
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
subTitle: [
|
|
88
|
+
{
|
|
89
|
+
label: "Photo ID",
|
|
90
|
+
value: "photo-id",
|
|
91
|
+
subTitle: "Passport, driver's license, or national ID card",
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
label: "Proof of Address",
|
|
95
|
+
value: "proof-of-address",
|
|
96
|
+
subTitle: "Utility bill, bank statement, or government correspondence",
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
users: [
|
|
100
|
+
{
|
|
101
|
+
label: "John Doe",
|
|
102
|
+
value: "john-doe",
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
label: "Jane Smith",
|
|
106
|
+
value: "jane-smith",
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
label: "Bob Johnson",
|
|
110
|
+
value: "bob-johnson",
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/*
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
*/
|
|
121
|
+
|
|
122
|
+
const meta: Meta<DropdownProps> = {
|
|
123
|
+
title: "Components/Dropdown",
|
|
124
|
+
component: Dropdown,
|
|
125
|
+
parameters: {
|
|
126
|
+
docs: {
|
|
127
|
+
description: {
|
|
128
|
+
component:
|
|
129
|
+
"Dropdown is a fancy select/combobox built on top of [react-select](https://react-select.com), that can be used to select a value from a list.\n\nThe main (and Oakma specific) props can be seen below, however, a full list of accepted props can be found in the react-select [docs](https://react-select.com/props). For details on how to use the component with `react-hook-form` see [here](#usage-with-react-hook-form).",
|
|
130
|
+
},
|
|
131
|
+
page: renderDocsWithProps({ extraDocs }),
|
|
132
|
+
},
|
|
133
|
+
controls: {
|
|
134
|
+
exclude: ["className", "ref", "children"],
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
argTypes: {
|
|
138
|
+
options: {
|
|
139
|
+
type: {
|
|
140
|
+
name: "other",
|
|
141
|
+
value: "(Option | Group)[]",
|
|
142
|
+
},
|
|
143
|
+
description: "Array of options that populate the Dropdown menu.",
|
|
144
|
+
table: {
|
|
145
|
+
type: {
|
|
146
|
+
summary: "(Option | Group)[]",
|
|
147
|
+
},
|
|
148
|
+
defaultValue: {
|
|
149
|
+
summary: "undefined",
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
label: {
|
|
154
|
+
type: "string",
|
|
155
|
+
description: "Visible label rendered above the Dropdown.",
|
|
156
|
+
table: {
|
|
157
|
+
type: { summary: "string" },
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
size: {
|
|
161
|
+
type: "string",
|
|
162
|
+
description: "Determines the size of the Dropdown field.",
|
|
163
|
+
table: {
|
|
164
|
+
type: { summary: TEXT_FIELD_SIZES.map(size => `"${size}"`).join(" | ") },
|
|
165
|
+
defaultValue: { summary: '"md"' },
|
|
166
|
+
},
|
|
167
|
+
options: TEXT_FIELD_SIZES,
|
|
168
|
+
control: { type: "select" },
|
|
169
|
+
},
|
|
170
|
+
leadingIcon: {
|
|
171
|
+
type: {
|
|
172
|
+
name: "other",
|
|
173
|
+
value: "{ icon: ReactNode }",
|
|
174
|
+
},
|
|
175
|
+
description:
|
|
176
|
+
"An object with an `icon` property containing a React node to render inside the field, aligned to the left.",
|
|
177
|
+
table: {
|
|
178
|
+
type: {
|
|
179
|
+
summary: "{ icon: ReactNode }",
|
|
180
|
+
},
|
|
181
|
+
defaultValue: {
|
|
182
|
+
summary: "undefined",
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
showOptional: {
|
|
187
|
+
type: "boolean",
|
|
188
|
+
description:
|
|
189
|
+
'When `true`, and if the `required` prop is not set, an "Optional" tag will be displayed next to the label.',
|
|
190
|
+
table: {
|
|
191
|
+
type: { summary: "boolean" },
|
|
192
|
+
defaultValue: { summary: "true" },
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
isLoading: {
|
|
196
|
+
type: "boolean",
|
|
197
|
+
description: "Is the Dropdown in a state of loading (async).",
|
|
198
|
+
table: {
|
|
199
|
+
type: {
|
|
200
|
+
summary: "boolean",
|
|
201
|
+
},
|
|
202
|
+
defaultValue: {
|
|
203
|
+
summary: "false",
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
isMulti: {
|
|
208
|
+
type: "boolean",
|
|
209
|
+
description: "Support multiple selected options.",
|
|
210
|
+
table: {
|
|
211
|
+
type: {
|
|
212
|
+
summary: "boolean",
|
|
213
|
+
},
|
|
214
|
+
defaultValue: {
|
|
215
|
+
summary: "false",
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
isDisabled: {
|
|
220
|
+
type: "boolean",
|
|
221
|
+
description: "Is the Dropdown disabled.",
|
|
222
|
+
table: {
|
|
223
|
+
type: {
|
|
224
|
+
summary: "boolean",
|
|
225
|
+
},
|
|
226
|
+
defaultValue: {
|
|
227
|
+
summary: "false",
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
isClearable: {
|
|
232
|
+
type: "boolean",
|
|
233
|
+
description: "Is the Dropdown value clearable.",
|
|
234
|
+
table: {
|
|
235
|
+
type: {
|
|
236
|
+
summary: "boolean",
|
|
237
|
+
},
|
|
238
|
+
defaultValue: {
|
|
239
|
+
summary: "false",
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
args: {
|
|
245
|
+
label: "Label",
|
|
246
|
+
showOptional: true,
|
|
247
|
+
size: "md",
|
|
248
|
+
required: false,
|
|
249
|
+
},
|
|
250
|
+
decorators: [
|
|
251
|
+
Story => (
|
|
252
|
+
<div style={{ width: 450, height: 300 }}>
|
|
253
|
+
<Story />
|
|
254
|
+
</div>
|
|
255
|
+
),
|
|
256
|
+
],
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
//
|
|
260
|
+
const Template: StoryFn<DropdownProps> = args => <Dropdown {...args} />
|
|
261
|
+
|
|
262
|
+
const RequiredTemplate: StoryFn<DropdownProps> = args => (
|
|
263
|
+
<form
|
|
264
|
+
className="flex flex-col items-stretch gap-3"
|
|
265
|
+
onSubmit={e => {
|
|
266
|
+
e.preventDefault()
|
|
267
|
+
alert("Form submitted!")
|
|
268
|
+
}}
|
|
269
|
+
>
|
|
270
|
+
<Dropdown {...args} />
|
|
271
|
+
<StorybookActionButton type="submit">Submit</StorybookActionButton>
|
|
272
|
+
</form>
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
const mockLoadOptions = (inputValue: string) =>
|
|
276
|
+
new Promise<(typeof mockOptions.users)[number][]>(resolve => {
|
|
277
|
+
setTimeout(() => {
|
|
278
|
+
resolve(
|
|
279
|
+
mockOptions.users.filter(opt => opt.label.toLowerCase().includes(inputValue.toLowerCase())),
|
|
280
|
+
)
|
|
281
|
+
}, 600)
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
const AsyncTemplate: StoryFn<DropdownProps> = args => (
|
|
285
|
+
<AsyncDropdown
|
|
286
|
+
id="async-dropdown"
|
|
287
|
+
label={args.label}
|
|
288
|
+
showOptional={args.showOptional}
|
|
289
|
+
size={args.size}
|
|
290
|
+
loadOptions={mockLoadOptions}
|
|
291
|
+
defaultOptions={mockOptions.users}
|
|
292
|
+
placeholder="Search users…"
|
|
293
|
+
/>
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
const MultiSelectTemplate: StoryFn = () => {
|
|
297
|
+
return (
|
|
298
|
+
<p className="text-center text-base">
|
|
299
|
+
The{" "}
|
|
300
|
+
<code
|
|
301
|
+
className="inline rounded-sm p-0.5"
|
|
302
|
+
style={{ backgroundColor: "#f0f0f0", color: "#333", fontSize: 15 }}
|
|
303
|
+
>
|
|
304
|
+
isMulti
|
|
305
|
+
</code>{" "}
|
|
306
|
+
prop is not supported in the{" "}
|
|
307
|
+
<code
|
|
308
|
+
className="inline rounded-sm p-0.5"
|
|
309
|
+
style={{ backgroundColor: "#f0f0f0", color: "#333", fontSize: 15 }}
|
|
310
|
+
>
|
|
311
|
+
Dropdown
|
|
312
|
+
</code>{" "}
|
|
313
|
+
component. For multi-select functionality, use the{" "}
|
|
314
|
+
<a
|
|
315
|
+
href="/?path=/docs/components-multiselect--docs"
|
|
316
|
+
className="text-blue-primary inline rounded-sm p-0.5 hover:underline focus-visible:underline"
|
|
317
|
+
style={{
|
|
318
|
+
fontFamily: "monospace",
|
|
319
|
+
color: "#0000ff",
|
|
320
|
+
backgroundColor: "#f0f0f0",
|
|
321
|
+
fontSize: 15,
|
|
322
|
+
}}
|
|
323
|
+
>
|
|
324
|
+
{"<MultiSelect/>"}
|
|
325
|
+
</a>{" "}
|
|
326
|
+
component instead.
|
|
327
|
+
</p>
|
|
328
|
+
)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const Default = Template.bind({})
|
|
332
|
+
const Subtitle = Template.bind({})
|
|
333
|
+
const Grouped = Template.bind({})
|
|
334
|
+
const LongOption = Template.bind({})
|
|
335
|
+
const WithLeadingIcon = Template.bind({})
|
|
336
|
+
const Async = AsyncTemplate.bind({})
|
|
337
|
+
const Required = RequiredTemplate.bind({})
|
|
338
|
+
const MultiSelect = MultiSelectTemplate.bind({})
|
|
339
|
+
|
|
340
|
+
Default.args = {
|
|
341
|
+
options: mockOptions.default,
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
Grouped.args = {
|
|
345
|
+
options: mockOptions.grouped,
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
LongOption.args = {
|
|
349
|
+
options: mockOptions.long,
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
WithLeadingIcon.args = {
|
|
353
|
+
options: mockOptions.default,
|
|
354
|
+
leadingIcon: { icon: <DummyPhoneIcon className="size-5" /> },
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
Required.args = {
|
|
358
|
+
options: mockOptions.default,
|
|
359
|
+
required: true,
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
Subtitle.args = {
|
|
363
|
+
options: mockOptions.subTitle,
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Parameters
|
|
367
|
+
Subtitle.parameters = {
|
|
368
|
+
docs: {
|
|
369
|
+
description: {
|
|
370
|
+
story:
|
|
371
|
+
"Options can include a `subTitle` string to provide additional context below the label.",
|
|
372
|
+
},
|
|
373
|
+
},
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
Grouped.parameters = {
|
|
377
|
+
docs: {
|
|
378
|
+
description: {
|
|
379
|
+
story: "Options can be grouped under labelled headings by passing an array of group objects.",
|
|
380
|
+
},
|
|
381
|
+
},
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
LongOption.parameters = {
|
|
385
|
+
docs: {
|
|
386
|
+
description: {
|
|
387
|
+
story: "Long option labels wrap naturally within the menu without truncation.",
|
|
388
|
+
},
|
|
389
|
+
},
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
WithLeadingIcon.parameters = {
|
|
393
|
+
docs: {
|
|
394
|
+
description: {
|
|
395
|
+
story: "The `leadingIcon` prop renders an icon inside the field, aligned to the left.",
|
|
396
|
+
},
|
|
397
|
+
},
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
Async.parameters = {
|
|
401
|
+
docs: {
|
|
402
|
+
description: {
|
|
403
|
+
story:
|
|
404
|
+
"Use `AsyncDropdown` when options are fetched remotely. Pass a `loadOptions` function that takes the current input string and returns a promise of options. `defaultOptions` populates the list before the user types.",
|
|
405
|
+
},
|
|
406
|
+
},
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
Required.parameters = {
|
|
410
|
+
docs: {
|
|
411
|
+
description: {
|
|
412
|
+
story:
|
|
413
|
+
'Setting `required` hides the "Optional" tag and marks the field as required. The form will not submit until a value is selected.',
|
|
414
|
+
},
|
|
415
|
+
},
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
export { Default, Subtitle, Grouped, LongOption, WithLeadingIcon, Async, Required, MultiSelect }
|
|
419
|
+
export default meta
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import React from "react"
|
|
4
|
+
|
|
5
|
+
// Components
|
|
6
|
+
import { Container, ErrorMessage, Label } from "../TextField"
|
|
7
|
+
import ReactSelect from "react-select"
|
|
8
|
+
|
|
9
|
+
// Types
|
|
10
|
+
import type { DropdownSharedProps } from "./types"
|
|
11
|
+
|
|
12
|
+
// Utils
|
|
13
|
+
import { useDropdown } from "./useDropdown"
|
|
14
|
+
|
|
15
|
+
// Params
|
|
16
|
+
type SelectProps = React.ComponentPropsWithRef<typeof ReactSelect>
|
|
17
|
+
|
|
18
|
+
interface DropdownProps extends Omit<SelectProps, "isMulti">, DropdownSharedProps {
|
|
19
|
+
/**
|
|
20
|
+
* The `isMulti` prop is not supported in the `Dropdown` component. Please use the `MultiSelect` component for multi-select functionality.
|
|
21
|
+
*/
|
|
22
|
+
isMulti?: never
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/*
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
const Dropdown: React.FC<DropdownProps> = ({
|
|
34
|
+
id,
|
|
35
|
+
label,
|
|
36
|
+
leadingIcon,
|
|
37
|
+
showOptional = true,
|
|
38
|
+
className,
|
|
39
|
+
required,
|
|
40
|
+
error,
|
|
41
|
+
size = "md",
|
|
42
|
+
// variant = "dark",
|
|
43
|
+
onMenuOpen,
|
|
44
|
+
inputId: _inputId,
|
|
45
|
+
instanceId: _instanceId,
|
|
46
|
+
components: _components,
|
|
47
|
+
classNames,
|
|
48
|
+
openMenuOnFocus = true,
|
|
49
|
+
showSelectedCount = true,
|
|
50
|
+
...rest
|
|
51
|
+
}) => {
|
|
52
|
+
const { fieldId, instanceId, inputId, handleMenuOpen, stableComponents, selectClassNames } =
|
|
53
|
+
useDropdown({
|
|
54
|
+
id,
|
|
55
|
+
leadingIcon,
|
|
56
|
+
onMenuOpen,
|
|
57
|
+
required,
|
|
58
|
+
classNames,
|
|
59
|
+
size,
|
|
60
|
+
showSelectedCount,
|
|
61
|
+
components: _components,
|
|
62
|
+
inputId: _inputId,
|
|
63
|
+
instanceId: _instanceId,
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<div className={className} id={fieldId}>
|
|
68
|
+
<Container
|
|
69
|
+
size={size}
|
|
70
|
+
hasButton={false}
|
|
71
|
+
label={label}
|
|
72
|
+
showOptional={showOptional}
|
|
73
|
+
required={required}
|
|
74
|
+
variant={"dark"}
|
|
75
|
+
>
|
|
76
|
+
{label && (
|
|
77
|
+
<Label
|
|
78
|
+
inputId={inputId}
|
|
79
|
+
label={label}
|
|
80
|
+
showOptional={showOptional}
|
|
81
|
+
required={required}
|
|
82
|
+
variant={"dark"}
|
|
83
|
+
/>
|
|
84
|
+
)}
|
|
85
|
+
|
|
86
|
+
<ReactSelect
|
|
87
|
+
unstyled
|
|
88
|
+
inputId={inputId}
|
|
89
|
+
required={required}
|
|
90
|
+
onMenuOpen={handleMenuOpen}
|
|
91
|
+
openMenuOnFocus={openMenuOnFocus}
|
|
92
|
+
components={stableComponents}
|
|
93
|
+
classNames={selectClassNames}
|
|
94
|
+
instanceId={instanceId}
|
|
95
|
+
{...rest}
|
|
96
|
+
/>
|
|
97
|
+
</Container>
|
|
98
|
+
{error && <ErrorMessage id={inputId} error={error} />}
|
|
99
|
+
</div>
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export default Dropdown
|
|
104
|
+
export type { DropdownProps }
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Utils
|
|
2
|
+
import { components } from "react-select"
|
|
3
|
+
|
|
4
|
+
// Props
|
|
5
|
+
type MultiValueComponent = typeof components.MultiValue & { displayName?: string }
|
|
6
|
+
|
|
7
|
+
/*
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const MultiValueDisplayName = "MultiValue"
|
|
15
|
+
|
|
16
|
+
const MutliValueComponent: MultiValueComponent = components.MultiValue
|
|
17
|
+
MutliValueComponent.displayName = MultiValueDisplayName
|
|
18
|
+
|
|
19
|
+
export { MutliValueComponent as MultiValue, MultiValueDisplayName }
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import React, { useLayoutEffect, useMemo, useState, isValidElement } from "react"
|
|
4
|
+
|
|
5
|
+
// Components
|
|
6
|
+
import { Icon } from "../TextField"
|
|
7
|
+
|
|
8
|
+
// Types
|
|
9
|
+
import type { DropdownSize } from "../.."
|
|
10
|
+
|
|
11
|
+
// Utils
|
|
12
|
+
import {
|
|
13
|
+
type ValueContainerProps as ReactSelectValueContainerProps,
|
|
14
|
+
components,
|
|
15
|
+
} from "react-select"
|
|
16
|
+
import { MultiValueDisplayName } from "./MultiValue"
|
|
17
|
+
import { clsx } from "clsx"
|
|
18
|
+
import { useCombinedRefs } from "../../hooks"
|
|
19
|
+
import { extractDisplayName } from "../../utils/react"
|
|
20
|
+
import { isFunction } from "../../utils/common"
|
|
21
|
+
|
|
22
|
+
// Params
|
|
23
|
+
interface ValueContainerProps extends ReactSelectValueContainerProps {
|
|
24
|
+
leadingIcon?: { icon: React.ReactNode }
|
|
25
|
+
showSelectedCount?: boolean
|
|
26
|
+
size?: DropdownSize
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/*
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
const ValueContainer: React.FC<ValueContainerProps> = ({
|
|
36
|
+
children,
|
|
37
|
+
leadingIcon,
|
|
38
|
+
className,
|
|
39
|
+
showSelectedCount,
|
|
40
|
+
size = "md",
|
|
41
|
+
...rest
|
|
42
|
+
}) => {
|
|
43
|
+
// Refs
|
|
44
|
+
const combinedRef = useCombinedRefs<HTMLDivElement>(rest?.innerProps?.ref)
|
|
45
|
+
|
|
46
|
+
// State
|
|
47
|
+
const [isOverflowing, setIsOverflowing] = useState(false)
|
|
48
|
+
|
|
49
|
+
// Data
|
|
50
|
+
const childCount = useMemo(() => {
|
|
51
|
+
return React.Children.toArray(children)?.filter(i => {
|
|
52
|
+
return (
|
|
53
|
+
isValidElement(i) &&
|
|
54
|
+
isFunction(i.type) &&
|
|
55
|
+
extractDisplayName(i.type) === MultiValueDisplayName
|
|
56
|
+
)
|
|
57
|
+
}).length
|
|
58
|
+
}, [children])
|
|
59
|
+
|
|
60
|
+
// Effects
|
|
61
|
+
useLayoutEffect(() => {
|
|
62
|
+
const el = combinedRef?.current
|
|
63
|
+
if (!el) return
|
|
64
|
+
|
|
65
|
+
const observer = new ResizeObserver(() => {
|
|
66
|
+
setIsOverflowing(el.scrollWidth > el.clientWidth)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
observer.observe(el)
|
|
70
|
+
return () => observer.disconnect()
|
|
71
|
+
}, [combinedRef, childCount])
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<>
|
|
75
|
+
{!!leadingIcon?.icon && (
|
|
76
|
+
<Icon position="leading" fieldSize={size} variant="dark">
|
|
77
|
+
{leadingIcon.icon}
|
|
78
|
+
</Icon>
|
|
79
|
+
)}
|
|
80
|
+
<components.ValueContainer
|
|
81
|
+
{...rest}
|
|
82
|
+
className={clsx(
|
|
83
|
+
className,
|
|
84
|
+
leadingIcon?.icon && "pl-12",
|
|
85
|
+
isOverflowing && "[&>div:not(:last-child)]:opacity-0!",
|
|
86
|
+
)}
|
|
87
|
+
innerProps={{ ...rest?.innerProps, ref: combinedRef }}
|
|
88
|
+
>
|
|
89
|
+
{children}
|
|
90
|
+
</components.ValueContainer>
|
|
91
|
+
{showSelectedCount && isOverflowing && (
|
|
92
|
+
<div
|
|
93
|
+
className={clsx(
|
|
94
|
+
"absolute top-0 flex h-full shrink-0 items-center font-medium whitespace-nowrap text-neutral-100",
|
|
95
|
+
!leadingIcon?.icon && "rounded-l-full",
|
|
96
|
+
size === "sm" && [
|
|
97
|
+
"pl-3",
|
|
98
|
+
!!leadingIcon?.icon ? "left-8 w-[calc(100%-4rem)]" : "left-0 w-[calc(100%-2rem)]",
|
|
99
|
+
],
|
|
100
|
+
size === "md" && [
|
|
101
|
+
"pl-5",
|
|
102
|
+
!!leadingIcon?.icon ? "left-8 w-[calc(100%-4rem)]" : "left-0 w-[calc(100%-2.75rem)]",
|
|
103
|
+
],
|
|
104
|
+
)}
|
|
105
|
+
style={{ fontSize: "inherit" }}
|
|
106
|
+
>
|
|
107
|
+
{childCount} Selected
|
|
108
|
+
</div>
|
|
109
|
+
)}
|
|
110
|
+
</>
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export { ValueContainer }
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { TextFieldSize, TextFieldVariant } from "../TextField"
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export type DropdownSize = TextFieldSize
|
|
12
|
+
export type DropdownVariant = TextFieldVariant
|
|
13
|
+
|
|
14
|
+
export interface DropdownSharedProps {
|
|
15
|
+
label?: string
|
|
16
|
+
size?: DropdownSize
|
|
17
|
+
// variant?: DropdownVariant
|
|
18
|
+
showOptional?: boolean
|
|
19
|
+
leadingIcon?: { icon: React.ReactNode }
|
|
20
|
+
required?: boolean
|
|
21
|
+
error?: {
|
|
22
|
+
message: string
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* When `true`, the dropdown will display a count of selected options when multiple options are selected. This prop is only applicable when using the `MultiSelect` component.
|
|
26
|
+
* @default true
|
|
27
|
+
*/
|
|
28
|
+
showSelectedCount?: boolean
|
|
29
|
+
}
|