@fusion.dev/helix-headless 0.147.0 → 0.148.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 (141) hide show
  1. package/custom-elements.json +4512 -790
  2. package/dist/accordion/item/accordion-item.d.ts +5 -2
  3. package/dist/accordion/item/accordion-item.js +11 -7
  4. package/dist/accordion/root/accordion-root.js +4 -6
  5. package/dist/announcement/root/announcement-root.d.ts +56 -5
  6. package/dist/announcement/root/announcement-root.js +124 -21
  7. package/dist/area-chart/root/area-chart-root.js +3 -4
  8. package/dist/badge/count.d.ts +14 -0
  9. package/dist/badge/count.js +20 -0
  10. package/dist/badge/root/badge-root.js +8 -22
  11. package/dist/blockquote/root/blockquote-root.d.ts +66 -8
  12. package/dist/blockquote/root/blockquote-root.js +85 -8
  13. package/dist/breadcrumbs/breadcrumb/breadcrumb.js +2 -2
  14. package/dist/button-group/arrows.d.ts +14 -0
  15. package/dist/button-group/arrows.js +10 -0
  16. package/dist/button-group/root/button-group-root.d.ts +141 -3
  17. package/dist/button-group/root/button-group-root.js +142 -2
  18. package/dist/canvas/edge-pan.d.ts +61 -0
  19. package/dist/canvas/edge-pan.js +63 -0
  20. package/dist/canvas/node/canvas-node.d.ts +12 -2
  21. package/dist/canvas/node/canvas-node.js +6 -2
  22. package/dist/canvas/root/canvas-document.d.ts +10 -0
  23. package/dist/canvas/root/canvas-document.js +1 -0
  24. package/dist/canvas/root/canvas-root.d.ts +107 -5
  25. package/dist/canvas/root/canvas-root.js +140 -36
  26. package/dist/canvas/snap.d.ts +42 -0
  27. package/dist/canvas/snap.js +32 -0
  28. package/dist/carousel/root/carousel-root.d.ts +4 -3
  29. package/dist/carousel/root/carousel-root.js +2 -6
  30. package/dist/chat/root/chat-root.d.ts +13 -3
  31. package/dist/chat/root/chat-root.js +2 -6
  32. package/dist/checkbox-group/root/checkbox-group-root.d.ts +13 -0
  33. package/dist/checkbox-group/root/checkbox-group-root.js +18 -10
  34. package/dist/checkbox-group/select-all.d.ts +11 -0
  35. package/dist/checkbox-group/select-all.js +5 -0
  36. package/dist/code-block/root/code-block-root.d.ts +8 -6
  37. package/dist/code-block/root/code-block-root.js +4 -3
  38. package/dist/color-picker/root/color-picker-root.js +6 -12
  39. package/dist/color-picker/wheel.d.ts +11 -0
  40. package/dist/color-picker/wheel.js +9 -0
  41. package/dist/combobox/root/combobox-root.d.ts +70 -11
  42. package/dist/combobox/root/combobox-root.js +68 -18
  43. package/dist/command/root/command-root.d.ts +4 -3
  44. package/dist/data-table/root/data-table-root.js +17 -44
  45. package/dist/description-list/item/description-list-item.d.ts +4 -0
  46. package/dist/description-list/item/description-list-item.js +7 -1
  47. package/dist/divider/root/divider-root.d.ts +45 -16
  48. package/dist/divider/root/divider-root.js +28 -4
  49. package/dist/drilldown/item/drilldown-item.js +3 -4
  50. package/dist/element-events.generated.js +1 -0
  51. package/dist/empty/root/empty-root.d.ts +17 -3
  52. package/dist/empty/root/empty-root.js +29 -5
  53. package/dist/file-input/root/file-input-root.js +3 -4
  54. package/dist/file-input/status.js +2 -1
  55. package/dist/filter-input/chip/filter-chip.js +2 -3
  56. package/dist/format-date/fields.d.ts +20 -0
  57. package/dist/format-date/fields.js +14 -0
  58. package/dist/format-date/options.d.ts +2 -1
  59. package/dist/format-date/options.js +9 -38
  60. package/dist/format-date/root/format-date-root.d.ts +35 -3
  61. package/dist/format-date/root/format-date-root.js +24 -32
  62. package/dist/graph/root/graph-root.d.ts +23 -5
  63. package/dist/graph/root/graph-root.js +17 -20
  64. package/dist/heatmap/root/heatmap-root.d.ts +12 -0
  65. package/dist/heatmap/root/heatmap-root.js +26 -11
  66. package/dist/image/root/image-root.d.ts +11 -0
  67. package/dist/kbd/root/kbd-root.js +1 -2
  68. package/dist/legend/root/legend-root.js +4 -17
  69. package/dist/list/root/list-root.js +8 -22
  70. package/dist/listbox/root/listbox-root.d.ts +119 -24
  71. package/dist/listbox/root/listbox-root.js +130 -132
  72. package/dist/map/marker/map-marker.d.ts +2 -1
  73. package/dist/map/root/map-root.d.ts +40 -2
  74. package/dist/map/root/map-root.js +64 -23
  75. package/dist/megamenu/root/megamenu-root.js +4 -11
  76. package/dist/menu/root/menu-root.d.ts +2 -2
  77. package/dist/navigation/root/navigation-root.js +5 -8
  78. package/dist/notifications/group/notification-group.d.ts +8 -1
  79. package/dist/notifications/group/notification-group.js +12 -6
  80. package/dist/notifications/notification/notification.d.ts +12 -1
  81. package/dist/notifications/notification/notification.js +3 -1
  82. package/dist/notifications/root/notifications-root.d.ts +41 -5
  83. package/dist/notifications/root/notifications-root.js +27 -7
  84. package/dist/object-inspector/root/object-inspector-root.d.ts +5 -1
  85. package/dist/object-inspector/root/object-inspector-root.js +21 -11
  86. package/dist/pagination/size/pagination-size.d.ts +1 -0
  87. package/dist/pagination/size/pagination-size.js +1 -1
  88. package/dist/pin-input/code.d.ts +23 -0
  89. package/dist/pin-input/code.js +25 -0
  90. package/dist/pin-input/root/pin-input-root.js +9 -23
  91. package/dist/qr-code/root/qr-code-root.d.ts +14 -3
  92. package/dist/qr-code/root/qr-code-root.js +19 -5
  93. package/dist/scatter-chart/measures.d.ts +22 -0
  94. package/dist/scatter-chart/measures.js +11 -0
  95. package/dist/scatter-chart/root/scatter-chart-root.d.ts +2 -6
  96. package/dist/scatter-chart/root/scatter-chart-root.js +6 -19
  97. package/dist/select/root/select-root.d.ts +56 -8
  98. package/dist/select/root/select-root.js +33 -9
  99. package/dist/speed-dial/group/speed-dial-group.d.ts +48 -0
  100. package/dist/speed-dial/group/speed-dial-group.js +43 -0
  101. package/dist/speed-dial/index.d.ts +1 -0
  102. package/dist/speed-dial/index.js +1 -0
  103. package/dist/speed-dial/item/speed-dial-item.d.ts +26 -1
  104. package/dist/speed-dial/item/speed-dial-item.js +41 -3
  105. package/dist/speed-dial/long-press.d.ts +50 -0
  106. package/dist/speed-dial/long-press.js +48 -0
  107. package/dist/speed-dial/root/speed-dial-root.d.ts +80 -1
  108. package/dist/speed-dial/root/speed-dial-root.js +114 -5
  109. package/dist/splitter/root/splitter-root.d.ts +7 -3
  110. package/dist/splitter/root/splitter-root.js +16 -3
  111. package/dist/table/cell/table-cell.d.ts +8 -1
  112. package/dist/table/cell/table-cell.js +6 -2
  113. package/dist/table/grid-controller.d.ts +117 -0
  114. package/dist/table/grid-controller.js +293 -0
  115. package/dist/table/root/table-root.d.ts +47 -3
  116. package/dist/table/root/table-root.js +81 -24
  117. package/dist/table/row/table-row.d.ts +13 -1
  118. package/dist/table/row/table-row.js +20 -6
  119. package/dist/tag/group/tag-group.js +6 -15
  120. package/dist/time-picker/root/time-picker-root.d.ts +1 -0
  121. package/dist/time-picker/root/time-picker-root.js +3 -0
  122. package/dist/toast/item/toast-item.d.ts +6 -3
  123. package/dist/toast/item/toast-item.js +1 -0
  124. package/dist/toast/root/toast-root.d.ts +13 -1
  125. package/dist/toast/root/toast-root.js +20 -14
  126. package/dist/toggle/group/toggle-group.js +3 -5
  127. package/dist/toolbar/root/toolbar-root.d.ts +31 -0
  128. package/dist/toolbar/root/toolbar-root.js +39 -8
  129. package/dist/tooltip/root/tooltip-root.js +1 -2
  130. package/dist/video/root/video-root.d.ts +11 -0
  131. package/dist/video/root/video-root.js +15 -1
  132. package/dist/virtual-list/root/virtual-list-root.js +6 -18
  133. package/lint.json +1 -1
  134. package/package.json +5 -5
  135. package/types/jsx.d.ts +2005 -266
  136. package/types/svelte.d.ts +1 -0
  137. package/types/vue.d.ts +338 -62
  138. package/dist/kbd/keys.d.ts +0 -29
  139. package/dist/kbd/keys.js +0 -52
  140. package/dist/tooltip/focus-target.d.ts +0 -6
  141. package/dist/tooltip/focus-target.js +0 -9
@@ -4,33 +4,110 @@ 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, markRegion } from '@fusion.dev/helix-core';
8
- import { html, nothing } from 'lit';
7
+ import { customElement, HelixElement, markRegion, reportRuntime } from '@fusion.dev/helix-core';
8
+ import { css, html, nothing, } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
+ import { typedMarks } from '../../quote/marks.js';
10
11
  let HelixHeadlessBlockquote = class HelixHeadlessBlockquote extends HelixElement {
11
12
  constructor() {
12
13
  super(...arguments);
14
+ this.attribution = '';
13
15
  this.cite = '';
16
+ this.marks = false;
17
+ this.warned = false;
18
+ this.citeWarned = false;
19
+ this.readPassage = () => {
20
+ if (!this.marks || this.warned) {
21
+ return;
22
+ }
23
+ const text = [...this.childNodes]
24
+ .filter((node) => !(node instanceof Element) || !node.slot)
25
+ .map((node) => node.textContent ?? '')
26
+ .join('')
27
+ .trim();
28
+ if (!typedMarks(text.charAt(0))) {
29
+ return;
30
+ }
31
+ this.warned = true;
32
+ reportRuntime({
33
+ kind: 'typed-marks',
34
+ message: `<${this.localName} marks> starts with a typed quotation mark, and the element draws its own, so the mark shows twice. Take the typed mark out.`,
35
+ tag: this.localName,
36
+ });
37
+ };
14
38
  }
39
+ static { this.styles = css `
40
+ :host([marks]) [part='blockquote']::before {
41
+ content: open-quote / '';
42
+ }
43
+
44
+ :host([marks]) [part='blockquote']::after {
45
+ content: no-close-quote;
46
+ }
47
+ `; }
15
48
  markEmpty() {
16
- markRegion(this.renderRoot, 'attribution', ['attribution']);
49
+ markRegion(this.renderRoot, 'attribution', ['attribution'], {
50
+ occupied: 'slot[name="attribution"] > *',
51
+ });
52
+ const named = !this.partNamed('attribution')?.hasAttribute('data-empty');
53
+ const figure = this.partNamed('figure');
54
+ if (named) {
55
+ figure?.setAttribute('role', 'figure');
56
+ figure?.setAttribute('aria-labelledby', 'attribution');
57
+ }
58
+ else {
59
+ figure?.removeAttribute('role');
60
+ figure?.removeAttribute('aria-labelledby');
61
+ }
62
+ }
63
+ readCite() {
64
+ if (this.citeWarned || !/\S\s+\S/.test(this.cite || '')) {
65
+ return;
66
+ }
67
+ this.citeWarned = true;
68
+ reportRuntime({
69
+ kind: 'cite-not-url',
70
+ message: `<${this.localName} cite="${this.cite}"> holds words, and \`cite\` takes the URL of the source. Put who said it in \`attribution\`.`,
71
+ tag: this.localName,
72
+ });
73
+ }
74
+ firstUpdated(changed) {
75
+ super.firstUpdated(changed);
76
+ this.renderRoot.addEventListener('slotchange', this.readPassage);
17
77
  }
18
78
  updated(changed) {
19
79
  super.updated(changed);
20
80
  this.markEmpty();
81
+ if (changed.has('marks')) {
82
+ this.readPassage();
83
+ }
84
+ if (changed.has('cite')) {
85
+ this.readCite();
86
+ }
21
87
  }
22
88
  render() {
23
- return html `<figure aria-labelledby="attribution" part="figure">
89
+ return html `<div aria-labelledby="attribution" part="figure" role="figure">
24
90
  <blockquote part="blockquote" cite=${this.cite || nothing}><slot></slot></blockquote>
25
- <figcaption id="attribution" part="attribution"
26
- ><slot name="attribution" @slotchange=${() => this.markEmpty()}></slot
27
- ></figcaption>
28
- </figure>`;
91
+ ${this.renderAttribution()}
92
+ </div>`;
93
+ }
94
+ renderAttribution() {
95
+ return html `<div id="attribution" part="attribution"
96
+ ><slot name="attribution" @slotchange=${() => this.markEmpty()}
97
+ >${this.attribution ? html `<span>${this.attribution}</span>` : nothing}</slot
98
+ ></div
99
+ >`;
29
100
  }
30
101
  };
102
+ __decorate([
103
+ property()
104
+ ], HelixHeadlessBlockquote.prototype, "attribution", void 0);
31
105
  __decorate([
32
106
  property()
33
107
  ], HelixHeadlessBlockquote.prototype, "cite", void 0);
108
+ __decorate([
109
+ property({ reflect: true, type: Boolean })
110
+ ], HelixHeadlessBlockquote.prototype, "marks", void 0);
34
111
  HelixHeadlessBlockquote = __decorate([
35
112
  customElement('helix-headless-blockquote')
36
113
  ], HelixHeadlessBlockquote);
@@ -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 } from '@fusion.dev/helix-core';
7
+ import { customElement, HelixElement, hostAria, slotFilled, slotNamed, } from '@fusion.dev/helix-core';
8
8
  import { caretRight } from '@fusion.dev/helix-icons';
9
9
  import { html, nothing } from 'lit';
10
10
  import { property } from 'lit/decorators.js';
@@ -29,7 +29,7 @@ let HelixHeadlessBreadcrumb = class HelixHeadlessBreadcrumb extends HelixElement
29
29
  this.markEnd();
30
30
  }
31
31
  markEnd() {
32
- this.toggleState('end', slotFilled(this.renderRoot?.querySelector('slot[name="end"]')));
32
+ this.toggleState('end', slotFilled(slotNamed(this.renderRoot, 'end')));
33
33
  }
34
34
  renderSeparator() {
35
35
  return html `<slot name="separator"
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The index a key moves focus to, or `-1` for a key that moves nothing.
3
+ *
4
+ * Down and Up step forward and back, and Left and Right step by `step`, which
5
+ * core's `inlineStep` turns on a right-to-left page. Home and End go to the
6
+ * ends. A step wraps from the last button round to the first, as a toolbar's
7
+ * does.
8
+ *
9
+ * @param key The key pressed.
10
+ * @param step `1` or `-1` for Left and Right, from `inlineStep`, and `0` otherwise.
11
+ * @param from The index focus is on, or `-1` where it is on none of them.
12
+ * @param count How many buttons focus can move between.
13
+ */
14
+ export declare const arrowTarget: (key: string, step: number, from: number, count: number) => number;
@@ -0,0 +1,10 @@
1
+ export const arrowTarget = (key, step, from, count) => {
2
+ const by = key === 'ArrowDown' ? 1 : key === 'ArrowUp' ? -1 : step;
3
+ if (from < 0 || count === 0) {
4
+ return -1;
5
+ }
6
+ if (key === 'Home' || key === 'End') {
7
+ return key === 'Home' ? 0 : count - 1;
8
+ }
9
+ return by === 0 ? -1 : (from + by + count) % count;
10
+ };
@@ -9,19 +9,62 @@ import { type PropertyValues, type TemplateResult } from 'lit';
9
9
  * controls, and a development build warns once with the kind `missing-name`.
10
10
  *
11
11
  * The group does not manage focus, so each button keeps its own tab stop. For
12
- * one tab stop with arrow keys between the buttons, the APG toolbar pattern,
13
- * use Toolbar.
12
+ * one tab stop with arrow keys between a mix of controls, the APG toolbar
13
+ * pattern, use Toolbar.
14
14
  *
15
15
  * Buttons are slotted, so they stay your own elements and keep their own
16
16
  * appearance and size.
17
17
  *
18
+ * **Buttons that stay pressed.** With `selection-mode`, each button with a
19
+ * `value` is a toggle button: a press presses or releases it, and it says so
20
+ * with `aria-pressed`. `single` keeps one pressed at a time, and a press on the
21
+ * pressed one releases it. `multiple` lets any number be pressed. `value` is
22
+ * the pressed values, comma separated. A press sends a cancelable
23
+ * `helix-before-change` with the value it would leave, then `change`.
24
+ *
25
+ * The pressed state is written on the control inside each button's shadow
26
+ * root, so the markup you rendered is never changed and a framework hydrating
27
+ * it sees what its server sent. That is also why only Helix buttons are
28
+ * toggles: a native `<button>` or a button without a `value` stays a plain
29
+ * action button in the group, and a native one without its own
30
+ * `aria-pressed` warns once, with the runtime kind `untold-toggle`. The inner
31
+ * control also carries
32
+ * `--button-group-pressed`, `1` or `0`, for a `::part(button)` rule of yours.
33
+ *
34
+ * Use Toggle Group instead for a choice that belongs in a form, or one where
35
+ * the arrow keys choose as they move: it is a `radiogroup` of options with a
36
+ * form value, `required` and validation. A Button Group in a selection mode is
37
+ * a set of actions that stay on, such as bold and italic, or a view that a
38
+ * press switches, and it submits nothing.
39
+ *
40
+ * **Keyboard.** In a selection mode the group is one tab stop: the button
41
+ * focus was last on, then the first pressed one, then the first one.
42
+ *
43
+ * | Key | What it does |
44
+ * |---|---|
45
+ * | Right, Down | Moves to the next button, from the last back to the first. |
46
+ * | Left, Up | Moves to the button before, from the first round to the last. |
47
+ * | Home, End | Moves to the first or the last button. |
48
+ * | Space, Enter | Presses or releases the focused button. |
49
+ *
50
+ * Left and Right swap on a right-to-left page, and a disabled button is
51
+ * passed over. In a selection mode the group is a `toolbar`, with
52
+ * `aria-orientation` under `orientation="vertical"`. Inside a toolbar it stays
53
+ * a named `group`, and the toolbar's own arrow keys walk its buttons, so a
54
+ * reader meets one toolbar.
55
+ *
18
56
  * @since 0.1.0
19
57
  * @status stable
20
58
  * @category actions
21
- * @aliases Segmented buttons, Split button
59
+ * @aliases Segmented buttons, Split button, Toggle buttons
22
60
  * @documentation https://fusion.dev/helix/components/button-group
23
61
  *
24
62
  * @slot - The buttons in the group.
63
+ *
64
+ * @event {Event} change - Fired after a press changes `value`, in a selection mode.
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
+ *
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.
25
68
  */
26
69
  export declare class HelixHeadlessButtonGroup extends HelixElement {
27
70
  /**
@@ -51,8 +94,103 @@ export declare class HelixHeadlessButtonGroup extends HelixElement {
51
94
  * column.
52
95
  */
53
96
  orientation?: 'horizontal' | 'vertical';
97
+ /**
98
+ * Whether the buttons stay pressed: `single` keeps one pressed at a time and
99
+ * `multiple` any number. Unset, they are action buttons, each its own tab
100
+ * stop, as before.
101
+ *
102
+ * Optional, so the attribute is absent until a consumer writes it.
103
+ */
104
+ selectionMode?: 'multiple' | 'single';
105
+ /**
106
+ * The values of the pressed buttons, comma separated, in a selection mode.
107
+ * A button's own `value` names it, so a value holds no comma.
108
+ *
109
+ * The attribute is the seed, and a press changes the property, which does
110
+ * not reflect, as a native control's `value` does not.
111
+ */
112
+ value: string;
113
+ /**
114
+ * The Helix buttons of a group in a selection mode, in the order they are
115
+ * written, and none otherwise. Empty on a server, which has no tree.
116
+ */
117
+ private get buttons();
118
+ /**
119
+ * Whether the group holds the tab stop and the arrow keys: in a selection
120
+ * mode, and not inside a toolbar, whose own keys walk the buttons.
121
+ */
122
+ private get roving();
123
+ /** Whether a button is pressed: its value is among the group's. */
124
+ private pressed;
125
+ /** The button focus was last on, which holds the tab stop. */
126
+ private lastFocused;
127
+ /**
128
+ * The buttons this group has told something, each with whether it moved
129
+ * their tab stop, so a button that leaves the group, or a group that leaves
130
+ * its selection mode, is put back as it was.
131
+ */
132
+ private readonly told;
133
+ private watcher;
134
+ /**
135
+ * Tells every button whether it is pressed and whether it holds the stop,
136
+ * and puts back the ones that left.
137
+ */
138
+ private sync;
139
+ /** Whether this group has said a native button in it cannot be a toggle, so it says so once. */
140
+ private warned;
141
+ /**
142
+ * Says once that a native `<button>` in a selection mode stays an action
143
+ * button, since its pressed state would be an attribute on the page's markup.
144
+ * One with its own `aria-pressed` is the page's to say, and is not reported.
145
+ */
146
+ private warn;
147
+ /**
148
+ * Writes a button's pressed state onto its control, once the button has
149
+ * rendered it, and takes it off a button the group no longer holds.
150
+ *
151
+ * **On the control inside the button's shadow root, never on the button.**
152
+ * An attribute written on the consumer's element is a hydration mismatch a
153
+ * React consumer's hydration reports and does not patch up, which is why
154
+ * `tellDisclosure` hands a trigger `expanded` as a property. Inside the
155
+ * button's own shadow root no framework compares the attribute, and the
156
+ * reader is on that control. Read at the moment of the write, so the last
157
+ * press wins whatever order renders finish in.
158
+ *
159
+ * The control is the `<button>` a Helix button renders, which is where its
160
+ * `delegatesFocus` sends focus and what a reader is on.
161
+ *
162
+ * A button that carries `aria-pressed` itself is the consumer's to say, and
163
+ * its own forwarding carries that down instead.
164
+ */
165
+ private mark;
166
+ /**
167
+ * Says on a button's control whether it is pressed, or that it is no toggle
168
+ * at all where `pressed` is undefined. A styled tier draws the look here.
169
+ *
170
+ * @param control The focusable control inside the button's shadow root.
171
+ * @param pressed Whether the button is pressed, or undefined where it is not a toggle.
172
+ */
173
+ protected markPressed(control: HTMLElement, pressed: boolean | undefined): void;
174
+ /**
175
+ * Presses or releases the button a press acted on. `helix-press` rather than
176
+ * `click`, since a button sends it only for a press that acted: never while
177
+ * it is disabled or pending.
178
+ */
179
+ private readonly handlePress;
180
+ /**
181
+ * Moves focus between the buttons without pressing them, a toolbar's
182
+ * reading: a press is its own act, on Space or Enter, which the button
183
+ * answers natively. Both axes, as every set here answers both, and the
184
+ * horizontal pair turns on a right-to-left page.
185
+ */
186
+ private readonly handleKeydown;
187
+ /** Moves the tab stop to the button focus arrived on. */
188
+ private readonly noteFocus;
189
+ connectedCallback(): void;
190
+ disconnectedCallback(): void;
54
191
  protected willUpdate(changed: PropertyValues): void;
55
192
  protected firstUpdated(changed: PropertyValues): void;
193
+ protected updated(changed: PropertyValues): void;
56
194
  protected render(): TemplateResult;
57
195
  }
58
196
  declare global {
@@ -4,22 +4,156 @@ 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 { auditName, customElement, HelixElement, hostAria } from '@fusion.dev/helix-core';
7
+ import { auditName, commaList, customElement, HelixElement, hostAria, inlineStep, pressedValue, reportRuntime, requestValue, rovingStop, toolbarAround, whenHydrated, } from '@fusion.dev/helix-core';
8
8
  import { html } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
+ import { arrowTarget } from '../arrows.js';
11
+ const isGroupButton = (child) => child instanceof HTMLElement && 'focusable' in child && 'value' in child;
10
12
  let HelixHeadlessButtonGroup = class HelixHeadlessButtonGroup extends HelixElement {
11
13
  constructor() {
12
14
  super(...arguments);
13
15
  this.label = '';
16
+ this.value = '';
17
+ this.lastFocused = undefined;
18
+ this.told = new Map();
19
+ this.warned = false;
20
+ this.handlePress = (event) => {
21
+ const mode = this.selectionMode;
22
+ const button = this.buttons.find((one) => event.composedPath().includes(one));
23
+ if (mode && button && button.value !== '') {
24
+ requestValue(this, pressedValue(mode, this.value, button.value), (next) => {
25
+ this.value = next;
26
+ });
27
+ }
28
+ };
29
+ this.handleKeydown = (event) => {
30
+ const buttons = this.roving ? this.buttons.filter((one) => one.disabled !== true) : [];
31
+ const from = buttons.findIndex((one) => event.composedPath().includes(one));
32
+ const to = arrowTarget(event.key, inlineStep(this, event.key), from, buttons.length);
33
+ if (to >= 0 && !event.defaultPrevented) {
34
+ event.preventDefault();
35
+ buttons[to]?.focus();
36
+ }
37
+ };
38
+ this.noteFocus = (event) => {
39
+ const button = this.buttons.find((one) => event.composedPath().includes(one));
40
+ if (button && button !== this.lastFocused) {
41
+ this.lastFocused = button;
42
+ this.sync();
43
+ }
44
+ };
45
+ }
46
+ get buttons() {
47
+ return this.selectionMode && this.children ? [...this.children].filter(isGroupButton) : [];
48
+ }
49
+ get roving() {
50
+ return this.selectionMode !== undefined && toolbarAround(this) === null;
51
+ }
52
+ pressed(button) {
53
+ return button.value !== '' && commaList(this.value).includes(button.value);
54
+ }
55
+ sync() {
56
+ const buttons = this.buttons;
57
+ const roving = this.roving;
58
+ const stop = roving
59
+ ? rovingStop(buttons, this.lastFocused, {
60
+ disabled: (button) => button.disabled === true,
61
+ selected: (button) => this.pressed(button),
62
+ })
63
+ : undefined;
64
+ for (const [button, roved] of this.told) {
65
+ if (!buttons.includes(button)) {
66
+ this.told.delete(button);
67
+ this.mark(button);
68
+ if (roved) {
69
+ whenHydrated(button, () => {
70
+ button.focusable = true;
71
+ });
72
+ }
73
+ }
74
+ }
75
+ for (const button of buttons) {
76
+ this.told.set(button, roving);
77
+ this.mark(button);
78
+ if (roving) {
79
+ whenHydrated(button, () => {
80
+ button.focusable = button === stop;
81
+ });
82
+ }
83
+ }
84
+ for (const child of this.selectionMode && this.children ? this.children : []) {
85
+ if (child.localName.includes('-') && !isGroupButton(child)) {
86
+ void customElements.whenDefined(child.localName).then(() => this.sync());
87
+ }
88
+ else if (child.localName === 'button' && !child.hasAttribute('aria-pressed')) {
89
+ this.warn(child);
90
+ }
91
+ }
92
+ }
93
+ warn(button) {
94
+ if (this.warned) {
95
+ return;
96
+ }
97
+ this.warned = true;
98
+ reportRuntime({
99
+ kind: 'untold-toggle',
100
+ message: `<${this.localName}> cannot press a native <button>: its state would be written onto your ` +
101
+ 'markup. Slot a Helix button with a `value`, or set `aria-pressed` on the button yourself.',
102
+ tag: button.localName,
103
+ });
104
+ }
105
+ mark(button) {
106
+ void button.updateComplete.then(() => {
107
+ const control = button.shadowRoot?.querySelector('button');
108
+ if (control && !button.hasAttribute('aria-pressed')) {
109
+ const toggle = this.told.has(button) && button.value !== '';
110
+ this.markPressed(control, toggle ? this.pressed(button) : undefined);
111
+ }
112
+ });
113
+ }
114
+ markPressed(control, pressed) {
115
+ if (pressed === undefined) {
116
+ control.removeAttribute('aria-pressed');
117
+ }
118
+ else {
119
+ control.setAttribute('aria-pressed', String(pressed));
120
+ }
121
+ control.style.setProperty('--button-group-pressed', pressed === undefined ? '' : pressed ? '1' : '0');
122
+ }
123
+ connectedCallback() {
124
+ super.connectedCallback();
125
+ this.addEventListener('focusin', this.noteFocus);
126
+ this.addEventListener('helix-press', this.handlePress);
127
+ this.addEventListener('keydown', this.handleKeydown);
128
+ this.watcher = new MutationObserver(() => this.sync());
129
+ this.watcher.observe(this, {
130
+ attributeFilter: ['disabled', 'value'],
131
+ childList: true,
132
+ subtree: true,
133
+ });
134
+ this.requestUpdate();
135
+ }
136
+ disconnectedCallback() {
137
+ super.disconnectedCallback();
138
+ this.removeEventListener('focusin', this.noteFocus);
139
+ this.removeEventListener('helix-press', this.handlePress);
140
+ this.removeEventListener('keydown', this.handleKeydown);
141
+ this.watcher?.disconnect();
14
142
  }
15
143
  willUpdate(changed) {
16
144
  super.willUpdate(changed);
17
- hostAria(this.internals, { label: this.label, role: 'group' });
145
+ const toolbar = this.selectionMode !== undefined && toolbarAround(this) === null;
146
+ hostAria(this.internals, { label: this.label, role: toolbar ? 'toolbar' : 'group' });
147
+ this.internals.ariaOrientation = toolbar ? (this.orientation ?? null) : null;
18
148
  }
19
149
  firstUpdated(changed) {
20
150
  super.firstUpdated(changed);
21
151
  auditName(this, () => null, 'Give it a `label`, or an `aria-label` that says what the buttons are for.', () => this.label !== '');
22
152
  }
153
+ updated(changed) {
154
+ super.updated(changed);
155
+ this.sync();
156
+ }
23
157
  render() {
24
158
  return html `<slot></slot>`;
25
159
  }
@@ -30,6 +164,12 @@ __decorate([
30
164
  __decorate([
31
165
  property({ reflect: true })
32
166
  ], HelixHeadlessButtonGroup.prototype, "orientation", void 0);
167
+ __decorate([
168
+ property({ attribute: 'selection-mode', reflect: true })
169
+ ], HelixHeadlessButtonGroup.prototype, "selectionMode", void 0);
170
+ __decorate([
171
+ property()
172
+ ], HelixHeadlessButtonGroup.prototype, "value", void 0);
33
173
  HelixHeadlessButtonGroup = __decorate([
34
174
  customElement('helix-headless-button-group')
35
175
  ], HelixHeadlessButtonGroup);
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Moving the view while something is carried to its edge: a card, a corner, a
3
+ * connection being drawn, or the box that selects.
4
+ *
5
+ * **A board is larger than its box, and a hand stops at the edge of the box.**
6
+ * Without this, carrying a card to a place off screen took a drop at the edge,
7
+ * a pan, and a second pick-up, and a connection to a card off screen could not
8
+ * be drawn by pointer at all.
9
+ */
10
+ /** A point on screen, in pixels. */
11
+ export interface EdgePoint {
12
+ x: number;
13
+ y: number;
14
+ }
15
+ /** The box the view is drawn in, on screen. */
16
+ export interface EdgeRect {
17
+ bottom: number;
18
+ left: number;
19
+ right: number;
20
+ top: number;
21
+ }
22
+ /** How near an edge, in pixels, the view starts to move. */
23
+ export declare const EDGE_ZONE = 32;
24
+ /** The most the view moves in one frame, in pixels: at the edge and past it. */
25
+ export declare const EDGE_MOST = 12;
26
+ /**
27
+ * How far the view should move toward each side in one frame, in pixels, for
28
+ * a pointer at `point`: positive toward the right and the bottom, nothing
29
+ * outside the zone, and faster the nearer the edge, up to `most` at it and
30
+ * past it.
31
+ *
32
+ * A box narrower than two zones moves only once the pointer is past its edge,
33
+ * so a small board is not always moving.
34
+ */
35
+ export declare const edgeSpeed: (point: EdgePoint, rect: EdgeRect, zone?: number, most?: number) => EdgePoint;
36
+ /**
37
+ * The frames that move the view while a pointer holds something near an edge.
38
+ *
39
+ * `follow` with every pointer move keeps it going while the pointer is in the
40
+ * zone, and `stop` ends it, which the board calls when the gesture ends and
41
+ * when it leaves the page. Each frame hands `step` how far to move, and the
42
+ * board moves the view and carries what is in hand with it.
43
+ */
44
+ export declare class EdgePan {
45
+ private readonly rect;
46
+ private readonly step;
47
+ private readonly ended;
48
+ /** The frame asked for, while the view is moving. */
49
+ private frame;
50
+ /** Where the pointer last was. */
51
+ private point;
52
+ constructor(rect: () => EdgeRect | undefined, step: (by: EdgePoint) => void, ended?: () => void);
53
+ /** Whether the view is moving. */
54
+ get moving(): boolean;
55
+ /** Where the pointer is now, which starts the frames where it is in the zone. */
56
+ follow(point: EdgePoint): void;
57
+ /** Ends the frames. */
58
+ stop(): void;
59
+ private speed;
60
+ private readonly tick;
61
+ }
@@ -0,0 +1,63 @@
1
+ export const EDGE_ZONE = 32;
2
+ export const EDGE_MOST = 12;
3
+ export const edgeSpeed = (point, rect, zone = EDGE_ZONE, most = EDGE_MOST) => {
4
+ const along = (at, start, end) => {
5
+ if (at >= end) {
6
+ return 1;
7
+ }
8
+ if (at <= start) {
9
+ return -1;
10
+ }
11
+ const reach = end - start < zone * 2 ? 0 : zone;
12
+ if (reach > 0 && at > end - reach) {
13
+ return (at - (end - reach)) / reach;
14
+ }
15
+ if (reach > 0 && at < start + reach) {
16
+ return -(start + reach - at) / reach;
17
+ }
18
+ return 0;
19
+ };
20
+ return {
21
+ x: Math.round(along(point.x, rect.left, rect.right) * most),
22
+ y: Math.round(along(point.y, rect.top, rect.bottom) * most),
23
+ };
24
+ };
25
+ export class EdgePan {
26
+ constructor(rect, step, ended = () => undefined) {
27
+ this.rect = rect;
28
+ this.step = step;
29
+ this.ended = ended;
30
+ this.tick = () => {
31
+ const by = this.speed();
32
+ if (by.x === 0 && by.y === 0) {
33
+ this.frame = undefined;
34
+ this.ended();
35
+ return;
36
+ }
37
+ this.step(by);
38
+ this.frame = requestAnimationFrame(this.tick);
39
+ };
40
+ }
41
+ get moving() {
42
+ return this.frame !== undefined;
43
+ }
44
+ follow(point) {
45
+ this.point = point;
46
+ const by = this.speed();
47
+ if (this.frame === undefined && (by.x !== 0 || by.y !== 0)) {
48
+ this.frame = requestAnimationFrame(this.tick);
49
+ }
50
+ }
51
+ stop() {
52
+ this.point = undefined;
53
+ if (this.frame !== undefined) {
54
+ cancelAnimationFrame(this.frame);
55
+ this.frame = undefined;
56
+ this.ended();
57
+ }
58
+ }
59
+ speed() {
60
+ const rect = this.rect();
61
+ return this.point && rect ? edgeSpeed(this.point, rect) : { x: 0, y: 0 };
62
+ }
63
+ }
@@ -150,6 +150,16 @@ export declare class HelixHeadlessCanvasNode extends HelixElement {
150
150
  * point and one to a circle on its curve without the board knowing which.
151
151
  */
152
152
  shape?: 'circle' | 'diamond' | 'pill' | 'square';
153
+ /**
154
+ * @internal What the card's state adds to what it says, in the page's words,
155
+ * written by the board: its `selected-label` while it is selected. Read
156
+ * first, before what the tone means.
157
+ *
158
+ * **A selection a reader cannot see has to be said.** A card is an article,
159
+ * and an article has no selected state for `aria-selected` to carry, so a
160
+ * reader who selected three cards heard nothing on any of them.
161
+ */
162
+ status: string;
153
163
  /**
154
164
  * @internal Whether this is the card the reader selected, written by the
155
165
  * board. Kept apart from focus, so a reader who clicks a button beside the
@@ -211,8 +221,8 @@ export declare class HelixHeadlessCanvasNode extends HelixElement {
211
221
  */
212
222
  focus(options?: FocusOptions): void;
213
223
  /**
214
- * The card, and the sentence that says what its tone means and where it
215
- * leads.
224
+ * The card, and the sentence that says whether it is selected, what its
225
+ * tone means and where it leads.
216
226
  *
217
227
  * **`aria-describedby` at a hidden element, which is allowed and is the
218
228
  * point.** A description is computed from its target whether or not the