@fusion.dev/helix-headless 0.154.0 → 0.156.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 (104) hide show
  1. package/custom-elements.json +3213 -179
  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/breadcrumbs/breadcrumb/breadcrumb.d.ts +36 -3
  7. package/dist/breadcrumbs/breadcrumb/breadcrumb.js +42 -2
  8. package/dist/breadcrumbs/root/breadcrumbs-root.d.ts +15 -5
  9. package/dist/breadcrumbs/root/breadcrumbs-root.js +29 -4
  10. package/dist/button-group/root/button-group-root.d.ts +2 -0
  11. package/dist/button-group/root/button-group-root.js +1 -0
  12. package/dist/canvas/node/canvas-node.js +1 -1
  13. package/dist/canvas/root/canvas-root.d.ts +17 -2
  14. package/dist/canvas/root/canvas-root.js +23 -6
  15. package/dist/chat/composer/chat-composer.js +1 -1
  16. package/dist/chat/root/chat-root.d.ts +14 -1
  17. package/dist/chat/root/chat-root.js +14 -4
  18. package/dist/color-input/root/color-input-root.d.ts +9 -3
  19. package/dist/color-input/root/color-input-root.js +3 -1
  20. package/dist/color-picker/channels.d.ts +5 -3
  21. package/dist/color-picker/channels.js +79 -21
  22. package/dist/color-picker/root/color-picker-root.d.ts +58 -5
  23. package/dist/color-picker/root/color-picker-root.js +75 -13
  24. package/dist/combobox/root/combobox-root.d.ts +10 -0
  25. package/dist/combobox/root/combobox-root.js +18 -3
  26. package/dist/command/root/command-root.d.ts +116 -1
  27. package/dist/command/root/command-root.js +124 -15
  28. package/dist/data-table/root/data-table-root.d.ts +25 -0
  29. package/dist/data-table/root/data-table-root.js +13 -3
  30. package/dist/date-input/root/date-input-root.d.ts +33 -1
  31. package/dist/date-input/root/date-input-root.js +9 -2
  32. package/dist/date-picker/root/date-picker-root.d.ts +43 -13
  33. package/dist/date-picker/root/date-picker-root.js +54 -31
  34. package/dist/element-events.generated.js +3 -1
  35. package/dist/graph/root/graph-root.d.ts +13 -0
  36. package/dist/graph/root/graph-root.js +16 -0
  37. package/dist/list/root/list-root.d.ts +74 -1
  38. package/dist/list/root/list-root.js +92 -4
  39. package/dist/list/selection.js +3 -3
  40. package/dist/map/root/map-root.d.ts +6 -0
  41. package/dist/map/root/map-root.js +8 -1
  42. package/dist/navigation/root/navigation-root.d.ts +26 -0
  43. package/dist/navigation/root/navigation-root.js +58 -1
  44. package/dist/number-input/root/number-input-root.d.ts +63 -6
  45. package/dist/number-input/root/number-input-root.js +96 -11
  46. package/dist/object-inspector/root/object-inspector-root.d.ts +36 -1
  47. package/dist/object-inspector/root/object-inspector-root.js +26 -2
  48. package/dist/option/root/option-root.d.ts +32 -0
  49. package/dist/option/root/option-root.js +44 -5
  50. package/dist/outline/root/outline-root.d.ts +6 -2
  51. package/dist/outline/root/outline-root.js +7 -3
  52. package/dist/select/root/select-root.d.ts +29 -3
  53. package/dist/select/root/select-root.js +34 -9
  54. package/dist/slider/root/slider-root.d.ts +71 -9
  55. package/dist/slider/root/slider-root.js +129 -42
  56. package/dist/speed-dial/item/speed-dial-item.d.ts +29 -1
  57. package/dist/speed-dial/item/speed-dial-item.js +48 -7
  58. package/dist/speed-dial/root/speed-dial-root.d.ts +32 -6
  59. package/dist/speed-dial/root/speed-dial-root.js +50 -5
  60. package/dist/splitter/root/splitter-root.d.ts +22 -0
  61. package/dist/splitter/root/splitter-root.js +36 -7
  62. package/dist/steps/reach.d.ts +5 -1
  63. package/dist/steps/reach.js +2 -2
  64. package/dist/steps/root/steps-root.d.ts +37 -2
  65. package/dist/steps/root/steps-root.js +49 -7
  66. package/dist/steps/step/step.d.ts +6 -0
  67. package/dist/steps/step/step.js +4 -0
  68. package/dist/table/root/table-root.d.ts +20 -0
  69. package/dist/table/root/table-root.js +13 -3
  70. package/dist/tabs/root/tabs-root.d.ts +37 -0
  71. package/dist/tabs/root/tabs-root.js +92 -2
  72. package/dist/tabs/tab/tab.d.ts +38 -2
  73. package/dist/tabs/tab/tab.js +45 -4
  74. package/dist/tag/group/tag-group.d.ts +29 -3
  75. package/dist/tag/group/tag-group.js +42 -11
  76. package/dist/time-input/root/time-input-root.d.ts +32 -1
  77. package/dist/time-input/root/time-input-root.js +9 -2
  78. package/dist/time-picker/root/time-picker-root.d.ts +63 -1
  79. package/dist/time-picker/root/time-picker-root.js +58 -7
  80. package/dist/timeline/item/timeline-item.d.ts +25 -0
  81. package/dist/timeline/item/timeline-item.js +27 -2
  82. package/dist/toggle/root/toggle-root.d.ts +2 -0
  83. package/dist/toggle/root/toggle-root.js +1 -1
  84. package/dist/tree/item/tree-item.d.ts +10 -0
  85. package/dist/tree/item/tree-item.js +12 -0
  86. package/dist/tree/root/tree-root.d.ts +40 -1
  87. package/dist/tree/root/tree-root.js +120 -4
  88. package/dist/tree/selection.d.ts +6 -0
  89. package/dist/tree/selection.js +11 -0
  90. package/dist/video/root/keys.d.ts +6 -2
  91. package/dist/video/root/keys.js +18 -1
  92. package/dist/video/root/video-root.d.ts +60 -1
  93. package/dist/video/root/video-root.js +70 -2
  94. package/dist/video-playlist/item/video-playlist-item.d.ts +15 -1
  95. package/dist/video-playlist/item/video-playlist-item.js +7 -0
  96. package/dist/video-playlist/root/video-playlist-root.d.ts +43 -4
  97. package/dist/video-playlist/root/video-playlist-root.js +69 -2
  98. package/dist/virtual-list/root/virtual-list-root.d.ts +8 -0
  99. package/dist/virtual-list/root/virtual-list-root.js +3 -0
  100. package/lint.json +1 -1
  101. package/package.json +5 -5
  102. package/types/jsx.d.ts +3885 -383
  103. package/types/vue.d.ts +1045 -107
  104. package/types/words.d.ts +146 -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) {
@@ -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);
@@ -77,7 +77,7 @@ let HelixHeadlessCanvasNode = class HelixHeadlessCanvasNode extends HelixElement
77
77
  aria-describedby=${this.status || this.meaning || this.links ? 'about' : nothing}
78
78
  aria-label=${this.label || nothing}
79
79
  aria-labelledby=${this.label || this.group ? nothing : 'content'}
80
- aria-keyshortcuts=${this.connectable ? 'Enter F2 Space Shift+ArrowRight Shift+ArrowDown Alt+ArrowRight Alt+ArrowDown C Delete' : nothing}
80
+ aria-keyshortcuts=${this.connectable ? 'Enter F2 Space Control+ArrowDown Control+ArrowUp Shift+ArrowRight Shift+ArrowDown Alt+ArrowRight Alt+ArrowDown C Delete' : nothing}
81
81
  role=${this.group ? 'group' : 'article'}
82
82
  tabindex=${this.current ? '0' : '-1'}
83
83
  >
@@ -46,6 +46,10 @@ declare const HelixHeadlessCanvas_base: (new (...args: any[]) => import("@fusion
46
46
  * - One card holds the board's tab stop.
47
47
  * - The arrow keys move through the cards in reading order. The card you land
48
48
  * on scrolls into view, and its connections are announced with it.
49
+ * - The card you land on is selected. Control or Command with an arrow key,
50
+ * Home, or End moves without changing the selection, and Space adds the card
51
+ * to the selection or takes it out, so you can select cards that are not
52
+ * next to each other.
49
53
  * - Shift with an arrow key moves the card: by 16 units, or to the next
50
54
  * multiple of `snap` where it is set. Alt with an arrow key resizes it from
51
55
  * its bottom right corner the same way.
@@ -143,6 +147,8 @@ declare const HelixHeadlessCanvas_base: (new (...args: any[]) => import("@fusion
143
147
  * @csspart zoom-in - The button that zooms in.
144
148
  * @csspart zoom-out - The button that zooms out.
145
149
  *
150
+ * @cssproperty --canvas-zoom - The board's zoom, which the board writes: 1 at the document's own size, 0.5 at half. The cards inherit it, so a card's stylesheet can draw less when the board is far out, such as a border that stays one pixel wide at `calc(1px / var(--canvas-zoom))`.
151
+ *
146
152
  * @cssstate connecting - While you draw a connection.
147
153
  * @cssstate direct - The board moves without easing, while you drag it or while it settles into its box.
148
154
  * @cssstate empty - While the document has no nodes.
@@ -271,14 +277,21 @@ export declare class HelixHeadlessCanvas extends HelixHeadlessCanvas_base {
271
277
  * units.
272
278
  */
273
279
  resizedLabel: string;
280
+ /**
281
+ * What a screen reader calls the board in place of its role, after its
282
+ * name: "Release plan, canvas". Said only where the board has a name of its
283
+ * own, since the fallback name is the same word. Empty leaves the role.
284
+ */
285
+ roleLabel: string;
274
286
  /**
275
287
  * The names of the selected cards and connections, comma separated. Empty
276
288
  * when nothing is selected.
277
289
  *
278
290
  * **Comma separated, the way `Listbox` writes a multiple value**, so a page
279
291
  * reads one selection the same way wherever it comes from. A click, or a key
280
- * that lands on one, selects it alone; Shift, Command or Control with a click
281
- * adds or takes one away, and so does Space; Command or Control with A
292
+ * that lands on one, selects it alone; Command or Control with an arrow key
293
+ * moves without selecting; Shift, Command or Control with a click adds or
294
+ * takes one away, and so does Space; Command or Control with A
282
295
  * selects everything; Shift and a drag across the empty board selects the
283
296
  * cards the box touches. A click on the empty board, or Escape, selects
284
297
  * nothing. Dragging, Shift with an arrow, and Delete act on the whole
@@ -882,6 +895,8 @@ export declare class HelixHeadlessCanvas extends HelixHeadlessCanvas_base {
882
895
  * asks for it and Escape stops.
883
896
  */
884
897
  private readonly handleStopKeydown;
898
+ /** Whether the stop focus is moving to keeps the selection as it is. */
899
+ private walkingApart;
885
900
  /**
886
901
  * A stop a reader landed on becomes the tab stop, and a card comes into view.
887
902
  *
@@ -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 { assignSlots, capturePointer, customElement, formatPercent, formatWords, HelixBeforeMoveEvent, HelixBeforeReconnectEvent, HelixBeforeRelabelEvent, HelixCanvasEditEvent, HelixCanvasPanEvent, HelixCanvasViewEvent, HelixCanvasZoomEvent, HelixConnectEvent, HelixElement, HelixMoveEvent, HelixReconnectEvent, HelixRelabelEvent, HelixRemoveEvent, HelixSelectEvent, hostAria, inlineStep, isRtl, MixinViewport, words, wordsConverter, wordsLocale, } from '@fusion.dev/helix-core';
7
+ import { assignSlots, auditName, capturePointer, customElement, formatPercent, formatWords, HelixBeforeMoveEvent, HelixBeforeReconnectEvent, HelixBeforeRelabelEvent, HelixCanvasEditEvent, HelixCanvasPanEvent, HelixCanvasViewEvent, HelixCanvasZoomEvent, HelixConnectEvent, HelixElement, HelixMoveEvent, HelixReconnectEvent, HelixRelabelEvent, HelixRemoveEvent, HelixSelectEvent, hostAria, inlineStep, isRtl, MixinViewport, words, wordsConverter, wordsLocale, } from '@fusion.dev/helix-core';
8
8
  import { html, nothing, svg } from 'lit';
9
9
  import { property, state } from 'lit/decorators.js';
10
10
  import { descriptionOf } from '../describe.js';
@@ -57,6 +57,7 @@ let HelixHeadlessCanvas = class HelixHeadlessCanvas extends MixinViewport(HelixE
57
57
  this.readOnly = false;
58
58
  this.reconnectingLabel = 'Moving the end of the connection from {from} to {to}. Move to a card and press Enter, or Escape to stop.';
59
59
  this.resizedLabel = '{name} resized to {width} by {height}';
60
+ this.roleLabel = 'canvas';
60
61
  this.selected = '';
61
62
  this.selectedLabel = 'selected';
62
63
  this.selectionLabel = '{count} selected';
@@ -592,8 +593,15 @@ let HelixHeadlessCanvas = class HelixHeadlessCanvas extends MixinViewport(HelixE
592
593
  return;
593
594
  }
594
595
  event.preventDefault();
595
- this.focusStop(walk[to]?.key);
596
+ this.walkingApart = event.ctrlKey || event.metaKey;
597
+ try {
598
+ this.focusStop(walk[to]?.key);
599
+ }
600
+ finally {
601
+ this.walkingApart = false;
602
+ }
596
603
  };
604
+ this.walkingApart = false;
597
605
  this.handleFocusIn = (event) => {
598
606
  const edge = this.edgeOf(event);
599
607
  const card = edge === undefined ? this.cardOf(event) : undefined;
@@ -603,7 +611,10 @@ let HelixHeadlessCanvas = class HelixHeadlessCanvas extends MixinViewport(HelixE
603
611
  this.refocus = undefined;
604
612
  this.focusedCard = card;
605
613
  this.stop = edge ?? card?.name;
606
- if (!this.pointing && this.stop !== undefined && !this.picked.includes(this.stop)) {
614
+ if (!this.pointing &&
615
+ !this.walkingApart &&
616
+ this.stop !== undefined &&
617
+ !this.picked.includes(this.stop)) {
607
618
  this.choose(this.stop);
608
619
  }
609
620
  if (card) {
@@ -1650,6 +1661,7 @@ let HelixHeadlessCanvas = class HelixHeadlessCanvas extends MixinViewport(HelixE
1650
1661
  firstUpdated(changed) {
1651
1662
  super.firstUpdated(changed);
1652
1663
  this.finishHydration();
1664
+ auditName(this, () => null, 'Give it a `label` that says what the board holds, such as "Release plan".', () => this.label !== '');
1653
1665
  if (this.waiting) {
1654
1666
  this.measured = this.box;
1655
1667
  this.view = this.waiting;
@@ -1941,7 +1953,7 @@ let HelixHeadlessCanvas = class HelixHeadlessCanvas extends MixinViewport(HelixE
1941
1953
  d=${drawn.d}
1942
1954
  data-edge=${key}
1943
1955
  fill="none"
1944
- aria-keyshortcuts="Enter F2 C Delete"
1956
+ aria-keyshortcuts="Enter F2 Space Control+ArrowDown Control+ArrowUp C Delete"
1945
1957
  role="button"
1946
1958
  tabindex=${key === stop ? '0' : '-1'}
1947
1959
  />
@@ -2050,7 +2062,8 @@ let HelixHeadlessCanvas = class HelixHeadlessCanvas extends MixinViewport(HelixE
2050
2062
  return html `<div
2051
2063
  part="region"
2052
2064
  role="group"
2053
- aria-label=${this.label || this.fallbackLabel}
2065
+ aria-label=${this.label || this.getAttribute?.('aria-label') || this.fallbackLabel}
2066
+ aria-roledescription=${(this.label || this.getAttribute?.('aria-label')) && this.roleLabel ? this.roleLabel : nothing}
2054
2067
  aria-keyshortcuts="ArrowUp ArrowDown ArrowLeft ArrowRight Plus Minus"
2055
2068
  tabindex="0"
2056
2069
  @focusin=${this.idle.stir}
@@ -2063,7 +2076,7 @@ let HelixHeadlessCanvas = class HelixHeadlessCanvas extends MixinViewport(HelixE
2063
2076
  </div>
2064
2077
  <div
2065
2078
  part="viewport"
2066
- style=${`--_canvas-x: ${this.at.x}px; --_canvas-y: ${this.at.y}px; --_canvas-zoom: ${this.at.zoom}; --_canvas-rule-x: ${this.at.x - origin.x * this.at.zoom}px; --_canvas-rule-y: ${this.at.y - origin.y * this.at.zoom}px; --_canvas-rule-zoom: ${ruling(this.at.zoom)}`}
2079
+ style=${`--_canvas-x: ${this.at.x}px; --_canvas-y: ${this.at.y}px; --_canvas-zoom: ${this.at.zoom}; --canvas-zoom: ${this.at.zoom}; --_canvas-rule-x: ${this.at.x - origin.x * this.at.zoom}px; --_canvas-rule-y: ${this.at.y - origin.y * this.at.zoom}px; --_canvas-rule-zoom: ${ruling(this.at.zoom)}`}
2067
2080
  @wheel=${this.handleWheel}
2068
2081
  >
2069
2082
  <!-- **One shape, whatever the board holds**, which is what hydration
@@ -2175,6 +2188,10 @@ __decorate([
2175
2188
  words,
2176
2189
  property({ attribute: 'resized-label' })
2177
2190
  ], HelixHeadlessCanvas.prototype, "resizedLabel", void 0);
2191
+ __decorate([
2192
+ words,
2193
+ property({ attribute: 'role-label' })
2194
+ ], HelixHeadlessCanvas.prototype, "roleLabel", void 0);
2178
2195
  __decorate([
2179
2196
  property()
2180
2197
  ], HelixHeadlessCanvas.prototype, "selected", void 0);
@@ -105,7 +105,7 @@ let HelixHeadlessChatComposer = class HelixHeadlessChatComposer extends HelixFor
105
105
  super.updated(changed);
106
106
  this.markEmpty();
107
107
  if (changed.has('value')) {
108
- this.internals.setFormValue(this.value === '' ? null : this.value);
108
+ this.internals.setFormValue(this.value === '' ? null : this.value, this.value);
109
109
  }
110
110
  const field = this.partNamed('field');
111
111
  if (field?.validity) {
@@ -40,7 +40,8 @@ declare const HelixHeadlessChat_base: (new (...args: any[]) => import("@fusion.d
40
40
  * The log is `aria-live="off"`. A separate status region announces each new
41
41
  * turn once, when it has finished arriving. It skips the user's own turns
42
42
  * unless they failed to send, turns loaded as history, and the turns already
43
- * there when the transcript opened.
43
+ * there when the transcript opened. A turn is said with its author first, such
44
+ * as "Ada: It ships today" (`arrival-label`), where its author can be named.
44
45
  *
45
46
  * Pass the turns as children or as `turns` data. Do not use both. Children
46
47
  * render from the server and suit most conversations. `turns` draws a long
@@ -145,6 +146,13 @@ export declare class HelixHeadlessChat extends HelixHeadlessChat_base {
145
146
  static shadowRootOptions: ShadowRootInit;
146
147
  /** How tall a turn is assumed to be before it has been drawn, in pixels. */
147
148
  static readonly ESTIMATE = 72;
149
+ /**
150
+ * What a screen reader hears when a turn arrives. `{author}` is who wrote
151
+ * it, as its name says it, and `{text}` its words. A turn with no author a
152
+ * reader can be told, a turn by `them` with no `author-label`, says its
153
+ * words alone.
154
+ */
155
+ arrivalLabel: string;
148
156
  /**
149
157
  * Declarations for the turns from `turns` by each author, keyed by `author`,
150
158
  * as JSON in the attribute: `{"ada": "--chat-message-background-color: …"}`.
@@ -430,6 +438,11 @@ export declare class HelixHeadlessChat extends HelixHeadlessChat_base {
430
438
  private announce;
431
439
  /** The turns after the newest one already accounted for, by the rules above. */
432
440
  private arrived;
441
+ /**
442
+ * What is said for a turn: a failed one with `failed-turn-label`, and
443
+ * another with `arrival-label` where its author can be named, so a reader
444
+ * in a conversation of several knows who spoke.
445
+ */
433
446
  private phrase;
434
447
  /** Watches what is drawn for size, re-observing only when that changed. */
435
448
  private watchSizes;