@mosaicajs/components 0.3.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 +116 -37
  17. package/dist/explorer.js +476 -210
  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 +47 -16
  27. package/dist/item-card.js +241 -108
  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 +1837 -833
  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/filter.js CHANGED
@@ -40,6 +40,7 @@ import { FloatingList } from './float.js';
40
40
  import { MosaicaElement, columnCaption, emit } from './base.js';
41
41
  import { attachContextRoot, defineOnce } from './define.js';
42
42
  import { icon } from './icons.js';
43
+ import { NamedQuery } from './states.js';
43
44
  import { chrome, tokens } from './tokens.js';
44
45
  /** The operators a keyword control can send. */
45
46
  const KEYWORD_OPERATORS = ['contains', 'prefix', 'eq'];
@@ -57,11 +58,16 @@ const BAR_FLOOR = 2;
57
58
  * - A text column takes words that an item must all contain. Double quotes match a phrase where the
58
59
  * column supports phrases, and `OR` between terms matches either.
59
60
  * - A category column suggests values as the viewer types. The arrow keys move through the
60
- * suggestions, Enter chooses or removes one, and Escape clears what was typed.
61
+ * suggestions, as do Home and End once one is reached. Enter chooses or removes the suggestion
62
+ * they reached, and Escape clears what was typed. A line under the box says how many values are
63
+ * listed, or why none is, and that they are loading once that has taken a quarter of a second.
61
64
  * - A keyword column has a menu for choosing how to match, by `contains`, `prefix` or `eq`.
62
65
  *
63
66
  * The box sets the filter 350 ms after the last keystroke, so that the map does not redraw for
64
- * every letter. If your code sets a text filter the box cannot express, the box shows it read-only.
67
+ * every letter. While the query that change started is still loading, and once it has run for a
68
+ * quarter of a second, a spinner takes the search icon's place. The spinner is not read out:
69
+ * `<mosaica-status>` is what announces a running query. If your code sets a text filter the box
70
+ * cannot express, the box shows it read-only.
65
71
  *
66
72
  * @summary A search box that filters the map by one column.
67
73
  * @tagname mosaica-filter
@@ -69,11 +75,13 @@ const BAR_FLOOR = 2;
69
75
  * @fires {CustomEvent<MosaicaEventDetails['mosaica-filterchange']>} mosaica-filterchange - The box
70
76
  * changed the column's filter.
71
77
  * @csspart entry - The box.
78
+ * @csspart spinner - The spinner in the search icon's place while the box's own query runs.
72
79
  * @csspart mode - The keyword column's operator button, which opens its menu.
73
80
  * @csspart operators - The menu of a keyword column's operators.
74
81
  * @csspart operator - One operator in the menu, with `aria-checked`.
75
82
  * @csspart values - The typeahead's suggestions.
76
- * @csspart tick - One suggested value, with `aria-selected`.
83
+ * @csspart tick - One suggested value, with `aria-selected`, and `data-active` on the one the arrow
84
+ * keys reached.
77
85
  * @csspart bar - A bar for a suggested value's share of the items counted.
78
86
  * @csspart value-count - A suggested value's count.
79
87
  * @csspart more - The note under the box when nothing matches, or when more values match than are
@@ -144,6 +152,10 @@ let MosaicaFilter = (() => {
144
152
  .input input {
145
153
  font-size: 12px;
146
154
  }
155
+ /* In the search icon's place, at its width. */
156
+ .input .spinner {
157
+ margin: 0 1px;
158
+ }
147
159
  .ctl-row {
148
160
  display: flex;
149
161
  align-items: center;
@@ -233,6 +245,11 @@ let MosaicaFilter = (() => {
233
245
  [part~='tick'][aria-selected='true'] {
234
246
  background: var(--_mosaica-surface-2);
235
247
  }
248
+ /* The box keeps focus, so the suggestion the arrow keys reached is ringed as if it had it. */
249
+ [part~='tick'][data-active] {
250
+ outline: 2px solid var(--_mosaica-focus);
251
+ outline-offset: -2px;
252
+ }
236
253
  [part~='tick'][aria-selected='true'] {
237
254
  font-weight: 600;
238
255
  }
@@ -243,9 +260,10 @@ let MosaicaFilter = (() => {
243
260
  min-width: 0;
244
261
  }
245
262
  .opt .name {
263
+ white-space: var(--_mosaica-long-text, nowrap);
246
264
  overflow: hidden;
247
265
  text-overflow: ellipsis;
248
- white-space: nowrap;
266
+ overflow-wrap: anywhere;
249
267
  }
250
268
  /* The matched span the server gave. */
251
269
  [part~='tick'] mark {
@@ -264,11 +282,13 @@ let MosaicaFilter = (() => {
264
282
  height: 3px;
265
283
  border-radius: 2px;
266
284
  background: var(--_mosaica-surface-3);
285
+ forced-color-adjust: none;
267
286
  }
268
287
  [part='bar'] {
269
288
  height: 3px;
270
289
  border-radius: 2px;
271
290
  background: color-mix(in srgb, var(--_mosaica-ink-2) 75%, var(--_mosaica-surface));
291
+ forced-color-adjust: none;
272
292
  }
273
293
  [aria-selected='true'] [part='bar'] {
274
294
  background: var(--_mosaica-ink);
@@ -298,6 +318,21 @@ let MosaicaFilter = (() => {
298
318
  .skel {
299
319
  margin-top: 8px;
300
320
  }
321
+ @media (forced-colors: active) {
322
+ [part~='tick'][data-active] {
323
+ outline-color: Highlight;
324
+ }
325
+ .track {
326
+ background: GrayText;
327
+ }
328
+ [part='bar'] {
329
+ background: CanvasText;
330
+ }
331
+ /* A chosen row is painted Highlight there. */
332
+ [aria-selected='true'] [part='bar'] {
333
+ background: HighlightText;
334
+ }
335
+ }
301
336
  `
302
337
  ];
303
338
  #column_accessor_storage = __runInitializers(this, _column_initializers, '');
@@ -324,7 +359,7 @@ let MosaicaFilter = (() => {
324
359
  get search() { return this.#search_accessor_storage; }
325
360
  set search(value) { this.#search_accessor_storage = value; }
326
361
  #activeCode_accessor_storage = (__runInitializers(this, _search_extraInitializers), __runInitializers(this, _activeCode_initializers, null));
327
- /** The suggestion the arrow keys moved to, by code; `null` is the first. @internal */
362
+ /** The suggestion the arrow keys reached, by code; `null` before they reach one. @internal */
328
363
  get activeCode() { return this.#activeCode_accessor_storage; }
329
364
  set activeCode(value) { this.#activeCode_accessor_storage = value; }
330
365
  #focused_accessor_storage = (__runInitializers(this, _activeCode_extraInitializers), __runInitializers(this, _focused_initializers, false));
@@ -336,6 +371,11 @@ let MosaicaFilter = (() => {
336
371
  get operatorsOpen() { return this.#operatorsOpen_accessor_storage; }
337
372
  set operatorsOpen(value) { this.#operatorsOpen_accessor_storage = value; }
338
373
  sent = (__runInitializers(this, _operatorsOpen_extraInitializers), null);
374
+ /** Whether the query the store is running was started by this box. */
375
+ asked = false;
376
+ query = new NamedQuery(this);
377
+ /** Whether the suggestions have been awaited long enough to say so. */
378
+ awaited = new NamedQuery(this);
339
379
  floating = new FloatingList(() => {
340
380
  const list = this.renderRoot.querySelector('[part="values"]');
341
381
  const anchor = this.renderRoot.querySelector('.combo');
@@ -366,6 +406,7 @@ let MosaicaFilter = (() => {
366
406
  // The draft being edited was the previous store's; the next is seeded from the one adopted.
367
407
  this.draft = null;
368
408
  this.sent = null;
409
+ this.asked = false;
369
410
  }
370
411
  get resolvedOperand() {
371
412
  return this.resolvedStore?.get('meta')?.filterOperands.find((o) => o.column === this.column) ?? null;
@@ -396,14 +437,22 @@ let MosaicaFilter = (() => {
396
437
  this.ask('');
397
438
  this.floating.stop();
398
439
  this.operatorMenu.stop();
440
+ this.query.stop();
441
+ this.awaited.stop();
399
442
  super.disconnectedCallback();
400
443
  }
401
444
  onStoreChange() {
402
- // Re-seed from the store only when its draft changed underneath (a clear all, or the first
403
- // meta), not while the user's own edit is in flight.
445
+ // Re-seed from the store only when its draft changed underneath (a clear all, a cancelled
446
+ // query, or the first meta), not while the user's own edit is in flight. A draft that no
447
+ // longer holds the control this box sent has dropped it.
404
448
  const stored = this.resolvedStore?.get('filters').draft.filter[this.column] ?? null;
405
- if (stored && stored !== this.sent && JSON.stringify(stored) !== JSON.stringify(this.draft)) {
406
- this.draft = structuredClone(stored);
449
+ const changed = stored ? stored !== this.sent && JSON.stringify(stored) !== JSON.stringify(this.draft) : this.sent !== null;
450
+ if (changed) {
451
+ // What was typed since is not sent over the draft that replaced it.
452
+ if (this.typing)
453
+ clearTimeout(this.typing);
454
+ this.typing = null;
455
+ this.draft = stored ? structuredClone(stored) : null;
407
456
  this.sent = stored;
408
457
  }
409
458
  if (this.resolvedOperand?.family === 'category') {
@@ -428,6 +477,24 @@ let MosaicaFilter = (() => {
428
477
  super.updated(changed);
429
478
  this.floating.update();
430
479
  this.operatorMenu.update();
480
+ if (changed.has('activeCode'))
481
+ this.renderRoot.querySelector('[part~="tick"][data-active]')?.scrollIntoView({ block: 'nearest' });
482
+ if (!this.querying)
483
+ this.asked = false;
484
+ this.query.track(this.asked);
485
+ this.awaited.track(this.awaiting);
486
+ }
487
+ get querying() {
488
+ return this.resolvedStore?.get('status').querying ?? false;
489
+ }
490
+ /** Whether a category box holds text whose suggestions have neither landed nor been refused. */
491
+ get awaiting() {
492
+ const filters = this.resolvedStore?.get('filters');
493
+ return this.resolvedOperand?.family === 'category' && this.search !== '' && this.resolvedSuggestion === null && !filters?.suggestErrors[this.column];
494
+ }
495
+ /** The search icon, or a spinner while the query this box started has run long enough to name. */
496
+ lead() {
497
+ return this.asked && this.querying && this.query.named ? html `<span part="spinner" class="spinner" aria-hidden="true"></span>` : icon('search', 12);
431
498
  }
432
499
  /** The box's draft: the one being edited, else the store's, else the client's empty one, which also sets a keyword's starting operator. */
433
500
  currentDraft(o) {
@@ -447,6 +514,8 @@ let MosaicaFilter = (() => {
447
514
  const draft = { ...held, filter: { ...held.filter, [column]: next } };
448
515
  this.sent = next;
449
516
  s.setFilters(draft);
517
+ this.asked = s.get('status').querying;
518
+ this.requestUpdate();
450
519
  emit(this, 'mosaica-filterchange', { column, verb: 'filter', expr: composeFilters(draft, 'filter') });
451
520
  };
452
521
  if (immediate)
@@ -472,16 +541,18 @@ let MosaicaFilter = (() => {
472
541
  return nothing;
473
542
  }
474
543
  }
475
- label() {
476
- return this.placeholder || columnCaption(this.column);
544
+ /** The box's name: the column, and the placeholder the box shows. */
545
+ label(shown) {
546
+ return shown ? `${columnCaption(this.column)}: ${shown}` : columnCaption(this.column);
477
547
  }
478
548
  text(draft) {
479
549
  // An expression set from outside that no query writes is shown as it is, and cannot be typed over.
480
550
  if (draft.expr !== undefined) {
481
- return html `<div class="input">${icon('search', 12)}<input id="ctl" part="entry" readonly aria-label=${this.label()} .value=${JSON.stringify(draft.expr)} /></div>`;
551
+ return html `<div class="input">${icon('search', 12)}<input id="ctl" part="entry" readonly aria-label=${this.label('')} .value=${JSON.stringify(draft.expr)} /></div>`;
482
552
  }
483
- return html `<div class="input">${icon('search', 12)}<input id="ctl" part="entry" type="search" .value=${draft.query} placeholder=${this.placeholder || 'Search the text'} autocomplete="off"
484
- aria-label=${this.label()} @input=${(e) => this.change({ family: 'text', query: e.target.value, phrase: draft.phrase }, false)} /></div>`;
553
+ const shown = this.placeholder || 'Search the text';
554
+ return html `<div class="input">${this.lead()}<input id="ctl" part="entry" type="search" .value=${draft.query} placeholder=${shown} autocomplete="off"
555
+ aria-label=${this.label(shown)} @input=${(e) => this.change({ family: 'text', query: e.target.value, phrase: draft.phrase }, false)} /></div>`;
485
556
  }
486
557
  /** A keyword box, offering the operators the column publishes in a menu. */
487
558
  keyword(o, draft) {
@@ -516,7 +587,7 @@ let MosaicaFilter = (() => {
516
587
  void this.updateComplete.then(() => this.renderRoot.querySelector('[part~="operator"][aria-checked="true"]')?.focus());
517
588
  };
518
589
  return html `<div class="ctl-row">
519
- <div class="input grow">${icon('search', 12)}<input id="ctl" part="entry" type="search" .value=${draft.needle} autocomplete="off" placeholder=${this.placeholder || nothing} aria-label=${this.label()}
590
+ <div class="input grow">${this.lead()}<input id="ctl" part="entry" type="search" .value=${draft.needle} autocomplete="off" placeholder=${this.placeholder || nothing} aria-label=${this.label(this.placeholder)}
520
591
  @input=${(e) => this.change({ ...draft, needle: e.target.value }, false)} /></div>
521
592
  <div class="op" @focusout=${(e) => {
522
593
  const to = e.relatedTarget;
@@ -574,18 +645,20 @@ let MosaicaFilter = (() => {
574
645
  this.change({ ...draft, keys: chosen.has(key) ? draft.keys.filter((k) => k !== key) : [...draft.keys, key] }, true);
575
646
  };
576
647
  const typed = this.search !== '';
648
+ const listed = suggestion?.values ?? [];
577
649
  // The suggestions show while the box has focus and holds text; the list closes as focus leaves.
578
- const rows = typed && this.focused ? (suggestion?.values ?? []) : [];
579
- // The row the keys act on: the one the arrows reached, else the first.
580
- const active = rows.find((v) => v.code === this.activeCode) ?? rows[0] ?? null;
581
- const move = (by) => {
582
- if (rows.length === 0)
583
- return;
584
- const at = active ? rows.indexOf(active) : -1;
585
- this.activeCode = rows[(at + by + rows.length) % rows.length].code;
650
+ const rows = typed && this.focused ? listed : [];
651
+ // The row Enter acts on: the one the arrow keys reached. Until they reach one there is none, so
652
+ // Enter does not take whichever row a page that lands puts first.
653
+ const active = rows.find((v) => v.code === this.activeCode) ?? null;
654
+ const at = active ? rows.indexOf(active) : -1;
655
+ const reach = (i) => {
656
+ if (rows.length > 0)
657
+ this.activeCode = rows[(i + rows.length) % rows.length].code;
586
658
  };
587
- const field = html `<div class="input">${icon('search', 12)}<input id="ctl" part="entry" type="search" autocomplete="off" placeholder=${this.placeholder || 'Type a value'}
588
- aria-label=${this.label()} role="combobox" aria-expanded=${rows.length > 0 ? 'true' : 'false'} aria-controls="values" aria-activedescendant=${active ? `value-${active.code}` : nothing}
659
+ const shown = this.placeholder || 'Type a value';
660
+ const field = html `<div class="input">${this.lead()}<input id="ctl" part="entry" type="search" autocomplete="off" placeholder=${shown}
661
+ aria-label=${this.label(shown)} role="combobox" aria-autocomplete="list" aria-expanded=${rows.length > 0 ? 'true' : 'false'} aria-controls="values" aria-activedescendant=${active ? `value-${active.code}` : nothing}
589
662
  .value=${this.search}
590
663
  @focus=${() => (this.focused = true)}
591
664
  @blur=${() => (this.focused = false)}
@@ -598,7 +671,12 @@ let MosaicaFilter = (() => {
598
671
  @keydown=${(e) => {
599
672
  if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
600
673
  e.preventDefault();
601
- move(e.key === 'ArrowDown' ? 1 : -1);
674
+ reach(e.key === 'ArrowDown' ? at + 1 : Math.max(at, 0) - 1);
675
+ }
676
+ else if ((e.key === 'Home' || e.key === 'End') && active) {
677
+ // Before the arrow keys reach a suggestion, Home and End move in the text.
678
+ e.preventDefault();
679
+ reach(e.key === 'Home' ? 0 : -1);
602
680
  }
603
681
  else if (e.key === 'Enter') {
604
682
  if (active)
@@ -623,20 +701,21 @@ let MosaicaFilter = (() => {
623
701
  </button>`;
624
702
  };
625
703
  const list = rows.length > 0
626
- ? html `<div part="values" id="values" popover="manual" role="listbox" aria-label=${`${columnCaption(this.column)} values`} style=${countWidth(rows.map((v) => v.count))}>${repeat(rows, (v) => v.code, option)}</div>`
704
+ ? html `<div part="values" id="values" popover="manual" role="listbox" aria-multiselectable=${choose ? nothing : 'true'} aria-label=${`${columnCaption(this.column)} values`} style=${countWidth(rows.map((v) => v.count))}>${repeat(rows, (v) => v.code, option)}</div>`
627
705
  : nothing;
628
- const note = !typed
706
+ // How many values are listed, or why none is, for a reader as the list changes under the box.
707
+ const said = !typed
629
708
  ? nothing
630
709
  : refusal
631
710
  ? html `<span part="refusal" data-code=${refusal.code}><span class="dot refuse"></span>Values unavailable</span>`
632
711
  : suggestion === null
633
- ? html `<span class="skel" aria-hidden="true"></span>`
634
- : rows.length === 0
712
+ ? html `<span class="skel" aria-hidden="true"></span>${this.awaited.named ? html `<span class="sr-only">Loading values</span>` : nothing}`
713
+ : listed.length === 0
635
714
  ? html `<span part="more">No value starts with that</span>`
636
- : suggestion.more
637
- ? html `<span part="more">Type more to narrow the list</span>`
638
- : nothing;
639
- return html `<div class="combo">${field}${list}</div>${note}`;
715
+ : rows.length === 0
716
+ ? nothing
717
+ : html `<span class="sr-only">${rows.length} ${rows.length === 1 ? 'value' : 'values'} listed. </span>${suggestion.more ? html `<span part="more">Type more to narrow the list</span>` : nothing}`;
718
+ return html `<div class="combo">${field}${list}</div><div role="status">${said}</div>`;
640
719
  }
641
720
  };
642
721
  })();
@@ -10,10 +10,19 @@ import './count.js';
10
10
  * hierarchy, filter layers among them. When there are several, a select switches between them.
11
11
  *
12
12
  * Each row shows how many of the artifact's items the viewer may see. While filters are set, it
13
- * also shows how many of those items match. A parent the viewer may not see is left out, so its
14
- * children appear at the top level.
13
+ * also shows how many of those items match, and a screen reader is given each count with what it
14
+ * counts. An artifact the viewer may not see is left out, and its children appear under its
15
+ * nearest ancestors the viewer may see, or at the top level where it has none. A row under several
16
+ * served parents names the others, and a parent no page has served yet is asked for by name. When
17
+ * the filters change, the rows are fetched again, a few requests at a time: as many pages of the
18
+ * roots and of each open row as More had loaded, since a page's cursor is good only for the
19
+ * filters it was served under. Focus stays on the control it was on.
15
20
  *
16
- * A row's buttons highlight or filter the map by its artifact. Its Fit button only fires
21
+ * A row's name is one line, cut with an ellipsis where it is too long, unless
22
+ * `--mosaica-long-text` is `normal`. A row's buttons highlight or filter the map by its artifact.
23
+ * They are shown in place of its counts while the pointer or focus is in the row, and Escape puts
24
+ * them away again; where there is no pointer to hover with, they stand beside the counts. Its Fit
25
+ * button only fires
17
26
  * `mosaica-artifactfit`, so you handle the event by passing its id to the map's `fitTo`. The
18
27
  * explorer does this for a hierarchy you put in one of its slots. `fitTo` finds only the artifacts
19
28
  * the map currently draws, so Fit does nothing for a row whose artifact is not drawn at this zoom.
@@ -28,31 +37,39 @@ import './count.js';
28
37
  * @fires {CustomEvent<MosaicaEventDetails['mosaica-artifactfit']>} mosaica-artifactfit - A row's Fit
29
38
  * button was pressed.
30
39
  * @csspart title - The heading.
31
- * @csspart state - The state line, with `data-state`.
40
+ * @csspart state - The state line, with `data-state`, inside a `role="status"` region, which also
41
+ * says which rows are loading children and which were refused them.
32
42
  * @csspart refusal - The words "View refused" or "Layer unavailable", with `data-code`.
33
43
  * @csspart reason - The server's reason for a refusal, under "View refused".
34
44
  * @csspart layer - The layer select, shown if there are several layers.
35
45
  * @csspart search - The search box.
36
- * @csspart tree - The tree of rows.
37
- * @csspart row - One row, with `data-id`, and `data-clause` (`filter`, `highlight` or both,
38
- * separated by a space) while its artifact is filtered or highlighted.
39
- * @csspart expander - A row's expand button. On a row with no children it has `data-leaf` and is
40
- * hidden, keeping the names aligned.
41
- * @csspart name - A row's name, with `data-unnamed` if it has none. Pressing it highlights the
42
- * artifact.
43
- * @csspart counts - A row's counts.
46
+ * @csspart tree - The tree of rows, named by the layer.
47
+ * @csspart row - One row, with `data-id`, `data-clause` (`filter`, `highlight` or both, separated
48
+ * by a space) while its artifact is filtered or highlighted, and `data-buttons-away` once Escape has put
49
+ * its buttons away.
50
+ * @csspart expander - A row's expand button, with `aria-expanded` while its children are shown. On
51
+ * a row with no children it has `data-leaf` and is hidden, keeping the names aligned.
52
+ * @csspart name - A row's name. Where the artifact has none it is the artifact's key, with
53
+ * `data-key-name`, and where it has no key either it is the word Unnamed, with `data-unnamed`.
54
+ * Pressing it highlights the artifact, and it has `aria-pressed` while the artifact is
55
+ * highlighted.
56
+ * @csspart counts - A row's counts as they are drawn, hidden from a screen reader, which is given
57
+ * them beside it with what each counts. They are not drawn while the row's buttons are shown
58
+ * over them.
44
59
  * @csspart count-matched - The `<mosaica-count>` of a row's items that match the filters, while a
45
60
  * filter is set.
46
61
  * @csspart count-masked - The `<mosaica-count>` of a row's items the viewer may see.
47
- * @csspart actions - A row's buttons.
62
+ * @csspart actions - A row's buttons, shown over its counts while the pointer or focus is in the
63
+ * row.
48
64
  * @csspart highlight - A row's highlight button, with `aria-pressed`.
49
65
  * @csspart filter - A row's filter button, with `aria-pressed`.
50
66
  * @csspart fit - A row's Fit button, on a layer that draws.
51
67
  * @csspart dismiss - A × that removes a row's filter or highlight, one for each, with `data-verb`.
52
- * @csspart also - The row's other parents, or "Children unavailable" with `data-code` if its
53
- * children were refused.
68
+ * The highlight's has a bar under it.
69
+ * @csspart also - The row's other parents, each with `data-key-name` where it is called by its key,
70
+ * or "Children unavailable" with `data-code` if its children were refused.
54
71
  * @csspart children - An expanded row's children.
55
- * @csspart more - The More button, which loads the next page.
72
+ * @csspart more - The More button, which loads the next page and moves focus to its first row.
56
73
  */
57
74
  export declare class MosaicaHierarchy extends MosaicaElement {
58
75
  static styles: import("lit").CSSResult[];
@@ -60,34 +77,22 @@ export declare class MosaicaHierarchy extends MosaicaElement {
60
77
  accessor layer: string;
61
78
  /** Rows per page, from 1 up to the server's `maxBrowseRows`. */
62
79
  accessor limit: number;
63
- private accessor roots;
64
- private accessor rootsNext;
65
80
  private accessor query;
66
81
  private accessor searching;
67
- private accessor loading;
68
- private accessor refusal;
82
+ /** Why the last search was refused. */
83
+ private accessor searchRefusal;
69
84
  /** Which paths are open, so a re-render of the tree keeps the walk. */
70
85
  private accessor open;
71
- /** Bumped to re-render after a node's children land, since the tree in `roots` is mutated. */
72
- private accessor revision;
73
- /** The question the roots were fetched under; see {@link question}. */
74
- private fetchedUnder;
75
- /**
76
- * Every name the walk has seen, by identifier. A filter layer's artifacts are never served in a
77
- * viewport, so this is the client's only source of their names.
78
- */
79
- private names;
86
+ /** The paths of the rows whose buttons Escape has put away, each until the pointer and focus leave it. */
87
+ private accessor buttonsAway;
88
+ /** The browse pages of the current layer: its roots and the children of the rows opened. */
89
+ private readonly lineage;
80
90
  private searchTimer;
81
- /** Moved by {@link resetServerData}, so a page asked for before it is dropped. */
91
+ /** Moved by {@link resetServerData}, so a search asked for before it is dropped. */
82
92
  private epoch;
83
93
  protected resetServerData(): void;
84
94
  private layers;
85
95
  private current;
86
- /**
87
- * The question the panel is asking, as a string: the layer and the map's filter. A change
88
- * refetches the roots and drops the walk, whose counts answer the old question.
89
- */
90
- private question;
91
96
  /**
92
97
  * Whether the panel is shown. A hidden panel sends no request, so a panel in a closed drawer
93
98
  * does not load the roots alongside the first viewport.
@@ -99,30 +104,48 @@ export declare class MosaicaHierarchy extends MosaicaElement {
99
104
  private observer;
100
105
  connectedCallback(): void;
101
106
  disconnectedCallback(): void;
107
+ /**
108
+ * Escape puts away the buttons shown over a row's counts, wherever focus is, since the pointer
109
+ * alone shows them. Focus on one of them goes to the row's name.
110
+ */
111
+ private onEscape;
112
+ /** The buttons Escape put away come back once the pointer and focus have both left the row. */
113
+ private leftRow;
114
+ /** Let a row show its buttons again. */
115
+ private shownAgain;
102
116
  /** Whether this element is displayed; see {@link shown}. */
103
117
  private displayed;
104
118
  /** The roots, where the question they were fetched under is no longer the one being asked. */
105
119
  private loadRootsIfStale;
106
120
  protected onStoreChange(): void;
121
+ /** The control focus was on as the last render began. */
122
+ private held;
107
123
  /** Re-asked before every render, so putting the panel away and taking it out again both count. */
108
124
  protected willUpdate(changed: PropertyValues<this>): void;
109
125
  protected updated(): void;
110
- /** One page of the current layer, under the map's filter, which the store supplies. */
111
- private page;
112
- private loadRoots;
113
- private node;
114
- /** What to call an artifact: its name where the walk has met one. */
115
- private nameOf;
126
+ /** Open a row, fetching its children where it has none yet, or close one that is open or opening. */
127
+ private toggle;
116
128
  /**
117
- * Expand a node: its children, paged. A node keeps what it fetched while {@link question} is
118
- * unchanged, so reopening it fetches nothing.
129
+ * Load the next page of the roots, or of `parent`'s children, and move focus from the More
130
+ * button, which the page replaces or removes, to the first row it added. Focus that has gone
131
+ * anywhere else meanwhile stays there.
119
132
  */
120
- private expand;
121
- private toggle;
133
+ private more;
134
+ private rowElements;
135
+ /** The control in a row that focus is on, read before a render so {@link keepFocus} can put it back. */
136
+ private heldControl;
137
+ /**
138
+ * After a render, put focus back on the control it was on, in the row it was on. Where a ×
139
+ * removed itself, focus goes to the row's other ×, else to the button that makes the clause.
140
+ * Where the row is gone, it goes to the row now in its place, else the last, else the search box.
141
+ */
142
+ private keepFocus;
122
143
  private search;
123
144
  /** The positions of the clauses on this row's artifact, which colour the row and set the buttons. */
124
145
  private clausesOn;
125
146
  private apply;
147
+ /** What the rows' own fetches say, for the status region: the rows loading children, and those refused them. */
148
+ private rowNotes;
126
149
  render(): TemplateResult | typeof nothing;
127
150
  private renderNode;
128
151
  }