@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.
Files changed (121) hide show
  1. package/.prettierrc.cjs +25 -0
  2. package/.storybook/components/ActionButton.tsx +44 -0
  3. package/.storybook/components/DummyIcons.tsx +47 -0
  4. package/.storybook/components/index.ts +2 -0
  5. package/.storybook/docs/blocks/ImportStatement.tsx +52 -0
  6. package/.storybook/docs/blocks/index.ts +1 -0
  7. package/.storybook/docs/page.tsx +41 -0
  8. package/.storybook/main.ts +21 -0
  9. package/.storybook/postcss.config.cjs +8 -0
  10. package/.storybook/preview-body.html +20 -0
  11. package/.storybook/preview-head.html +6 -0
  12. package/.storybook/preview.tsx +30 -0
  13. package/.storybook/tailwind.css +6 -0
  14. package/.storybook/utils/index.ts +2 -0
  15. package/.storybook/utils/renderAsReact.tsx +30 -0
  16. package/.storybook/utils/renderDocsWithProps.tsx +22 -0
  17. package/@types/markdown.d.ts +4 -0
  18. package/README.md +3 -0
  19. package/eslint.config.js +91 -0
  20. package/package.json +63 -0
  21. package/postcss.config.cjs +8 -0
  22. package/scripts/release.sh +76 -0
  23. package/src/components/Button/Button.stories.tsx +314 -0
  24. package/src/components/Button/Button.tsx +132 -0
  25. package/src/components/Button/index.ts +2 -0
  26. package/src/components/Button/types.ts +19 -0
  27. package/src/components/Checkbox/Checkbox.stories.tsx +152 -0
  28. package/src/components/Checkbox/Checkbox.tsx +90 -0
  29. package/src/components/Checkbox/index.ts +2 -0
  30. package/src/components/Checkbox/types.ts +6 -0
  31. package/src/components/Chip/Chip.stories.tsx +146 -0
  32. package/src/components/Chip/Chip.tsx +59 -0
  33. package/src/components/Chip/index.ts +2 -0
  34. package/src/components/Chip/types.ts +6 -0
  35. package/src/components/Drawer/Drawer.docs.md +88 -0
  36. package/src/components/Drawer/Drawer.stories.tsx +239 -0
  37. package/src/components/Drawer/Drawer.tsx +194 -0
  38. package/src/components/Drawer/index.ts +3 -0
  39. package/src/components/Drawer/types.ts +3 -0
  40. package/src/components/Dropdown/AsyncDropdown.tsx +105 -0
  41. package/src/components/Dropdown/Dropdown.docs.md +33 -0
  42. package/src/components/Dropdown/Dropdown.stories.tsx +419 -0
  43. package/src/components/Dropdown/Dropdown.tsx +104 -0
  44. package/src/components/Dropdown/MultiValue.tsx +19 -0
  45. package/src/components/Dropdown/ValueContainer.tsx +114 -0
  46. package/src/components/Dropdown/index.ts +4 -0
  47. package/src/components/Dropdown/types.ts +29 -0
  48. package/src/components/Dropdown/useDropdown.tsx +257 -0
  49. package/src/components/Logo/Logo.stories.tsx +130 -0
  50. package/src/components/Logo/Logo.tsx +80 -0
  51. package/src/components/Logo/index.ts +2 -0
  52. package/src/components/Modal/Modal.docs.md +94 -0
  53. package/src/components/Modal/Modal.stories.tsx +318 -0
  54. package/src/components/Modal/Modal.tsx +217 -0
  55. package/src/components/Modal/index.ts +1 -0
  56. package/src/components/MultiSelect/AsyncMultiSelect.tsx +47 -0
  57. package/src/components/MultiSelect/MultiSelect.docs.md +37 -0
  58. package/src/components/MultiSelect/MultiSelect.stories.tsx +493 -0
  59. package/src/components/MultiSelect/MultiSelect.tsx +81 -0
  60. package/src/components/MultiSelect/index.ts +2 -0
  61. package/src/components/Notification/Notification.stories.tsx +158 -0
  62. package/src/components/Notification/Notification.tsx +110 -0
  63. package/src/components/Notification/index.ts +1 -0
  64. package/src/components/Notification/types.ts +11 -0
  65. package/src/components/Notifications/Notifications.docs.md +103 -0
  66. package/src/components/Notifications/Notifications.stories.tsx +159 -0
  67. package/src/components/Notifications/Notifications.tsx +90 -0
  68. package/src/components/Notifications/NotificationsContext.tsx +90 -0
  69. package/src/components/Notifications/index.ts +7 -0
  70. package/src/components/Select/Select.stories.tsx +234 -0
  71. package/src/components/Select/Select.tsx +129 -0
  72. package/src/components/Select/index.ts +2 -0
  73. package/src/components/Select/types.ts +1 -0
  74. package/src/components/Spinner/Spinner.stories.tsx +55 -0
  75. package/src/components/Spinner/Spinner.tsx +48 -0
  76. package/src/components/Spinner/index.ts +2 -0
  77. package/src/components/Spinner/types.ts +8 -0
  78. package/src/components/TextArea/TextArea.stories.tsx +243 -0
  79. package/src/components/TextArea/TextArea.tsx +133 -0
  80. package/src/components/TextArea/index.ts +2 -0
  81. package/src/components/TextArea/types.ts +4 -0
  82. package/src/components/TextField/Container.tsx +68 -0
  83. package/src/components/TextField/ErrorMessage.tsx +37 -0
  84. package/src/components/TextField/Icon.tsx +77 -0
  85. package/src/components/TextField/Label.tsx +56 -0
  86. package/src/components/TextField/NotchBorder.tsx +67 -0
  87. package/src/components/TextField/index.ts +14 -0
  88. package/src/components/TextField/types.ts +15 -0
  89. package/src/components/TextField/useInputKeyboardFocus.tsx +63 -0
  90. package/src/components/TextInput/TextInput.stories.tsx +384 -0
  91. package/src/components/TextInput/TextInput.tsx +255 -0
  92. package/src/components/TextInput/index.ts +2 -0
  93. package/src/components/TextInput/types.ts +4 -0
  94. package/src/components/Toggle/Toggle.stories.tsx +142 -0
  95. package/src/components/Toggle/Toggle.tsx +69 -0
  96. package/src/components/Toggle/index.ts +1 -0
  97. package/src/hooks/index.ts +6 -0
  98. package/src/hooks/useCombinedRefs.ts +37 -0
  99. package/src/hooks/useEventListener.ts +87 -0
  100. package/src/hooks/useFocusTrap/createAriaHider.ts +62 -0
  101. package/src/hooks/useFocusTrap/index.ts +1 -0
  102. package/src/hooks/useFocusTrap/scopeTab.ts +46 -0
  103. package/src/hooks/useFocusTrap/tabbable.ts +107 -0
  104. package/src/hooks/useFocusTrap/useFocusTrap.ts +97 -0
  105. package/src/hooks/useIsomorphicLayoutEffect.ts +14 -0
  106. package/src/hooks/useLockBodyScroll.ts +24 -0
  107. package/src/hooks/useOnClickOutside.ts +53 -0
  108. package/src/index.ts +22 -0
  109. package/src/tailwind.css +4 -0
  110. package/src/types/helpers.ts +11 -0
  111. package/src/types/polymorphic.ts +39 -0
  112. package/src/utils/animation/variants.ts +21 -0
  113. package/src/utils/array/index.ts +1 -0
  114. package/src/utils/array/uniqBy.ts +12 -0
  115. package/src/utils/common/index.ts +1 -0
  116. package/src/utils/common/isFunction.ts +17 -0
  117. package/src/utils/react/extractDisplayName.ts +15 -0
  118. package/src/utils/react/index.ts +1 -0
  119. package/tsconfig.json +16 -0
  120. package/tsconfig.production.json +19 -0
  121. 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,4 @@
1
+ export { default, type DropdownProps } from "./Dropdown"
2
+ export { default as AsyncDropdown, type AsyncDropdownProps } from "./AsyncDropdown"
3
+
4
+ export type { DropdownSharedProps, DropdownSize, DropdownVariant } from "./types"
@@ -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
+ }