@aceshooting/lyra-ui 1.0.1 → 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.
Files changed (148) hide show
  1. package/README.md +8 -9
  2. package/custom-elements.json +12479 -6535
  3. package/dist/components/chart/chart.d.ts +46 -1
  4. package/dist/components/chart/chart.js +148 -10
  5. package/dist/components/chart/chart.stories.d.ts +32 -0
  6. package/dist/components/chart/chart.stories.js +141 -0
  7. package/dist/components/chart/chart.styles.js +16 -0
  8. package/dist/components/chart/histogram.d.ts +2 -0
  9. package/dist/components/chart/histogram.js +18 -2
  10. package/dist/components/chart/line-chart.stories.d.ts +17 -0
  11. package/dist/components/chart/line-chart.stories.js +75 -0
  12. package/dist/components/chart/lite-chart.d.ts +74 -0
  13. package/dist/components/chart/lite-chart.js +384 -0
  14. package/dist/components/chart/lite-chart.stories.d.ts +12 -0
  15. package/dist/components/chart/lite-chart.stories.js +98 -0
  16. package/dist/components/chart/lite-chart.styles.d.ts +1 -0
  17. package/dist/components/chart/lite-chart.styles.js +83 -0
  18. package/dist/components/combobox/combobox.d.ts +9 -0
  19. package/dist/components/combobox/combobox.js +75 -12
  20. package/dist/components/combobox/combobox.stories.d.ts +18 -0
  21. package/dist/components/combobox/combobox.stories.js +75 -0
  22. package/dist/components/date-picker/calendar-core.js +13 -2
  23. package/dist/components/date-picker/date-input.d.ts +9 -1
  24. package/dist/components/date-picker/date-input.js +55 -7
  25. package/dist/components/date-picker/date-input.stories.d.ts +5 -0
  26. package/dist/components/date-picker/date-input.stories.js +36 -0
  27. package/dist/components/date-picker/date-picker.d.ts +11 -3
  28. package/dist/components/date-picker/date-picker.js +87 -37
  29. package/dist/components/date-picker/date-picker.stories.d.ts +4 -0
  30. package/dist/components/date-picker/date-picker.stories.js +12 -0
  31. package/dist/components/date-picker/date-picker.styles.js +1 -1
  32. package/dist/components/empty/empty.d.ts +12 -1
  33. package/dist/components/empty/empty.js +20 -3
  34. package/dist/components/empty/empty.stories.d.ts +2 -0
  35. package/dist/components/empty/empty.stories.js +20 -0
  36. package/dist/components/empty/empty.styles.js +11 -0
  37. package/dist/components/export-button/csv.js +6 -2
  38. package/dist/components/export-button/export-button.d.ts +15 -1
  39. package/dist/components/export-button/export-button.js +89 -12
  40. package/dist/components/export-button/export-button.stories.d.ts +1 -0
  41. package/dist/components/export-button/export-button.stories.js +5 -0
  42. package/dist/components/export-button/export-button.styles.js +11 -3
  43. package/dist/components/file-input/accept.d.ts +8 -2
  44. package/dist/components/file-input/accept.js +9 -3
  45. package/dist/components/file-input/file-input.d.ts +5 -2
  46. package/dist/components/file-input/file-input.js +23 -8
  47. package/dist/components/file-input/file-input.stories.d.ts +2 -0
  48. package/dist/components/file-input/file-input.stories.js +9 -0
  49. package/dist/components/flag/flag.d.ts +48 -3
  50. package/dist/components/flag/flag.js +65 -29
  51. package/dist/components/flag/flag.styles.js +1 -1
  52. package/dist/components/flag/language-map.d.ts +11 -0
  53. package/dist/components/flag/language-map.js +12 -1
  54. package/dist/components/gauge/gauge.d.ts +2 -0
  55. package/dist/components/gauge/gauge.js +19 -5
  56. package/dist/components/gauge/gauge.stories.d.ts +2 -0
  57. package/dist/components/gauge/gauge.stories.js +6 -0
  58. package/dist/components/graph/graph-loader.d.ts +28 -0
  59. package/dist/components/graph/graph-loader.js +44 -0
  60. package/dist/components/graph/graph.d.ts +41 -15
  61. package/dist/components/graph/graph.js +146 -71
  62. package/dist/components/graph/graph.stories.d.ts +3 -0
  63. package/dist/components/graph/graph.stories.js +38 -0
  64. package/dist/components/graph/graph.styles.js +6 -1
  65. package/dist/components/heatmap/calendar-grid.d.ts +7 -0
  66. package/dist/components/heatmap/calendar-grid.js +27 -10
  67. package/dist/components/heatmap/heatmap.d.ts +137 -12
  68. package/dist/components/heatmap/heatmap.js +427 -27
  69. package/dist/components/heatmap/heatmap.stories.d.ts +17 -0
  70. package/dist/components/heatmap/heatmap.stories.js +130 -0
  71. package/dist/components/heatmap/heatmap.styles.js +60 -2
  72. package/dist/components/map/map.d.ts +17 -1
  73. package/dist/components/map/map.js +68 -21
  74. package/dist/components/map/map.stories.d.ts +10 -0
  75. package/dist/components/map/map.stories.js +107 -12
  76. package/dist/components/map/map.styles.js +2 -2
  77. package/dist/components/playback/playback.d.ts +6 -2
  78. package/dist/components/playback/playback.js +36 -12
  79. package/dist/components/playback/playback.stories.d.ts +2 -0
  80. package/dist/components/playback/playback.stories.js +6 -0
  81. package/dist/components/playback/playback.styles.js +10 -2
  82. package/dist/components/select/select.d.ts +122 -0
  83. package/dist/components/select/select.js +516 -0
  84. package/dist/components/select/select.stories.d.ts +19 -0
  85. package/dist/components/select/select.stories.js +75 -0
  86. package/dist/components/select/select.styles.d.ts +1 -0
  87. package/dist/components/select/select.styles.js +188 -0
  88. package/dist/components/skeleton/skeleton.js +4 -7
  89. package/dist/components/sparkline/sparkline.d.ts +1 -1
  90. package/dist/components/sparkline/sparkline.js +40 -13
  91. package/dist/components/sparkline/sparkline.stories.d.ts +4 -0
  92. package/dist/components/sparkline/sparkline.stories.js +22 -0
  93. package/dist/components/split/split.d.ts +24 -3
  94. package/dist/components/split/split.js +155 -31
  95. package/dist/components/split/split.stories.d.ts +2 -0
  96. package/dist/components/split/split.stories.js +20 -0
  97. package/dist/components/stat/stat.d.ts +27 -1
  98. package/dist/components/stat/stat.js +56 -3
  99. package/dist/components/stat/stat.stories.d.ts +3 -0
  100. package/dist/components/stat/stat.stories.js +49 -1
  101. package/dist/components/stat/stat.styles.js +44 -0
  102. package/dist/components/table/table.d.ts +64 -2
  103. package/dist/components/table/table.js +213 -17
  104. package/dist/components/table/table.stories.d.ts +6 -0
  105. package/dist/components/table/table.stories.js +45 -0
  106. package/dist/components/table/table.styles.js +51 -4
  107. package/dist/components/time-range/time-range.d.ts +34 -5
  108. package/dist/components/time-range/time-range.js +217 -36
  109. package/dist/components/time-range/time-range.stories.d.ts +2 -0
  110. package/dist/components/time-range/time-range.stories.js +13 -0
  111. package/dist/components/time-range/time-range.styles.js +44 -2
  112. package/dist/components/toast/toast-item.d.ts +14 -2
  113. package/dist/components/toast/toast-item.js +134 -17
  114. package/dist/components/toast/toast-item.styles.js +28 -2
  115. package/dist/components/toast/toast.stories.d.ts +3 -0
  116. package/dist/components/toast/toast.stories.js +41 -1
  117. package/dist/components/toast/toast.styles.js +11 -11
  118. package/dist/components/toast/toaster.d.ts +1 -1
  119. package/dist/components/toast/toaster.js +13 -6
  120. package/dist/components/tree/tree-node.d.ts +13 -7
  121. package/dist/components/tree/tree-node.js +24 -10
  122. package/dist/components/tree/tree.d.ts +64 -15
  123. package/dist/components/tree/tree.js +113 -22
  124. package/dist/components/tree/tree.stories.d.ts +4 -0
  125. package/dist/components/tree/tree.stories.js +31 -1
  126. package/dist/components/tree/update-cascade.d.ts +13 -0
  127. package/dist/components/tree/update-cascade.js +13 -0
  128. package/dist/components/widget/widget.d.ts +15 -1
  129. package/dist/components/widget/widget.js +140 -5
  130. package/dist/components/widget/widget.stories.d.ts +1 -0
  131. package/dist/components/widget/widget.stories.js +10 -0
  132. package/dist/components/widget/widget.styles.js +11 -1
  133. package/dist/components/word-cloud/word-cloud-layout.d.ts +63 -0
  134. package/dist/components/word-cloud/word-cloud-layout.js +145 -0
  135. package/dist/components/word-cloud/word-cloud.d.ts +65 -0
  136. package/dist/components/word-cloud/word-cloud.js +255 -0
  137. package/dist/components/word-cloud/word-cloud.stories.d.ts +10 -0
  138. package/dist/components/word-cloud/word-cloud.stories.js +55 -0
  139. package/dist/components/word-cloud/word-cloud.styles.d.ts +1 -0
  140. package/dist/components/word-cloud/word-cloud.styles.js +54 -0
  141. package/dist/internal/a11y.js +2 -1
  142. package/dist/internal/rtl.d.ts +7 -0
  143. package/dist/internal/rtl.js +9 -0
  144. package/dist/lyra.d.ts +9 -1
  145. package/dist/lyra.js +6 -0
  146. package/llms-full.txt +892 -276
  147. package/llms.txt +12 -9
  148. 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 }`, re-emitted from a child `<lyra-tree-node>`.
23
- * @event lyra-node-select - `detail: { id }`, re-emitted from a child `<lyra-tree-node>`.
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
- /** Unchanged from the existing implementation — by-id reconciliation of top-level items. */
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
- * `updated()` only pushes the new `activeId` to *top-level* nodes; a
43
- * nested node only receives it once its ancestor chain's own renders
44
- * cascade it down (one more pending update per depth level). Cascade
45
- * `updateComplete` itself to match: awaiting `<lyra-tree>`'s (or any
46
- * `<lyra-tree-node>`'s see the matching override there) `updateComplete`
47
- * now transitively waits for every visible descendant node's own update,
48
- * not just this element's. Without this, `focusNode()`'s `.focus()` call
49
- * can run while a nested target is still mid-cascade (no `tabindex`
50
- * attribute committed yet), and `.focus()` on an element with no
51
- * `tabindex` attribute at all is a silent no-op.
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. (unchanged) */
97
+ /** Expand every node in the tree, recursively. */
56
98
  expandAll(): void;
57
- /** Collapse every node in the tree. (unchanged) */
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 }`, re-emitted from a child `<lyra-tree-node>`.
26
- * @event lyra-node-select - `detail: { id }`, re-emitted from a child `<lyra-tree-node>`.
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 'ArrowRight':
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 ArrowRight steps into the first child
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 'ArrowLeft':
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
- /** Unchanged from the existing implementation — by-id reconciliation of top-level items. */
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; a
178
- * nested node only receives it once its ancestor chain's own renders
179
- * cascade it down (one more pending update per depth level). Cascade
180
- * `updateComplete` itself to match: awaiting `<lyra-tree>`'s (or any
181
- * `<lyra-tree-node>`'s see the matching override there) `updateComplete`
182
- * now transitively waits for every visible descendant node's own update,
183
- * not just this element's. Without this, `focusNode()`'s `.focus()` call
184
- * can run while a nested target is still mid-cascade (no `tabindex`
185
- * attribute committed yet), and `.focus()` on an element with no
186
- * `tabindex` attribute at all is a silent no-op.
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 Promise.all(this.nodeElements.map((n) => n.updateComplete));
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. (unchanged) */
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
- /** Collapse every node in the tree. (unchanged) */
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.expanded = false;
289
+ n.collapse();
209
290
  }
210
291
  };
211
292
  setAll(this.nodeElements);
212
293
  }
213
294
  render() {
214
295
  return html `
215
- <div part="base" role="tree" @keydown=${this.onTreeKeyDown}>
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, header, title, label, sublabel, actions, body, backdrop
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
  }