@fusion.dev/helix-headless 0.21.0 → 0.23.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.
@@ -3777,7 +3777,7 @@
3777
3777
  "name": ""
3778
3778
  },
3779
3779
  {
3780
- "description": "After it, for a count or a mark.",
3780
+ "description": "After the label and outside the link, so it may be a control: this is where a switcher's menu trigger goes. A count or a mark is equally at home here and is no longer read into the link's accessible name.",
3781
3781
  "name": "end"
3782
3782
  },
3783
3783
  {
@@ -38926,6 +38926,23 @@
38926
38926
  "default": "false",
38927
38927
  "description": "Whether anything is slotted into `submenu`. Read after first render.\n\nState rather than a getter over `children`. A getter is read during\n`render()` with nothing to re-read it, so a submenu arriving after the\nrow's first render produces no caret and no `aria-haspopup` while selecting\nthe row still drills in, which is what a framework filling a submenu on\ndemand hits every time. `Tree.Item` backs `branch` the same way for the\nsame reason.\n\nKept in step by `readSubmenu`, which is bound in the template rather than\nonce at first render. See the note there."
38928
38928
  },
38929
+ {
38930
+ "kind": "field",
38931
+ "name": "start",
38932
+ "type": {
38933
+ "text": "boolean"
38934
+ },
38935
+ "privacy": "private",
38936
+ "default": "false",
38937
+ "description": "Whether anything is slotted before the label.\n\nThe leading box is drawn only where there is something to put in it, the\nsame way the trailing mark is. What keeps the labels on one edge is the\ntrack the menu reserves for the page, not a box on every row, so a row with\nnothing here takes no box and still lines up with its neighbors.\n\nRead from the slot rather than from the light DOM, because the light DOM\nanswer is not reactive: a consumer who adds an icon to one row after first\nrender changes nothing this element is told about, and `slotchange` is\nexactly the thing that is."
38938
+ },
38939
+ {
38940
+ "kind": "field",
38941
+ "name": "readStart",
38942
+ "privacy": "private",
38943
+ "readonly": true,
38944
+ "description": "Re-reads whether anything is before the label.\n\nThe slot itself is always rendered and it is the box around it that comes\nand goes, which is what makes this safe where `readSubmenu` needed the note\nit carries: there is one `slot[name=\"start\"]` in one template branch, so it\nis never rebuilt and never fires on its way out."
38945
+ },
38929
38946
  {
38930
38947
  "kind": "field",
38931
38948
  "name": "readSubmenu",
@@ -39528,6 +39545,17 @@
39528
39545
  "privacy": "private",
39529
39546
  "readonly": true
39530
39547
  },
39548
+ {
39549
+ "kind": "method",
39550
+ "name": "markColumns",
39551
+ "privacy": "private",
39552
+ "return": {
39553
+ "type": {
39554
+ "text": "void"
39555
+ }
39556
+ },
39557
+ "description": "Says whether the page showing reserves a column either side of its labels.\n\n**A fact no row can answer for itself**, which is why the columns were\nunconditional. A row knows about its own start slot and nothing about its\nneighbors, so a page where one row has an icon and the rest do not draws two\ndifferent left edges for its text. Reserving on every row instead charged\nevery menu for a column nothing in it was ever going to fill: measured on a\npage of two plain rows, the text started 36 pixels into the row, of which 12\nwas the row's own padding and 24 a leading column and its seam. The same\npage now starts at 12, and a page where one row does have a mark still\nstarts both of its rows at 36.\n\n**Per page rather than per menu**, because a submenu is a different set of\nrows: a page of checkable filters under a page of plain ones reserves on one\nand not on the other, and asking the whole tree would hand the plain page\nthe filters' column. `Notifications` answers the same question for its media\ncolumn the same way, and for the same reason.\n\nRead off the light DOM rather than off the rows, so it does not depend on\nwhich children have upgraded by the time this runs."
39558
+ },
39531
39559
  {
39532
39560
  "kind": "method",
39533
39561
  "name": "mark",
@@ -37,7 +37,9 @@ import { type PropertyValues, type TemplateResult } from 'lit';
37
37
  * @slot - The step's label. Without an `href` this may be the consumer's own
38
38
  * anchor, styled with `data-link="quiet"`, since a slotted anchor belongs to the
39
39
  * outer tree and cannot be reached from this component's styles.
40
- * @slot end - After it, for a count or a mark.
40
+ * @slot end - After the label and outside the link, so it may be a control: this is where a
41
+ * switcher's menu trigger goes. A count or a mark is equally at home here and is no longer
42
+ * read into the link's accessible name.
41
43
  * @slot separator - Replaces the chevron shown before this item.
42
44
  * @slot start - Before the label, for an icon.
43
45
  *
@@ -29,7 +29,7 @@ let HelixHeadlessBreadcrumb = class HelixHeadlessBreadcrumb extends HelixElement
29
29
  >`;
30
30
  }
31
31
  render() {
32
- const label = html `<slot name="start"></slot><slot></slot><slot name="end"></slot>`;
32
+ const label = html `<slot name="start"></slot><slot></slot>`;
33
33
  const linked = (this.href ?? '') !== '' && !this.current;
34
34
  return html `<span part="separator" aria-hidden="true"
35
35
  >${this.renderSeparator()}</span
@@ -41,7 +41,7 @@ let HelixHeadlessBreadcrumb = class HelixHeadlessBreadcrumb extends HelixElement
41
41
  rel=${this.rel || nothing}
42
42
  >${label}</a
43
43
  >`
44
- : html `<span part="link">${label}</span>`}`;
44
+ : html `<span part="link">${label}</span>`}<slot name="end"></slot>`;
45
45
  }
46
46
  };
47
47
  __decorate([
@@ -144,7 +144,30 @@ export declare class HelixHeadlessMenuItem extends HelixElement {
144
144
  * once at first render. See the note there.
145
145
  */
146
146
  private submenu;
147
+ /**
148
+ * Whether anything is slotted before the label.
149
+ *
150
+ * The leading box is drawn only where there is something to put in it, the
151
+ * same way the trailing mark is. What keeps the labels on one edge is the
152
+ * track the menu reserves for the page, not a box on every row, so a row with
153
+ * nothing here takes no box and still lines up with its neighbors.
154
+ *
155
+ * Read from the slot rather than from the light DOM, because the light DOM
156
+ * answer is not reactive: a consumer who adds an icon to one row after first
157
+ * render changes nothing this element is told about, and `slotchange` is
158
+ * exactly the thing that is.
159
+ */
160
+ private start;
147
161
  protected firstUpdated(changed: PropertyValues): void;
162
+ /**
163
+ * Re-reads whether anything is before the label.
164
+ *
165
+ * The slot itself is always rendered and it is the box around it that comes
166
+ * and goes, which is what makes this safe where `readSubmenu` needed the note
167
+ * it carries: there is one `slot[name="start"]` in one template branch, so it
168
+ * is never rebuilt and never fires on its way out.
169
+ */
170
+ private readonly readStart;
148
171
  /**
149
172
  * Re-reads whether anything is in the submenu, from whichever slot is current.
150
173
  *
@@ -24,6 +24,11 @@ let HelixHeadlessMenuItem = class HelixHeadlessMenuItem extends HelixElement {
24
24
  this.value = '';
25
25
  this.isMenuItem = true;
26
26
  this.submenu = false;
27
+ this.start = false;
28
+ this.readStart = () => {
29
+ const slot = this.renderRoot.querySelector('slot[name="start"]');
30
+ this.start = (slot?.assignedNodes().length ?? 0) > 0;
31
+ };
27
32
  this.readSubmenu = () => {
28
33
  const slot = this.renderRoot.querySelector('slot[name="submenu"]');
29
34
  this.submenu = (slot?.assignedElements().length ?? 0) > 0;
@@ -53,6 +58,7 @@ let HelixHeadlessMenuItem = class HelixHeadlessMenuItem extends HelixElement {
53
58
  firstUpdated(changed) {
54
59
  super.firstUpdated(changed);
55
60
  this.readSubmenu();
61
+ this.readStart();
56
62
  }
57
63
  focus() {
58
64
  this.renderRoot.querySelector('[part="item"]')?.focus();
@@ -83,7 +89,9 @@ let HelixHeadlessMenuItem = class HelixHeadlessMenuItem extends HelixElement {
83
89
  const linked = this.href !== '' && !this.disabled;
84
90
  const role = this.checkable || this.checked ? 'menuitemcheckbox' : 'menuitem';
85
91
  const checkedState = this.checkable || this.checked ? (this.checked ? 'true' : 'false') : nothing;
86
- const inside = html `<span part="start"><slot name="start"></slot></span>
92
+ const inside = html `<span part="start" ?data-empty=${!this.start}
93
+ ><slot name="start" @slotchange=${this.readStart}></slot
94
+ ></span>
87
95
  <slot></slot>
88
96
  ${submenu
89
97
  ? html `<svg
@@ -104,7 +112,8 @@ let HelixHeadlessMenuItem = class HelixHeadlessMenuItem extends HelixElement {
104
112
  >
105
113
  ${unsafeSVG(check.content)}
106
114
  </svg>`
107
- : html `<span part="mark" aria-hidden="true"></span>`}`;
115
+ :
116
+ nothing}`;
108
117
  return html `${linked
109
118
  ? html `<a
110
119
  part="item"
@@ -165,6 +174,9 @@ __decorate([
165
174
  __decorate([
166
175
  state()
167
176
  ], HelixHeadlessMenuItem.prototype, "submenu", void 0);
177
+ __decorate([
178
+ state()
179
+ ], HelixHeadlessMenuItem.prototype, "start", void 0);
168
180
  HelixHeadlessMenuItem = __decorate([
169
181
  customElement('helix-headless-menu-item')
170
182
  ], HelixHeadlessMenuItem);
@@ -126,6 +126,29 @@ export declare class HelixHeadlessMenu extends HelixHeadlessMenu_base {
126
126
  connectedCallback(): void;
127
127
  disconnectedCallback(): void;
128
128
  protected updated(changed: PropertyValues): void;
129
+ /**
130
+ * Says whether the page showing reserves a column either side of its labels.
131
+ *
132
+ * **A fact no row can answer for itself**, which is why the columns were
133
+ * unconditional. A row knows about its own start slot and nothing about its
134
+ * neighbors, so a page where one row has an icon and the rest do not draws two
135
+ * different left edges for its text. Reserving on every row instead charged
136
+ * every menu for a column nothing in it was ever going to fill: measured on a
137
+ * page of two plain rows, the text started 36 pixels into the row, of which 12
138
+ * was the row's own padding and 24 a leading column and its seam. The same
139
+ * page now starts at 12, and a page where one row does have a mark still
140
+ * starts both of its rows at 36.
141
+ *
142
+ * **Per page rather than per menu**, because a submenu is a different set of
143
+ * rows: a page of checkable filters under a page of plain ones reserves on one
144
+ * and not on the other, and asking the whole tree would hand the plain page
145
+ * the filters' column. `Notifications` answers the same question for its media
146
+ * column the same way, and for the same reason.
147
+ *
148
+ * Read off the light DOM rather than off the rows, so it does not depend on
149
+ * which children have upgraded by the time this runs.
150
+ */
151
+ private markColumns;
129
152
  /**
130
153
  * Walks the tree marking which rows are on the page showing.
131
154
  *
@@ -159,6 +159,12 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
159
159
  }
160
160
  this.mark(item, this.drilldown.atRoot);
161
161
  }
162
+ this.markColumns();
163
+ }
164
+ markColumns() {
165
+ const rows = this.drilldown.rows;
166
+ this.toggleState('start', rows.some((row) => row.querySelector(':scope > [slot="start"]') !== null));
167
+ this.toggleState('mark', rows.some((row) => row.checkable || row.checked || this.drilldown.hasPage(row)));
162
168
  }
163
169
  mark(item, onPage) {
164
170
  item.onPage = onPage;
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.21.0",
4
+ "version": "0.23.0",
5
5
  "license": "MIT",
6
6
  "author": "Ross Sheppard <ross.sheppard@fusion.dev>",
7
7
  "repository": {
@@ -45,14 +45,14 @@
45
45
  },
46
46
  "bugs": "https://github.com/project-fusion/fusion/issues",
47
47
  "dependencies": {
48
- "@fusion.dev/helix-core": "0.21.0",
49
- "@fusion.dev/helix-icons": "0.21.0",
48
+ "@fusion.dev/helix-core": "0.23.0",
49
+ "@fusion.dev/helix-icons": "0.23.0",
50
50
  "uqr": "^0.1.3"
51
51
  },
52
52
  "devDependencies": {
53
53
  "@custom-elements-manifest/analyzer": "^0.11.0",
54
- "@fusion.dev/helix-cem": "0.21.0",
55
- "@fusion.dev/helix-testing": "0.21.0",
54
+ "@fusion.dev/helix-cem": "0.23.0",
55
+ "@fusion.dev/helix-testing": "0.23.0",
56
56
  "@fusion.dev/scripts": "0.0.0-dev",
57
57
  "@happy-dom/global-registrator": "^20.0.0",
58
58
  "@lit-labs/ssr": "^3.3.0",
package/types/jsx.d.ts CHANGED
@@ -16544,7 +16544,7 @@ export type CustomElements = {
16544
16544
  * Areas where markup can be added to the component.
16545
16545
  *
16546
16546
  * - `(default)`: The step's label. Without an `href` this may be the consumer's own anchor, styled with `data-link="quiet"`, since a slotted anchor belongs to the outer tree and cannot be reached from this component's styles.
16547
- * - `end`: After it, for a count or a mark.
16547
+ * - `end`: After the label and outside the link, so it may be a control: this is where a switcher's menu trigger goes. A count or a mark is equally at home here and is no longer read into the link's accessible name.
16548
16548
  * - `separator`: Replaces the chevron shown before this item.
16549
16549
  * - `start`: Before the label, for an icon.
16550
16550
  *
@@ -31007,7 +31007,7 @@ export type CustomElementsSolidJs = {
31007
31007
  * Areas where markup can be added to the component.
31008
31008
  *
31009
31009
  * - `(default)`: The step's label. Without an `href` this may be the consumer's own anchor, styled with `data-link="quiet"`, since a slotted anchor belongs to the outer tree and cannot be reached from this component's styles.
31010
- * - `end`: After it, for a count or a mark.
31010
+ * - `end`: After the label and outside the link, so it may be a control: this is where a switcher's menu trigger goes. A count or a mark is equally at home here and is no longer read into the link's accessible name.
31011
31011
  * - `separator`: Replaces the chevron shown before this item.
31012
31012
  * - `start`: Before the label, for an icon.
31013
31013
  *