@fusion.dev/helix-headless 0.215.2 → 0.216.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 (113) hide show
  1. package/api.json +1 -1
  2. package/custom-elements.json +1 -1
  3. package/dist/area-chart/root/area-chart-root.d.ts +1 -0
  4. package/dist/area-chart/root/area-chart-root.js +1 -1
  5. package/dist/bar-chart/root/bar-chart-root.d.ts +1 -0
  6. package/dist/bar-chart/root/bar-chart-root.js +1 -1
  7. package/dist/button/root/button-root.d.ts +3 -1
  8. package/dist/button/root/button-root.js +2 -1
  9. package/dist/chat/composer/chat-composer.d.ts +18 -1
  10. package/dist/chat/composer/chat-composer.js +8 -3
  11. package/dist/checkbox/root/checkbox-root.d.ts +3 -1
  12. package/dist/checkbox/root/checkbox-root.js +2 -1
  13. package/dist/code-block/highlight.d.ts +9 -0
  14. package/dist/code-block/highlight.js +39 -0
  15. package/dist/code-block/highlighter.d.ts +44 -0
  16. package/dist/code-block/highlighter.js +44 -0
  17. package/dist/code-block/index.d.ts +1 -0
  18. package/dist/code-block/index.js +1 -0
  19. package/dist/code-block/keyed-lines.d.ts +23 -0
  20. package/dist/code-block/keyed-lines.js +75 -0
  21. package/dist/code-block/root/code-block-root.d.ts +6 -23
  22. package/dist/code-block/root/code-block-root.js +8 -25
  23. package/dist/code-editor/index.d.ts +1 -0
  24. package/dist/code-editor/index.js +1 -0
  25. package/dist/code-editor/root/code-editor-root.d.ts +367 -0
  26. package/dist/code-editor/root/code-editor-root.js +746 -0
  27. package/dist/code-input/index.d.ts +1 -0
  28. package/dist/code-input/index.js +1 -0
  29. package/dist/code-input/root/code-input-root.d.ts +119 -0
  30. package/dist/code-input/root/code-input-root.js +142 -0
  31. package/dist/combobox/root/combobox-root.d.ts +6 -1
  32. package/dist/combobox/root/combobox-root.js +4 -2
  33. package/dist/command/root/command-root.d.ts +61 -11
  34. package/dist/command/root/command-root.js +88 -3
  35. package/dist/donut-chart/root/donut-chart-root.d.ts +1 -0
  36. package/dist/donut-chart/root/donut-chart-root.js +1 -1
  37. package/dist/element-events.generated.js +36 -3
  38. package/dist/filter-input/root/filter-input-root.d.ts +9 -1
  39. package/dist/filter-input/root/filter-input-root.js +7 -3
  40. package/dist/heatmap/root/heatmap-root.d.ts +1 -0
  41. package/dist/heatmap/root/heatmap-root.js +1 -1
  42. package/dist/index.d.ts +4 -0
  43. package/dist/index.js +4 -0
  44. package/dist/input/root/input-root.d.ts +14 -2
  45. package/dist/input/root/input-root.js +11 -3
  46. package/dist/kbd/root/kbd-root.js +9 -2
  47. package/dist/line-chart/root/line-chart-root.d.ts +1 -0
  48. package/dist/line-chart/root/line-chart-root.js +1 -1
  49. package/dist/listbox/root/listbox-root.d.ts +8 -1
  50. package/dist/listbox/root/listbox-root.js +7 -4
  51. package/dist/markdown/blocks.d.ts +66 -0
  52. package/dist/markdown/blocks.js +326 -0
  53. package/dist/markdown/index.d.ts +6 -0
  54. package/dist/markdown/index.js +5 -0
  55. package/dist/markdown/inline.d.ts +57 -0
  56. package/dist/markdown/inline.js +483 -0
  57. package/dist/markdown/root/markdown-root.d.ts +99 -0
  58. package/dist/markdown/root/markdown-root.js +93 -0
  59. package/dist/markdown/stream.d.ts +47 -0
  60. package/dist/markdown/stream.js +62 -0
  61. package/dist/markdown/tree.d.ts +57 -0
  62. package/dist/markdown/tree.js +144 -0
  63. package/dist/markdown/writer.d.ts +33 -0
  64. package/dist/markdown/writer.js +139 -0
  65. package/dist/menu/root/menu-root.d.ts +53 -0
  66. package/dist/menu/root/menu-root.js +92 -15
  67. package/dist/pagination/size/pagination-size.d.ts +3 -0
  68. package/dist/pagination/size/pagination-size.js +2 -1
  69. package/dist/scatter-chart/root/scatter-chart-root.d.ts +1 -0
  70. package/dist/scatter-chart/root/scatter-chart-root.js +1 -1
  71. package/dist/select/root/select-root.d.ts +6 -1
  72. package/dist/select/root/select-root.js +4 -2
  73. package/dist/shortcut-overlay/index.d.ts +1 -0
  74. package/dist/shortcut-overlay/index.js +1 -0
  75. package/dist/shortcut-overlay/root/shortcut-overlay-root.d.ts +128 -0
  76. package/dist/shortcut-overlay/root/shortcut-overlay-root.js +275 -0
  77. package/dist/slider/root/slider-root.d.ts +3 -1
  78. package/dist/slider/root/slider-root.js +2 -1
  79. package/dist/spatial-bar-chart/root/spatial-bar-chart-root.d.ts +1 -0
  80. package/dist/spatial-bar-chart/root/spatial-bar-chart-root.js +1 -1
  81. package/dist/spatial-heatmap/root/spatial-heatmap-root.d.ts +1 -0
  82. package/dist/spatial-heatmap/root/spatial-heatmap-root.js +1 -1
  83. package/dist/spatial-ribbon-chart/root/spatial-ribbon-chart-root.d.ts +1 -0
  84. package/dist/spatial-ribbon-chart/root/spatial-ribbon-chart-root.js +1 -1
  85. package/dist/spatial-scatter-chart/root/spatial-scatter-chart-root.d.ts +1 -0
  86. package/dist/spatial-scatter-chart/root/spatial-scatter-chart-root.js +1 -1
  87. package/dist/spatial-surface-chart/root/spatial-surface-chart-root.d.ts +1 -0
  88. package/dist/spatial-surface-chart/root/spatial-surface-chart-root.js +1 -1
  89. package/dist/spatial-treemap/root/spatial-treemap-root.d.ts +1 -0
  90. package/dist/spatial-treemap/root/spatial-treemap-root.js +1 -1
  91. package/dist/switch/root/switch-root.d.ts +3 -1
  92. package/dist/switch/root/switch-root.js +2 -1
  93. package/dist/tabs/tab/tab.d.ts +3 -1
  94. package/dist/tabs/tab/tab.js +2 -1
  95. package/dist/textarea/root/textarea-root.d.ts +20 -4
  96. package/dist/textarea/root/textarea-root.js +31 -23
  97. package/dist/time-input/root/time-input-root.d.ts +6 -0
  98. package/dist/time-input/root/time-input-root.js +4 -2
  99. package/dist/time-picker/root/time-picker-root.d.ts +6 -0
  100. package/dist/time-picker/root/time-picker-root.js +4 -2
  101. package/dist/toast/root/toast-root.d.ts +64 -0
  102. package/dist/toast/root/toast-root.js +128 -3
  103. package/dist/toggle/root/toggle-root.d.ts +3 -1
  104. package/dist/toggle/root/toggle-root.js +2 -1
  105. package/dist/virtual-list/root/virtual-list-root.d.ts +18 -2
  106. package/dist/virtual-list/root/virtual-list-root.js +103 -5
  107. package/examples.json +1 -1
  108. package/lint.json +1 -1
  109. package/package.json +8 -8
  110. package/types/jsx.d.ts +5315 -596
  111. package/types/svelte.d.ts +4 -0
  112. package/types/vue.d.ts +1085 -4
  113. package/types/words.d.ts +491 -0
@@ -0,0 +1 @@
1
+ export * from './root/code-input-root.js';
@@ -0,0 +1 @@
1
+ export * from './root/code-input-root.js';
@@ -0,0 +1,119 @@
1
+ import { type PropertyValues, type TemplateResult } from 'lit';
2
+ import { type CodeBlockHighlighter, HighlighterController } from '../../code-block/highlighter.js';
3
+ import { HelixHeadlessTextarea } from '../../textarea/root/textarea-root.js';
4
+ /**
5
+ * @summary A field for a short piece of code, highlighted as it is typed.
6
+ *
7
+ * @description
8
+ * A `Textarea` for code: the same field in a form, with the same label, hint,
9
+ * validation, `voice` and selection methods, drawing its text in the
10
+ * highlighter's colors. The reader types in a real `<textarea>`, so the caret,
11
+ * selection, undo, spellcheck being off and autofill being off are the
12
+ * platform's, and the field works before any script loads.
13
+ *
14
+ * - `language` picks the grammar, such as `json`, `css`, `ts`, `html` or `sh`.
15
+ * - The field grows with its text from `rows` lines to `max-rows`, and
16
+ * scrolls past them.
17
+ * - Lines keep their length and the field scrolls sideways, unless `wrap` is
18
+ * `soft`.
19
+ *
20
+ * **The colors are the page's choice.** Helix ships no grammar, so a page
21
+ * that wants color registers a highlighter once, the one Code Block uses:
22
+ * `import '@fusion.dev/helix-sandbox/code-block'` registers Helix's own, or
23
+ * `registerHighlighter` takes Shiki or Prism. Without one the code is drawn
24
+ * plain, and `highlight` sets one for a single field.
25
+ *
26
+ * For a whole file, with line numbers, folding, search and problems listed by
27
+ * line, use `Code Editor`. Code Input is for a few dozen lines in a form: a
28
+ * setting in JSON, a style rule, a query, a template.
29
+ *
30
+ * **Keyboard.** Tab indents the line, or every line the selection touches,
31
+ * and Shift+Tab takes the indent away. Enter starts the next line at the same
32
+ * indent. Escape then Tab leaves the field, so it is never a trap for a reader
33
+ * without a pointer.
34
+ *
35
+ * @since 0.216.0
36
+ * @status experimental
37
+ * @category forms
38
+ * @aliases Code field, Code textarea, JSON input, Syntax highlighted input
39
+ * @documentation https://fusion.dev/helix/components/code-input
40
+ *
41
+ * @slot clear-icon - Replaces the cross on the clear button, under `clearable`.
42
+ * @slot end - After the code, for an icon or an action beside it.
43
+ * @slot feedback - The message that says what is wrong, for content richer than a string. It shows while there is a message.
44
+ * @slot hint - Help under the field, for content richer than the `hint` text.
45
+ * @slot label - The label, for content richer than the `label` text.
46
+ * @slot start - Before the code, for an icon.
47
+ *
48
+ * @event {Event} change - Sent from the textarea when the user finishes an edit.
49
+ * @event {HelixBeforeChangeEvent} helix-before-change - Fired as the text changes, and cancelable.
50
+ * @event {HelixBeforeListenEvent} helix-before-listen - The dictation button was pressed. Cancelable: cancel it to keep the field from listening.
51
+ * @event {HelixClearEvent} helix-clear - Fired after the clear button has emptied the field, after `input` and `change`.
52
+ * @event {HelixListenEvent} helix-listen - Dictation settled on a phrase and put it in the value, after the field's own `input`.
53
+ * @event {Event} input - Fired after the value changes.
54
+ *
55
+ * @csspart clear - The button that empties the field, under `clearable`.
56
+ * @csspart control - The textarea the reader types in, whose own text is transparent over the highlighted text.
57
+ * @csspart feedback - The message that says what is wrong.
58
+ * @csspart field - The box that holds the code.
59
+ * @csspart highlight - The highlighted text, drawn under the textarea.
60
+ * @csspart hint - Help under the field, while nothing is wrong.
61
+ * @csspart label - The label above the field.
62
+ * @csspart layers - The highlighted text and the textarea, one over the other.
63
+ * @csspart scroller - The box that scrolls past `max-rows`, and sideways past a long line.
64
+ * @csspart voice - The dictation button, under `voice`.
65
+ * @csspart voice-listening - The dictation button while it listens. Set alongside `voice`.
66
+ * @csspart voice-status - What a screen reader hears as dictation starts and stops, drawn for no one else.
67
+ *
68
+ * @cssstate blank - The field holds no text.
69
+ * @cssstate invalid - The control has an error to show, from the browser's validation, `validate`, or your `feedback`.
70
+ * @cssstate listening - While the field listens for dictation. The element sets `--dictation-level` on itself meanwhile, from 0 to 1, for a meter.
71
+ */
72
+ export declare class HelixHeadlessCodeInput extends HelixHeadlessTextarea {
73
+ protected static markRules: string[];
74
+ /**
75
+ * The key this family's words are registered under, its styled tag:
76
+ * `registerWords('de', { 'helix-code-input': { clearLabel: 'Leeren' } })`.
77
+ */
78
+ static words: string;
79
+ /**
80
+ * Turns the code into highlighted markup for its `language`, for this field
81
+ * alone. Absent, the field draws with the highlighter `registerHighlighter`
82
+ * gave the page, and plain where there is none. The markup is drawn as
83
+ * given, so it escapes the code, as every highlighter does.
84
+ */
85
+ highlight?: CodeBlockHighlighter | undefined;
86
+ /** The grammar to highlight, such as `json`, `css`, `ts`, `html` or `sh`. Empty is plain text. */
87
+ language: string;
88
+ /**
89
+ * How long lines are drawn: `off` keeps each line its length and scrolls
90
+ * sideways, as code reads, and `soft` wraps at the field's edge.
91
+ */
92
+ wrap: 'hard' | 'off' | 'soft';
93
+ /** Whether Escape came just before, which lets Tab leave the field. */
94
+ private escaped;
95
+ /** On the page's list of elements a highlighter registration reaches. */
96
+ protected readonly highlighting: HighlighterController;
97
+ /** The page registered another highlighter: draws again with it. */
98
+ highlighterChanged(): void;
99
+ /** The layers grow with the highlighted text, so the textarea never measures itself. */
100
+ protected grow(): void;
101
+ protected firstUpdated(changed: PropertyValues): void;
102
+ protected updated(changed: PropertyValues): void;
103
+ /** The code as markup: highlighted where a highlighter reads its language, plain otherwise. */
104
+ private markup;
105
+ /** Indents or outdents every line the selection touches, and keeps them selected. */
106
+ private shift;
107
+ private readonly handleKeydown;
108
+ /**
109
+ * The textarea over its highlighted text. The text ends in a space where
110
+ * the code ends in a line break, or the highlight would be a line shorter
111
+ * than the textarea and the caret on the last line would sit below it.
112
+ */
113
+ protected renderControl(): TemplateResult;
114
+ }
115
+ declare global {
116
+ interface HTMLElementTagNameMap {
117
+ 'helix-headless-code-input': HelixHeadlessCodeInput;
118
+ }
119
+ }
@@ -0,0 +1,142 @@
1
+ import { __decorate } from "tslib";
2
+ import { customElement, LAYERS_RULE, SCROLLER_RULE, SPOKEN_RULE } from '@fusion.dev/helix-core';
3
+ import { html } from 'lit';
4
+ import { property } from 'lit/decorators.js';
5
+ import { highlightedLines } from '../../code-block/highlight.js';
6
+ import { HighlighterController, registeredHighlighter, } from '../../code-block/highlighter.js';
7
+ import { keyedLines } from '../../code-block/keyed-lines.js';
8
+ import { HelixHeadlessTextarea } from '../../textarea/root/textarea-root.js';
9
+ const escapeText = (text) => text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
10
+ const INDENT = ' ';
11
+ const lineStart = (text, at) => text.lastIndexOf('\n', at - 1) + 1;
12
+ const lineEnd = (text, at) => {
13
+ const end = text.indexOf('\n', at);
14
+ return end === -1 ? text.length : end;
15
+ };
16
+ const typeText = (control, text) => {
17
+ const typed = typeof document.execCommand === 'function' && document.execCommand('insertText', false, text);
18
+ if (!typed) {
19
+ control.setRangeText(text, control.selectionStart, control.selectionEnd, 'end');
20
+ control.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
21
+ }
22
+ };
23
+ let HelixHeadlessCodeInput = class HelixHeadlessCodeInput extends HelixHeadlessTextarea {
24
+ constructor() {
25
+ super(...arguments);
26
+ this.language = '';
27
+ this.wrap = 'off';
28
+ this.escaped = false;
29
+ this.highlighting = new HighlighterController(this);
30
+ this.handleKeydown = (event) => {
31
+ const control = event.target;
32
+ if (event.key === 'Escape') {
33
+ this.escaped = true;
34
+ return;
35
+ }
36
+ const escaped = this.escaped;
37
+ this.escaped = false;
38
+ if (this.readOnly ||
39
+ event.isComposing ||
40
+ event.altKey ||
41
+ event.ctrlKey ||
42
+ event.metaKey ||
43
+ !(control instanceof HTMLTextAreaElement)) {
44
+ return;
45
+ }
46
+ if (event.key === 'Tab' && !escaped) {
47
+ event.preventDefault();
48
+ const { selectionEnd, selectionStart, value } = control;
49
+ if (event.shiftKey || value.slice(selectionStart, selectionEnd).includes('\n')) {
50
+ this.shift(control, event.shiftKey);
51
+ }
52
+ else {
53
+ typeText(control, INDENT);
54
+ }
55
+ return;
56
+ }
57
+ if (event.key === 'Enter' && !event.shiftKey) {
58
+ const { selectionStart, value } = control;
59
+ const indent = /^[ \t]*/.exec(value.slice(lineStart(value, selectionStart), selectionStart));
60
+ if (indent?.[0]) {
61
+ event.preventDefault();
62
+ typeText(control, `\n${indent[0]}`);
63
+ }
64
+ }
65
+ };
66
+ }
67
+ static { this.markRules = [SCROLLER_RULE, LAYERS_RULE, SPOKEN_RULE]; }
68
+ static { this.words = 'helix-code-input'; }
69
+ highlighterChanged() {
70
+ this.requestUpdate();
71
+ }
72
+ grow() { }
73
+ firstUpdated(changed) {
74
+ super.firstUpdated(changed);
75
+ const control = this.control;
76
+ control?.setAttribute('autocapitalize', 'off');
77
+ control?.setAttribute('autocorrect', 'off');
78
+ }
79
+ updated(changed) {
80
+ super.updated(changed);
81
+ const scroller = this.partNamed('scroller');
82
+ if (scroller && changed.has('maxRows')) {
83
+ scroller.style.maxBlockSize = this.maxRows ? `${this.maxRows}lh` : '';
84
+ }
85
+ }
86
+ markup() {
87
+ const highlight = this.highlight ?? registeredHighlighter();
88
+ try {
89
+ const markup = highlight && this.value !== '' ? highlight(this.value, this.language) : undefined;
90
+ if (markup !== undefined) {
91
+ return highlightedLines(markup);
92
+ }
93
+ }
94
+ catch (error) {
95
+ globalThis.reportError?.(error);
96
+ }
97
+ return escapeText(this.value);
98
+ }
99
+ shift(control, outdent) {
100
+ const text = control.value;
101
+ const start = lineStart(text, control.selectionStart);
102
+ const end = lineEnd(text, Math.max(control.selectionStart, control.selectionEnd - 1));
103
+ const lines = text.slice(start, end).split('\n');
104
+ const next = lines.map((line) => outdent ? line.replace(/^(?: {1,2}|\t)/, '') : `${INDENT}${line}`);
105
+ const block = next.join('\n');
106
+ if (block === text.slice(start, end)) {
107
+ return;
108
+ }
109
+ control.setSelectionRange(start, end);
110
+ typeText(control, block);
111
+ control.setSelectionRange(start, start + block.length);
112
+ }
113
+ renderControl() {
114
+ const tail = this.value === '' || this.value.endsWith('\n') ? ' ' : '';
115
+ return html `<div part="scroller" data-scroller>
116
+ <div
117
+ part="layers"
118
+ data-layers
119
+ ?data-wrap=${this.wrap !== 'off'}
120
+ spellcheck="false"
121
+ translate="no"
122
+ @keydown=${this.handleKeydown}
123
+ >
124
+ <pre part="highlight" aria-hidden="true">${keyedLines(this.markup())}${tail}</pre>
125
+ ${super.renderControl()}
126
+ </div>
127
+ </div>`;
128
+ }
129
+ };
130
+ __decorate([
131
+ property({ attribute: false })
132
+ ], HelixHeadlessCodeInput.prototype, "highlight", void 0);
133
+ __decorate([
134
+ property()
135
+ ], HelixHeadlessCodeInput.prototype, "language", void 0);
136
+ __decorate([
137
+ property()
138
+ ], HelixHeadlessCodeInput.prototype, "wrap", void 0);
139
+ HelixHeadlessCodeInput = __decorate([
140
+ customElement('helix-headless-code-input')
141
+ ], HelixHeadlessCodeInput);
142
+ export { HelixHeadlessCodeInput };
@@ -1,4 +1,4 @@
1
- import { AriaForwardController, HelixFormElement, PopoverController, UpgradeController } from '@fusion.dev/helix-core';
1
+ import { AriaForwardController, HelixFormElement, ListedController, PopoverController, UpgradeController } from '@fusion.dev/helix-core';
2
2
  import { type PropertyValues, type TemplateResult } from 'lit';
3
3
  import '../../listbox/index.js';
4
4
  import '../../tag/root/index.js';
@@ -120,6 +120,9 @@ declare const HelixHeadlessCombobox_base: (new (...args: any[]) => import("@fusi
120
120
  * @csspart listbox-loading - Shown while the options are loading.
121
121
  * @csspart listbox-search - The region holding the field that narrows the list.
122
122
  * @csspart listbox-search-field - The filter field inside that region.
123
+ * @csspart listbox-voice - The dictation button in the filter field.
124
+ * @csspart listbox-voice-listening - The dictation button while it listens.
125
+ * @csspart listbox-voice-status - What a screen reader hears as dictation starts and stops.
123
126
  * @csspart live - The region VoiceOver hears the highlight, the count and a choice from on Apple devices, drawn once there is one to say. Visually hidden in the styled tier.
124
127
  * @csspart rest - The count of choices held beyond `max-tags`. A `<button>` with `expandable`, a `<span>` without it.
125
128
  * @csspart surface - The floating panel.
@@ -140,6 +143,8 @@ declare const HelixHeadlessCombobox_base: (new (...args: any[]) => import("@fusi
140
143
  * @cssstate open - While the list is open.
141
144
  */
142
145
  export declare class HelixHeadlessCombobox extends HelixHeadlessCombobox_base {
146
+ /** On the page's list of controls, where a command palette and a shortcut overlay find it. */
147
+ protected readonly listed: ListedController;
143
148
  /**
144
149
  * The key this family's words are registered under, its styled tag:
145
150
  * `registerWords('de', { 'helix-combobox': { emptyText: 'Keine Treffer' } })`.
@@ -1,5 +1,5 @@
1
1
  import { __decorate } from "tslib";
2
- import { AriaForwardController, afterLinkFollowed, auditChildren, chosenValues, countLabel, customElement, DelayController, drawAutofill, drawStatus, formatWords, HelixCreateEvent, HelixFilterEvent, HelixFormElement, handedLocale, MixinControllerOpen, MixinControllerValue, MixinField, MixinTokens, onApple, PopoverController, reportValues, StatusController, submitImplicitly, timeConverter, UpgradeController, words, wordsLocale, } from '@fusion.dev/helix-core';
2
+ import { AriaForwardController, afterLinkFollowed, auditChildren, chosenValues, countLabel, customElement, DelayController, drawAutofill, drawStatus, formatWords, HelixCreateEvent, HelixFilterEvent, HelixFormElement, handedLocale, ListedController, MixinControllerOpen, MixinControllerValue, MixinField, MixinTokens, onApple, PopoverController, reportValues, StatusController, submitImplicitly, timeConverter, UpgradeController, words, wordsLocale, } from '@fusion.dev/helix-core';
3
3
  import { caretDown } from '@fusion.dev/helix-icons';
4
4
  import { nothing } from 'lit';
5
5
  import { property, state } from 'lit/decorators.js';
@@ -13,6 +13,7 @@ const isInsertion = (event) => !event.inputType?.startsWith('delete');
13
13
  let HelixHeadlessCombobox = class HelixHeadlessCombobox extends MixinControllerOpen(MixinTokens(MixinControllerValue(MixinField(HelixFormElement)))) {
14
14
  constructor() {
15
15
  super(...arguments);
16
+ this.listed = new ListedController(this, 'focus', () => this.internals?.ariaKeyShortcuts);
16
17
  this.aria = new AriaForwardController(this, {
17
18
  target: () => this.partNamed('control'),
18
19
  });
@@ -586,7 +587,8 @@ let HelixHeadlessCombobox = class HelixHeadlessCombobox extends MixinControllerO
586
587
  exportparts="empty: listbox-empty, error: listbox-error, feedback: listbox-feedback,
587
588
  filter-icon: listbox-filter-icon, filter-mark: listbox-filter-mark, hint: listbox-hint,
588
589
  items: listbox-items, label: listbox-label, live: listbox-live, loading: listbox-loading,
589
- search: listbox-search, search-field: listbox-search-field"
590
+ search: listbox-search, search-field: listbox-search-field,
591
+ voice: listbox-voice, voice-listening: listbox-voice-listening, voice-status: listbox-voice-status"
590
592
  aria-label=${this.listLabel}
591
593
  empty-text=${this.emptyText}
592
594
  load-error=${this.loadError}
@@ -1,7 +1,8 @@
1
1
  import { type DataOptionItem, HelixElement } from '@fusion.dev/helix-core';
2
- import { type PropertyValues, type TemplateResult } from 'lit';
2
+ import { nothing, type PropertyValues, type TemplateResult } from 'lit';
3
3
  import '../../dialog/index.js';
4
4
  import '../../listbox/index.js';
5
+ import '../../option/index.js';
5
6
  declare const HelixHeadlessCommand_base: (new (...args: any[]) => import("@fusion.dev/helix-core").MixinControllerOpenInterface) & typeof HelixElement;
6
7
  /**
7
8
  * @summary A command palette: a list of commands that opens from anywhere on the page and filters as you type.
@@ -72,17 +73,17 @@ declare const HelixHeadlessCommand_base: (new (...args: any[]) => import("@fusio
72
73
  * Aria's `Autocomplete` is. The commands are a `listbox` of `option`s. How many commands typing left, the empty text and
73
74
  * the loading text are said in a polite live region.
74
75
  *
75
- * **The commands are an import of their own.** A command is an option, the
76
- * Option family's, which the palette does not register, so a page that writes
77
- * one imports it beside the palette:
76
+ * **The palette registers Option**, since it draws the controls on screen as
77
+ * options itself with `controls`, so a page that writes commands imports the
78
+ * palette alone. A tier of your own registers its own option, and in
79
+ * development a palette holding one nobody registered says so.
78
80
  *
79
- * ```js
80
- * import '@fusion.dev/helix/command'
81
- * import '@fusion.dev/helix/option'
82
- * ```
83
- *
84
- * In development, a palette holding an option nobody registered says so once
85
- * the page has loaded, with the line to add.
81
+ * **Set `controls` to find anything on screen.** Every labeled Helix control
82
+ * puts itself on the page's list while it is connected, and the palette lists
83
+ * the ones a reader can reach under its own commands once they type, with
84
+ * each one's shortcut. Choosing one closes the palette and presses it, or
85
+ * moves focus into a field. `unlisted` on a control, or on a region around it,
86
+ * leaves it out.
86
87
  *
87
88
  * @since 0.1.0
88
89
  * @status stable
@@ -106,6 +107,16 @@ declare const HelixHeadlessCommand_base: (new (...args: any[]) => import("@fusio
106
107
  * @event {HelixSelectEvent} helix-select - A command was chosen, with its `href` where the option is a link. Cancelable: cancel it to keep the palette open, and to keep the browser from following the link.
107
108
  * @event {ToggleEvent} toggle - The palette opened or closed. The event carries `newState` and `oldState`.
108
109
  *
110
+ * @csspart control - A control on screen, listed with `controls`.
111
+ * @csspart control-description - Its region's name.
112
+ * @csspart control-item - Its row.
113
+ * @csspart control-item-active - Its row while the keyboard is on it.
114
+ * @csspart control-item-selected - Its row while selected.
115
+ * @csspart control-label - Its name.
116
+ * @csspart control-text - Its name and region, as one column.
117
+ * @csspart controls - The group of controls on screen.
118
+ * @csspart controls-items - What holds the group's controls.
119
+ * @csspart controls-label - The group's heading.
109
120
  * @csspart dialog-body - The dialog's slot for its content, which holds the list. It draws no box until you give it one.
110
121
  * @csspart dialog-dismiss - The dialog's close control, drawn when the dialog is `dismissible`.
111
122
  * @csspart dialog-dismiss-mark - The glyph on the dialog's close control, where none is slotted.
@@ -126,6 +137,9 @@ declare const HelixHeadlessCommand_base: (new (...args: any[]) => import("@fusio
126
137
  * @csspart listbox-loading - The text shown while the commands are loading.
127
138
  * @csspart listbox-search - The region that holds the filter field.
128
139
  * @csspart listbox-search-field - The filter field.
140
+ * @csspart listbox-voice - The dictation button in the filter field, under `voice`.
141
+ * @csspart listbox-voice-listening - The dictation button while it listens.
142
+ * @csspart listbox-voice-status - What a screen reader hears as dictation starts and stops.
129
143
  * @csspart live - The region a screen reader announces the page shown from, by the words of the command that opens it, drawn once there is one to say. Visually hidden in the styled tier.
130
144
  *
131
145
  * @cssstate empty - The list shows no commands: there are none, or typing hid every one.
@@ -140,6 +154,19 @@ export declare class HelixHeadlessCommand extends HelixHeadlessCommand_base {
140
154
  * `registerWords('de', { 'helix-command': { placeholder: 'Befehle suchen' } })`.
141
155
  */
142
156
  static words: string;
157
+ /**
158
+ * Lists the controls on screen below the palette's own commands, once the
159
+ * reader types: every labeled Helix button, field, tab, switch and the
160
+ * rest, by its name, with its shortcut. Choosing one closes the palette and
161
+ * presses the control, or moves focus into a field.
162
+ *
163
+ * A control that is disabled, inert, hidden or under `unlisted` is left out,
164
+ * and so is anything inside the palette. Off by default, so a palette used
165
+ * as a search box lists only what it holds.
166
+ */
167
+ controls: boolean;
168
+ /** The heading over the controls on screen. */
169
+ controlsLabel: string;
143
170
  /**
144
171
  * The text shown when the filter matches no commands.
145
172
  */
@@ -178,6 +205,11 @@ export declare class HelixHeadlessCommand extends HelixHeadlessCommand_base {
178
205
  * with: one key, said once.
179
206
  */
180
207
  hotkey: string;
208
+ /**
209
+ * What the hotkey does, as a list of the page's shortcuts says it, with
210
+ * `{label}` for the palette's `label`.
211
+ */
212
+ hotkeyLabel: string;
181
213
  /**
182
214
  * Draws the palette in the page, where it stands, with no dialog: a search
183
215
  * over a list of commands, as React Aria's Autocomplete is drawn.
@@ -263,6 +295,11 @@ export declare class HelixHeadlessCommand extends HelixHeadlessCommand_base {
263
295
  * {# poleceń} other {# polecenia}}`.
264
296
  */
265
297
  resultsLabel: string;
298
+ /**
299
+ * Draws a dictation button in the search field, which listens through the
300
+ * page's speech engine and types what it hears, as a field's `voice` does.
301
+ */
302
+ voice: boolean;
266
303
  /**
267
304
  * Whether the consumer put anything under the list.
268
305
  *
@@ -359,6 +396,12 @@ export declare class HelixHeadlessCommand extends HelixHeadlessCommand_base {
359
396
  private readonly handleAlternate;
360
397
  private readonly swallowOptionSelect;
361
398
  connectedCallback(): void;
399
+ /** Takes the hotkey off the page's list of shortcuts. */
400
+ private unregister;
401
+ /** The controls on screen, read as the palette opens. */
402
+ private onScreen;
403
+ /** What the reader has typed, since the controls are listed only once they type. */
404
+ private typed;
362
405
  disconnectedCallback(): void;
363
406
  protected willUpdate(changed: PropertyValues): void;
364
407
  protected firstUpdated(changed: PropertyValues): void;
@@ -388,6 +431,13 @@ export declare class HelixHeadlessCommand extends HelixHeadlessCommand_base {
388
431
  * from the same template.
389
432
  */
390
433
  protected renderDialog(): TemplateResult;
434
+ /** Reads the controls a reader can reach now, leaving out the palette and anything in it. */
435
+ private readControls;
436
+ /**
437
+ * The controls on screen as a group of options, once the reader types, on
438
+ * the first page and only where the commands are children.
439
+ */
440
+ protected renderControls(): TemplateResult | typeof nothing;
391
441
  /**
392
442
  * The filterable list of commands, which the dialog holds, or the page with
393
443
  * `inline`, behind a seam the styled tier can move.
@@ -1,11 +1,12 @@
1
1
  import { __decorate } from "tslib";
2
- import { auditChildren, customElement, drawStatus, HelixElement, HelixSelectEvent, MixinControllerOpen, matchesChord, StatusController, timeConverter, words, } from '@fusion.dev/helix-core';
2
+ import { auditChildren, chordParts, customElement, drawStatus, formatWords, HelixElement, HelixSelectEvent, keyNames, listControls, MixinControllerOpen, matchesChord, onApple, registerShortcut, runControl, StatusController, timeConverter, words, } from '@fusion.dev/helix-core';
3
3
  import { nothing } from 'lit';
4
4
  import { property, state } from 'lit/decorators.js';
5
5
  import { html } from 'lit/static-html.js';
6
6
  import { optionsAmong } from '../../option/among.js';
7
7
  import '../../dialog/index.js';
8
8
  import '../../listbox/index.js';
9
+ import '../../option/index.js';
9
10
  const says = (element, state) => {
10
11
  try {
11
12
  return element?.matches(`:state(${state})`) ?? false;
@@ -14,12 +15,16 @@ const says = (element, state) => {
14
15
  return false;
15
16
  }
16
17
  };
18
+ const CONTROL = 'helix-control:';
17
19
  let HelixHeadlessCommand = class HelixHeadlessCommand extends MixinControllerOpen(HelixElement) {
18
20
  constructor() {
19
21
  super(...arguments);
22
+ this.controls = false;
23
+ this.controlsLabel = 'On this screen';
20
24
  this.emptyText = 'No commands';
21
25
  this.filterDelay = 0;
22
26
  this.hotkey = 'mod+k';
27
+ this.hotkeyLabel = 'Open {label}';
23
28
  this.inline = false;
24
29
  this.items = undefined;
25
30
  this.label = 'Commands';
@@ -30,6 +35,7 @@ let HelixHeadlessCommand = class HelixHeadlessCommand extends MixinControllerOpe
30
35
  this.trail = [];
31
36
  this.placeholder = 'Search commands';
32
37
  this.resultsLabel = '{count, plural, one {# command} other {# commands}}';
38
+ this.voice = false;
33
39
  this.footed = false;
34
40
  this.handleHotkey = (event) => {
35
41
  if (!matchesChord(event, this.hotkey, true) && !matchesChord(event, this.hotkey, false)) {
@@ -46,6 +52,14 @@ let HelixHeadlessCommand = class HelixHeadlessCommand extends MixinControllerOpe
46
52
  event.stopPropagation();
47
53
  event.preventDefault();
48
54
  const { value } = event.detail;
55
+ const control = value.startsWith(CONTROL)
56
+ ? this.onScreen[Number(value.slice(CONTROL.length))]
57
+ : undefined;
58
+ if (control) {
59
+ this.requestOpen(false);
60
+ void this.updateComplete.then(() => runControl(control));
61
+ return;
62
+ }
49
63
  if (!this.dispatchEvent(new HelixSelectEvent({ value }))) {
50
64
  return;
51
65
  }
@@ -103,7 +117,14 @@ let HelixHeadlessCommand = class HelixHeadlessCommand extends MixinControllerOpe
103
117
  }
104
118
  this.requestOpen(false);
105
119
  };
106
- this.handleFilter = () => {
120
+ this.onScreen = [];
121
+ this.typed = '';
122
+ this.handleFilter = (event) => {
123
+ const typed = event.detail?.value ?? '';
124
+ if (this.typed.trim() === '' && typed.trim() !== '') {
125
+ this.readControls();
126
+ }
127
+ this.typed = typed;
107
128
  void Promise.resolve()
108
129
  .then(() => this.listbox?.updateComplete)
109
130
  .then(() => this.markStatus());
@@ -148,9 +169,15 @@ let HelixHeadlessCommand = class HelixHeadlessCommand extends MixinControllerOpe
148
169
  this.addEventListener('keydown', this.handleBack);
149
170
  this.addEventListener('click', this.handleAlternate, { capture: true });
150
171
  document.addEventListener('keydown', this.handleHotkey);
172
+ this.unregister = registerShortcut({
173
+ keys: () => (this.inline ? '' : this.hotkey),
174
+ label: () => formatWords(this, 'hotkeyLabel', { label: this.label }),
175
+ owner: this,
176
+ });
151
177
  }
152
178
  disconnectedCallback() {
153
179
  super.disconnectedCallback();
180
+ this.unregister?.();
154
181
  this.removeEventListener('helix-select', this.swallowOptionSelect);
155
182
  this.removeEventListener('keydown', this.handleAlternate, { capture: true });
156
183
  this.removeEventListener('keydown', this.handleBack);
@@ -196,6 +223,9 @@ let HelixHeadlessCommand = class HelixHeadlessCommand extends MixinControllerOpe
196
223
  return;
197
224
  }
198
225
  if (this.open) {
226
+ this.onScreen = this.controls
227
+ ? listControls().filter((control) => control.element !== this && !this.contains(control.element))
228
+ : [];
199
229
  this.listbox?.highlight('none');
200
230
  this.searchField?.focus();
201
231
  }
@@ -226,13 +256,46 @@ let HelixHeadlessCommand = class HelixHeadlessCommand extends MixinControllerOpe
226
256
  ${this.renderFooter('footer')}
227
257
  </${this.tag('dialog')}>`;
228
258
  }
259
+ readControls() {
260
+ this.onScreen = this.controls
261
+ ? listControls().filter((control) => control.element !== this && !this.contains(control.element))
262
+ : [];
263
+ }
264
+ renderControls() {
265
+ if (!this.controls ||
266
+ this.typed.trim() === '' ||
267
+ this.page !== '' ||
268
+ this.items !== undefined) {
269
+ return nothing;
270
+ }
271
+ const apple = onApple();
272
+ const group = this.tag('option-group');
273
+ const option = this.tag('option');
274
+ return html `<${group}
275
+ part="controls"
276
+ exportparts="items: controls-items, label: controls-label"
277
+ label=${this.controlsLabel}
278
+ >${this.onScreen.map((control, index) => {
279
+ const [keys] = control.keys;
280
+ const names = keys ? keyNames(chordParts(keys), apple) : undefined;
281
+ return html `<${option}
282
+ part="control"
283
+ exportparts="description: control-description, item: control-item, item-active: control-item-active,
284
+ item-selected: control-item-selected, label: control-label, text: control-text"
285
+ value=${`${CONTROL}${index}`}
286
+ text-value=${control.name}
287
+ description=${control.group || nothing}
288
+ >${control.name}${names ? html `<span slot="end" aria-hidden="true">${names.keys.join(names.separator)}</span>` : nothing}</${option}>`;
289
+ })}</${group}>`;
290
+ }
229
291
  renderListbox() {
230
292
  return html `<${this.tag('listbox')}
231
293
  part="listbox"
232
294
  exportparts="empty: listbox-empty, error: listbox-error, feedback: listbox-feedback,
233
295
  filter-icon: listbox-filter-icon, filter-mark: listbox-filter-mark, hint: listbox-hint,
234
296
  items: listbox-items, label: listbox-label, live: listbox-live, loading: listbox-loading,
235
- search: listbox-search, search-field: listbox-search-field"
297
+ search: listbox-search, search-field: listbox-search-field,
298
+ voice: listbox-voice, voice-listening: listbox-voice-listening, voice-status: listbox-voice-status"
236
299
  aria-label=${this.listName}
237
300
  empty-text=${this.emptyText}
238
301
  filter-delay=${this.filterDelay}
@@ -247,6 +310,7 @@ let HelixHeadlessCommand = class HelixHeadlessCommand extends MixinControllerOpe
247
310
  .items=${this.items}
248
311
  .fieldHomeEnd=${this.homeEnd === 'list'}
249
312
  layout=${this.layout ?? nothing}
313
+ ?voice=${this.voice}
250
314
  filterable
251
315
  @helix-before-change=${this.handleChange}
252
316
  @helix-filter=${this.handleFilter}
@@ -254,6 +318,7 @@ let HelixHeadlessCommand = class HelixHeadlessCommand extends MixinControllerOpe
254
318
  @input=${this.stopInner}
255
319
  >
256
320
  ${this.page === '' ? html `<slot></slot>` : html `<slot name=${`page:${this.page}`}></slot>`}
321
+ ${this.renderControls()}
257
322
  <!-- Handed on with the text as the fallback, for the reason Combobox
258
323
  gives: the list's own fallback never shows behind a passed slot. -->
259
324
  <slot name="empty" slot="empty">${this.emptyText}</slot>
@@ -290,6 +355,13 @@ let HelixHeadlessCommand = class HelixHeadlessCommand extends MixinControllerOpe
290
355
  return html `${this.renderDialog()}`;
291
356
  }
292
357
  };
358
+ __decorate([
359
+ property({ type: Boolean })
360
+ ], HelixHeadlessCommand.prototype, "controls", void 0);
361
+ __decorate([
362
+ words,
363
+ property({ attribute: 'controls-label' })
364
+ ], HelixHeadlessCommand.prototype, "controlsLabel", void 0);
293
365
  __decorate([
294
366
  words,
295
367
  property({ attribute: 'empty-text' })
@@ -306,6 +378,10 @@ __decorate([
306
378
  __decorate([
307
379
  property()
308
380
  ], HelixHeadlessCommand.prototype, "hotkey", void 0);
381
+ __decorate([
382
+ words,
383
+ property({ attribute: 'hotkey-label' })
384
+ ], HelixHeadlessCommand.prototype, "hotkeyLabel", void 0);
309
385
  __decorate([
310
386
  property({ reflect: true, type: Boolean })
311
387
  ], HelixHeadlessCommand.prototype, "inline", void 0);
@@ -346,9 +422,18 @@ __decorate([
346
422
  words,
347
423
  property({ attribute: 'results-label' })
348
424
  ], HelixHeadlessCommand.prototype, "resultsLabel", void 0);
425
+ __decorate([
426
+ property({ type: Boolean })
427
+ ], HelixHeadlessCommand.prototype, "voice", void 0);
349
428
  __decorate([
350
429
  state()
351
430
  ], HelixHeadlessCommand.prototype, "footed", void 0);
431
+ __decorate([
432
+ state()
433
+ ], HelixHeadlessCommand.prototype, "onScreen", void 0);
434
+ __decorate([
435
+ state()
436
+ ], HelixHeadlessCommand.prototype, "typed", void 0);
352
437
  HelixHeadlessCommand = __decorate([
353
438
  customElement('helix-headless-command')
354
439
  ], HelixHeadlessCommand);
@@ -78,6 +78,7 @@ declare const HelixHeadlessDonutChart_base: typeof HelixElement & (new (...args:
78
78
  * @csspart segment-active - The slice under the pointer. Set alongside `segment`.
79
79
  * @csspart segment-edge - One slice as a closed shape, used to draw its outline.
80
80
  * @csspart slice-label - One slice's share, written on the slice under `slice-labels`. Placed with `left` and `top`, or `inset-inline-start` and `top` on a half ring, and carries its series color as `--chart-series`.
81
+ * @csspart sonify - The button that plays the chart as sound, under `sonify`.
81
82
  * @csspart summary - The chart's name and each slice's share, written as a sentence for screen readers. Hide it visually.
82
83
  * @csspart table - The readings as a table, for a screen reader, rendered only where they were drawn from `data`, a page of them at a time past a few hundred. Hide it visually.
83
84
  * @csspart table-pages - Which readings a page of the `table` holds, and the buttons that show the previous and the next page, rendered only while it holds a page. Hide it visually.