@fusion.dev/helix-headless 0.155.0 → 0.157.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 (74) hide show
  1. package/custom-elements.json +2695 -137
  2. package/dist/canvas/node/canvas-node.js +1 -1
  3. package/dist/canvas/root/canvas-root.d.ts +17 -2
  4. package/dist/canvas/root/canvas-root.js +23 -6
  5. package/dist/chat/composer/chat-composer.js +1 -1
  6. package/dist/chat/root/chat-root.d.ts +14 -1
  7. package/dist/chat/root/chat-root.js +14 -4
  8. package/dist/combobox/root/combobox-root.d.ts +10 -0
  9. package/dist/combobox/root/combobox-root.js +18 -3
  10. package/dist/command/root/command-root.d.ts +116 -1
  11. package/dist/command/root/command-root.js +124 -15
  12. package/dist/data-table/root/data-table-root.d.ts +25 -0
  13. package/dist/data-table/root/data-table-root.js +13 -3
  14. package/dist/date-input/root/date-input-root.d.ts +32 -1
  15. package/dist/date-input/root/date-input-root.js +7 -1
  16. package/dist/drilldown/item/drilldown-item.d.ts +49 -1
  17. package/dist/drilldown/item/drilldown-item.js +77 -17
  18. package/dist/drilldown/root/drilldown-root.d.ts +45 -5
  19. package/dist/drilldown/root/drilldown-root.js +51 -5
  20. package/dist/element-events.generated.js +2 -1
  21. package/dist/filter-input/menu/filter-menu.d.ts +1 -0
  22. package/dist/filter-input/menu/filter-menu.js +4 -4
  23. package/dist/graph/root/graph-root.d.ts +13 -0
  24. package/dist/graph/root/graph-root.js +16 -0
  25. package/dist/list/root/list-root.d.ts +74 -1
  26. package/dist/list/root/list-root.js +92 -4
  27. package/dist/list/selection.js +3 -3
  28. package/dist/map/root/map-root.d.ts +6 -0
  29. package/dist/map/root/map-root.js +8 -1
  30. package/dist/megamenu/root/megamenu-root.d.ts +50 -13
  31. package/dist/megamenu/root/megamenu-root.js +44 -6
  32. package/dist/menu/item/menu-item.d.ts +24 -1
  33. package/dist/menu/item/menu-item.js +42 -2
  34. package/dist/menu/root/menu-root.d.ts +120 -9
  35. package/dist/menu/root/menu-root.js +329 -26
  36. package/dist/notifications/root/notifications-root.d.ts +20 -0
  37. package/dist/notifications/root/notifications-root.js +32 -1
  38. package/dist/number-input/root/number-input-root.d.ts +63 -6
  39. package/dist/number-input/root/number-input-root.js +96 -11
  40. package/dist/object-inspector/root/object-inspector-root.d.ts +36 -1
  41. package/dist/object-inspector/root/object-inspector-root.js +26 -2
  42. package/dist/option/root/option-root.d.ts +32 -0
  43. package/dist/option/root/option-root.js +44 -5
  44. package/dist/outline/root/outline-root.d.ts +6 -2
  45. package/dist/outline/root/outline-root.js +7 -3
  46. package/dist/table/root/table-root.d.ts +20 -0
  47. package/dist/table/root/table-root.js +13 -3
  48. package/dist/time-input/root/time-input-root.d.ts +32 -1
  49. package/dist/time-input/root/time-input-root.js +9 -2
  50. package/dist/time-picker/root/time-picker-root.d.ts +63 -1
  51. package/dist/time-picker/root/time-picker-root.js +58 -7
  52. package/dist/timeline/item/timeline-item.d.ts +25 -0
  53. package/dist/timeline/item/timeline-item.js +27 -2
  54. package/dist/toast/item/toast-item.d.ts +26 -0
  55. package/dist/toast/item/toast-item.js +55 -3
  56. package/dist/toast/root/toast-root.d.ts +32 -0
  57. package/dist/toast/root/toast-root.js +47 -2
  58. package/dist/video/root/keys.d.ts +6 -2
  59. package/dist/video/root/keys.js +18 -1
  60. package/dist/video/root/video-root.d.ts +61 -1
  61. package/dist/video/root/video-root.js +79 -11
  62. package/dist/video-playlist/item/video-playlist-item.d.ts +15 -1
  63. package/dist/video-playlist/item/video-playlist-item.js +7 -0
  64. package/dist/video-playlist/menu/video-playlist-menu.d.ts +1 -0
  65. package/dist/video-playlist/menu/video-playlist-menu.js +1 -1
  66. package/dist/video-playlist/root/video-playlist-root.d.ts +43 -4
  67. package/dist/video-playlist/root/video-playlist-root.js +69 -2
  68. package/dist/virtual-list/root/virtual-list-root.d.ts +8 -0
  69. package/dist/virtual-list/root/virtual-list-root.js +3 -0
  70. package/lint.json +1 -1
  71. package/package.json +5 -5
  72. package/types/jsx.d.ts +3346 -266
  73. package/types/vue.d.ts +910 -66
  74. package/types/words.d.ts +124 -0
@@ -4,11 +4,28 @@ 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 { AriaForwardController, anchorElement, customElement, DrilldownController, deepActiveElement, deepContains, HelixElement, inlineStep, KeyboardNavigationController, MixinCollection, MixinControllerOpen, PopoverController, scrollItemIntoView, slottedWords, tellDisclosure, typeaheadBy, words, } from '@fusion.dev/helix-core';
7
+ import { AriaForwardController, anchorElement, customElement, DrilldownController, deepActiveElement, deepContains, focusTarget, HelixElement, inlineStep, KeyboardNavigationController, linkDescription, MixinCollection, MixinControllerOpen, PopoverController, scrollItemIntoView, slottedWords, tellDisclosure, typeaheadBy, unlinkDescription, words, } from '@fusion.dev/helix-core';
8
8
  import { caretLeft } from '@fusion.dev/helix-icons';
9
9
  import { html, nothing } from 'lit';
10
10
  import { property } from 'lit/decorators.js';
11
11
  import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
12
+ import { LongPress } from '../../speed-dial/long-press.js';
13
+ const isItem = (element) => element !== null &&
14
+ typeof element === 'object' &&
15
+ 'isMenuItem' in element &&
16
+ element.isMenuItem === true;
17
+ const HOVER_DELAY = 100;
18
+ const GRACE = 300;
19
+ const inTriangle = (x, y, [a, b, c]) => {
20
+ if (!a || !b || !c) {
21
+ return false;
22
+ }
23
+ const side = (p, q) => (q[0] - p[0]) * (y - p[1]) - (q[1] - p[1]) * (x - p[0]);
24
+ const one = side(a, b);
25
+ const two = side(b, c);
26
+ const three = side(c, a);
27
+ return (one >= 0 && two >= 0 && three >= 0) || (one <= 0 && two <= 0 && three <= 0);
28
+ };
12
29
  const isSection = (element) => 'isMenuSection' in element && element.isMenuSection === true;
13
30
  let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinControllerOpen(HelixElement)) {
14
31
  constructor() {
@@ -16,8 +33,25 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
16
33
  this.backLabel = 'Back';
17
34
  this.context = false;
18
35
  this.label = '';
36
+ this.longPressLabel = 'Long press or press Alt+Down to show the menu';
19
37
  this.offset = 4;
38
+ this.longPress = new LongPress(this, {
39
+ onLongPress: () => {
40
+ if (this.context) {
41
+ const at = this.heldAt;
42
+ if (at) {
43
+ this.openContextAt(at);
44
+ }
45
+ return;
46
+ }
47
+ if (!this.open) {
48
+ this.show();
49
+ }
50
+ },
51
+ });
52
+ this.openOn = 'click';
20
53
  this.placement = 'bottom-start';
54
+ this.submenus = 'drill';
21
55
  this.drilldown = new DrilldownController(this, {
22
56
  isRow: (element) => 'isMenuItem' in element && element.isMenuItem === true,
23
57
  root: () => [...this.children]
@@ -46,6 +80,7 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
46
80
  placement: () => this.placement,
47
81
  surface: () => this.partNamed('surface'),
48
82
  });
83
+ this.settledOnce = false;
49
84
  this.handleContextMenu = (event) => {
50
85
  if (!this.context) {
51
86
  return;
@@ -56,21 +91,19 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
56
91
  return;
57
92
  }
58
93
  event.preventDefault();
59
- const box = this.trigger.getBoundingClientRect();
60
- const keyed = event.clientX === 0 && event.clientY === 0;
61
- this.point = keyed ? { x: box.left, y: box.top } : { x: event.clientX, y: event.clientY };
62
- if (this.open) {
63
- this.requestUpdate();
64
- this.updateComplete.then(() => this.panel.reposition());
94
+ if (this.open && this.longPress.takeClick()) {
65
95
  return;
66
96
  }
67
- const active = deepActiveElement();
68
- this.returnTo = active instanceof HTMLElement ? active : undefined;
69
- this.show();
97
+ const box = this.trigger.getBoundingClientRect();
98
+ const keyed = event.clientX === 0 && event.clientY === 0;
99
+ this.openContextAt(keyed ? { x: box.left, y: box.top } : { x: event.clientX, y: event.clientY });
70
100
  };
71
101
  this.handleSelect = (event) => {
72
102
  const item = event.target;
73
- if (!this.items.includes(item)) {
103
+ const reachable = this.flies
104
+ ? isItem(item) && item.onPage
105
+ : this.items.includes(item);
106
+ if (!reachable) {
74
107
  return;
75
108
  }
76
109
  const chosen = item;
@@ -90,7 +123,10 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
90
123
  if (!item) {
91
124
  return;
92
125
  }
93
- const surface = this.partNamed('surface');
126
+ const holder = this.flies ? this.holderOf(item) : null;
127
+ const surface = holder
128
+ ? holder.shadowRoot?.querySelector('[part="surface"]')
129
+ : this.partNamed('surface');
94
130
  scrollItemIntoView(item, { padding: 4, ...(surface ? { scroller: surface } : {}) });
95
131
  };
96
132
  this.handleKeydown = (event) => {
@@ -109,6 +145,9 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
109
145
  this.enter(items[at]);
110
146
  }
111
147
  else if (along === -1) {
148
+ if (this.flies && this.keyPage === null) {
149
+ return;
150
+ }
112
151
  this.leave();
113
152
  }
114
153
  else if (event.key === 'Tab') {
@@ -127,21 +166,166 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
127
166
  if (this.open) {
128
167
  this.hide();
129
168
  }
130
- else {
169
+ else if (!this.pressHeld) {
131
170
  this.show();
132
171
  }
133
172
  };
173
+ this.handlePointerDown = (event) => {
174
+ if (this.open || !this.fromTrigger(event)) {
175
+ return;
176
+ }
177
+ if (this.pressHeld || (this.context && event.pointerType !== 'mouse')) {
178
+ this.heldAt = { x: event.clientX, y: event.clientY };
179
+ this.longPress.down(event);
180
+ }
181
+ };
182
+ this.handlePointerMove = (event) => {
183
+ this.longPress.move(event);
184
+ };
185
+ this.handlePointerUp = () => {
186
+ this.longPress.up();
187
+ };
188
+ this.handlePointerCancel = () => {
189
+ this.longPress.cancel();
190
+ };
191
+ this.handleHeldClick = (event) => {
192
+ if (this.pressHeld && this.longPress.takeClick()) {
193
+ event.stopImmediatePropagation();
194
+ event.preventDefault();
195
+ }
196
+ };
197
+ this.handleTriggerFocusIn = (event) => {
198
+ if (this.pressHeld && this.fromTrigger(event)) {
199
+ this.describeTrigger(true);
200
+ }
201
+ };
202
+ this.handleHover = (event) => {
203
+ if (!this.flies || !this.open || event.pointerType !== 'mouse') {
204
+ return;
205
+ }
206
+ const row = event
207
+ .composedPath()
208
+ .find((node) => isItem(node));
209
+ if (!row?.onPage || row === this.hovered || this.inGrace(event)) {
210
+ return;
211
+ }
212
+ this.hovered = row;
213
+ this.grace = undefined;
214
+ if (!row.disabled) {
215
+ row.focus();
216
+ }
217
+ clearTimeout(this.hoverTimer);
218
+ this.hoverTimer = setTimeout(() => {
219
+ if (!this.open || this.hovered !== row) {
220
+ return;
221
+ }
222
+ if (row.hasSubmenu && !row.disabled) {
223
+ if (this.drilldown.page !== row) {
224
+ this.drilldown.enter(row);
225
+ }
226
+ return;
227
+ }
228
+ const holder = this.holderOf(row);
229
+ if (this.drilldown.page !== holder) {
230
+ this.openPage(holder);
231
+ }
232
+ }, HOVER_DELAY);
233
+ };
234
+ this.handleRowOut = (event) => {
235
+ if (!this.flies || event.pointerType !== 'mouse') {
236
+ return;
237
+ }
238
+ const row = event
239
+ .composedPath()
240
+ .find((node) => isItem(node));
241
+ const panel = row?.expanded
242
+ ? row.shadowRoot?.querySelector('[part="surface"]')
243
+ : undefined;
244
+ if (!panel) {
245
+ return;
246
+ }
247
+ const box = panel.getBoundingClientRect();
248
+ const edge = box.left >= event.clientX ? box.left : box.right;
249
+ this.grace = {
250
+ points: [
251
+ [event.clientX, event.clientY],
252
+ [edge, box.top],
253
+ [edge, box.bottom],
254
+ ],
255
+ until: Date.now() + GRACE,
256
+ };
257
+ };
134
258
  }
135
259
  static { this.words = 'helix-menu'; }
260
+ get flies() {
261
+ return this.submenus === 'flyout';
262
+ }
263
+ get topRows() {
264
+ return [...this.children]
265
+ .filter((child) => child.slot !== 'trigger')
266
+ .flatMap((child) => (isSection(child) ? [...child.children] : [child]))
267
+ .filter(isItem);
268
+ }
269
+ rowsOf(holder) {
270
+ return holder
271
+ ? [...holder.children].filter((child) => child.slot === 'submenu' && isItem(child))
272
+ : this.topRows;
273
+ }
274
+ holderOf(row) {
275
+ const parent = row.parentElement;
276
+ return row.slot === 'submenu' && parent && isItem(parent) ? parent : null;
277
+ }
278
+ get openPages() {
279
+ const pages = [];
280
+ for (let page = this.drilldown.page; page; page = this.holderOf(page)) {
281
+ pages.unshift(page);
282
+ }
283
+ return [null, ...pages];
284
+ }
285
+ get keyPage() {
286
+ for (const page of this.openPages) {
287
+ const active = this.rowsOf(page).find((row) => row.active);
288
+ if (active) {
289
+ return page;
290
+ }
291
+ }
292
+ return this.drilldown.page;
293
+ }
294
+ openPage(holder) {
295
+ if (holder) {
296
+ this.drilldown.enter(holder);
297
+ }
298
+ else {
299
+ this.drilldown.reset();
300
+ }
301
+ }
302
+ get pressHeld() {
303
+ return !this.context && this.openOn === 'long-press';
304
+ }
136
305
  tellTrigger() {
137
306
  const slot = this.renderRoot?.querySelector('slot[name="trigger"]');
138
- tellDisclosure(slot?.assignedElements({ flatten: true })[0], this.open, 'menu');
307
+ const trigger = slot?.assignedElements({ flatten: true })[0];
308
+ if (!trigger || trigger.localName.includes('-')) {
309
+ tellDisclosure(trigger, this.open, 'menu');
310
+ return;
311
+ }
312
+ if (trigger !== this.told) {
313
+ if (!this.settledOnce || trigger.hasAttribute('aria-expanded')) {
314
+ return;
315
+ }
316
+ this.told = trigger;
317
+ if (!trigger.hasAttribute('aria-haspopup')) {
318
+ trigger.setAttribute('aria-haspopup', 'menu');
319
+ }
320
+ }
321
+ trigger.setAttribute('aria-expanded', String(this.open));
139
322
  }
140
323
  get trigger() {
141
324
  return anchorElement(this.renderRoot?.querySelector('slot[name="trigger"]'), this);
142
325
  }
143
326
  get items() {
144
- return this.drilldown.rows.filter((row) => !row.disabled);
327
+ const rows = this.flies ? this.rowsOf(this.keyPage) : this.drilldown.rows;
328
+ return rows.filter((row) => !row.disabled);
145
329
  }
146
330
  show() {
147
331
  this.openAt('first');
@@ -161,6 +345,7 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
161
345
  }
162
346
  this.panel.hide();
163
347
  this.drilldown.reset();
348
+ this.stopHover();
164
349
  if (deepContains(this, deepActiveElement())) {
165
350
  ;
166
351
  (this.context ? (this.returnTo ?? this.trigger) : this.trigger).focus();
@@ -168,13 +353,43 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
168
353
  this.point = undefined;
169
354
  this.returnTo = undefined;
170
355
  }
356
+ openContextAt(point) {
357
+ this.point = point;
358
+ if (this.open) {
359
+ this.requestUpdate();
360
+ this.updateComplete.then(() => this.panel.reposition());
361
+ return;
362
+ }
363
+ const active = deepActiveElement();
364
+ this.returnTo = active instanceof HTMLElement ? active : undefined;
365
+ this.show();
366
+ }
171
367
  enter(item) {
172
368
  if (!this.drilldown.enter(item)) {
173
369
  return;
174
370
  }
175
- this.updateComplete.then(() => this.items[0]?.focusItem());
371
+ void this.updateComplete.then(async () => {
372
+ if (!this.flies) {
373
+ this.items[0]?.focusItem();
374
+ return;
375
+ }
376
+ await item.updateComplete;
377
+ const first = this.rowsOf(item).find((row) => !row.disabled);
378
+ await first?.updateComplete;
379
+ first?.focusItem();
380
+ });
176
381
  }
177
382
  leave() {
383
+ if (this.flies) {
384
+ const holder = this.keyPage;
385
+ if (!holder) {
386
+ this.hide();
387
+ return;
388
+ }
389
+ this.openPage(this.holderOf(holder));
390
+ this.updateComplete.then(() => holder.focus());
391
+ return;
392
+ }
178
393
  if (this.drilldown.atRoot) {
179
394
  this.hide();
180
395
  return;
@@ -184,42 +399,107 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
184
399
  this.updateComplete.then(() => leaving?.focus());
185
400
  }
186
401
  handleClosedKeydown(event) {
402
+ if (this.context && event.shiftKey && event.key === 'F10' && this.fromTrigger(event)) {
403
+ event.preventDefault();
404
+ const box = this.trigger.getBoundingClientRect();
405
+ this.openContextAt({ x: box.left, y: box.top });
406
+ return;
407
+ }
187
408
  if (this.context || (event.key !== 'ArrowDown' && event.key !== 'ArrowUp')) {
188
409
  return;
189
410
  }
190
- const slot = this.renderRoot?.querySelector('slot[name="trigger"]');
191
- const triggers = slot?.assignedElements({ flatten: true }) ?? [];
192
- if (!event.composedPath().some((node) => triggers.includes(node))) {
411
+ if (this.pressHeld !== event.altKey || !this.fromTrigger(event)) {
193
412
  return;
194
413
  }
195
414
  event.preventDefault();
196
415
  this.openAt(event.key === 'ArrowDown' ? 'first' : 'last');
197
416
  }
417
+ fromTrigger(event) {
418
+ const slot = this.renderRoot?.querySelector('slot[name="trigger"]');
419
+ const triggers = slot?.assignedElements({ flatten: true }) ?? [];
420
+ return event.composedPath().some((node) => triggers.includes(node));
421
+ }
422
+ describeTrigger(light = false) {
423
+ const slot = this.renderRoot?.querySelector('slot[name="trigger"]');
424
+ const trigger = slot?.assignedElements({ flatten: true })[0];
425
+ const control = this.pressHeld && trigger instanceof HTMLElement ? focusTarget(trigger) : undefined;
426
+ if (this.described && this.described !== control) {
427
+ unlinkDescription(this.described, this);
428
+ this.described = undefined;
429
+ }
430
+ if (control && control !== this.described && linkDescription(control, this, light)) {
431
+ this.described = control;
432
+ }
433
+ this.internals.ariaLabel = this.described ? this.longPressLabel : null;
434
+ }
435
+ stopHover() {
436
+ clearTimeout(this.hoverTimer);
437
+ this.hoverTimer = undefined;
438
+ this.hovered = undefined;
439
+ this.grace = undefined;
440
+ }
441
+ inGrace(event) {
442
+ const grace = this.grace;
443
+ return (grace !== undefined &&
444
+ Date.now() < grace.until &&
445
+ inTriangle(event.clientX, event.clientY, grace.points));
446
+ }
198
447
  connectedCallback() {
199
448
  super.connectedCallback();
449
+ this.addEventListener('pointermove', this.handleHover);
450
+ this.addEventListener('pointerout', this.handleRowOut);
200
451
  this.addEventListener('helix-select', this.handleSelect, true);
201
452
  this.addEventListener('keydown', this.handleKeydown);
202
453
  this.addEventListener('focusin', this.handleFocusIn);
203
454
  this.addEventListener('contextmenu', this.handleContextMenu);
455
+ this.addEventListener('pointerdown', this.handlePointerDown);
456
+ this.addEventListener('pointermove', this.handlePointerMove);
457
+ this.addEventListener('pointerup', this.handlePointerUp);
458
+ this.addEventListener('pointercancel', this.handlePointerCancel);
459
+ this.addEventListener('click', this.handleHeldClick, true);
460
+ this.addEventListener('focusin', this.handleTriggerFocusIn);
461
+ void this.whenSettled.then(() => {
462
+ this.settledOnce = true;
463
+ if (!this.context) {
464
+ this.tellTrigger();
465
+ }
466
+ });
204
467
  }
205
468
  disconnectedCallback() {
206
469
  super.disconnectedCallback();
470
+ this.removeEventListener('pointermove', this.handleHover);
471
+ this.removeEventListener('pointerout', this.handleRowOut);
472
+ this.stopHover();
207
473
  this.removeEventListener('helix-select', this.handleSelect, true);
208
474
  this.removeEventListener('keydown', this.handleKeydown);
209
475
  this.removeEventListener('focusin', this.handleFocusIn);
210
476
  this.removeEventListener('contextmenu', this.handleContextMenu);
477
+ this.removeEventListener('pointerdown', this.handlePointerDown);
478
+ this.removeEventListener('pointermove', this.handlePointerMove);
479
+ this.removeEventListener('pointerup', this.handlePointerUp);
480
+ this.removeEventListener('pointercancel', this.handlePointerCancel);
481
+ this.removeEventListener('click', this.handleHeldClick, true);
482
+ this.removeEventListener('focusin', this.handleTriggerFocusIn);
483
+ if (this.described) {
484
+ unlinkDescription(this.described, this);
485
+ this.described = undefined;
486
+ }
211
487
  }
212
488
  updated(changed) {
213
489
  super.updated(changed);
214
490
  if (!this.context) {
215
491
  this.tellTrigger();
216
492
  }
493
+ if (changed.has('openOn') || changed.has('context') || this.described) {
494
+ this.describeTrigger();
495
+ }
496
+ const top = this.flies || this.drilldown.atRoot;
217
497
  for (const child of this.children) {
218
498
  if (isSection(child)) {
219
- child.onPage = this.drilldown.atRoot;
499
+ child.onPage = top;
220
500
  for (const row of child.children) {
221
501
  if (row.isMenuItem === true) {
222
- this.mark(row, this.drilldown.atRoot);
502
+ this.mark(row, top);
223
503
  }
224
504
  }
225
505
  continue;
@@ -228,9 +508,9 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
228
508
  if (item.isMenuItem !== true) {
229
509
  continue;
230
510
  }
231
- this.mark(item, this.drilldown.atRoot);
511
+ this.mark(item, top);
232
512
  }
233
- this.toggleState('drilled', !this.drilldown.atRoot);
513
+ this.toggleState('drilled', !this.flies && !this.drilldown.atRoot);
234
514
  this.markColumns();
235
515
  this.nameSurface();
236
516
  }
@@ -249,13 +529,26 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
249
529
  surface.ariaLabelledByElements = trigger && !named ? [trigger] : null;
250
530
  }
251
531
  markColumns() {
252
- const rows = this.drilldown.rows;
532
+ const rows = this.flies ? this.topRows : this.drilldown.rows;
253
533
  this.toggleState('start', rows.some((row) => row.querySelector(':scope > [slot="start"]') !== null));
254
534
  this.toggleState('mark', rows.some((row) => row.checkable || row.checked || this.drilldown.hasPage(row)));
255
535
  }
256
536
  mark(item, onPage) {
257
537
  const page = this.drilldown.page;
258
538
  item.onPage = onPage;
539
+ const holds = page !== null && item.contains(page);
540
+ item.flyout = this.flies;
541
+ item.expanded = this.flies && holds;
542
+ if (this.flies) {
543
+ item.showingSubmenu = false;
544
+ for (const child of item.children) {
545
+ const nested = child;
546
+ if (nested.isMenuItem === true && child.slot === 'submenu') {
547
+ this.mark(nested, onPage && holds);
548
+ }
549
+ }
550
+ return;
551
+ }
259
552
  item.showingSubmenu = page !== null && item.contains(page);
260
553
  for (const child of item.children) {
261
554
  const nested = child;
@@ -279,10 +572,10 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
279
572
  role="menu"
280
573
  aria-busy=${this.loading ? 'true' : nothing}
281
574
  aria-label=${this.label || nothing}
282
- data-drilled=${this.drilldown.atRoot ? nothing : ''}
575
+ data-drilled=${this.flies || this.drilldown.atRoot ? nothing : ''}
283
576
  ?hidden=${!this.open}
284
577
  >
285
- ${this.drilldown.atRoot
578
+ ${this.flies || this.drilldown.atRoot
286
579
  ? nothing
287
580
  : html `<div part="header">
288
581
  <button
@@ -314,12 +607,22 @@ __decorate([
314
607
  __decorate([
315
608
  property()
316
609
  ], HelixHeadlessMenu.prototype, "label", void 0);
610
+ __decorate([
611
+ words,
612
+ property({ attribute: 'long-press-label' })
613
+ ], HelixHeadlessMenu.prototype, "longPressLabel", void 0);
317
614
  __decorate([
318
615
  property({ type: Number })
319
616
  ], HelixHeadlessMenu.prototype, "offset", void 0);
617
+ __decorate([
618
+ property({ attribute: 'open-on' })
619
+ ], HelixHeadlessMenu.prototype, "openOn", void 0);
320
620
  __decorate([
321
621
  property()
322
622
  ], HelixHeadlessMenu.prototype, "placement", void 0);
623
+ __decorate([
624
+ property()
625
+ ], HelixHeadlessMenu.prototype, "submenus", void 0);
323
626
  HelixHeadlessMenu = __decorate([
324
627
  customElement('helix-headless-menu')
325
628
  ], HelixHeadlessMenu);
@@ -40,6 +40,11 @@ declare const HelixHeadlessNotifications_base: (new (...args: any[]) => import("
40
40
  * | Control+Home | From anywhere in the feed, moves to the nearest control before it on the page. |
41
41
  * | Tab | Moves through the controls in a row, and on out of the feed. |
42
42
  *
43
+ * **A notification that arrives says so.** Unread rows added above the rows
44
+ * there were are counted in the live region,
45
+ * "1 new notification", in `arrived-label`. Rows that arrive while `loading`
46
+ * is set say nothing, so set it while a page is on its way.
47
+ *
43
48
  * **`load-more` adds a button after the list** that sends `helix-load-more`,
44
49
  * for a feed that holds one page of a longer list. Append the next page, set
45
50
  * `loading` while it is on its way, and take `load-more` off once there is
@@ -100,6 +105,11 @@ export declare class HelixHeadlessNotifications extends HelixHeadlessNotificatio
100
105
  * A notification's own words are under `helix-notification`.
101
106
  */
102
107
  static words: string;
108
+ /**
109
+ * What the feed says when unread notifications arrive, with `{count}` the
110
+ * number of them. Empty it to say nothing.
111
+ */
112
+ arrivedLabel: string;
103
113
  /** The region's name where `label` is empty. */
104
114
  fallbackLabel: string;
105
115
  /**
@@ -272,6 +282,16 @@ export declare class HelixHeadlessNotifications extends HelixHeadlessNotificatio
272
282
  private rove;
273
283
  connectedCallback(): void;
274
284
  disconnectedCallback(): void;
285
+ /** The rows the feed has met, so a row that arrives later is told apart. */
286
+ private seen;
287
+ /** Empties the arrival a moment after it is said, so the next one is news. */
288
+ private readonly hush;
289
+ /**
290
+ * Counts the unread rows that arrived above the rows there were, and says
291
+ * how many. Rows that fill an empty feed are a page, which says nothing. The rows there are at the first update are
292
+ * the feed's to begin with, and say nothing.
293
+ */
294
+ private noteArrivals;
275
295
  protected willUpdate(changed: PropertyValues): void;
276
296
  /**
277
297
  * The error, the empty or the loading line, each around a slot of the same
@@ -4,13 +4,14 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
4
4
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
- import { assignSlots, ChildrenController, customElement, deepContains, focusableElements, HelixElement, HelixLoadMoreEvent, HelixReadEvent, hostAria, MixinCollection, markRegion, rovingStop, whenHydrated, words, } from '@fusion.dev/helix-core';
7
+ import { assignSlots, ChildrenController, customElement, DelayController, deepContains, focusableElements, formatWords, HelixElement, HelixLoadMoreEvent, HelixReadEvent, hostAria, MixinCollection, markRegion, rovingStop, whenHydrated, words, } from '@fusion.dev/helix-core';
8
8
  import { html, nothing } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
10
  import { feedStep } from '../feed-keys.js';
11
11
  let HelixHeadlessNotifications = class HelixHeadlessNotifications extends MixinCollection(HelixElement) {
12
12
  constructor() {
13
13
  super(...arguments);
14
+ this.arrivedLabel = '{count, plural, one {# new notification} other {# new notifications}}';
14
15
  this.fallbackLabel = 'Notifications';
15
16
  this.label = '';
16
17
  this.loadError = '';
@@ -60,6 +61,7 @@ let HelixHeadlessNotifications = class HelixHeadlessNotifications extends MixinC
60
61
  rowBox(next)?.focus();
61
62
  }
62
63
  };
64
+ this.hush = new DelayController(this, { renders: false });
63
65
  this.askForMore = () => {
64
66
  if (!this.loading) {
65
67
  this.dispatchEvent(new HelixLoadMoreEvent());
@@ -196,8 +198,33 @@ let HelixHeadlessNotifications = class HelixHeadlessNotifications extends MixinC
196
198
  this.removeEventListener('focusin', this.handleFocusIn);
197
199
  this.removeEventListener('keydown', this.handleKeydown);
198
200
  }
201
+ noteArrivals() {
202
+ if (!this.childrenKnown) {
203
+ return;
204
+ }
205
+ const items = this.items;
206
+ const seen = this.seen;
207
+ if (!seen) {
208
+ this.seen = new WeakSet(items);
209
+ return;
210
+ }
211
+ const firstKnown = items.findIndex((item) => seen.has(item));
212
+ const fresh = items.filter((item, at) => !seen.has(item) && at < firstKnown && !item.read);
213
+ for (const item of items) {
214
+ seen.add(item);
215
+ }
216
+ if (fresh.length === 0 || this.loading || this.arrivedLabel === '') {
217
+ return;
218
+ }
219
+ this.announced = formatWords(this, 'arrivedLabel', { count: fresh.length });
220
+ this.hush.start(1000, () => {
221
+ this.announced = '';
222
+ this.requestUpdate();
223
+ });
224
+ }
199
225
  willUpdate(changed) {
200
226
  super.willUpdate(changed);
227
+ this.noteArrivals();
201
228
  hostAria(this.internals, { label: this.label || this.fallbackLabel, role: 'region' });
202
229
  const failed = this.failed;
203
230
  this.toggleState('error', failed);
@@ -284,6 +311,10 @@ let HelixHeadlessNotifications = class HelixHeadlessNotifications extends MixinC
284
311
  <div part="footer"><slot name="footer"></slot></div>`;
285
312
  }
286
313
  };
314
+ __decorate([
315
+ words,
316
+ property({ attribute: 'arrived-label' })
317
+ ], HelixHeadlessNotifications.prototype, "arrivedLabel", void 0);
287
318
  __decorate([
288
319
  words,
289
320
  property({ attribute: 'fallback-label' })