@sproutsocial/seeds-react-tree 0.4.0 → 0.4.3
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/.turbo/turbo-build.log +9 -10
- package/CHANGELOG.md +33 -0
- package/dist/esm/index.js +106 -281
- package/dist/esm/index.js.map +1 -1
- package/dist/index.js +106 -281
- package/dist/index.js.map +1 -1
- package/dist/tree.css +318 -0
- package/package.json +11 -10
- package/src/Common/TreeSearchableSelectBase.tsx +36 -119
- package/src/Tree.stories.tsx +26 -0
- package/src/Tree.tsx +4 -4
- package/src/TreeCombobox.tsx +17 -71
- package/src/TreeItem.tsx +33 -22
- package/src/__tests__/Tree.test.tsx +61 -0
- package/src/cn.ts +28 -0
- package/src/tree.css +318 -0
- package/src/TreeStyles.tsx +0 -115
package/dist/tree.css
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Seeds Tree component classes
|
|
3
|
+
* CSS-class mirror of the styled-components that previously styled the tree
|
|
4
|
+
* package (TreeStyles.tsx, TreeCombobox.tsx, Common/TreeSearchableSelectBase.tsx).
|
|
5
|
+
*
|
|
6
|
+
* Requires @sproutsocial/seeds-react-theme/dist/theme-all.css imported for
|
|
7
|
+
* CSS variable definitions and dark mode support.
|
|
8
|
+
*
|
|
9
|
+
* Rules live in @layer components so consumer Tailwind utilities (which sit in
|
|
10
|
+
* the later `utilities` layer) can override component styles on equal
|
|
11
|
+
* specificity. Without the layer, these unlayered rules would out-rank any
|
|
12
|
+
* utility class.
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* <ul class="seeds-tree" role="tree">
|
|
16
|
+
* <li class="seeds-tree-item" role="treeitem">
|
|
17
|
+
* <div class="seeds-tree-item-row" style="--tree-level: 1">…</div>
|
|
18
|
+
* </li>
|
|
19
|
+
* </ul>
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
@layer components {
|
|
23
|
+
/* ---------------------------------------------------------------------- */
|
|
24
|
+
/* Tree / TreeItem (mirror of TreeStyles.tsx) */
|
|
25
|
+
/* ---------------------------------------------------------------------- */
|
|
26
|
+
|
|
27
|
+
.seeds-tree {
|
|
28
|
+
list-style: none;
|
|
29
|
+
margin: 0;
|
|
30
|
+
padding: 0;
|
|
31
|
+
font-family: var(--font-family);
|
|
32
|
+
font-size: var(--font-size-200);
|
|
33
|
+
line-height: var(--line-height-200);
|
|
34
|
+
color: var(--color-text-body);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.seeds-tree-item {
|
|
38
|
+
list-style: none;
|
|
39
|
+
outline: none;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/*
|
|
43
|
+
* Tree rows stack tightly, so the standard outset Seeds focusRing bleeds
|
|
44
|
+
* into adjacent rows. Use an inset outline so the ring is drawn just inside
|
|
45
|
+
* the row's edge and never overlaps siblings or children.
|
|
46
|
+
*
|
|
47
|
+
* The same ring is drawn when [data-treeitem-active] is set so combobox
|
|
48
|
+
* hosts that drive the tree via aria-activedescendant (no real DOM focus)
|
|
49
|
+
* still get a visible "active" indicator on the row.
|
|
50
|
+
*/
|
|
51
|
+
.seeds-tree-item:focus-visible > .seeds-tree-item-row,
|
|
52
|
+
.seeds-tree-item[data-treeitem-active] > .seeds-tree-item-row {
|
|
53
|
+
outline: 2px solid var(--color-button-primary-bg-base);
|
|
54
|
+
outline-offset: -2px;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.seeds-tree-item-row {
|
|
58
|
+
display: flex;
|
|
59
|
+
align-items: center;
|
|
60
|
+
gap: var(--space-300);
|
|
61
|
+
padding: var(--space-300);
|
|
62
|
+
/* Indentation per depth: --tree-level is set inline on each row. */
|
|
63
|
+
padding-left: calc(
|
|
64
|
+
var(--space-300) + (var(--tree-level) - 1) * var(--space-400)
|
|
65
|
+
);
|
|
66
|
+
border-radius: var(--radius-400);
|
|
67
|
+
cursor: pointer;
|
|
68
|
+
user-select: none;
|
|
69
|
+
background: transparent;
|
|
70
|
+
transition: background-color var(--duration-fast) var(--ease-ease_in);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.seeds-tree-item-row:hover {
|
|
74
|
+
background: var(--color-listItem-bg-hover);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.seeds-tree-item-row.selected {
|
|
78
|
+
background: var(--color-listItem-bg-hover);
|
|
79
|
+
font-weight: var(--font-weight-semibold);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.seeds-tree-item-row.disabled {
|
|
83
|
+
opacity: 0.4;
|
|
84
|
+
cursor: not-allowed;
|
|
85
|
+
pointer-events: none;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.seeds-tree-item-group {
|
|
89
|
+
list-style: none;
|
|
90
|
+
margin: 0;
|
|
91
|
+
padding: 0;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.seeds-tree-item-label {
|
|
95
|
+
flex: 1;
|
|
96
|
+
min-width: 0;
|
|
97
|
+
overflow: hidden;
|
|
98
|
+
text-overflow: ellipsis;
|
|
99
|
+
white-space: nowrap;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.seeds-tree-item-icon {
|
|
103
|
+
display: inline-flex;
|
|
104
|
+
align-items: center;
|
|
105
|
+
flex-shrink: 0;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.seeds-tree-item-chevron {
|
|
109
|
+
display: inline-flex;
|
|
110
|
+
align-items: center;
|
|
111
|
+
justify-content: center;
|
|
112
|
+
flex-shrink: 0;
|
|
113
|
+
width: var(--space-500);
|
|
114
|
+
height: var(--space-500);
|
|
115
|
+
padding: 0;
|
|
116
|
+
border: none;
|
|
117
|
+
background: transparent;
|
|
118
|
+
color: var(--color-icon-base);
|
|
119
|
+
cursor: pointer;
|
|
120
|
+
border-radius: var(--radius-300);
|
|
121
|
+
transition: transform var(--duration-fast) var(--ease-ease_in);
|
|
122
|
+
transform: rotate(-90deg);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.seeds-tree-item-chevron.expanded {
|
|
126
|
+
transform: rotate(0deg);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.seeds-tree-item-chevron:hover {
|
|
130
|
+
background: var(--color-listItem-bg-hover);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/* ---------------------------------------------------------------------- */
|
|
134
|
+
/* TreeCombobox (mirror of TreeCombobox.tsx) */
|
|
135
|
+
/* ---------------------------------------------------------------------- */
|
|
136
|
+
|
|
137
|
+
.seeds-tree-combobox {
|
|
138
|
+
display: flex;
|
|
139
|
+
flex-direction: column;
|
|
140
|
+
flex: 1;
|
|
141
|
+
min-height: 0;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.seeds-tree-combobox-search {
|
|
145
|
+
padding: var(--space-300);
|
|
146
|
+
border-bottom: 1px solid var(--color-container-border-base);
|
|
147
|
+
flex-shrink: 0;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.seeds-tree-combobox-input-group {
|
|
151
|
+
display: flex;
|
|
152
|
+
align-items: center;
|
|
153
|
+
gap: var(--space-200);
|
|
154
|
+
padding: var(--space-200) var(--space-300);
|
|
155
|
+
border-radius: var(--radius-500);
|
|
156
|
+
border: 1px solid var(--color-container-border-base);
|
|
157
|
+
background: var(--color-container-bg-base);
|
|
158
|
+
color: var(--color-icon-base);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.seeds-tree-combobox-input-group:focus-within {
|
|
162
|
+
box-shadow:
|
|
163
|
+
0 0 0 1px var(--color-button-primary-bg-base),
|
|
164
|
+
0 0px 0px 4px
|
|
165
|
+
color-mix(
|
|
166
|
+
in srgb,
|
|
167
|
+
var(--color-button-primary-bg-base),
|
|
168
|
+
transparent 70%
|
|
169
|
+
);
|
|
170
|
+
outline: none;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.seeds-tree-combobox-input {
|
|
174
|
+
flex: 1;
|
|
175
|
+
border: none;
|
|
176
|
+
background: transparent;
|
|
177
|
+
outline: none;
|
|
178
|
+
font-family: var(--font-family);
|
|
179
|
+
font-size: var(--font-size-200);
|
|
180
|
+
line-height: var(--line-height-200);
|
|
181
|
+
color: var(--color-text-body);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.seeds-tree-combobox-input::placeholder {
|
|
185
|
+
color: var(--color-text-subtext);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.seeds-tree-combobox-list {
|
|
189
|
+
padding: var(--space-200);
|
|
190
|
+
overflow: auto;
|
|
191
|
+
flex: 1;
|
|
192
|
+
min-height: 0;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.seeds-tree-combobox-empty {
|
|
196
|
+
padding: var(--space-300) var(--space-350);
|
|
197
|
+
text-align: center;
|
|
198
|
+
color: var(--color-text-subtext);
|
|
199
|
+
font-size: var(--font-size-200);
|
|
200
|
+
line-height: var(--line-height-200);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/* ---------------------------------------------------------------------- */
|
|
204
|
+
/* TreeSearchableSelect (mirror of Common/TreeSearchableSelectBase.tsx) */
|
|
205
|
+
/* ---------------------------------------------------------------------- */
|
|
206
|
+
|
|
207
|
+
.seeds-tree-select-trigger {
|
|
208
|
+
display: flex;
|
|
209
|
+
align-items: center;
|
|
210
|
+
justify-content: space-between;
|
|
211
|
+
gap: var(--space-300);
|
|
212
|
+
padding: var(--space-200) var(--space-350);
|
|
213
|
+
min-width: 160px;
|
|
214
|
+
width: auto;
|
|
215
|
+
border-radius: var(--radius-500);
|
|
216
|
+
border: 1px solid var(--color-form-border-base);
|
|
217
|
+
background: var(--color-form-bg-base);
|
|
218
|
+
font-family: var(--font-family);
|
|
219
|
+
font-size: var(--font-size-200);
|
|
220
|
+
line-height: var(--line-height-200);
|
|
221
|
+
color: var(--color-text-body);
|
|
222
|
+
text-align: left;
|
|
223
|
+
cursor: default;
|
|
224
|
+
outline: none;
|
|
225
|
+
user-select: none;
|
|
226
|
+
transition:
|
|
227
|
+
border-color var(--duration-fast) var(--ease-ease_in),
|
|
228
|
+
box-shadow var(--duration-fast) var(--ease-ease_in);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.seeds-tree-select-trigger.full-width {
|
|
232
|
+
width: 100%;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.seeds-tree-select-trigger:focus-visible {
|
|
236
|
+
box-shadow:
|
|
237
|
+
0 0 0 1px var(--color-button-primary-bg-base),
|
|
238
|
+
0 0px 0px 4px
|
|
239
|
+
color-mix(
|
|
240
|
+
in srgb,
|
|
241
|
+
var(--color-button-primary-bg-base),
|
|
242
|
+
transparent 70%
|
|
243
|
+
);
|
|
244
|
+
outline: none;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.seeds-tree-select-trigger::-moz-focus-inner {
|
|
248
|
+
border: 0;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.seeds-tree-select-trigger[aria-disabled="true"],
|
|
252
|
+
.seeds-tree-select-trigger:disabled {
|
|
253
|
+
opacity: 0.4;
|
|
254
|
+
cursor: not-allowed;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.seeds-tree-select-trigger.invalid {
|
|
258
|
+
border-color: var(--color-form-border-error);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
.seeds-tree-select-trigger-label {
|
|
262
|
+
flex: 1;
|
|
263
|
+
min-width: 0;
|
|
264
|
+
overflow: hidden;
|
|
265
|
+
text-overflow: ellipsis;
|
|
266
|
+
white-space: nowrap;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.seeds-tree-select-placeholder {
|
|
270
|
+
color: var(--color-text-subtext);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
.seeds-tree-select-chevron {
|
|
274
|
+
display: inline-flex;
|
|
275
|
+
align-items: center;
|
|
276
|
+
color: var(--color-icon-base);
|
|
277
|
+
transition: transform var(--duration-fast) var(--ease-ease_in);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
.seeds-tree-select-chevron.open {
|
|
281
|
+
transform: rotate(-180deg);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/*
|
|
285
|
+
* Visually-hidden input that mirrors the combobox's selected value into a
|
|
286
|
+
* form-submittable element. Matches Base UI's pattern (used by v3
|
|
287
|
+
* SearchableSelect): the input stays in the DOM so HTML forms pick up the
|
|
288
|
+
* value, but it's removed from view and from the accessibility tree.
|
|
289
|
+
*/
|
|
290
|
+
.seeds-tree-select-hidden-input {
|
|
291
|
+
clip-path: inset(50%);
|
|
292
|
+
overflow: hidden;
|
|
293
|
+
white-space: nowrap;
|
|
294
|
+
border: 0;
|
|
295
|
+
padding: 0;
|
|
296
|
+
width: 1px;
|
|
297
|
+
height: 1px;
|
|
298
|
+
margin: -1px;
|
|
299
|
+
position: fixed;
|
|
300
|
+
top: 0;
|
|
301
|
+
left: 0;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.seeds-tree-select-popout {
|
|
305
|
+
display: flex;
|
|
306
|
+
flex-direction: column;
|
|
307
|
+
/* Width sizes to content within bounds: at least the trigger width, never */
|
|
308
|
+
/* wider than the available viewport space. Shallow popups stay flush with */
|
|
309
|
+
/* the trigger; deeply nested trees expand horizontally as needed. */
|
|
310
|
+
min-width: var(--radix-popover-trigger-width);
|
|
311
|
+
max-width: var(--radix-popper-available-width);
|
|
312
|
+
/* Height caps at 400px (or available viewport) so a popup with many */
|
|
313
|
+
/* expanded branches scrolls vertically instead of growing off-screen. */
|
|
314
|
+
max-height: min(var(--radix-popper-available-height), 400px);
|
|
315
|
+
/* Width is set inline via --tree-select-width (defaults to max-content). */
|
|
316
|
+
width: var(--tree-select-width, max-content);
|
|
317
|
+
}
|
|
318
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sproutsocial/seeds-react-tree",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
4
4
|
"description": "Seeds React Tree (WAI-ARIA treeview)",
|
|
5
5
|
"author": "Sprout Social, Inc.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -17,11 +17,12 @@
|
|
|
17
17
|
"types": "./dist/index.d.ts",
|
|
18
18
|
"import": "./dist/esm/index.js",
|
|
19
19
|
"require": "./dist/index.js"
|
|
20
|
-
}
|
|
20
|
+
},
|
|
21
|
+
"./dist/tree.css": "./dist/tree.css"
|
|
21
22
|
},
|
|
22
23
|
"scripts": {
|
|
23
|
-
"build": "tsup --dts",
|
|
24
|
-
"build:debug": "tsup --dts --metafile",
|
|
24
|
+
"build": "tsup --dts && cp src/tree.css dist/tree.css",
|
|
25
|
+
"build:debug": "tsup --dts --metafile && cp src/tree.css dist/tree.css",
|
|
25
26
|
"dev": "tsup --watch --dts",
|
|
26
27
|
"clean": "rm -rf .turbo dist",
|
|
27
28
|
"clean:modules": "rm -rf node_modules",
|
|
@@ -30,17 +31,17 @@
|
|
|
30
31
|
"test:watch": "jest --watch --coverage=false"
|
|
31
32
|
},
|
|
32
33
|
"dependencies": {
|
|
33
|
-
"@sproutsocial/seeds-react-icon": "^2.4.
|
|
34
|
-
"@sproutsocial/seeds-react-mixins": "^4.3.
|
|
35
|
-
"@sproutsocial/seeds-react-popout": "^2.5.
|
|
34
|
+
"@sproutsocial/seeds-react-icon": "^2.4.2",
|
|
35
|
+
"@sproutsocial/seeds-react-mixins": "^4.3.9",
|
|
36
|
+
"@sproutsocial/seeds-react-popout": "^2.5.11",
|
|
36
37
|
"@sproutsocial/seeds-react-system-props": "^3.1.1",
|
|
37
|
-
"@sproutsocial/seeds-react-theme": "^4.
|
|
38
|
+
"@sproutsocial/seeds-react-theme": "^4.2.0",
|
|
38
39
|
"@sproutsocial/seeds-react-utilities": "^4.3.0"
|
|
39
40
|
},
|
|
40
41
|
"devDependencies": {
|
|
41
42
|
"@sproutsocial/eslint-config-seeds": "*",
|
|
42
|
-
"@sproutsocial/seeds-react-button": "^2.2.
|
|
43
|
-
"@sproutsocial/seeds-react-popout": "^2.5.
|
|
43
|
+
"@sproutsocial/seeds-react-button": "^2.2.3",
|
|
44
|
+
"@sproutsocial/seeds-react-popout": "^2.5.11",
|
|
44
45
|
"@sproutsocial/seeds-react-testing-library": "*",
|
|
45
46
|
"@sproutsocial/seeds-testing": "*",
|
|
46
47
|
"@sproutsocial/seeds-tsconfig": "*",
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
-
import styled, { css } from "styled-components";
|
|
3
2
|
import { Icon } from "@sproutsocial/seeds-react-icon";
|
|
4
|
-
import { focusRing } from "@sproutsocial/seeds-react-mixins";
|
|
5
3
|
import { Popout } from "@sproutsocial/seeds-react-popout/v2";
|
|
6
4
|
import { TreeCombobox } from "../TreeCombobox";
|
|
5
|
+
import { cn } from "../cn";
|
|
7
6
|
import { flattenTreeItems } from "./flattenTree";
|
|
8
7
|
import type {
|
|
9
8
|
TreeItemData,
|
|
@@ -12,113 +11,14 @@ import type {
|
|
|
12
11
|
TreeSelectionMode,
|
|
13
12
|
} from "./types";
|
|
14
13
|
|
|
15
|
-
const invalidStyles = css`
|
|
16
|
-
border-color: ${({ theme }) => theme.colors.form.border.error};
|
|
17
|
-
`;
|
|
18
|
-
|
|
19
|
-
const Trigger = styled.button<{ $isInvalid?: boolean; $fullWidth: boolean }>`
|
|
20
|
-
display: flex;
|
|
21
|
-
align-items: center;
|
|
22
|
-
justify-content: space-between;
|
|
23
|
-
gap: ${({ theme }) => theme.space[300]};
|
|
24
|
-
padding: ${({ theme }) => theme.space[200]} ${({ theme }) => theme.space[350]};
|
|
25
|
-
min-width: 160px;
|
|
26
|
-
width: ${({ $fullWidth }) => ($fullWidth ? "100%" : "auto")};
|
|
27
|
-
border-radius: ${({ theme }) => theme.radii[500]};
|
|
28
|
-
border: 1px solid ${({ theme }) => theme.colors.form.border.base};
|
|
29
|
-
background: ${({ theme }) => theme.colors.form.background.base};
|
|
30
|
-
font-family: ${({ theme }) => theme.fontFamily};
|
|
31
|
-
font-size: ${({ theme }) => theme.typography[200].fontSize};
|
|
32
|
-
line-height: ${({ theme }) => theme.typography[200].lineHeight};
|
|
33
|
-
color: ${({ theme }) => theme.colors.text.body};
|
|
34
|
-
text-align: left;
|
|
35
|
-
cursor: default;
|
|
36
|
-
outline: none;
|
|
37
|
-
user-select: none;
|
|
38
|
-
transition: border-color ${({ theme }) => theme.duration.fast}
|
|
39
|
-
${({ theme }) => theme.easing.ease_in},
|
|
40
|
-
box-shadow ${({ theme }) => theme.duration.fast}
|
|
41
|
-
${({ theme }) => theme.easing.ease_in};
|
|
42
|
-
|
|
43
|
-
&:focus-visible {
|
|
44
|
-
${focusRing}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
&[aria-disabled="true"],
|
|
48
|
-
&:disabled {
|
|
49
|
-
opacity: 0.4;
|
|
50
|
-
cursor: not-allowed;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
${({ $isInvalid }) => $isInvalid && invalidStyles}
|
|
54
|
-
`;
|
|
55
|
-
|
|
56
|
-
const TriggerLabel = styled.span`
|
|
57
|
-
flex: 1;
|
|
58
|
-
min-width: 0;
|
|
59
|
-
overflow: hidden;
|
|
60
|
-
text-overflow: ellipsis;
|
|
61
|
-
white-space: nowrap;
|
|
62
|
-
`;
|
|
63
|
-
|
|
64
|
-
const PlaceholderText = styled.span`
|
|
65
|
-
color: ${({ theme }) => theme.colors.text.subtext};
|
|
66
|
-
`;
|
|
67
|
-
|
|
68
|
-
const Chevron = styled.span<{ $open: boolean }>`
|
|
69
|
-
display: inline-flex;
|
|
70
|
-
align-items: center;
|
|
71
|
-
color: ${({ theme }) => theme.colors.icon.base};
|
|
72
|
-
transition: transform ${({ theme }) => theme.duration.fast}
|
|
73
|
-
${({ theme }) => theme.easing.ease_in};
|
|
74
|
-
${({ $open }) =>
|
|
75
|
-
$open &&
|
|
76
|
-
css`
|
|
77
|
-
transform: rotate(-180deg);
|
|
78
|
-
`}
|
|
79
|
-
`;
|
|
80
|
-
|
|
81
|
-
/*
|
|
82
|
-
* Visually-hidden input that mirrors the combobox's selected value into a
|
|
83
|
-
* form-submittable element. Matches Base UI's pattern (used by v3
|
|
84
|
-
* SearchableSelect): the input stays in the DOM so HTML forms pick up the
|
|
85
|
-
* value, but it's removed from view and from the accessibility tree.
|
|
86
|
-
*/
|
|
87
|
-
const HiddenInput = styled.input`
|
|
88
|
-
clip-path: inset(50%);
|
|
89
|
-
overflow: hidden;
|
|
90
|
-
white-space: nowrap;
|
|
91
|
-
border: 0;
|
|
92
|
-
padding: 0;
|
|
93
|
-
width: 1px;
|
|
94
|
-
height: 1px;
|
|
95
|
-
margin: -1px;
|
|
96
|
-
position: fixed;
|
|
97
|
-
top: 0;
|
|
98
|
-
left: 0;
|
|
99
|
-
`;
|
|
100
|
-
|
|
101
|
-
const PopoutContent = styled.div<{ $width?: number | string }>`
|
|
102
|
-
display: flex;
|
|
103
|
-
flex-direction: column;
|
|
104
|
-
/* Width sizes to content within bounds: at least the trigger width, never */
|
|
105
|
-
/* wider than the available viewport space. Shallow popups stay flush with */
|
|
106
|
-
/* the trigger; deeply nested trees expand horizontally as needed. */
|
|
107
|
-
min-width: var(--radix-popover-trigger-width);
|
|
108
|
-
max-width: var(--radix-popper-available-width);
|
|
109
|
-
/* Height caps at 400px (or available viewport) so a popup with many */
|
|
110
|
-
/* expanded branches scrolls vertically instead of growing off-screen. */
|
|
111
|
-
max-height: min(var(--radix-popper-available-height), 400px);
|
|
112
|
-
width: ${({ $width }) =>
|
|
113
|
-
$width === undefined
|
|
114
|
-
? "max-content"
|
|
115
|
-
: typeof $width === "number"
|
|
116
|
-
? `${$width}px`
|
|
117
|
-
: $width};
|
|
118
|
-
`;
|
|
119
|
-
|
|
120
14
|
const MAX_INLINE_LABELS = 3;
|
|
121
15
|
|
|
16
|
+
/** Resolve the popout width prop to a CSS length for `--tree-select-width`. */
|
|
17
|
+
function popoutWidth(width?: number | string): string | undefined {
|
|
18
|
+
if (width === undefined) return undefined;
|
|
19
|
+
return typeof width === "number" ? `${width}px` : width;
|
|
20
|
+
}
|
|
21
|
+
|
|
122
22
|
export type RenderTrigger = (selected: TreeItemData[]) => React.ReactNode;
|
|
123
23
|
|
|
124
24
|
type TreeSearchableSelectBaseProps = {
|
|
@@ -189,7 +89,9 @@ function defaultRenderTrigger(
|
|
|
189
89
|
placeholder: string
|
|
190
90
|
): React.ReactNode {
|
|
191
91
|
if (selected.length === 0) {
|
|
192
|
-
return
|
|
92
|
+
return (
|
|
93
|
+
<span className="seeds-tree-select-placeholder">{placeholder}</span>
|
|
94
|
+
);
|
|
193
95
|
}
|
|
194
96
|
if (selected.length <= MAX_INLINE_LABELS) {
|
|
195
97
|
return selected.map((s) => s.label).join(", ");
|
|
@@ -284,7 +186,8 @@ export function TreeSearchableSelectBase(props: TreeSearchableSelectBaseProps) {
|
|
|
284
186
|
|
|
285
187
|
return (
|
|
286
188
|
<>
|
|
287
|
-
<
|
|
189
|
+
<input
|
|
190
|
+
className="seeds-tree-select-hidden-input"
|
|
288
191
|
type="text"
|
|
289
192
|
id={primaryInputId}
|
|
290
193
|
name={primaryInputName}
|
|
@@ -327,7 +230,15 @@ export function TreeSearchableSelectBase(props: TreeSearchableSelectBaseProps) {
|
|
|
327
230
|
}
|
|
328
231
|
}}
|
|
329
232
|
content={
|
|
330
|
-
<
|
|
233
|
+
<div
|
|
234
|
+
className="seeds-tree-select-popout"
|
|
235
|
+
id={popupId}
|
|
236
|
+
style={
|
|
237
|
+
{
|
|
238
|
+
"--tree-select-width": popoutWidth(contentWidth),
|
|
239
|
+
} as React.CSSProperties
|
|
240
|
+
}
|
|
241
|
+
>
|
|
331
242
|
<TreeCombobox
|
|
332
243
|
/*
|
|
333
244
|
* The wrapper's `aria-label` / `aria-labelledby` already names the
|
|
@@ -352,15 +263,16 @@ export function TreeSearchableSelectBase(props: TreeSearchableSelectBaseProps) {
|
|
|
352
263
|
expanded={expanded}
|
|
353
264
|
onExpandedChange={onExpandedChange}
|
|
354
265
|
/>
|
|
355
|
-
</
|
|
266
|
+
</div>
|
|
356
267
|
}
|
|
357
268
|
>
|
|
358
|
-
<
|
|
269
|
+
<button
|
|
270
|
+
className={cn("seeds-tree-select-trigger", className, {
|
|
271
|
+
"full-width": fullWidth,
|
|
272
|
+
invalid: !!isInvalid,
|
|
273
|
+
})}
|
|
359
274
|
type="button"
|
|
360
275
|
id={id}
|
|
361
|
-
className={className}
|
|
362
|
-
$isInvalid={isInvalid}
|
|
363
|
-
$fullWidth={fullWidth}
|
|
364
276
|
disabled={disabled}
|
|
365
277
|
role="combobox"
|
|
366
278
|
aria-haspopup="dialog"
|
|
@@ -372,11 +284,16 @@ export function TreeSearchableSelectBase(props: TreeSearchableSelectBaseProps) {
|
|
|
372
284
|
aria-invalid={isInvalid || undefined}
|
|
373
285
|
aria-required={required || undefined}
|
|
374
286
|
>
|
|
375
|
-
<
|
|
376
|
-
|
|
287
|
+
<span className="seeds-tree-select-trigger-label">
|
|
288
|
+
{triggerContent}
|
|
289
|
+
</span>
|
|
290
|
+
<span
|
|
291
|
+
className={cn("seeds-tree-select-chevron", { open })}
|
|
292
|
+
aria-hidden
|
|
293
|
+
>
|
|
377
294
|
<Icon name="chevron-down-outline" size="small" />
|
|
378
|
-
</
|
|
379
|
-
</
|
|
295
|
+
</span>
|
|
296
|
+
</button>
|
|
380
297
|
</Popout>
|
|
381
298
|
</>
|
|
382
299
|
);
|
package/src/Tree.stories.tsx
CHANGED
|
@@ -229,3 +229,29 @@ export const DisabledItems: Story = {
|
|
|
229
229
|
</Wrapper>
|
|
230
230
|
),
|
|
231
231
|
};
|
|
232
|
+
|
|
233
|
+
export const TailwindOverrides: Story = {
|
|
234
|
+
name: "Tailwind utility overrides",
|
|
235
|
+
render: () => (
|
|
236
|
+
<Wrapper>
|
|
237
|
+
{/*
|
|
238
|
+
The CSS-class path lands a bare `className` directly on the
|
|
239
|
+
`.seeds-tree` <ul>. `mx-200`, `p-300`, and `inline-flex` come from the
|
|
240
|
+
Tailwind `utilities` layer and only win over tree.css's `margin: 0`,
|
|
241
|
+
`padding: 0`, and default `display` because tree.css's rules live in
|
|
242
|
+
`@layer components`. The border makes the applied margin/padding
|
|
243
|
+
visible, proving the utilities take effect.
|
|
244
|
+
*/}
|
|
245
|
+
<Tree
|
|
246
|
+
aria-label="Tree with Tailwind utility overrides"
|
|
247
|
+
defaultExpanded={["src"]}
|
|
248
|
+
className="mx-200 p-300 inline-flex border border-container-border-base rounded-400"
|
|
249
|
+
>
|
|
250
|
+
<TreeItem id="src" label="src">
|
|
251
|
+
<TreeItem id="Button.tsx" label="Button.tsx" />
|
|
252
|
+
<TreeItem id="Tree.tsx" label="Tree.tsx" />
|
|
253
|
+
</TreeItem>
|
|
254
|
+
</Tree>
|
|
255
|
+
</Wrapper>
|
|
256
|
+
),
|
|
257
|
+
};
|
package/src/Tree.tsx
CHANGED
|
@@ -10,7 +10,7 @@ import type {
|
|
|
10
10
|
TreeSelectionIndicator,
|
|
11
11
|
TreeSelectionMode,
|
|
12
12
|
} from "./Common/types";
|
|
13
|
-
import {
|
|
13
|
+
import { cn } from "./cn";
|
|
14
14
|
|
|
15
15
|
export type TreeProps = {
|
|
16
16
|
children: React.ReactNode;
|
|
@@ -164,17 +164,17 @@ export const Tree = React.forwardRef<HTMLUListElement, TreeProps>(function Tree(
|
|
|
164
164
|
return (
|
|
165
165
|
<TreeContext.Provider value={ctxValue}>
|
|
166
166
|
<TreeItemContext.Provider value={{ level: 1 }}>
|
|
167
|
-
<
|
|
167
|
+
<ul
|
|
168
168
|
ref={innerRef}
|
|
169
169
|
role="tree"
|
|
170
170
|
id={id}
|
|
171
|
-
className={className}
|
|
171
|
+
className={cn("seeds-tree", className)}
|
|
172
172
|
aria-label={ariaLabel}
|
|
173
173
|
aria-labelledby={ariaLabelledBy}
|
|
174
174
|
aria-multiselectable={selectionMode === "multiple" ? true : undefined}
|
|
175
175
|
>
|
|
176
176
|
{children}
|
|
177
|
-
</
|
|
177
|
+
</ul>
|
|
178
178
|
</TreeItemContext.Provider>
|
|
179
179
|
</TreeContext.Provider>
|
|
180
180
|
);
|