@fusion.dev/helix-headless 0.171.0 → 0.173.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 (67) hide show
  1. package/custom-elements.json +1350 -124
  2. package/dist/button-group/root/button-group-root.d.ts +30 -0
  3. package/dist/button-group/root/button-group-root.js +50 -4
  4. package/dist/canvas/root/canvas-root.d.ts +7 -0
  5. package/dist/canvas/root/canvas-root.js +7 -0
  6. package/dist/chat/composer/chat-composer.d.ts +28 -1
  7. package/dist/chat/composer/chat-composer.js +22 -3
  8. package/dist/chat/root/chat-root.d.ts +38 -2
  9. package/dist/chat/root/chat-root.js +38 -1
  10. package/dist/color-input/root/color-input-root.d.ts +15 -1
  11. package/dist/color-input/root/color-input-root.js +19 -1
  12. package/dist/color-picker/root/color-picker-root.d.ts +5 -1
  13. package/dist/color-picker/root/color-picker-root.js +1 -1
  14. package/dist/copy-button/root/copy-button-root.d.ts +43 -3
  15. package/dist/copy-button/root/copy-button-root.js +49 -3
  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 +3 -0
  22. package/dist/graph/root/graph-root.d.ts +22 -0
  23. package/dist/graph/root/graph-root.js +21 -5
  24. package/dist/lightbox/root/lightbox-root.js +1 -0
  25. package/dist/list/root/list-root.d.ts +6 -1
  26. package/dist/list/root/list-root.js +3 -2
  27. package/dist/listbox/root/listbox-root.d.ts +2 -1
  28. package/dist/listbox/root/listbox-root.js +3 -2
  29. package/dist/menu/item/menu-item.d.ts +2 -0
  30. package/dist/menu/item/menu-item.js +2 -0
  31. package/dist/menu/root/menu-root.d.ts +73 -12
  32. package/dist/menu/root/menu-root.js +103 -7
  33. package/dist/outline/root/outline-root.d.ts +57 -0
  34. package/dist/outline/root/outline-root.js +98 -29
  35. package/dist/popover/root/popover-root.d.ts +59 -6
  36. package/dist/popover/root/popover-root.js +51 -3
  37. package/dist/select/root/select-root.d.ts +2 -2
  38. package/dist/select/root/select-root.js +2 -2
  39. package/dist/speed-dial/root/speed-dial-root.d.ts +14 -0
  40. package/dist/speed-dial/root/speed-dial-root.js +34 -2
  41. package/dist/splitter/root/splitter-root.d.ts +25 -2
  42. package/dist/splitter/root/splitter-root.js +19 -4
  43. package/dist/tag/group/tag-group.d.ts +8 -1
  44. package/dist/tag/group/tag-group.js +8 -0
  45. package/dist/tag/root/tag-root.d.ts +12 -0
  46. package/dist/tag/root/tag-root.js +12 -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/time-picker/root/time-picker-root.d.ts +14 -0
  50. package/dist/time-picker/root/time-picker-root.js +8 -1
  51. package/dist/timeline/item/timeline-item.d.ts +5 -1
  52. package/dist/timeline/item/timeline-item.js +9 -0
  53. package/dist/timeline/root/timeline-root.d.ts +23 -0
  54. package/dist/timeline/root/timeline-root.js +17 -3
  55. package/dist/tooltip/root/tooltip-root.d.ts +10 -0
  56. package/dist/tooltip/root/tooltip-root.js +14 -2
  57. package/dist/virtual-list/reorder/index.d.ts +1 -0
  58. package/dist/virtual-list/reorder/index.js +4 -0
  59. package/dist/virtual-list/reorder/virtual-list-reorder.d.ts +86 -0
  60. package/dist/virtual-list/reorder/virtual-list-reorder.js +193 -0
  61. package/dist/virtual-list/root/virtual-list-root.d.ts +49 -11
  62. package/dist/virtual-list/root/virtual-list-root.js +10 -1
  63. package/lint.json +1 -1
  64. package/package.json +5 -5
  65. package/types/jsx.d.ts +880 -48
  66. package/types/vue.d.ts +174 -19
  67. package/types/words.d.ts +22 -0
@@ -17,11 +17,14 @@ let HelixHeadlessOutline = class HelixHeadlessOutline extends HelixElement {
17
17
  active: () => this.links.find((link) => link.hasAttribute('aria-current')),
18
18
  indicator: () => this.partNamed('indicator'),
19
19
  surface: () => this.partNamed('nav'),
20
+ through: () => this.links.findLast((link) => link.hasAttribute('aria-current')),
20
21
  });
21
22
  this.behavior = 'auto';
22
23
  this.follow = false;
23
24
  this.label = 'On this page';
25
+ this.marks = 'reading';
24
26
  this.offset = 0;
27
+ this.markedIds = [];
25
28
  this.reading = '';
26
29
  this.frame = 0;
27
30
  this.handleScroll = () => {
@@ -42,34 +45,18 @@ let HelixHeadlessOutline = class HelixHeadlessOutline extends HelixElement {
42
45
  if (!section) {
43
46
  return;
44
47
  }
45
- this.landing = section;
46
- this.markTo(section);
47
- this.unland();
48
- const arrive = () => {
49
- this.unland();
50
- this.landing = undefined;
51
- this.mark();
52
- };
53
- this.arrive = arrive;
54
- document.addEventListener('scrollend', arrive, { capture: true, once: true });
55
- this.landed = setTimeout(arrive, 1200);
56
- if (this.behavior === 'auto') {
57
- return;
58
- }
59
- event.preventDefault();
60
- const reduced = globalThis.matchMedia?.('(prefers-reduced-motion: reduce)').matches === true;
61
- section.target.scrollIntoView({
62
- behavior: reduced && this.behavior === 'smooth' ? 'instant' : this.behavior,
63
- block: 'start',
64
- });
65
- globalThis.history?.pushState(null, '', link?.getAttribute('href') ?? '');
66
- const target = section.target;
67
- if (!target.hasAttribute('tabindex')) {
68
- target.setAttribute('tabindex', '-1');
48
+ if (this.travel(section, this.behavior !== 'auto')) {
49
+ event.preventDefault();
50
+ globalThis.history?.pushState(null, '', link?.getAttribute('href') ?? '');
51
+ const target = section.target;
52
+ if (!target.hasAttribute('tabindex')) {
53
+ target.setAttribute('tabindex', '-1');
54
+ }
55
+ target.focus({ preventScroll: true });
69
56
  }
70
- target.focus({ preventScroll: true });
71
57
  };
72
58
  this.handleSlotChange = () => {
59
+ this.writeDepths();
73
60
  this.watch();
74
61
  this.indicator.place();
75
62
  };
@@ -87,6 +74,9 @@ let HelixHeadlessOutline = class HelixHeadlessOutline extends HelixElement {
87
74
  get current() {
88
75
  return this.reading;
89
76
  }
77
+ get marked() {
78
+ return [...this.markedIds];
79
+ }
90
80
  get links() {
91
81
  const slots = [
92
82
  ...(this.renderRoot?.querySelectorAll?.('slot[data-link], nav > slot:not([role])') ?? []),
@@ -127,20 +117,34 @@ let HelixHeadlessOutline = class HelixHeadlessOutline extends HelixElement {
127
117
  const origin = page ? 0 : scroller.getBoundingClientRect().top;
128
118
  const passed = sections.filter(({ target }) => target.getBoundingClientRect().top - origin - this.offset <= 1);
129
119
  const active = this.atEnd ? sections.at(-1) : (passed.at(-1) ?? sections[0]);
130
- this.markTo(active);
120
+ const shown = this.marks === 'visible' ? this.onScreen(sections, origin, scroller) : [];
121
+ this.markTo(shown.length > 0 ? shown : active ? [active] : []);
131
122
  }
132
- markTo(active) {
123
+ onScreen(sections, origin, scroller) {
124
+ const line = origin + this.offset;
125
+ const bottom = origin + (scroller === this.page ? this.page.clientHeight : scroller.clientHeight);
126
+ const tops = sections.map(({ target }) => target.getBoundingClientRect().top);
127
+ return sections.filter((_, at) => {
128
+ const end = tops[at + 1] ?? Number.POSITIVE_INFINITY;
129
+ return (tops[at] ?? 0) < bottom - 1 && end > line + 1;
130
+ });
131
+ }
132
+ markTo(marked) {
133
+ const on = new Set(marked.map(({ target }) => target));
133
134
  for (const { link, target } of this.sections) {
134
- if (target === active?.target) {
135
+ if (on.has(target)) {
135
136
  link.setAttribute('aria-current', 'location');
136
137
  }
137
138
  else {
138
139
  link.removeAttribute('aria-current');
139
140
  }
140
141
  }
142
+ const active = marked[0];
141
143
  const next = active?.target.id ?? '';
142
- if (next !== this.reading) {
144
+ const ids = marked.map(({ target }) => target.id);
145
+ if (next !== this.reading || ids.join(' ') !== this.markedIds.join(' ')) {
143
146
  this.reading = next;
147
+ this.markedIds = ids;
144
148
  this.indicator.place();
145
149
  if (this.follow && active) {
146
150
  this.reveal(active.link);
@@ -173,6 +177,36 @@ let HelixHeadlessOutline = class HelixHeadlessOutline extends HelixElement {
173
177
  element.style.scrollMarginBlockStart = this.offset > 0 ? `${this.offset}px` : '';
174
178
  }
175
179
  }
180
+ scrollToSection(id) {
181
+ const section = this.sections.find(({ target }) => target.id === id);
182
+ if (!section) {
183
+ return false;
184
+ }
185
+ this.travel(section, true);
186
+ return true;
187
+ }
188
+ travel(section, scroll) {
189
+ this.landing = section;
190
+ this.markTo([section]);
191
+ this.unland();
192
+ const arrive = () => {
193
+ this.unland();
194
+ this.landing = undefined;
195
+ this.mark();
196
+ };
197
+ this.arrive = arrive;
198
+ document.addEventListener('scrollend', arrive, { capture: true, once: true });
199
+ this.landed = setTimeout(arrive, 1200);
200
+ if (!scroll) {
201
+ return false;
202
+ }
203
+ const reduced = globalThis.matchMedia?.('(prefers-reduced-motion: reduce)').matches === true;
204
+ section.target.scrollIntoView({
205
+ behavior: reduced && this.behavior === 'smooth' ? 'instant' : this.behavior,
206
+ block: 'start',
207
+ });
208
+ return true;
209
+ }
176
210
  watch() {
177
211
  this.observer?.disconnect();
178
212
  if (this.sections.length === 0) {
@@ -188,6 +222,35 @@ let HelixHeadlessOutline = class HelixHeadlessOutline extends HelixElement {
188
222
  this.reach();
189
223
  this.mark();
190
224
  }
225
+ writeDepths() {
226
+ const links = this.links;
227
+ const items = new Map();
228
+ for (const link of links) {
229
+ let count = 0;
230
+ for (let node = link.parentElement; node && node !== this; node = node.parentElement) {
231
+ count += node.localName === 'li' ? 1 : 0;
232
+ }
233
+ items.set(link, count);
234
+ }
235
+ const flat = links.filter((link) => items.get(link) === 0);
236
+ const depth = new Map();
237
+ const walk = (branches, at) => {
238
+ for (const branch of branches) {
239
+ const link = flat[branch.index];
240
+ if (link) {
241
+ depth.set(link, at);
242
+ }
243
+ walk(branch.children, at + 1);
244
+ }
245
+ };
246
+ walk(nest(flat.map((link) => levelOf(link.getAttribute('data-level')))), 1);
247
+ for (const link of links) {
248
+ const at = String(depth.get(link) ?? items.get(link) ?? 1);
249
+ if (link.getAttribute('data-depth') !== at) {
250
+ link.setAttribute('data-depth', at);
251
+ }
252
+ }
253
+ }
191
254
  disconnectedCallback() {
192
255
  super.disconnectedCallback();
193
256
  this.childWatch?.disconnect();
@@ -258,9 +321,12 @@ let HelixHeadlessOutline = class HelixHeadlessOutline extends HelixElement {
258
321
  updated(changed) {
259
322
  super.updated(changed);
260
323
  this.assign();
324
+ this.writeDepths();
261
325
  this.nameByHeading();
262
326
  if (changed.has('offset')) {
263
327
  this.reach();
328
+ }
329
+ if (changed.has('offset') || changed.has('marks')) {
264
330
  this.mark();
265
331
  }
266
332
  if (!this.observer) {
@@ -295,6 +361,9 @@ __decorate([
295
361
  words,
296
362
  property()
297
363
  ], HelixHeadlessOutline.prototype, "label", void 0);
364
+ __decorate([
365
+ property()
366
+ ], HelixHeadlessOutline.prototype, "marks", void 0);
298
367
  __decorate([
299
368
  property({ type: Number })
300
369
  ], HelixHeadlessOutline.prototype, "offset", void 0);
@@ -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
@@ -200,8 +200,22 @@ export declare class HelixHeadlessSpeedDial extends HelixHeadlessSpeedDial_base
200
200
  * attribute the consumer's server never sent is a mismatch whether or not it
201
201
  * moves afterwards, measured in both load orders, and a trigger that says it
202
202
  * is expanded is already saying it has something to expand.
203
+ *
204
+ * **A plain trigger once a reader has reached it.** A `<button>` of yours has
205
+ * no shadow root, so it takes `aria-haspopup="menu"` and `aria-expanded` as
206
+ * attributes, written only once focus or a pointer has reached it. That is
207
+ * after hydration, which is the only time React compares them. A trigger
208
+ * that carries either attribute itself is yours to keep up to date.
203
209
  */
204
210
  private announce;
211
+ /** The plain trigger this dial has written its state on, so it can be taken back. */
212
+ private toldPlain;
213
+ /** Whether a reader has reached the trigger, by focus or a pointer, which is after hydration. */
214
+ private reached;
215
+ /** Takes the state off a plain trigger that is no longer this dial's. */
216
+ private untellPlain;
217
+ /** A reader reaching the trigger, which lets a plain one be told its state. */
218
+ private readonly handleReach;
205
219
  /** The control the dial has described, so the description can be taken back. */
206
220
  private described;
207
221
  /**
@@ -139,6 +139,13 @@ let HelixHeadlessSpeedDial = class HelixHeadlessSpeedDial extends MixinControlle
139
139
  this.close();
140
140
  event.preventDefault();
141
141
  };
142
+ this.reached = false;
143
+ this.handleReach = (event) => {
144
+ if (!this.reached && event.composedPath().includes(this.trigger)) {
145
+ this.reached = true;
146
+ this.announce();
147
+ }
148
+ };
142
149
  this.handleTriggerFocusIn = () => {
143
150
  if (this.pressHeld) {
144
151
  this.describeTrigger(true);
@@ -186,6 +193,8 @@ let HelixHeadlessSpeedDial = class HelixHeadlessSpeedDial extends MixinControlle
186
193
  this.addEventListener('click', this.handleHeldClick, true);
187
194
  this.addEventListener('pointerenter', this.handlePointerEnter);
188
195
  this.addEventListener('pointerleave', this.handlePointerLeave);
196
+ this.addEventListener('focusin', this.handleReach);
197
+ this.addEventListener('pointerover', this.handleReach);
189
198
  }
190
199
  disconnectedCallback() {
191
200
  super.disconnectedCallback();
@@ -199,14 +208,37 @@ let HelixHeadlessSpeedDial = class HelixHeadlessSpeedDial extends MixinControlle
199
208
  this.removeEventListener('click', this.handleHeldClick, true);
200
209
  this.removeEventListener('pointerenter', this.handlePointerEnter);
201
210
  this.removeEventListener('pointerleave', this.handlePointerLeave);
211
+ this.removeEventListener('focusin', this.handleReach);
212
+ this.removeEventListener('pointerover', this.handleReach);
202
213
  this.stay();
203
214
  }
204
215
  announce() {
205
216
  const trigger = this.trigger;
206
- if (trigger === this.partNamed('trigger')) {
217
+ if (trigger === this.partNamed('trigger') || trigger === this) {
218
+ return;
219
+ }
220
+ if (trigger !== this.toldPlain) {
221
+ this.untellPlain();
222
+ }
223
+ if (trigger.localName.includes('-')) {
224
+ tellDisclosure(trigger, this.open, 'menu');
207
225
  return;
208
226
  }
209
- tellDisclosure(trigger, this.open, 'menu');
227
+ if (trigger !== this.toldPlain) {
228
+ if (!this.reached ||
229
+ trigger.hasAttribute('aria-expanded') ||
230
+ trigger.hasAttribute('aria-haspopup')) {
231
+ return;
232
+ }
233
+ this.toldPlain = trigger;
234
+ }
235
+ trigger.setAttribute('aria-haspopup', 'menu');
236
+ trigger.setAttribute('aria-expanded', String(this.open));
237
+ }
238
+ untellPlain() {
239
+ this.toldPlain?.removeAttribute('aria-haspopup');
240
+ this.toldPlain?.removeAttribute('aria-expanded');
241
+ this.toldPlain = undefined;
210
242
  }
211
243
  describeTrigger(light = false) {
212
244
  const trigger = this.trigger;
@@ -28,7 +28,11 @@ import { type PropertyValues, type TemplateResult } from 'lit';
28
28
  * back to the position where the collapsing gesture began. A drag records that position
29
29
  * on press, and every other way of collapsing records it in `settle`.
30
30
  * `collapsed-size` keeps a sliver of the pane visible, such as a tab strip.
31
- * Collapsing overrides `min` and `max`.
31
+ * Collapsing overrides `min` and `max`. `collapsible` says which panes can be
32
+ * collapsed at all, as Zag's panels each say.
33
+ *
34
+ * `min` is the least the start pane keeps and `end-min` the least the end pane
35
+ * keeps, each a percentage or a CSS length such as `16rem`.
32
36
  *
33
37
  * Set `primary` to the pane that keeps its size when the splitter's own size
34
38
  * changes, such as a sidebar beside a window being resized. The other pane
@@ -49,7 +53,7 @@ import { type PropertyValues, type TemplateResult } from 'lit';
49
53
  * | Left, Right | Moves a side-by-side divider by `step`. On a right-to-left page, Left moves it toward the end. |
50
54
  * | Up, Down | Moves a stacked divider by `step`. |
51
55
  * | Home, End | Moves the divider to its bounds. |
52
- * | Enter | Collapses the start pane, and opens it again. |
56
+ * | Enter | Collapses the start pane, or the end pane where `collapsible="end"`, and opens it again. |
53
57
  * | F6 | Moves focus from one pane to the first control in the other, and from the divider to the start pane. |
54
58
  *
55
59
  * @since 0.1.0
@@ -119,6 +123,15 @@ export declare class HelixHeadlessSplitter extends HelixElement {
119
123
  * consumer's to set.
120
124
  */
121
125
  collapsedSize: number;
126
+ /**
127
+ * Which panes can be collapsed, as Zag's panels each take `collapsible`:
128
+ * `start`, `end`, `both`, or `none`. A pane that cannot be collapsed stops
129
+ * at the edge without shutting. `Enter` collapses the end pane where only
130
+ * the end pane can be collapsed, and the start pane otherwise.
131
+ *
132
+ * Unset is `both`, and the attribute stays absent until you write it.
133
+ */
134
+ collapsible?: 'both' | 'end' | 'none' | 'start';
122
135
  /**
123
136
  * The name of the handle where two dividers cross, read by screen readers.
124
137
  */
@@ -141,6 +154,14 @@ export declare class HelixHeadlessSplitter extends HelixElement {
141
154
  * Where the divider starts, when `position` is not set.
142
155
  */
143
156
  defaultPosition: number;
157
+ /**
158
+ * The least the end pane keeps, in the units `min` takes: a percentage, or
159
+ * a CSS length such as `16rem` resolved against the splitter's box. The
160
+ * divider stops that far from the end, whatever `max` allows, so `min` and
161
+ * `end-min` hold a minimum for each pane, as Zag's panels each take
162
+ * `minSize`.
163
+ */
164
+ endMin: number | string;
144
165
  /**
145
166
  * The divider's name, read by screen readers.
146
167
  *
@@ -403,6 +424,8 @@ export declare class HelixHeadlessSplitter extends HelixElement {
403
424
  * latches on any drag that runs off the end.
404
425
  */
405
426
  private shutAt;
427
+ /** Whether `collapsible` lets this pane be collapsed. */
428
+ private canCollapse;
406
429
  /** Pulled between the bounds, which are the ends unless a consumer moved them. */
407
430
  private clamp;
408
431
  /**