tharaday 0.7.2 → 0.7.4
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/.storybook/main.ts +1 -1
- package/.storybook/preview.ts +0 -2
- package/.storybook/vitest.setup.ts +2 -0
- package/dist/ds.css +1 -1
- package/dist/ds.js +883 -815
- package/dist/ds.umd.cjs +1 -1
- package/dist/src/components/Tree/Tree.d.ts +1 -1
- package/dist/src/components/Tree/Tree.stories.d.ts +1 -1
- package/dist/src/components/Tree/TreeItem.d.ts +1 -1
- package/dist/src/components/Tree/TreeItem.types.d.ts +6 -0
- package/package.json +8 -1
- package/src/components/Accordion/Accordion.test.tsx +82 -0
- package/src/components/Avatar/Avatar.test.tsx +36 -0
- package/src/components/Badge/Badge.test.tsx +15 -0
- package/src/components/Breadcrumbs/Breadcrumbs.test.tsx +96 -0
- package/src/components/Checkbox/Checkbox.module.css +8 -7
- package/src/components/Checkbox/Checkbox.test.tsx +68 -0
- package/src/components/Dropdown/Dropdown.test.tsx +104 -0
- package/src/components/Input/Input.test.tsx +61 -0
- package/src/components/List/List.module.css +12 -12
- package/src/components/List/List.test.tsx +46 -0
- package/src/components/Modal/Modal.module.css +5 -5
- package/src/components/Modal/Modal.test.tsx +86 -0
- package/src/components/NavBar/NavBar.module.css +3 -3
- package/src/components/Notification/Notification.module.css +6 -6
- package/src/components/Notification/Notification.test.tsx +38 -0
- package/src/components/Pagination/Pagination.test.tsx +70 -0
- package/src/components/ProgressBar/ProgressBar.test.tsx +58 -0
- package/src/components/RadioButton/RadioButton.test.tsx +51 -0
- package/src/components/Select/Select.test.tsx +64 -0
- package/src/components/Slider/Slider.test.tsx +49 -0
- package/src/components/Stepper/Step.module.css +2 -2
- package/src/components/Stepper/Stepper.test.tsx +51 -0
- package/src/components/Switch/Switch.test.tsx +53 -0
- package/src/components/Table/Table.test.tsx +78 -0
- package/src/components/Tabs/Tabs.test.tsx +83 -0
- package/src/components/Textarea/Textarea.test.tsx +56 -0
- package/src/components/Tooltip/Tooltip.module.css +6 -6
- package/src/components/Tree/Tree.test.tsx +116 -0
- package/src/components/Tree/Tree.tsx +65 -1
- package/src/components/Tree/TreeItem.module.css +20 -26
- package/src/components/Tree/TreeItem.tsx +144 -79
- package/src/components/Tree/TreeItem.types.ts +6 -0
- package/src/styles/ds.css +14 -9
- package/src/styles/palette.css +71 -0
- package/src/styles/themes/dark.css +35 -35
- package/src/styles/themes/light.css +35 -35
- package/src/styles/themes/retro-dark.css +35 -35
- package/src/styles/themes/retro-light.css +35 -35
- package/src/styles/themes/retro-palette.css +85 -0
- package/src/styles/themes/sanzo-152-dark.css +35 -35
- package/src/styles/themes/sanzo-152-light.css +35 -35
- package/src/styles/themes/sanzo-152-palette.css +66 -0
- package/src/styles/tokens.css +14 -224
- package/src/styles/semantic.css +0 -56
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
3
|
+
|
|
4
|
+
import { Tree } from './Tree.tsx';
|
|
5
|
+
|
|
6
|
+
const objectData = { name: 'Alice', age: 30, address: { city: 'Warsaw', zip: '00-001' } };
|
|
7
|
+
|
|
8
|
+
describe('Tree', () => {
|
|
9
|
+
it('renders a tree widget', () => {
|
|
10
|
+
render(<Tree data={objectData} />);
|
|
11
|
+
expect(screen.getByRole('tree')).toBeInTheDocument();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('renders top-level keys as treeitems', () => {
|
|
15
|
+
render(<Tree data={objectData} />);
|
|
16
|
+
const items = screen.getAllByRole('treeitem');
|
|
17
|
+
const labels = items.map((el) => el.textContent);
|
|
18
|
+
expect(labels.some((t) => t?.includes('name'))).toBe(true);
|
|
19
|
+
expect(labels.some((t) => t?.includes('age'))).toBe(true);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('renders leaf values inline', () => {
|
|
23
|
+
render(<Tree data={{ score: 42 }} />);
|
|
24
|
+
expect(screen.getByText('42')).toBeInTheDocument();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('collapses nested objects by default', () => {
|
|
28
|
+
render(<Tree data={objectData} />);
|
|
29
|
+
expect(screen.queryByText('Warsaw')).not.toBeInTheDocument();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('expands a branch on click', async () => {
|
|
33
|
+
render(<Tree data={objectData} />);
|
|
34
|
+
const addressItem = screen
|
|
35
|
+
.getAllByRole('treeitem')
|
|
36
|
+
.find((el) => el.textContent?.includes('address'))!;
|
|
37
|
+
await userEvent.click(addressItem);
|
|
38
|
+
expect(screen.getByText('Warsaw')).toBeInTheDocument();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('collapses an expanded branch on click', async () => {
|
|
42
|
+
render(<Tree data={objectData} defaultExpanded />);
|
|
43
|
+
const addressItem = screen
|
|
44
|
+
.getAllByRole('treeitem')
|
|
45
|
+
.find((el) => el.textContent?.includes('address'))!;
|
|
46
|
+
await userEvent.click(addressItem);
|
|
47
|
+
expect(screen.queryByText('Warsaw')).not.toBeInTheDocument();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('sets aria-expanded on branch items', () => {
|
|
51
|
+
render(<Tree data={objectData} />);
|
|
52
|
+
const addressItem = screen
|
|
53
|
+
.getAllByRole('treeitem')
|
|
54
|
+
.find((el) => el.textContent?.includes('address'))!;
|
|
55
|
+
expect(addressItem).toHaveAttribute('aria-expanded', 'false');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('sets correct aria-level on top-level items', () => {
|
|
59
|
+
render(<Tree data={objectData} />);
|
|
60
|
+
const items = screen.getAllByRole('treeitem');
|
|
61
|
+
items.forEach((item) => expect(item).toHaveAttribute('aria-level', '1'));
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('expands all branches when defaultExpanded is true', () => {
|
|
65
|
+
render(<Tree data={objectData} defaultExpanded />);
|
|
66
|
+
expect(screen.getByText('Warsaw')).toBeInTheDocument();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('navigates down with ArrowDown', async () => {
|
|
70
|
+
render(<Tree data={objectData} />);
|
|
71
|
+
const items = screen.getAllByRole('treeitem');
|
|
72
|
+
items[0].focus();
|
|
73
|
+
await userEvent.keyboard('{ArrowDown}');
|
|
74
|
+
expect(document.activeElement).toBe(items[1]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('navigates up with ArrowUp', async () => {
|
|
78
|
+
render(<Tree data={objectData} />);
|
|
79
|
+
const items = screen.getAllByRole('treeitem');
|
|
80
|
+
items[1].tabIndex = 0;
|
|
81
|
+
items[1].focus();
|
|
82
|
+
await userEvent.keyboard('{ArrowUp}');
|
|
83
|
+
expect(document.activeElement).toBe(items[0]);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('expands a branch with ArrowRight', async () => {
|
|
87
|
+
render(<Tree data={objectData} />);
|
|
88
|
+
const addressItem = screen
|
|
89
|
+
.getAllByRole('treeitem')
|
|
90
|
+
.find((el) => el.textContent?.includes('address'))!;
|
|
91
|
+
addressItem.focus();
|
|
92
|
+
await userEvent.keyboard('{ArrowRight}');
|
|
93
|
+
expect(addressItem).toHaveAttribute('aria-expanded', 'true');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('collapses a branch with ArrowLeft', async () => {
|
|
97
|
+
render(<Tree data={objectData} defaultExpanded />);
|
|
98
|
+
const addressItem = screen
|
|
99
|
+
.getAllByRole('treeitem')
|
|
100
|
+
.find((el) => el.textContent?.startsWith('address'))!;
|
|
101
|
+
addressItem.focus();
|
|
102
|
+
await userEvent.keyboard('{ArrowLeft}');
|
|
103
|
+
expect(addressItem).toHaveAttribute('aria-expanded', 'false');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('renders array data with indexed items', () => {
|
|
107
|
+
render(<Tree data={['x', 'y', 'z']} />);
|
|
108
|
+
expect(screen.getByText('x')).toBeInTheDocument();
|
|
109
|
+
expect(screen.getByText('y')).toBeInTheDocument();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('renders null value', () => {
|
|
113
|
+
render(<Tree data={{ val: null }} />);
|
|
114
|
+
expect(screen.getByText('null')).toBeInTheDocument();
|
|
115
|
+
});
|
|
116
|
+
});
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import clsx from 'clsx';
|
|
2
|
+
import { useEffect, useRef } from 'react';
|
|
3
|
+
import type { KeyboardEvent } from 'react';
|
|
2
4
|
import type { TreeProps } from './Tree.types';
|
|
3
5
|
import styles from './Tree.module.css';
|
|
4
6
|
import { TreeItem } from './TreeItem';
|
|
@@ -11,14 +13,76 @@ export const Tree = ({
|
|
|
11
13
|
collapseIcon,
|
|
12
14
|
...props
|
|
13
15
|
}: TreeProps) => {
|
|
16
|
+
const treeRef = useRef<HTMLDivElement>(null);
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
const first = treeRef.current?.querySelector<HTMLElement>('[role="treeitem"]');
|
|
20
|
+
if (first) {
|
|
21
|
+
first.tabIndex = 0;
|
|
22
|
+
}
|
|
23
|
+
}, []);
|
|
24
|
+
|
|
25
|
+
const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {
|
|
26
|
+
const tree = treeRef.current;
|
|
27
|
+
if (!tree) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const items = Array.from(tree.querySelectorAll<HTMLElement>('[role="treeitem"]'));
|
|
31
|
+
if (items.length === 0) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const current = items.find((el) => el.tabIndex === 0) ?? items[0];
|
|
35
|
+
const idx = items.indexOf(current);
|
|
36
|
+
|
|
37
|
+
switch (e.key) {
|
|
38
|
+
case 'ArrowDown':
|
|
39
|
+
e.preventDefault();
|
|
40
|
+
if (idx < items.length - 1) {
|
|
41
|
+
current.tabIndex = -1;
|
|
42
|
+
items[idx + 1].tabIndex = 0;
|
|
43
|
+
items[idx + 1].focus();
|
|
44
|
+
}
|
|
45
|
+
break;
|
|
46
|
+
case 'ArrowUp':
|
|
47
|
+
e.preventDefault();
|
|
48
|
+
if (idx > 0) {
|
|
49
|
+
current.tabIndex = -1;
|
|
50
|
+
items[idx - 1].tabIndex = 0;
|
|
51
|
+
items[idx - 1].focus();
|
|
52
|
+
}
|
|
53
|
+
break;
|
|
54
|
+
case 'Home':
|
|
55
|
+
e.preventDefault();
|
|
56
|
+
current.tabIndex = -1;
|
|
57
|
+
items[0].tabIndex = 0;
|
|
58
|
+
items[0].focus();
|
|
59
|
+
break;
|
|
60
|
+
case 'End':
|
|
61
|
+
e.preventDefault();
|
|
62
|
+
current.tabIndex = -1;
|
|
63
|
+
items[items.length - 1].tabIndex = 0;
|
|
64
|
+
items[items.length - 1].focus();
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
14
69
|
return (
|
|
15
|
-
<div
|
|
70
|
+
<div
|
|
71
|
+
ref={treeRef}
|
|
72
|
+
role="tree"
|
|
73
|
+
className={clsx(styles.root, className)}
|
|
74
|
+
onKeyDown={handleKeyDown}
|
|
75
|
+
{...props}
|
|
76
|
+
>
|
|
16
77
|
<TreeItem
|
|
17
78
|
data={data}
|
|
18
79
|
defaultExpanded={defaultExpanded}
|
|
19
80
|
expandIcon={expandIcon}
|
|
20
81
|
collapseIcon={collapseIcon}
|
|
21
82
|
isRoot
|
|
83
|
+
level={1}
|
|
84
|
+
setSize={1}
|
|
85
|
+
posInSet={1}
|
|
22
86
|
/>
|
|
23
87
|
</div>
|
|
24
88
|
);
|
|
@@ -1,25 +1,32 @@
|
|
|
1
|
-
.
|
|
2
|
-
|
|
3
|
-
padding-left: var(--ds-space-4);
|
|
4
|
-
margin: 0;
|
|
1
|
+
.item {
|
|
2
|
+
outline: none;
|
|
5
3
|
}
|
|
6
4
|
|
|
7
|
-
.item {
|
|
8
|
-
|
|
5
|
+
.item:focus-visible > .itemHeader {
|
|
6
|
+
box-shadow:
|
|
7
|
+
0 0 0 2px var(--ds-ring-offset),
|
|
8
|
+
0 0 0 4px var(--ds-ring);
|
|
9
|
+
border-radius: var(--ds-radius-sm);
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
.itemHeader {
|
|
12
13
|
display: flex;
|
|
13
14
|
align-items: center;
|
|
14
15
|
min-height: var(--ds-space-6);
|
|
16
|
+
padding: 0 var(--ds-space-1);
|
|
17
|
+
border-radius: var(--ds-radius-sm);
|
|
18
|
+
cursor: default;
|
|
15
19
|
}
|
|
16
20
|
|
|
17
|
-
.
|
|
18
|
-
background: none;
|
|
19
|
-
border: none;
|
|
20
|
-
padding: 0;
|
|
21
|
-
margin: 0;
|
|
21
|
+
.branch > .itemHeader {
|
|
22
22
|
cursor: pointer;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.itemHeader:hover {
|
|
26
|
+
background-color: var(--ds-surface-1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.toggleIcon {
|
|
23
30
|
display: inline-flex;
|
|
24
31
|
align-items: center;
|
|
25
32
|
justify-content: center;
|
|
@@ -27,11 +34,6 @@
|
|
|
27
34
|
width: var(--ds-space-5);
|
|
28
35
|
height: var(--ds-space-5);
|
|
29
36
|
flex-shrink: 0;
|
|
30
|
-
transition: transform var(--ds-transition-fast);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
.toggleButton:hover {
|
|
34
|
-
color: var(--ds-text-1);
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
.key {
|
|
@@ -50,14 +52,6 @@
|
|
|
50
52
|
margin-left: var(--ds-space-1);
|
|
51
53
|
}
|
|
52
54
|
|
|
53
|
-
.
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
.expanded {
|
|
58
|
-
display: block;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
.rootList {
|
|
62
|
-
padding-left: 0;
|
|
55
|
+
.childGroup {
|
|
56
|
+
padding-left: var(--ds-space-4);
|
|
63
57
|
}
|
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
import clsx from 'clsx';
|
|
2
|
-
import { useState } from 'react';
|
|
2
|
+
import { useRef, useState } from 'react';
|
|
3
|
+
import type { FocusEvent, KeyboardEvent, MouseEvent } from 'react';
|
|
3
4
|
import type { TreeItemProps } from './TreeItem.types';
|
|
4
5
|
import styles from './TreeItem.module.css';
|
|
5
6
|
|
|
7
|
+
function focusTreeItem(item: HTMLElement) {
|
|
8
|
+
const tree = item.closest('[role="tree"]');
|
|
9
|
+
if (!tree) {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
tree.querySelectorAll<HTMLElement>('[role="treeitem"]').forEach((el) => {
|
|
13
|
+
el.tabIndex = -1;
|
|
14
|
+
});
|
|
15
|
+
item.tabIndex = 0;
|
|
16
|
+
item.focus();
|
|
17
|
+
}
|
|
18
|
+
|
|
6
19
|
export const TreeItem = ({
|
|
7
20
|
data,
|
|
8
21
|
label,
|
|
@@ -10,14 +23,91 @@ export const TreeItem = ({
|
|
|
10
23
|
expandIcon,
|
|
11
24
|
collapseIcon,
|
|
12
25
|
isRoot,
|
|
26
|
+
level,
|
|
27
|
+
setSize,
|
|
28
|
+
posInSet,
|
|
13
29
|
}: TreeItemProps) => {
|
|
14
30
|
const [isExpanded, setIsExpanded] = useState(isRoot ? true : (defaultExpanded ?? false));
|
|
31
|
+
const itemRef = useRef<HTMLDivElement>(null);
|
|
15
32
|
|
|
16
33
|
const isObject = data !== null && typeof data === 'object';
|
|
17
34
|
const hasChildren =
|
|
18
|
-
isObject && (Array.isArray(data) ? data.length > 0 : Object.keys(data).length > 0);
|
|
35
|
+
isObject && (Array.isArray(data) ? data.length > 0 : Object.keys(data as object).length > 0);
|
|
36
|
+
|
|
37
|
+
const childLevel = isRoot ? level : level + 1;
|
|
38
|
+
|
|
39
|
+
const getChildren = (): { key: string; value: unknown }[] => {
|
|
40
|
+
if (!isObject) {
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
if (Array.isArray(data)) {
|
|
44
|
+
return (data as unknown[]).map((v, i) => ({ key: String(i), value: v }));
|
|
45
|
+
}
|
|
46
|
+
return Object.entries(data as Record<string, unknown>).map(([k, v]) => ({ key: k, value: v }));
|
|
47
|
+
};
|
|
19
48
|
|
|
20
|
-
const
|
|
49
|
+
const children = getChildren();
|
|
50
|
+
|
|
51
|
+
const handleClick = (e: MouseEvent) => {
|
|
52
|
+
e.stopPropagation();
|
|
53
|
+
if (hasChildren) {
|
|
54
|
+
setIsExpanded((prev) => !prev);
|
|
55
|
+
}
|
|
56
|
+
if (itemRef.current) {
|
|
57
|
+
focusTreeItem(itemRef.current);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const handleFocus = (e: FocusEvent) => {
|
|
62
|
+
e.stopPropagation();
|
|
63
|
+
if (!itemRef.current) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const tree = itemRef.current.closest('[role="tree"]');
|
|
67
|
+
if (!tree) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
tree.querySelectorAll<HTMLElement>('[role="treeitem"]').forEach((el) => {
|
|
71
|
+
el.tabIndex = -1;
|
|
72
|
+
});
|
|
73
|
+
itemRef.current.tabIndex = 0;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {
|
|
77
|
+
switch (e.key) {
|
|
78
|
+
case 'Enter':
|
|
79
|
+
case ' ':
|
|
80
|
+
e.preventDefault();
|
|
81
|
+
if (hasChildren) {
|
|
82
|
+
setIsExpanded((prev) => !prev);
|
|
83
|
+
}
|
|
84
|
+
break;
|
|
85
|
+
case 'ArrowRight':
|
|
86
|
+
e.preventDefault();
|
|
87
|
+
e.stopPropagation();
|
|
88
|
+
if (hasChildren && !isExpanded) {
|
|
89
|
+
setIsExpanded(true);
|
|
90
|
+
} else if (hasChildren && isExpanded) {
|
|
91
|
+
const firstChild = itemRef.current?.querySelector<HTMLElement>('[role="treeitem"]');
|
|
92
|
+
if (firstChild) {
|
|
93
|
+
focusTreeItem(firstChild);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
break;
|
|
97
|
+
case 'ArrowLeft':
|
|
98
|
+
e.preventDefault();
|
|
99
|
+
e.stopPropagation();
|
|
100
|
+
if (hasChildren && isExpanded) {
|
|
101
|
+
setIsExpanded(false);
|
|
102
|
+
} else {
|
|
103
|
+
const parent = itemRef.current?.parentElement?.closest<HTMLElement>('[role="treeitem"]');
|
|
104
|
+
if (parent) {
|
|
105
|
+
focusTreeItem(parent);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
};
|
|
21
111
|
|
|
22
112
|
const defaultExpandIcon = (
|
|
23
113
|
<svg
|
|
@@ -49,98 +139,73 @@ export const TreeItem = ({
|
|
|
49
139
|
</svg>
|
|
50
140
|
);
|
|
51
141
|
|
|
52
|
-
const
|
|
142
|
+
const renderLeafContent = () => {
|
|
53
143
|
if (data === undefined) {
|
|
54
144
|
return <span className={styles.empty}>undefined</span>;
|
|
55
145
|
}
|
|
56
|
-
|
|
57
146
|
if (data === null) {
|
|
58
147
|
return <span className={styles.empty}>null</span>;
|
|
59
148
|
}
|
|
60
|
-
|
|
61
149
|
if (typeof data !== 'object') {
|
|
62
150
|
return <span className={styles.value}>{String(data)}</span>;
|
|
63
151
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
if (data.length === 0) {
|
|
67
|
-
return <span className={styles.empty}>[]</span>;
|
|
68
|
-
}
|
|
69
|
-
return (
|
|
70
|
-
<ul
|
|
71
|
-
className={clsx(
|
|
72
|
-
styles.list,
|
|
73
|
-
styles.collapsibleContent,
|
|
74
|
-
isExpanded && styles.expanded,
|
|
75
|
-
isRoot && styles.rootList
|
|
76
|
-
)}
|
|
77
|
-
>
|
|
78
|
-
{data.map((item, index) => (
|
|
79
|
-
<li key={index} className={styles.item}>
|
|
80
|
-
<TreeItem
|
|
81
|
-
data={item}
|
|
82
|
-
defaultExpanded={defaultExpanded}
|
|
83
|
-
expandIcon={expandIcon}
|
|
84
|
-
collapseIcon={collapseIcon}
|
|
85
|
-
/>
|
|
86
|
-
</li>
|
|
87
|
-
))}
|
|
88
|
-
</ul>
|
|
89
|
-
);
|
|
152
|
+
if (Array.isArray(data) && (data as unknown[]).length === 0) {
|
|
153
|
+
return <span className={styles.empty}>[]</span>;
|
|
90
154
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
return <span className={styles.empty}>{}</span>;
|
|
155
|
+
if (!Array.isArray(data) && Object.keys(data as object).length === 0) {
|
|
156
|
+
return <span className={styles.empty}>{'{}'}</span>;
|
|
94
157
|
}
|
|
95
|
-
|
|
96
|
-
return (
|
|
97
|
-
<ul
|
|
98
|
-
className={clsx(
|
|
99
|
-
styles.list,
|
|
100
|
-
styles.collapsibleContent,
|
|
101
|
-
isExpanded && styles.expanded,
|
|
102
|
-
isRoot && styles.rootList
|
|
103
|
-
)}
|
|
104
|
-
>
|
|
105
|
-
{Object.entries(data).map(([key, value]) => (
|
|
106
|
-
<li key={key} className={styles.item}>
|
|
107
|
-
<TreeItem
|
|
108
|
-
label={key}
|
|
109
|
-
data={value}
|
|
110
|
-
defaultExpanded={defaultExpanded}
|
|
111
|
-
expandIcon={expandIcon}
|
|
112
|
-
collapseIcon={collapseIcon}
|
|
113
|
-
/>
|
|
114
|
-
</li>
|
|
115
|
-
))}
|
|
116
|
-
</ul>
|
|
117
|
-
);
|
|
158
|
+
return null;
|
|
118
159
|
};
|
|
119
160
|
|
|
161
|
+
const childrenNodes = children.map((child, index) => (
|
|
162
|
+
<TreeItem
|
|
163
|
+
key={child.key}
|
|
164
|
+
label={Array.isArray(data) ? undefined : child.key}
|
|
165
|
+
data={child.value}
|
|
166
|
+
defaultExpanded={defaultExpanded}
|
|
167
|
+
expandIcon={expandIcon}
|
|
168
|
+
collapseIcon={collapseIcon}
|
|
169
|
+
level={childLevel}
|
|
170
|
+
setSize={children.length}
|
|
171
|
+
posInSet={index + 1}
|
|
172
|
+
/>
|
|
173
|
+
));
|
|
174
|
+
|
|
175
|
+
if (isRoot) {
|
|
176
|
+
return hasChildren ? <>{childrenNodes}</> : <>{renderLeafContent()}</>;
|
|
177
|
+
}
|
|
178
|
+
|
|
120
179
|
return (
|
|
121
|
-
<div
|
|
122
|
-
{
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
180
|
+
<div
|
|
181
|
+
ref={itemRef}
|
|
182
|
+
role="treeitem"
|
|
183
|
+
aria-expanded={hasChildren ? isExpanded : undefined}
|
|
184
|
+
aria-level={level}
|
|
185
|
+
aria-setsize={setSize}
|
|
186
|
+
aria-posinset={posInSet}
|
|
187
|
+
tabIndex={-1}
|
|
188
|
+
className={clsx(styles.item, hasChildren && styles.branch)}
|
|
189
|
+
onClick={handleClick}
|
|
190
|
+
onFocus={handleFocus}
|
|
191
|
+
onKeyDown={handleKeyDown}
|
|
192
|
+
>
|
|
193
|
+
<div className={styles.itemHeader}>
|
|
194
|
+
<span className={styles.toggleIcon} aria-hidden="true">
|
|
195
|
+
{hasChildren
|
|
196
|
+
? isExpanded
|
|
197
|
+
? (collapseIcon ?? defaultCollapseIcon)
|
|
198
|
+
: (expandIcon ?? defaultExpandIcon)
|
|
199
|
+
: null}
|
|
200
|
+
</span>
|
|
201
|
+
{label !== undefined && <span className={styles.key}>{label}:</span>}
|
|
202
|
+
{!hasChildren && renderLeafContent()}
|
|
203
|
+
</div>
|
|
204
|
+
{hasChildren && isExpanded && (
|
|
205
|
+
<div role="group" className={styles.childGroup}>
|
|
206
|
+
{childrenNodes}
|
|
140
207
|
</div>
|
|
141
208
|
)}
|
|
142
|
-
{hasChildren && renderContent()}
|
|
143
|
-
{isRoot && !hasChildren && <div className={styles.itemHeader}>{renderContent()}</div>}
|
|
144
209
|
</div>
|
|
145
210
|
);
|
|
146
211
|
};
|
|
@@ -13,4 +13,10 @@ export interface TreeItemProps {
|
|
|
13
13
|
collapseIcon?: ReactNode;
|
|
14
14
|
/** Whether the tree item is a root element */
|
|
15
15
|
isRoot?: boolean;
|
|
16
|
+
/** ARIA level (depth in the tree, 1-based) */
|
|
17
|
+
level: number;
|
|
18
|
+
/** Total number of siblings at this level */
|
|
19
|
+
setSize: number;
|
|
20
|
+
/** 1-based position among siblings */
|
|
21
|
+
posInSet: number;
|
|
16
22
|
}
|
package/src/styles/ds.css
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
|
-
/*
|
|
2
|
-
@
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
@import '
|
|
6
|
-
@import '
|
|
7
|
-
@import 'themes/
|
|
8
|
-
@import 'themes/sanzo-152-
|
|
9
|
-
@import '
|
|
1
|
+
/* Explicit layer order: tokens < theme */
|
|
2
|
+
@layer ds.tokens, ds.theme;
|
|
3
|
+
|
|
4
|
+
/* Design tokens: primitives (palette.css, tokens.css) → theme semantics (themes/*.css) */
|
|
5
|
+
@import 'palette.css' layer(ds.tokens);
|
|
6
|
+
@import 'tokens.css' layer(ds.tokens);
|
|
7
|
+
@import 'themes/retro-palette.css' layer(ds.tokens);
|
|
8
|
+
@import 'themes/sanzo-152-palette.css' layer(ds.tokens);
|
|
9
|
+
@import 'themes/light.css' layer(ds.theme);
|
|
10
|
+
@import 'themes/dark.css' layer(ds.theme);
|
|
11
|
+
@import 'themes/retro-light.css' layer(ds.theme);
|
|
12
|
+
@import 'themes/retro-dark.css' layer(ds.theme);
|
|
13
|
+
@import 'themes/sanzo-152-light.css' layer(ds.theme);
|
|
14
|
+
@import 'themes/sanzo-152-dark.css' layer(ds.theme);
|
|
10
15
|
|
|
11
16
|
:root {
|
|
12
17
|
font-family: var(--ds-font-family-base);
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
/* Neutral */
|
|
3
|
+
--ds-p-neutral-0: #ffffff;
|
|
4
|
+
--ds-p-neutral-50: #fafafa;
|
|
5
|
+
--ds-p-neutral-100: #f5f5f5;
|
|
6
|
+
--ds-p-neutral-200: #eeeeee;
|
|
7
|
+
--ds-p-neutral-300: #e0e0e0;
|
|
8
|
+
--ds-p-neutral-400: #a5a5ae;
|
|
9
|
+
--ds-p-neutral-500: #7b7b85;
|
|
10
|
+
--ds-p-neutral-600: #63636e;
|
|
11
|
+
--ds-p-neutral-700: #424249;
|
|
12
|
+
--ds-p-neutral-800: #323237;
|
|
13
|
+
--ds-p-neutral-900: #242429;
|
|
14
|
+
--ds-p-neutral-950: #09090b;
|
|
15
|
+
|
|
16
|
+
/* Blue */
|
|
17
|
+
--ds-p-blue: #637ab3;
|
|
18
|
+
--ds-p-blue-50: color-mix(in srgb, var(--ds-p-blue) 10%, #ffffff);
|
|
19
|
+
--ds-p-blue-100: color-mix(in srgb, var(--ds-p-blue) 20%, #ffffff);
|
|
20
|
+
--ds-p-blue-200: color-mix(in srgb, var(--ds-p-blue) 35%, #ffffff);
|
|
21
|
+
--ds-p-blue-300: color-mix(in srgb, var(--ds-p-blue) 55%, #ffffff);
|
|
22
|
+
--ds-p-blue-400: color-mix(in srgb, var(--ds-p-blue) 75%, #ffffff);
|
|
23
|
+
--ds-p-blue-500: var(--ds-p-blue);
|
|
24
|
+
--ds-p-blue-600: color-mix(in srgb, var(--ds-p-blue) 85%, #000000);
|
|
25
|
+
--ds-p-blue-700: color-mix(in srgb, var(--ds-p-blue) 70%, #000000);
|
|
26
|
+
--ds-p-blue-800: color-mix(in srgb, var(--ds-p-blue) 55%, #000000);
|
|
27
|
+
--ds-p-blue-900: color-mix(in srgb, var(--ds-p-blue) 40%, #000000);
|
|
28
|
+
--ds-p-blue-950: color-mix(in srgb, var(--ds-p-blue) 25%, #000000);
|
|
29
|
+
|
|
30
|
+
/* Green */
|
|
31
|
+
--ds-p-green: #64af76;
|
|
32
|
+
--ds-p-green-50: color-mix(in srgb, var(--ds-p-green) 10%, #ffffff);
|
|
33
|
+
--ds-p-green-100: color-mix(in srgb, var(--ds-p-green) 20%, #ffffff);
|
|
34
|
+
--ds-p-green-200: color-mix(in srgb, var(--ds-p-green) 35%, #ffffff);
|
|
35
|
+
--ds-p-green-300: color-mix(in srgb, var(--ds-p-green) 55%, #ffffff);
|
|
36
|
+
--ds-p-green-400: color-mix(in srgb, var(--ds-p-green) 75%, #ffffff);
|
|
37
|
+
--ds-p-green-500: var(--ds-p-green);
|
|
38
|
+
--ds-p-green-600: color-mix(in srgb, var(--ds-p-green) 85%, #000000);
|
|
39
|
+
--ds-p-green-700: color-mix(in srgb, var(--ds-p-green) 70%, #000000);
|
|
40
|
+
--ds-p-green-800: color-mix(in srgb, var(--ds-p-green) 55%, #000000);
|
|
41
|
+
--ds-p-green-900: color-mix(in srgb, var(--ds-p-green) 40%, #000000);
|
|
42
|
+
--ds-p-green-950: color-mix(in srgb, var(--ds-p-green) 25%, #000000);
|
|
43
|
+
|
|
44
|
+
/* Red */
|
|
45
|
+
--ds-p-red: #d55a5a;
|
|
46
|
+
--ds-p-red-50: color-mix(in srgb, var(--ds-p-red) 10%, #ffffff);
|
|
47
|
+
--ds-p-red-100: color-mix(in srgb, var(--ds-p-red) 20%, #ffffff);
|
|
48
|
+
--ds-p-red-200: color-mix(in srgb, var(--ds-p-red) 35%, #ffffff);
|
|
49
|
+
--ds-p-red-300: color-mix(in srgb, var(--ds-p-red) 55%, #ffffff);
|
|
50
|
+
--ds-p-red-400: color-mix(in srgb, var(--ds-p-red) 75%, #ffffff);
|
|
51
|
+
--ds-p-red-500: var(--ds-p-red);
|
|
52
|
+
--ds-p-red-600: color-mix(in srgb, var(--ds-p-red) 85%, #000000);
|
|
53
|
+
--ds-p-red-700: color-mix(in srgb, var(--ds-p-red) 70%, #000000);
|
|
54
|
+
--ds-p-red-800: color-mix(in srgb, var(--ds-p-red) 55%, #000000);
|
|
55
|
+
--ds-p-red-900: color-mix(in srgb, var(--ds-p-red) 40%, #000000);
|
|
56
|
+
--ds-p-red-950: color-mix(in srgb, var(--ds-p-red) 25%, #000000);
|
|
57
|
+
|
|
58
|
+
/* Orange */
|
|
59
|
+
--ds-p-orange: #bf914f;
|
|
60
|
+
--ds-p-orange-50: color-mix(in srgb, var(--ds-p-orange) 10%, #ffffff);
|
|
61
|
+
--ds-p-orange-100: color-mix(in srgb, var(--ds-p-orange) 20%, #ffffff);
|
|
62
|
+
--ds-p-orange-200: color-mix(in srgb, var(--ds-p-orange) 35%, #ffffff);
|
|
63
|
+
--ds-p-orange-300: color-mix(in srgb, var(--ds-p-orange) 55%, #ffffff);
|
|
64
|
+
--ds-p-orange-400: color-mix(in srgb, var(--ds-p-orange) 75%, #ffffff);
|
|
65
|
+
--ds-p-orange-500: var(--ds-p-orange);
|
|
66
|
+
--ds-p-orange-600: color-mix(in srgb, var(--ds-p-orange) 85%, #000000);
|
|
67
|
+
--ds-p-orange-700: color-mix(in srgb, var(--ds-p-orange) 70%, #000000);
|
|
68
|
+
--ds-p-orange-800: color-mix(in srgb, var(--ds-p-orange) 55%, #000000);
|
|
69
|
+
--ds-p-orange-900: color-mix(in srgb, var(--ds-p-orange) 40%, #000000);
|
|
70
|
+
--ds-p-orange-950: color-mix(in srgb, var(--ds-p-orange) 25%, #000000);
|
|
71
|
+
}
|