@fusion.dev/helix-headless 0.153.0 → 0.155.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 (81) hide show
  1. package/custom-elements.json +1885 -140
  2. package/dist/accordion/item/accordion-item.d.ts +15 -0
  3. package/dist/accordion/item/accordion-item.js +12 -1
  4. package/dist/accordion/root/accordion-root.d.ts +2 -1
  5. package/dist/accordion/root/accordion-root.js +1 -0
  6. package/dist/badge/root/badge-root.d.ts +15 -2
  7. package/dist/badge/root/badge-root.js +20 -3
  8. package/dist/breadcrumbs/breadcrumb/breadcrumb.d.ts +36 -3
  9. package/dist/breadcrumbs/breadcrumb/breadcrumb.js +42 -2
  10. package/dist/breadcrumbs/root/breadcrumbs-root.d.ts +15 -5
  11. package/dist/breadcrumbs/root/breadcrumbs-root.js +29 -4
  12. package/dist/button-group/root/button-group-root.d.ts +2 -0
  13. package/dist/button-group/root/button-group-root.js +1 -0
  14. package/dist/card/root/card-root.d.ts +31 -11
  15. package/dist/card/root/card-root.js +25 -11
  16. package/dist/code/root/code-root.d.ts +7 -0
  17. package/dist/code/root/code-root.js +1 -1
  18. package/dist/code-block/root/code-block-root.d.ts +37 -1
  19. package/dist/code-block/root/code-block-root.js +38 -5
  20. package/dist/color-input/root/color-input-root.d.ts +9 -3
  21. package/dist/color-input/root/color-input-root.js +3 -1
  22. package/dist/color-picker/channels.d.ts +5 -3
  23. package/dist/color-picker/channels.js +79 -21
  24. package/dist/color-picker/root/color-picker-root.d.ts +58 -5
  25. package/dist/color-picker/root/color-picker-root.js +75 -13
  26. package/dist/date-input/root/date-input-root.d.ts +1 -0
  27. package/dist/date-input/root/date-input-root.js +2 -1
  28. package/dist/date-picker/root/date-picker-root.d.ts +43 -13
  29. package/dist/date-picker/root/date-picker-root.js +54 -31
  30. package/dist/element-events.generated.js +2 -1
  31. package/dist/empty/root/empty-root.d.ts +19 -1
  32. package/dist/empty/root/empty-root.js +13 -4
  33. package/dist/icon/fetch.d.ts +20 -0
  34. package/dist/icon/fetch.js +70 -0
  35. package/dist/icon/root/icon-root.d.ts +27 -5
  36. package/dist/icon/root/icon-root.js +41 -17
  37. package/dist/navigation/root/navigation-root.d.ts +26 -0
  38. package/dist/navigation/root/navigation-root.js +58 -1
  39. package/dist/quote/root/quote-root.d.ts +17 -1
  40. package/dist/quote/root/quote-root.js +15 -2
  41. package/dist/select/root/select-root.d.ts +29 -3
  42. package/dist/select/root/select-root.js +34 -9
  43. package/dist/skeleton/root/skeleton-root.d.ts +7 -0
  44. package/dist/skeleton/root/skeleton-root.js +4 -1
  45. package/dist/slider/root/slider-root.d.ts +71 -9
  46. package/dist/slider/root/slider-root.js +129 -42
  47. package/dist/speed-dial/item/speed-dial-item.d.ts +29 -1
  48. package/dist/speed-dial/item/speed-dial-item.js +48 -7
  49. package/dist/speed-dial/root/speed-dial-root.d.ts +32 -6
  50. package/dist/speed-dial/root/speed-dial-root.js +50 -5
  51. package/dist/splitter/root/splitter-root.d.ts +22 -0
  52. package/dist/splitter/root/splitter-root.js +36 -7
  53. package/dist/stat/group/stat-group.d.ts +23 -4
  54. package/dist/stat/group/stat-group.js +13 -1
  55. package/dist/stat/root/stat-root.d.ts +46 -1
  56. package/dist/stat/root/stat-root.js +29 -5
  57. package/dist/steps/reach.d.ts +5 -1
  58. package/dist/steps/reach.js +2 -2
  59. package/dist/steps/root/steps-root.d.ts +37 -2
  60. package/dist/steps/root/steps-root.js +49 -7
  61. package/dist/steps/step/step.d.ts +6 -0
  62. package/dist/steps/step/step.js +4 -0
  63. package/dist/tabs/root/tabs-root.d.ts +37 -0
  64. package/dist/tabs/root/tabs-root.js +92 -2
  65. package/dist/tabs/tab/tab.d.ts +38 -2
  66. package/dist/tabs/tab/tab.js +45 -4
  67. package/dist/tag/group/tag-group.d.ts +29 -3
  68. package/dist/tag/group/tag-group.js +42 -11
  69. package/dist/toggle/root/toggle-root.d.ts +2 -0
  70. package/dist/toggle/root/toggle-root.js +1 -1
  71. package/dist/tree/item/tree-item.d.ts +10 -0
  72. package/dist/tree/item/tree-item.js +12 -0
  73. package/dist/tree/root/tree-root.d.ts +40 -1
  74. package/dist/tree/root/tree-root.js +120 -4
  75. package/dist/tree/selection.d.ts +6 -0
  76. package/dist/tree/selection.js +11 -0
  77. package/lint.json +1 -1
  78. package/package.json +5 -5
  79. package/types/jsx.d.ts +1412 -207
  80. package/types/vue.d.ts +294 -65
  81. package/types/words.d.ts +49 -1
@@ -25,6 +25,10 @@ declare const HelixHeadlessAccordionItem_base: (new (...args: any[]) => import("
25
25
  * and a closed section is `hidden` and `inert` instead, and find in page skips
26
26
  * it.
27
27
  *
28
+ * A section whose trigger has no words, with nothing in its `summary` slot,
29
+ * is announced as a bare button, and a development build warns once with the
30
+ * kind `missing-name`.
31
+ *
28
32
  * While it is open, the content part carries the content's height as
29
33
  * `--accordion-content-height`, as Radix's accordion does, for a stylesheet
30
34
  * that animates the section open with keyframes of its own.
@@ -51,6 +55,7 @@ declare const HelixHeadlessAccordionItem_base: (new (...args: any[]) => import("
51
55
  *
52
56
  * @cssproperty --accordion-content-height - The height of the content, in pixels, written on the `content` part while the section is open.
53
57
  *
58
+ * @cssstate closing - While the item has closed and its content is still drawn, until the content part's animations end.
54
59
  * @cssstate open - While the item is open.
55
60
  */
56
61
  export declare class HelixHeadlessAccordionItem extends HelixHeadlessAccordionItem_base {
@@ -88,6 +93,11 @@ export declare class HelixHeadlessAccordionItem extends HelixHeadlessAccordionIt
88
93
  * for the reason `groupHeadingLevel` is a property.
89
94
  */
90
95
  groupHiddenUntilFound: boolean;
96
+ /**
97
+ * @internal The accordion's `mode`, written by the accordion, so the trigger
98
+ * of the open section says it cannot close under `single`.
99
+ */
100
+ groupMode: 'single' | 'single-collapsible' | undefined;
91
101
  /**
92
102
  * The level of the heading around the trigger, from 1 to 6.
93
103
  *
@@ -155,6 +165,7 @@ export declare class HelixHeadlessAccordionItem extends HelixHeadlessAccordionIt
155
165
  * and runs before the next paint or key press.
156
166
  */
157
167
  private readonly handleBeforeMatch;
168
+ protected firstUpdated(changed: PropertyValues): void;
158
169
  /** Whether the item was open at the last update, and `undefined` before one. */
159
170
  private wasOpen;
160
171
  /**
@@ -239,6 +250,10 @@ export declare class HelixHeadlessAccordionItem extends HelixHeadlessAccordionIt
239
250
  * the accessibility tree by itself, and an inert region is never searched.
240
251
  * It leaves the region's own box in the tree, so a closed findable section
241
252
  * drops `role="region"` and its name until it opens.
253
+ *
254
+ * Under `mode="single"` the open section's trigger is `aria-disabled`, as in
255
+ * Radix, because a press cannot close the last open section. It stays
256
+ * focusable, so the arrow keys still reach it.
242
257
  */
243
258
  protected render(): TemplateResult;
244
259
  }
@@ -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 { animationsFinished, closestInstance, customElement, HelixElement, headingLevel, MixinControllerOpen, onUnlessFalse, } from '@fusion.dev/helix-core';
7
+ import { animationsFinished, auditName, closestInstance, customElement, HelixElement, headingLevel, MixinControllerOpen, onUnlessFalse, } from '@fusion.dev/helix-core';
8
8
  import { caretDown } from '@fusion.dev/helix-icons';
9
9
  import { html, nothing } from 'lit';
10
10
  import { property, state } from 'lit/decorators.js';
@@ -18,6 +18,7 @@ let HelixHeadlessAccordionItem = class HelixHeadlessAccordionItem extends MixinC
18
18
  this.disabled = false;
19
19
  this.groupHeadingLevel = undefined;
20
20
  this.groupHiddenUntilFound = true;
21
+ this.groupMode = undefined;
21
22
  this.closing = false;
22
23
  this.handleBeforeMatch = (event) => {
23
24
  if (this.open || this.requestOpen(true)) {
@@ -76,6 +77,7 @@ let HelixHeadlessAccordionItem = class HelixHeadlessAccordionItem extends MixinC
76
77
  if (changed.has('open')) {
77
78
  this.closing = !this.open && changed.get('open') === true && this.findable;
78
79
  }
80
+ this.toggleState('closing', this.closing);
79
81
  }
80
82
  finishClosing() {
81
83
  void animationsFinished(this.partNamed('content')).then(() => {
@@ -84,6 +86,10 @@ let HelixHeadlessAccordionItem = class HelixHeadlessAccordionItem extends MixinC
84
86
  }
85
87
  });
86
88
  }
89
+ firstUpdated(changed) {
90
+ super.firstUpdated(changed);
91
+ auditName(this, () => this.renderRoot.querySelector(`#${TRIGGER_ID}`), 'Write its title in the `summary` slot.');
92
+ }
87
93
  get accordion() {
88
94
  return closestInstance(this, HelixHeadlessAccordion);
89
95
  }
@@ -129,12 +135,14 @@ let HelixHeadlessAccordionItem = class HelixHeadlessAccordionItem extends MixinC
129
135
  const shown = open || this.closing;
130
136
  const landmark = shown || !this.findable;
131
137
  const level = headingLevel(this.headingLevel ?? this.groupHeadingLevel);
138
+ const locked = open && this.groupMode === 'single';
132
139
  return html `
133
140
  <div part="container">
134
141
  <div part="heading" role="heading" aria-level=${level}>
135
142
  <button
136
143
  id=${TRIGGER_ID}
137
144
  aria-controls=${CONTENT_ID}
145
+ aria-disabled=${locked ? 'true' : nothing}
138
146
  aria-expanded=${open ? 'true' : 'false'}
139
147
  part="trigger"
140
148
  type="button"
@@ -175,6 +183,9 @@ __decorate([
175
183
  __decorate([
176
184
  property({ attribute: false })
177
185
  ], HelixHeadlessAccordionItem.prototype, "groupHiddenUntilFound", void 0);
186
+ __decorate([
187
+ property({ attribute: false })
188
+ ], HelixHeadlessAccordionItem.prototype, "groupMode", void 0);
178
189
  __decorate([
179
190
  property({ attribute: 'heading-level', type: Number })
180
191
  ], HelixHeadlessAccordionItem.prototype, "headingLevel", void 0);
@@ -20,7 +20,8 @@ import { HelixHeadlessAccordionItem } from '../item/accordion-item.js';
20
20
  * Keys, on a trigger: Up and Down Arrow move to the previous and the next
21
21
  * trigger, and so do Left and Right, wrapping at the ends; Home and End go to
22
22
  * the first and the last. Enter and Space open and close the section. Disabled
23
- * sections are passed over.
23
+ * sections are passed over. Under `single` the open section's trigger is
24
+ * `aria-disabled`, because a press cannot close it.
24
25
  *
25
26
  * Each trigger is a button with `aria-expanded` and `aria-controls`, inside a
26
27
  * heading, level 3 unless `heading-level` says otherwise, so the sections are
@@ -32,6 +32,7 @@ let HelixHeadlessAccordion = class HelixHeadlessAccordion extends HelixElement {
32
32
  whenHydrated(item, () => {
33
33
  item.groupHeadingLevel = this.headingLevel;
34
34
  item.groupHiddenUntilFound = this.hiddenUntilFound;
35
+ item.groupMode = this.mode;
35
36
  });
36
37
  }
37
38
  if (this.mode === undefined) {
@@ -35,6 +35,12 @@ import { type PropertyValues, type TemplateResult } from 'lit';
35
35
  * button "Inbox 8 unread", and stays current as the count changes. The styled
36
36
  * tier's `placement="inline"` draws the mark in the line for that case.
37
37
  *
38
+ * **An icon goes inside the mark** through the `icon` slot, before the count,
39
+ * as Web Awesome's badge takes one at its start. With no `value` the icon is
40
+ * the whole mark, such as a check on an avatar, drawn where the dot would be.
41
+ * The icon is hidden from screen readers: the count and `label` are what the
42
+ * mark says.
43
+ *
38
44
  * @since 0.1.0
39
45
  * @status stable
40
46
  * @category markers
@@ -42,12 +48,15 @@ import { type PropertyValues, type TemplateResult } from 'lit';
42
48
  * @documentation https://fusion.dev/helix/components/badge
43
49
  *
44
50
  * @slot - The element being decorated.
51
+ * @slot icon - An icon inside the mark, before the count, or the whole mark when there is no value. Hidden from screen readers.
45
52
  *
46
- * @csspart indicator - The count, or the dot when there is no value.
53
+ * @csspart icon - The slot the icon arrives through, inside the indicator. A slot draws no box, so it passes inherited properties, such as color, to the icon.
54
+ * @csspart indicator - The count, or the dot when there is no value. Carries `data-dot` when there is no value, `data-single` when the count is one character, and `data-blank` when the value is zero.
47
55
  *
48
56
  * @cssstate blank - The value is zero, so there is no mark.
49
57
  * @cssstate capped - The count is past `max`, so the mark shows `{max}+`.
50
- * @cssstate dot - There is no value, so the mark is a dot.
58
+ * @cssstate dot - There is no value and no icon, so the mark is a dot.
59
+ * @cssstate has-icon - Something is slotted into `icon`, so it is drawn inside the mark.
51
60
  */
52
61
  export declare class HelixHeadlessBadge extends HelixElement {
53
62
  /**
@@ -98,6 +107,10 @@ export declare class HelixHeadlessBadge extends HelixElement {
98
107
  * is showing a count must not lose the count for being handed one.
99
108
  */
100
109
  value: number | string;
110
+ /** Whether something is slotted into `icon`, read from the children. */
111
+ private hasIcon;
112
+ /** Reads whether an icon is slotted, from the children where they can be known. */
113
+ private readonly readIcon;
101
114
  /** The value as text, whatever it was set to. */
102
115
  private get given();
103
116
  /** Whether the value counts nothing, in which case there is no mark. */
@@ -6,7 +6,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
6
6
  };
7
7
  import { customElement, focusTarget, HelixElement, wordsLocale } from '@fusion.dev/helix-core';
8
8
  import { html, nothing } from 'lit';
9
- import { property } from 'lit/decorators.js';
9
+ import { property, state } from 'lit/decorators.js';
10
10
  import { badgeBlank, badgeCapped, badgeText, badgeWords } from '../count.js';
11
11
  let HelixHeadlessBadge = class HelixHeadlessBadge extends HelixElement {
12
12
  constructor() {
@@ -14,6 +14,10 @@ let HelixHeadlessBadge = class HelixHeadlessBadge extends HelixElement {
14
14
  this.label = '';
15
15
  this.locale = '';
16
16
  this.value = '';
17
+ this.hasIcon = false;
18
+ this.readIcon = () => {
19
+ this.hasIcon = this.knownChildren.some((child) => child.slot === 'icon');
20
+ };
17
21
  this.handleSlotchange = () => {
18
22
  this.describe();
19
23
  };
@@ -81,9 +85,13 @@ let HelixHeadlessBadge = class HelixHeadlessBadge extends HelixElement {
81
85
  }
82
86
  willUpdate(changed) {
83
87
  super.willUpdate(changed);
88
+ if (!this.hasUpdated) {
89
+ this.readIcon();
90
+ }
84
91
  this.toggleState('capped', this.capped);
85
- this.toggleState('dot', this.given.trim() === '');
92
+ this.toggleState('dot', this.given.trim() === '' && !this.hasIcon);
86
93
  this.toggleState('blank', this.blank);
94
+ this.toggleState('has-icon', this.hasIcon);
87
95
  }
88
96
  updated(changed) {
89
97
  super.updated(changed);
@@ -97,10 +105,16 @@ let HelixHeadlessBadge = class HelixHeadlessBadge extends HelixElement {
97
105
  <span
98
106
  part="indicator"
99
107
  data-blank=${blank ? '' : nothing}
108
+ data-dot=${this.given.trim() === '' ? '' : nothing}
100
109
  data-single=${text.length <= 1 ? '' : nothing}
101
110
  role="status"
102
111
  aria-label=${announced ? this.words : nothing}
103
- >${blank ? html `<span aria-hidden="true">${text}</span>` : text}</span
112
+ ><slot
113
+ aria-hidden="true"
114
+ name="icon"
115
+ part="icon"
116
+ @slotchange=${this.readIcon}
117
+ ></slot>${blank ? html `<span aria-hidden="true">${text}</span>` : text}</span
104
118
  >`;
105
119
  }
106
120
  };
@@ -119,6 +133,9 @@ __decorate([
119
133
  __decorate([
120
134
  property()
121
135
  ], HelixHeadlessBadge.prototype, "value", void 0);
136
+ __decorate([
137
+ state()
138
+ ], HelixHeadlessBadge.prototype, "hasIcon", void 0);
122
139
  HelixHeadlessBadge = __decorate([
123
140
  customElement('helix-headless-badge')
124
141
  ], HelixHeadlessBadge);
@@ -1,5 +1,5 @@
1
1
  import { HelixElement } from '@fusion.dev/helix-core';
2
- import { type CSSResultGroup, type PropertyValues, type TemplateResult } from 'lit';
2
+ import { type CSSResultGroup, nothing, type PropertyValues, type TemplateResult } from 'lit';
3
3
  /**
4
4
  * @summary One step in a breadcrumb trail.
5
5
  *
@@ -11,8 +11,11 @@ import { type CSSResultGroup, type PropertyValues, type TemplateResult } from 'l
11
11
  * Set `href` to make the whole crumb an anchor, which works with Enter, middle
12
12
  * click, and the status bar, and before any JavaScript runs. A router cannot
13
13
  * reach an anchor inside a shadow root, so intercept the click for client-side
14
- * navigation, as with `Tree.Item` and `List.Item`. A trail that collapses also
15
- * reads `href` to rebuild its hidden crumbs elsewhere.
14
+ * navigation, as with `Tree.Item` and `List.Item`: a plain press on the link
15
+ * sends `helix-select` with the `href` as its value first, and canceling it
16
+ * keeps the browser from following, so a client-side router follows it
17
+ * instead, as React Aria's `onAction` lets a router do. A press with a
18
+ * modifier opens the link where the browser opens it and sends nothing.
16
19
  *
17
20
  * Without `href`, the crumb renders a plain `<span>` around what you slot,
18
21
  * including your own anchor.
@@ -37,12 +40,18 @@ import { type CSSResultGroup, type PropertyValues, type TemplateResult } from 'l
37
40
  * @slot separator - Replaces the chevron shown before this item.
38
41
  * @slot start - Content before the label, such as an icon.
39
42
  *
43
+ * @event {HelixSelectEvent} helix-select - A plain press on the crumb's link, with its `href` as the value, before the browser follows it. Cancelable: cancel it to keep the browser from following, as a client-side router does.
44
+ *
45
+ * @csspart expand - The button after the first crumb of a folded slotted trail, which shows the folded crumbs.
46
+ * @csspart fold-separator - The chevron before that button.
40
47
  * @csspart link - The crumb itself, which is an `<a>` where `href` is set.
41
48
  * @csspart separator - The separator preceding the item.
42
49
  * @csspart separator-mark - The chevron between crumbs, shown when no separator is slotted.
43
50
  *
51
+ * @cssstate current - While the crumb is the current page, marked `current` or the last of a trail with none marked.
44
52
  * @cssstate end - While the `end` slot holds something, so a stylesheet can join
45
53
  * the label and what follows it into one control.
54
+ * @cssstate folded - While a trail longer than its `max-items` has folded the crumb away. It takes no room.
46
55
  */
47
56
  export declare class HelixHeadlessBreadcrumb extends HelixElement {
48
57
  static styles: CSSResultGroup;
@@ -50,6 +59,18 @@ export declare class HelixHeadlessBreadcrumb extends HelixElement {
50
59
  * Marks this item as the page you are on.
51
60
  */
52
61
  current: boolean;
62
+ /**
63
+ * @internal Set by the trail: how many crumbs after this one a slotted trail
64
+ * longer than its `max-items` has folded, so this crumb draws the button
65
+ * that shows them. 0 draws none.
66
+ */
67
+ fold: number;
68
+ /**
69
+ * @internal Set by the trail: whether this crumb is folded away.
70
+ */
71
+ folded: boolean;
72
+ /** @internal Set by the trail: the name of the button that shows the folded crumbs. */
73
+ foldLabel: string;
53
74
  /**
54
75
  * The URL the crumb links to. The current page has none.
55
76
  *
@@ -74,6 +95,8 @@ export declare class HelixHeadlessBreadcrumb extends HelixElement {
74
95
  * `current`, so this one is said to be the current page.
75
96
  */
76
97
  implied: boolean;
98
+ /** @internal Set by the trail: what a press on the fold's button does. */
99
+ onFold: (() => void) | undefined;
77
100
  /**
78
101
  * The link's `rel` value, which describes how the linked page relates to this
79
102
  * one.
@@ -86,6 +109,16 @@ export declare class HelixHeadlessBreadcrumb extends HelixElement {
86
109
  */
87
110
  target: string;
88
111
  protected willUpdate(changed: PropertyValues): void;
112
+ /**
113
+ * Sends `helix-select` for a plain press on the link, and keeps the browser
114
+ * from following where it was canceled.
115
+ */
116
+ private readonly handleLinkClick;
117
+ /**
118
+ * The button that shows the crumbs a slotted trail folded after this one,
119
+ * with a chevron before it, drawn after the crumb's own label.
120
+ */
121
+ protected renderFold(): TemplateResult | typeof nothing;
89
122
  protected updated(changed: PropertyValues): void;
90
123
  /**
91
124
  * Says whether anything sits after the label.
@@ -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, hostAria, slotFilled, slotNamed, } from '@fusion.dev/helix-core';
7
+ import { customElement, HelixElement, HelixSelectEvent, hostAria, plainClick, slotFilled, slotNamed, } from '@fusion.dev/helix-core';
8
8
  import { caretRight } from '@fusion.dev/helix-icons';
9
9
  import { css, html, nothing, } from 'lit';
10
10
  import { property } from 'lit/decorators.js';
@@ -13,9 +13,21 @@ let HelixHeadlessBreadcrumb = class HelixHeadlessBreadcrumb extends HelixElement
13
13
  constructor() {
14
14
  super(...arguments);
15
15
  this.current = false;
16
+ this.fold = 0;
17
+ this.folded = false;
18
+ this.foldLabel = '';
16
19
  this.implied = false;
20
+ this.onFold = undefined;
17
21
  this.rel = '';
18
22
  this.target = '';
23
+ this.handleLinkClick = (event) => {
24
+ if (!plainClick(event)) {
25
+ return;
26
+ }
27
+ if (!this.dispatchEvent(new HelixSelectEvent({ value: this.href ?? '' }))) {
28
+ event.preventDefault();
29
+ }
30
+ };
19
31
  this.handleEnd = () => {
20
32
  this.markEnd();
21
33
  };
@@ -24,11 +36,29 @@ let HelixHeadlessBreadcrumb = class HelixHeadlessBreadcrumb extends HelixElement
24
36
  :host(:first-child) [part='separator'] {
25
37
  display: none;
26
38
  }
39
+
40
+ :host(:state(folded)) {
41
+ display: none;
42
+ }
27
43
  `; }
28
44
  willUpdate(changed) {
29
45
  super.willUpdate(changed);
30
46
  hostAria(this.internals, { role: 'listitem' });
31
47
  this.internals.ariaCurrent = this.current || this.implied ? 'page' : null;
48
+ this.toggleState('current', this.current || this.implied);
49
+ this.toggleState('folded', this.folded);
50
+ }
51
+ renderFold() {
52
+ if (this.fold === 0) {
53
+ return nothing;
54
+ }
55
+ return html `<span part="fold-separator" aria-hidden="true"
56
+ ><svg data-mirror fill="currentColor" viewBox=${caretRight.viewBox}>
57
+ ${unsafeSVG(caretRight.content)}
58
+ </svg></span
59
+ ><button part="expand" type="button" aria-label=${this.foldLabel} @click=${() => this.onFold?.()}>
60
+ …
61
+ </button>`;
32
62
  }
33
63
  updated(changed) {
34
64
  super.updated(changed);
@@ -55,14 +85,24 @@ let HelixHeadlessBreadcrumb = class HelixHeadlessBreadcrumb extends HelixElement
55
85
  href=${this.href ?? ''}
56
86
  target=${this.target || nothing}
57
87
  rel=${this.rel || nothing}
88
+ @click=${this.handleLinkClick}
58
89
  >${label}</a
59
90
  >`
60
- : html `<span part="link">${label}</span>`}<slot name="end" @slotchange=${this.handleEnd}></slot>`;
91
+ : html `<span part="link">${label}</span>`}<slot name="end" @slotchange=${this.handleEnd}></slot>${this.renderFold()}`;
61
92
  }
62
93
  };
63
94
  __decorate([
64
95
  property({ reflect: true, type: Boolean })
65
96
  ], HelixHeadlessBreadcrumb.prototype, "current", void 0);
97
+ __decorate([
98
+ property({ attribute: false })
99
+ ], HelixHeadlessBreadcrumb.prototype, "fold", void 0);
100
+ __decorate([
101
+ property({ attribute: false })
102
+ ], HelixHeadlessBreadcrumb.prototype, "folded", void 0);
103
+ __decorate([
104
+ property({ attribute: false })
105
+ ], HelixHeadlessBreadcrumb.prototype, "foldLabel", void 0);
66
106
  __decorate([
67
107
  property({ reflect: true })
68
108
  ], HelixHeadlessBreadcrumb.prototype, "href", void 0);
@@ -35,11 +35,13 @@ export interface BreadcrumbItem {
35
35
  * array of `{ label, href }`, drawn as crumbs in order, the last one as the
36
36
  * current page. Slotted crumbs follow them. Use one way or the other in a trail.
37
37
  *
38
- * With `max-items`, a trail drawn from `items` that is longer folds its middle
39
- * into one crumb: the first crumb, a button, then the last `max-items` less one.
40
- * The button is named by `expand-label` and shows the whole trail, moving focus
41
- * to the first crumb it brought back. A new `items` folds again. Slotted crumbs
42
- * do not fold; write the folded crumb where the gap should be.
38
+ * With `max-items`, a trail that is longer folds its middle into one place:
39
+ * the first crumb, a button, then the last `max-items` less one. The button is
40
+ * named by `expand-label` and shows the whole trail, moving focus to the first
41
+ * crumb it brought back. A trail drawn from `items` draws the button as a
42
+ * crumb of its own, and a new `items` folds again. A slotted trail hides the
43
+ * crumbs it folds, which take no room, and its first crumb draws the button
44
+ * after its link; a change to the crumbs folds again.
43
45
  *
44
46
  * **Keyboard.** Each crumb with an `href` is a link and one tab stop, and
45
47
  * Enter follows it. The current page and the separators are not in the tab
@@ -58,6 +60,8 @@ export interface BreadcrumbItem {
58
60
  * @csspart crumb - Each crumb drawn from `items`.
59
61
  * @csspart crumb-collapsed - The crumb that holds the folded middle of a trail longer than `max-items`.
60
62
  * @csspart crumb-current - The crumb drawn from `items` for the current page.
63
+ * @csspart crumb-expand - The fold button a crumb draws after the first crumb of a folded slotted trail. A trail drawn from `items` draws its fold as a crumb of its own, `crumb-collapsed`, so this one is never drawn there.
64
+ * @csspart crumb-fold-separator - The chevron before that button.
61
65
  * @csspart crumb-link - The link, or the current page's words, of a crumb drawn from `items`.
62
66
  * @csspart crumb-separator - The separator before a crumb drawn from `items`.
63
67
  * @csspart crumb-separator-mark - The chevron before a crumb drawn from `items`.
@@ -98,6 +102,10 @@ export declare class HelixHeadlessBreadcrumbs extends HelixElement {
98
102
  maxItems?: number;
99
103
  /** Whether a reader has asked to see the whole of a folded trail. */
100
104
  private expanded;
105
+ /** The crumbs slotted into the trail, in order. */
106
+ private get slotted();
107
+ /** Shows the whole of a folded slotted trail and moves focus to the first crumb it brought back. */
108
+ private readonly unfold;
101
109
  /** Shows the whole trail and moves focus to the first crumb it brought back. */
102
110
  private readonly expand;
103
111
  protected willUpdate(changed: PropertyValues): void;
@@ -110,6 +118,8 @@ export declare class HelixHeadlessBreadcrumbs extends HelixElement {
110
118
  * not reflect, so nothing is written onto your markup.
111
119
  */
112
120
  private readonly markLast;
121
+ /** New crumbs fold again, as a new `items` does. */
122
+ private readonly handleSlotChange;
113
123
  /** The crumbs `items` describes, each a crumb of this tier. */
114
124
  protected renderItems(): TemplateResult | typeof nothing;
115
125
  protected render(): TemplateResult;
@@ -17,6 +17,15 @@ let HelixHeadlessBreadcrumbs = class HelixHeadlessBreadcrumbs extends HelixEleme
17
17
  this.items = [];
18
18
  this.label = 'Breadcrumb';
19
19
  this.expanded = false;
20
+ this.unfold = () => {
21
+ this.expanded = true;
22
+ this.updateComplete.then(() => {
23
+ const crumb = this.slotted[1];
24
+ const link = crumb?.shadowRoot?.querySelector('a[part="link"]') ??
25
+ crumb?.querySelector('a[href]');
26
+ link?.focus();
27
+ });
28
+ };
20
29
  this.expand = () => {
21
30
  this.expanded = true;
22
31
  this.updateComplete.then(() => {
@@ -25,16 +34,32 @@ let HelixHeadlessBreadcrumbs = class HelixHeadlessBreadcrumbs extends HelixEleme
25
34
  });
26
35
  };
27
36
  this.markLast = () => {
28
- const crumbs = (this.renderRoot.querySelector('slot:not([name])')?.assignedElements() ?? []).filter((one) => one instanceof HelixHeadlessBreadcrumb);
37
+ const crumbs = this.slotted;
29
38
  const marked = crumbs.some((one) => one.current);
39
+ const most = this.maxItems ?? 0;
40
+ const fold = !this.expanded && this.items.length === 0 && most >= 2 && crumbs.length > most
41
+ ? crumbs.length - most
42
+ : 0;
43
+ const label = fold > 0 ? formatWords(this, 'expandLabel', { count: fold }) : '';
30
44
  crumbs.forEach((crumb, index) => {
31
45
  whenHydrated(crumb, () => {
32
46
  crumb.implied = !marked && this.items.length === 0 && index === crumbs.length - 1;
47
+ crumb.folded = index > 0 && index <= fold;
48
+ crumb.fold = index === 0 ? fold : 0;
49
+ crumb.foldLabel = index === 0 ? label : '';
50
+ crumb.onFold = index === 0 && fold > 0 ? this.unfold : undefined;
33
51
  });
34
52
  });
35
53
  };
54
+ this.handleSlotChange = () => {
55
+ this.expanded = false;
56
+ this.markLast();
57
+ };
36
58
  }
37
59
  static { this.words = 'helix-breadcrumbs'; }
60
+ get slotted() {
61
+ return (this.renderRoot?.querySelector('slot:not([name])')?.assignedElements() ?? []).filter((one) => one instanceof HelixHeadlessBreadcrumb);
62
+ }
38
63
  willUpdate(changed) {
39
64
  super.willUpdate(changed);
40
65
  hostAria(this.internals, { label: this.label, role: 'navigation' });
@@ -65,7 +90,7 @@ let HelixHeadlessBreadcrumbs = class HelixHeadlessBreadcrumbs extends HelixEleme
65
90
  ? nothing
66
91
  : html `<${crumb}
67
92
  part="crumb crumb-collapsed"
68
- exportparts="link: crumb-link, separator: crumb-separator, separator-mark: crumb-separator-mark"
93
+ exportparts="expand: crumb-expand, fold-separator: crumb-fold-separator, link: crumb-link, separator: crumb-separator, separator-mark: crumb-separator-mark"
69
94
  ><button
70
95
  part="expand"
71
96
  type="button"
@@ -79,7 +104,7 @@ let HelixHeadlessBreadcrumbs = class HelixHeadlessBreadcrumbs extends HelixEleme
79
104
  const rest = shown.slice(1);
80
105
  const draw = ({ index, item }) => html `<${crumb}
81
106
  part="crumb${index === last ? ' crumb-current' : ''}"
82
- exportparts="link: crumb-link, separator: crumb-separator, separator-mark: crumb-separator-mark"
107
+ exportparts="expand: crumb-expand, fold-separator: crumb-fold-separator, link: crumb-link, separator: crumb-separator, separator-mark: crumb-separator-mark"
83
108
  href=${index === last ? nothing : (item.href ?? nothing)}
84
109
  rel=${item.rel ?? nothing}
85
110
  target=${item.target ?? nothing}
@@ -90,7 +115,7 @@ let HelixHeadlessBreadcrumbs = class HelixHeadlessBreadcrumbs extends HelixEleme
90
115
  }
91
116
  render() {
92
117
  return html `<ol part="list" role="list">
93
- ${this.renderItems()}<slot @slotchange=${this.markLast}></slot>
118
+ ${this.renderItems()}<slot @slotchange=${this.handleSlotChange}></slot>
94
119
  </ol>`;
95
120
  }
96
121
  };
@@ -65,6 +65,8 @@ import { type PropertyValues, type TemplateResult } from 'lit';
65
65
  * @event {HelixBeforeChangeEvent} helix-before-change - Fired before a press changes `value`, with the value it would leave in `detail.value`. Cancelable: call `preventDefault()` to keep the buttons as they are.
66
66
  *
67
67
  * @cssproperty --button-group-pressed - Written on the control inside each toggle button of a group with `selection-mode`: `1` while it is pressed and `0` while it is not. Read it in a `::part(button)` rule to draw the pressed look.
68
+ *
69
+ * @cssstate pressed - While a group with `selection-mode` has a button pressed.
68
70
  */
69
71
  export declare class HelixHeadlessButtonGroup extends HelixElement {
70
72
  /**
@@ -145,6 +145,7 @@ let HelixHeadlessButtonGroup = class HelixHeadlessButtonGroup extends HelixEleme
145
145
  const toolbar = this.roving;
146
146
  hostAria(this.internals, { label: this.label, role: toolbar ? 'toolbar' : 'group' });
147
147
  this.internals.ariaOrientation = toolbar ? (this.orientation ?? null) : null;
148
+ this.toggleState('pressed', Boolean(this.selectionMode) && commaList(this.value).length > 0);
148
149
  }
149
150
  firstUpdated(changed) {
150
151
  super.firstUpdated(changed);
@@ -13,13 +13,17 @@ import { type PropertyValues, type TemplateResult } from 'lit';
13
13
  * through with `::part`.
14
14
  *
15
15
  * Name the card with `label`, as a Stat Group is named, or with `aria-label`
16
- * or `aria-labelledby`. These are the only names that work with server
17
- * rendering, because reading a title from the light DOM needs a DOM, and the
18
- * server has none. `label` is set through `ElementInternals`, so it writes no
19
- * attribute onto the card. **A named card is a group**, so a
16
+ * or `aria-labelledby`. `label` is set through `ElementInternals`, so it
17
+ * writes no attribute onto the card. **A named card is a group**, so a
20
18
  * reader entering it hears its name; an unnamed one has no role, since a group
21
19
  * with no name only adds "group" to what a reader says.
22
20
  *
21
+ * **With none of those, a heading in `header` names the card**, such as the
22
+ * `<h2>` of its title, by reference, so the name follows the heading's words.
23
+ * Reading a heading needs a DOM, so a served card is named by it once the
24
+ * script runs; `label`, `aria-label` and `aria-labelledby` are the names a
25
+ * server writes.
26
+ *
23
27
  * Each region's slot is a part of the same name, `body` for the main content,
24
28
  * as Web Awesome's card names its regions. A slot draws no box, so a part
25
29
  * passes inherited properties to what is slotted, and a page wanting a box
@@ -29,7 +33,9 @@ import { type PropertyValues, type TemplateResult } from 'lit';
29
33
  * header's and the footer's line, as Web Awesome's card does. While one holds
30
34
  * something, its line is a `header-row` or `footer-row` part; otherwise there
31
35
  * is no wrapper and every region is a bare slot. A server cannot see slotted
32
- * content, so served actions sit on their own line until the script runs.
36
+ * content, so a served card has no row; the styled tier sets served actions at
37
+ * the end of their line from the stylesheet alone, so they do not move when
38
+ * the script runs.
33
39
  *
34
40
  * **The card says which regions it holds**, as `:state(has-media)`,
35
41
  * `:state(has-header)` and `:state(has-footer)`, so a sheet can draw a card
@@ -87,20 +93,34 @@ export declare class HelixHeadlessCard extends HelixElement {
87
93
  */
88
94
  mediaPlacement?: 'end';
89
95
  /**
90
- * @internal The host's `aria-labelledby`, observed so the card becomes a
91
- * group once it is named. Not reflected: the attribute is the consumer's.
96
+ * Hears a name written on the card, which no property mirrors.
97
+ *
98
+ * Lit observes the attributes of its own properties only, and `ariaLabel`
99
+ * and `ariaLabelledByElements` belong to the platform. Observed here rather
100
+ * than through a property, so the manifest lists no attribute of the card's
101
+ * own for them.
92
102
  */
93
- protected labelledBy: string | null;
103
+ static get observedAttributes(): string[];
94
104
  /**
95
- * @internal The host's `aria-label`, observed for the same reason as
96
- * `labelledBy`. Not reflected: the attribute is the consumer's.
105
+ * Called by the browser when an observed attribute changes. A name written
106
+ * or taken off asks for an update, because it decides whether the card is a
107
+ * group and whether its heading names it.
97
108
  */
98
- protected namedBy: string | null;
109
+ attributeChangedCallback(name: string, old: string | null, value: string | null): void;
99
110
  /** Whether `footer-actions` holds something, read from the slot. */
100
111
  private footerActions;
101
112
  /** Whether `header-actions` holds something, read from the slot. */
102
113
  private headerActions;
103
114
  protected willUpdate(changed: PropertyValues): void;
115
+ /**
116
+ * Says what the card is and what names it. On the host, since the host is
117
+ * the box, and from `willUpdate`, which the server calls too.
118
+ *
119
+ * **A heading in `header` is the name only where nothing else is**, and it
120
+ * names the card by reference, so a renamed heading renames the card. An
121
+ * engine without element references hears its words.
122
+ */
123
+ private writeName;
104
124
  protected updated(changed: PropertyValues): void;
105
125
  protected firstUpdated(changed: PropertyValues): void;
106
126
  private readonly readRegions;