tharaday 0.6.3 → 0.7.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/.release-it.json +16 -0
- package/.storybook/main.ts +1 -1
- package/.storybook/preview.ts +2 -0
- package/dist/ds.css +1 -1
- package/dist/ds.js +38 -19
- package/dist/ds.umd.cjs +1 -1
- package/dist/src/components/Button/Button.d.ts +1 -1
- package/dist/src/components/Button/Button.stories.d.ts +1 -1
- package/dist/src/components/Modal/Modal.stories.d.ts +5 -0
- package/dist/src/components/Tabs/Tabs.stories.d.ts +5 -0
- package/dist/src/components/Tree/Tree.stories.d.ts +7 -0
- package/eslint.config.js +0 -7
- package/package.json +27 -20
- package/src/components/Button/Button.module.css +20 -0
- package/src/components/Button/Button.test.tsx +77 -0
- package/src/components/Button/Button.tsx +18 -0
- package/src/components/Dropdown/Dropdown.stories.tsx +24 -0
- package/src/components/Dropdown/Dropdown.tsx +2 -2
- package/src/components/Modal/Modal.stories.tsx +22 -0
- package/src/components/Tabs/Tabs.stories.tsx +19 -0
- package/src/components/Tooltip/Tooltip.stories.tsx +24 -0
- package/src/components/Tooltip/Tooltip.test.tsx +73 -0
- package/src/components/Tooltip/Tooltip.tsx +12 -14
- package/src/components/Tree/Tree.stories.tsx +15 -0
- package/src/styles/tokens.css +11 -11
- package/src/test/setup.ts +1 -0
- package/tsconfig.app.json +2 -1
- package/.versionrc.json +0 -6
|
@@ -6,6 +6,30 @@ const meta: Meta<typeof Dropdown> = {
|
|
|
6
6
|
title: 'Components/Dropdown',
|
|
7
7
|
component: Dropdown,
|
|
8
8
|
tags: ['autodocs'],
|
|
9
|
+
parameters: {
|
|
10
|
+
docs: {
|
|
11
|
+
description: {
|
|
12
|
+
component: `
|
|
13
|
+
A custom select built on a \`button\` + \`listbox\` pattern. Supports both controlled (\`value\` + \`onChange\`) and uncontrolled (\`defaultValue\`) usage. Options can carry an optional description and can be individually disabled.
|
|
14
|
+
|
|
15
|
+
**When to use**
|
|
16
|
+
Use Dropdown when you need richer option rendering (descriptions, icons) or tighter visual control over the trigger. For simple native selects, use the \`Select\` component instead.
|
|
17
|
+
|
|
18
|
+
**Keyboard interaction**
|
|
19
|
+
|
|
20
|
+
| Key | Behaviour |
|
|
21
|
+
|-----|-----------|
|
|
22
|
+
| \`Enter\` / \`Space\` | Toggle open; select focused option when open |
|
|
23
|
+
| \`ArrowDown\` | Open list or move focus to next option |
|
|
24
|
+
| \`ArrowUp\` | Open list or move focus to previous option |
|
|
25
|
+
| \`Home\` | Move focus to first option |
|
|
26
|
+
| \`End\` | Move focus to last option |
|
|
27
|
+
| \`Escape\` | Close list and return focus to trigger |
|
|
28
|
+
| \`Tab\` | Close list and move focus to next element |
|
|
29
|
+
`,
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
},
|
|
9
33
|
argTypes: {
|
|
10
34
|
onChange: { action: 'changed' },
|
|
11
35
|
},
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import clsx from 'clsx';
|
|
2
|
-
import { useId, useState, useRef, useEffect } from 'react';
|
|
2
|
+
import { useId, useState, useRef, useEffect, type KeyboardEvent } from 'react';
|
|
3
3
|
|
|
4
4
|
import styles from './Dropdown.module.css';
|
|
5
5
|
import type { DropdownProps, DropdownOption } from './Dropdown.types.ts';
|
|
@@ -64,7 +64,7 @@ export const Dropdown = ({
|
|
|
64
64
|
triggerRef.current?.focus();
|
|
65
65
|
};
|
|
66
66
|
|
|
67
|
-
const handleKeyDown = (event:
|
|
67
|
+
const handleKeyDown = (event: KeyboardEvent<HTMLButtonElement>) => {
|
|
68
68
|
if (disabled) {
|
|
69
69
|
return;
|
|
70
70
|
}
|
|
@@ -15,6 +15,28 @@ const meta = {
|
|
|
15
15
|
tags: ['autodocs'],
|
|
16
16
|
parameters: {
|
|
17
17
|
layout: 'centered',
|
|
18
|
+
docs: {
|
|
19
|
+
description: {
|
|
20
|
+
component: `
|
|
21
|
+
A dialog overlay that traps focus while open and restores it to the trigger element on close. Renders into the normal DOM tree (no portal) and is controlled via \`isOpen\` + \`onClose\`.
|
|
22
|
+
|
|
23
|
+
**Usage**
|
|
24
|
+
Always provide a \`title\` — it is wired to \`aria-labelledby\` on the dialog element. Pass action buttons via the \`footer\` prop to keep them visually anchored to the bottom.
|
|
25
|
+
|
|
26
|
+
**Keyboard interaction**
|
|
27
|
+
|
|
28
|
+
| Key | Behaviour |
|
|
29
|
+
|-----|-----------|
|
|
30
|
+
| \`Tab\` | Cycle focus through all focusable elements inside the modal |
|
|
31
|
+
| \`Shift + Tab\` | Cycle focus backwards |
|
|
32
|
+
| \`Escape\` | Close the modal |
|
|
33
|
+
|
|
34
|
+
**Sizes**
|
|
35
|
+
|
|
36
|
+
\`sm\` · \`md\` (default) · \`lg\` · \`xl\` · \`full\`
|
|
37
|
+
`,
|
|
38
|
+
},
|
|
39
|
+
},
|
|
18
40
|
},
|
|
19
41
|
argTypes: {
|
|
20
42
|
size: {
|
|
@@ -8,6 +8,25 @@ const meta = {
|
|
|
8
8
|
tags: ['autodocs'],
|
|
9
9
|
parameters: {
|
|
10
10
|
layout: 'centered',
|
|
11
|
+
docs: {
|
|
12
|
+
description: {
|
|
13
|
+
component: `
|
|
14
|
+
A tab widget implementing the [ARIA Tabs pattern](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/). Supports controlled (\`activeId\` + \`onChange\`) and uncontrolled (\`defaultActiveId\`) modes. Individual tabs can be disabled. Two visual variants are available: \`line\` (default) and \`pill\`.
|
|
15
|
+
|
|
16
|
+
**Keyboard interaction**
|
|
17
|
+
|
|
18
|
+
Focus moves into the tab list on \`Tab\`. Arrow keys then navigate between tabs and automatically activate them (automatic activation pattern).
|
|
19
|
+
|
|
20
|
+
| Key | Behaviour |
|
|
21
|
+
|-----|-----------|
|
|
22
|
+
| \`ArrowRight\` / \`ArrowDown\` | Move to and activate next enabled tab (wraps) |
|
|
23
|
+
| \`ArrowLeft\` / \`ArrowUp\` | Move to and activate previous enabled tab (wraps) |
|
|
24
|
+
| \`Home\` | Move to and activate first enabled tab |
|
|
25
|
+
| \`End\` | Move to and activate last enabled tab |
|
|
26
|
+
| \`Tab\` | Move focus from tab list into the active tab panel |
|
|
27
|
+
`,
|
|
28
|
+
},
|
|
29
|
+
},
|
|
11
30
|
},
|
|
12
31
|
} satisfies Meta<typeof Tabs>;
|
|
13
32
|
|
|
@@ -7,6 +7,30 @@ const meta: Meta<typeof Tooltip> = {
|
|
|
7
7
|
title: 'Components/Tooltip',
|
|
8
8
|
component: Tooltip,
|
|
9
9
|
tags: ['autodocs'],
|
|
10
|
+
parameters: {
|
|
11
|
+
docs: {
|
|
12
|
+
description: {
|
|
13
|
+
component: `
|
|
14
|
+
Wraps any focusable trigger element and shows supplementary content on hover or focus. The tooltip element always stays in the DOM (CSS opacity toggle) so that \`aria-describedby\` references remain valid at all times.
|
|
15
|
+
|
|
16
|
+
**Usage**
|
|
17
|
+
Pass a single focusable element as \`children\`. The component clones it to inject \`aria-describedby\`, so the trigger does not need to be modified manually. Use short, non-essential text — tooltips are not a substitute for visible labels.
|
|
18
|
+
|
|
19
|
+
**Keyboard interaction**
|
|
20
|
+
|
|
21
|
+
| Key | Behaviour |
|
|
22
|
+
|-----|-----------|
|
|
23
|
+
| Focus trigger | Show tooltip after \`delay\` ms |
|
|
24
|
+
| Blur trigger | Hide tooltip immediately |
|
|
25
|
+
| \`Escape\` | Hide tooltip while trigger is focused |
|
|
26
|
+
|
|
27
|
+
**Positions:** \`top\` (default) · \`bottom\` · \`left\` · \`right\`
|
|
28
|
+
|
|
29
|
+
**Variants:** \`dark\` (default) · \`light\`
|
|
30
|
+
`,
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
},
|
|
10
34
|
decorators: [
|
|
11
35
|
(Story) => (
|
|
12
36
|
<div
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
|
|
4
|
+
import { Button } from '../Button/Button.tsx';
|
|
5
|
+
import { Tooltip } from './Tooltip.tsx';
|
|
6
|
+
|
|
7
|
+
describe('Tooltip', () => {
|
|
8
|
+
it('renders the trigger element', () => {
|
|
9
|
+
render(
|
|
10
|
+
<Tooltip content="Save file">
|
|
11
|
+
<Button>Save</Button>
|
|
12
|
+
</Tooltip>
|
|
13
|
+
);
|
|
14
|
+
expect(screen.getByRole('button', { name: 'Save' })).toBeInTheDocument();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('keeps tooltip element in DOM when not visible', () => {
|
|
18
|
+
render(
|
|
19
|
+
<Tooltip content="Save file">
|
|
20
|
+
<Button>Save</Button>
|
|
21
|
+
</Tooltip>
|
|
22
|
+
);
|
|
23
|
+
expect(screen.getByRole('tooltip')).toBeInTheDocument();
|
|
24
|
+
expect(screen.getByRole('tooltip')).toHaveTextContent('Save file');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('always applies aria-describedby to the trigger', () => {
|
|
28
|
+
render(
|
|
29
|
+
<Tooltip content="Save file">
|
|
30
|
+
<Button>Save</Button>
|
|
31
|
+
</Tooltip>
|
|
32
|
+
);
|
|
33
|
+
const trigger = screen.getByRole('button');
|
|
34
|
+
const tooltip = screen.getByRole('tooltip');
|
|
35
|
+
expect(trigger).toHaveAttribute('aria-describedby', tooltip.id);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('shows tooltip on hover', async () => {
|
|
39
|
+
render(
|
|
40
|
+
<Tooltip content="Tooltip text" delay={0}>
|
|
41
|
+
<Button>Hover me</Button>
|
|
42
|
+
</Tooltip>
|
|
43
|
+
);
|
|
44
|
+
const tooltip = screen.getByRole('tooltip');
|
|
45
|
+
expect(tooltip).not.toHaveClass('visible');
|
|
46
|
+
await userEvent.hover(screen.getByRole('button'));
|
|
47
|
+
expect(tooltip).toHaveClass('visible');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('hides tooltip on mouse leave', async () => {
|
|
51
|
+
render(
|
|
52
|
+
<Tooltip content="Tooltip text" delay={0}>
|
|
53
|
+
<Button>Hover me</Button>
|
|
54
|
+
</Tooltip>
|
|
55
|
+
);
|
|
56
|
+
const trigger = screen.getByRole('button');
|
|
57
|
+
await userEvent.hover(trigger);
|
|
58
|
+
await userEvent.unhover(trigger);
|
|
59
|
+
expect(screen.getByRole('tooltip')).not.toHaveClass('visible');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('hides tooltip on Escape key', async () => {
|
|
63
|
+
render(
|
|
64
|
+
<Tooltip content="Tooltip text" delay={0}>
|
|
65
|
+
<Button>Focus me</Button>
|
|
66
|
+
</Tooltip>
|
|
67
|
+
);
|
|
68
|
+
await userEvent.tab(); // focuses the button, onFocus shows the tooltip
|
|
69
|
+
expect(screen.getByRole('tooltip')).toHaveClass('visible');
|
|
70
|
+
await userEvent.keyboard('{Escape}');
|
|
71
|
+
expect(screen.getByRole('tooltip')).not.toHaveClass('visible');
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -68,20 +68,18 @@ export const Tooltip = ({
|
|
|
68
68
|
onKeyDown={handleKeyDown}
|
|
69
69
|
>
|
|
70
70
|
<div className={styles.trigger}>{trigger}</div>
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
</div>
|
|
84
|
-
)}
|
|
71
|
+
<div
|
|
72
|
+
className={clsx(
|
|
73
|
+
styles.tooltip,
|
|
74
|
+
styles[position],
|
|
75
|
+
styles[variant],
|
|
76
|
+
isVisible && styles.visible
|
|
77
|
+
)}
|
|
78
|
+
id={tooltipId}
|
|
79
|
+
role="tooltip"
|
|
80
|
+
>
|
|
81
|
+
{content}
|
|
82
|
+
</div>
|
|
85
83
|
</div>
|
|
86
84
|
);
|
|
87
85
|
};
|
|
@@ -5,6 +5,21 @@ const meta = {
|
|
|
5
5
|
title: 'Components/Tree',
|
|
6
6
|
component: Tree,
|
|
7
7
|
tags: ['autodocs'],
|
|
8
|
+
parameters: {
|
|
9
|
+
docs: {
|
|
10
|
+
description: {
|
|
11
|
+
component: `
|
|
12
|
+
Renders any JavaScript value — object, array, or primitive — as an expandable tree. Useful for debugging, JSON inspection, or displaying structured data. Nested objects and arrays are collapsible; primitives and \`null\` are displayed inline.
|
|
13
|
+
|
|
14
|
+
**Usage**
|
|
15
|
+
Pass any serialisable value to \`data\`. Control the initial expand state with \`defaultExpanded\` (default: \`true\`). Custom expand/collapse icons can be provided via \`expandIcon\` and \`collapseIcon\`.
|
|
16
|
+
|
|
17
|
+
**Limitations**
|
|
18
|
+
This component is a data visualiser, not a navigation tree. It does not implement the [ARIA Tree pattern](https://www.w3.org/WAI/ARIA/apg/patterns/treeview/) — there is no keyboard navigation between nodes. For navigable trees (e.g. file explorers, side nav), a fully accessible tree widget should be used instead.
|
|
19
|
+
`,
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
},
|
|
8
23
|
} satisfies Meta<typeof Tree>;
|
|
9
24
|
|
|
10
25
|
export default meta;
|
package/src/styles/tokens.css
CHANGED
|
@@ -58,17 +58,17 @@
|
|
|
58
58
|
--red-950: color-mix(in srgb, var(--red) 25%, #000000);
|
|
59
59
|
|
|
60
60
|
/* Warning (Amber/Orange) */
|
|
61
|
-
--orange-50: #
|
|
62
|
-
--orange-100: #
|
|
63
|
-
--orange-200: #
|
|
64
|
-
--orange-300: #
|
|
65
|
-
--orange-400: #
|
|
66
|
-
--orange-500:
|
|
67
|
-
--orange-600: #
|
|
68
|
-
--orange-700: #
|
|
69
|
-
--orange-800: #
|
|
70
|
-
--orange-900: #
|
|
71
|
-
--orange-950: #
|
|
61
|
+
--orange-50: color-mix(in srgb, var(--orange) 10%, #ffffff);
|
|
62
|
+
--orange-100: color-mix(in srgb, var(--orange) 20%, #ffffff);
|
|
63
|
+
--orange-200: color-mix(in srgb, var(--orange) 35%, #ffffff);
|
|
64
|
+
--orange-300: color-mix(in srgb, var(--orange) 55%, #ffffff);
|
|
65
|
+
--orange-400: color-mix(in srgb, var(--orange) 75%, #ffffff);
|
|
66
|
+
--orange-500: var(--orange);
|
|
67
|
+
--orange-600: color-mix(in srgb, var(--orange) 85%, #000000);
|
|
68
|
+
--orange-700: color-mix(in srgb, var(--orange) 70%, #000000);
|
|
69
|
+
--orange-800: color-mix(in srgb, var(--orange) 55%, #000000);
|
|
70
|
+
--orange-900: color-mix(in srgb, var(--orange) 40%, #000000);
|
|
71
|
+
--orange-950: color-mix(in srgb, var(--orange) 25%, #000000);
|
|
72
72
|
|
|
73
73
|
/* Retro palette */
|
|
74
74
|
--retro-yellow: #faca78;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import '@testing-library/jest-dom';
|
package/tsconfig.app.json
CHANGED