@fusion.dev/helix-headless 0.170.0 → 0.172.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 (57) hide show
  1. package/custom-elements.json +1045 -154
  2. package/dist/chat/composer/chat-composer.d.ts +30 -2
  3. package/dist/chat/composer/chat-composer.js +29 -4
  4. package/dist/chat/root/chat-root.d.ts +73 -7
  5. package/dist/chat/root/chat-root.js +90 -5
  6. package/dist/code-block/root/code-block-root.d.ts +20 -2
  7. package/dist/code-block/root/code-block-root.js +29 -8
  8. package/dist/color-input/root/color-input-root.d.ts +15 -1
  9. package/dist/color-input/root/color-input-root.js +19 -1
  10. package/dist/color-picker/root/color-picker-root.d.ts +54 -7
  11. package/dist/color-picker/root/color-picker-root.js +62 -1
  12. package/dist/combobox/root/combobox-root.d.ts +16 -6
  13. package/dist/combobox/root/combobox-root.js +9 -2
  14. package/dist/command/root/command-root.d.ts +16 -1
  15. package/dist/command/root/command-root.js +8 -0
  16. package/dist/data-table/root/data-table-root.d.ts +5 -1
  17. package/dist/data-table/root/data-table-root.js +1 -0
  18. package/dist/dialog/root/dialog-root.js +1 -0
  19. package/dist/drawer/root/drawer-root.d.ts +4 -0
  20. package/dist/drawer/root/drawer-root.js +1 -0
  21. package/dist/element-events.generated.js +1 -0
  22. package/dist/filter-input/chip/filter-chip.d.ts +13 -3
  23. package/dist/filter-input/chip/filter-chip.js +5 -0
  24. package/dist/filter-input/menu/filter-menu.js +0 -1
  25. package/dist/filter-input/root/filter-input-root.d.ts +71 -4
  26. package/dist/filter-input/root/filter-input-root.js +87 -2
  27. package/dist/format-date/root/format-date-root.d.ts +3 -1
  28. package/dist/input/root/input-root.d.ts +6 -0
  29. package/dist/input/root/input-root.js +13 -0
  30. package/dist/lightbox/root/lightbox-root.js +1 -0
  31. package/dist/listbox/root/listbox-root.d.ts +8 -1
  32. package/dist/listbox/root/listbox-root.js +9 -3
  33. package/dist/menu/item/menu-item.d.ts +2 -0
  34. package/dist/menu/item/menu-item.js +2 -0
  35. package/dist/menu/root/menu-root.d.ts +73 -12
  36. package/dist/menu/root/menu-root.js +103 -7
  37. package/dist/number-input/root/number-input-root.d.ts +16 -6
  38. package/dist/number-input/root/number-input-root.js +15 -4
  39. package/dist/popover/root/popover-root.d.ts +59 -6
  40. package/dist/popover/root/popover-root.js +51 -3
  41. package/dist/select/root/select-root.d.ts +2 -2
  42. package/dist/select/root/select-root.js +4 -2
  43. package/dist/steps/root/steps-root.d.ts +32 -8
  44. package/dist/steps/root/steps-root.js +70 -9
  45. package/dist/steps/step/step.d.ts +9 -1
  46. package/dist/steps/step/step.js +5 -0
  47. package/dist/textarea/root/textarea-root.d.ts +8 -0
  48. package/dist/textarea/root/textarea-root.js +16 -0
  49. package/dist/video-playlist/root/video-playlist-root.d.ts +19 -1
  50. package/dist/video-playlist/root/video-playlist-root.js +14 -3
  51. package/dist/virtual-list/root/virtual-list-root.d.ts +8 -0
  52. package/dist/virtual-list/root/virtual-list-root.js +7 -2
  53. package/lint.json +1 -1
  54. package/package.json +5 -5
  55. package/types/jsx.d.ts +800 -144
  56. package/types/vue.d.ts +165 -31
  57. package/types/words.d.ts +20 -2
@@ -4,7 +4,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
4
4
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
- import { AriaForwardController, anchorElement, customElement, DrilldownController, deepActiveElement, deepContains, filterKeeps, focusTarget, HelixElement, HelixFilterEvent, inlineStep, KeyboardNavigationController, linkDescription, MixinCollection, MixinControllerOpen, markMatches, onUnlessFalse, PopoverController, scrollItemIntoView, sideOf, slottedWords, tellDisclosure, typeaheadBy, unlinkDescription, words, } from '@fusion.dev/helix-core';
7
+ import { AriaForwardController, anchorElement, customElement, DrilldownController, deepActiveElement, deepContains, filterKeeps, focusTarget, formatWords, HelixElement, HelixFilterEvent, inlineStep, KeyboardNavigationController, linkDescription, MixinCollection, MixinControllerOpen, markMatches, OpenPhaseController, onUnlessFalse, 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';
@@ -30,6 +30,7 @@ const isSection = (element) => 'isMenuSection' in element && element.isMenuSecti
30
30
  let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinControllerOpen(HelixElement)) {
31
31
  constructor() {
32
32
  super(...arguments);
33
+ this.statusSlots = true;
33
34
  this.backLabel = 'Back';
34
35
  this.closeOnSelect = true;
35
36
  this.context = false;
@@ -40,6 +41,7 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
40
41
  this.longPressLabel = 'Long press or press Alt+Down to show the menu';
41
42
  this.noResultsText = 'No matches';
42
43
  this.offset = 4;
44
+ this.openOn = 'click';
43
45
  this.longPress = new LongPress(this, {
44
46
  onLongPress: () => {
45
47
  if (this.context) {
@@ -54,8 +56,8 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
54
56
  }
55
57
  },
56
58
  });
57
- this.openOn = 'click';
58
59
  this.placement = 'bottom-start';
60
+ this.resultsLabel = '{count, plural, one {# result} other {# results}}';
59
61
  this.submenus = 'drill';
60
62
  this.drilldown = new DrilldownController(this, {
61
63
  isRow: (element) => 'isMenuItem' in element && element.isMenuItem === true,
@@ -70,6 +72,14 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
70
72
  selects: () => false,
71
73
  typeahead: typeaheadBy((item) => slottedWords(item, [''])),
72
74
  });
75
+ this.phases = new OpenPhaseController(this, {
76
+ mark: {
77
+ entering: (on) => this.toggleState('entering', on),
78
+ exiting: (on) => this.toggleState('exiting', on),
79
+ },
80
+ open: () => this.open,
81
+ surface: () => this.partNamed('surface'),
82
+ });
73
83
  this.aria = new AriaForwardController(this, {
74
84
  label: true,
75
85
  target: () => this.menuBox,
@@ -98,6 +108,37 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
98
108
  event.stopPropagation();
99
109
  this.filterBy(event.target.value);
100
110
  };
111
+ this.followRemoved = (records) => {
112
+ if (!this.open) {
113
+ return;
114
+ }
115
+ const active = deepActiveElement();
116
+ const lost = active === null || active === document.body || !active.isConnected;
117
+ const record = records.find((one) => [...one.removedNodes].some((node) => isItem(node) && (this.filterable ? node === this.highlighted : lost)));
118
+ if (!record) {
119
+ return;
120
+ }
121
+ const items = this.items;
122
+ const after = (from, step) => {
123
+ for (let node = from; node; node = node[step]) {
124
+ if (items.includes(node)) {
125
+ return node;
126
+ }
127
+ }
128
+ return undefined;
129
+ };
130
+ const next = after(record.nextSibling, 'nextSibling') ??
131
+ after(record.previousSibling, 'previousSibling') ??
132
+ items.at(-1);
133
+ if (this.filterable || next) {
134
+ this.reach(next);
135
+ return;
136
+ }
137
+ this.hide();
138
+ if (!this.context) {
139
+ this.trigger.focus();
140
+ }
141
+ };
101
142
  this.handleContextMenu = (event) => {
102
143
  if (!this.context) {
103
144
  return;
@@ -124,6 +165,7 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
124
165
  return;
125
166
  }
126
167
  const chosen = item;
168
+ this.tellChecked();
127
169
  if (chosen.hasSubmenu) {
128
170
  event.stopPropagation();
129
171
  this.enter(chosen);
@@ -141,6 +183,7 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
141
183
  if (item) {
142
184
  this.revealRow(item);
143
185
  }
186
+ this.nameSurface();
144
187
  };
145
188
  this.handleKeydown = (event) => {
146
189
  if (!this.open) {
@@ -235,7 +278,7 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
235
278
  }
236
279
  };
237
280
  this.handleHover = (event) => {
238
- if (!(this.flies || this.filterable) || !this.open || event.pointerType !== 'mouse') {
281
+ if (!this.open || event.pointerType !== 'mouse') {
239
282
  return;
240
283
  }
241
284
  const row = event
@@ -293,6 +336,7 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
293
336
  until: Date.now() + GRACE,
294
337
  };
295
338
  };
339
+ this.namedByPage = false;
296
340
  }
297
341
  static { this.words = 'helix-menu'; }
298
342
  get flies() {
@@ -487,6 +531,27 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
487
531
  this.drilldown.back();
488
532
  this.updateComplete.then(() => this.reach(leaving ?? undefined));
489
533
  }
534
+ get choiceRows() {
535
+ return [...this.querySelectorAll('*')].filter((node) => isItem(node) && node.choice);
536
+ }
537
+ get checkedValues() {
538
+ return this.choiceRows.filter((row) => row.checked).map((row) => row.value);
539
+ }
540
+ set checkedValues(values) {
541
+ const wanted = new Set(values);
542
+ for (const row of this.choiceRows) {
543
+ row.checked = wanted.has(row.value);
544
+ }
545
+ this.toldChecked = this.checkedValues.join('\u0000');
546
+ }
547
+ tellChecked() {
548
+ const now = this.checkedValues.join('\u0000');
549
+ const before = this.toldChecked;
550
+ this.toldChecked = now;
551
+ if (before !== undefined && before !== now) {
552
+ this.dispatchEvent(new Event('change', { bubbles: true }));
553
+ }
554
+ }
490
555
  revealRow(item) {
491
556
  const holder = this.flies ? this.holderOf(item) : null;
492
557
  const surface = holder
@@ -555,6 +620,8 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
555
620
  this.addEventListener('pointercancel', this.handlePointerCancel);
556
621
  this.addEventListener('click', this.handleHeldClick, true);
557
622
  this.addEventListener('focusin', this.handleTriggerFocusIn);
623
+ this.rowWatch = new MutationObserver(this.followRemoved);
624
+ this.rowWatch.observe(this, { childList: true, subtree: true });
558
625
  void this.whenSettled.then(() => {
559
626
  this.settledOnce = true;
560
627
  if (!this.context) {
@@ -581,6 +648,8 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
581
648
  this.removeEventListener('pointercancel', this.handlePointerCancel);
582
649
  this.removeEventListener('click', this.handleHeldClick, true);
583
650
  this.removeEventListener('focusin', this.handleTriggerFocusIn);
651
+ this.rowWatch?.disconnect();
652
+ this.rowWatch = undefined;
584
653
  if (this.described) {
585
654
  unlinkDescription(this.described, this);
586
655
  this.described = undefined;
@@ -588,6 +657,9 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
588
657
  }
589
658
  updated(changed) {
590
659
  super.updated(changed);
660
+ if (changed.has('open') && this.open) {
661
+ this.toldChecked = this.checkedValues.join('\u0000');
662
+ }
591
663
  if (!this.context) {
592
664
  this.tellTrigger();
593
665
  }
@@ -628,7 +700,20 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
628
700
  }
629
701
  nameSurface() {
630
702
  const surface = this.menuBox;
631
- if (!surface || !('ariaLabelledByElements' in surface)) {
703
+ if (!surface) {
704
+ return;
705
+ }
706
+ if (!this.flies && !this.drilldown.atRoot && this.partNamed('title')) {
707
+ surface.setAttribute('aria-labelledby', 'title');
708
+ this.namedByPage = true;
709
+ return;
710
+ }
711
+ if (this.namedByPage) {
712
+ this.namedByPage = false;
713
+ surface.removeAttribute('aria-labelledby');
714
+ this.aria.forward();
715
+ }
716
+ if (!('ariaLabelledByElements' in surface)) {
632
717
  return;
633
718
  }
634
719
  if (this.hasAttribute('aria-labelledby')) {
@@ -673,8 +758,15 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
673
758
  render() {
674
759
  const point = this.context && this.point;
675
760
  const filtering = this.filtering;
676
- const none = filtering && !this.rowsOf(this.filterPage).some((row) => this.kept(row));
677
- this.announced = none ? this.noResultsText : '';
761
+ const count = filtering
762
+ ? this.rowsOf(this.filterPage).filter((row) => this.kept(row)).length
763
+ : 0;
764
+ const none = filtering && count === 0;
765
+ this.announced = none
766
+ ? this.noResultsText
767
+ : filtering
768
+ ? formatWords(this, 'resultsLabel', { count })
769
+ : '';
678
770
  const rows = html `${this.flies || this.drilldown.atRoot
679
771
  ? nothing
680
772
  : html `<div part="header">
@@ -690,7 +782,7 @@ let HelixHeadlessMenu = class HelixHeadlessMenu extends MixinCollection(MixinCon
690
782
  ${unsafeSVG(caretLeft.content)}
691
783
  </svg>
692
784
  </button>
693
- <span part="title">${this.drilldown.label}</span>
785
+ <span part="title" id="title">${this.drilldown.label}</span>
694
786
  </div>`}
695
787
  <slot @slotchange=${(event) => this.countRows(event)}></slot>
696
788
  ${none
@@ -775,6 +867,10 @@ __decorate([
775
867
  __decorate([
776
868
  property()
777
869
  ], HelixHeadlessMenu.prototype, "placement", void 0);
870
+ __decorate([
871
+ words,
872
+ property({ attribute: 'results-label' })
873
+ ], HelixHeadlessMenu.prototype, "resultsLabel", void 0);
778
874
  __decorate([
779
875
  property()
780
876
  ], HelixHeadlessMenu.prototype, "submenus", void 0);
@@ -124,7 +124,9 @@ export declare class HelixHeadlessNumberInput extends HelixHeadlessNumberInput_b
124
124
  */
125
125
  badInputMessage: string;
126
126
  /**
127
- * The name of the step down button, read by screen readers.
127
+ * The name of the step down button, read by screen readers. The field's
128
+ * label follows it, so a reader hears "Decrease Quantity", as React Aria
129
+ * names its steppers.
128
130
  */
129
131
  decrementLabel: string;
130
132
  /**
@@ -138,7 +140,9 @@ export declare class HelixHeadlessNumberInput extends HelixHeadlessNumberInput_b
138
140
  */
139
141
  formatOptions?: Intl.NumberFormatOptions;
140
142
  /**
141
- * The name of the step up button, read by screen readers.
143
+ * The name of the step up button, read by screen readers. The field's
144
+ * label follows it, so a reader hears "Increase Quantity", as React Aria
145
+ * names its steppers.
142
146
  */
143
147
  incrementLabel: string;
144
148
  /**
@@ -321,16 +325,22 @@ export declare class HelixHeadlessNumberInput extends HelixHeadlessNumberInput_b
321
325
  * refused step, or one pinned at a bound, finished nothing.
322
326
  */
323
327
  private land;
324
- /** Steps the value up, by `largeStep` steps when `large` is set. */
325
- stepUp(large?: boolean): void;
326
328
  /**
327
- * Steps the value down, by `largeStep` steps when `large` is set.
329
+ * Steps the value up by `step`, `count` times, as the native method and
330
+ * `Input.stepUp` do. `true` takes `largeStep` steps, as `PageUp` does.
331
+ */
332
+ stepUp(count?: number | boolean): void;
333
+ /**
334
+ * Steps the value down by `step`, `count` times. `true` takes `largeStep`
335
+ * steps, as `PageDown` does.
328
336
  *
329
337
  * The same path a press of the down arrow takes, so a consumer driving this
330
338
  * from their own control gets the clamping and the announcement rather than
331
339
  * having to repeat both.
332
340
  */
333
- stepDown(large?: boolean): void;
341
+ stepDown(count?: number | boolean): void;
342
+ /** How many steps a `stepUp` argument asks for: a count, or `largeStep` for `true`. */
343
+ private stepsOf;
334
344
  /** The timer of a held stepper button. */
335
345
  private repeat;
336
346
  /** Whether a pointer press stepped already, so the click that ends it does not step again. */
@@ -324,11 +324,17 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
324
324
  this.announceChange();
325
325
  }
326
326
  }
327
- stepUp(large = false) {
328
- this.nudge(large ? (this.largeStep ?? 10) : 1);
327
+ stepUp(count = 1) {
328
+ this.nudge(this.stepsOf(count));
329
329
  }
330
- stepDown(large = false) {
331
- this.nudge(large ? -(this.largeStep ?? 10) : -1);
330
+ stepDown(count = 1) {
331
+ this.nudge(-this.stepsOf(count));
332
+ }
333
+ stepsOf(count) {
334
+ if (typeof count === 'boolean') {
335
+ return count ? (this.largeStep ?? 10) : 1;
336
+ }
337
+ return Number.isFinite(count) ? Math.trunc(count) : 1;
332
338
  }
333
339
  disconnectedCallback() {
334
340
  super.disconnectedCallback();
@@ -342,6 +348,7 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
342
348
  return html `<label
343
349
  part="label"
344
350
  for=${this.controlId}
351
+ id=${this.labelId}
345
352
  ?data-hidden=${hidden !== null}
346
353
  @pointercancel=${this.handleScrubEnd}
347
354
  @pointerdown=${this.handleScrubDown}
@@ -455,8 +462,10 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
455
462
  <div part="field" role="group" aria-labelledby=${this.labeled ? this.labelId : nothing}>
456
463
  <button
457
464
  part="decrement"
465
+ id="decrement"
458
466
  type="button"
459
467
  aria-label=${this.decrementLabel}
468
+ aria-labelledby=${this.labeled ? `decrement ${this.labelId}` : nothing}
460
469
  tabindex="-1"
461
470
  ?disabled=${this.isDisabled || this.readOnly || atMin}
462
471
  @click=${this.handleStepperClick}
@@ -511,8 +520,10 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
511
520
  <slot name="end"></slot>
512
521
  <button
513
522
  part="increment"
523
+ id="increment"
514
524
  type="button"
515
525
  aria-label=${this.incrementLabel}
526
+ aria-labelledby=${this.labeled ? `increment ${this.labelId}` : nothing}
516
527
  tabindex="-1"
517
528
  ?disabled=${this.isDisabled || this.readOnly || atMax}
518
529
  @click=${this.handleStepperClick}
@@ -22,7 +22,9 @@ declare const HelixHeadlessPopover_base: (new (...args: any[]) => import("@fusio
22
22
  *
23
23
  * It opens on click and from a command. `command="--show"` and the related
24
24
  * commands work as they do on `Dialog` and `Drawer`, so a button anywhere on
25
- * the page can open the popover with no script.
25
+ * the page can open the popover with no script. From its first press, a
26
+ * button that opens it this way says whether the popover is open: a Helix
27
+ * button through `expanded`, and any other element with `aria-expanded`.
26
28
  *
27
29
  * **Opening moves focus into the panel**, to the first control in it, or to the
28
30
  * panel itself where it holds none, and closing from inside gives it back to
@@ -32,8 +34,12 @@ declare const HelixHeadlessPopover_base: (new (...args: any[]) => import("@fusio
32
34
  * `modal` keeps `Tab` inside the panel while it is open and says so with
33
35
  * `aria-modal`, for a panel that has to be answered before the page goes on.
34
36
  * The rest of the page is hidden from screen readers with `aria-hidden` while
35
- * it is open, so the virtual cursor stays in the panel too. Without it the
36
- * popover does not trap focus.
37
+ * it is open, so the virtual cursor stays in the panel too, and the page does
38
+ * not scroll behind it. Without it the popover does not trap focus.
39
+ *
40
+ * A press outside closes the panel and leaves focus where the press put it.
41
+ * Where it put focus nowhere, as a press on text or on the page's background
42
+ * does, focus goes back to the trigger.
37
43
  *
38
44
  * The panel's name is `label` when you set it. Otherwise it is the first
39
45
  * heading in the panel, or the elements an `aria-labelledby` on the host names.
@@ -49,10 +55,17 @@ declare const HelixHeadlessPopover_base: (new (...args: any[]) => import("@fusio
49
55
  * | Escape | Closes the panel and puts focus back on the trigger. |
50
56
  * | Tab, Shift+Tab | Moves through the controls in the panel. Leaving the panel and the trigger closes it. With `modal`, moves from the last control round to the first and from the first round to the last. |
51
57
  *
58
+ * The panel opens against the trigger, or against the element `anchor` names,
59
+ * such as a field beside the button that opens it. `offset` moves it away from
60
+ * that element, `cross-offset` slides it along the side it is on,
61
+ * `container-padding` keeps it that far from the window's edges, and
62
+ * `flip="false"` keeps it on its side where the other side has more room.
63
+ *
52
64
  * `arrow` draws a point on the panel's edge facing the trigger. The side the
53
- * panel landed on is on the host as `:state(placed-top)` and its kin, and the
54
- * trigger's width is on the panel as `--floating-anchor-inline-size`, for a
55
- * panel as wide as its trigger:
65
+ * panel landed on is on the host as `:state(placed-top)` and its kin, its
66
+ * alignment along that side as `:state(aligned-start)` or
67
+ * `:state(aligned-end)`, and the trigger's width is on the panel as
68
+ * `--floating-anchor-inline-size`, for a panel as wide as its trigger:
56
69
  *
57
70
  * ```css
58
71
  * helix-popover::part(popover) {
@@ -84,6 +97,8 @@ declare const HelixHeadlessPopover_base: (new (...args: any[]) => import("@fusio
84
97
  * @cssproperty --floating-anchor-inline-size - The trigger's width, written on the panel while it is open, as in `min-inline-size: var(--floating-anchor-inline-size)`. Read it; a value set here is written over.
85
98
  * @cssproperty --floating-available-block-size - The room between the panel and the edge of the screen it opens toward, written on the panel while it is open. Read it; a value set here is written over.
86
99
  *
100
+ * @cssstate aligned-end - While the panel is aligned to the end of the side it sits on.
101
+ * @cssstate aligned-start - While the panel is aligned to the start of the side it sits on.
87
102
  * @cssstate entering - From the moment the panel shows until its entrance has finished.
88
103
  * @cssstate exiting - From the moment the panel hides until its exit has finished.
89
104
  * @cssstate open - While the popover is open.
@@ -98,6 +113,13 @@ export declare class HelixHeadlessPopover extends HelixHeadlessPopover_base {
98
113
  * `registerWords('de', { 'helix-popover': { fallbackLabel: 'Popup' } })`.
99
114
  */
100
115
  static words: string;
116
+ /**
117
+ * The element the panel opens against, where that is not the trigger: its
118
+ * `id` in the same document or shadow root, or the element itself. Left
119
+ * empty, the panel opens against the trigger, or against the button whose
120
+ * command opened it.
121
+ */
122
+ anchor: Element | string;
101
123
  /**
102
124
  * Draws a point on the panel's edge facing the trigger.
103
125
  *
@@ -105,11 +127,29 @@ export declare class HelixHeadlessPopover extends HelixHeadlessPopover_base {
105
127
  * trigger already reads as belonging to it.
106
128
  */
107
129
  arrow: boolean;
130
+ /**
131
+ * How close to the window's edges the panel may come, in pixels, when it
132
+ * slides, shrinks or moves to the other side to fit. Left unset, it slides
133
+ * and shrinks to 8 pixels from an edge and moves to the other side only
134
+ * once it would cross one.
135
+ */
136
+ containerPadding?: number;
137
+ /**
138
+ * How far to slide the panel along the side it is on, in pixels: to the
139
+ * right on the top or the bottom, and down on the left or the right.
140
+ */
141
+ crossOffset: number;
108
142
  /**
109
143
  * The panel's name where nothing else names it: no `label`, no heading in
110
144
  * the panel, and no `aria-labelledby` on the host.
111
145
  */
112
146
  fallbackLabel: string;
147
+ /**
148
+ * Whether the panel moves to the other side of the trigger when its own
149
+ * side has no room. On by default. Write `flip="false"` to keep it on its
150
+ * side, where it shrinks to fit, as React Aria's `shouldFlip` turns off.
151
+ */
152
+ flip: boolean;
113
153
  /**
114
154
  * The panel's name, read by screen readers. Left empty, the first heading in
115
155
  * the panel names it, and then `fallback-label`.
@@ -166,7 +206,20 @@ export declare class HelixHeadlessPopover extends HelixHeadlessPopover_base {
166
206
  private invoker;
167
207
  protected readonly commands: CommandController;
168
208
  /** The element the panel opens against. */
209
+ private get placedAgainst();
210
+ /** The element that opened the panel, which focus goes back to. */
169
211
  private get trigger();
212
+ /**
213
+ * A press outside, or Escape, closing the panel.
214
+ *
215
+ * **Focus goes back to the trigger where the press put it nowhere.** The
216
+ * press is heard on `pointerdown`, and the browser moves focus a moment
217
+ * later: onto what was pressed where that takes focus, and to the body where
218
+ * it does not. A popover dismissed by a press on text was left with focus on
219
+ * the body, measured in Chromium, where React Aria's returns it to the
220
+ * trigger. Read after the press, once the browser has moved it.
221
+ */
222
+ private dismiss;
170
223
  /** Opens the panel. */
171
224
  show(): void;
172
225
  /** Closes the panel and puts focus back on the trigger. */
@@ -4,15 +4,18 @@ 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, arrowReach, auditName, CommandController, customElement, deepActiveElement, FocusScopeController, HelixElement, headingIn, MixinControllerOpen, OpenPhaseController, PopoverController, reflectElements, sideOf, tellDisclosure, words, } from '@fusion.dev/helix-core';
7
+ import { AriaForwardController, anchorElement, arrowReach, auditName, CommandController, customElement, deepActiveElement, FocusScopeController, HelixElement, headingIn, lockScroll, MixinControllerOpen, OpenPhaseController, onUnlessFalse, PopoverController, reflectElements, sideOf, tellDisclosure, unlockScroll, words, } from '@fusion.dev/helix-core';
8
8
  import { html, nothing } from 'lit';
9
9
  import { property, state } from 'lit/decorators.js';
10
10
  import { hideOutside } from '../hide-outside.js';
11
11
  let HelixHeadlessPopover = class HelixHeadlessPopover extends MixinControllerOpen(HelixElement) {
12
12
  constructor() {
13
13
  super(...arguments);
14
+ this.anchor = '';
14
15
  this.arrow = false;
16
+ this.crossOffset = 0;
15
17
  this.fallbackLabel = 'Popover';
18
+ this.flip = true;
16
19
  this.label = '';
17
20
  this.modal = false;
18
21
  this.aria = new AriaForwardController(this, {
@@ -29,13 +32,17 @@ let HelixHeadlessPopover = class HelixHeadlessPopover extends MixinControllerOpe
29
32
  surface: () => this.partNamed('popover'),
30
33
  });
31
34
  this.panel = new PopoverController(this, {
32
- anchor: () => this.trigger,
35
+ anchor: () => this.placedAgainst,
33
36
  arrow: () => this.partNamed('arrow'),
34
37
  boundary: () => [this, this.invoker],
38
+ crossOffset: () => this.crossOffset,
39
+ flip: () => this.flip,
35
40
  offset: () => this.offset + arrowReach(this.partNamed('arrow')),
36
- onDismiss: () => this.hide(),
41
+ onDismiss: () => this.dismiss(),
37
42
  onPlace: (placement) => {
38
43
  const side = sideOf(placement);
44
+ this.toggleState('aligned-end', placement.endsWith('-end'));
45
+ this.toggleState('aligned-start', placement.endsWith('-start'));
39
46
  this.toggleState('placed-bottom', side === 'bottom');
40
47
  this.toggleState('placed-left', side === 'left');
41
48
  this.toggleState('placed-right', side === 'right');
@@ -43,6 +50,7 @@ let HelixHeadlessPopover = class HelixHeadlessPopover extends MixinControllerOpe
43
50
  this.side = side;
44
51
  },
45
52
  open: () => this.open,
53
+ padding: () => this.containerPadding,
46
54
  placement: () => this.placement,
47
55
  surface: () => this.partNamed('popover'),
48
56
  });
@@ -69,6 +77,7 @@ let HelixHeadlessPopover = class HelixHeadlessPopover extends MixinControllerOpe
69
77
  }
70
78
  },
71
79
  },
80
+ expanded: () => this.open,
72
81
  });
73
82
  this.closingFromInside = false;
74
83
  this.handleFocusOut = (event) => {
@@ -111,10 +120,31 @@ let HelixHeadlessPopover = class HelixHeadlessPopover extends MixinControllerOpe
111
120
  const heading = asked ? null : headingIn(this.slotted);
112
121
  this.namedByHeading = reflectElements(this.partNamed('popover'), 'ariaLabelledByElements', heading ? [heading] : [], this.namedByHeading);
113
122
  }
123
+ get placedAgainst() {
124
+ const named = typeof this.anchor === 'string'
125
+ ? this.anchor === ''
126
+ ? null
127
+ : this.getRootNode().getElementById?.(this.anchor)
128
+ : this.anchor;
129
+ return named instanceof HTMLElement ? named : this.trigger;
130
+ }
114
131
  get trigger() {
115
132
  return (this.invoker ??
116
133
  anchorElement(this.renderRoot?.querySelector('slot[name="trigger"]'), this));
117
134
  }
135
+ dismiss() {
136
+ const inside = this.focusInside;
137
+ this.hide();
138
+ if (!inside) {
139
+ return;
140
+ }
141
+ setTimeout(() => {
142
+ const active = deepActiveElement();
143
+ if (!this.open && (active === null || active === document.body)) {
144
+ this.trigger.focus();
145
+ }
146
+ });
147
+ }
118
148
  show() {
119
149
  if (this.open || !this.requestOpen(true)) {
120
150
  return;
@@ -179,6 +209,12 @@ let HelixHeadlessPopover = class HelixHeadlessPopover extends MixinControllerOpe
179
209
  this.showOutside();
180
210
  this.showOutside = undefined;
181
211
  }
212
+ if (hide) {
213
+ lockScroll(this);
214
+ }
215
+ else {
216
+ unlockScroll(this);
217
+ }
182
218
  }
183
219
  updated(changed) {
184
220
  super.updated(changed);
@@ -220,13 +256,25 @@ let HelixHeadlessPopover = class HelixHeadlessPopover extends MixinControllerOpe
220
256
  </div>`;
221
257
  }
222
258
  };
259
+ __decorate([
260
+ property()
261
+ ], HelixHeadlessPopover.prototype, "anchor", void 0);
223
262
  __decorate([
224
263
  property({ type: Boolean })
225
264
  ], HelixHeadlessPopover.prototype, "arrow", void 0);
265
+ __decorate([
266
+ property({ attribute: 'container-padding', type: Number })
267
+ ], HelixHeadlessPopover.prototype, "containerPadding", void 0);
268
+ __decorate([
269
+ property({ attribute: 'cross-offset', type: Number })
270
+ ], HelixHeadlessPopover.prototype, "crossOffset", void 0);
226
271
  __decorate([
227
272
  words,
228
273
  property({ attribute: 'fallback-label' })
229
274
  ], HelixHeadlessPopover.prototype, "fallbackLabel", void 0);
275
+ __decorate([
276
+ property({ converter: onUnlessFalse })
277
+ ], HelixHeadlessPopover.prototype, "flip", void 0);
230
278
  __decorate([
231
279
  property()
232
280
  ], HelixHeadlessPopover.prototype, "label", void 0);
@@ -33,7 +33,7 @@ declare const HelixHeadlessSelect_base: (new (...args: any[]) => import("@fusion
33
33
  * | Down, Enter, Space | Opens the list on the chosen option, or the first. |
34
34
  * | Up | Opens the list on the chosen option, or the last. |
35
35
  * | Home, End | Opens the list on the first or the last option. |
36
- * | Left, Right | Chooses the option before or after the chosen one without opening, as React Aria's Select does, in either direction of text. |
36
+ * | Left, Right | Chooses the option before or after the chosen one without opening, as React Aria's Select does. On a right-to-left page Left chooses the one after. |
37
37
  * | A letter or a word | Chooses the option whose words start with what you typed, as a native `<select>` does. With `multiple`, opens the list on it. |
38
38
  *
39
39
  * In the open list, which is a `Listbox`:
@@ -371,7 +371,7 @@ export declare class HelixHeadlessSelect extends HelixHeadlessSelect_base {
371
371
  * middle of those letters is one of them. With `multiple`, a letter opens the
372
372
  * list on the match instead, because adding to a set is a choice the reader
373
373
  * makes in the list. ArrowLeft and ArrowRight choose the option before and
374
- * after the chosen one.
374
+ * after the chosen one, the other way round on a right-to-left page.
375
375
  */
376
376
  private readonly handleTriggerKeydown;
377
377
  /**
@@ -4,7 +4,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
4
4
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
- import { AriaForwardController, afterLinkFollowed, chosenValues, customElement, dataOptions, drawAutofill, drawStatus, HelixFormElement, hostAria, hostWords, isRtl, MixinControllerOpen, MixinControllerValue, MixinField, onApple, PopoverController, reportValues, StatusController, sideOf, typesCharacter, UpgradeController, words, wordsLocale, } from '@fusion.dev/helix-core';
7
+ import { AriaForwardController, afterLinkFollowed, chosenValues, customElement, dataOptions, drawAutofill, drawStatus, HelixFormElement, hostAria, hostWords, inlineStep, isRtl, MixinControllerOpen, MixinControllerValue, MixinField, onApple, PopoverController, reportValues, StatusController, sideOf, typesCharacter, UpgradeController, words, wordsLocale, } from '@fusion.dev/helix-core';
8
8
  import { caretDown } from '@fusion.dev/helix-icons';
9
9
  import { nothing } from 'lit';
10
10
  import { property } from 'lit/decorators.js';
@@ -131,7 +131,7 @@ let HelixHeadlessSelect = class HelixHeadlessSelect extends MixinControllerOpen(
131
131
  }
132
132
  return;
133
133
  }
134
- const step = event.key === 'ArrowRight' ? 1 : event.key === 'ArrowLeft' ? -1 : 0;
134
+ const step = inlineStep(this, event.key);
135
135
  if (step !== 0 && !this.multiple && !(event.altKey || event.ctrlKey || event.metaKey)) {
136
136
  event.preventDefault();
137
137
  const reachable = this.items
@@ -310,6 +310,8 @@ let HelixHeadlessSelect = class HelixHeadlessSelect extends MixinControllerOpen(
310
310
  }
311
311
  else {
312
312
  this.stopAligning();
313
+ this.toggleState('placed-bottom', false);
314
+ this.toggleState('placed-top', false);
313
315
  this.listbox?.requestFilter('');
314
316
  }
315
317
  }