@fusion.dev/helix-headless 0.53.0 → 0.53.1

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.
@@ -20811,6 +20811,16 @@
20811
20811
  "description": "The chosen options themselves, for their words.\n\nRead off the options rather than stored, because the words are the\nconsumer's content and a copy of them goes stale the moment they change it.\nSelect records the same.\n\nIn the value's own order, which is the order they were chosen. It has to\nbe: a created answer is not in the list at all, so there is no position in\nthe list to give it. Neither order ever reshuffles what is already there,\nso a tag does not move out from under the pointer about to remove it.",
20812
20812
  "readonly": true
20813
20813
  },
20814
+ {
20815
+ "kind": "field",
20816
+ "name": "unresolved",
20817
+ "type": {
20818
+ "text": "boolean"
20819
+ },
20820
+ "privacy": "private",
20821
+ "description": "Whether a value is declared and this render cannot say what it is.\n\n**A placeholder is a statement, so a render that cannot read the words does\nnot make it.** `value` is an attribute and a server has it; the words are\nthe options' and the options are light-DOM children a server has none of.\nThe field then came out empty with \"Search owners\" over it, which says\nnothing is chosen where the consumer had already said what is. Blank, it\nsays nothing, keeps its box, and is filled in by the render after\nhydration, which is the answer `Select.Root.display` reaches for in the same\nposition and for the same reason.\n\nThe placeholder still stands where there is genuinely no value, which is\nwhat it is for and the one case a server can be sure of.",
20822
+ "readonly": true
20823
+ },
20814
20824
  {
20815
20825
  "kind": "field",
20816
20826
  "name": "showing",
@@ -25430,12 +25440,22 @@
25430
25440
  },
25431
25441
  "privacy": "protected",
25432
25442
  "readonly": true,
25433
- "description": "Emptiness is a fact about the data here, not about slotted children.\n\n**The page rather than the window**, which is the difference between the two\ncounts `view` carries and the reason this reads `available`. A windowed\ntable draws a slice, and a slice can be short of the rows that are there:\nasking whether *this* slice has rows says \"nothing matched\" about a result\nthat matched plenty.\n\nThe mixin's own answer counts children with no `slot`, which for this\nelement is the columns. Left in place, a table with columns and no data\nwould call itself full.",
25443
+ "description": "Emptiness is a fact about the data here, not about slotted children.\n\n**The page rather than the window**, which is the difference between the two\ncounts `view` carries and the reason this reads `available`. A windowed\ntable draws a slice, and a slice can be short of the rows that are there:\nasking whether *this* slice has rows says \"nothing matched\" about a result\nthat matched plenty.\n\nThe mixin's own answer counts children with no `slot`, which for this\nelement is the columns. Left in place, a table with columns and no data\nwould call itself full.\n\n**`sourceKnown` is the half the mixin was keeping**, and overriding without\nit is what put \"Nothing to show\" over eight rows of files on every server\nrender of this family's own preview. `MixinCollection.hasRows` answers true\nuntil the rows have been counted for exactly that reason, and a count is\nwhat this replaced.",
25434
25444
  "inheritedFrom": {
25435
25445
  "name": "MixinCollection",
25436
25446
  "module": "src/mixins/mixin-collection/mixin-collection.ts"
25437
25447
  }
25438
25448
  },
25449
+ {
25450
+ "kind": "field",
25451
+ "name": "sourceKnown",
25452
+ "type": {
25453
+ "text": "boolean"
25454
+ },
25455
+ "privacy": "private",
25456
+ "description": "Whether this render can tell a result with no rows from one not yet handed\nover.\n\n**Unknown is not empty, which is what an empty state must never say.** The\nrows are a property, and a property is set by whoever holds the element: a\nconsumer binding it in a template has it on the server, and a consumer\nassigning it from a script has it only once that script has run, which is\nafter the parse a server's markup ends at. A render that cannot see a\ndocument cannot tell the two apart, and a hydrating render must not look,\nits output having to be the server's.\n\nThe same shape as `Heatmap.sourceKnown`, asked of a property rather than of\na document, and `childrenKnown` is already both halves of the question: a\nserver has no tree and a hydrating render may not read one.",
25457
+ "readonly": true
25458
+ },
25439
25459
  {
25440
25460
  "kind": "method",
25441
25461
  "name": "requestSort",
@@ -75607,10 +75627,10 @@
75607
75627
  "kind": "field",
75608
75628
  "name": "display",
75609
75629
  "type": {
75610
- "text": "string"
75630
+ "text": "string | typeof nothing"
75611
75631
  },
75612
75632
  "privacy": "private",
75613
- "description": "What the trigger says: the chosen options' words, or the placeholder.\n\nRead off the options rather than stored, because the words are the\nconsumer's content and storing a copy is storing something that goes stale\nthe moment they change it.\n\n**Off this element's own children, not the listbox's**, which reach the\nlistbox through a slot and so only after a render has made it: read there,\nthe first render showed the placeholder over a chosen value and waited on a\n`slotchange`, which a declarative shadow root fires before anything listens,\nso a hydrated select kept \"Choose\" for good. Nothing on a server or while\nhydrating what one sent, which drew the placeholder too.",
75633
+ "description": "What the trigger says: the chosen options' words, or the placeholder.\n\nRead off the options rather than stored, because the words are the\nconsumer's content and storing a copy is storing something that goes stale\nthe moment they change it.\n\n**Off this element's own children, not the listbox's**, which reach the\nlistbox through a slot and so only after a render has made it: read there,\nthe first render showed the placeholder over a chosen value and waited on a\n`slotchange`, which a declarative shadow root fires before anything listens,\nso a hydrated select kept \"Choose\" for good. Nothing on a server or while\nhydrating what one sent, which drew the placeholder too.\n\n**And the placeholder is a statement, so a render that cannot read the\nwords does not make it.** \"Choose\" means nothing is chosen. `value` is an\nattribute and a server has it, the words are the children's and a server\nhas none, so a `<helix-select default-value=\"two\">` served \"Choose\" over an\nanswer the consumer had already given: false to a reader, and false to a\ncrawler, on every select in the library's own examples. Where a value is\ndeclared and the word for it cannot be read, the trigger says nothing and\nkeeps its box, which is the answer this library reaches for everywhere it\ncannot tell empty from unknown. `firstUpdated` asks for the render that\nfills it in.\n\nThe placeholder still stands where there is genuinely no value, which is\nwhat it is for and the one case a server can be sure of.\n\n**`nothing` and never `''`, which is the difference between a word that\narrives and one that never does.** Lit's hydration primes a child part with\nwhatever the render computed, and `''` is a primitive, so `_commitText`\ntakes the branch that assumes the server left a text node to overwrite.\nThere is none, the write lands on the closing part marker's comment data,\nand the trigger stays empty for the life of the page: measured by\n`check:spec:dsd`, which caught all nine of these and showed a blank select\nbeside a client's \"Option Two\". `nothing` primes the sentinel instead, so\nthe commit creates the text node it needs.",
75614
75634
  "readonly": true
75615
75635
  },
75616
75636
  {
@@ -61,7 +61,7 @@ let HelixHeadlessCheckbox = class HelixHeadlessCheckbox extends MixinControllerC
61
61
  <input
62
62
  part="control"
63
63
  type="checkbox"
64
- .checked=${this.checked}
64
+ .checked=${this.checked || nothing}
65
65
  .indeterminate=${this.indeterminate}
66
66
  ?disabled=${this.isDisabled}
67
67
  ?required=${this.required}
@@ -208,6 +208,22 @@ export declare class HelixHeadlessCombobox extends HelixHeadlessCombobox_base {
208
208
  * so a tag does not move out from under the pointer about to remove it.
209
209
  */
210
210
  private get chosen();
211
+ /**
212
+ * Whether a value is declared and this render cannot say what it is.
213
+ *
214
+ * **A placeholder is a statement, so a render that cannot read the words does
215
+ * not make it.** `value` is an attribute and a server has it; the words are
216
+ * the options' and the options are light-DOM children a server has none of.
217
+ * The field then came out empty with "Search owners" over it, which says
218
+ * nothing is chosen where the consumer had already said what is. Blank, it
219
+ * says nothing, keeps its box, and is filled in by the render after
220
+ * hydration, which is the answer `Select.Root.display` reaches for in the same
221
+ * position and for the same reason.
222
+ *
223
+ * The placeholder still stands where there is genuinely no value, which is
224
+ * what it is for and the one case a server can be sure of.
225
+ */
226
+ private get unresolved();
211
227
  /**
212
228
  * Whether the filter has left anything to choose.
213
229
  *
@@ -147,6 +147,9 @@ let HelixHeadlessCombobox = class HelixHeadlessCombobox extends MixinControllerO
147
147
  .filter((value) => known || words.has(value))
148
148
  .map((value) => ({ text: words.get(value) ?? value, value }));
149
149
  }
150
+ get unresolved() {
151
+ return this.values.length > 0 && !this.childrenKnown;
152
+ }
150
153
  get showing() {
151
154
  return (this.listbox?.options ?? []).some((option) => !option.hidden);
152
155
  }
@@ -304,7 +307,7 @@ let HelixHeadlessCombobox = class HelixHeadlessCombobox extends MixinControllerO
304
307
  aria-autocomplete=${this.suggest ? 'both' : 'list'}
305
308
  aria-expanded=${this.open ? 'true' : 'false'}
306
309
  aria-describedby=${this.describedBy ?? nothing}
307
- placeholder=${(this.chosen.length > 0 ? '' : this.placeholder) || nothing}
310
+ placeholder=${(this.chosen.length > 0 || this.unresolved ? '' : this.placeholder) || nothing}
308
311
  ?disabled=${this.isDisabled}
309
312
  .value=${this.multiple || this.open ? this.suggestion || this.typed : this.word}
310
313
  @input=${this.handleInput}
@@ -473,8 +473,31 @@ export declare class HelixHeadlessDataTable extends HelixHeadlessDataTable_base
473
473
  * The mixin's own answer counts children with no `slot`, which for this
474
474
  * element is the columns. Left in place, a table with columns and no data
475
475
  * would call itself full.
476
+ *
477
+ * **`sourceKnown` is the half the mixin was keeping**, and overriding without
478
+ * it is what put "Nothing to show" over eight rows of files on every server
479
+ * render of this family's own preview. `MixinCollection.hasRows` answers true
480
+ * until the rows have been counted for exactly that reason, and a count is
481
+ * what this replaced.
476
482
  */
477
483
  protected get hasRows(): boolean;
484
+ /**
485
+ * Whether this render can tell a result with no rows from one not yet handed
486
+ * over.
487
+ *
488
+ * **Unknown is not empty, which is what an empty state must never say.** The
489
+ * rows are a property, and a property is set by whoever holds the element: a
490
+ * consumer binding it in a template has it on the server, and a consumer
491
+ * assigning it from a script has it only once that script has run, which is
492
+ * after the parse a server's markup ends at. A render that cannot see a
493
+ * document cannot tell the two apart, and a hydrating render must not look,
494
+ * its output having to be the server's.
495
+ *
496
+ * The same shape as `Heatmap.sourceKnown`, asked of a property rather than of
497
+ * a document, and `childrenKnown` is already both halves of the question: a
498
+ * server has no tree and a hydrating render may not read one.
499
+ */
500
+ private get sourceKnown();
478
501
  /**
479
502
  * Asks for a different order, and reports whether the request stood.
480
503
  *
@@ -353,7 +353,10 @@ let HelixHeadlessDataTable = class HelixHeadlessDataTable extends MixinCollectio
353
353
  })).length;
354
354
  }
355
355
  get hasRows() {
356
- return this.view.available > 0;
356
+ return this.view.available > 0 || !this.sourceKnown;
357
+ }
358
+ get sourceKnown() {
359
+ return this.childrenKnown;
357
360
  }
358
361
  requestSort(next) {
359
362
  return requestChange({
@@ -618,12 +621,14 @@ let HelixHeadlessDataTable = class HelixHeadlessDataTable extends MixinCollectio
618
621
  const template = this.template;
619
622
  const columns = this.shownColumns;
620
623
  const pagination = this.tag('pagination');
624
+ const counted = this.mode === 'server' ? Math.max(this.total, available) : matched;
625
+ const countKnown = counted > 0 || this.sourceKnown;
621
626
  return html `<div part="toolbar"><slot name="toolbar"></slot></div>
622
627
  <div
623
628
  part="table"
624
629
  role="rowgroup"
625
630
  aria-busy=${this.loading ? 'true' : nothing}
626
- aria-rowcount=${this.mode === 'server' ? Math.max(this.total, available) + 1 : matched + 1}
631
+ aria-rowcount=${countKnown ? counted + 1 : nothing}
627
632
  aria-colcount=${columns.length ? columns.length + (this.selectable ? 1 : 0) : nothing}
628
633
  style=${template ? `--data-table-template: ${template}` : nothing}
629
634
  @scroll=${this.handleScroll}
@@ -648,7 +653,7 @@ let HelixHeadlessDataTable = class HelixHeadlessDataTable extends MixinCollectio
648
653
  status: pagination-status"
649
654
  page=${this.page}
650
655
  page-size=${this.pageSize || 1}
651
- total=${matched}
656
+ total=${countKnown ? matched : nothing}
652
657
  @helix-navigate=${this.handleNavigate}
653
658
  ></${pagination}>`
654
659
  : nothing}
@@ -1,5 +1,5 @@
1
1
  import { HelixElement } from '@fusion.dev/helix-core';
2
- import { type TemplateResult } from 'lit';
2
+ import { type PropertyValues, type TemplateResult } from 'lit';
3
3
  declare const HelixHeadlessRadio_base: (new (...args: any[]) => import("@fusion.dev/helix-core").MixinSelectableItemInterface) & typeof HelixElement;
4
4
  /**
5
5
  * @summary One choice in a radio group.
@@ -89,6 +89,16 @@ export declare class HelixHeadlessRadio extends HelixHeadlessRadio_base {
89
89
  * same reason.
90
90
  */
91
91
  focusable: boolean;
92
+ /**
93
+ * Asks for the render that follows a hydration, so the stop stops being every
94
+ * choice's.
95
+ *
96
+ * `tabindex` reads `childrenKnown` and a hydrating render is one where that is
97
+ * false, which is what keeps the hydrating render the server's. Nothing else
98
+ * would ask: the group writes an already-false `focusable` onto every choice
99
+ * but one, which changes nothing and so re-renders nothing.
100
+ */
101
+ protected firstUpdated(changed: PropertyValues): void;
92
102
  protected render(): TemplateResult;
93
103
  }
94
104
  declare global {
@@ -18,6 +18,10 @@ let HelixHeadlessRadio = class HelixHeadlessRadio extends MixinSelectableItem(He
18
18
  set defaultSelected(next) {
19
19
  this.toggleAttribute('selected', next);
20
20
  }
21
+ firstUpdated(changed) {
22
+ super.firstUpdated(changed);
23
+ this.finishHydration();
24
+ }
21
25
  render() {
22
26
  return html `<button
23
27
  part="item"
@@ -25,7 +29,7 @@ let HelixHeadlessRadio = class HelixHeadlessRadio extends MixinSelectableItem(He
25
29
  role="radio"
26
30
  aria-checked=${this.selected ? 'true' : 'false'}
27
31
  ?disabled=${this.disabled}
28
- tabindex=${this.selected || this.focusable ? '0' : '-1'}
32
+ tabindex=${this.selected || this.focusable || !this.childrenKnown ? '0' : '-1'}
29
33
  @click=${() => this.select()}
30
34
  >
31
35
  <span part="box"><span part="mark" aria-hidden="true"></span></span>
@@ -134,6 +134,30 @@ export declare class HelixHeadlessSelect extends HelixHeadlessSelect_base {
134
134
  * `slotchange`, which a declarative shadow root fires before anything listens,
135
135
  * so a hydrated select kept "Choose" for good. Nothing on a server or while
136
136
  * hydrating what one sent, which drew the placeholder too.
137
+ *
138
+ * **And the placeholder is a statement, so a render that cannot read the
139
+ * words does not make it.** "Choose" means nothing is chosen. `value` is an
140
+ * attribute and a server has it, the words are the children's and a server
141
+ * has none, so a `<helix-select default-value="two">` served "Choose" over an
142
+ * answer the consumer had already given: false to a reader, and false to a
143
+ * crawler, on every select in the library's own examples. Where a value is
144
+ * declared and the word for it cannot be read, the trigger says nothing and
145
+ * keeps its box, which is the answer this library reaches for everywhere it
146
+ * cannot tell empty from unknown. `firstUpdated` asks for the render that
147
+ * fills it in.
148
+ *
149
+ * The placeholder still stands where there is genuinely no value, which is
150
+ * what it is for and the one case a server can be sure of.
151
+ *
152
+ * **`nothing` and never `''`, which is the difference between a word that
153
+ * arrives and one that never does.** Lit's hydration primes a child part with
154
+ * whatever the render computed, and `''` is a primitive, so `_commitText`
155
+ * takes the branch that assumes the server left a text node to overwrite.
156
+ * There is none, the write lands on the closing part marker's comment data,
157
+ * and the trigger stays empty for the life of the page: measured by
158
+ * `check:spec:dsd`, which caught all nine of these and showed a blank select
159
+ * beside a client's "Option Two". `nothing` primes the sentinel instead, so
160
+ * the commit creates the text node it needs.
137
161
  */
138
162
  private get display();
139
163
  /** Closes the list and puts focus back where it came from. */
@@ -74,7 +74,10 @@ let HelixHeadlessSelect = class HelixHeadlessSelect extends MixinControllerOpen(
74
74
  const words = optionsAmong(this.knownChildren)
75
75
  .filter((option) => chosen.has(option.value))
76
76
  .map((option) => option.text);
77
- return words.length > 0 ? words.join(', ') : this.placeholder;
77
+ if (words.length > 0) {
78
+ return words.join(', ');
79
+ }
80
+ return chosen.size > 0 && !this.childrenKnown ? nothing : this.placeholder;
78
81
  }
79
82
  close() {
80
83
  if (!this.open) {
@@ -47,7 +47,7 @@ let HelixHeadlessSwitch = class HelixHeadlessSwitch extends MixinControllerCheck
47
47
  part="control"
48
48
  type="checkbox"
49
49
  role="switch"
50
- .checked=${this.checked}
50
+ .checked=${this.checked || nothing}
51
51
  ?disabled=${this.isDisabled}
52
52
  ?required=${this.required}
53
53
  aria-describedby=${this.describedBy ?? nothing}
@@ -4,7 +4,7 @@ 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 { customElement, HelixElement, IndicatorController, KeyboardNavigationController, MixinControllerActive, } from '@fusion.dev/helix-core';
7
+ import { customElement, HelixElement, IndicatorController, KeyboardNavigationController, MixinControllerActive, whenHydrated, } from '@fusion.dev/helix-core';
8
8
  import { html } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
10
  import { HelixHeadlessTabPanel } from '../panel/tab-panel.js';
@@ -47,12 +47,19 @@ let HelixHeadlessTabs = class HelixHeadlessTabs extends MixinControllerActive(He
47
47
  const declared = this.active === '' ? tabs.find((tab) => tab.selected) : undefined;
48
48
  const value = (declared ?? this.resolveActive(tabs))?.value ?? '';
49
49
  for (const tab of tabs) {
50
- tab.selected = tab.value === value;
50
+ const selected = tab.value === value;
51
+ whenHydrated(tab, () => {
52
+ tab.selected = selected;
53
+ });
51
54
  }
52
55
  const names = new Map(tabs.map((tab) => [tab.value, (tab.textContent ?? '').trim()]));
53
56
  for (const panel of this.panels) {
54
- panel.selected = panel.value === value;
55
- panel.label = names.get(panel.value) ?? '';
57
+ const selected = panel.value === value;
58
+ const label = names.get(panel.value) ?? '';
59
+ whenHydrated(panel, () => {
60
+ panel.selected = selected;
61
+ panel.label = label;
62
+ });
56
63
  }
57
64
  }
58
65
  connectedCallback() {
@@ -1,5 +1,5 @@
1
1
  import { HelixElement } from '@fusion.dev/helix-core';
2
- import { type TemplateResult } from 'lit';
2
+ import { type PropertyValues, type TemplateResult } from 'lit';
3
3
  declare const HelixHeadlessTab_base: (new (...args: any[]) => import("@fusion.dev/helix-core").MixinSelectableItemInterface) & typeof HelixElement;
4
4
  /**
5
5
  * @summary One tab in a tab list.
@@ -27,6 +27,16 @@ declare const HelixHeadlessTab_base: (new (...args: any[]) => import("@fusion.de
27
27
  */
28
28
  export declare class HelixHeadlessTab extends HelixHeadlessTab_base {
29
29
  protected itemPart: string;
30
+ /**
31
+ * Asks for the render that follows a hydration, so the stop stops being every
32
+ * tab's.
33
+ *
34
+ * `tabindex` reads `childrenKnown` and a hydrating render is one where that is
35
+ * false, which is what keeps the hydrating render the server's. Nothing else
36
+ * would ask: the root writes an already-false `selected` onto an unchosen tab,
37
+ * which changes nothing and so re-renders nothing.
38
+ */
39
+ protected firstUpdated(changed: PropertyValues): void;
30
40
  protected render(): TemplateResult;
31
41
  }
32
42
  declare global {
@@ -11,6 +11,10 @@ let HelixHeadlessTab = class HelixHeadlessTab extends MixinSelectableItem(HelixE
11
11
  super(...arguments);
12
12
  this.itemPart = 'tab';
13
13
  }
14
+ firstUpdated(changed) {
15
+ super.firstUpdated(changed);
16
+ this.finishHydration();
17
+ }
14
18
  render() {
15
19
  return html `<button
16
20
  part="tab"
@@ -18,7 +22,7 @@ let HelixHeadlessTab = class HelixHeadlessTab extends MixinSelectableItem(HelixE
18
22
  type="button"
19
23
  aria-selected=${this.selected ? 'true' : 'false'}
20
24
  ?disabled=${this.disabled}
21
- tabindex=${this.selected ? '0' : '-1'}
25
+ tabindex=${this.selected || !this.childrenKnown ? '0' : '-1'}
22
26
  @click=${() => this.select()}
23
27
  >
24
28
  <slot></slot>
@@ -1,5 +1,5 @@
1
1
  import { HelixElement } from '@fusion.dev/helix-core';
2
- import { type TemplateResult } from 'lit';
2
+ import { type PropertyValues, type TemplateResult } from 'lit';
3
3
  declare const HelixHeadlessToggle_base: (new (...args: any[]) => import("@fusion.dev/helix-core").MixinSelectableItemInterface) & typeof HelixElement;
4
4
  /**
5
5
  * @summary One option in a toggle group.
@@ -65,6 +65,16 @@ export declare class HelixHeadlessToggle extends HelixHeadlessToggle_base {
65
65
  * hydration reports and does not patch up.
66
66
  */
67
67
  focusable: boolean;
68
+ /**
69
+ * Asks for the render that follows a hydration, so the stop stops being every
70
+ * choice's.
71
+ *
72
+ * `tabindex` reads `childrenKnown` and a hydrating render is one where that is
73
+ * false, which is what keeps the hydrating render the server's. Nothing else
74
+ * would ask: the group writes an already-false `focusable` onto every choice
75
+ * but one, which changes nothing and so re-renders nothing.
76
+ */
77
+ protected firstUpdated(changed: PropertyValues): void;
68
78
  protected render(): TemplateResult;
69
79
  }
70
80
  declare global {
@@ -18,6 +18,10 @@ let HelixHeadlessToggle = class HelixHeadlessToggle extends MixinSelectableItem(
18
18
  set defaultSelected(next) {
19
19
  this.toggleAttribute('selected', next);
20
20
  }
21
+ firstUpdated(changed) {
22
+ super.firstUpdated(changed);
23
+ this.finishHydration();
24
+ }
21
25
  render() {
22
26
  return html `<button
23
27
  part="item"
@@ -25,7 +29,7 @@ let HelixHeadlessToggle = class HelixHeadlessToggle extends MixinSelectableItem(
25
29
  role="radio"
26
30
  aria-checked=${this.selected ? 'true' : 'false'}
27
31
  ?disabled=${this.disabled}
28
- tabindex=${this.selected || this.focusable ? '0' : '-1'}
32
+ tabindex=${this.selected || this.focusable || !this.childrenKnown ? '0' : '-1'}
29
33
  @click=${() => this.select()}
30
34
  >
31
35
  <slot></slot>
@@ -104,14 +104,15 @@ let HelixHeadlessTreeItem = class HelixHeadlessTreeItem extends MixinControllerO
104
104
  render() {
105
105
  const linked = this.href !== '';
106
106
  const level = this.level ?? 1;
107
+ const depth = this.level ?? nothing;
107
108
  const expanded = this.branch ? (this.open ? 'true' : 'false') : nothing;
108
- const tabIndex = this.focusable ? '0' : '-1';
109
+ const tabIndex = this.focusable || !this.childrenKnown ? '0' : '-1';
109
110
  return html `<div
110
111
  part="item"
111
112
  role=${linked ? 'none' : 'treeitem'}
112
113
  aria-expanded=${linked ? nothing : expanded}
113
114
  aria-selected=${linked ? nothing : this.selected ? 'true' : 'false'}
114
- aria-level=${linked ? nothing : level}
115
+ aria-level=${linked ? nothing : depth}
115
116
  tabindex=${linked ? nothing : tabIndex}
116
117
  >
117
118
  ${linked
@@ -123,7 +124,7 @@ let HelixHeadlessTreeItem = class HelixHeadlessTreeItem extends MixinControllerO
123
124
  rel=${this.rel || nothing}
124
125
  aria-expanded=${expanded}
125
126
  aria-selected=${this.selected ? 'true' : 'false'}
126
- aria-level=${level}
127
+ aria-level=${depth}
127
128
  tabindex=${tabIndex}
128
129
  style="--tree-level: ${level}"
129
130
  @click=${this.handleRowClick}
@@ -4,7 +4,7 @@ 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 { closestInstance, customElement, HelixElement, inlineStep, MixinCollection, MixinControllerActive, scrollItemIntoView, } from '@fusion.dev/helix-core';
7
+ import { closestInstance, customElement, HelixElement, inlineStep, MixinCollection, MixinControllerActive, scrollItemIntoView, whenHydrated, } from '@fusion.dev/helix-core';
8
8
  import { html, nothing } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
10
  import { HelixHeadlessTreeItem } from '../item/tree-item.js';
@@ -101,8 +101,8 @@ let HelixHeadlessTree = class HelixHeadlessTree extends MixinCollection(MixinCon
101
101
  const held = visible.find((item) => item.focusable);
102
102
  const stop = held ?? chosen;
103
103
  for (const item of this.items) {
104
- item.selected = item.value !== '' && item.value === this.active;
105
- item.focusable = item === stop;
104
+ const selected = item.value !== '' && item.value === this.active;
105
+ const focusable = item === stop;
106
106
  let level = 1;
107
107
  let parent = item.parentElement;
108
108
  while (parent && parent !== this) {
@@ -111,7 +111,11 @@ let HelixHeadlessTree = class HelixHeadlessTree extends MixinCollection(MixinCon
111
111
  }
112
112
  parent = parent.parentElement;
113
113
  }
114
- item.level = level;
114
+ whenHydrated(item, () => {
115
+ item.selected = selected;
116
+ item.focusable = focusable;
117
+ item.level = level;
118
+ });
115
119
  }
116
120
  }
117
121
  move(from, delta) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@fusion.dev/helix-headless",
3
3
  "description": "Headless component tier. Structure, accessibility, state, and form participation. No styling.",
4
- "version": "0.53.0",
4
+ "version": "0.53.1",
5
5
  "license": "MIT",
6
6
  "author": "Ross Sheppard <ross.sheppard@fusion.dev>",
7
7
  "repository": {
@@ -46,14 +46,14 @@
46
46
  },
47
47
  "bugs": "https://github.com/project-fusion/fusion/issues",
48
48
  "dependencies": {
49
- "@fusion.dev/helix-core": "0.53.0",
50
- "@fusion.dev/helix-icons": "0.53.0",
49
+ "@fusion.dev/helix-core": "0.53.1",
50
+ "@fusion.dev/helix-icons": "0.53.1",
51
51
  "uqr": "^0.1.3"
52
52
  },
53
53
  "devDependencies": {
54
54
  "@custom-elements-manifest/analyzer": "^0.11.0",
55
- "@fusion.dev/helix-cem": "0.53.0",
56
- "@fusion.dev/helix-testing": "0.53.0",
55
+ "@fusion.dev/helix-cem": "0.53.1",
56
+ "@fusion.dev/helix-testing": "0.53.1",
57
57
  "@fusion.dev/scripts": "0.0.0-dev",
58
58
  "@happy-dom/global-registrator": "^20.0.0",
59
59
  "@lit-labs/ssr": "^4.1.0",