@fusion.dev/helix-headless 0.162.1 → 0.164.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 (45) hide show
  1. package/custom-elements.json +1041 -47
  2. package/dist/callout/root/callout-root.d.ts +43 -4
  3. package/dist/callout/root/callout-root.js +58 -2
  4. package/dist/canvas/index.d.ts +1 -1
  5. package/dist/canvas/root/canvas-document.d.ts +33 -0
  6. package/dist/canvas/root/canvas-root.d.ts +50 -2
  7. package/dist/canvas/root/canvas-root.js +98 -3
  8. package/dist/chat/composer/chat-composer.js +1 -1
  9. package/dist/chat/root/chat-root.d.ts +15 -4
  10. package/dist/chat/root/chat-root.js +19 -8
  11. package/dist/code-block/patch.d.ts +42 -0
  12. package/dist/code-block/patch.js +118 -0
  13. package/dist/code-block/root/code-block-root.d.ts +37 -1
  14. package/dist/code-block/root/code-block-root.js +57 -9
  15. package/dist/color-picker/root/color-picker-root.d.ts +7 -3
  16. package/dist/color-picker/root/color-picker-root.js +7 -4
  17. package/dist/combobox/root/combobox-root.d.ts +1 -0
  18. package/dist/combobox/root/combobox-root.js +10 -1
  19. package/dist/data-table/root/data-table-root.d.ts +1 -0
  20. package/dist/data-table/root/data-table-root.js +1 -0
  21. package/dist/element-events.generated.js +7 -1
  22. package/dist/map/root/map-root.d.ts +8 -1
  23. package/dist/map/root/map-root.js +12 -0
  24. package/dist/menu/item/menu-item.d.ts +38 -7
  25. package/dist/menu/item/menu-item.js +20 -9
  26. package/dist/menu/root/menu-root.d.ts +10 -1
  27. package/dist/menu/root/menu-root.js +8 -3
  28. package/dist/object-inspector/root/object-inspector-root.d.ts +23 -0
  29. package/dist/object-inspector/root/object-inspector-root.js +19 -1
  30. package/dist/steps/root/steps-root.d.ts +7 -1
  31. package/dist/steps/root/steps-root.js +6 -1
  32. package/dist/table/grid-controller.d.ts +7 -0
  33. package/dist/table/grid-controller.js +15 -1
  34. package/dist/table/root/table-root.d.ts +10 -0
  35. package/dist/table/root/table-root.js +18 -1
  36. package/dist/video/root/video-root.js +2 -1
  37. package/dist/video-playlist/item/video-playlist-item.d.ts +8 -0
  38. package/dist/video-playlist/item/video-playlist-item.js +7 -0
  39. package/dist/video-playlist/root/video-playlist-root.d.ts +60 -0
  40. package/dist/video-playlist/root/video-playlist-root.js +55 -1
  41. package/lint.json +1 -1
  42. package/package.json +5 -5
  43. package/types/jsx.d.ts +502 -40
  44. package/types/vue.d.ts +126 -14
  45. package/types/words.d.ts +58 -0
@@ -63,8 +63,79 @@ export const parsePatch = (patch) => {
63
63
  }
64
64
  }
65
65
  }
66
+ for (const one of files) {
67
+ for (const { lines } of one.hunks) {
68
+ markWords(lines);
69
+ }
70
+ }
66
71
  return files;
67
72
  };
73
+ const TOKEN = /[\p{L}\p{N}_]+|\s+|[^\p{L}\p{N}_\s]/gu;
74
+ const MOST_TOKENS = 200;
75
+ const joined = (pieces) => {
76
+ const out = [];
77
+ for (const piece of pieces) {
78
+ const last = out.at(-1);
79
+ if (last && last.changed === piece.changed) {
80
+ last.text += piece.text;
81
+ }
82
+ else {
83
+ out.push({ ...piece });
84
+ }
85
+ }
86
+ return out;
87
+ };
88
+ export const changedWords = (before, after) => {
89
+ const a = before.match(TOKEN) ?? [];
90
+ const b = after.match(TOKEN) ?? [];
91
+ if (a.length === 0 || b.length === 0 || a.length > MOST_TOKENS || b.length > MOST_TOKENS) {
92
+ return undefined;
93
+ }
94
+ const width = b.length + 1;
95
+ const table = new Uint16Array((a.length + 1) * width);
96
+ for (let i = a.length - 1; i >= 0; i -= 1) {
97
+ for (let j = b.length - 1; j >= 0; j -= 1) {
98
+ table[i * width + j] =
99
+ a[i] === b[j]
100
+ ? table[(i + 1) * width + j + 1] + 1
101
+ : Math.max(table[(i + 1) * width + j], table[i * width + j + 1]);
102
+ }
103
+ }
104
+ const left = [];
105
+ const right = [];
106
+ let shared = false;
107
+ let i = 0;
108
+ let j = 0;
109
+ while (i < a.length || j < b.length) {
110
+ if (i < a.length && j < b.length && a[i] === b[j]) {
111
+ shared ||= a[i].trim() !== '';
112
+ left.push({ changed: false, text: a[i] });
113
+ right.push({ changed: false, text: b[j] });
114
+ i += 1;
115
+ j += 1;
116
+ }
117
+ else if (j >= b.length ||
118
+ (i < a.length &&
119
+ table[(i + 1) * width + j] >= table[i * width + j + 1])) {
120
+ left.push({ changed: true, text: a[i] });
121
+ i += 1;
122
+ }
123
+ else {
124
+ right.push({ changed: true, text: b[j] });
125
+ j += 1;
126
+ }
127
+ }
128
+ if (!shared) {
129
+ return undefined;
130
+ }
131
+ const bridged = (pieces) => joined(pieces.map((piece, at) => !piece.changed &&
132
+ piece.text.trim() === '' &&
133
+ pieces[at - 1]?.changed &&
134
+ pieces[at + 1]?.changed
135
+ ? { ...piece, changed: true }
136
+ : piece));
137
+ return [bridged(left), bridged(right)];
138
+ };
68
139
  export const pairLines = (lines) => {
69
140
  const rows = [];
70
141
  let at = 0;
@@ -91,3 +162,50 @@ export const pairLines = (lines) => {
91
162
  }
92
163
  return rows;
93
164
  };
165
+ const markWords = (lines) => {
166
+ for (const { left, right } of pairLines(lines)) {
167
+ if (left?.kind !== 'removed' || right?.kind !== 'added') {
168
+ continue;
169
+ }
170
+ const pieces = changedWords(left.text, right.text);
171
+ if (pieces) {
172
+ ;
173
+ [left.pieces, right.pieces] = pieces;
174
+ }
175
+ }
176
+ };
177
+ export const foldContext = (lines, keep) => {
178
+ const runs = [];
179
+ const add = (start, end, folded) => {
180
+ if (end <= start) {
181
+ return;
182
+ }
183
+ const last = runs.at(-1);
184
+ if (last && !last.folded && !folded) {
185
+ last.lines.push(...lines.slice(start, end));
186
+ }
187
+ else {
188
+ runs.push({ folded, lines: lines.slice(start, end), start });
189
+ }
190
+ };
191
+ let at = 0;
192
+ while (at < lines.length) {
193
+ const context = lines[at]?.kind === 'context';
194
+ let end = at;
195
+ while (end < lines.length && (lines[end]?.kind === 'context') === context) {
196
+ end += 1;
197
+ }
198
+ const head = context && at > 0 ? keep : 0;
199
+ const tail = context && end < lines.length ? keep : 0;
200
+ if (context && end - at - head - tail >= 2) {
201
+ add(at, at + head, false);
202
+ add(at + head, end - tail, true);
203
+ add(end - tail, end, false);
204
+ }
205
+ else {
206
+ add(at, end, false);
207
+ }
208
+ at = end;
209
+ }
210
+ return runs;
211
+ };
@@ -45,9 +45,14 @@ import '../../copy-button/index.js';
45
45
  * removed. The block never computes a diff itself. Line numbers come from the
46
46
  * hunk headers and are hidden from the accessibility tree. Changed lines are
47
47
  * `<ins>` and `<del>` with the `+` and `-` kept as text, so the change does not
48
- * depend on color. Each file in the patch is its own block under its own name,
48
+ * depend on color. Where a removed line is paired with the added line that
49
+ * replaced it, the words each does not share with the other are the `changed`
50
+ * part, so the eye goes to what changed within the line. Each file in the patch is its own block under its own name,
49
51
  * and its copy button copies the new version of the file. `variant="split"`
50
52
  * shows the old version beside the new, each removal opposite its replacement.
53
+ * `context-lines` folds each long run of unchanged lines, keeping that many
54
+ * beside each change, behind a button that shows them and hides them again,
55
+ * as a code host draws a diff written with more context than a reader needs.
51
56
  *
52
57
  * **A block that scrolls is a tab stop.** While its lines run past its width or
53
58
  * its cap, the scroller is a region named for the block, such as "Code,
@@ -83,6 +88,7 @@ import '../../copy-button/index.js';
83
88
  * @csspart actions - The box in the header that holds the `actions` slot, drawn while something is slotted there.
84
89
  * @csspart added - In a diff, a line only the new version has, an `<ins>`.
85
90
  * @csspart block - The scrolling `<pre>`.
91
+ * @csspart changed - In a diff, the words of a changed line that the line opposite it does not have, inside its `text`.
86
92
  * @csspart code - The `<code>` inside it.
87
93
  * @csspart context - In a diff, a line both versions share.
88
94
  * @csspart copy - The copy button. It sits in the header when there is one, and beside the code otherwise.
@@ -104,6 +110,7 @@ import '../../copy-button/index.js';
104
110
  * @csspart expand - The button under a capped block that shows every line, and fewer again. Drawn when `expandable` is set and the cap hides something.
105
111
  * @csspart file - In a diff, one file's header and block.
106
112
  * @csspart filler - In a split diff, the empty row opposite a line the other side does not have.
113
+ * @csspart fold - In a diff with `context-lines`, the button that shows a folded run of unchanged lines, and hides it again, saying which with `aria-expanded`.
107
114
  * @csspart frame - The code, and the copy button beside it where there is no header.
108
115
  * @csspart gutter - The line numbers beside the code, shown when `line-numbers` is set.
109
116
  * @csspart header - The bar that holds the name and the copy button, shown when there is a name.
@@ -148,6 +155,12 @@ export declare class HelixHeadlessCodeBlock extends HelixElement {
148
155
  * The expand button's words while the block shows every line.
149
156
  */
150
157
  collapseLabel: string;
158
+ /**
159
+ * In a diff, how many unchanged lines to keep beside each change. Each
160
+ * longer run of unchanged lines is folded behind a button that shows it.
161
+ * Unset shows every line the diff holds.
162
+ */
163
+ contextLines?: number;
151
164
  /**
152
165
  * The copy button's name and tooltip text after a copy.
153
166
  */
@@ -185,6 +198,12 @@ export declare class HelixHeadlessCodeBlock extends HelixElement {
185
198
  * The expand button's words while the cap hides lines.
186
199
  */
187
200
  expandLabel: string;
201
+ /**
202
+ * The words of a button that hides a run of unchanged lines it showed, in a
203
+ * diff with `context-lines`. `{count}` is how many, and each language
204
+ * writes the forms it has.
205
+ */
206
+ hideUnchangedLabel: string;
188
207
  /**
189
208
  * Turns the code into highlighted markup, which the block draws in place of
190
209
  * the plain text.
@@ -263,6 +282,11 @@ export declare class HelixHeadlessCodeBlock extends HelixElement {
263
282
  * `false`.
264
283
  */
265
284
  noCopy: boolean;
285
+ /**
286
+ * The words of a button that shows a folded run of unchanged lines, in a
287
+ * diff with `context-lines`. `{count}` is how many.
288
+ */
289
+ showUnchangedLabel: string;
266
290
  /**
267
291
  * How a diff's two versions are arranged. It has no effect without `diff`.
268
292
  *
@@ -295,6 +319,18 @@ export declare class HelixHeadlessCodeBlock extends HelixElement {
295
319
  private parsed;
296
320
  /** The files in the content, when it is a diff and it can be read yet. */
297
321
  private get files();
322
+ /** The folds a reader opened, by file, hunk and the line each starts at. */
323
+ private readonly unfolded;
324
+ /** A hunk's lines in runs, folded where `context-lines` asks. */
325
+ private runsOf;
326
+ /**
327
+ * The button over a folded run, and the run once it is open.
328
+ *
329
+ * A disclosure, so focus stays on the button as the lines appear under it.
330
+ * The new side of a split diff draws the same words hidden from the
331
+ * accessibility tree, there to keep the rows level and to answer a press.
332
+ */
333
+ private renderFold;
298
334
  /**
299
335
  * The code as text, read from what the default slot is given rather than
300
336
  * stored.
@@ -12,7 +12,7 @@ import { html } from 'lit/static-html.js';
12
12
  import '../../copy-button/index.js';
13
13
  import { highlightedLines } from '../highlight.js';
14
14
  import { gutterNumbers, lineCount, lineRuns, lineSpans } from '../lines.js';
15
- import { pairLines, parsePatch } from '../patch.js';
15
+ import { foldContext, pairLines, parsePatch, } from '../patch.js';
16
16
  let HelixHeadlessCodeBlock = class HelixHeadlessCodeBlock extends HelixElement {
17
17
  constructor() {
18
18
  super(...arguments);
@@ -25,15 +25,18 @@ let HelixHeadlessCodeBlock = class HelixHeadlessCodeBlock extends HelixElement {
25
25
  this.diff = false;
26
26
  this.expandable = false;
27
27
  this.expandLabel = 'Show all lines';
28
+ this.hideUnchangedLabel = '{count, plural, one {Hide # unchanged line} other {Hide # unchanged lines}}';
28
29
  this.label = '';
29
30
  this.language = '';
30
31
  this.lineNumbers = false;
31
32
  this.markLines = '';
32
33
  this.noCopy = false;
34
+ this.showUnchangedLabel = '{count, plural, one {Show # unchanged line} other {Show # unchanged lines}}';
33
35
  this.wrap = false;
34
36
  this.wrapLabel = 'Wrap lines';
35
37
  this.wrappable = false;
36
38
  this.parsed = { files: [], text: '' };
39
+ this.unfolded = new Set();
37
40
  this.capped = false;
38
41
  this.expanded = false;
39
42
  this.measure = () => {
@@ -69,9 +72,32 @@ let HelixHeadlessCodeBlock = class HelixHeadlessCodeBlock extends HelixElement {
69
72
  const text = this.text;
70
73
  if (text !== this.parsed.text) {
71
74
  this.parsed = { files: parsePatch(text), text };
75
+ this.unfolded.clear();
72
76
  }
73
77
  return this.parsed.files;
74
78
  }
79
+ runsOf(hunk) {
80
+ const keep = this.contextLines;
81
+ return keep === undefined || keep < 0
82
+ ? [{ folded: false, lines: hunk.lines, start: 0 }]
83
+ : foldContext(hunk.lines, Math.floor(keep));
84
+ }
85
+ renderFold(run, key, line, hidden = false) {
86
+ const open = this.unfolded.has(key);
87
+ const words = formatWords(this, open ? 'hideUnchangedLabel' : 'showUnchangedLabel', {
88
+ count: run.lines.length,
89
+ });
90
+ const toggle = () => {
91
+ if (!this.unfolded.delete(key)) {
92
+ this.unfolded.add(key);
93
+ }
94
+ this.requestUpdate();
95
+ };
96
+ const control = hidden
97
+ ? html `<span part="fold" aria-hidden="true" @click=${toggle}>${words}</span>`
98
+ : html `<button part="fold" type="button" aria-expanded=${open ? 'true' : 'false'} @click=${toggle}>${words}</button>`;
99
+ return html `${control}\n${open ? run.lines.map(line) : nothing}`;
100
+ }
75
101
  get text() {
76
102
  if (this.hydrating) {
77
103
  return '';
@@ -319,7 +345,10 @@ let HelixHeadlessCodeBlock = class HelixHeadlessCodeBlock extends HelixElement {
319
345
  const numbers = side
320
346
  ? this.renderNumber(side === 'old' ? line.old : line.new, width)
321
347
  : html `${this.renderNumber(line.old, width)}${this.renderNumber(line.new, width)}`;
322
- const text = html `<span part="text">${line.text}\n</span>`;
348
+ const words = line.pieces
349
+ ? line.pieces.map((piece) => piece.changed ? html `<span part="changed">${piece.text}</span>` : piece.text)
350
+ : line.text;
351
+ const text = html `<span part="text">${words}\n</span>`;
323
352
  if (line.kind === 'added') {
324
353
  return html `<ins part="added">${numbers}<span part="marker">+</span>${text}</ins>`;
325
354
  }
@@ -331,16 +360,22 @@ let HelixHeadlessCodeBlock = class HelixHeadlessCodeBlock extends HelixElement {
331
360
  renderHunk(hunk, hidden = false) {
332
361
  return html `<span part="hunk" aria-hidden=${hidden ? 'true' : nothing}>${hunk.header}\n</span>`;
333
362
  }
334
- renderSplit(file, width) {
335
- const hunks = file.hunks.map((hunk) => ({ hunk, rows: pairLines(hunk.lines) }));
363
+ renderSplit(file, width, at) {
364
+ const hunks = file.hunks.map((hunk) => ({ hunk, runs: this.runsOf(hunk) }));
336
365
  const filler = html `<span part="filler" aria-hidden="true">\n</span>`;
366
+ const side = (old) => hunks.map(({ hunk, runs }, h) => html `${this.renderHunk(hunk, !old)}${runs.map((run) => run.folded
367
+ ? this.renderFold(run, `${at}:${h}:${run.start}`, (line) => this.renderLine(line, width, old ? 'old' : 'new'), !old)
368
+ : pairLines(run.lines).map(({ left, right }) => {
369
+ const line = old ? left : right;
370
+ return line ? this.renderLine(line, width, old ? 'old' : 'new') : filler;
371
+ }))}`);
337
372
  return html `<div part="split">
338
- <pre part="old" translate="no"><code part="code">${hunks.map(({ hunk, rows }) => html `${this.renderHunk(hunk)}${rows.map(({ left }) => left ? this.renderLine(left, width, 'old') : filler)}`)}</code></pre>
339
- <pre part="new" translate="no"><code part="code">${hunks.map(({ hunk, rows }) => html `${this.renderHunk(hunk, true)}${rows.map(({ right }) => right ? this.renderLine(right, width, 'new') : filler)}`)}</code></pre>
373
+ <pre part="old" translate="no"><code part="code">${side(true)}</code></pre>
374
+ <pre part="new" translate="no"><code part="code">${side(false)}</code></pre>
340
375
  </div>`;
341
376
  }
342
377
  renderDiff(files) {
343
- return html `${files.map((file) => {
378
+ return html `${files.map((file, at) => {
344
379
  const name = file.name || this.label;
345
380
  const widest = Math.max(0, ...file.hunks.flatMap((hunk) => hunk.lines.map((line) => Math.max(line.old ?? 0, line.new ?? 0))));
346
381
  const width = String(widest).length;
@@ -352,8 +387,10 @@ let HelixHeadlessCodeBlock = class HelixHeadlessCodeBlock extends HelixElement {
352
387
  ${this.renderHeader(name, now)}
353
388
  <div part="frame">
354
389
  ${this.variant === 'split'
355
- ? this.renderSplit(file, width)
356
- : html `<pre part="block" translate="no"><code part="code">${file.hunks.map((hunk) => html `${this.renderHunk(hunk)}${hunk.lines.map((line) => this.renderLine(line, width))}`)}</code></pre>`}
390
+ ? this.renderSplit(file, width, at)
391
+ : html `<pre part="block" translate="no"><code part="code">${file.hunks.map((hunk, h) => html `${this.renderHunk(hunk)}${this.runsOf(hunk).map((run) => run.folded
392
+ ? this.renderFold(run, `${at}:${h}:${run.start}`, (line) => this.renderLine(line, width))
393
+ : run.lines.map((line) => this.renderLine(line, width)))}`)}</code></pre>`}
357
394
  ${this.headed(name) ? nothing : this.renderCopy(now)}
358
395
  </div>
359
396
  </div>`;
@@ -372,6 +409,9 @@ __decorate([
372
409
  words,
373
410
  property({ attribute: 'collapse-label' })
374
411
  ], HelixHeadlessCodeBlock.prototype, "collapseLabel", void 0);
412
+ __decorate([
413
+ property({ attribute: 'context-lines', type: Number })
414
+ ], HelixHeadlessCodeBlock.prototype, "contextLines", void 0);
375
415
  __decorate([
376
416
  words,
377
417
  property({ attribute: 'copied-label' })
@@ -394,6 +434,10 @@ __decorate([
394
434
  words,
395
435
  property({ attribute: 'expand-label' })
396
436
  ], HelixHeadlessCodeBlock.prototype, "expandLabel", void 0);
437
+ __decorate([
438
+ words,
439
+ property({ attribute: 'hide-unchanged-label' })
440
+ ], HelixHeadlessCodeBlock.prototype, "hideUnchangedLabel", void 0);
397
441
  __decorate([
398
442
  property({ attribute: false })
399
443
  ], HelixHeadlessCodeBlock.prototype, "highlight", void 0);
@@ -412,6 +456,10 @@ __decorate([
412
456
  __decorate([
413
457
  property({ attribute: 'no-copy', reflect: true, type: Boolean })
414
458
  ], HelixHeadlessCodeBlock.prototype, "noCopy", void 0);
459
+ __decorate([
460
+ words,
461
+ property({ attribute: 'show-unchanged-label' })
462
+ ], HelixHeadlessCodeBlock.prototype, "showUnchangedLabel", void 0);
415
463
  __decorate([
416
464
  property({ reflect: true })
417
465
  ], HelixHeadlessCodeBlock.prototype, "variant", void 0);
@@ -22,8 +22,10 @@ declare const HelixHeadlessColorPicker_base: (new (...args: any[]) => import("@f
22
22
  * gray after you drag to the bottom of the square and back.
23
23
  *
24
24
  * The square is one tab stop. In it, the up and down arrows move brightness
25
- * and the left and right arrows move saturation, and PageUp and PageDown move
26
- * brightness by ten. The picker sends `input` as the color moves and `change`
25
+ * and the left and right arrows move saturation, by ten with Shift. PageUp and
26
+ * PageDown move brightness by ten, and Home and End move saturation by ten,
27
+ * turned on a right-to-left page as the arrows are, as React Aria's ColorArea
28
+ * does. The picker sends `input` as the color moves and `change`
27
29
  * when a drag, a key, a typed value or a preset settles it, as
28
30
  * `<input type="color">` does.
29
31
  *
@@ -546,7 +548,9 @@ export declare class HelixHeadlessColorPicker extends HelixHeadlessColorPicker_b
546
548
  * its own one axis: on the saturation input, ArrowUp moved the handle
547
549
  * sideways. Here the vertical keys move brightness and the horizontal keys
548
550
  * move saturation, turned on a right-to-left page as the pointer is, which is
549
- * what React Aria's ColorArea does.
551
+ * what React Aria's ColorArea does. Shift makes an arrow a step of ten, and
552
+ * Home and End are saturation's steps of ten, as PageUp and PageDown are
553
+ * brightness's.
550
554
  */
551
555
  private readonly handleAreaKeydown;
552
556
  /**
@@ -211,11 +211,14 @@ let HelixHeadlessColorPicker = class HelixHeadlessColorPicker extends MixinPicke
211
211
  const color = this.color;
212
212
  const along = isRtl(this) ? -1 : 1;
213
213
  const held = (at) => clamp(at, 0, 100);
214
+ const step = event.shiftKey ? 10 : 1;
214
215
  const moves = {
215
- ArrowDown: { brightness: held(color.brightness - 1) },
216
- ArrowLeft: { saturation: held(color.saturation - along) },
217
- ArrowRight: { saturation: held(color.saturation + along) },
218
- ArrowUp: { brightness: held(color.brightness + 1) },
216
+ ArrowDown: { brightness: held(color.brightness - step) },
217
+ ArrowLeft: { saturation: held(color.saturation - along * step) },
218
+ ArrowRight: { saturation: held(color.saturation + along * step) },
219
+ ArrowUp: { brightness: held(color.brightness + step) },
220
+ End: { saturation: held(color.saturation + along * 10) },
221
+ Home: { saturation: held(color.saturation - along * 10) },
219
222
  PageDown: { brightness: held(color.brightness - 10) },
220
223
  PageUp: { brightness: held(color.brightness + 10) },
221
224
  };
@@ -26,6 +26,7 @@ declare const HelixHeadlessCombobox_base: (new (...args: any[]) => import("@fusi
26
26
  * | Key | What it does |
27
27
  * |---|---|
28
28
  * | Down, Up | On a closed field, opens the list on the chosen option, or the first or the last. In the open list, moves the highlight, round from the last option to the first. |
29
+ * | Alt with Down, Alt with Up | Opens the list without moving the highlight, and closes it, choosing nothing. |
29
30
  * | Page Down, Page Up | In the open list, moves the highlight by as many options as the list shows at once. |
30
31
  * | Enter | Chooses the highlighted option. With nothing left in the list, creates what was typed with `creatable`, or takes it with `allows-custom-value`. On a closed field, submits the form. |
31
32
  * | Escape | Closes the list and puts the chosen words back. On a closed field, a second Escape clears a single choice. |
@@ -154,7 +154,11 @@ let HelixHeadlessCombobox = class HelixHeadlessCombobox extends MixinControllerO
154
154
  return;
155
155
  }
156
156
  if (!this.open) {
157
- if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
157
+ if (event.key === 'ArrowDown' && event.altKey) {
158
+ event.preventDefault();
159
+ this.requestOpen(true);
160
+ }
161
+ else if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
158
162
  event.preventDefault();
159
163
  const fallback = event.key === 'ArrowDown' ? 'first' : 'last';
160
164
  this.opening = this.values.length > 0 ? 'selected' : fallback;
@@ -162,6 +166,11 @@ let HelixHeadlessCombobox = class HelixHeadlessCombobox extends MixinControllerO
162
166
  }
163
167
  return;
164
168
  }
169
+ if (event.key === 'ArrowUp' && event.altKey) {
170
+ event.preventDefault();
171
+ this.close();
172
+ return;
173
+ }
165
174
  if (this.moveAroundCreate(event)) {
166
175
  void this.updateComplete.then(() => this.linkActive());
167
176
  return;
@@ -215,6 +215,7 @@ declare const HelixHeadlessDataTable_base: (new (...args: any[]) => import("@fus
215
215
  *
216
216
  * @cssstate empty - While there are no rows to show and none are loading.
217
217
  * @cssstate error - While `error-text` is set.
218
+ * @cssstate loading - While `loading` is set, as the rows are on their way.
218
219
  */
219
220
  export declare class HelixHeadlessDataTable extends HelixHeadlessDataTable_base {
220
221
  /**
@@ -984,6 +984,7 @@ let HelixHeadlessDataTable = class HelixHeadlessDataTable extends MixinCollectio
984
984
  this.markEmpty();
985
985
  this.toggleState('error', this.errorText !== '');
986
986
  this.toggleState('empty', !this.loading && !this.hasRows);
987
+ this.toggleState('loading', this.loading);
987
988
  this.assign();
988
989
  this.announceSort();
989
990
  this.quietControls();
@@ -335,7 +335,13 @@ registerElementEvents({
335
335
  'volumechange',
336
336
  'waiting',
337
337
  ],
338
- 'helix-headless-video-playlist': ['helix-activate', 'helix-before-activate', 'helix-end'],
338
+ 'helix-headless-video-playlist': [
339
+ 'helix-activate',
340
+ 'helix-before-activate',
341
+ 'helix-before-reorder',
342
+ 'helix-end',
343
+ 'helix-reorder',
344
+ ],
339
345
  'helix-headless-video-playlist-item': ['helix-select'],
340
346
  'helix-headless-video-playlist-menu': ['helix-select'],
341
347
  'helix-headless-virtual-list': [
@@ -46,7 +46,8 @@ declare const HelixHeadlessMap_base: (new (...args: any[]) => import("@fusion.de
46
46
  * same `group`, such as a route and its dotted legs, share one tile. A tile
47
47
  * sends `helix-before-toggle` from the layer first, so you can store the
48
48
  * choice or refuse it, and `helix-toggle` from it once `hidden` holds the
49
- * choice.
49
+ * choice. Escape closes the menu and returns focus to its button, and the menu
50
+ * closes when focus moves on from it or a press lands outside the map.
50
51
  *
51
52
  * Imagery, a `helix-map-raster` with `ground`, is a map type and not a tile.
52
53
  * The menu opens with a card for each type, Standard, Hybrid, and Satellite,
@@ -315,6 +316,12 @@ export declare class HelixHeadlessMap extends HelixHeadlessMap_base {
315
316
  private hiding?;
316
317
  /** Closes the layers menu on a press anywhere outside the frame. */
317
318
  private readonly handleOutside;
319
+ /**
320
+ * Closes the layers menu when focus moves on from its button and the menu,
321
+ * as a Tab past the last tile does. Focus leaving the page, with nothing
322
+ * focused in its place, leaves the menu as it is.
323
+ */
324
+ private readonly handleMenuFocusOut;
318
325
  /**
319
326
  * Keys pressed in the layers menu. Escape closes it and returns to its
320
327
  * button, and no key reaches the region, where the arrows would pan the map.
@@ -82,6 +82,16 @@ let HelixHeadlessMap = class HelixHeadlessMap extends MixinViewport(HelixElement
82
82
  this.layersOpen = false;
83
83
  }
84
84
  };
85
+ this.handleMenuFocusOut = (event) => {
86
+ const to = event.relatedTarget;
87
+ if (!(this.layersOpen && to)) {
88
+ return;
89
+ }
90
+ const owners = this.renderRoot.querySelectorAll('[part="layers-toggle"], [part="layers-menu"]');
91
+ if (![...owners].some((owner) => owner.contains(to))) {
92
+ this.layersOpen = false;
93
+ }
94
+ };
85
95
  this.handleMenuKeydown = (event) => {
86
96
  event.stopPropagation();
87
97
  if (event.key === 'Escape' && this.layersOpen) {
@@ -402,6 +412,7 @@ let HelixHeadlessMap = class HelixHeadlessMap extends MixinViewport(HelixElement
402
412
  this.dispatchEvent(new HelixMapPartChangeEvent({ kind: 'menu' }));
403
413
  }
404
414
  }}
415
+ @focusout=${this.handleMenuFocusOut}
405
416
  @keydown=${this.handleMenuKeydown}
406
417
  >
407
418
  <slot name="layers-icon">
@@ -416,6 +427,7 @@ let HelixHeadlessMap = class HelixHeadlessMap extends MixinViewport(HelixElement
416
427
  role="group"
417
428
  aria-label=${this.layersLabel}
418
429
  ?hidden=${!this.layersOpen}
430
+ @focusout=${this.handleMenuFocusOut}
419
431
  @keydown=${this.handleMenuKeydown}
420
432
  >
421
433
  ${this.imagery.length > 0 ? this.renderMapTypes() : nothing}
@@ -10,16 +10,22 @@ import '../../kbd/index.js';
10
10
  * - `checked` makes the row a choice.
11
11
  * - With neither, the row is a command.
12
12
  *
13
- * The role follows the kind. A row with `checked` is `menuitemcheckbox` with
14
- * `aria-checked`, and `exclusive` makes it `menuitemradio`, for a set where
15
- * only one can be on. A row that opens a page keeps `menuitem` and sets
16
- * `aria-haspopup`.
13
+ * The role follows the kind. A row with `checkable` or `checked` is
14
+ * `menuitemcheckbox` with `aria-checked`, and an `exclusive` row is
15
+ * `menuitemradio`, for a set where only one can be on. A row that opens a page
16
+ * keeps `menuitem` and sets `aria-haspopup`.
17
17
  *
18
18
  * Choosing a choice row checks or unchecks it before `helix-select` is sent,
19
19
  * so a listener reads the new `checked`. An `exclusive` row turns on and turns
20
20
  * off the `exclusive` rows next to it. Cancel `helix-before-toggle` to keep a
21
21
  * row as it is and set `checked` yourself.
22
22
  *
23
+ * Space on a choice row changes it and leaves the menu open, so a reader can
24
+ * set several in a row, as the APG's menu does. Enter and a press change it and
25
+ * close the menu. `close-on-select="false"` on the row keeps the menu open after
26
+ * any choice of it, and `close-on-select` on the row closes it after a Space as
27
+ * well.
28
+ *
23
29
  * The menu sets whether the row's page is showing as a property, never as an
24
30
  * attribute. An attribute written onto your element after it upgrades causes a
25
31
  * hydration mismatch.
@@ -82,7 +88,8 @@ export declare class HelixHeadlessMenuItem extends HelixElement {
82
88
  * as a checkbox tells a reader its siblings are commands, which is the one
83
89
  * thing they are not.
84
90
  *
85
- * `checked` implies it, so a row that is on never has to say both.
91
+ * `checked` and `exclusive` imply it, so a row that is on, or one of a set,
92
+ * never has to say both.
86
93
  *
87
94
  * Choosing the row checks or unchecks it, after a `helix-before-toggle` that
88
95
  * you can cancel.
@@ -95,6 +102,14 @@ export declare class HelixHeadlessMenuItem extends HelixElement {
95
102
  * state you start the row in, and `:state(checked)` carries the current one.
96
103
  */
97
104
  checked: boolean;
105
+ /**
106
+ * Whether choosing the row closes the menu.
107
+ *
108
+ * Unset takes the menu's `close-on-select`, which is on, and keeps the menu
109
+ * open after a Space on a choice row. Write `close-on-select="false"` for a
110
+ * row that changes something the reader is watching, such as a zoom step.
111
+ */
112
+ closeOnSelect?: boolean;
98
113
  /**
99
114
  * Whether the row is disabled, so it cannot be chosen.
100
115
  */
@@ -116,8 +131,7 @@ export declare class HelixHeadlessMenuItem extends HelixElement {
116
131
  * it checked. `menuitemradio` tells a reader which kind of set they are in,
117
132
  * where `menuitemcheckbox` says every row is independent.
118
133
  *
119
- * Meaningless without `checkable`, since a row that cannot be ticked is not
120
- * one of a set of anything.
134
+ * It makes the row a choice, so it needs no `checkable` beside it.
121
135
  */
122
136
  exclusive: boolean;
123
137
  /**
@@ -192,6 +206,17 @@ export declare class HelixHeadlessMenuItem extends HelixElement {
192
206
  * protected. Not a consumer's to read or set.
193
207
  */
194
208
  readonly isMenuItem = true;
209
+ /**
210
+ * @internal Whether the last choice of this row asked the menu to stay open:
211
+ * a Space on a choice row, or `close-on-select="false"`. Read by the menu
212
+ * while it hears the row's `helix-select`.
213
+ */
214
+ keepsOpen: boolean;
215
+ /**
216
+ * @internal Whether the row is a choice: it can be ticked, or is one of a
217
+ * set. Read by the menu to reserve the trailing column.
218
+ */
219
+ get choice(): boolean;
195
220
  /**
196
221
  * Whether anything was slotted into `submenu`.
197
222
  *
@@ -342,8 +367,14 @@ export declare class HelixHeadlessMenuItem extends HelixElement {
342
367
  private tick;
343
368
  /** The other `exclusive` rows in this row's run of siblings. */
344
369
  private exclusivePeers;
370
+ /**
371
+ * Chooses the row. `space` is whether a Space chose it, which keeps the menu
372
+ * open on a choice row unless the row says otherwise.
373
+ */
345
374
  private choose;
346
375
  private readonly handleClick;
376
+ /** Whether the click on its way is the one a Space on a linked row sent. */
377
+ private spacePressed;
347
378
  /**
348
379
  * Enter and Space choose the row, which a `<div>` would not do on its own.
349
380
  *