@mosaicajs/components 0.2.0 → 0.4.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 (52) hide show
  1. package/README.md +2 -0
  2. package/dist/aggregate.d.ts +4 -10
  3. package/dist/aggregate.js +7 -13
  4. package/dist/artifact-card.d.ts +21 -7
  5. package/dist/artifact-card.js +136 -79
  6. package/dist/base.d.ts +44 -5
  7. package/dist/base.js +54 -5
  8. package/dist/cluster-filter.d.ts +29 -25
  9. package/dist/cluster-filter.js +123 -106
  10. package/dist/colour-editor.d.ts +15 -4
  11. package/dist/colour-editor.js +107 -34
  12. package/dist/colour-picker.d.ts +12 -5
  13. package/dist/colour-picker.js +119 -32
  14. package/dist/display.js +77 -9
  15. package/dist/events.d.ts +21 -7
  16. package/dist/explorer.d.ts +118 -37
  17. package/dist/explorer.js +487 -212
  18. package/dist/field-card.d.ts +304 -48
  19. package/dist/field-card.js +1364 -244
  20. package/dist/filter-panel.d.ts +54 -10
  21. package/dist/filter-panel.js +242 -42
  22. package/dist/filter.d.ts +22 -4
  23. package/dist/filter.js +113 -34
  24. package/dist/hierarchy.d.ts +68 -45
  25. package/dist/hierarchy.js +320 -243
  26. package/dist/item-card.d.ts +62 -14
  27. package/dist/item-card.js +310 -110
  28. package/dist/key-picker.d.ts +2 -1
  29. package/dist/key-picker.js +4 -3
  30. package/dist/layer-picker.d.ts +4 -2
  31. package/dist/layer-picker.js +16 -7
  32. package/dist/lineage.d.ts +132 -0
  33. package/dist/lineage.js +307 -0
  34. package/dist/map.d.ts +135 -24
  35. package/dist/map.js +387 -117
  36. package/dist/mosaica-components.js +1848 -832
  37. package/dist/mosaica-components.js.sri +1 -1
  38. package/dist/mosaica-components.json +2 -2
  39. package/dist/parts.d.ts +4 -4
  40. package/dist/parts.js +18 -4
  41. package/dist/paths.d.ts +34 -12
  42. package/dist/paths.js +84 -46
  43. package/dist/selection.d.ts +22 -3
  44. package/dist/selection.js +125 -59
  45. package/dist/states.d.ts +72 -0
  46. package/dist/states.js +135 -26
  47. package/dist/status.d.ts +63 -5
  48. package/dist/status.js +215 -36
  49. package/dist/tokens.d.ts +8 -4
  50. package/dist/tokens.js +144 -14
  51. package/dist/view-picker.js +5 -0
  52. package/package.json +4 -3
package/dist/hierarchy.js CHANGED
@@ -34,11 +34,12 @@ var __runInitializers = (this && this.__runInitializers) || function (thisArg, i
34
34
  };
35
35
  import { css, html, nothing } from 'lit';
36
36
  import { property, state } from 'lit/decorators.js';
37
- import { artifactName, browsableLayers, isFilterLayer, withMember, withoutMember } from '@mosaicajs/client';
37
+ import { browsableLayers, isFilterLayer, withMember, withoutMember } from '@mosaicajs/client';
38
38
  import { refusalOf } from '@mosaicajs/client/internal';
39
- import { MosaicaElement, UNNAMED, emit, idString } from './base.js';
39
+ import { MosaicaElement, called, calledList, emit, focusLost, idString, labelOf } from './base.js';
40
40
  import { attachContextRoot, defineOnce } from './define.js';
41
41
  import { icon } from './icons.js';
42
+ import { Lineage, focusAdded, spokenCounts } from './lineage.js';
42
43
  import { renderState, stateOf } from './states.js';
43
44
  import { chrome, tokens } from './tokens.js';
44
45
  import './count.js';
@@ -52,10 +53,19 @@ const masked = (n) => ({ value: Number(n), exact: true });
52
53
  * hierarchy, filter layers among them. When there are several, a select switches between them.
53
54
  *
54
55
  * Each row shows how many of the artifact's items the viewer may see. While filters are set, it
55
- * also shows how many of those items match. A parent the viewer may not see is left out, so its
56
- * children appear at the top level.
56
+ * also shows how many of those items match, and a screen reader is given each count with what it
57
+ * counts. An artifact the viewer may not see is left out, and its children appear under its
58
+ * nearest ancestors the viewer may see, or at the top level where it has none. A row under several
59
+ * served parents names the others, and a parent no page has served yet is asked for by name. When
60
+ * the filters change, the rows are fetched again, a few requests at a time: as many pages of the
61
+ * roots and of each open row as More had loaded, since a page's cursor is good only for the
62
+ * filters it was served under. Focus stays on the control it was on.
57
63
  *
58
- * A row's buttons highlight or filter the map by its artifact. Its Fit button only fires
64
+ * A row's name is one line, cut with an ellipsis where it is too long, unless
65
+ * `--mosaica-long-text` is `normal`. A row's buttons highlight or filter the map by its artifact.
66
+ * They are shown in place of its counts while the pointer or focus is in the row, and Escape puts
67
+ * them away again; where there is no pointer to hover with, they stand beside the counts. Its Fit
68
+ * button only fires
59
69
  * `mosaica-artifactfit`, so you handle the event by passing its id to the map's `fitTo`. The
60
70
  * explorer does this for a hierarchy you put in one of its slots. `fitTo` finds only the artifacts
61
71
  * the map currently draws, so Fit does nothing for a row whose artifact is not drawn at this zoom.
@@ -70,31 +80,39 @@ const masked = (n) => ({ value: Number(n), exact: true });
70
80
  * @fires {CustomEvent<MosaicaEventDetails['mosaica-artifactfit']>} mosaica-artifactfit - A row's Fit
71
81
  * button was pressed.
72
82
  * @csspart title - The heading.
73
- * @csspart state - The state line, with `data-state`.
83
+ * @csspart state - The state line, with `data-state`, inside a `role="status"` region, which also
84
+ * says which rows are loading children and which were refused them.
74
85
  * @csspart refusal - The words "View refused" or "Layer unavailable", with `data-code`.
75
86
  * @csspart reason - The server's reason for a refusal, under "View refused".
76
87
  * @csspart layer - The layer select, shown if there are several layers.
77
88
  * @csspart search - The search box.
78
- * @csspart tree - The tree of rows.
79
- * @csspart row - One row, with `data-id`, and `data-clause` (`filter`, `highlight` or both,
80
- * separated by a space) while its artifact is filtered or highlighted.
81
- * @csspart expander - A row's expand button. On a row with no children it has `data-leaf` and is
82
- * hidden, keeping the names aligned.
83
- * @csspart name - A row's name, with `data-unnamed` if it has none. Pressing it highlights the
84
- * artifact.
85
- * @csspart counts - A row's counts.
89
+ * @csspart tree - The tree of rows, named by the layer.
90
+ * @csspart row - One row, with `data-id`, `data-clause` (`filter`, `highlight` or both, separated
91
+ * by a space) while its artifact is filtered or highlighted, and `data-buttons-away` once Escape has put
92
+ * its buttons away.
93
+ * @csspart expander - A row's expand button, with `aria-expanded` while its children are shown. On
94
+ * a row with no children it has `data-leaf` and is hidden, keeping the names aligned.
95
+ * @csspart name - A row's name. Where the artifact has none it is the artifact's key, with
96
+ * `data-key-name`, and where it has no key either it is the word Unnamed, with `data-unnamed`.
97
+ * Pressing it highlights the artifact, and it has `aria-pressed` while the artifact is
98
+ * highlighted.
99
+ * @csspart counts - A row's counts as they are drawn, hidden from a screen reader, which is given
100
+ * them beside it with what each counts. They are not drawn while the row's buttons are shown
101
+ * over them.
86
102
  * @csspart count-matched - The `<mosaica-count>` of a row's items that match the filters, while a
87
103
  * filter is set.
88
104
  * @csspart count-masked - The `<mosaica-count>` of a row's items the viewer may see.
89
- * @csspart actions - A row's buttons.
105
+ * @csspart actions - A row's buttons, shown over its counts while the pointer or focus is in the
106
+ * row.
90
107
  * @csspart highlight - A row's highlight button, with `aria-pressed`.
91
108
  * @csspart filter - A row's filter button, with `aria-pressed`.
92
109
  * @csspart fit - A row's Fit button, on a layer that draws.
93
110
  * @csspart dismiss - A × that removes a row's filter or highlight, one for each, with `data-verb`.
94
- * @csspart also - The row's other parents, or "Children unavailable" with `data-code` if its
95
- * children were refused.
111
+ * The highlight's has a bar under it.
112
+ * @csspart also - The row's other parents, each with `data-key-name` where it is called by its key,
113
+ * or "Children unavailable" with `data-code` if its children were refused.
96
114
  * @csspart children - An expanded row's children.
97
- * @csspart more - The More button, which loads the next page.
115
+ * @csspart more - The More button, which loads the next page and moves focus to its first row.
98
116
  */
99
117
  let MosaicaHierarchy = (() => {
100
118
  let _classSuper = MosaicaElement;
@@ -104,30 +122,21 @@ let MosaicaHierarchy = (() => {
104
122
  let _limit_decorators;
105
123
  let _limit_initializers = [];
106
124
  let _limit_extraInitializers = [];
107
- let _roots_decorators;
108
- let _roots_initializers = [];
109
- let _roots_extraInitializers = [];
110
- let _rootsNext_decorators;
111
- let _rootsNext_initializers = [];
112
- let _rootsNext_extraInitializers = [];
113
125
  let _query_decorators;
114
126
  let _query_initializers = [];
115
127
  let _query_extraInitializers = [];
116
128
  let _searching_decorators;
117
129
  let _searching_initializers = [];
118
130
  let _searching_extraInitializers = [];
119
- let _loading_decorators;
120
- let _loading_initializers = [];
121
- let _loading_extraInitializers = [];
122
- let _refusal_decorators;
123
- let _refusal_initializers = [];
124
- let _refusal_extraInitializers = [];
131
+ let _searchRefusal_decorators;
132
+ let _searchRefusal_initializers = [];
133
+ let _searchRefusal_extraInitializers = [];
125
134
  let _open_decorators;
126
135
  let _open_initializers = [];
127
136
  let _open_extraInitializers = [];
128
- let _revision_decorators;
129
- let _revision_initializers = [];
130
- let _revision_extraInitializers = [];
137
+ let _buttonsAway_decorators;
138
+ let _buttonsAway_initializers = [];
139
+ let _buttonsAway_extraInitializers = [];
131
140
  let _shown_decorators;
132
141
  let _shown_initializers = [];
133
142
  let _shown_extraInitializers = [];
@@ -136,25 +145,19 @@ let MosaicaHierarchy = (() => {
136
145
  const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
137
146
  _layer_decorators = [property()];
138
147
  _limit_decorators = [property({ type: Number })];
139
- _roots_decorators = [state()];
140
- _rootsNext_decorators = [state()];
141
148
  _query_decorators = [state()];
142
149
  _searching_decorators = [state()];
143
- _loading_decorators = [state()];
144
- _refusal_decorators = [state()];
150
+ _searchRefusal_decorators = [state()];
145
151
  _open_decorators = [state()];
146
- _revision_decorators = [state()];
152
+ _buttonsAway_decorators = [state()];
147
153
  _shown_decorators = [state()];
148
154
  __esDecorate(this, null, _layer_decorators, { kind: "accessor", name: "layer", static: false, private: false, access: { has: obj => "layer" in obj, get: obj => obj.layer, set: (obj, value) => { obj.layer = value; } }, metadata: _metadata }, _layer_initializers, _layer_extraInitializers);
149
155
  __esDecorate(this, null, _limit_decorators, { kind: "accessor", name: "limit", static: false, private: false, access: { has: obj => "limit" in obj, get: obj => obj.limit, set: (obj, value) => { obj.limit = value; } }, metadata: _metadata }, _limit_initializers, _limit_extraInitializers);
150
- __esDecorate(this, null, _roots_decorators, { kind: "accessor", name: "roots", static: false, private: false, access: { has: obj => "roots" in obj, get: obj => obj.roots, set: (obj, value) => { obj.roots = value; } }, metadata: _metadata }, _roots_initializers, _roots_extraInitializers);
151
- __esDecorate(this, null, _rootsNext_decorators, { kind: "accessor", name: "rootsNext", static: false, private: false, access: { has: obj => "rootsNext" in obj, get: obj => obj.rootsNext, set: (obj, value) => { obj.rootsNext = value; } }, metadata: _metadata }, _rootsNext_initializers, _rootsNext_extraInitializers);
152
156
  __esDecorate(this, null, _query_decorators, { kind: "accessor", name: "query", static: false, private: false, access: { has: obj => "query" in obj, get: obj => obj.query, set: (obj, value) => { obj.query = value; } }, metadata: _metadata }, _query_initializers, _query_extraInitializers);
153
157
  __esDecorate(this, null, _searching_decorators, { kind: "accessor", name: "searching", static: false, private: false, access: { has: obj => "searching" in obj, get: obj => obj.searching, set: (obj, value) => { obj.searching = value; } }, metadata: _metadata }, _searching_initializers, _searching_extraInitializers);
154
- __esDecorate(this, null, _loading_decorators, { kind: "accessor", name: "loading", static: false, private: false, access: { has: obj => "loading" in obj, get: obj => obj.loading, set: (obj, value) => { obj.loading = value; } }, metadata: _metadata }, _loading_initializers, _loading_extraInitializers);
155
- __esDecorate(this, null, _refusal_decorators, { kind: "accessor", name: "refusal", static: false, private: false, access: { has: obj => "refusal" in obj, get: obj => obj.refusal, set: (obj, value) => { obj.refusal = value; } }, metadata: _metadata }, _refusal_initializers, _refusal_extraInitializers);
158
+ __esDecorate(this, null, _searchRefusal_decorators, { kind: "accessor", name: "searchRefusal", static: false, private: false, access: { has: obj => "searchRefusal" in obj, get: obj => obj.searchRefusal, set: (obj, value) => { obj.searchRefusal = value; } }, metadata: _metadata }, _searchRefusal_initializers, _searchRefusal_extraInitializers);
156
159
  __esDecorate(this, null, _open_decorators, { kind: "accessor", name: "open", static: false, private: false, access: { has: obj => "open" in obj, get: obj => obj.open, set: (obj, value) => { obj.open = value; } }, metadata: _metadata }, _open_initializers, _open_extraInitializers);
157
- __esDecorate(this, null, _revision_decorators, { kind: "accessor", name: "revision", static: false, private: false, access: { has: obj => "revision" in obj, get: obj => obj.revision, set: (obj, value) => { obj.revision = value; } }, metadata: _metadata }, _revision_initializers, _revision_extraInitializers);
160
+ __esDecorate(this, null, _buttonsAway_decorators, { kind: "accessor", name: "buttonsAway", static: false, private: false, access: { has: obj => "buttonsAway" in obj, get: obj => obj.buttonsAway, set: (obj, value) => { obj.buttonsAway = value; } }, metadata: _metadata }, _buttonsAway_initializers, _buttonsAway_extraInitializers);
158
161
  __esDecorate(this, null, _shown_decorators, { kind: "accessor", name: "shown", static: false, private: false, access: { has: obj => "shown" in obj, get: obj => obj.shown, set: (obj, value) => { obj.shown = value; } }, metadata: _metadata }, _shown_initializers, _shown_extraInitializers);
159
162
  if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
160
163
  }
@@ -172,15 +175,15 @@ let MosaicaHierarchy = (() => {
172
175
  [part='search'] {
173
176
  margin-bottom: 8px;
174
177
  }
178
+ /* The box reaches 3 px left of the rows, so the expander's target at a row's edge is whole. */
175
179
  [part='tree'] {
176
180
  list-style: none;
177
- margin: 0;
178
- padding: 0;
181
+ margin: 0 0 0 -3px;
182
+ padding: 0 0 0 3px;
179
183
  max-height: 320px;
180
184
  overflow-y: auto;
181
185
  }
182
186
  [part='row'] {
183
- position: relative;
184
187
  display: flex;
185
188
  align-items: center;
186
189
  gap: 6px;
@@ -199,19 +202,36 @@ let MosaicaHierarchy = (() => {
199
202
  background: var(--_mosaica-highlight-soft);
200
203
  color: var(--_mosaica-highlight);
201
204
  }
205
+ /* Drawn 14 px wide. The pointer's target is 24 px square around it, in the row's own room. */
202
206
  [part='expander'] {
207
+ position: relative;
203
208
  display: inline-flex;
209
+ align-items: center;
204
210
  width: 14px;
211
+ height: 24px;
212
+ flex: none;
205
213
  color: var(--_mosaica-ink-3);
206
214
  }
215
+ [part='expander']::before {
216
+ content: '';
217
+ position: absolute;
218
+ inset: 0 -5px;
219
+ }
220
+ [part='expander']:focus-visible {
221
+ outline-offset: 0;
222
+ }
207
223
  [part='expander'][data-leaf] {
208
224
  visibility: hidden;
209
225
  }
226
+ /* One line, cut with an ellipsis, unless the host asks for long names to wrap. */
210
227
  [part='name'] {
211
228
  flex: 1;
229
+ min-width: 0;
230
+ min-height: 24px;
212
231
  overflow: hidden;
213
232
  text-overflow: ellipsis;
214
- white-space: nowrap;
233
+ white-space: var(--_mosaica-long-text);
234
+ overflow-wrap: anywhere;
215
235
  text-align: left;
216
236
  }
217
237
  [part='counts'] {
@@ -221,28 +241,46 @@ let MosaicaHierarchy = (() => {
221
241
  font-size: 12px;
222
242
  color: var(--_mosaica-ink-2);
223
243
  }
224
- /* Over the counts, so hidden buttons take no width from the name; still reachable by Tab. */
244
+ /* The counts and the row's buttons. Where there is no pointer to hover with, side by side. */
245
+ .figures {
246
+ position: relative;
247
+ display: inline-flex;
248
+ align-items: center;
249
+ gap: 4px;
250
+ flex: none;
251
+ }
225
252
  [part='actions'] {
226
- position: absolute;
227
- right: 2px;
228
- top: 50%;
229
- transform: translateY(-50%);
230
253
  display: inline-flex;
231
- gap: 2px;
232
- padding-left: 6px;
233
- background: var(--_mosaica-surface-2);
234
- border-radius: var(--_mosaica-radius-control);
235
- opacity: 0;
236
- pointer-events: none;
237
254
  }
238
- [part='row']:hover [part='actions'],
239
- [part='row']:focus-within [part='actions'] {
240
- opacity: 1;
241
- pointer-events: auto;
255
+ /* With a pointer, the buttons take no room: they are shown over the counts, which are not
256
+ drawn meanwhile, and over the end of a name beside short counts, while the pointer or
257
+ focus is in the row and Escape has not put them away. They are above the counts, which
258
+ come after them and, not drawn, would still lie over them. */
259
+ @media (hover: hover) {
260
+ [part='actions'] {
261
+ position: absolute;
262
+ right: 0;
263
+ z-index: 1;
264
+ padding-left: 6px;
265
+ background: var(--_mosaica-surface-2);
266
+ border-radius: var(--_mosaica-radius-control);
267
+ opacity: 0;
268
+ pointer-events: none;
269
+ }
270
+ [part='row']:not([data-buttons-away]):is(:hover, :focus-within) [part='actions'] {
271
+ opacity: 1;
272
+ pointer-events: auto;
273
+ }
274
+ [part='row']:not([data-buttons-away]):is(:hover, :focus-within) [part='counts'] {
275
+ opacity: 0;
276
+ pointer-events: none;
277
+ }
242
278
  }
243
279
  [part='actions'] button {
244
- display: inline-flex;
245
- padding: 3px;
280
+ display: inline-grid;
281
+ place-items: center;
282
+ width: 24px;
283
+ height: 24px;
246
284
  color: var(--_mosaica-ink-2);
247
285
  border-radius: 4px;
248
286
  }
@@ -250,7 +288,14 @@ let MosaicaHierarchy = (() => {
250
288
  color: var(--_mosaica-ink);
251
289
  background: var(--_mosaica-surface-3);
252
290
  }
291
+ [part='actions'] button[aria-pressed='true'] {
292
+ color: var(--_mosaica-ink);
293
+ background: var(--_mosaica-surface-3);
294
+ box-shadow: inset 0 -2px 0 currentColor;
295
+ }
296
+ /* Drawn 20 px square, with a 24 px target around it. */
253
297
  [part='dismiss'] {
298
+ position: relative;
254
299
  display: inline-grid;
255
300
  place-items: center;
256
301
  width: 20px;
@@ -259,8 +304,15 @@ let MosaicaHierarchy = (() => {
259
304
  border-radius: 4px;
260
305
  color: var(--_mosaica-ink-2);
261
306
  }
307
+ [part='dismiss']::before {
308
+ content: '';
309
+ position: absolute;
310
+ inset: -2px;
311
+ }
312
+ /* The × that removes a highlight has a bar under it, so it differs from the filter's by more than its colour. */
262
313
  [part='dismiss'][data-verb='highlight'] {
263
314
  color: var(--_mosaica-highlight);
315
+ box-shadow: inset 0 -2px 0 currentColor;
264
316
  }
265
317
  [part='dismiss']:hover {
266
318
  background: var(--_mosaica-surface-3);
@@ -270,9 +322,25 @@ let MosaicaHierarchy = (() => {
270
322
  font-size: 11px;
271
323
  color: var(--_mosaica-ink-3);
272
324
  }
325
+ [part='also'] [data-key-name] {
326
+ color: inherit;
327
+ }
328
+ /* Drawn as a line of text, with a target 24 px high and at least as wide around it. */
273
329
  [part='more'] {
330
+ position: relative;
274
331
  margin: 4px 0 4px calc(18px + var(--depth, 0) * 14px);
275
332
  }
333
+ [part='more']::before {
334
+ content: '';
335
+ position: absolute;
336
+ inset: -4px 0;
337
+ min-width: 24px;
338
+ }
339
+ [part='children'] {
340
+ list-style: none;
341
+ margin: 0;
342
+ padding: 0;
343
+ }
276
344
  [part='refusal'] {
277
345
  display: flex;
278
346
  align-items: center;
@@ -289,52 +357,42 @@ let MosaicaHierarchy = (() => {
289
357
  /** Rows per page, from 1 up to the server's `maxBrowseRows`. */
290
358
  get limit() { return this.#limit_accessor_storage; }
291
359
  set limit(value) { this.#limit_accessor_storage = value; }
292
- #roots_accessor_storage = (__runInitializers(this, _limit_extraInitializers), __runInitializers(this, _roots_initializers, null));
293
- get roots() { return this.#roots_accessor_storage; }
294
- set roots(value) { this.#roots_accessor_storage = value; }
295
- #rootsNext_accessor_storage = (__runInitializers(this, _roots_extraInitializers), __runInitializers(this, _rootsNext_initializers, null));
296
- get rootsNext() { return this.#rootsNext_accessor_storage; }
297
- set rootsNext(value) { this.#rootsNext_accessor_storage = value; }
298
- #query_accessor_storage = (__runInitializers(this, _rootsNext_extraInitializers), __runInitializers(this, _query_initializers, ''));
360
+ #query_accessor_storage = (__runInitializers(this, _limit_extraInitializers), __runInitializers(this, _query_initializers, ''));
299
361
  get query() { return this.#query_accessor_storage; }
300
362
  set query(value) { this.#query_accessor_storage = value; }
301
363
  #searching_accessor_storage = (__runInitializers(this, _query_extraInitializers), __runInitializers(this, _searching_initializers, null));
302
364
  get searching() { return this.#searching_accessor_storage; }
303
365
  set searching(value) { this.#searching_accessor_storage = value; }
304
- #loading_accessor_storage = (__runInitializers(this, _searching_extraInitializers), __runInitializers(this, _loading_initializers, false));
305
- get loading() { return this.#loading_accessor_storage; }
306
- set loading(value) { this.#loading_accessor_storage = value; }
307
- #refusal_accessor_storage = (__runInitializers(this, _loading_extraInitializers), __runInitializers(this, _refusal_initializers, null));
308
- get refusal() { return this.#refusal_accessor_storage; }
309
- set refusal(value) { this.#refusal_accessor_storage = value; }
310
- #open_accessor_storage = (__runInitializers(this, _refusal_extraInitializers), __runInitializers(this, _open_initializers, new Set()));
366
+ #searchRefusal_accessor_storage = (__runInitializers(this, _searching_extraInitializers), __runInitializers(this, _searchRefusal_initializers, null));
367
+ /** Why the last search was refused. */
368
+ get searchRefusal() { return this.#searchRefusal_accessor_storage; }
369
+ set searchRefusal(value) { this.#searchRefusal_accessor_storage = value; }
370
+ #open_accessor_storage = (__runInitializers(this, _searchRefusal_extraInitializers), __runInitializers(this, _open_initializers, new Set()));
311
371
  /** Which paths are open, so a re-render of the tree keeps the walk. */
312
372
  get open() { return this.#open_accessor_storage; }
313
373
  set open(value) { this.#open_accessor_storage = value; }
314
- #revision_accessor_storage = (__runInitializers(this, _open_extraInitializers), __runInitializers(this, _revision_initializers, 0));
315
- /** Bumped to re-render after a node's children land, since the tree in `roots` is mutated. */
316
- get revision() { return this.#revision_accessor_storage; }
317
- set revision(value) { this.#revision_accessor_storage = value; }
318
- /** The question the roots were fetched under; see {@link question}. */
319
- fetchedUnder = (__runInitializers(this, _revision_extraInitializers), '');
320
- /**
321
- * Every name the walk has seen, by identifier. A filter layer's artifacts are never served in a
322
- * viewport, so this is the client's only source of their names.
323
- */
324
- names = new Map();
374
+ #buttonsAway_accessor_storage = (__runInitializers(this, _open_extraInitializers), __runInitializers(this, _buttonsAway_initializers, new Set()));
375
+ /** The paths of the rows whose buttons Escape has put away, each until the pointer and focus leave it. */
376
+ get buttonsAway() { return this.#buttonsAway_accessor_storage; }
377
+ set buttonsAway(value) { this.#buttonsAway_accessor_storage = value; }
378
+ /** The browse pages of the current layer: its roots and the children of the rows opened. */
379
+ lineage = (__runInitializers(this, _buttonsAway_extraInitializers), new Lineage({
380
+ store: () => this.resolvedStore,
381
+ layer: () => this.current(),
382
+ limit: () => this.limit,
383
+ isOpen: (path) => this.open.has(path),
384
+ changed: () => this.requestUpdate()
385
+ }));
325
386
  searchTimer = null;
326
- /** Moved by {@link resetServerData}, so a page asked for before it is dropped. */
387
+ /** Moved by {@link resetServerData}, so a search asked for before it is dropped. */
327
388
  epoch = 0;
328
389
  resetServerData() {
329
390
  this.epoch += 1;
330
- this.roots = null;
331
- this.rootsNext = null;
391
+ this.lineage.reset();
332
392
  this.searching = null;
333
- this.loading = false;
334
- this.refusal = null;
393
+ this.searchRefusal = null;
335
394
  this.open = new Set();
336
- this.names.clear();
337
- this.fetchedUnder = '';
395
+ this.buttonsAway = new Set();
338
396
  if (this.searchTimer)
339
397
  clearTimeout(this.searchTimer);
340
398
  this.searchTimer = null;
@@ -347,15 +405,6 @@ let MosaicaHierarchy = (() => {
347
405
  const offered = this.layers();
348
406
  return offered.find((l) => l.name === this.layer) ?? offered[0] ?? null;
349
407
  }
350
- /**
351
- * The question the panel is asking, as a string: the layer and the map's filter. A change
352
- * refetches the roots and drops the walk, whose counts answer the old question.
353
- */
354
- question() {
355
- // Hashed from `requestFilters()`, what `store.browse` sends, which includes the drawn region.
356
- // A `member_of` leaf holds its artifact as a decimal string, so it serialises.
357
- return `${this.current()?.name ?? ''}|${JSON.stringify(this.resolvedStore?.requestFilters() ?? null)}`;
358
- }
359
408
  #shown_accessor_storage = __runInitializers(this, _shown_initializers, false);
360
409
  /**
361
410
  * Whether the panel is shown. A hidden panel sends no request, so a panel in a closed drawer
@@ -369,6 +418,7 @@ let MosaicaHierarchy = (() => {
369
418
  observer = (__runInitializers(this, _shown_extraInitializers), null);
370
419
  connectedCallback() {
371
420
  super.connectedCallback();
421
+ document.addEventListener('keydown', this.onEscape);
372
422
  if (typeof IntersectionObserver === 'undefined')
373
423
  return;
374
424
  this.observer ??= new IntersectionObserver((entries) => {
@@ -380,123 +430,128 @@ let MosaicaHierarchy = (() => {
380
430
  this.observer.observe(this);
381
431
  }
382
432
  disconnectedCallback() {
433
+ document.removeEventListener('keydown', this.onEscape);
383
434
  this.observer?.disconnect();
384
435
  this.observer = null;
385
436
  super.disconnectedCallback();
386
437
  }
438
+ /**
439
+ * Escape puts away the buttons shown over a row's counts, wherever focus is, since the pointer
440
+ * alone shows them. Focus on one of them goes to the row's name.
441
+ */
442
+ onEscape = (e) => {
443
+ if (e.key !== 'Escape')
444
+ return;
445
+ const active = this.shadowRoot?.activeElement ?? null;
446
+ const focused = active?.closest('[part="row"]') ?? null;
447
+ const shown = [focused, this.renderRoot.querySelector('[part="row"]:hover')].filter((row) => row !== null && !this.buttonsAway.has(row.path));
448
+ if (shown.length === 0)
449
+ return;
450
+ this.buttonsAway = new Set([...this.buttonsAway, ...shown.map((row) => row.path)]);
451
+ if (active?.closest('[part="actions"]'))
452
+ focused?.querySelector('[part="name"]')?.focus();
453
+ };
454
+ /** The buttons Escape put away come back once the pointer and focus have both left the row. */
455
+ leftRow(e, path) {
456
+ if (!this.buttonsAway.has(path))
457
+ return;
458
+ const row = e.currentTarget;
459
+ const to = e.type === 'focusout' ? e.relatedTarget : (this.shadowRoot?.activeElement ?? null);
460
+ if (row.contains(to) || (e.type === 'focusout' && row.matches(':hover')))
461
+ return;
462
+ this.shownAgain(path);
463
+ }
464
+ /** Let a row show its buttons again. */
465
+ shownAgain(path) {
466
+ if (this.buttonsAway.has(path))
467
+ this.buttonsAway = new Set([...this.buttonsAway].filter((p) => p !== path));
468
+ }
387
469
  /** Whether this element is displayed; see {@link shown}. */
388
470
  displayed() {
389
471
  return typeof this.checkVisibility === 'function' ? this.checkVisibility() : true;
390
472
  }
391
473
  /** The roots, where the question they were fetched under is no longer the one being asked. */
392
474
  loadRootsIfStale() {
393
- const q = this.question();
394
- if (this.shown && q !== this.fetchedUnder && this.current()) {
395
- this.fetchedUnder = q;
396
- void this.loadRoots();
397
- }
475
+ if (this.shown)
476
+ this.lineage.loadIfStale();
398
477
  }
399
478
  onStoreChange() {
400
479
  this.loadRootsIfStale();
401
480
  super.onStoreChange();
402
481
  }
482
+ /** The control focus was on as the last render began. */
483
+ held = null;
403
484
  /** Re-asked before every render, so putting the panel away and taking it out again both count. */
404
485
  willUpdate(changed) {
405
486
  super.willUpdate(changed);
406
487
  if (!this.shown && this.displayed())
407
488
  this.shown = true;
489
+ this.held = this.heldControl();
408
490
  }
409
491
  updated() {
410
- if (this.shown && this.roots === null && this.current() && this.resolvedStore && !this.loading && this.fetchedUnder === '') {
411
- this.fetchedUnder = this.question();
412
- void this.loadRoots();
413
- }
414
- }
415
- /** One page of the current layer, under the map's filter, which the store supplies. */
416
- page(req) {
417
- const s = this.resolvedStore;
418
- const layer = this.current();
419
- if (!s || !layer)
420
- return null;
421
- const ceiling = s.get('meta')?.selection.maxBrowseRows ?? this.limit;
422
- return s.browse({ ...req, layer: layer.name, limit: Math.max(1, Math.min(this.limit, ceiling)) });
423
- }
424
- async loadRoots(cursor) {
425
- const request = this.page(cursor === undefined ? {} : { cursor });
426
- if (!request)
427
- return;
428
- const epoch = this.epoch;
429
- this.loading = true;
430
- this.refusal = null;
431
- try {
432
- const page = await request;
433
- if (epoch !== this.epoch)
434
- return;
435
- const nodes = page.artifacts.map((row) => this.node(row, ''));
436
- this.roots = cursor === undefined ? nodes : [...(this.roots ?? []), ...nodes];
437
- this.rootsNext = page.next;
438
- }
439
- catch (error) {
440
- if (epoch !== this.epoch)
441
- return;
442
- this.refusal = refusalOf(error);
443
- this.roots = this.roots ?? [];
444
- }
445
- finally {
446
- if (epoch === this.epoch)
447
- this.loading = false;
448
- }
449
- }
450
- node(row, parentPath) {
451
- const name = artifactName(row);
452
- if (name !== null)
453
- this.names.set(row.mosaicaId, name);
454
- return { row, path: `${parentPath}/${row.mosaicaId}`, children: null, next: null, loading: false, refusal: null };
455
- }
456
- /** What to call an artifact: its name where the walk has met one. */
457
- nameOf(id) {
458
- return this.names.get(id) ?? UNNAMED;
459
- }
460
- /**
461
- * Expand a node: its children, paged. A node keeps what it fetched while {@link question} is
462
- * unchanged, so reopening it fetches nothing.
463
- */
464
- async expand(node, more = false) {
465
- const request = this.page({ parent: node.row.mosaicaId, ...(more && node.next ? { cursor: node.next } : {}) });
466
- if (!request)
467
- return;
468
- const epoch = this.epoch;
469
- node.loading = true;
470
- node.refusal = null;
471
- this.revision++;
472
- try {
473
- const page = await request;
474
- if (epoch !== this.epoch)
475
- return;
476
- const nodes = page.artifacts.map((row) => this.node(row, node.path));
477
- node.children = more ? [...(node.children ?? []), ...nodes] : nodes;
478
- node.next = page.next;
479
- }
480
- catch (error) {
481
- node.refusal = refusalOf(error);
482
- node.children = node.children ?? [];
483
- }
484
- finally {
485
- node.loading = false;
486
- this.revision++;
487
- }
492
+ if (this.held)
493
+ this.keepFocus(this.held);
494
+ this.loadRootsIfStale();
488
495
  }
496
+ /** Open a row, fetching its children where it has none yet, or close one that is open or opening. */
489
497
  toggle(node) {
490
498
  const open = new Set(this.open);
491
- if (open.has(node.path))
499
+ if (open.has(node.path) && (node.children !== null || node.loading))
492
500
  open.delete(node.path);
493
501
  else {
494
502
  open.add(node.path);
495
503
  if (node.children === null && !node.loading)
496
- void this.expand(node);
504
+ void this.lineage.expand(node);
497
505
  }
498
506
  this.open = open;
499
507
  }
508
+ /**
509
+ * Load the next page of the roots, or of `parent`'s children, and move focus from the More
510
+ * button, which the page replaces or removes, to the first row it added. Focus that has gone
511
+ * anywhere else meanwhile stays there.
512
+ */
513
+ async more(button, parent) {
514
+ const added = await (parent ? this.lineage.expand(parent, true) : this.lineage.loadMore());
515
+ const first = added[0];
516
+ if (first)
517
+ await focusAdded(this, button, () => this.rowElements().find((r) => r.path === first.path)?.querySelector('[part="name"]'));
518
+ }
519
+ rowElements() {
520
+ return Array.from(this.renderRoot.querySelectorAll('[part="row"]'));
521
+ }
522
+ /** The control in a row that focus is on, read before a render so {@link keepFocus} can put it back. */
523
+ heldControl() {
524
+ const active = this.shadowRoot?.activeElement;
525
+ const row = active?.closest('[part="row"]');
526
+ const part = active?.getAttribute('part');
527
+ if (!active || !row || !part)
528
+ return null;
529
+ return { path: row.path, at: this.rowElements().indexOf(row), part, verb: active.getAttribute('data-verb') };
530
+ }
531
+ /**
532
+ * After a render, put focus back on the control it was on, in the row it was on. Where a ×
533
+ * removed itself, focus goes to the row's other ×, else to the button that makes the clause.
534
+ * Where the row is gone, it goes to the row now in its place, else the last, else the search box.
535
+ */
536
+ keepFocus(held) {
537
+ const rows = this.rowElements();
538
+ const active = this.shadowRoot?.activeElement ?? null;
539
+ // Focus is still in the tree, or the render removed the control that had it.
540
+ if (!active && !focusLost())
541
+ return;
542
+ const same = rows.find((r) => r.path === held.path);
543
+ const row = same ?? (active ? null : rows[Math.min(held.at, rows.length - 1)]);
544
+ if (!row) {
545
+ if (!active)
546
+ this.renderRoot.querySelector('[part="search"] input')?.focus();
547
+ return;
548
+ }
549
+ const exact = row.querySelector(`[part="${held.part}"]${held.verb ? `[data-verb="${held.verb}"]` : ''}`);
550
+ const other = held.part === 'dismiss' ? (row.querySelector('[part="dismiss"]') ?? row.querySelector(`[part="actions"] [data-verb="${held.verb}"]`)) : null;
551
+ const to = exact ?? other ?? row.querySelector('[part="name"]');
552
+ if (to !== active)
553
+ to?.focus();
554
+ }
500
555
  search(text) {
501
556
  this.query = text;
502
557
  if (this.searchTimer)
@@ -507,21 +562,24 @@ let MosaicaHierarchy = (() => {
507
562
  const q = text.trim();
508
563
  if (q.length === 0) {
509
564
  this.searching = null;
565
+ this.searchRefusal = null;
510
566
  return;
511
567
  }
512
- const request = this.page({ q });
568
+ const request = this.lineage.search(q);
513
569
  if (!request)
514
570
  return;
515
571
  const epoch = this.epoch;
516
572
  void request
517
- .then((page) => {
518
- if (epoch === this.epoch)
519
- this.searching = page.artifacts.map((row) => this.node(row, 'q'));
573
+ .then((nodes) => {
574
+ if (epoch !== this.epoch)
575
+ return;
576
+ this.searching = nodes;
577
+ this.searchRefusal = null;
520
578
  })
521
579
  .catch((error) => {
522
580
  if (epoch !== this.epoch)
523
581
  return;
524
- this.refusal = refusalOf(error);
582
+ this.searchRefusal = refusalOf(error);
525
583
  this.searching = [];
526
584
  });
527
585
  }, 250);
@@ -539,12 +597,16 @@ let MosaicaHierarchy = (() => {
539
597
  return;
540
598
  const held = s.get('filters').members;
541
599
  const on = this.clausesOn(id).includes(verb);
542
- const label = this.names.get(id);
543
- s.setMembers(on
544
- ? withoutMember(held, layer.name, id, verb)
545
- : withMember(held, { layer: layer.name, artifact: id, outside: false, verb, ...(label === undefined ? {} : { label }) }));
600
+ s.setMembers(on ? withoutMember(held, layer.name, id, verb) : withMember(held, { layer: layer.name, artifact: id, outside: false, verb, ...labelOf(this.lineage.nameOf(id)) }));
546
601
  emit(this, 'mosaica-clausechange', { id: idString(id), layer: layer.name, outside: false, verb, on: !on });
547
602
  }
603
+ /** What the rows' own fetches say, for the status region: the rows loading children, and those refused them. */
604
+ rowNotes(nodes) {
605
+ return nodes.flatMap((n) => {
606
+ const name = called(n.row.name, n.row.key).text;
607
+ return [...(n.loading ? [`Loading children of ${name}`] : []), ...(n.refusal ? [`Children of ${name} unavailable`] : []), ...this.rowNotes(n.children ?? [])];
608
+ });
609
+ }
548
610
  render() {
549
611
  const s = this.resolvedStore;
550
612
  const heading = html `<h2 part="title">Hierarchy</h2>`;
@@ -555,14 +617,26 @@ let MosaicaHierarchy = (() => {
555
617
  if (offered.length === 0)
556
618
  return html `<div class="panel">${heading}<span part="state" data-state="empty">No hierarchical layers</span></div>`;
557
619
  const layer = this.current();
558
- const filtered = s.get('filters').expr !== null || s.get('filters').members.length > 0;
559
- const rows = this.searching ?? this.roots;
620
+ const title = layer.title || layer.name;
621
+ const { roots, loading } = this.lineage;
622
+ const refusal = this.searchRefusal ?? this.lineage.refusal;
623
+ const rows = this.searching ?? roots;
624
+ const state = refusal ? 'refused' : rows === null ? (loading ? 'loading' : 'shown') : rows.length === 0 ? 'empty' : 'shown';
625
+ const said = refusal
626
+ ? html `<span part="refusal" data-code=${refusal.code}><span class="dot refuse"></span>Layer unavailable</span>`
627
+ : state === 'loading'
628
+ ? html `<span class="sr-only">Loading</span>`
629
+ : state === 'empty'
630
+ ? this.searching
631
+ ? 'No match'
632
+ : 'Nothing here'
633
+ : nothing;
634
+ const notes = this.rowNotes(rows ?? []);
560
635
  return html `<div class="panel">${heading}
561
- <span part="state" data-state=${this.refusal ? 'refused' : this.loading && rows === null ? 'loading' : 'shown'}></span>
562
636
  ${offered.length > 1
563
637
  ? html `<select part="layer" aria-label="Hierarchy layer" .value=${layer.name} @change=${(e) => {
564
638
  this.layer = e.target.value;
565
- this.roots = null;
639
+ this.lineage.clear();
566
640
  this.searching = null;
567
641
  this.open = new Set();
568
642
  }}>
@@ -571,66 +645,69 @@ let MosaicaHierarchy = (() => {
571
645
  : nothing}
572
646
  <div part="search" class="input">${icon('search', 14)}<input
573
647
  type="search"
574
- aria-label=${`Search ${layer.title || layer.name}`}
648
+ aria-label=${`Search ${title}`}
575
649
  .value=${this.query}
576
650
  placeholder="Search names"
577
651
  autocomplete="off"
578
652
  @input=${(e) => this.search(e.target.value)}
579
653
  /></div>
580
- ${this.refusal ? html `<span part="refusal" data-code=${this.refusal.code}><span class="dot refuse"></span>Layer unavailable</span>` : nothing}
581
- ${rows === null || (this.refusal && rows.length === 0)
582
- ? nothing
583
- : rows.length === 0
584
- ? html `<span part="state" data-state="empty">${this.searching ? 'No match' : 'Nothing here'}</span>`
585
- : html `<ul part="tree">${rows.map((n) => this.renderNode(n, 0, layer, filtered))}</ul>`}
586
- ${this.searching === null && this.rootsNext
587
- ? html `<button part="more" class="more-link" type="button" @click=${() => void this.loadRoots(this.rootsNext ?? undefined)}>More</button>`
654
+ <div role="status"><span part="state" data-state=${state}>${said}</span>${notes.length > 0 ? html `<span class="sr-only">${notes.join('. ')}</span>` : nothing}</div>
655
+ ${rows === null || rows.length === 0 ? nothing : html `<ul part="tree" aria-label=${title}>${rows.map((n) => this.renderNode(n, 0, layer))}</ul>`}
656
+ ${this.searching === null && this.lineage.hasMore
657
+ ? html `<button part="more" class="more-link" type="button" @click=${(e) => void this.more(e.currentTarget, null)}>More</button>`
588
658
  : nothing}
589
659
  </div>`;
590
660
  }
591
- renderNode(node, depth, layer, filtered) {
592
- const open = this.open.has(node.path);
593
- const clauses = this.clausesOn(node.row.mosaicaId);
594
- const name = artifactName(node.row);
661
+ renderNode(node, depth, layer) {
662
+ // A row is expanded while its children are drawn, not while they are on their way.
663
+ const open = this.open.has(node.path) && node.children !== null;
664
+ const id = node.row.mosaicaId;
665
+ const clauses = this.clausesOn(id);
666
+ const name = called(node.row.name, node.row.key);
667
+ const text = name.text;
595
668
  // A `dag` node is drawn under each served parent; the row names the others.
596
- const also = node.row.parentIds.filter((p) => String(p) !== node.path.split('/').at(-2));
669
+ const also = this.lineage.alsoUnder(node);
597
670
  const drawn = !isFilterLayer(layer);
671
+ const pressed = (verb) => (clauses.includes(verb) ? 'true' : 'false');
598
672
  return html `<li>
599
- <div part="row" style=${`--depth:${depth}`} data-id=${idString(node.row.mosaicaId)} data-clause=${clauses.length > 0 ? clauses.sort().join(' ') : nothing}>
673
+ <div part="row" style=${`--depth:${depth}`} .path=${node.path} data-id=${idString(id)} data-clause=${clauses.length > 0 ? clauses.sort().join(' ') : nothing}
674
+ data-buttons-away=${this.buttonsAway.has(node.path) ? '' : nothing} @pointerleave=${(e) => this.leftRow(e, node.path)} @focusout=${(e) => this.leftRow(e, node.path)}>
600
675
  <button part="expander" type="button" data-leaf=${node.row.childCount === 0 ? '' : nothing}
601
676
  aria-expanded=${open ? 'true' : 'false'}
602
- aria-label=${open ? `Collapse ${name ?? 'row'}` : `Expand ${name ?? 'row'}`}
603
- @click=${() => this.toggle(node)}>${icon(open ? 'chev' : 'chevr', 12)}</button>
604
- <button part="name" type="button" data-unnamed=${name === null ? '' : nothing}
605
- title="Highlight"
606
- @click=${() => this.apply(node.row.mosaicaId, 'highlight')}>${name ?? UNNAMED}</button>
607
- <span part="counts">
608
- ${filtered && node.row.matchedCount !== null
609
- ? html `<mosaica-count part="count-matched" .masked=${masked(node.row.matchedCount)}></mosaica-count>/`
610
- : nothing}<mosaica-count part="count-masked" .masked=${masked(node.row.maskedCount)}></mosaica-count>
611
- </span>
612
- ${clauses.map((verb) => html `<button part="dismiss" type="button" data-verb=${verb} aria-label=${verb === 'highlight' ? `Stop highlighting ${name ?? UNNAMED}` : `Stop filtering to ${name ?? UNNAMED}`}
613
- title=${verb === 'highlight' ? 'Stop highlighting' : 'Stop filtering'} @click=${() => this.apply(node.row.mosaicaId, verb)}>${icon('close', 12)}</button>`)}
614
- <span part="actions">
615
- <button part="highlight" type="button" data-verb="highlight" aria-pressed=${clauses.includes('highlight') ? 'true' : 'false'}
616
- title="Highlight this" @click=${() => this.apply(node.row.mosaicaId, 'highlight')}>${icon('highlight', 13)}</button>
617
- <button part="filter" type="button" data-verb="filter" aria-pressed=${clauses.includes('filter') ? 'true' : 'false'}
618
- title="Filter to this" @click=${() => this.apply(node.row.mosaicaId, 'filter')}>${icon('filter', 13)}</button>
619
- ${
677
+ aria-label=${open ? `Collapse ${name.from === 'none' ? 'row' : text}` : `Expand ${name.from === 'none' ? 'row' : text}`}
678
+ @click=${() => this.toggle(node)}>${node.loading ? html `<span class="spinner"></span>` : icon(open ? 'chev' : 'chevr', 12)}</button>
679
+ <button part="name" type="button" ?data-key-name=${name.from === 'key'} ?data-unnamed=${name.from === 'none'}
680
+ aria-label=${`Highlight ${text}`} aria-pressed=${pressed('highlight')} title="Highlight"
681
+ @click=${() => this.apply(id, 'highlight')}>${text}</button>
682
+ <span class="figures">
683
+ <span part="actions" @focusin=${() => this.shownAgain(node.path)}>
684
+ <button part="highlight" type="button" data-verb="highlight" aria-label=${`Highlight ${text}`} aria-pressed=${pressed('highlight')}
685
+ title="Highlight this" @click=${() => this.apply(id, 'highlight')}>${icon('highlight', 13)}</button>
686
+ <button part="filter" type="button" data-verb="filter" aria-label=${`Filter to ${text}`} aria-pressed=${pressed('filter')}
687
+ title="Filter to this" @click=${() => this.apply(id, 'filter')}>${icon('filter', 13)}</button>
688
+ ${
620
689
  // A filter layer draws nothing, so there is nothing to fit to.
621
690
  drawn
622
- ? html `<button part="fit" type="button" title="Fit the map to it"
623
- @click=${() => emit(this, 'mosaica-artifactfit', { id: idString(node.row.mosaicaId) })}>${icon('fit', 13)}</button>`
691
+ ? html `<button part="fit" type="button" aria-label=${`Fit the map to ${text}`} title="Fit the map to it"
692
+ @click=${() => emit(this, 'mosaica-artifactfit', { id: idString(id) })}>${icon('fit', 13)}</button>`
624
693
  : nothing}
694
+ </span>
695
+ <span part="counts" aria-hidden="true">
696
+ ${node.row.matchedCount !== null
697
+ ? html `<mosaica-count part="count-matched" .masked=${masked(node.row.matchedCount)}></mosaica-count>/`
698
+ : nothing}<mosaica-count part="count-masked" .masked=${masked(node.row.maskedCount)}></mosaica-count>
699
+ </span><span class="sr-only">${spokenCounts(node.row)}</span>
625
700
  </span>
701
+ ${clauses.map((verb) => html `<button part="dismiss" type="button" data-verb=${verb} aria-label=${verb === 'highlight' ? `Stop highlighting ${text}` : `Stop filtering to ${text}`}
702
+ title=${verb === 'highlight' ? 'Stop highlighting' : 'Stop filtering'} @click=${() => this.apply(id, verb)}>${icon('close', 12)}</button>`)}
626
703
  </div>
627
- ${also.length > 0 ? html `<div part="also" style=${`--depth:${depth}`}>also under ${also.map((p) => this.nameOf(p)).join(', ')}</div>` : nothing}
704
+ ${also.length > 0 ? html `<div part="also" style=${`--depth:${depth}`}>also under ${calledList(also)}</div>` : nothing}
628
705
  ${node.refusal ? html `<div part="also" style=${`--depth:${depth}`} data-code=${node.refusal.code}>Children unavailable</div>` : nothing}
629
- ${open && node.children
630
- ? html `<ul part="children" class="list" style="list-style:none;margin:0;padding:0">
631
- ${node.children.map((c) => this.renderNode(c, depth + 1, layer, filtered))}
706
+ ${open
707
+ ? html `<ul part="children" aria-label=${`Children of ${text}`}>
708
+ ${node.children.map((c) => this.renderNode(c, depth + 1, layer))}
632
709
  ${node.next
633
- ? html `<li><button part="more" class="more-link" style=${`--depth:${depth + 1}`} type="button" @click=${() => void this.expand(node, true)}>More</button></li>`
710
+ ? html `<li><button part="more" class="more-link" style=${`--depth:${depth + 1}`} type="button" @click=${(e) => void this.more(e.currentTarget, node)}>More</button></li>`
634
711
  : nothing}
635
712
  </ul>`
636
713
  : nothing}