@aceshooting/lyra-ui 1.0.0 → 1.1.0
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/README.md +8 -9
- package/custom-elements.json +10463 -4519
- package/dist/components/chart/chart.d.ts +46 -1
- package/dist/components/chart/chart.js +148 -10
- package/dist/components/chart/chart.stories.d.ts +32 -0
- package/dist/components/chart/chart.stories.js +141 -0
- package/dist/components/chart/chart.styles.js +16 -0
- package/dist/components/chart/histogram.d.ts +2 -0
- package/dist/components/chart/histogram.js +18 -2
- package/dist/components/chart/line-chart.stories.d.ts +17 -0
- package/dist/components/chart/line-chart.stories.js +75 -0
- package/dist/components/chart/lite-chart.d.ts +74 -0
- package/dist/components/chart/lite-chart.js +384 -0
- package/dist/components/chart/lite-chart.stories.d.ts +12 -0
- package/dist/components/chart/lite-chart.stories.js +98 -0
- package/dist/components/chart/lite-chart.styles.d.ts +1 -0
- package/dist/components/chart/lite-chart.styles.js +83 -0
- package/dist/components/combobox/combobox.d.ts +9 -0
- package/dist/components/combobox/combobox.js +75 -12
- package/dist/components/combobox/combobox.stories.d.ts +18 -0
- package/dist/components/combobox/combobox.stories.js +75 -0
- package/dist/components/date-picker/calendar-core.js +13 -2
- package/dist/components/date-picker/date-input.d.ts +9 -1
- package/dist/components/date-picker/date-input.js +55 -7
- package/dist/components/date-picker/date-input.stories.d.ts +5 -0
- package/dist/components/date-picker/date-input.stories.js +36 -0
- package/dist/components/date-picker/date-picker.d.ts +11 -3
- package/dist/components/date-picker/date-picker.js +87 -37
- package/dist/components/date-picker/date-picker.stories.d.ts +4 -0
- package/dist/components/date-picker/date-picker.stories.js +12 -0
- package/dist/components/date-picker/date-picker.styles.js +1 -1
- package/dist/components/empty/empty.d.ts +12 -1
- package/dist/components/empty/empty.js +20 -3
- package/dist/components/empty/empty.stories.d.ts +2 -0
- package/dist/components/empty/empty.stories.js +20 -0
- package/dist/components/empty/empty.styles.js +11 -0
- package/dist/components/export-button/csv.js +6 -2
- package/dist/components/export-button/export-button.d.ts +15 -1
- package/dist/components/export-button/export-button.js +89 -12
- package/dist/components/export-button/export-button.stories.d.ts +1 -0
- package/dist/components/export-button/export-button.stories.js +5 -0
- package/dist/components/export-button/export-button.styles.js +11 -3
- package/dist/components/file-input/accept.d.ts +8 -2
- package/dist/components/file-input/accept.js +9 -3
- package/dist/components/file-input/file-input.d.ts +5 -2
- package/dist/components/file-input/file-input.js +23 -8
- package/dist/components/file-input/file-input.stories.d.ts +2 -0
- package/dist/components/file-input/file-input.stories.js +9 -0
- package/dist/components/flag/flag.d.ts +48 -3
- package/dist/components/flag/flag.js +65 -29
- package/dist/components/flag/flag.styles.js +1 -1
- package/dist/components/flag/language-map.d.ts +11 -0
- package/dist/components/flag/language-map.js +12 -1
- package/dist/components/gauge/gauge.d.ts +2 -0
- package/dist/components/gauge/gauge.js +19 -5
- package/dist/components/gauge/gauge.stories.d.ts +2 -0
- package/dist/components/gauge/gauge.stories.js +6 -0
- package/dist/components/graph/graph-loader.d.ts +28 -0
- package/dist/components/graph/graph-loader.js +44 -0
- package/dist/components/graph/graph.d.ts +41 -15
- package/dist/components/graph/graph.js +146 -71
- package/dist/components/graph/graph.stories.d.ts +3 -0
- package/dist/components/graph/graph.stories.js +38 -0
- package/dist/components/graph/graph.styles.js +6 -1
- package/dist/components/heatmap/calendar-grid.d.ts +7 -0
- package/dist/components/heatmap/calendar-grid.js +27 -10
- package/dist/components/heatmap/heatmap.d.ts +137 -12
- package/dist/components/heatmap/heatmap.js +427 -27
- package/dist/components/heatmap/heatmap.stories.d.ts +17 -0
- package/dist/components/heatmap/heatmap.stories.js +130 -0
- package/dist/components/heatmap/heatmap.styles.js +60 -2
- package/dist/components/map/map.d.ts +17 -1
- package/dist/components/map/map.js +68 -21
- package/dist/components/map/map.stories.d.ts +10 -0
- package/dist/components/map/map.stories.js +107 -12
- package/dist/components/map/map.styles.js +2 -2
- package/dist/components/playback/playback.d.ts +6 -2
- package/dist/components/playback/playback.js +36 -12
- package/dist/components/playback/playback.stories.d.ts +2 -0
- package/dist/components/playback/playback.stories.js +6 -0
- package/dist/components/playback/playback.styles.js +10 -2
- package/dist/components/select/select.d.ts +122 -0
- package/dist/components/select/select.js +516 -0
- package/dist/components/select/select.stories.d.ts +19 -0
- package/dist/components/select/select.stories.js +75 -0
- package/dist/components/select/select.styles.d.ts +1 -0
- package/dist/components/select/select.styles.js +188 -0
- package/dist/components/skeleton/skeleton.js +4 -7
- package/dist/components/sparkline/sparkline.d.ts +1 -1
- package/dist/components/sparkline/sparkline.js +40 -13
- package/dist/components/sparkline/sparkline.stories.d.ts +4 -0
- package/dist/components/sparkline/sparkline.stories.js +22 -0
- package/dist/components/split/split.d.ts +24 -3
- package/dist/components/split/split.js +155 -31
- package/dist/components/split/split.stories.d.ts +2 -0
- package/dist/components/split/split.stories.js +20 -0
- package/dist/components/stat/stat.d.ts +27 -1
- package/dist/components/stat/stat.js +56 -3
- package/dist/components/stat/stat.stories.d.ts +3 -0
- package/dist/components/stat/stat.stories.js +49 -1
- package/dist/components/stat/stat.styles.js +44 -0
- package/dist/components/table/table.d.ts +64 -2
- package/dist/components/table/table.js +213 -17
- package/dist/components/table/table.stories.d.ts +6 -0
- package/dist/components/table/table.stories.js +45 -0
- package/dist/components/table/table.styles.js +51 -4
- package/dist/components/time-range/time-range.d.ts +34 -5
- package/dist/components/time-range/time-range.js +217 -36
- package/dist/components/time-range/time-range.stories.d.ts +2 -0
- package/dist/components/time-range/time-range.stories.js +13 -0
- package/dist/components/time-range/time-range.styles.js +44 -2
- package/dist/components/toast/toast-item.d.ts +14 -2
- package/dist/components/toast/toast-item.js +134 -17
- package/dist/components/toast/toast-item.styles.js +28 -2
- package/dist/components/toast/toast.stories.d.ts +3 -0
- package/dist/components/toast/toast.stories.js +41 -1
- package/dist/components/toast/toast.styles.js +11 -11
- package/dist/components/toast/toaster.d.ts +1 -1
- package/dist/components/toast/toaster.js +13 -6
- package/dist/components/tree/tree-node.d.ts +13 -7
- package/dist/components/tree/tree-node.js +24 -10
- package/dist/components/tree/tree.d.ts +64 -15
- package/dist/components/tree/tree.js +113 -22
- package/dist/components/tree/tree.stories.d.ts +4 -0
- package/dist/components/tree/tree.stories.js +31 -1
- package/dist/components/tree/update-cascade.d.ts +13 -0
- package/dist/components/tree/update-cascade.js +13 -0
- package/dist/components/widget/widget.d.ts +15 -1
- package/dist/components/widget/widget.js +140 -5
- package/dist/components/widget/widget.stories.d.ts +1 -0
- package/dist/components/widget/widget.stories.js +10 -0
- package/dist/components/widget/widget.styles.js +11 -1
- package/dist/components/word-cloud/word-cloud-layout.d.ts +63 -0
- package/dist/components/word-cloud/word-cloud-layout.js +145 -0
- package/dist/components/word-cloud/word-cloud.d.ts +65 -0
- package/dist/components/word-cloud/word-cloud.js +255 -0
- package/dist/components/word-cloud/word-cloud.stories.d.ts +10 -0
- package/dist/components/word-cloud/word-cloud.stories.js +55 -0
- package/dist/components/word-cloud/word-cloud.styles.d.ts +1 -0
- package/dist/components/word-cloud/word-cloud.styles.js +54 -0
- package/dist/internal/a11y.js +2 -1
- package/dist/internal/rtl.d.ts +7 -0
- package/dist/internal/rtl.js +9 -0
- package/dist/lyra.d.ts +9 -1
- package/dist/lyra.js +6 -0
- package/llms-full.txt +892 -276
- package/llms.txt +12 -9
- package/package.json +6 -4
|
@@ -19,42 +19,91 @@ export interface TreeItem {
|
|
|
19
19
|
* `<lyra-tree-node>`'s own shadow root still reaches this listener.
|
|
20
20
|
*
|
|
21
21
|
* @customElement lyra-tree
|
|
22
|
-
* @event lyra-node-toggle - `detail: { id, expanded }`,
|
|
23
|
-
* @event lyra-node-select - `detail: { id }`,
|
|
22
|
+
* @event lyra-node-toggle - `detail: { id, expanded }`, dispatched by a descendant `<lyra-tree-node>` and observed here (bubbling, composed) to keep the roving-tabindex `activeId` in sync.
|
|
23
|
+
* @event lyra-node-select - `detail: { id }`, dispatched by a descendant `<lyra-tree-node>` and observed here (bubbling, composed) to keep the roving-tabindex `activeId` in sync.
|
|
24
24
|
* @csspart base
|
|
25
25
|
* @slot - `<lyra-tree-node>` elements (top-level tree items).
|
|
26
26
|
*/
|
|
27
27
|
export declare class LyraTree extends LyraElement {
|
|
28
28
|
static styles: import("lit").CSSResultGroup[];
|
|
29
29
|
data: TreeItem[];
|
|
30
|
+
/** Accessible name for the tree -- `role="tree"` lives on an internal element, not the host, so `aria-label`/`aria-labelledby` set directly on `<lyra-tree>` would not label it. */
|
|
31
|
+
label: string;
|
|
30
32
|
private activeId;
|
|
33
|
+
/** Set by `willUpdate()` when a `data` reassignment removes the node that currently holds real DOM focus; consumed by `updated()` to refocus the newly-designated `activeId` node once it's actually focusable. */
|
|
34
|
+
private pendingFocusId;
|
|
31
35
|
private get nodeElements();
|
|
32
36
|
private childrenOf;
|
|
37
|
+
/**
|
|
38
|
+
* Memoized result of the last `visibleNodeElements()` walk. Every keydown
|
|
39
|
+
* (ArrowUp/Down/Home/End especially) reads the full visible-node list, and
|
|
40
|
+
* rebuilding it means a `shadowRoot.querySelectorAll` per currently-expanded
|
|
41
|
+
* node -- invalidated below on the only two things that can change which
|
|
42
|
+
* nodes are visible: a `data` reassignment, or a node expanding/collapsing.
|
|
43
|
+
*/
|
|
44
|
+
private cachedVisible;
|
|
45
|
+
private invalidateVisibleCache;
|
|
33
46
|
/** Every currently *visible* (ancestor-expanded) node, top-to-bottom. */
|
|
34
47
|
private visibleNodeElements;
|
|
35
48
|
private findItem;
|
|
36
49
|
protected willUpdate(changed: PropertyValues): void;
|
|
37
50
|
protected updated(changed: PropertyValues): void;
|
|
38
|
-
/**
|
|
51
|
+
/** By-id reconciliation of top-level items: reuses/reorders existing `<lyra-tree-node>` elements and removes ones no longer present in `data`. */
|
|
39
52
|
private syncNodes;
|
|
40
53
|
private focusNode;
|
|
41
54
|
/**
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
55
|
+
* A mouse click always lands directly on the node it interacts with --
|
|
56
|
+
* `select()`/`expand()`/`collapse()` all emit their own node's id --
|
|
57
|
+
* independent of whatever `activeId` currently holds. Sync `activeId` to
|
|
58
|
+
* that id here so a click always becomes the tree's new roving-tabindex
|
|
59
|
+
* target: this keeps it aligned with real DOM focus, keeps the next
|
|
60
|
+
* arrow-key press relative to the item the user just clicked (rather than
|
|
61
|
+
* a stale `activeId`), and keeps `activeId` valid when a click collapses
|
|
62
|
+
* an ancestor of the previously-active node -- the collapsed node's own
|
|
63
|
+
* id (always still visible, since collapsing never removes a node's own
|
|
64
|
+
* top-level or already-rendered self) replaces the now-hidden descendant's
|
|
65
|
+
* id, so at least one node keeps a roving tabindex of 0. Keyboard-driven
|
|
66
|
+
* toggles/selects always target the already-active node, so this is a
|
|
67
|
+
* same-value, no-op assignment for them.
|
|
68
|
+
*/
|
|
69
|
+
private onNodeActivate;
|
|
70
|
+
/**
|
|
71
|
+
* `updated()` only pushes the new `activeId` to *top-level* nodes; nested
|
|
72
|
+
* nodes only receive it once their ancestor chain's own renders cascade it
|
|
73
|
+
* down (one more pending update per depth level). Cascade `updateComplete`
|
|
74
|
+
* to match (see `cascadeUpdateComplete`), so `focusNode()`'s `.focus()`
|
|
75
|
+
* call never runs while a nested target is still mid-cascade -- `.focus()`
|
|
76
|
+
* on an element with no `tabindex` attribute committed yet is a silent
|
|
77
|
+
* no-op.
|
|
78
|
+
*
|
|
79
|
+
* The `pendingFocusId` refocus (set by `willUpdate()` when a `data`
|
|
80
|
+
* reassignment removes the node that currently holds real DOM focus) is
|
|
81
|
+
* also resolved *here*, after the cascade above, rather than from
|
|
82
|
+
* `updated()` firing a detached `void this.updateComplete.then(...)` of
|
|
83
|
+
* its own: `updateComplete`'s getter (see the base class) calls this
|
|
84
|
+
* method fresh on *every* access rather than caching one promise, so a
|
|
85
|
+
* second, independent invocation started from inside `updated()` isn't
|
|
86
|
+
* the same promise chain a caller's own `await el.updateComplete` is
|
|
87
|
+
* following -- both ultimately settle once the same underlying update
|
|
88
|
+
* resolves, but as separate chains their `.then()` continuations aren't
|
|
89
|
+
* ordered against each other, so a caller's `await` can win the race and
|
|
90
|
+
* observe focus *not yet* restored. Doing the refocus inline, before this
|
|
91
|
+
* method's own `await` chain resolves, makes it unconditionally part of
|
|
92
|
+
* whatever `updateComplete` promise every caller (this class's own
|
|
93
|
+
* `focusNode()` included) is already waiting on.
|
|
52
94
|
*/
|
|
53
95
|
protected getUpdateComplete(): Promise<boolean>;
|
|
54
96
|
private onTreeKeyDown;
|
|
55
|
-
/** Expand every node in the tree, recursively.
|
|
97
|
+
/** Expand every node in the tree, recursively. */
|
|
56
98
|
expandAll(): void;
|
|
57
|
-
/**
|
|
99
|
+
/**
|
|
100
|
+
* Collapse every node in the tree, recursively. Goes through each node's
|
|
101
|
+
* own `collapse()` (rather than assigning `expanded` directly) so its
|
|
102
|
+
* `lyra-node-toggle` emit reaches `onNodeActivate` -- that keeps `activeId`
|
|
103
|
+
* re-synced to a node that's still visible after collapsing, even when the
|
|
104
|
+
* roving-tabindex target was a nested descendant whose ancestor's
|
|
105
|
+
* `role="group"` is about to disappear.
|
|
106
|
+
*/
|
|
58
107
|
collapseAll(): void;
|
|
59
108
|
render(): TemplateResult;
|
|
60
109
|
}
|
|
@@ -4,11 +4,13 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
4
4
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
5
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
6
|
};
|
|
7
|
-
import { html } from 'lit';
|
|
7
|
+
import { html, nothing } from 'lit';
|
|
8
8
|
import { property, state } from 'lit/decorators.js';
|
|
9
9
|
import { LyraElement } from '../../internal/lyra-element.js';
|
|
10
10
|
import { defineElement } from '../../internal/prefix.js';
|
|
11
|
+
import { isRtl } from '../../internal/rtl.js';
|
|
11
12
|
import { styles } from './tree.styles.js';
|
|
13
|
+
import { cascadeUpdateComplete } from './update-cascade.js';
|
|
12
14
|
import './tree-node.js';
|
|
13
15
|
/**
|
|
14
16
|
* `<lyra-tree>` — an expand/collapse hierarchy for graph/document navigation.
|
|
@@ -22,8 +24,8 @@ import './tree-node.js';
|
|
|
22
24
|
* `<lyra-tree-node>`'s own shadow root still reaches this listener.
|
|
23
25
|
*
|
|
24
26
|
* @customElement lyra-tree
|
|
25
|
-
* @event lyra-node-toggle - `detail: { id, expanded }`,
|
|
26
|
-
* @event lyra-node-select - `detail: { id }`,
|
|
27
|
+
* @event lyra-node-toggle - `detail: { id, expanded }`, dispatched by a descendant `<lyra-tree-node>` and observed here (bubbling, composed) to keep the roving-tabindex `activeId` in sync.
|
|
28
|
+
* @event lyra-node-select - `detail: { id }`, dispatched by a descendant `<lyra-tree-node>` and observed here (bubbling, composed) to keep the roving-tabindex `activeId` in sync.
|
|
27
29
|
* @csspart base
|
|
28
30
|
* @slot - `<lyra-tree-node>` elements (top-level tree items).
|
|
29
31
|
*/
|
|
@@ -31,13 +33,50 @@ export class LyraTree extends LyraElement {
|
|
|
31
33
|
constructor() {
|
|
32
34
|
super(...arguments);
|
|
33
35
|
this.data = [];
|
|
36
|
+
/** Accessible name for the tree -- `role="tree"` lives on an internal element, not the host, so `aria-label`/`aria-labelledby` set directly on `<lyra-tree>` would not label it. */
|
|
37
|
+
this.label = '';
|
|
34
38
|
this.activeId = null;
|
|
39
|
+
/** Set by `willUpdate()` when a `data` reassignment removes the node that currently holds real DOM focus; consumed by `updated()` to refocus the newly-designated `activeId` node once it's actually focusable. */
|
|
40
|
+
this.pendingFocusId = null;
|
|
41
|
+
/**
|
|
42
|
+
* Memoized result of the last `visibleNodeElements()` walk. Every keydown
|
|
43
|
+
* (ArrowUp/Down/Home/End especially) reads the full visible-node list, and
|
|
44
|
+
* rebuilding it means a `shadowRoot.querySelectorAll` per currently-expanded
|
|
45
|
+
* node -- invalidated below on the only two things that can change which
|
|
46
|
+
* nodes are visible: a `data` reassignment, or a node expanding/collapsing.
|
|
47
|
+
*/
|
|
48
|
+
this.cachedVisible = null;
|
|
49
|
+
/**
|
|
50
|
+
* A mouse click always lands directly on the node it interacts with --
|
|
51
|
+
* `select()`/`expand()`/`collapse()` all emit their own node's id --
|
|
52
|
+
* independent of whatever `activeId` currently holds. Sync `activeId` to
|
|
53
|
+
* that id here so a click always becomes the tree's new roving-tabindex
|
|
54
|
+
* target: this keeps it aligned with real DOM focus, keeps the next
|
|
55
|
+
* arrow-key press relative to the item the user just clicked (rather than
|
|
56
|
+
* a stale `activeId`), and keeps `activeId` valid when a click collapses
|
|
57
|
+
* an ancestor of the previously-active node -- the collapsed node's own
|
|
58
|
+
* id (always still visible, since collapsing never removes a node's own
|
|
59
|
+
* top-level or already-rendered self) replaces the now-hidden descendant's
|
|
60
|
+
* id, so at least one node keeps a roving tabindex of 0. Keyboard-driven
|
|
61
|
+
* toggles/selects always target the already-active node, so this is a
|
|
62
|
+
* same-value, no-op assignment for them.
|
|
63
|
+
*/
|
|
64
|
+
this.onNodeActivate = (e) => {
|
|
65
|
+
if (e.type === 'lyra-node-toggle')
|
|
66
|
+
this.invalidateVisibleCache();
|
|
67
|
+
this.activeId = e.detail.id;
|
|
68
|
+
};
|
|
35
69
|
this.onTreeKeyDown = (e) => {
|
|
36
70
|
const visible = this.visibleNodeElements();
|
|
37
71
|
if (visible.length === 0)
|
|
38
72
|
return;
|
|
39
73
|
const currentIndex = visible.findIndex((n) => n.item.id === this.activeId);
|
|
40
74
|
const current = currentIndex >= 0 ? visible[currentIndex] : visible[0];
|
|
75
|
+
// Expand/step-in and collapse/step-out are physical-direction actions --
|
|
76
|
+
// swap which arrow key does which in RTL, matching split.ts/time-range.ts.
|
|
77
|
+
const rtl = isRtl(this);
|
|
78
|
+
const expandKey = rtl ? 'ArrowLeft' : 'ArrowRight';
|
|
79
|
+
const collapseKey = rtl ? 'ArrowRight' : 'ArrowLeft';
|
|
41
80
|
switch (e.key) {
|
|
42
81
|
case 'ArrowDown':
|
|
43
82
|
e.preventDefault();
|
|
@@ -55,12 +94,12 @@ export class LyraTree extends LyraElement {
|
|
|
55
94
|
e.preventDefault();
|
|
56
95
|
this.focusNode(visible[visible.length - 1]);
|
|
57
96
|
break;
|
|
58
|
-
case
|
|
97
|
+
case expandKey:
|
|
59
98
|
e.preventDefault();
|
|
60
99
|
if (!current.hasChildren)
|
|
61
100
|
break;
|
|
62
101
|
if (!current.expanded) {
|
|
63
|
-
current.expand(); // focus stays put; a 2nd
|
|
102
|
+
current.expand(); // focus stays put; a 2nd press steps into the first child
|
|
64
103
|
}
|
|
65
104
|
else {
|
|
66
105
|
const child = visible[currentIndex + 1];
|
|
@@ -68,7 +107,7 @@ export class LyraTree extends LyraElement {
|
|
|
68
107
|
this.focusNode(child);
|
|
69
108
|
}
|
|
70
109
|
break;
|
|
71
|
-
case
|
|
110
|
+
case collapseKey:
|
|
72
111
|
e.preventDefault();
|
|
73
112
|
if (current.hasChildren && current.expanded) {
|
|
74
113
|
current.collapse();
|
|
@@ -99,8 +138,13 @@ export class LyraTree extends LyraElement {
|
|
|
99
138
|
childrenOf(node) {
|
|
100
139
|
return [...(node.shadowRoot?.querySelectorAll('lyra-tree-node') ?? [])];
|
|
101
140
|
}
|
|
141
|
+
invalidateVisibleCache() {
|
|
142
|
+
this.cachedVisible = null;
|
|
143
|
+
}
|
|
102
144
|
/** Every currently *visible* (ancestor-expanded) node, top-to-bottom. */
|
|
103
145
|
visibleNodeElements() {
|
|
146
|
+
if (this.cachedVisible)
|
|
147
|
+
return this.cachedVisible;
|
|
104
148
|
const acc = [];
|
|
105
149
|
const walk = (nodes) => {
|
|
106
150
|
for (const n of nodes) {
|
|
@@ -110,6 +154,7 @@ export class LyraTree extends LyraElement {
|
|
|
110
154
|
}
|
|
111
155
|
};
|
|
112
156
|
walk(this.nodeElements);
|
|
157
|
+
this.cachedVisible = acc;
|
|
113
158
|
return acc;
|
|
114
159
|
}
|
|
115
160
|
findItem(items, id) {
|
|
@@ -124,10 +169,20 @@ export class LyraTree extends LyraElement {
|
|
|
124
169
|
}
|
|
125
170
|
willUpdate(changed) {
|
|
126
171
|
if (changed.has('data')) {
|
|
172
|
+
this.invalidateVisibleCache();
|
|
173
|
+
// `document.activeElement` collapses to the outermost light-DOM node
|
|
174
|
+
// even when the real focus target is a nested descendant several
|
|
175
|
+
// shadow roots down, so this also catches a focused nested node whose
|
|
176
|
+
// top-level ancestor is about to be removed.
|
|
177
|
+
const focused = this.nodeElements.find((n) => n === document.activeElement);
|
|
127
178
|
this.syncNodes();
|
|
128
179
|
if (!this.activeId || !this.findItem(this.data, this.activeId)) {
|
|
129
180
|
this.activeId = this.data[0]?.id ?? null;
|
|
130
181
|
}
|
|
182
|
+
// `node.remove()` inside `syncNodes()` (per the DOM spec) already moved
|
|
183
|
+
// real focus off `focused` and onto <body> by this point; record the
|
|
184
|
+
// newly-resolved `activeId` so `updated()` can restore focus to it.
|
|
185
|
+
this.pendingFocusId = focused && !focused.isConnected ? this.activeId : null;
|
|
131
186
|
}
|
|
132
187
|
}
|
|
133
188
|
updated(changed) {
|
|
@@ -140,7 +195,7 @@ export class LyraTree extends LyraElement {
|
|
|
140
195
|
});
|
|
141
196
|
}
|
|
142
197
|
}
|
|
143
|
-
/**
|
|
198
|
+
/** By-id reconciliation of top-level items: reuses/reorders existing `<lyra-tree-node>` elements and removes ones no longer present in `data`. */
|
|
144
199
|
syncNodes() {
|
|
145
200
|
const existingById = new Map();
|
|
146
201
|
for (const node of this.nodeElements) {
|
|
@@ -174,45 +229,78 @@ export class LyraTree extends LyraElement {
|
|
|
174
229
|
node.focus();
|
|
175
230
|
}
|
|
176
231
|
/**
|
|
177
|
-
* `updated()` only pushes the new `activeId` to *top-level* nodes;
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
232
|
+
* `updated()` only pushes the new `activeId` to *top-level* nodes; nested
|
|
233
|
+
* nodes only receive it once their ancestor chain's own renders cascade it
|
|
234
|
+
* down (one more pending update per depth level). Cascade `updateComplete`
|
|
235
|
+
* to match (see `cascadeUpdateComplete`), so `focusNode()`'s `.focus()`
|
|
236
|
+
* call never runs while a nested target is still mid-cascade -- `.focus()`
|
|
237
|
+
* on an element with no `tabindex` attribute committed yet is a silent
|
|
238
|
+
* no-op.
|
|
239
|
+
*
|
|
240
|
+
* The `pendingFocusId` refocus (set by `willUpdate()` when a `data`
|
|
241
|
+
* reassignment removes the node that currently holds real DOM focus) is
|
|
242
|
+
* also resolved *here*, after the cascade above, rather than from
|
|
243
|
+
* `updated()` firing a detached `void this.updateComplete.then(...)` of
|
|
244
|
+
* its own: `updateComplete`'s getter (see the base class) calls this
|
|
245
|
+
* method fresh on *every* access rather than caching one promise, so a
|
|
246
|
+
* second, independent invocation started from inside `updated()` isn't
|
|
247
|
+
* the same promise chain a caller's own `await el.updateComplete` is
|
|
248
|
+
* following -- both ultimately settle once the same underlying update
|
|
249
|
+
* resolves, but as separate chains their `.then()` continuations aren't
|
|
250
|
+
* ordered against each other, so a caller's `await` can win the race and
|
|
251
|
+
* observe focus *not yet* restored. Doing the refocus inline, before this
|
|
252
|
+
* method's own `await` chain resolves, makes it unconditionally part of
|
|
253
|
+
* whatever `updateComplete` promise every caller (this class's own
|
|
254
|
+
* `focusNode()` included) is already waiting on.
|
|
187
255
|
*/
|
|
188
256
|
async getUpdateComplete() {
|
|
189
257
|
const result = await super.getUpdateComplete();
|
|
190
|
-
await
|
|
258
|
+
await cascadeUpdateComplete(this.nodeElements);
|
|
259
|
+
if (this.pendingFocusId != null) {
|
|
260
|
+
const id = this.pendingFocusId;
|
|
261
|
+
this.pendingFocusId = null;
|
|
262
|
+
this.nodeElements.find((n) => n.item.id === id)?.focus();
|
|
263
|
+
}
|
|
191
264
|
return result;
|
|
192
265
|
}
|
|
193
|
-
/** Expand every node in the tree, recursively.
|
|
266
|
+
/** Expand every node in the tree, recursively. */
|
|
194
267
|
expandAll() {
|
|
195
268
|
const setAll = (nodes) => {
|
|
196
269
|
for (const n of nodes) {
|
|
197
270
|
n.expanded = true;
|
|
271
|
+
this.invalidateVisibleCache();
|
|
198
272
|
void n.updateComplete.then(() => setAll(this.childrenOf(n)));
|
|
199
273
|
}
|
|
200
274
|
};
|
|
201
275
|
setAll(this.nodeElements);
|
|
202
276
|
}
|
|
203
|
-
/**
|
|
277
|
+
/**
|
|
278
|
+
* Collapse every node in the tree, recursively. Goes through each node's
|
|
279
|
+
* own `collapse()` (rather than assigning `expanded` directly) so its
|
|
280
|
+
* `lyra-node-toggle` emit reaches `onNodeActivate` -- that keeps `activeId`
|
|
281
|
+
* re-synced to a node that's still visible after collapsing, even when the
|
|
282
|
+
* roving-tabindex target was a nested descendant whose ancestor's
|
|
283
|
+
* `role="group"` is about to disappear.
|
|
284
|
+
*/
|
|
204
285
|
collapseAll() {
|
|
205
286
|
const setAll = (nodes) => {
|
|
206
287
|
for (const n of nodes) {
|
|
207
288
|
setAll(this.childrenOf(n));
|
|
208
|
-
n.
|
|
289
|
+
n.collapse();
|
|
209
290
|
}
|
|
210
291
|
};
|
|
211
292
|
setAll(this.nodeElements);
|
|
212
293
|
}
|
|
213
294
|
render() {
|
|
214
295
|
return html `
|
|
215
|
-
<div
|
|
296
|
+
<div
|
|
297
|
+
part="base"
|
|
298
|
+
role="tree"
|
|
299
|
+
aria-label=${this.label || nothing}
|
|
300
|
+
@keydown=${this.onTreeKeyDown}
|
|
301
|
+
@lyra-node-toggle=${this.onNodeActivate}
|
|
302
|
+
@lyra-node-select=${this.onNodeActivate}
|
|
303
|
+
>
|
|
216
304
|
<slot></slot>
|
|
217
305
|
</div>
|
|
218
306
|
`;
|
|
@@ -221,6 +309,9 @@ export class LyraTree extends LyraElement {
|
|
|
221
309
|
__decorate([
|
|
222
310
|
property({ attribute: false })
|
|
223
311
|
], LyraTree.prototype, "data", void 0);
|
|
312
|
+
__decorate([
|
|
313
|
+
property()
|
|
314
|
+
], LyraTree.prototype, "label", void 0);
|
|
224
315
|
__decorate([
|
|
225
316
|
state()
|
|
226
317
|
], LyraTree.prototype, "activeId", void 0);
|
|
@@ -3,3 +3,7 @@ declare const meta: Meta;
|
|
|
3
3
|
export default meta;
|
|
4
4
|
type Story = StoryObj;
|
|
5
5
|
export declare const Default: Story;
|
|
6
|
+
/** Demonstrates the imperative `expandAll()`/`collapseAll()` methods. */
|
|
7
|
+
export declare const ExpandCollapseAll: Story;
|
|
8
|
+
/** A single branch nested well past the 8rem indentation cap, so the capped indent and truncated label are both visible without expanding anything by hand. */
|
|
9
|
+
export declare const DeeplyNested: Story;
|
|
@@ -18,5 +18,35 @@ const meta = {
|
|
|
18
18
|
};
|
|
19
19
|
export default meta;
|
|
20
20
|
export const Default = {
|
|
21
|
-
render: () => html `<lyra-tree style="max-width: 20rem" .data=${data}></lyra-tree>`,
|
|
21
|
+
render: () => html `<lyra-tree style="max-width: 20rem" label="File explorer" .data=${data}></lyra-tree>`,
|
|
22
|
+
};
|
|
23
|
+
/** Demonstrates the imperative `expandAll()`/`collapseAll()` methods. */
|
|
24
|
+
export const ExpandCollapseAll = {
|
|
25
|
+
render: () => {
|
|
26
|
+
const getTree = () => document.getElementById('imperative-tree');
|
|
27
|
+
return html `
|
|
28
|
+
<div style="display:flex; flex-direction:column; gap:1rem; max-width:20rem">
|
|
29
|
+
<div style="display:flex; gap:0.5rem">
|
|
30
|
+
<button @click=${() => getTree().expandAll()}>Expand all</button>
|
|
31
|
+
<button @click=${() => getTree().collapseAll()}>Collapse all</button>
|
|
32
|
+
</div>
|
|
33
|
+
<lyra-tree id="imperative-tree" label="File explorer" .data=${data}></lyra-tree>
|
|
34
|
+
</div>
|
|
35
|
+
`;
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
const buildDeepData = (depth) => {
|
|
39
|
+
let node = { id: `d${depth}`, label: `A very long deeply-nested label all the way at depth ${depth}` };
|
|
40
|
+
for (let d = depth - 1; d >= 0; d--) {
|
|
41
|
+
node = { id: `d${d}`, label: `Level ${d} node with a fairly long descriptive label`, children: [node] };
|
|
42
|
+
}
|
|
43
|
+
return [node];
|
|
44
|
+
};
|
|
45
|
+
/** A single branch nested well past the 8rem indentation cap, so the capped indent and truncated label are both visible without expanding anything by hand. */
|
|
46
|
+
export const DeeplyNested = {
|
|
47
|
+
render: () => html `<lyra-tree style="max-width: 20rem" label="Deeply nested example" .data=${buildDeepData(12)}></lyra-tree>`,
|
|
48
|
+
play: async ({ canvasElement }) => {
|
|
49
|
+
const tree = canvasElement.querySelector('lyra-tree');
|
|
50
|
+
tree.expandAll();
|
|
51
|
+
},
|
|
22
52
|
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Both `<lyra-tree>` and `<lyra-tree-node>` override `getUpdateComplete()` to
|
|
3
|
+
* additionally await their currently-known child `<lyra-tree-node>`
|
|
4
|
+
* elements' own `updateComplete`. A nested node only receives a pushed-down
|
|
5
|
+
* property (like `activeId`, and the `tabIndex` derived from it) once its
|
|
6
|
+
* own render has committed -- one more pending update per depth level -- so
|
|
7
|
+
* without this, code that awaits a parent's `updateComplete` (e.g.
|
|
8
|
+
* `focusNode()`) could run before an arbitrarily-nested descendant has
|
|
9
|
+
* actually settled.
|
|
10
|
+
*/
|
|
11
|
+
export declare function cascadeUpdateComplete(children: Iterable<{
|
|
12
|
+
updateComplete: Promise<unknown>;
|
|
13
|
+
}>): Promise<void>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Both `<lyra-tree>` and `<lyra-tree-node>` override `getUpdateComplete()` to
|
|
3
|
+
* additionally await their currently-known child `<lyra-tree-node>`
|
|
4
|
+
* elements' own `updateComplete`. A nested node only receives a pushed-down
|
|
5
|
+
* property (like `activeId`, and the `tabIndex` derived from it) once its
|
|
6
|
+
* own render has committed -- one more pending update per depth level -- so
|
|
7
|
+
* without this, code that awaits a parent's `updateComplete` (e.g.
|
|
8
|
+
* `focusNode()`) could run before an arbitrarily-nested descendant has
|
|
9
|
+
* actually settled.
|
|
10
|
+
*/
|
|
11
|
+
export async function cascadeUpdateComplete(children) {
|
|
12
|
+
await Promise.all([...children].map((child) => child.updateComplete));
|
|
13
|
+
}
|
|
@@ -11,7 +11,16 @@ import { LyraElement } from '../../internal/lyra-element.js';
|
|
|
11
11
|
* @slot actions - Header action controls, rendered before the collapse/expand buttons.
|
|
12
12
|
* @event lyra-collapse-change - `detail: boolean` (the new `collapsed` state).
|
|
13
13
|
* @event lyra-fullscreen-change - `detail: boolean` (the new `fullscreen` state).
|
|
14
|
-
* @csspart base
|
|
14
|
+
* @csspart base - The panel root (dialog role + backdrop when fullscreen).
|
|
15
|
+
* @csspart header - The header row containing the title, actions, and toggle buttons.
|
|
16
|
+
* @csspart title - The wrapper around the label/sublabel.
|
|
17
|
+
* @csspart label - The panel title text.
|
|
18
|
+
* @csspart sublabel - The panel subtitle text.
|
|
19
|
+
* @csspart actions - The wrapper around the `actions` slot.
|
|
20
|
+
* @csspart collapse-button - The collapse/expand toggle button.
|
|
21
|
+
* @csspart fullscreen-button - The fullscreen toggle button.
|
|
22
|
+
* @csspart body - The wrapper around the default slot (the panel body).
|
|
23
|
+
* @csspart backdrop - The fullscreen scrim behind the panel.
|
|
15
24
|
*/
|
|
16
25
|
export declare class LyraWidget extends LyraElement {
|
|
17
26
|
static styles: import("lit").CSSResultGroup[];
|
|
@@ -24,12 +33,17 @@ export declare class LyraWidget extends LyraElement {
|
|
|
24
33
|
private hasActionsSlot;
|
|
25
34
|
private releaseScrollLock?;
|
|
26
35
|
private lastTrigger?;
|
|
36
|
+
private readonly bodyId;
|
|
27
37
|
protected willUpdate(changed: PropertyValues): void;
|
|
38
|
+
protected updated(changed: PropertyValues): void;
|
|
39
|
+
connectedCallback(): void;
|
|
28
40
|
disconnectedCallback(): void;
|
|
29
41
|
private onActionsSlotChange;
|
|
30
42
|
private toggleCollapsed;
|
|
31
43
|
private toggleFullscreen;
|
|
32
44
|
private onDocKeyDown;
|
|
45
|
+
private getFocusableElements;
|
|
46
|
+
private getActiveElement;
|
|
33
47
|
private onBackdropClick;
|
|
34
48
|
render(): TemplateResult;
|
|
35
49
|
}
|