@arbor-education/design-system.components 0.16.1 → 0.17.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/.gather/instructions/project-overview.md +0 -4
- package/.gather/skills/aroo-hunni/SKILL.md +58 -0
- package/CHANGELOG.md +31 -0
- package/CONTRIBUTING.md +1 -0
- package/dist/components/arborLogo/ArborLogo.d.ts +9 -0
- package/dist/components/arborLogo/ArborLogo.d.ts.map +1 -0
- package/dist/components/arborLogo/ArborLogo.js +17 -0
- package/dist/components/arborLogo/ArborLogo.js.map +1 -0
- package/dist/components/arborLogo/ArborLogo.stories.d.ts +94 -0
- package/dist/components/arborLogo/ArborLogo.stories.d.ts.map +1 -0
- package/dist/components/arborLogo/ArborLogo.stories.js +418 -0
- package/dist/components/arborLogo/ArborLogo.stories.js.map +1 -0
- package/dist/components/arborLogo/ArborLogo.test.d.ts +2 -0
- package/dist/components/arborLogo/ArborLogo.test.d.ts.map +1 -0
- package/dist/components/arborLogo/ArborLogo.test.js +32 -0
- package/dist/components/arborLogo/ArborLogo.test.js.map +1 -0
- package/dist/components/dataViewCard/DataViewCard.d.ts +19 -0
- package/dist/components/dataViewCard/DataViewCard.d.ts.map +1 -0
- package/dist/components/dataViewCard/DataViewCard.js +13 -0
- package/dist/components/dataViewCard/DataViewCard.js.map +1 -0
- package/dist/components/dataViewCard/DataViewCard.stories.d.ts +100 -0
- package/dist/components/dataViewCard/DataViewCard.stories.d.ts.map +1 -0
- package/dist/components/dataViewCard/DataViewCard.stories.js +317 -0
- package/dist/components/dataViewCard/DataViewCard.stories.js.map +1 -0
- package/dist/components/dataViewCard/DataViewCard.test.d.ts +2 -0
- package/dist/components/dataViewCard/DataViewCard.test.d.ts.map +1 -0
- package/dist/components/dataViewCard/DataViewCard.test.js +67 -0
- package/dist/components/dataViewCard/DataViewCard.test.js.map +1 -0
- package/dist/components/row/Row.d.ts +2 -1
- package/dist/components/row/Row.d.ts.map +1 -1
- package/dist/components/row/Row.js +2 -2
- package/dist/components/row/Row.js.map +1 -1
- package/dist/components/treeRow/TreeRow.d.ts +32 -0
- package/dist/components/treeRow/TreeRow.d.ts.map +1 -0
- package/dist/components/treeRow/TreeRow.js +19 -0
- package/dist/components/treeRow/TreeRow.js.map +1 -0
- package/dist/components/treeRow/TreeRow.stories.d.ts +13 -0
- package/dist/components/treeRow/TreeRow.stories.d.ts.map +1 -0
- package/dist/components/treeRow/TreeRow.stories.js +774 -0
- package/dist/components/treeRow/TreeRow.stories.js.map +1 -0
- package/dist/components/treeRow/TreeRow.test.d.ts +2 -0
- package/dist/components/treeRow/TreeRow.test.d.ts.map +1 -0
- package/dist/components/treeRow/TreeRow.test.js +262 -0
- package/dist/components/treeRow/TreeRow.test.js.map +1 -0
- package/dist/components/treeRow/TreeRowSection.d.ts +12 -0
- package/dist/components/treeRow/TreeRowSection.d.ts.map +1 -0
- package/dist/components/treeRow/TreeRowSection.js +20 -0
- package/dist/components/treeRow/TreeRowSection.js.map +1 -0
- package/dist/index.css +146 -1
- package/dist/index.css.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/src/components/arborLogo/ArborLogo.stories.tsx +663 -0
- package/src/components/arborLogo/ArborLogo.test.tsx +36 -0
- package/src/components/arborLogo/ArborLogo.tsx +92 -0
- package/src/components/arborLogo/__snapshots__/ArborLogo.test.tsx.snap +424 -0
- package/src/components/dataViewCard/DataViewCard.stories.tsx +464 -0
- package/src/components/dataViewCard/DataViewCard.test.tsx +127 -0
- package/src/components/dataViewCard/DataViewCard.tsx +62 -0
- package/src/components/dataViewCard/dataViewCard.scss +25 -0
- package/src/components/row/Row.tsx +4 -1
- package/src/components/row/row.scss +9 -1
- package/src/components/treeRow/TreeRow.stories.tsx +870 -0
- package/src/components/treeRow/TreeRow.test.tsx +371 -0
- package/src/components/treeRow/TreeRow.tsx +85 -0
- package/src/components/treeRow/TreeRowSection.tsx +56 -0
- package/src/components/treeRow/treeRow.scss +134 -0
- package/src/docs/Contributing.mdx +1 -0
- package/src/index.scss +2 -0
- package/src/index.ts +4 -1
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
import { describe, test, expect, vi } from 'vitest';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import userEvent from '@testing-library/user-event';
|
|
4
|
+
import '@testing-library/jest-dom/vitest';
|
|
5
|
+
import { TreeRow } from 'Components/treeRow/TreeRow';
|
|
6
|
+
import { Row } from 'Components/row/Row';
|
|
7
|
+
|
|
8
|
+
describe('TreeRow', () => {
|
|
9
|
+
describe('data-driven: flat list', () => {
|
|
10
|
+
test('renders all leaf items', () => {
|
|
11
|
+
const items = [
|
|
12
|
+
{ label: 'First Name', value: 'John' },
|
|
13
|
+
{ label: 'Last Name', value: 'Smith' },
|
|
14
|
+
];
|
|
15
|
+
render(<TreeRow items={items} />);
|
|
16
|
+
expect(screen.getByText('First Name')).toBeInTheDocument();
|
|
17
|
+
expect(screen.getByText('John')).toBeInTheDocument();
|
|
18
|
+
expect(screen.getByText('Last Name')).toBeInTheDocument();
|
|
19
|
+
expect(screen.getByText('Smith')).toBeInTheDocument();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('applies custom className to root', () => {
|
|
23
|
+
render(<TreeRow items={[{ label: 'L', value: 'V' }]} className="custom" />);
|
|
24
|
+
expect(document.querySelector('.ds-tree-row')).toHaveClass('custom');
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('data-driven: collapsible sections', () => {
|
|
29
|
+
test('renders a trigger button for items with children', () => {
|
|
30
|
+
const items = [{ label: 'Subject', children: [{ label: 'Child', value: 'V' }] }];
|
|
31
|
+
render(<TreeRow items={items} />);
|
|
32
|
+
expect(screen.getByRole('button', { name: /subject/i })).toBeInTheDocument();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('children are not in the document when collapsed', () => {
|
|
36
|
+
const items = [{ label: 'Subject', children: [{ label: 'Child', value: 'V' }] }];
|
|
37
|
+
render(<TreeRow items={items} />);
|
|
38
|
+
expect(screen.queryByText('Child')).not.toBeInTheDocument();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('clicking the trigger reveals children', async () => {
|
|
42
|
+
const user = userEvent.setup();
|
|
43
|
+
const items = [{ label: 'Subject', children: [{ label: 'Child', value: 'V' }] }];
|
|
44
|
+
render(<TreeRow items={items} />);
|
|
45
|
+
await user.click(screen.getByRole('button', { name: /subject/i }));
|
|
46
|
+
expect(screen.getByText('Child')).toBeInTheDocument();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('clicking the trigger again hides children', async () => {
|
|
50
|
+
const user = userEvent.setup();
|
|
51
|
+
const items = [{ label: 'Subject', children: [{ label: 'Child', value: 'V' }] }];
|
|
52
|
+
render(<TreeRow items={items} />);
|
|
53
|
+
const trigger = screen.getByRole('button', { name: /subject/i });
|
|
54
|
+
await user.click(trigger);
|
|
55
|
+
await user.click(trigger);
|
|
56
|
+
expect(screen.queryByText('Child')).not.toBeInTheDocument();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('trigger has aria-expanded false when closed', () => {
|
|
60
|
+
const items = [{ label: 'Subject', children: [{ label: 'Child', value: 'V' }] }];
|
|
61
|
+
render(<TreeRow items={items} />);
|
|
62
|
+
expect(screen.getByRole('button', { name: /subject/i })).toHaveAttribute('aria-expanded', 'false');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('trigger has aria-expanded true when open', async () => {
|
|
66
|
+
const user = userEvent.setup();
|
|
67
|
+
const items = [{ label: 'Subject', children: [{ label: 'Child', value: 'V' }] }];
|
|
68
|
+
render(<TreeRow items={items} />);
|
|
69
|
+
await user.click(screen.getByRole('button', { name: /subject/i }));
|
|
70
|
+
expect(screen.getByRole('button', { name: /subject/i })).toHaveAttribute('aria-expanded', 'true');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('multiple sections can be open simultaneously', async () => {
|
|
74
|
+
const user = userEvent.setup();
|
|
75
|
+
const items = [
|
|
76
|
+
{ label: 'Section A', children: [{ label: 'Child A', value: 'Va' }] },
|
|
77
|
+
{ label: 'Section B', children: [{ label: 'Child B', value: 'Vb' }] },
|
|
78
|
+
];
|
|
79
|
+
render(<TreeRow items={items} />);
|
|
80
|
+
await user.click(screen.getByRole('button', { name: /section a/i }));
|
|
81
|
+
await user.click(screen.getByRole('button', { name: /section b/i }));
|
|
82
|
+
expect(screen.getByText('Child A')).toBeInTheDocument();
|
|
83
|
+
expect(screen.getByText('Child B')).toBeInTheDocument();
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe('accessibility', () => {
|
|
88
|
+
test('decorative right caret icons are aria-hidden', () => {
|
|
89
|
+
const items = [{ label: 'Subject', onClick: vi.fn(), children: [{ label: 'Child', value: 'V' }] }];
|
|
90
|
+
render(<TreeRow items={items} />);
|
|
91
|
+
const carets = document.querySelectorAll('.ds-tree-row__caret');
|
|
92
|
+
expect(carets.length).toBeGreaterThan(0);
|
|
93
|
+
carets.forEach((caret) => {
|
|
94
|
+
expect(caret).toHaveAttribute('aria-hidden', 'true');
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('Enter key expands a collapsed section', async () => {
|
|
99
|
+
const user = userEvent.setup();
|
|
100
|
+
const items = [{ label: 'Subject', children: [{ label: 'Child', value: 'V' }] }];
|
|
101
|
+
render(<TreeRow items={items} />);
|
|
102
|
+
const trigger = screen.getByRole('button', { name: /subject/i });
|
|
103
|
+
trigger.focus();
|
|
104
|
+
await user.keyboard('{Enter}');
|
|
105
|
+
expect(screen.getByText('Child')).toBeInTheDocument();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('Space key expands a collapsed section', async () => {
|
|
109
|
+
const user = userEvent.setup();
|
|
110
|
+
const items = [{ label: 'Subject', children: [{ label: 'Child', value: 'V' }] }];
|
|
111
|
+
render(<TreeRow items={items} />);
|
|
112
|
+
const trigger = screen.getByRole('button', { name: /subject/i });
|
|
113
|
+
trigger.focus();
|
|
114
|
+
await user.keyboard(' ');
|
|
115
|
+
expect(screen.getByText('Child')).toBeInTheDocument();
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('depth indentation', () => {
|
|
120
|
+
test('nested section content is wrapped in ds-tree-row__content for CSS indentation', () => {
|
|
121
|
+
render(
|
|
122
|
+
<TreeRow>
|
|
123
|
+
<TreeRow.Section label="Parent">
|
|
124
|
+
<TreeRow.Section label="Child">
|
|
125
|
+
<Row label="Leaf" value="V" />
|
|
126
|
+
</TreeRow.Section>
|
|
127
|
+
</TreeRow.Section>
|
|
128
|
+
</TreeRow>,
|
|
129
|
+
);
|
|
130
|
+
const contents = document.querySelectorAll('.ds-tree-row__content');
|
|
131
|
+
expect(contents.length).toBeGreaterThan(0);
|
|
132
|
+
// Each content wrapper provides the indentation via CSS padding-left
|
|
133
|
+
contents.forEach((content) => {
|
|
134
|
+
expect(content).toHaveClass('ds-tree-row__content');
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe('compound component API', () => {
|
|
140
|
+
test('TreeRow.Section renders a trigger button', () => {
|
|
141
|
+
render(
|
|
142
|
+
<TreeRow>
|
|
143
|
+
<TreeRow.Section label="My Section">
|
|
144
|
+
<Row label="Child" value="V" />
|
|
145
|
+
</TreeRow.Section>
|
|
146
|
+
</TreeRow>,
|
|
147
|
+
);
|
|
148
|
+
expect(screen.getByRole('button', { name: /my section/i })).toBeInTheDocument();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('Row renders a leaf row inside TreeRow', () => {
|
|
152
|
+
render(
|
|
153
|
+
<TreeRow>
|
|
154
|
+
<Row label="Name" value="John" />
|
|
155
|
+
</TreeRow>,
|
|
156
|
+
);
|
|
157
|
+
expect(screen.getByText('Name')).toBeInTheDocument();
|
|
158
|
+
expect(screen.getByText('John')).toBeInTheDocument();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('compound Section expands on click', async () => {
|
|
162
|
+
const user = userEvent.setup();
|
|
163
|
+
render(
|
|
164
|
+
<TreeRow>
|
|
165
|
+
<TreeRow.Section label="Section">
|
|
166
|
+
<Row label="Child" value="V" />
|
|
167
|
+
</TreeRow.Section>
|
|
168
|
+
</TreeRow>,
|
|
169
|
+
);
|
|
170
|
+
await user.click(screen.getByRole('button', { name: /section/i }));
|
|
171
|
+
expect(screen.getByText('Child')).toBeInTheDocument();
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test('Row with onClick renders a clickable row', async () => {
|
|
175
|
+
const user = userEvent.setup();
|
|
176
|
+
const onClick = vi.fn();
|
|
177
|
+
render(
|
|
178
|
+
<TreeRow>
|
|
179
|
+
<Row label="Name" value="Jacob Black" onClick={onClick} />
|
|
180
|
+
</TreeRow>,
|
|
181
|
+
);
|
|
182
|
+
await user.click(screen.getByText('Name'));
|
|
183
|
+
expect(onClick).toHaveBeenCalledTimes(1);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
describe('defaultValue — pre-opened sections', () => {
|
|
188
|
+
test('section with matching id starts open', () => {
|
|
189
|
+
const items = [
|
|
190
|
+
{ id: 'address', label: 'Address', children: [{ label: 'Street', value: '671 Olympic Ave' }] },
|
|
191
|
+
];
|
|
192
|
+
render(<TreeRow items={items} defaultValue={['address']} />);
|
|
193
|
+
expect(screen.getByText('Street')).toBeInTheDocument();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test('section without matching id starts closed', () => {
|
|
197
|
+
const items = [
|
|
198
|
+
{ id: 'address', label: 'Address', children: [{ label: 'Street', value: '671 Olympic Ave' }] },
|
|
199
|
+
];
|
|
200
|
+
render(<TreeRow items={items} defaultValue={['other-id']} />);
|
|
201
|
+
expect(screen.queryByText('Street')).not.toBeInTheDocument();
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test('multiple sections can be pre-opened via defaultValue', () => {
|
|
205
|
+
const items = [
|
|
206
|
+
{ id: 'personal', label: 'Personal', children: [{ label: 'Name', value: 'Bella Swan' }] },
|
|
207
|
+
{ id: 'address', label: 'Address', children: [{ label: 'Street', value: '671 Olympic Ave' }] },
|
|
208
|
+
];
|
|
209
|
+
render(<TreeRow items={items} defaultValue={['personal', 'address']} />);
|
|
210
|
+
expect(screen.getByText('Name')).toBeInTheDocument();
|
|
211
|
+
expect(screen.getByText('Street')).toBeInTheDocument();
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test('compound TreeRow.Section with id starts open when id in defaultValue', () => {
|
|
215
|
+
render(
|
|
216
|
+
<TreeRow defaultValue={['contacts']}>
|
|
217
|
+
<TreeRow.Section id="contacts" label="Contacts">
|
|
218
|
+
<Row label="Charlie Swan" value="Father" />
|
|
219
|
+
</TreeRow.Section>
|
|
220
|
+
</TreeRow>,
|
|
221
|
+
);
|
|
222
|
+
expect(screen.getByText('Charlie Swan')).toBeInTheDocument();
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test('section without id uses generated id and starts closed by default', () => {
|
|
226
|
+
const items = [
|
|
227
|
+
{ label: 'Address', children: [{ label: 'Street', value: '671 Olympic Ave' }] },
|
|
228
|
+
];
|
|
229
|
+
render(<TreeRow items={items} />);
|
|
230
|
+
expect(screen.queryByText('Street')).not.toBeInTheDocument();
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
describe('Section onClick', () => {
|
|
235
|
+
test('clicking the row fires onClick', async () => {
|
|
236
|
+
const user = userEvent.setup();
|
|
237
|
+
const onClick = vi.fn();
|
|
238
|
+
render(
|
|
239
|
+
<TreeRow>
|
|
240
|
+
<TreeRow.Section label="Contacts" onClick={onClick}>
|
|
241
|
+
<Row label="Name" value="Jacob Black" />
|
|
242
|
+
</TreeRow.Section>
|
|
243
|
+
</TreeRow>,
|
|
244
|
+
);
|
|
245
|
+
await user.click(document.querySelector('.ds-tree-row__row--branch')!);
|
|
246
|
+
expect(onClick).toHaveBeenCalledTimes(1);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test('clicking the trigger does not fire section onClick', async () => {
|
|
250
|
+
const user = userEvent.setup();
|
|
251
|
+
const onClick = vi.fn();
|
|
252
|
+
render(
|
|
253
|
+
<TreeRow>
|
|
254
|
+
<TreeRow.Section label="Contacts" onClick={onClick}>
|
|
255
|
+
<Row label="Name" value="Jacob Black" />
|
|
256
|
+
</TreeRow.Section>
|
|
257
|
+
</TreeRow>,
|
|
258
|
+
);
|
|
259
|
+
await user.click(screen.getByRole('button', { name: /contacts/i }));
|
|
260
|
+
expect(onClick).not.toHaveBeenCalled();
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test('clicking the trigger still expands the section when onClick is set', async () => {
|
|
264
|
+
const user = userEvent.setup();
|
|
265
|
+
render(
|
|
266
|
+
<TreeRow>
|
|
267
|
+
<TreeRow.Section label="Contacts" onClick={vi.fn()}>
|
|
268
|
+
<Row label="Name" value="Jacob Black" />
|
|
269
|
+
</TreeRow.Section>
|
|
270
|
+
</TreeRow>,
|
|
271
|
+
);
|
|
272
|
+
await user.click(screen.getByRole('button', { name: /contacts/i }));
|
|
273
|
+
expect(screen.getByText('Jacob Black')).toBeInTheDocument();
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test('section with onClick has ds-tree-row__row--clickable class', () => {
|
|
277
|
+
const onClick = vi.fn();
|
|
278
|
+
render(
|
|
279
|
+
<TreeRow>
|
|
280
|
+
<TreeRow.Section label="Contacts" onClick={onClick}>
|
|
281
|
+
<Row label="Name" value="V" />
|
|
282
|
+
</TreeRow.Section>
|
|
283
|
+
</TreeRow>,
|
|
284
|
+
);
|
|
285
|
+
expect(document.querySelector('.ds-tree-row__row--clickable')).toBeInTheDocument();
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test('section without onClick does not have ds-tree-row__row--clickable class', () => {
|
|
289
|
+
render(
|
|
290
|
+
<TreeRow>
|
|
291
|
+
<TreeRow.Section label="Contacts">
|
|
292
|
+
<Row label="Name" value="V" />
|
|
293
|
+
</TreeRow.Section>
|
|
294
|
+
</TreeRow>,
|
|
295
|
+
);
|
|
296
|
+
expect(document.querySelector('.ds-tree-row__row--clickable')).not.toBeInTheDocument();
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
test('section with onClick renders an arrow-right caret icon', () => {
|
|
300
|
+
render(
|
|
301
|
+
<TreeRow>
|
|
302
|
+
<TreeRow.Section label="Contacts" onClick={vi.fn()}>
|
|
303
|
+
<Row label="Name" value="V" />
|
|
304
|
+
</TreeRow.Section>
|
|
305
|
+
</TreeRow>,
|
|
306
|
+
);
|
|
307
|
+
expect(document.querySelector('.ds-tree-row__caret.ds-icon-arrow-right')).toBeInTheDocument();
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
test('section without onClick does not render an arrow-right caret icon', () => {
|
|
311
|
+
render(
|
|
312
|
+
<TreeRow>
|
|
313
|
+
<TreeRow.Section label="Contacts">
|
|
314
|
+
<Row label="Name" value="V" />
|
|
315
|
+
</TreeRow.Section>
|
|
316
|
+
</TreeRow>,
|
|
317
|
+
);
|
|
318
|
+
expect(document.querySelector('.ds-tree-row__caret.ds-icon-arrow-right')).not.toBeInTheDocument();
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
describe('onClick on data-driven items', () => {
|
|
323
|
+
test('clicking a leaf row fires the onClick handler', async () => {
|
|
324
|
+
const user = userEvent.setup();
|
|
325
|
+
const onClick = vi.fn();
|
|
326
|
+
const items = [{ label: 'Name', value: 'Bella Swan', onClick }];
|
|
327
|
+
render(<TreeRow items={items} />);
|
|
328
|
+
await user.click(screen.getByText('Name'));
|
|
329
|
+
expect(onClick).toHaveBeenCalledTimes(1);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
test('clickable leaf row has ds-row--clickable class', () => {
|
|
333
|
+
const onClick = vi.fn();
|
|
334
|
+
const items = [{ label: 'Name', value: 'Bella Swan', onClick }];
|
|
335
|
+
render(<TreeRow items={items} />);
|
|
336
|
+
expect(document.querySelector('.ds-row--clickable')).toBeInTheDocument();
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
test('non-clickable leaf row does not have ds-row--clickable class', () => {
|
|
340
|
+
const items = [{ label: 'Name', value: 'Bella Swan' }];
|
|
341
|
+
render(<TreeRow items={items} />);
|
|
342
|
+
expect(document.querySelector('.ds-row--clickable')).not.toBeInTheDocument();
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
test('clicking a section row fires onClick', async () => {
|
|
346
|
+
const user = userEvent.setup();
|
|
347
|
+
const onClick = vi.fn();
|
|
348
|
+
const items = [{ label: 'Section', onClick, children: [{ label: 'Child', value: 'V' }] }];
|
|
349
|
+
render(<TreeRow items={items} />);
|
|
350
|
+
await user.click(document.querySelector('.ds-tree-row__row--branch')!);
|
|
351
|
+
expect(onClick).toHaveBeenCalledTimes(1);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
test('clicking the trigger on a section item does not fire onClick', async () => {
|
|
355
|
+
const user = userEvent.setup();
|
|
356
|
+
const onClick = vi.fn();
|
|
357
|
+
const items = [{ label: 'Section', onClick, children: [{ label: 'Child', value: 'V' }] }];
|
|
358
|
+
render(<TreeRow items={items} />);
|
|
359
|
+
await user.click(screen.getByRole('button', { name: /section/i }));
|
|
360
|
+
expect(onClick).not.toHaveBeenCalled();
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
test('section item with onClick still expands when trigger is clicked', async () => {
|
|
364
|
+
const user = userEvent.setup();
|
|
365
|
+
const items = [{ label: 'Section', onClick: vi.fn(), children: [{ label: 'Child', value: 'V' }] }];
|
|
366
|
+
render(<TreeRow items={items} />);
|
|
367
|
+
await user.click(screen.getByRole('button', { name: /section/i }));
|
|
368
|
+
expect(screen.getByText('Child')).toBeInTheDocument();
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { useContext } from 'react';
|
|
2
|
+
import type { MouseEventHandler, ReactNode } from 'react';
|
|
3
|
+
import * as Accordion from '@radix-ui/react-accordion';
|
|
4
|
+
import classNames from 'classnames';
|
|
5
|
+
import { Row } from 'Components/row/Row';
|
|
6
|
+
import { DepthContext, getDepthPadding, TreeRowSection } from 'Components/treeRow/TreeRowSection';
|
|
7
|
+
|
|
8
|
+
export type { TreeRowSectionProps } from 'Components/treeRow/TreeRowSection';
|
|
9
|
+
|
|
10
|
+
export type TreeRowItem = {
|
|
11
|
+
id?: string;
|
|
12
|
+
label?: string;
|
|
13
|
+
value?: string;
|
|
14
|
+
note?: string;
|
|
15
|
+
onClick?: MouseEventHandler<HTMLDivElement>;
|
|
16
|
+
children?: TreeRowItem[];
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type TreeRowBaseProps = {
|
|
20
|
+
defaultValue?: string[];
|
|
21
|
+
className?: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type TreeRowDataDrivenProps = TreeRowBaseProps & {
|
|
25
|
+
items: TreeRowItem[];
|
|
26
|
+
children?: never;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
type TreeRowCompoundProps = TreeRowBaseProps & {
|
|
30
|
+
items?: never;
|
|
31
|
+
children?: ReactNode;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type TreeRowProps = TreeRowDataDrivenProps | TreeRowCompoundProps;
|
|
35
|
+
|
|
36
|
+
const DataDrivenList = ({ items }: { items: TreeRowItem[] }) => {
|
|
37
|
+
const depth = useContext(DepthContext);
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<>
|
|
41
|
+
{items.map((item, index) => {
|
|
42
|
+
const hasChildren = Array.isArray(item.children) && item.children.length > 0;
|
|
43
|
+
|
|
44
|
+
if (!hasChildren) {
|
|
45
|
+
return (
|
|
46
|
+
<Row
|
|
47
|
+
key={item.id ?? item.label ?? String(index)}
|
|
48
|
+
label={item.label}
|
|
49
|
+
value={item.value}
|
|
50
|
+
note={item.note}
|
|
51
|
+
onClick={item.onClick}
|
|
52
|
+
className="ds-tree-row__row"
|
|
53
|
+
style={{ paddingLeft: getDepthPadding(depth) }}
|
|
54
|
+
/>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<TreeRowSection key={item.id ?? item.label ?? String(index)} id={item.id} label={item.label ?? ''} onClick={item.onClick}>
|
|
60
|
+
<DataDrivenList items={item.children ?? []} />
|
|
61
|
+
</TreeRowSection>
|
|
62
|
+
);
|
|
63
|
+
})}
|
|
64
|
+
</>
|
|
65
|
+
);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const TreeRow = ({ items, defaultValue, children, className }: TreeRowProps) => (
|
|
69
|
+
<Accordion.Root
|
|
70
|
+
type="multiple"
|
|
71
|
+
defaultValue={defaultValue}
|
|
72
|
+
className={classNames('ds-tree-row', className)}
|
|
73
|
+
>
|
|
74
|
+
<DepthContext.Provider value={0}>
|
|
75
|
+
{items ? <DataDrivenList items={items} /> : children}
|
|
76
|
+
</DepthContext.Provider>
|
|
77
|
+
</Accordion.Root>
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
TreeRow.Section = TreeRowSection;
|
|
81
|
+
|
|
82
|
+
export namespace TreeRow {
|
|
83
|
+
export type Item = TreeRowItem;
|
|
84
|
+
export type Props = TreeRowProps;
|
|
85
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { createContext, useContext, useId } from 'react';
|
|
2
|
+
import type { MouseEventHandler, ReactNode } from 'react';
|
|
3
|
+
import * as Accordion from '@radix-ui/react-accordion';
|
|
4
|
+
import classNames from 'classnames';
|
|
5
|
+
import { Icon } from 'Components/icon/Icon';
|
|
6
|
+
|
|
7
|
+
export const DepthContext = createContext(0);
|
|
8
|
+
|
|
9
|
+
export function getDepthPadding(depth: number): string {
|
|
10
|
+
if (depth === 0) return 'var(--section-list-row-spacing-horizontal)';
|
|
11
|
+
return `calc(var(--spacing-xxlarge, 32px) * ${depth})`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type TreeRowSectionProps = {
|
|
15
|
+
id?: string;
|
|
16
|
+
label: string;
|
|
17
|
+
children: ReactNode;
|
|
18
|
+
className?: string;
|
|
19
|
+
onClick?: MouseEventHandler<HTMLDivElement>;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const TreeRowSection = ({ id: idProp, label, children, className, onClick }: TreeRowSectionProps) => {
|
|
23
|
+
const depth = useContext(DepthContext);
|
|
24
|
+
const generatedId = useId();
|
|
25
|
+
const id = idProp ?? generatedId;
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<Accordion.Item value={id} className={classNames('ds-tree-row__item', className)}>
|
|
29
|
+
<div
|
|
30
|
+
className={classNames('ds-tree-row__row', 'ds-tree-row__row--branch', {
|
|
31
|
+
'ds-tree-row__row--clickable': !!onClick,
|
|
32
|
+
})}
|
|
33
|
+
style={{ paddingLeft: getDepthPadding(depth) }}
|
|
34
|
+
onClick={onClick}
|
|
35
|
+
>
|
|
36
|
+
<Accordion.Trigger
|
|
37
|
+
className="ds-tree-row__trigger"
|
|
38
|
+
onClick={event => event.stopPropagation()}
|
|
39
|
+
>
|
|
40
|
+
<span className="ds-tree-row__trigger-label">{label}</span>
|
|
41
|
+
<Icon name="chevron-up" className="ds-tree-row__expand-icon" size={16} />
|
|
42
|
+
<Icon name="chevron-down" className="ds-tree-row__collapse-icon" size={16} />
|
|
43
|
+
</Accordion.Trigger>
|
|
44
|
+
{onClick && (
|
|
45
|
+
<>
|
|
46
|
+
<Icon name="chevron-right" className="ds-tree-row__caret" size={16} />
|
|
47
|
+
<Icon name="arrow-right" className="ds-tree-row__caret" size={16} />
|
|
48
|
+
</>
|
|
49
|
+
)}
|
|
50
|
+
</div>
|
|
51
|
+
<Accordion.Content className="ds-tree-row__content">
|
|
52
|
+
<DepthContext.Provider value={depth + 1}>{children}</DepthContext.Provider>
|
|
53
|
+
</Accordion.Content>
|
|
54
|
+
</Accordion.Item>
|
|
55
|
+
);
|
|
56
|
+
};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
.ds-tree-row {
|
|
2
|
+
width: 100%;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
.ds-tree-row__caret {
|
|
6
|
+
flex-shrink: 0;
|
|
7
|
+
color: var(--section-list-row-default-color-icon-arrow, #aaa);
|
|
8
|
+
|
|
9
|
+
&.ds-icon-arrow-right {
|
|
10
|
+
display: none;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.ds-tree-row__row {
|
|
15
|
+
display: flex;
|
|
16
|
+
align-items: center;
|
|
17
|
+
justify-content: space-between;
|
|
18
|
+
border-bottom: 1px solid var(--section-list-row-color-border, #f8f8f8);
|
|
19
|
+
border-radius: var(--section-list-row-radius, 8px);
|
|
20
|
+
|
|
21
|
+
&--branch {
|
|
22
|
+
background-color: var(--section-list-row-default-color-background);
|
|
23
|
+
padding-block: var(--spacing-xsmall, 4px);
|
|
24
|
+
padding-inline-end: var(--section-list-row-spacing-horizontal);
|
|
25
|
+
gap: var(--spacing-small);
|
|
26
|
+
|
|
27
|
+
&:hover:not(:has(.ds-tree-row__trigger:hover)) {
|
|
28
|
+
background: var(--section-list-row-hover-hover-bg, #f8f8f8);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
&--clickable {
|
|
33
|
+
cursor: pointer;
|
|
34
|
+
|
|
35
|
+
&:hover:not(:has(.ds-tree-row__trigger:hover)) {
|
|
36
|
+
.ds-tree-row__caret {
|
|
37
|
+
&.ds-icon-chevron-right {
|
|
38
|
+
display: none;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
&.ds-icon-arrow-right {
|
|
42
|
+
display: block;
|
|
43
|
+
color: var(--section-list-row-hover-color-icon-arrow, #2f2f2f);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.ds-tree-row__trigger {
|
|
52
|
+
display: inline-flex;
|
|
53
|
+
align-items: center;
|
|
54
|
+
gap: var(--spacing-small, 8px);
|
|
55
|
+
background: transparent;
|
|
56
|
+
border: none;
|
|
57
|
+
border-radius: var(--border-radius-small, 8px);
|
|
58
|
+
padding: var(--spacing-xsmall, 4px) var(--spacing-small, 8px);
|
|
59
|
+
color: var(--button-medium-secondary-default-color-text, #2f2f2f);
|
|
60
|
+
cursor: pointer;
|
|
61
|
+
flex-shrink: 0;
|
|
62
|
+
|
|
63
|
+
&:hover {
|
|
64
|
+
background: var(--button-medium-secondary-hover-color-background, #f8f8f8);
|
|
65
|
+
border-radius: var(--border-radius-small, 8px);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
&:focus-visible {
|
|
69
|
+
outline: none;
|
|
70
|
+
border-radius: var(--border-radius-small, 8px);
|
|
71
|
+
background: rgb(255 255 255 / 1%);
|
|
72
|
+
box-shadow: 0 0 0 3px var(--color-brand-300, #7ed28e);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.ds-tree-row__trigger-label {
|
|
77
|
+
font-weight: var(--type-body-bold-weight, 600);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.ds-tree-row__expand-icon {
|
|
81
|
+
display: block;
|
|
82
|
+
flex-shrink: 0;
|
|
83
|
+
color: var(--section-list-row-default-color-icon-arrow, #2f2f2f);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.ds-tree-row__collapse-icon {
|
|
87
|
+
display: none;
|
|
88
|
+
flex-shrink: 0;
|
|
89
|
+
color: var(--section-list-row-default-color-icon-arrow, #2f2f2f);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.ds-tree-row__item[data-state='open'] {
|
|
93
|
+
> .ds-tree-row__row {
|
|
94
|
+
.ds-tree-row__expand-icon {
|
|
95
|
+
display: none;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.ds-tree-row__collapse-icon {
|
|
99
|
+
display: block;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.ds-tree-row__content {
|
|
105
|
+
overflow: hidden;
|
|
106
|
+
|
|
107
|
+
&[data-state='open'] {
|
|
108
|
+
animation: ds-tree-row-open 150ms ease-out;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
&[data-state='closed'] {
|
|
112
|
+
animation: ds-tree-row-close 150ms ease-in;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
@keyframes ds-tree-row-open {
|
|
117
|
+
from {
|
|
118
|
+
height: 0;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
to {
|
|
122
|
+
height: var(--radix-accordion-content-height);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
@keyframes ds-tree-row-close {
|
|
127
|
+
from {
|
|
128
|
+
height: var(--radix-accordion-content-height);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
to {
|
|
132
|
+
height: 0;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -239,3 +239,4 @@ yarn watch
|
|
|
239
239
|
- [GitHub repository](https://github.com/arbor-education/design-system.components)
|
|
240
240
|
- [CONTRIBUTING.md](https://github.com/arbor-education/design-system.components/blob/main/CONTRIBUTING.md) — the full contribution guide
|
|
241
241
|
- [NPM package](https://www.npmjs.com/package/@arbor-education/design-system.components)
|
|
242
|
+
- [AI personalities (FRENDS)](https://orchard.atlassian.net/wiki/spaces/AG/pages/2575958235/AI+personalities) — opt-in personality skills for Claude Code / Cursor and why we don't default them in repos
|
package/src/index.scss
CHANGED
|
@@ -51,4 +51,6 @@
|
|
|
51
51
|
@use "components/row/row.scss";
|
|
52
52
|
@use "components/combobox/combobox.scss";
|
|
53
53
|
@use "components/toggle/toggle.scss";
|
|
54
|
+
@use "components/dataViewCard/dataViewCard.scss";
|
|
55
|
+
@use "components/treeRow/treeRow.scss";
|
|
54
56
|
@import "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap";
|
package/src/index.ts
CHANGED
|
@@ -52,8 +52,11 @@ export { Toast } from 'Components/toast/Toast';
|
|
|
52
52
|
export { Toggle } from 'Components/toggle/Toggle';
|
|
53
53
|
export { Tooltip } from 'Components/tooltip/Tooltip';
|
|
54
54
|
export { TooltipWrapper } from 'Components/tooltip/TooltipWrapper';
|
|
55
|
-
export {
|
|
55
|
+
export { GovhubLogo, KeyLogo, RobinLogo, SampeopleLogo, TimetablerLogo } from 'Components/userDropdown/assets/logos';
|
|
56
|
+
export { TreeRow, type TreeRowItem, type TreeRowProps, type TreeRowSectionProps } from 'Components/treeRow/TreeRow';
|
|
56
57
|
export { UserDropdown } from 'Components/userDropdown/UserDropdown';
|
|
57
58
|
export { ModalUtils } from 'Utils/ModalUtils';
|
|
58
59
|
export { PopupParentContext } from 'Utils/PopupParentContext';
|
|
59
60
|
export { SlideoverUtils } from 'Utils/SlideoverUtils';
|
|
61
|
+
export { ArborLogo, type ArborLogoProps } from 'Components/arborLogo/ArborLogo';
|
|
62
|
+
export { DataViewCard, type DataViewCardProps } from 'Components/dataViewCard/DataViewCard';
|