luxen-ui 0.9.0 → 0.9.2
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/cdn/custom-elements.json +148 -123
- package/cdn/elements/dropdown-item/dropdown-item.js +1 -1
- package/cdn/elements/dropdown-item/dropdown-item.js.map +1 -1
- package/cdn/elements/prose-editor/prose-editor.d.ts +24 -2
- package/cdn/elements/prose-editor/prose-editor.d.ts.map +1 -1
- package/cdn/elements/prose-editor/prose-editor.js +40 -39
- package/cdn/elements/prose-editor/prose-editor.js.map +1 -1
- package/cdn/elements/tree/tree.d.ts +11 -1
- package/cdn/elements/tree/tree.d.ts.map +1 -1
- package/cdn/elements/tree/tree.js +1 -3
- package/cdn/elements/tree/tree.js.map +1 -1
- package/cdn/elements/tree-item/tree-item.d.ts +17 -1
- package/cdn/elements/tree-item/tree-item.d.ts.map +1 -1
- package/cdn/elements/tree-item/tree-item.js +2 -1
- package/cdn/elements/tree-item/tree-item.js.map +1 -1
- package/cdn/standalone.css +4 -0
- package/cdn/standalone.js +99 -50
- package/cdn/standalone.js.map +1 -1
- package/cdn/styles/elements/button.css +4 -0
- package/dist/css/elements/button.css +4 -0
- package/dist/custom-elements.json +148 -123
- package/dist/elements/dropdown-item/dropdown-item.css +1 -0
- package/dist/elements/prose-editor/prose-editor.d.ts +24 -2
- package/dist/elements/prose-editor/prose-editor.d.ts.map +1 -1
- package/dist/elements/prose-editor/prose-editor.js +81 -48
- package/dist/elements/tree/tree.css +1 -1
- package/dist/elements/tree/tree.d.ts +11 -1
- package/dist/elements/tree/tree.d.ts.map +1 -1
- package/dist/elements/tree/tree.js +37 -11
- package/dist/elements/tree-item/tree-item.css +5 -1
- package/dist/elements/tree-item/tree-item.d.ts +17 -1
- package/dist/elements/tree-item/tree-item.d.ts.map +1 -1
- package/dist/elements/tree-item/tree-item.js +51 -10
- package/dist/metadata/index.json +22 -3
- package/dist/metadata/tree-item.json +20 -1
- package/dist/metadata/tree.json +1 -1
- package/dist/templates/elements/tree.md +18 -0
- package/package.json +4 -2
- package/postcss-plugin-prefix.js +43 -1
|
@@ -5,6 +5,9 @@ export type TreeSelection = 'single' | 'multiple' | 'leaf' | 'none';
|
|
|
5
5
|
/**
|
|
6
6
|
* A hierarchical tree view composed of `<l-tree-item>` children.
|
|
7
7
|
*
|
|
8
|
+
* The host carries `role="tree"`, so give it an accessible name with
|
|
9
|
+
* `aria-label` or `aria-labelledby` (e.g. `<l-tree aria-label="Files">`).
|
|
10
|
+
*
|
|
8
11
|
* @slot - One or more `l-tree-item` elements.
|
|
9
12
|
*
|
|
10
13
|
* @csspart base - The root tree container.
|
|
@@ -24,6 +27,7 @@ export type TreeSelection = 'single' | 'multiple' | 'leaf' | 'none';
|
|
|
24
27
|
*/
|
|
25
28
|
export declare class Tree extends LuxenElement {
|
|
26
29
|
static styles: import("lit").CSSResult[];
|
|
30
|
+
private _internals;
|
|
27
31
|
private _mutationObserver?;
|
|
28
32
|
private _lastFocusedItem;
|
|
29
33
|
/**
|
|
@@ -54,7 +58,13 @@ export declare class Tree extends LuxenElement {
|
|
|
54
58
|
/** Collapses every item. */
|
|
55
59
|
collapseAll(): void;
|
|
56
60
|
private _syncAll;
|
|
57
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Sync depth, checkbox visibility and ARIA position for a sibling group, then
|
|
63
|
+
* recurse. `aria-level`/`aria-setsize`/`aria-posinset` let screen readers
|
|
64
|
+
* announce "level N, M of K" — valuable here because `lazy` items mean the
|
|
65
|
+
* full set isn't always in the DOM (see WAI-ARIA Tree View pattern).
|
|
66
|
+
*/
|
|
67
|
+
private _syncLevel;
|
|
58
68
|
private _canShowCheckboxOn;
|
|
59
69
|
private _rootItems;
|
|
60
70
|
private _ensureTabStop;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tree.d.ts","sourceRoot":"","sources":["../../../src/html/elements/tree/tree.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmB,KAAK,cAAc,EAAE,MAAM,KAAK,CAAC;AAE3D,OAAO,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAE7D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AAM1D,MAAM,MAAM,aAAa,GAAG,QAAQ,GAAG,UAAU,GAAG,MAAM,GAAG,MAAM,CAAC;AAEpE
|
|
1
|
+
{"version":3,"file":"tree.d.ts","sourceRoot":"","sources":["../../../src/html/elements/tree/tree.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmB,KAAK,cAAc,EAAE,MAAM,KAAK,CAAC;AAE3D,OAAO,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAE7D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AAM1D,MAAM,MAAM,aAAa,GAAG,QAAQ,GAAG,UAAU,GAAG,MAAM,GAAG,MAAM,CAAC;AAEpE;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,qBAAa,IAAK,SAAQ,YAAY;IACpC,OAAgB,MAAM,4BAAwB;IAE9C,OAAO,CAAC,UAAU,CAA0B;IAC5C,OAAO,CAAC,iBAAiB,CAAC,CAAmB;IAC7C,OAAO,CAAC,gBAAgB,CAAyB;IAEjD;;;;;;OAMG;IAEH,QAAQ,CAAC,SAAS,EAAE,aAAa,CAAY;IAE7C;;;;OAIG;IAEH,QAAQ,CAAC,WAAW,UAAS;IAEpB,iBAAiB;IAgBjB,oBAAoB;IAMpB,OAAO,CAAC,OAAO,EAAE,cAAc,CAAC,IAAI,CAAC;IAe9C,yEAAyE;IACzE,WAAW,CAAC,EAAE,eAAsB,EAAE;;KAAK,GAAG,QAAQ,EAAE;IAOxD,wCAAwC;IACxC,YAAY,IAAI,QAAQ,EAAE;IAI1B,4CAA4C;IAC5C,SAAS;IAMT,4BAA4B;IAC5B,WAAW;IAQX,OAAO,CAAC,QAAQ;IAoBhB;;;;;OAKG;IACH,OAAO,CAAC,UAAU;IAUlB,OAAO,CAAC,kBAAkB;IAO1B,OAAO,CAAC,UAAU;IAKlB,OAAO,CAAC,cAAc;IAUtB,2DAA2D;IAC3D,OAAO,CAAC,aAAa;IAcrB,OAAO,CAAC,aAAa,CAGnB;IAEF,OAAO,CAAC,kBAAkB;IAuB1B,OAAO,CAAC,mBAAmB;IAQ3B,OAAO,CAAC,WAAW;IAcnB,OAAO,CAAC,oBAAoB;IAS5B,0EAA0E;IAC1E,OAAO,CAAC,mBAAmB;IA6B3B,OAAO,CAAC,oBAAoB;IAM5B,OAAO,CAAC,QAAQ,CAqDd;IAEF,OAAO,CAAC,cAAc;IAWtB,OAAO,CAAC,UAAU;IAQlB,OAAO,CAAC,UAAU,CAMhB;IAEF,OAAO,CAAC,UAAU,CAqEhB;IAEO,MAAM;CAahB"}
|
|
@@ -26,6 +26,9 @@ const styles = unsafeCSS(rawStyles);
|
|
|
26
26
|
/**
|
|
27
27
|
* A hierarchical tree view composed of `<l-tree-item>` children.
|
|
28
28
|
*
|
|
29
|
+
* The host carries `role="tree"`, so give it an accessible name with
|
|
30
|
+
* `aria-label` or `aria-labelledby` (e.g. `<l-tree aria-label="Files">`).
|
|
31
|
+
*
|
|
29
32
|
* @slot - One or more `l-tree-item` elements.
|
|
30
33
|
*
|
|
31
34
|
* @csspart base - The root tree container.
|
|
@@ -46,6 +49,7 @@ const styles = unsafeCSS(rawStyles);
|
|
|
46
49
|
export class Tree extends LuxenElement {
|
|
47
50
|
constructor() {
|
|
48
51
|
super(...arguments);
|
|
52
|
+
this._internals = this.attachInternals();
|
|
49
53
|
this._lastFocusedItem = null;
|
|
50
54
|
_Tree_selection_accessor_storage.set(this, 'single');
|
|
51
55
|
_Tree_independent_accessor_storage.set(this, false);
|
|
@@ -214,6 +218,13 @@ export class Tree extends LuxenElement {
|
|
|
214
218
|
set independent(value) { __classPrivateFieldSet(this, _Tree_independent_accessor_storage, value, "f"); }
|
|
215
219
|
connectedCallback() {
|
|
216
220
|
super.connectedCallback();
|
|
221
|
+
this._internals.role = 'tree';
|
|
222
|
+
// Mirror the role to a DOM attribute too. The ElementInternals role alone is
|
|
223
|
+
// not `[role]`-selectable (CSS, querySelector, Cypress/Playwright CSS), which
|
|
224
|
+
// silently breaks consumers migrating from libraries that expose an attribute
|
|
225
|
+
// role. Respect an author-provided role if one is already set.
|
|
226
|
+
if (!this.hasAttribute('role'))
|
|
227
|
+
this.setAttribute('role', 'tree');
|
|
217
228
|
this._mutationObserver = new MutationObserver(() => this._syncAll());
|
|
218
229
|
this._mutationObserver.observe(this, { childList: true, subtree: true });
|
|
219
230
|
this.addEventListener('l-tree-item-toggle', this._onItemToggle);
|
|
@@ -226,6 +237,13 @@ export class Tree extends LuxenElement {
|
|
|
226
237
|
this.removeEventListener('l-tree-item-toggle', this._onItemToggle);
|
|
227
238
|
}
|
|
228
239
|
updated(changed) {
|
|
240
|
+
if (changed.has('selection')) {
|
|
241
|
+
// Mirror to ElementInternals (a11y tree) and a content attribute, so
|
|
242
|
+
// `[aria-multiselectable]` selectors keep matching — see tree-item `_aria`.
|
|
243
|
+
const multiselectable = this.selection === 'multiple' ? 'true' : 'false';
|
|
244
|
+
this._internals.ariaMultiSelectable = multiselectable;
|
|
245
|
+
this.setAttribute('aria-multiselectable', multiselectable);
|
|
246
|
+
}
|
|
229
247
|
if (changed.has('selection') || changed.has('independent')) {
|
|
230
248
|
this._syncAll();
|
|
231
249
|
}
|
|
@@ -267,19 +285,25 @@ export class Tree extends LuxenElement {
|
|
|
267
285
|
return;
|
|
268
286
|
}
|
|
269
287
|
const showCheckbox = this.selection === 'multiple';
|
|
270
|
-
|
|
271
|
-
this._syncSubtree(root, 0, showCheckbox);
|
|
272
|
-
}
|
|
288
|
+
this._syncLevel(roots, 0, showCheckbox);
|
|
273
289
|
this._updateBranchStates();
|
|
274
290
|
// Ensure at least one item is tabbable.
|
|
275
291
|
this._ensureTabStop();
|
|
276
292
|
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
293
|
+
/**
|
|
294
|
+
* Sync depth, checkbox visibility and ARIA position for a sibling group, then
|
|
295
|
+
* recurse. `aria-level`/`aria-setsize`/`aria-posinset` let screen readers
|
|
296
|
+
* announce "level N, M of K" — valuable here because `lazy` items mean the
|
|
297
|
+
* full set isn't always in the DOM (see WAI-ARIA Tree View pattern).
|
|
298
|
+
*/
|
|
299
|
+
_syncLevel(items, depth, showCheckbox) {
|
|
300
|
+
const setSize = items.length;
|
|
301
|
+
items.forEach((item, index) => {
|
|
302
|
+
item.depth = depth;
|
|
303
|
+
item.showCheckbox = showCheckbox && this._canShowCheckboxOn(item);
|
|
304
|
+
item.setPosition(depth + 1, index + 1, setSize);
|
|
305
|
+
this._syncLevel(item.getChildrenItems(), depth + 1, showCheckbox);
|
|
306
|
+
});
|
|
283
307
|
}
|
|
284
308
|
_canShowCheckboxOn(_item) {
|
|
285
309
|
if (this.selection !== 'multiple')
|
|
@@ -322,6 +346,10 @@ export class Tree extends LuxenElement {
|
|
|
322
346
|
switch (this.selection) {
|
|
323
347
|
case 'single':
|
|
324
348
|
this._setSingleSelection(item);
|
|
349
|
+
// Mirror the row-click behaviour: activating a branch also toggles it,
|
|
350
|
+
// so keyboard users expand lazy branches (and trigger their fetch) too.
|
|
351
|
+
if (!item.isLeaf())
|
|
352
|
+
item.toggle();
|
|
325
353
|
break;
|
|
326
354
|
case 'leaf':
|
|
327
355
|
if (item.isLeaf())
|
|
@@ -421,8 +449,6 @@ export class Tree extends LuxenElement {
|
|
|
421
449
|
<div
|
|
422
450
|
class="tree"
|
|
423
451
|
part="base"
|
|
424
|
-
role="tree"
|
|
425
|
-
aria-multiselectable=${this.selection === 'multiple' ? 'true' : 'false'}
|
|
426
452
|
@click=${this._onClick}
|
|
427
453
|
@keydown=${this._onKeyDown}
|
|
428
454
|
@focusin=${this._onFocusIn}
|
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
color: var(--l-color-text-primary, CanvasText);
|
|
4
4
|
font-size: 0.875rem;
|
|
5
5
|
line-height: 1.5;
|
|
6
|
+
/* The host is the roving-tabindex focus target, but the visible ring is drawn
|
|
7
|
+
on the inner `.item` row. Suppress the host's UA outline so it doesn't paint
|
|
8
|
+
a second ring around the whole subtree (row + children). */
|
|
9
|
+
outline: none;
|
|
6
10
|
}
|
|
7
11
|
|
|
8
12
|
:host([disabled]) {
|
|
@@ -31,7 +35,7 @@
|
|
|
31
35
|
|
|
32
36
|
.item:focus-visible,
|
|
33
37
|
:host(:focus-visible) .item {
|
|
34
|
-
outline: 2px solid var(--l-focus-ring
|
|
38
|
+
outline: 2px solid var(--l-focus-ring);
|
|
35
39
|
outline-offset: 1px;
|
|
36
40
|
}
|
|
37
41
|
|
|
@@ -52,6 +52,13 @@ export declare class TreeItem extends LuxenElement {
|
|
|
52
52
|
set depth(value: number);
|
|
53
53
|
get depth(): number;
|
|
54
54
|
private _depth;
|
|
55
|
+
/**
|
|
56
|
+
* Set by `<l-tree>`: ARIA position within the tree. `level` is 1-based depth,
|
|
57
|
+
* `posInSet`/`setSize` describe the item's rank among its siblings. These let
|
|
58
|
+
* screen readers announce "level 2, 3 of 5" even when `lazy` children keep the
|
|
59
|
+
* full set out of the DOM.
|
|
60
|
+
*/
|
|
61
|
+
setPosition(level: number, posInSet: number, setSize: number): void;
|
|
55
62
|
/** Whether this item has nested tree-item children. */
|
|
56
63
|
get hasChildren(): boolean;
|
|
57
64
|
private _hasChildren;
|
|
@@ -66,9 +73,18 @@ export declare class TreeItem extends LuxenElement {
|
|
|
66
73
|
connectedCallback(): void;
|
|
67
74
|
disconnectedCallback(): void;
|
|
68
75
|
updated(changed: PropertyValues<this>): void;
|
|
76
|
+
/** Leaf items omit `aria-expanded` entirely; branches reflect their state. */
|
|
77
|
+
private _reflectExpanded;
|
|
78
|
+
/**
|
|
79
|
+
* Write an ARIA state to BOTH ElementInternals (the semantic source, in the
|
|
80
|
+
* accessibility tree) and a content attribute (so `[aria-*]` CSS / query / test
|
|
81
|
+
* selectors keep matching — same belt-and-suspenders as the mirrored `role`).
|
|
82
|
+
* A `null` value clears both.
|
|
83
|
+
*/
|
|
84
|
+
private _aria;
|
|
69
85
|
private _setState;
|
|
70
86
|
private _syncChildren;
|
|
71
|
-
/** Toggle expand state.
|
|
87
|
+
/** Toggle expand state. Opening a `lazy` item emits `lazy-load` (via `updated`). */
|
|
72
88
|
toggle(): void;
|
|
73
89
|
private _onCheckboxChange;
|
|
74
90
|
render(): import("lit").TemplateResult<1>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tree-item.d.ts","sourceRoot":"","sources":["../../../src/html/elements/tree-item/tree-item.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmB,KAAK,cAAc,EAAE,MAAM,KAAK,CAAC;AAE3D,OAAO,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAQ7D;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,qBAAa,QAAS,SAAQ,YAAY;IACxC,OAAgB,MAAM,4BAA4C;IAElE,OAAO,CAAC,UAAU,CAA0B;IAC5C,OAAO,CAAC,cAAc,CAAC,CAAmB;IAE1C,oCAAoC;IAEpC,QAAQ,CAAC,QAAQ,UAAS;IAE1B,oCAAoC;IAEpC,QAAQ,CAAC,QAAQ,UAAS;IAE1B,yEAAyE;IAEzE,QAAQ,CAAC,aAAa,UAAS;IAE/B,oCAAoC;IAEpC,QAAQ,CAAC,QAAQ,UAAS;IAE1B,8EAA8E;IAE9E,QAAQ,CAAC,IAAI,UAAS;IAEtB,+DAA+D;IAE/D,QAAQ,CAAC,OAAO,UAAS;IAEzB,sDAAsD;IACtD,IAAI,YAAY,CAAC,KAAK,EAAE,OAAO,EAG9B;IACD,IAAI,YAAY,IAAI,OAAO,CAE1B;IACD,OAAO,CAAC,aAAa,CAAS;IAE9B,mEAAmE;IACnE,IAAI,KAAK,CAAC,KAAK,EAAE,MAAM,EAGtB;IACD,IAAI,KAAK,IAAI,MAAM,CAElB;IACD,OAAO,CAAC,MAAM,CAAK;IAEnB,uDAAuD;IACvD,IAAI,WAAW,IAAI,OAAO,CAEzB;IACD,OAAO,CAAC,YAAY,CAAS;IAE7B,0EAA0E;IAC1E,gBAAgB,CAAC,EAAE,eAAsB,EAAE;;KAAK,GAAG,QAAQ,EAAE;IAO7D,4DAA4D;IAC5D,MAAM,IAAI,OAAO;IAIjB,2CAA2C;IAC3C,YAAY,IAAI,MAAM;IAUb,iBAAiB;
|
|
1
|
+
{"version":3,"file":"tree-item.d.ts","sourceRoot":"","sources":["../../../src/html/elements/tree-item/tree-item.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmB,KAAK,cAAc,EAAE,MAAM,KAAK,CAAC;AAE3D,OAAO,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAQ7D;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,qBAAa,QAAS,SAAQ,YAAY;IACxC,OAAgB,MAAM,4BAA4C;IAElE,OAAO,CAAC,UAAU,CAA0B;IAC5C,OAAO,CAAC,cAAc,CAAC,CAAmB;IAE1C,oCAAoC;IAEpC,QAAQ,CAAC,QAAQ,UAAS;IAE1B,oCAAoC;IAEpC,QAAQ,CAAC,QAAQ,UAAS;IAE1B,yEAAyE;IAEzE,QAAQ,CAAC,aAAa,UAAS;IAE/B,oCAAoC;IAEpC,QAAQ,CAAC,QAAQ,UAAS;IAE1B,8EAA8E;IAE9E,QAAQ,CAAC,IAAI,UAAS;IAEtB,+DAA+D;IAE/D,QAAQ,CAAC,OAAO,UAAS;IAEzB,sDAAsD;IACtD,IAAI,YAAY,CAAC,KAAK,EAAE,OAAO,EAG9B;IACD,IAAI,YAAY,IAAI,OAAO,CAE1B;IACD,OAAO,CAAC,aAAa,CAAS;IAE9B,mEAAmE;IACnE,IAAI,KAAK,CAAC,KAAK,EAAE,MAAM,EAGtB;IACD,IAAI,KAAK,IAAI,MAAM,CAElB;IACD,OAAO,CAAC,MAAM,CAAK;IAEnB;;;;;OAKG;IACH,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;IAM5D,uDAAuD;IACvD,IAAI,WAAW,IAAI,OAAO,CAEzB;IACD,OAAO,CAAC,YAAY,CAAS;IAE7B,0EAA0E;IAC1E,gBAAgB,CAAC,EAAE,eAAsB,EAAE;;KAAK,GAAG,QAAQ,EAAE;IAO7D,4DAA4D;IAC5D,MAAM,IAAI,OAAO;IAIjB,2CAA2C;IAC3C,YAAY,IAAI,MAAM;IAUb,iBAAiB;IAajB,oBAAoB;IAKpB,OAAO,CAAC,OAAO,EAAE,cAAc,CAAC,IAAI,CAAC;IAwB9C,8EAA8E;IAC9E,OAAO,CAAC,gBAAgB;IAIxB;;;;;OAKG;IACH,OAAO,CAAC,KAAK;IAMb,OAAO,CAAC,SAAS;IAMjB,OAAO,CAAC,aAAa;IAmBrB,oFAAoF;IACpF,MAAM;IAMN,OAAO,CAAC,iBAAiB,CAUvB;IAEO,MAAM;CAyFhB"}
|
|
@@ -109,6 +109,17 @@ export class TreeItem extends LuxenElement {
|
|
|
109
109
|
get depth() {
|
|
110
110
|
return this._depth;
|
|
111
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* Set by `<l-tree>`: ARIA position within the tree. `level` is 1-based depth,
|
|
114
|
+
* `posInSet`/`setSize` describe the item's rank among its siblings. These let
|
|
115
|
+
* screen readers announce "level 2, 3 of 5" even when `lazy` children keep the
|
|
116
|
+
* full set out of the DOM.
|
|
117
|
+
*/
|
|
118
|
+
setPosition(level, posInSet, setSize) {
|
|
119
|
+
this._aria('ariaLevel', 'aria-level', String(level));
|
|
120
|
+
this._aria('ariaPosInSet', 'aria-posinset', String(posInSet));
|
|
121
|
+
this._aria('ariaSetSize', 'aria-setsize', String(setSize));
|
|
122
|
+
}
|
|
112
123
|
/** Whether this item has nested tree-item children. */
|
|
113
124
|
get hasChildren() {
|
|
114
125
|
return this._hasChildren;
|
|
@@ -136,6 +147,12 @@ export class TreeItem extends LuxenElement {
|
|
|
136
147
|
connectedCallback() {
|
|
137
148
|
super.connectedCallback();
|
|
138
149
|
this._internals.role = 'treeitem';
|
|
150
|
+
// Mirror the role to a DOM attribute too, so `[role="treeitem"]` selectors
|
|
151
|
+
// (CSS, querySelector, Cypress/Playwright CSS) keep matching — the
|
|
152
|
+
// ElementInternals role alone is not attribute-selectable. ARIA states are
|
|
153
|
+
// mirrored the same way in `_aria()` (aria-expanded/selected/disabled/…).
|
|
154
|
+
if (!this.hasAttribute('role'))
|
|
155
|
+
this.setAttribute('role', 'treeitem');
|
|
139
156
|
this._childObserver = new MutationObserver(() => this._syncChildren());
|
|
140
157
|
this._childObserver.observe(this, { childList: true });
|
|
141
158
|
this._syncChildren();
|
|
@@ -146,16 +163,42 @@ export class TreeItem extends LuxenElement {
|
|
|
146
163
|
}
|
|
147
164
|
updated(changed) {
|
|
148
165
|
if (changed.has('expanded')) {
|
|
149
|
-
this.
|
|
166
|
+
this._reflectExpanded();
|
|
167
|
+
// Emit lazy-load for ANY transition to expanded (keyboard, chevron,
|
|
168
|
+
// `expandAll()`…), not just `toggle()` — otherwise keyboard users expand a
|
|
169
|
+
// lazy branch with no children and no fetch. Fires while still `lazy`; the
|
|
170
|
+
// consumer appends children and clears `lazy`, so it won't re-fire.
|
|
171
|
+
if (this.expanded && this.lazy)
|
|
172
|
+
this.emit('lazy-load');
|
|
150
173
|
this.emit(this.expanded ? 'expand' : 'collapse');
|
|
151
174
|
}
|
|
152
175
|
if (changed.has('selected')) {
|
|
153
|
-
this.
|
|
176
|
+
this._aria('ariaSelected', 'aria-selected', String(this.selected));
|
|
154
177
|
}
|
|
155
178
|
if (changed.has('disabled')) {
|
|
156
|
-
this.
|
|
179
|
+
this._aria('ariaDisabled', 'aria-disabled', this.disabled ? 'true' : null);
|
|
180
|
+
}
|
|
181
|
+
if (changed.has('loading')) {
|
|
182
|
+
this._aria('ariaBusy', 'aria-busy', this.loading ? 'true' : null);
|
|
157
183
|
}
|
|
158
184
|
}
|
|
185
|
+
/** Leaf items omit `aria-expanded` entirely; branches reflect their state. */
|
|
186
|
+
_reflectExpanded() {
|
|
187
|
+
this._aria('ariaExpanded', 'aria-expanded', this.isLeaf() ? null : String(this.expanded));
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Write an ARIA state to BOTH ElementInternals (the semantic source, in the
|
|
191
|
+
* accessibility tree) and a content attribute (so `[aria-*]` CSS / query / test
|
|
192
|
+
* selectors keep matching — same belt-and-suspenders as the mirrored `role`).
|
|
193
|
+
* A `null` value clears both.
|
|
194
|
+
*/
|
|
195
|
+
_aria(key, attr, value) {
|
|
196
|
+
this._internals[key] = value;
|
|
197
|
+
if (value === null)
|
|
198
|
+
this.removeAttribute(attr);
|
|
199
|
+
else
|
|
200
|
+
this.setAttribute(attr, value);
|
|
201
|
+
}
|
|
159
202
|
_setState(name, on) {
|
|
160
203
|
if (!this._internals.states)
|
|
161
204
|
return;
|
|
@@ -181,17 +224,14 @@ export class TreeItem extends LuxenElement {
|
|
|
181
224
|
if (!this._hasChildren && !this.lazy && this.expanded) {
|
|
182
225
|
this.expanded = false;
|
|
183
226
|
}
|
|
184
|
-
this.
|
|
227
|
+
this._reflectExpanded();
|
|
185
228
|
}
|
|
186
|
-
/** Toggle expand state.
|
|
229
|
+
/** Toggle expand state. Opening a `lazy` item emits `lazy-load` (via `updated`). */
|
|
187
230
|
toggle() {
|
|
188
231
|
if (this.isLeaf() && !this.lazy)
|
|
189
232
|
return;
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
this.emit('lazy-load');
|
|
193
|
-
}
|
|
194
|
-
this.expanded = next;
|
|
233
|
+
// lazy-load is emitted centrally from `updated()` on the expand transition.
|
|
234
|
+
this.expanded = !this.expanded;
|
|
195
235
|
}
|
|
196
236
|
render() {
|
|
197
237
|
return html `
|
|
@@ -245,6 +285,7 @@ export class TreeItem extends LuxenElement {
|
|
|
245
285
|
part="checkbox"
|
|
246
286
|
type="checkbox"
|
|
247
287
|
tabindex="-1"
|
|
288
|
+
aria-hidden="true"
|
|
248
289
|
.checked=${this.selected}
|
|
249
290
|
.indeterminate=${this.indeterminate}
|
|
250
291
|
?disabled=${this.disabled}
|
package/dist/metadata/index.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.9.
|
|
2
|
+
"version": "0.9.2",
|
|
3
3
|
"elements": [
|
|
4
4
|
{
|
|
5
5
|
"name": "avatar",
|
|
@@ -4017,7 +4017,7 @@
|
|
|
4017
4017
|
"tag": "l-tree",
|
|
4018
4018
|
"nativeTag": null,
|
|
4019
4019
|
"selector": "l-tree",
|
|
4020
|
-
"summary": "A hierarchical tree view composed of `<l-tree-item>` children.",
|
|
4020
|
+
"summary": "A hierarchical tree view composed of `<l-tree-item>` children.\n\nThe host carries `role=\"tree\"`, so give it an accessible name with\n`aria-label` or `aria-labelledby` (e.g. `<l-tree aria-label=\"Files\">`).",
|
|
4021
4021
|
"status": "stable",
|
|
4022
4022
|
"appearances": [],
|
|
4023
4023
|
"import": {
|
|
@@ -4247,6 +4247,25 @@
|
|
|
4247
4247
|
}
|
|
4248
4248
|
],
|
|
4249
4249
|
"methods": [
|
|
4250
|
+
{
|
|
4251
|
+
"name": "setPosition",
|
|
4252
|
+
"params": [
|
|
4253
|
+
{
|
|
4254
|
+
"name": "level",
|
|
4255
|
+
"type": "number"
|
|
4256
|
+
},
|
|
4257
|
+
{
|
|
4258
|
+
"name": "posInSet",
|
|
4259
|
+
"type": "number"
|
|
4260
|
+
},
|
|
4261
|
+
{
|
|
4262
|
+
"name": "setSize",
|
|
4263
|
+
"type": "number"
|
|
4264
|
+
}
|
|
4265
|
+
],
|
|
4266
|
+
"returns": null,
|
|
4267
|
+
"description": "Set by `<l-tree>`: ARIA position within the tree. `level` is 1-based depth,\n`posInSet`/`setSize` describe the item's rank among its siblings. These let\nscreen readers announce \"level 2, 3 of 5\" even when `lazy` children keep the\nfull set out of the DOM."
|
|
4268
|
+
},
|
|
4250
4269
|
{
|
|
4251
4270
|
"name": "getChildrenItems",
|
|
4252
4271
|
"params": [
|
|
@@ -4274,7 +4293,7 @@
|
|
|
4274
4293
|
"name": "toggle",
|
|
4275
4294
|
"params": [],
|
|
4276
4295
|
"returns": null,
|
|
4277
|
-
"description": "Toggle expand state.
|
|
4296
|
+
"description": "Toggle expand state. Opening a `lazy` item emits `lazy-load` (via `updated`)."
|
|
4278
4297
|
}
|
|
4279
4298
|
],
|
|
4280
4299
|
"slots": [
|
|
@@ -106,6 +106,25 @@
|
|
|
106
106
|
}
|
|
107
107
|
],
|
|
108
108
|
"methods": [
|
|
109
|
+
{
|
|
110
|
+
"name": "setPosition",
|
|
111
|
+
"params": [
|
|
112
|
+
{
|
|
113
|
+
"name": "level",
|
|
114
|
+
"type": "number"
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
"name": "posInSet",
|
|
118
|
+
"type": "number"
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
"name": "setSize",
|
|
122
|
+
"type": "number"
|
|
123
|
+
}
|
|
124
|
+
],
|
|
125
|
+
"returns": null,
|
|
126
|
+
"description": "Set by `<l-tree>`: ARIA position within the tree. `level` is 1-based depth,\n`posInSet`/`setSize` describe the item's rank among its siblings. These let\nscreen readers announce \"level 2, 3 of 5\" even when `lazy` children keep the\nfull set out of the DOM."
|
|
127
|
+
},
|
|
109
128
|
{
|
|
110
129
|
"name": "getChildrenItems",
|
|
111
130
|
"params": [
|
|
@@ -133,7 +152,7 @@
|
|
|
133
152
|
"name": "toggle",
|
|
134
153
|
"params": [],
|
|
135
154
|
"returns": null,
|
|
136
|
-
"description": "Toggle expand state.
|
|
155
|
+
"description": "Toggle expand state. Opening a `lazy` item emits `lazy-load` (via `updated`)."
|
|
137
156
|
}
|
|
138
157
|
],
|
|
139
158
|
"slots": [
|
package/dist/metadata/tree.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"tag": "l-tree",
|
|
7
7
|
"nativeTag": null,
|
|
8
8
|
"selector": "l-tree",
|
|
9
|
-
"summary": "A hierarchical tree view composed of `<l-tree-item>` children.",
|
|
9
|
+
"summary": "A hierarchical tree view composed of `<l-tree-item>` children.\n\nThe host carries `role=\"tree\"`, so give it an accessible name with\n`aria-label` or `aria-labelledby` (e.g. `<l-tree aria-label=\"Files\">`).",
|
|
10
10
|
"status": "stable",
|
|
11
11
|
"appearances": [],
|
|
12
12
|
"import": {
|
|
@@ -286,6 +286,24 @@ A roadmap of phases and tasks, where collapsing folds away phase details for a h
|
|
|
286
286
|
- `Space` — Activates the item (selects or toggles expansion depending on mode)
|
|
287
287
|
- `*` — Expands all sibling branches of the focused item
|
|
288
288
|
|
|
289
|
+
### Selectors & testing
|
|
290
|
+
|
|
291
|
+
Roles and ARIA states are set on `ElementInternals` (the accessibility-tree source) **and** mirrored to DOM attributes, so both `[role]` and `[aria-*]` selectors keep matching in CSS, `querySelector`, and Cypress/Playwright. Selection state is additionally exposed as the reflected `selected`/`expanded`/`disabled`/`indeterminate` boolean attributes (the component's own API).
|
|
292
|
+
|
|
293
|
+
```js
|
|
294
|
+
document.querySelectorAll('[role="treeitem"]'); // ✅ role is mirrored to an attribute
|
|
295
|
+
document.querySelectorAll('[aria-selected="true"]'); // ✅ ARIA state is mirrored too
|
|
296
|
+
tree.querySelectorAll('l-tree-item[selected]'); // ✅ reflected boolean attribute
|
|
297
|
+
screen.getByRole('treeitem', { selected: true, expanded: true }); // ✅ name + state
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
```css
|
|
301
|
+
/* Style by ARIA state or the reflected boolean attribute — both work. */
|
|
302
|
+
l-tree-item[aria-selected='true']::part(base) {
|
|
303
|
+
background: var(--l-color-bg-fill-brand-subtle);
|
|
304
|
+
}
|
|
305
|
+
```
|
|
306
|
+
|
|
289
307
|
## API reference
|
|
290
308
|
|
|
291
309
|
### Importing
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "luxen-ui",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.2",
|
|
4
4
|
"description": "Modern web components and CSS-first UI library built with Lit. Framework-agnostic, customizable prefix, design tokens.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"custom-elements",
|
|
@@ -161,7 +161,9 @@
|
|
|
161
161
|
"manifest": "cem analyze",
|
|
162
162
|
"metadata": "pnpm run manifest && node scripts/normalize-metadata.mjs && node scripts/check-metadata.mjs",
|
|
163
163
|
"preview": "vp preview",
|
|
164
|
-
"test": "vp test run --passWithNoTests",
|
|
164
|
+
"test": "vp test run --passWithNoTests && vp test run --config vitest.browser.config.ts",
|
|
165
|
+
"test:unit": "vp test run --passWithNoTests",
|
|
166
|
+
"test:components": "vp test run --config vitest.browser.config.ts",
|
|
165
167
|
"test:e2e": "playwright test"
|
|
166
168
|
}
|
|
167
169
|
}
|
package/postcss-plugin-prefix.js
CHANGED
|
@@ -19,6 +19,10 @@ const plugin = (opts = {}) => {
|
|
|
19
19
|
postcssPlugin: 'luxen-prefix',
|
|
20
20
|
|
|
21
21
|
Once(root) {
|
|
22
|
+
// Canonical `--l-*` token names whose declarations we rename to the
|
|
23
|
+
// custom prefix. Used to emit the bridge block below.
|
|
24
|
+
const renamedTokens = new Set();
|
|
25
|
+
|
|
22
26
|
root.walk((node) => {
|
|
23
27
|
if (node.type === 'atrule') {
|
|
24
28
|
if (node.name === 'keyframes' && node.params.startsWith('l-')) {
|
|
@@ -32,7 +36,9 @@ const plugin = (opts = {}) => {
|
|
|
32
36
|
|
|
33
37
|
if (node.type === 'decl') {
|
|
34
38
|
if (node.prop.startsWith('--l-')) {
|
|
35
|
-
|
|
39
|
+
const suffix = node.prop.slice('--l-'.length);
|
|
40
|
+
renamedTokens.add(suffix);
|
|
41
|
+
node.prop = `--${cssPrefix}-${suffix}`;
|
|
36
42
|
}
|
|
37
43
|
|
|
38
44
|
if (node.value.includes('l-')) {
|
|
@@ -40,12 +46,48 @@ const plugin = (opts = {}) => {
|
|
|
40
46
|
}
|
|
41
47
|
}
|
|
42
48
|
});
|
|
49
|
+
|
|
50
|
+
emitPrefixAliases(root, cssPrefix, renamedTokens);
|
|
43
51
|
},
|
|
44
52
|
};
|
|
45
53
|
};
|
|
46
54
|
|
|
47
55
|
plugin.postcss = true;
|
|
48
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Bridges the canonical `--l-*` design-token namespace to the consumer's
|
|
59
|
+
* custom prefix.
|
|
60
|
+
*
|
|
61
|
+
* Each element ships its shadow-DOM CSS baked into the element JS, built once
|
|
62
|
+
* with the default `l` prefix — that CSS never passes through this plugin, so
|
|
63
|
+
* it keeps reading `var(--l-focus-ring)` etc. Meanwhile the consumer's imported
|
|
64
|
+
* preset now only defines `--{cssPrefix}-*`. Without a bridge the two
|
|
65
|
+
* namespaces never meet and every shadow token resolves to nothing.
|
|
66
|
+
*
|
|
67
|
+
* So for every `--l-*` token declaration we just renamed, append a one-time
|
|
68
|
+
* `:root` alias pointing the canonical name at the prefixed one:
|
|
69
|
+
*
|
|
70
|
+
* :root { --l-focus-ring: var(--p-focus-ring); … }
|
|
71
|
+
*
|
|
72
|
+
* The block rides along with whichever imported file defines the tokens
|
|
73
|
+
* (preset / tokens / aliases), stays exhaustive automatically (it mirrors
|
|
74
|
+
* exactly what was renamed), and is a no-op for the default `l` build (the
|
|
75
|
+
* plugin early-returns before ever reaching here).
|
|
76
|
+
*
|
|
77
|
+
* @param {import('postcss').Root} root
|
|
78
|
+
* @param {string} cssPrefix
|
|
79
|
+
* @param {Set<string>} renamedTokens — canonical names without the `--l-` head
|
|
80
|
+
*/
|
|
81
|
+
function emitPrefixAliases(root, cssPrefix, renamedTokens) {
|
|
82
|
+
if (renamedTokens.size === 0) return;
|
|
83
|
+
const decls = [...renamedTokens]
|
|
84
|
+
.map((name) => ` --l-${name}: var(--${cssPrefix}-${name});`)
|
|
85
|
+
.join('\n');
|
|
86
|
+
root.append(
|
|
87
|
+
`\n/* luxen-prefix: bridge canonical --l-* tokens to --${cssPrefix}-* so shadow-DOM CSS resolves. */\n:root {\n${decls}\n}\n`,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
49
91
|
function rewriteSelector(selector, elementPrefix, cssPrefix) {
|
|
50
92
|
let result = selector;
|
|
51
93
|
result = result.replace(/\.l-/g, `.${cssPrefix}-`);
|