@recursica/adapter-mantine-v8 1.3.4 → 1.3.6

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 (60) hide show
  1. package/.storybook/commonArgTypes.ts +148 -0
  2. package/CHANGELOG.md +12 -0
  3. package/package.json +2 -2
  4. package/src/Introduction.stories.tsx +149 -0
  5. package/src/components/Accordion/Accordion.stories.tsx +189 -0
  6. package/src/components/AssistiveElement/AssistiveElement.stories.tsx +68 -0
  7. package/src/components/AutoComplete/AutoComplete.stories.tsx +336 -0
  8. package/src/components/Avatar/Avatar.stories.tsx +92 -0
  9. package/src/components/Badge/Badge.stories.tsx +78 -0
  10. package/src/components/Breadcrumb/Breadcrumb.stories.tsx +114 -0
  11. package/src/components/Button/Button.stories.tsx +192 -0
  12. package/src/components/Card/Card.stories.tsx +76 -0
  13. package/src/components/Checkbox/CHECKBOX_IMPLEMENTATION_NOTES.md +4 -0
  14. package/src/components/Checkbox/Checkbox.stories.tsx +97 -0
  15. package/src/components/Checkbox/CheckboxGroup.stories.tsx +139 -0
  16. package/src/components/Chip/Chip.stories.tsx +128 -0
  17. package/src/components/Container/Container.stories.tsx +118 -0
  18. package/src/components/DatePicker/DatePicker.stories.tsx +158 -0
  19. package/src/components/Dropdown/Dropdown.stories.tsx +252 -0
  20. package/src/components/FileInput/FileInput.stories.tsx +313 -0
  21. package/src/components/FileUpload/FileUpload.stories.tsx +362 -0
  22. package/src/components/Flex/Flex.stories.tsx +128 -0
  23. package/src/components/FormControlLayout/FormControlLayout.stories.tsx +118 -0
  24. package/src/components/FormControlWrapper/FormControlWrapper.stories.tsx +129 -0
  25. package/src/components/Group/Group.stories.tsx +120 -0
  26. package/src/components/Heading/Heading.stories.tsx +112 -0
  27. package/src/components/HoverCard/HoverCard.stories.tsx +172 -0
  28. package/src/components/Label/Label.stories.tsx +131 -0
  29. package/src/components/LayoutGrid/LayoutGrid.stories.tsx +61 -0
  30. package/src/components/Link/Link.stories.tsx +58 -0
  31. package/src/components/Loader/Loader.stories.tsx +123 -0
  32. package/src/components/Menu/Menu.stories.tsx +370 -0
  33. package/src/components/Modal/Modal.stories.tsx +105 -0
  34. package/src/components/NativeInputA11y.dom.test.tsx +60 -0
  35. package/src/components/NumberInput/NumberInput.stories.tsx +96 -0
  36. package/src/components/Pagination/Pagination.stories.tsx +48 -0
  37. package/src/components/Panel/Panel.stories.tsx +219 -0
  38. package/src/components/Popover/Popover.stories.tsx +133 -0
  39. package/src/components/Radio/RADIO_IMPLEMENTATION_NOTES.md +5 -0
  40. package/src/components/Radio/Radio.stories.tsx +93 -0
  41. package/src/components/Radio/RadioGroup.stories.tsx +117 -0
  42. package/src/components/ReadOnlyField/ReadOnlyField.stories.tsx +163 -0
  43. package/src/components/SegmentedControl/SegmentedControl.stories.tsx +106 -0
  44. package/src/components/Slider/Slider.stories.tsx +282 -0
  45. package/src/components/Stack/Stack.stories.tsx +108 -0
  46. package/src/components/Stepper/Stepper.stories.tsx +148 -0
  47. package/src/components/Switch/SWITCH_IMPLEMENTATION_NOTES.md +4 -0
  48. package/src/components/Switch/Switch.stories.tsx +114 -0
  49. package/src/components/Switch/SwitchGroup.stories.tsx +132 -0
  50. package/src/components/Table/Table.stories.tsx +140 -0
  51. package/src/components/Tabs/Tabs.stories.tsx +154 -0
  52. package/src/components/Text/Text.stories.tsx +148 -0
  53. package/src/components/TextArea/TextArea.stories.tsx +94 -0
  54. package/src/components/TextField/TextField.stories.tsx +162 -0
  55. package/src/components/TimePicker/TimePicker.stories.tsx +141 -0
  56. package/src/components/Timeline/Timeline.stories.tsx +128 -0
  57. package/src/components/Toast/Toast.stories.tsx +67 -0
  58. package/src/components/Tooltip/Tooltip.stories.tsx +165 -0
  59. package/src/components/TransferList/TransferList.stories.tsx +121 -0
  60. package/src/components/Tree/Tree.stories.tsx +104 -0
@@ -0,0 +1,370 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { Menu } from "./Menu";
3
+ import { Button } from "../Button";
4
+
5
+ /**
6
+ * Menu story axes:
7
+ * - trigger: click (default) vs hover behavior
8
+ * - position: dropdown alignment relative to target
9
+ * - withArrow: arrow indicator on the dropdown
10
+ * - content variations: icons, dividers, labels, disabled items, submenus
11
+ * - layer: tested via the global Layer decorator (withLayer/layer args)
12
+ */
13
+
14
+ const SettingsIcon = () => (
15
+ <svg
16
+ xmlns="http://www.w3.org/2000/svg"
17
+ width="14"
18
+ height="14"
19
+ viewBox="0 0 24 24"
20
+ fill="none"
21
+ stroke="currentColor"
22
+ strokeWidth="2"
23
+ strokeLinecap="round"
24
+ strokeLinejoin="round"
25
+ >
26
+ <circle cx="12" cy="12" r="3" />
27
+ <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" />
28
+ </svg>
29
+ );
30
+
31
+ const SearchIcon = () => (
32
+ <svg
33
+ xmlns="http://www.w3.org/2000/svg"
34
+ width="14"
35
+ height="14"
36
+ viewBox="0 0 24 24"
37
+ fill="none"
38
+ stroke="currentColor"
39
+ strokeWidth="2"
40
+ strokeLinecap="round"
41
+ strokeLinejoin="round"
42
+ >
43
+ <circle cx="11" cy="11" r="8" />
44
+ <line x1="21" y1="21" x2="16.65" y2="16.65" />
45
+ </svg>
46
+ );
47
+
48
+ const MessageIcon = () => (
49
+ <svg
50
+ xmlns="http://www.w3.org/2000/svg"
51
+ width="14"
52
+ height="14"
53
+ viewBox="0 0 24 24"
54
+ fill="none"
55
+ stroke="currentColor"
56
+ strokeWidth="2"
57
+ strokeLinecap="round"
58
+ strokeLinejoin="round"
59
+ >
60
+ <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
61
+ </svg>
62
+ );
63
+
64
+ const ImageIcon = () => (
65
+ <svg
66
+ xmlns="http://www.w3.org/2000/svg"
67
+ width="14"
68
+ height="14"
69
+ viewBox="0 0 24 24"
70
+ fill="none"
71
+ stroke="currentColor"
72
+ strokeWidth="2"
73
+ strokeLinecap="round"
74
+ strokeLinejoin="round"
75
+ >
76
+ <rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
77
+ <circle cx="8.5" cy="8.5" r="1.5" />
78
+ <polyline points="21 15 16 10 5 21" />
79
+ </svg>
80
+ );
81
+
82
+ const TrashIcon = () => (
83
+ <svg
84
+ xmlns="http://www.w3.org/2000/svg"
85
+ width="14"
86
+ height="14"
87
+ viewBox="0 0 24 24"
88
+ fill="none"
89
+ stroke="currentColor"
90
+ strokeWidth="2"
91
+ strokeLinecap="round"
92
+ strokeLinejoin="round"
93
+ >
94
+ <polyline points="3 6 5 6 21 6" />
95
+ <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
96
+ </svg>
97
+ );
98
+
99
+ const ArrowsIcon = () => (
100
+ <svg
101
+ xmlns="http://www.w3.org/2000/svg"
102
+ width="14"
103
+ height="14"
104
+ viewBox="0 0 24 24"
105
+ fill="none"
106
+ stroke="currentColor"
107
+ strokeWidth="2"
108
+ strokeLinecap="round"
109
+ strokeLinejoin="round"
110
+ >
111
+ <polyline points="17 1 21 5 17 9" />
112
+ <path d="M3 11V9a4 4 0 0 1 4-4h14" />
113
+ <polyline points="7 23 3 19 7 15" />
114
+ <path d="M21 13v2a4 4 0 0 1-4 4H3" />
115
+ </svg>
116
+ );
117
+
118
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
119
+ type MenuStoryArgs = Record<string, any>;
120
+
121
+ const meta: Meta = {
122
+ title: "UI-Kit/Menu",
123
+ component: Menu,
124
+ tags: ["autodocs"],
125
+ argTypes: {
126
+ trigger: {
127
+ control: "select",
128
+ options: ["click", "hover", "click-hover"],
129
+ description:
130
+ "Determines how the menu is triggered: click (default), hover, or both.",
131
+ },
132
+ position: {
133
+ control: "select",
134
+ options: [
135
+ "bottom",
136
+ "bottom-start",
137
+ "bottom-end",
138
+ "top",
139
+ "top-start",
140
+ "top-end",
141
+ "left",
142
+ "left-start",
143
+ "left-end",
144
+ "right",
145
+ "right-start",
146
+ "right-end",
147
+ ],
148
+ description: "The position of the dropdown relative to the target.",
149
+ },
150
+ withArrow: {
151
+ control: "boolean",
152
+ description: "Whether to display an arrow pointing to the target.",
153
+ },
154
+ offset: {
155
+ control: "number",
156
+ description:
157
+ "Distance in px between the dropdown and the target element.",
158
+ },
159
+ opened: {
160
+ control: "boolean",
161
+ description:
162
+ "Controlled open state. Leave undefined for uncontrolled behavior.",
163
+ },
164
+ maxHeight: {
165
+ control: "number",
166
+ description:
167
+ "Overrides the token-driven dropdown max-height with an explicit pixel value.",
168
+ },
169
+ },
170
+ parameters: {
171
+ docs: {
172
+ description: {
173
+ component: `
174
+ The \`Menu\` component combines a list of secondary actions into a single interactive overlay area, wrapping Mantine's composable \`Menu\` while enforcing strict Recursica design token styling.
175
+
176
+ ### Composable API
177
+
178
+ Menu uses a dot-notation composition pattern:
179
+ - \`<Menu>\` — Root container managing open/close state
180
+ - \`<Menu.Target>\` — Wrapper for the trigger element (must be a single element supporting ref)
181
+ - \`<Menu.Dropdown>\` — The popup panel
182
+ - \`<Menu.Item>\` — An actionable item with optional \`leftSection\` / \`rightSection\`
183
+ - \`<Menu.Divider>\` — A visual separator
184
+ - \`<Menu.Label>\` — A non-interactive section header
185
+
186
+ ### Sub-menus
187
+ Nested menus are supported via \`<Menu.Sub>\`, \`<Menu.Sub.Target>\`, \`<Menu.Sub.Item>\`, and \`<Menu.Sub.Dropdown>\`.
188
+
189
+ ### Design Token Limitations
190
+ Mantine's \`color\` prop on \`Menu.Item\` is stripped in strict mode to enforce design token adherence. Use \`overStyled={true}\` to bypass this restriction.
191
+ `,
192
+ },
193
+ },
194
+ },
195
+ };
196
+
197
+ export default meta;
198
+
199
+ type Story = StoryObj<MenuStoryArgs>;
200
+
201
+ export const Default: Story = {
202
+ args: {
203
+ trigger: "click",
204
+ position: "bottom-start",
205
+ withArrow: false,
206
+ offset: 5,
207
+ // Rendered open by default so this story is diffable against the MUI adapter
208
+ // without an interaction step.
209
+ opened: true,
210
+ },
211
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
212
+ render: ({ withLayer, layer, ...args }: MenuStoryArgs) => {
213
+ return (
214
+ <Menu {...args}>
215
+ <Menu.Target>
216
+ <Button variant="solid">Toggle Menu</Button>
217
+ </Menu.Target>
218
+
219
+ <Menu.Dropdown>
220
+ <Menu.Label>Application</Menu.Label>
221
+ <Menu.Item leftSection={<SettingsIcon />}>Settings</Menu.Item>
222
+ <Menu.Item leftSection={<MessageIcon />}>Messages</Menu.Item>
223
+ <Menu.Item leftSection={<ImageIcon />}>Gallery</Menu.Item>
224
+ <Menu.Item leftSection={<SearchIcon />}>Search</Menu.Item>
225
+
226
+ <Menu.Divider />
227
+
228
+ <Menu.Label>Danger zone</Menu.Label>
229
+ <Menu.Item leftSection={<ArrowsIcon />}>Transfer my data</Menu.Item>
230
+ <Menu.Item leftSection={<TrashIcon />}>Delete my account</Menu.Item>
231
+ </Menu.Dropdown>
232
+ </Menu>
233
+ );
234
+ },
235
+ };
236
+
237
+ export const WithDisabledItems: Story = {
238
+ args: {
239
+ position: "bottom-start",
240
+ // Rendered open by default so this story is diffable against the MUI adapter
241
+ // without an interaction step.
242
+ opened: true,
243
+ },
244
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
245
+ render: ({ withLayer, layer, ...args }: MenuStoryArgs) => {
246
+ return (
247
+ <Menu {...args}>
248
+ <Menu.Target>
249
+ <Button variant="solid">Menu with Disabled</Button>
250
+ </Menu.Target>
251
+
252
+ <Menu.Dropdown>
253
+ <Menu.Item leftSection={<SettingsIcon />}>Settings</Menu.Item>
254
+ <Menu.Item leftSection={<SearchIcon />} disabled>
255
+ Search (disabled)
256
+ </Menu.Item>
257
+ <Menu.Item leftSection={<MessageIcon />}>Messages</Menu.Item>
258
+ <Menu.Item leftSection={<TrashIcon />} disabled>
259
+ Delete (disabled)
260
+ </Menu.Item>
261
+ </Menu.Dropdown>
262
+ </Menu>
263
+ );
264
+ },
265
+ };
266
+
267
+ export const WithSubmenus: Story = {
268
+ args: {
269
+ position: "bottom-start",
270
+ // Rendered open by default so this story is diffable against the MUI adapter
271
+ // without an interaction step.
272
+ opened: true,
273
+ },
274
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
275
+ render: ({ withLayer, layer, ...args }: MenuStoryArgs) => {
276
+ return (
277
+ <Menu {...args}>
278
+ <Menu.Target>
279
+ <Button variant="solid">Menu with Submenus</Button>
280
+ </Menu.Target>
281
+
282
+ <Menu.Dropdown>
283
+ <Menu.Item>Dashboard</Menu.Item>
284
+ <Menu.Sub>
285
+ <Menu.Sub.Target>
286
+ <Menu.Sub.Item>Products</Menu.Sub.Item>
287
+ </Menu.Sub.Target>
288
+ <Menu.Sub.Dropdown>
289
+ <Menu.Item>All products</Menu.Item>
290
+ <Menu.Item>Categories</Menu.Item>
291
+ <Menu.Item>Tags</Menu.Item>
292
+ </Menu.Sub.Dropdown>
293
+ </Menu.Sub>
294
+ <Menu.Sub>
295
+ <Menu.Sub.Target>
296
+ <Menu.Sub.Item>Orders</Menu.Sub.Item>
297
+ </Menu.Sub.Target>
298
+ <Menu.Sub.Dropdown>
299
+ <Menu.Item>Open</Menu.Item>
300
+ <Menu.Item>Completed</Menu.Item>
301
+ <Menu.Item>Cancelled</Menu.Item>
302
+ </Menu.Sub.Dropdown>
303
+ </Menu.Sub>
304
+ </Menu.Dropdown>
305
+ </Menu>
306
+ );
307
+ },
308
+ };
309
+
310
+ export const WithMaxHeight: Story = {
311
+ args: {
312
+ position: "bottom-start",
313
+ maxHeight: 160,
314
+ // Rendered open by default so this story is diffable against the MUI adapter
315
+ // without an interaction step.
316
+ opened: true,
317
+ },
318
+ parameters: {
319
+ docs: {
320
+ description: {
321
+ story:
322
+ "`maxHeight` overrides the token-driven dropdown max-height with an explicit pixel value, scrolling the item list once it's exceeded.",
323
+ },
324
+ },
325
+ },
326
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
327
+ render: ({ withLayer, layer, ...args }: MenuStoryArgs) => {
328
+ return (
329
+ <Menu {...args}>
330
+ <Menu.Target>
331
+ <Button variant="solid">Menu with maxHeight</Button>
332
+ </Menu.Target>
333
+
334
+ <Menu.Dropdown>
335
+ <Menu.Item leftSection={<SettingsIcon />}>Settings</Menu.Item>
336
+ <Menu.Item leftSection={<MessageIcon />}>Messages</Menu.Item>
337
+ <Menu.Item leftSection={<ImageIcon />}>Gallery</Menu.Item>
338
+ <Menu.Item leftSection={<SearchIcon />}>Search</Menu.Item>
339
+ <Menu.Item leftSection={<ArrowsIcon />}>Transfer my data</Menu.Item>
340
+ <Menu.Item leftSection={<TrashIcon />}>Delete my account</Menu.Item>
341
+ </Menu.Dropdown>
342
+ </Menu>
343
+ );
344
+ },
345
+ };
346
+
347
+ export const HoverTrigger: Story = {
348
+ args: {
349
+ trigger: "click-hover",
350
+ position: "bottom-start",
351
+ openDelay: 100,
352
+ closeDelay: 400,
353
+ },
354
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
355
+ render: ({ withLayer, layer, ...args }: MenuStoryArgs) => {
356
+ return (
357
+ <Menu {...args}>
358
+ <Menu.Target>
359
+ <Button variant="solid">Hover or Click</Button>
360
+ </Menu.Target>
361
+
362
+ <Menu.Dropdown>
363
+ <Menu.Item leftSection={<SettingsIcon />}>Settings</Menu.Item>
364
+ <Menu.Item leftSection={<MessageIcon />}>Messages</Menu.Item>
365
+ <Menu.Item leftSection={<ImageIcon />}>Gallery</Menu.Item>
366
+ </Menu.Dropdown>
367
+ </Menu>
368
+ );
369
+ },
370
+ };
@@ -0,0 +1,105 @@
1
+ import { useState } from "react";
2
+ import type { Meta, StoryObj } from "@storybook/react";
3
+ import { Modal, type ModalProps } from "./Modal";
4
+ import { Button } from "../Button";
5
+
6
+ const meta: Meta<typeof Modal> = {
7
+ title: "UI-Kit/Modal",
8
+ component: Modal,
9
+ tags: ["autodocs"],
10
+ argTypes: {
11
+ opened: { table: { disable: true } },
12
+ onClose: { table: { disable: true } },
13
+ defaultChecked: { table: { disable: true } },
14
+ },
15
+ };
16
+
17
+ export default meta;
18
+
19
+ type Story = StoryObj<typeof Modal>;
20
+
21
+ const DefaultWrapper = (args: ModalProps) => {
22
+ // Starts opened so the modal is visible without pressing a button first.
23
+ const [opened, setOpened] = useState(true);
24
+ return (
25
+ <>
26
+ <Modal {...args} opened={opened} onClose={() => setOpened(false)}>
27
+ Please log in to continue accessing this feature.
28
+ <Modal.Footer>
29
+ <Button variant="outline" onClick={() => setOpened(false)}>
30
+ Cancel
31
+ </Button>
32
+ <Button onClick={() => setOpened(false)}>Confirm</Button>
33
+ </Modal.Footer>
34
+ </Modal>
35
+ <Button onClick={() => setOpened(true)}>Open Modal</Button>
36
+ </>
37
+ );
38
+ };
39
+
40
+ export const Default: Story = {
41
+ args: {
42
+ title: "Authentication Required",
43
+ },
44
+ render: (args) => <DefaultWrapper {...args} />,
45
+ };
46
+
47
+ const LongTitleWrapper = (args: ModalProps) => {
48
+ const [opened, setOpened] = useState(true);
49
+ return (
50
+ <>
51
+ <Modal {...args} opened={opened} onClose={() => setOpened(false)}>
52
+ The title above is longer than the header can display, so it truncates
53
+ with an ellipsis instead of wrapping onto a second line.
54
+ <Modal.Footer>
55
+ <Button onClick={() => setOpened(false)}>Got it</Button>
56
+ </Modal.Footer>
57
+ </Modal>
58
+ <Button onClick={() => setOpened(true)}>Open Modal</Button>
59
+ </>
60
+ );
61
+ };
62
+
63
+ export const LongTitle: Story = {
64
+ args: {
65
+ title:
66
+ "This Modal Title Is Deliberately Long Enough To Exceed The Available Header Width",
67
+ },
68
+ render: (args) => <LongTitleWrapper {...args} />,
69
+ };
70
+
71
+ const ScrollingWrapper = (args: ModalProps) => {
72
+ // Starts opened so the modal is visible without pressing a button first.
73
+ const [opened, setOpened] = useState(true);
74
+ return (
75
+ <>
76
+ <Modal {...args} opened={opened} onClose={() => setOpened(false)}>
77
+ <p>
78
+ This modal demonstrates the dynamically injected scroll dividers. When
79
+ this body content overflows, the borders between the Header and Footer
80
+ automatically appear to define the scrolling boundary.
81
+ </p>
82
+ {Array(20)
83
+ .fill(0)
84
+ .map((_, i) => (
85
+ <p key={i}>Scrolling content block {i + 1}...</p>
86
+ ))}
87
+ {/* The new Footer abstraction */}
88
+ <Modal.Footer>
89
+ <Button variant="outline" onClick={() => setOpened(false)}>
90
+ Decline
91
+ </Button>
92
+ <Button onClick={() => setOpened(false)}>Accept Terms</Button>
93
+ </Modal.Footer>
94
+ </Modal>
95
+ <Button onClick={() => setOpened(true)}>Open Scrolling Modal</Button>
96
+ </>
97
+ );
98
+ };
99
+
100
+ export const ScrollingContent: Story = {
101
+ args: {
102
+ title: "Terms and Conditions",
103
+ },
104
+ render: (args) => <ScrollingWrapper {...args} />,
105
+ };
@@ -0,0 +1,60 @@
1
+ import React from "react";
2
+ import { describe, it, expect } from "vitest";
3
+ import { createRoot } from "react-dom/client";
4
+ import { flushSync } from "react-dom";
5
+ import { Checkbox } from "./Checkbox/Checkbox";
6
+ import { Radio } from "./Radio/Radio";
7
+ import { Switch } from "./Switch/Switch";
8
+ import { MantineProvider } from "@mantine/core";
9
+ const wrap = (n: React.ReactElement) => <MantineProvider>{n}</MantineProvider>;
10
+
11
+ // Where the naming attributes land matters, not just that they render: a screen reader reads
12
+ // the native <input>, so aria-* on a wrapper leaves the control unnamed.
13
+ function render(node: React.ReactElement): HTMLElement {
14
+ const container = document.createElement("div");
15
+ document.body.appendChild(container);
16
+ flushSync(() => createRoot(container).render(wrap(node)));
17
+ return container;
18
+ }
19
+
20
+ const controls = [
21
+ ["Checkbox", Checkbox],
22
+ ["Radio", Radio],
23
+ ["Switch", Switch],
24
+ ] as const;
25
+
26
+ describe.each(controls)("%s accessible name", (_name, Control) => {
27
+ // The union type under test is already enforced at compile time; cast to exercise each branch.
28
+ const C = Control as unknown as React.ComponentType<Record<string, unknown>>;
29
+
30
+ it("puts aria-label on the native input only", () => {
31
+ const c = render(<C aria-label="Select all" />);
32
+ expect(c.querySelector("input")!.getAttribute("aria-label")).toBe(
33
+ "Select all",
34
+ );
35
+ expect(c.querySelectorAll("[aria-label]")).toHaveLength(1);
36
+ });
37
+
38
+ it("puts aria-labelledby and aria-describedby on the native input only", () => {
39
+ const c = render(<C aria-labelledby="a" aria-describedby="b" />);
40
+ const input = c.querySelector("input")!;
41
+ expect(input.getAttribute("aria-labelledby")).toBe("a");
42
+ expect(input.getAttribute("aria-describedby")).toBe("b");
43
+ expect(c.querySelectorAll("[aria-labelledby]")).toHaveLength(1);
44
+ expect(c.querySelectorAll("[aria-describedby]")).toHaveLength(1);
45
+ });
46
+
47
+ it("links a visible label to the input without a caller-supplied id", () => {
48
+ const c = render(<C label="Hello" />);
49
+ const input = c.querySelector("input")!;
50
+ const label = c.querySelector("label")!;
51
+ expect(input.id).not.toBe("");
52
+ expect(label.getAttribute("for")).toBe(input.id);
53
+ });
54
+
55
+ it("keeps a caller-supplied id", () => {
56
+ const c = render(<C label="Hello" id="mine" />);
57
+ expect(c.querySelector("input")!.id).toBe("mine");
58
+ expect(c.querySelector("label")!.getAttribute("for")).toBe("mine");
59
+ });
60
+ });
@@ -0,0 +1,96 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { NumberInput } from "./NumberInput";
3
+
4
+ const meta: Meta<typeof NumberInput> = {
5
+ title: "UI-Kit/NumberInput",
6
+ component: NumberInput,
7
+ tags: ["autodocs"],
8
+ argTypes: {
9
+ label: { control: "text" },
10
+ assistiveText: { control: "text" },
11
+ disabled: { control: "boolean" },
12
+ error: { control: "boolean" },
13
+ readOnly: { control: "boolean" },
14
+ required: { control: "boolean" },
15
+ hideControls: { control: "boolean" },
16
+ formLayout: {
17
+ control: "select",
18
+ options: ["stacked", "side-by-side"],
19
+ },
20
+ labelSize: {
21
+ control: "select",
22
+ options: ["small", "default", "large"],
23
+ },
24
+ assistiveWithIcon: { table: { disable: true } },
25
+ labelOptionalText: { table: { disable: true } },
26
+ labelWithEditIcon: { table: { disable: true } },
27
+ onLabelEditClick: { table: { disable: true } },
28
+ emptyValueComponent: { table: { disable: true } },
29
+ },
30
+ };
31
+
32
+ export default meta;
33
+
34
+ type Story = StoryObj<typeof NumberInput>;
35
+
36
+ export const Default: Story = {
37
+ args: {
38
+ label: "Amount",
39
+ placeholder: "Enter an amount",
40
+ assistiveText: "Must be greater than 0",
41
+ defaultValue: 10,
42
+ min: 0,
43
+ max: 100,
44
+ },
45
+ };
46
+
47
+ export const SideBySideLayout: Story = {
48
+ args: {
49
+ ...Default.args,
50
+ formLayout: "side-by-side",
51
+ },
52
+ };
53
+
54
+ export const States: Story = {
55
+ render: () => (
56
+ <div
57
+ style={{
58
+ display: "flex",
59
+ flexDirection: "column",
60
+ gap: "1rem",
61
+ maxWidth: 400,
62
+ }}
63
+ >
64
+ <NumberInput label="Default" placeholder="Enter a number" />
65
+ <NumberInput label="Disabled" placeholder="Disabled input" disabled />
66
+ <NumberInput label="Error" placeholder="Error state" error />
67
+ <NumberInput label="Read Only" value={42} readOnly />
68
+ <NumberInput label="Required" required />
69
+ </div>
70
+ ),
71
+ };
72
+
73
+ export const WithLeftIcon: Story = {
74
+ args: {
75
+ label: "Price",
76
+ placeholder: "0.00",
77
+ leftSection: <span>$</span>,
78
+ },
79
+ };
80
+
81
+ export const WithRightIcon: Story = {
82
+ args: {
83
+ label: "Percentage",
84
+ placeholder: "0",
85
+ rightSection: <span>%</span>,
86
+ hideControls: true, // Typically hiding controls if rightSection is occupied
87
+ },
88
+ };
89
+
90
+ export const HiddenControls: Story = {
91
+ args: {
92
+ label: "Zip Code",
93
+ placeholder: "Enter zip code",
94
+ hideControls: true,
95
+ },
96
+ };
@@ -0,0 +1,48 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { Pagination } from "./Pagination";
3
+
4
+ const meta: Meta<typeof Pagination> = {
5
+ title: "UI-Kit/Pagination",
6
+ component: Pagination,
7
+ tags: ["autodocs"],
8
+ argTypes: {
9
+ total: { control: "number" },
10
+ withEdges: { control: "boolean" },
11
+ withControls: { control: "boolean" },
12
+ disabled: { control: "boolean" },
13
+ withLabels: {
14
+ control: "boolean",
15
+ description: "Toggle text labels alongside icons",
16
+ },
17
+ defaultChecked: { table: { disable: true } },
18
+ },
19
+ args: {
20
+ total: 10,
21
+ withEdges: false,
22
+ withControls: true,
23
+ withLabels: false,
24
+ },
25
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unused-vars
26
+ render: ({ withLayer, layer, ...args }: any) => {
27
+ return <Pagination {...args} />;
28
+ },
29
+ };
30
+
31
+ export default meta;
32
+
33
+ type Story = StoryObj<typeof Pagination>;
34
+
35
+ export const Default: Story = {};
36
+
37
+ export const WithEdges: Story = {
38
+ args: {
39
+ withEdges: true,
40
+ },
41
+ };
42
+
43
+ export const WithTextLabels: Story = {
44
+ args: {
45
+ withEdges: true,
46
+ withLabels: true,
47
+ },
48
+ };