@fusion.dev/helix-headless 0.156.0 → 0.158.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 (47) hide show
  1. package/custom-elements.json +1085 -66
  2. package/dist/command/root/command-root.d.ts +19 -5
  3. package/dist/command/root/command-root.js +14 -4
  4. package/dist/data-table/root/data-table-root.js +5 -2
  5. package/dist/drilldown/item/drilldown-item.d.ts +49 -1
  6. package/dist/drilldown/item/drilldown-item.js +77 -17
  7. package/dist/drilldown/root/drilldown-root.d.ts +45 -5
  8. package/dist/drilldown/root/drilldown-root.js +51 -5
  9. package/dist/element-events.generated.js +1 -1
  10. package/dist/filter-input/menu/filter-menu.d.ts +1 -0
  11. package/dist/filter-input/menu/filter-menu.js +4 -4
  12. package/dist/list/item/list-item.d.ts +15 -1
  13. package/dist/list/item/list-item.js +20 -1
  14. package/dist/list/root/list-root.d.ts +29 -1
  15. package/dist/list/root/list-root.js +44 -10
  16. package/dist/listbox/root/listbox-root.d.ts +19 -2
  17. package/dist/listbox/root/listbox-root.js +16 -8
  18. package/dist/map/root/map-root.d.ts +4 -0
  19. package/dist/map/root/map-root.js +1 -1
  20. package/dist/megamenu/root/megamenu-root.d.ts +50 -13
  21. package/dist/megamenu/root/megamenu-root.js +44 -6
  22. package/dist/menu/item/menu-item.d.ts +24 -1
  23. package/dist/menu/item/menu-item.js +42 -2
  24. package/dist/menu/root/menu-root.d.ts +126 -11
  25. package/dist/menu/root/menu-root.js +336 -26
  26. package/dist/notifications/root/notifications-root.d.ts +20 -0
  27. package/dist/notifications/root/notifications-root.js +32 -1
  28. package/dist/number-input/root/number-input-root.d.ts +7 -4
  29. package/dist/number-input/root/number-input-root.js +3 -3
  30. package/dist/progress/root/progress-root.js +1 -1
  31. package/dist/select/root/select-root.d.ts +8 -1
  32. package/dist/select/root/select-root.js +8 -5
  33. package/dist/skeleton/root/skeleton-root.d.ts +2 -1
  34. package/dist/skeleton/root/skeleton-root.js +1 -1
  35. package/dist/toast/item/toast-item.d.ts +26 -0
  36. package/dist/toast/item/toast-item.js +55 -3
  37. package/dist/toast/root/toast-root.d.ts +32 -0
  38. package/dist/toast/root/toast-root.js +47 -2
  39. package/dist/video/root/video-root.d.ts +1 -0
  40. package/dist/video/root/video-root.js +9 -9
  41. package/dist/video-playlist/menu/video-playlist-menu.d.ts +1 -0
  42. package/dist/video-playlist/menu/video-playlist-menu.js +1 -1
  43. package/lint.json +1 -1
  44. package/package.json +5 -5
  45. package/types/jsx.d.ts +653 -74
  46. package/types/vue.d.ts +133 -16
  47. package/types/words.d.ts +14 -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, sideOf, 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]
@@ -42,10 +76,18 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
42
76
  boundary: () => this,
43
77
  offset: () => this.offset,
44
78
  onDismiss: () => this.hide(),
79
+ onPlace: (placement) => {
80
+ const side = sideOf(placement);
81
+ this.toggleState('placed-bottom', side === 'bottom');
82
+ this.toggleState('placed-left', side === 'left');
83
+ this.toggleState('placed-right', side === 'right');
84
+ this.toggleState('placed-top', side === 'top');
85
+ },
45
86
  open: () => this.open,
46
87
  placement: () => this.placement,
47
88
  surface: () => this.partNamed('surface'),
48
89
  });
90
+ this.settledOnce = false;
49
91
  this.handleContextMenu = (event) => {
50
92
  if (!this.context) {
51
93
  return;
@@ -56,21 +98,19 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
56
98
  return;
57
99
  }
58
100
  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());
101
+ if (this.open && this.longPress.takeClick()) {
65
102
  return;
66
103
  }
67
- const active = deepActiveElement();
68
- this.returnTo = active instanceof HTMLElement ? active : undefined;
69
- this.show();
104
+ const box = this.trigger.getBoundingClientRect();
105
+ const keyed = event.clientX === 0 && event.clientY === 0;
106
+ this.openContextAt(keyed ? { x: box.left, y: box.top } : { x: event.clientX, y: event.clientY });
70
107
  };
71
108
  this.handleSelect = (event) => {
72
109
  const item = event.target;
73
- if (!this.items.includes(item)) {
110
+ const reachable = this.flies
111
+ ? isItem(item) && item.onPage
112
+ : this.items.includes(item);
113
+ if (!reachable) {
74
114
  return;
75
115
  }
76
116
  const chosen = item;
@@ -90,7 +130,10 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
90
130
  if (!item) {
91
131
  return;
92
132
  }
93
- const surface = this.partNamed('surface');
133
+ const holder = this.flies ? this.holderOf(item) : null;
134
+ const surface = holder
135
+ ? holder.shadowRoot?.querySelector('[part="surface"]')
136
+ : this.partNamed('surface');
94
137
  scrollItemIntoView(item, { padding: 4, ...(surface ? { scroller: surface } : {}) });
95
138
  };
96
139
  this.handleKeydown = (event) => {
@@ -109,6 +152,9 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
109
152
  this.enter(items[at]);
110
153
  }
111
154
  else if (along === -1) {
155
+ if (this.flies ? this.keyPage === null : this.drilldown.atRoot) {
156
+ return;
157
+ }
112
158
  this.leave();
113
159
  }
114
160
  else if (event.key === 'Tab') {
@@ -127,21 +173,166 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
127
173
  if (this.open) {
128
174
  this.hide();
129
175
  }
130
- else {
176
+ else if (!this.pressHeld) {
131
177
  this.show();
132
178
  }
133
179
  };
180
+ this.handlePointerDown = (event) => {
181
+ if (this.open || !this.fromTrigger(event)) {
182
+ return;
183
+ }
184
+ if (this.pressHeld || (this.context && event.pointerType !== 'mouse')) {
185
+ this.heldAt = { x: event.clientX, y: event.clientY };
186
+ this.longPress.down(event);
187
+ }
188
+ };
189
+ this.handlePointerMove = (event) => {
190
+ this.longPress.move(event);
191
+ };
192
+ this.handlePointerUp = () => {
193
+ this.longPress.up();
194
+ };
195
+ this.handlePointerCancel = () => {
196
+ this.longPress.cancel();
197
+ };
198
+ this.handleHeldClick = (event) => {
199
+ if (this.pressHeld && this.longPress.takeClick()) {
200
+ event.stopImmediatePropagation();
201
+ event.preventDefault();
202
+ }
203
+ };
204
+ this.handleTriggerFocusIn = (event) => {
205
+ if (this.pressHeld && this.fromTrigger(event)) {
206
+ this.describeTrigger(true);
207
+ }
208
+ };
209
+ this.handleHover = (event) => {
210
+ if (!this.flies || !this.open || event.pointerType !== 'mouse') {
211
+ return;
212
+ }
213
+ const row = event
214
+ .composedPath()
215
+ .find((node) => isItem(node));
216
+ if (!row?.onPage || row === this.hovered || this.inGrace(event)) {
217
+ return;
218
+ }
219
+ this.hovered = row;
220
+ this.grace = undefined;
221
+ if (!row.disabled) {
222
+ row.focus();
223
+ }
224
+ clearTimeout(this.hoverTimer);
225
+ this.hoverTimer = setTimeout(() => {
226
+ if (!this.open || this.hovered !== row) {
227
+ return;
228
+ }
229
+ if (row.hasSubmenu && !row.disabled) {
230
+ if (this.drilldown.page !== row) {
231
+ this.drilldown.enter(row);
232
+ }
233
+ return;
234
+ }
235
+ const holder = this.holderOf(row);
236
+ if (this.drilldown.page !== holder) {
237
+ this.openPage(holder);
238
+ }
239
+ }, HOVER_DELAY);
240
+ };
241
+ this.handleRowOut = (event) => {
242
+ if (!this.flies || event.pointerType !== 'mouse') {
243
+ return;
244
+ }
245
+ const row = event
246
+ .composedPath()
247
+ .find((node) => isItem(node));
248
+ const panel = row?.expanded
249
+ ? row.shadowRoot?.querySelector('[part="surface"]')
250
+ : undefined;
251
+ if (!panel) {
252
+ return;
253
+ }
254
+ const box = panel.getBoundingClientRect();
255
+ const edge = box.left >= event.clientX ? box.left : box.right;
256
+ this.grace = {
257
+ points: [
258
+ [event.clientX, event.clientY],
259
+ [edge, box.top],
260
+ [edge, box.bottom],
261
+ ],
262
+ until: Date.now() + GRACE,
263
+ };
264
+ };
134
265
  }
135
266
  static { this.words = 'helix-menu'; }
267
+ get flies() {
268
+ return this.submenus === 'flyout';
269
+ }
270
+ get topRows() {
271
+ return [...this.children]
272
+ .filter((child) => child.slot !== 'trigger')
273
+ .flatMap((child) => (isSection(child) ? [...child.children] : [child]))
274
+ .filter(isItem);
275
+ }
276
+ rowsOf(holder) {
277
+ return holder
278
+ ? [...holder.children].filter((child) => child.slot === 'submenu' && isItem(child))
279
+ : this.topRows;
280
+ }
281
+ holderOf(row) {
282
+ const parent = row.parentElement;
283
+ return row.slot === 'submenu' && parent && isItem(parent) ? parent : null;
284
+ }
285
+ get openPages() {
286
+ const pages = [];
287
+ for (let page = this.drilldown.page; page; page = this.holderOf(page)) {
288
+ pages.unshift(page);
289
+ }
290
+ return [null, ...pages];
291
+ }
292
+ get keyPage() {
293
+ for (const page of this.openPages) {
294
+ const active = this.rowsOf(page).find((row) => row.active);
295
+ if (active) {
296
+ return page;
297
+ }
298
+ }
299
+ return this.drilldown.page;
300
+ }
301
+ openPage(holder) {
302
+ if (holder) {
303
+ this.drilldown.enter(holder);
304
+ }
305
+ else {
306
+ this.drilldown.reset();
307
+ }
308
+ }
309
+ get pressHeld() {
310
+ return !this.context && this.openOn === 'long-press';
311
+ }
136
312
  tellTrigger() {
137
313
  const slot = this.renderRoot?.querySelector('slot[name="trigger"]');
138
- tellDisclosure(slot?.assignedElements({ flatten: true })[0], this.open, 'menu');
314
+ const trigger = slot?.assignedElements({ flatten: true })[0];
315
+ if (!trigger || trigger.localName.includes('-')) {
316
+ tellDisclosure(trigger, this.open, 'menu');
317
+ return;
318
+ }
319
+ if (trigger !== this.told) {
320
+ if (!this.settledOnce || trigger.hasAttribute('aria-expanded')) {
321
+ return;
322
+ }
323
+ this.told = trigger;
324
+ if (!trigger.hasAttribute('aria-haspopup')) {
325
+ trigger.setAttribute('aria-haspopup', 'menu');
326
+ }
327
+ }
328
+ trigger.setAttribute('aria-expanded', String(this.open));
139
329
  }
140
330
  get trigger() {
141
331
  return anchorElement(this.renderRoot?.querySelector('slot[name="trigger"]'), this);
142
332
  }
143
333
  get items() {
144
- return this.drilldown.rows.filter((row) => !row.disabled);
334
+ const rows = this.flies ? this.rowsOf(this.keyPage) : this.drilldown.rows;
335
+ return rows.filter((row) => !row.disabled);
145
336
  }
146
337
  show() {
147
338
  this.openAt('first');
@@ -161,6 +352,7 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
161
352
  }
162
353
  this.panel.hide();
163
354
  this.drilldown.reset();
355
+ this.stopHover();
164
356
  if (deepContains(this, deepActiveElement())) {
165
357
  ;
166
358
  (this.context ? (this.returnTo ?? this.trigger) : this.trigger).focus();
@@ -168,13 +360,43 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
168
360
  this.point = undefined;
169
361
  this.returnTo = undefined;
170
362
  }
363
+ openContextAt(point) {
364
+ this.point = point;
365
+ if (this.open) {
366
+ this.requestUpdate();
367
+ this.updateComplete.then(() => this.panel.reposition());
368
+ return;
369
+ }
370
+ const active = deepActiveElement();
371
+ this.returnTo = active instanceof HTMLElement ? active : undefined;
372
+ this.show();
373
+ }
171
374
  enter(item) {
172
375
  if (!this.drilldown.enter(item)) {
173
376
  return;
174
377
  }
175
- this.updateComplete.then(() => this.items[0]?.focusItem());
378
+ void this.updateComplete.then(async () => {
379
+ if (!this.flies) {
380
+ this.items[0]?.focusItem();
381
+ return;
382
+ }
383
+ await item.updateComplete;
384
+ const first = this.rowsOf(item).find((row) => !row.disabled);
385
+ await first?.updateComplete;
386
+ first?.focusItem();
387
+ });
176
388
  }
177
389
  leave() {
390
+ if (this.flies) {
391
+ const holder = this.keyPage;
392
+ if (!holder) {
393
+ this.hide();
394
+ return;
395
+ }
396
+ this.openPage(this.holderOf(holder));
397
+ this.updateComplete.then(() => holder.focus());
398
+ return;
399
+ }
178
400
  if (this.drilldown.atRoot) {
179
401
  this.hide();
180
402
  return;
@@ -184,42 +406,107 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
184
406
  this.updateComplete.then(() => leaving?.focus());
185
407
  }
186
408
  handleClosedKeydown(event) {
409
+ if (this.context && event.shiftKey && event.key === 'F10' && this.fromTrigger(event)) {
410
+ event.preventDefault();
411
+ const box = this.trigger.getBoundingClientRect();
412
+ this.openContextAt({ x: box.left, y: box.top });
413
+ return;
414
+ }
187
415
  if (this.context || (event.key !== 'ArrowDown' && event.key !== 'ArrowUp')) {
188
416
  return;
189
417
  }
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))) {
418
+ if (this.pressHeld !== event.altKey || !this.fromTrigger(event)) {
193
419
  return;
194
420
  }
195
421
  event.preventDefault();
196
422
  this.openAt(event.key === 'ArrowDown' ? 'first' : 'last');
197
423
  }
424
+ fromTrigger(event) {
425
+ const slot = this.renderRoot?.querySelector('slot[name="trigger"]');
426
+ const triggers = slot?.assignedElements({ flatten: true }) ?? [];
427
+ return event.composedPath().some((node) => triggers.includes(node));
428
+ }
429
+ describeTrigger(light = false) {
430
+ const slot = this.renderRoot?.querySelector('slot[name="trigger"]');
431
+ const trigger = slot?.assignedElements({ flatten: true })[0];
432
+ const control = this.pressHeld && trigger instanceof HTMLElement ? focusTarget(trigger) : undefined;
433
+ if (this.described && this.described !== control) {
434
+ unlinkDescription(this.described, this);
435
+ this.described = undefined;
436
+ }
437
+ if (control && control !== this.described && linkDescription(control, this, light)) {
438
+ this.described = control;
439
+ }
440
+ this.internals.ariaLabel = this.described ? this.longPressLabel : null;
441
+ }
442
+ stopHover() {
443
+ clearTimeout(this.hoverTimer);
444
+ this.hoverTimer = undefined;
445
+ this.hovered = undefined;
446
+ this.grace = undefined;
447
+ }
448
+ inGrace(event) {
449
+ const grace = this.grace;
450
+ return (grace !== undefined &&
451
+ Date.now() < grace.until &&
452
+ inTriangle(event.clientX, event.clientY, grace.points));
453
+ }
198
454
  connectedCallback() {
199
455
  super.connectedCallback();
456
+ this.addEventListener('pointermove', this.handleHover);
457
+ this.addEventListener('pointerout', this.handleRowOut);
200
458
  this.addEventListener('helix-select', this.handleSelect, true);
201
459
  this.addEventListener('keydown', this.handleKeydown);
202
460
  this.addEventListener('focusin', this.handleFocusIn);
203
461
  this.addEventListener('contextmenu', this.handleContextMenu);
462
+ this.addEventListener('pointerdown', this.handlePointerDown);
463
+ this.addEventListener('pointermove', this.handlePointerMove);
464
+ this.addEventListener('pointerup', this.handlePointerUp);
465
+ this.addEventListener('pointercancel', this.handlePointerCancel);
466
+ this.addEventListener('click', this.handleHeldClick, true);
467
+ this.addEventListener('focusin', this.handleTriggerFocusIn);
468
+ void this.whenSettled.then(() => {
469
+ this.settledOnce = true;
470
+ if (!this.context) {
471
+ this.tellTrigger();
472
+ }
473
+ });
204
474
  }
205
475
  disconnectedCallback() {
206
476
  super.disconnectedCallback();
477
+ this.removeEventListener('pointermove', this.handleHover);
478
+ this.removeEventListener('pointerout', this.handleRowOut);
479
+ this.stopHover();
207
480
  this.removeEventListener('helix-select', this.handleSelect, true);
208
481
  this.removeEventListener('keydown', this.handleKeydown);
209
482
  this.removeEventListener('focusin', this.handleFocusIn);
210
483
  this.removeEventListener('contextmenu', this.handleContextMenu);
484
+ this.removeEventListener('pointerdown', this.handlePointerDown);
485
+ this.removeEventListener('pointermove', this.handlePointerMove);
486
+ this.removeEventListener('pointerup', this.handlePointerUp);
487
+ this.removeEventListener('pointercancel', this.handlePointerCancel);
488
+ this.removeEventListener('click', this.handleHeldClick, true);
489
+ this.removeEventListener('focusin', this.handleTriggerFocusIn);
490
+ if (this.described) {
491
+ unlinkDescription(this.described, this);
492
+ this.described = undefined;
493
+ }
211
494
  }
212
495
  updated(changed) {
213
496
  super.updated(changed);
214
497
  if (!this.context) {
215
498
  this.tellTrigger();
216
499
  }
500
+ if (changed.has('openOn') || changed.has('context') || this.described) {
501
+ this.describeTrigger();
502
+ }
503
+ const top = this.flies || this.drilldown.atRoot;
217
504
  for (const child of this.children) {
218
505
  if (isSection(child)) {
219
- child.onPage = this.drilldown.atRoot;
506
+ child.onPage = top;
220
507
  for (const row of child.children) {
221
508
  if (row.isMenuItem === true) {
222
- this.mark(row, this.drilldown.atRoot);
509
+ this.mark(row, top);
223
510
  }
224
511
  }
225
512
  continue;
@@ -228,9 +515,9 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
228
515
  if (item.isMenuItem !== true) {
229
516
  continue;
230
517
  }
231
- this.mark(item, this.drilldown.atRoot);
518
+ this.mark(item, top);
232
519
  }
233
- this.toggleState('drilled', !this.drilldown.atRoot);
520
+ this.toggleState('drilled', !this.flies && !this.drilldown.atRoot);
234
521
  this.markColumns();
235
522
  this.nameSurface();
236
523
  }
@@ -249,13 +536,26 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
249
536
  surface.ariaLabelledByElements = trigger && !named ? [trigger] : null;
250
537
  }
251
538
  markColumns() {
252
- const rows = this.drilldown.rows;
539
+ const rows = this.flies ? this.topRows : this.drilldown.rows;
253
540
  this.toggleState('start', rows.some((row) => row.querySelector(':scope > [slot="start"]') !== null));
254
541
  this.toggleState('mark', rows.some((row) => row.checkable || row.checked || this.drilldown.hasPage(row)));
255
542
  }
256
543
  mark(item, onPage) {
257
544
  const page = this.drilldown.page;
258
545
  item.onPage = onPage;
546
+ const holds = page !== null && item.contains(page);
547
+ item.flyout = this.flies;
548
+ item.expanded = this.flies && holds;
549
+ if (this.flies) {
550
+ item.showingSubmenu = false;
551
+ for (const child of item.children) {
552
+ const nested = child;
553
+ if (nested.isMenuItem === true && child.slot === 'submenu') {
554
+ this.mark(nested, onPage && holds);
555
+ }
556
+ }
557
+ return;
558
+ }
259
559
  item.showingSubmenu = page !== null && item.contains(page);
260
560
  for (const child of item.children) {
261
561
  const nested = child;
@@ -279,10 +579,10 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
279
579
  role="menu"
280
580
  aria-busy=${this.loading ? 'true' : nothing}
281
581
  aria-label=${this.label || nothing}
282
- data-drilled=${this.drilldown.atRoot ? nothing : ''}
582
+ data-drilled=${this.flies || this.drilldown.atRoot ? nothing : ''}
283
583
  ?hidden=${!this.open}
284
584
  >
285
- ${this.drilldown.atRoot
585
+ ${this.flies || this.drilldown.atRoot
286
586
  ? nothing
287
587
  : html `<div part="header">
288
588
  <button
@@ -314,12 +614,22 @@ __decorate([
314
614
  __decorate([
315
615
  property()
316
616
  ], HelixHeadlessMenu.prototype, "label", void 0);
617
+ __decorate([
618
+ words,
619
+ property({ attribute: 'long-press-label' })
620
+ ], HelixHeadlessMenu.prototype, "longPressLabel", void 0);
317
621
  __decorate([
318
622
  property({ type: Number })
319
623
  ], HelixHeadlessMenu.prototype, "offset", void 0);
624
+ __decorate([
625
+ property({ attribute: 'open-on' })
626
+ ], HelixHeadlessMenu.prototype, "openOn", void 0);
320
627
  __decorate([
321
628
  property()
322
629
  ], HelixHeadlessMenu.prototype, "placement", void 0);
630
+ __decorate([
631
+ property()
632
+ ], HelixHeadlessMenu.prototype, "submenus", void 0);
323
633
  HelixHeadlessMenu = __decorate([
324
634
  customElement('helix-headless-menu')
325
635
  ], 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' })
@@ -25,9 +25,9 @@ declare const HelixHeadlessNumberInput_base: (new (...args: any[]) => import("@f
25
25
  * The locale's group separators are read in every field, so `1,234` is a
26
26
  * number in `en-US`.
27
27
  *
28
- * The field and its two buttons sit in a `role="group"`, and the field carries
29
- * `role-label`, "number field" in English, as its `aria-roledescription`, as
30
- * React Aria's NumberField does. On iOS and iPadOS the field is a plain text
28
+ * The field and its two buttons sit in a `role="group"` named by the label,
29
+ * and the field carries `role-label`, "number field" in English, as its
30
+ * `aria-roledescription`, as React Aria's NumberField does. On iOS and iPadOS the field is a plain text
31
31
  * field, with no `role="spinbutton"`, which VoiceOver reads as a slider you
32
32
  * cannot type in, and no role description, so VoiceOver says the field is
33
33
  * required. React Aria's NumberField does the same.
@@ -492,8 +492,11 @@ export declare class HelixHeadlessNumberInput extends HelixHeadlessNumberInput_b
492
492
  protected renderIncrementIcon(): TemplateResult;
493
493
  protected render(): TemplateResult;
494
494
  /**
495
- * The value as a number, or `NaN` while the control is empty.
495
+ * The value as a number, or `NaN` while the control is empty or does not
496
+ * hold a number.
496
497
  *
498
+ * Read in the locale and the format, as the form reads it, so `1,5` typed
499
+ * in a locale with a decimal comma is 1.5 here while the form holds 1.5.
497
500
  * `NaN` and not zero, which is the trap the platform already avoids: `Number('')`
498
501
  * is zero, so an empty control would report a number it does not hold and a
499
502
  * form would submit it.