@fusion.dev/helix-headless 0.150.0 → 0.152.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 (75) hide show
  1. package/custom-elements.json +1388 -93
  2. package/dist/app-header/root/app-header-root.d.ts +75 -0
  3. package/dist/app-header/root/app-header-root.js +140 -5
  4. package/dist/area-chart/root/area-chart-root.d.ts +36 -0
  5. package/dist/area-chart/root/area-chart-root.js +17 -2
  6. package/dist/bar-chart/root/bar-chart-root.d.ts +28 -4
  7. package/dist/bar-chart/root/bar-chart-root.js +17 -4
  8. package/dist/breadcrumbs/breadcrumb/breadcrumb.d.ts +11 -2
  9. package/dist/breadcrumbs/breadcrumb/breadcrumb.js +11 -2
  10. package/dist/breadcrumbs/root/breadcrumbs-root.d.ts +14 -2
  11. package/dist/breadcrumbs/root/breadcrumbs-root.js +16 -3
  12. package/dist/callout/root/callout-root.d.ts +20 -0
  13. package/dist/callout/root/callout-root.js +19 -4
  14. package/dist/chat/composer/chat-composer.d.ts +30 -0
  15. package/dist/chat/composer/chat-composer.js +47 -2
  16. package/dist/chat/root/chat-root.d.ts +10 -0
  17. package/dist/chat/root/chat-root.js +14 -0
  18. package/dist/data-table/root/data-table-root.d.ts +34 -2
  19. package/dist/data-table/root/data-table-root.js +63 -2
  20. package/dist/dialog/description.d.ts +10 -0
  21. package/dist/dialog/description.js +22 -8
  22. package/dist/dialog/root/dialog-root.d.ts +17 -1
  23. package/dist/dialog/root/dialog-root.js +2 -2
  24. package/dist/divider/root/divider-root.d.ts +8 -6
  25. package/dist/divider/root/divider-root.js +10 -2
  26. package/dist/drawer/root/drawer-root.d.ts +38 -2
  27. package/dist/drawer/root/drawer-root.js +33 -6
  28. package/dist/element-events.generated.js +4 -0
  29. package/dist/kbd/root/kbd-root.d.ts +7 -1
  30. package/dist/kbd/root/kbd-root.js +8 -4
  31. package/dist/lightbox/root/lightbox-root.d.ts +44 -6
  32. package/dist/lightbox/root/lightbox-root.js +59 -11
  33. package/dist/lightbox/zoom-controller.d.ts +5 -0
  34. package/dist/lightbox/zoom-controller.js +8 -0
  35. package/dist/line-chart/root/line-chart-root.d.ts +36 -0
  36. package/dist/line-chart/root/line-chart-root.js +15 -2
  37. package/dist/menu/root/menu-root.js +2 -0
  38. package/dist/navigation/root/navigation-root.d.ts +11 -1
  39. package/dist/navigation/root/navigation-root.js +10 -5
  40. package/dist/notifications/root/notifications-root.d.ts +17 -0
  41. package/dist/notifications/root/notifications-root.js +21 -1
  42. package/dist/popover/root/popover-root.d.ts +18 -7
  43. package/dist/popover/root/popover-root.js +8 -1
  44. package/dist/scatter-chart/root/scatter-chart-root.d.ts +18 -0
  45. package/dist/scatter-chart/root/scatter-chart-root.js +38 -15
  46. package/dist/splitter/root/splitter-root.d.ts +41 -0
  47. package/dist/splitter/root/splitter-root.js +77 -0
  48. package/dist/table/column/table-column.js +3 -2
  49. package/dist/table/grid-controller.d.ts +14 -2
  50. package/dist/table/grid-controller.js +27 -1
  51. package/dist/table/root/table-root.d.ts +30 -4
  52. package/dist/table/root/table-root.js +45 -7
  53. package/dist/table/row/table-row.d.ts +12 -2
  54. package/dist/table/row/table-row.js +8 -2
  55. package/dist/tabs/root/tabs-root.d.ts +25 -0
  56. package/dist/tabs/root/tabs-root.js +28 -1
  57. package/dist/tour/naming.d.ts +9 -3
  58. package/dist/tour/naming.js +9 -3
  59. package/dist/tour/root/tour-root.d.ts +29 -5
  60. package/dist/tour/root/tour-root.js +13 -1
  61. package/dist/tour/stop/tour-stop.d.ts +2 -1
  62. package/dist/tour/stop/tour-stop.js +4 -1
  63. package/dist/tree/root/tree-root.d.ts +13 -0
  64. package/dist/tree/root/tree-root.js +42 -2
  65. package/dist/video/root/video-root.d.ts +6 -0
  66. package/dist/video/root/video-root.js +5 -1
  67. package/dist/video-playlist/root/video-playlist-root.d.ts +4 -0
  68. package/dist/video-playlist/root/video-playlist-root.js +4 -1
  69. package/dist/virtual-list/root/virtual-list-root.d.ts +23 -1
  70. package/dist/virtual-list/root/virtual-list-root.js +21 -2
  71. package/lint.json +1 -1
  72. package/package.json +5 -5
  73. package/types/jsx.d.ts +1098 -128
  74. package/types/vue.d.ts +248 -59
  75. package/types/words.d.ts +12 -2
@@ -1,3 +1,13 @@
1
+ /**
2
+ * The elements at the start of a list that hold words and no control, up to the
3
+ * first one that is or holds a control.
4
+ *
5
+ * `skip` is an element whose words are said elsewhere, such as the heading
6
+ * that names a panel. It is left out, and an element holding it is read for
7
+ * its other children, so a card's title names the panel and the card's text
8
+ * describes it.
9
+ */
10
+ export declare const wordsAmong: (elements: readonly Element[], skip?: Element | null) => Element[];
1
11
  /**
2
12
  * The elements at the start of a slot that hold words and no control, up to the
3
13
  * first one that is or holds a control.
@@ -14,15 +14,29 @@ const isControl = (element) => {
14
14
  root?.delegatesFocus === true ||
15
15
  root?.querySelector(CONTROL) != null);
16
16
  };
17
- export const wordsIn = (slot) => {
17
+ export const wordsAmong = (elements, skip = null) => {
18
18
  const words = [];
19
- for (const one of slot?.assignedElements({ flatten: true }) ?? []) {
20
- if (isControl(one) || [...one.querySelectorAll('*')].some(isControl)) {
21
- break;
19
+ const read = (list) => {
20
+ for (const one of list) {
21
+ if (one === skip) {
22
+ continue;
23
+ }
24
+ if (skip && one.contains(skip)) {
25
+ if (!read([...one.children])) {
26
+ return false;
27
+ }
28
+ continue;
29
+ }
30
+ if (isControl(one) || [...one.querySelectorAll('*')].some(isControl)) {
31
+ return false;
32
+ }
33
+ if ((one.textContent ?? '').trim() !== '') {
34
+ words.push(one);
35
+ }
22
36
  }
23
- if ((one.textContent ?? '').trim() !== '') {
24
- words.push(one);
25
- }
26
- }
37
+ return true;
38
+ };
39
+ read(elements);
27
40
  return words;
28
41
  };
42
+ export const wordsIn = (slot) => wordsAmong(slot?.assignedElements({ flatten: true }) ?? []);
@@ -39,6 +39,16 @@ declare const HelixHeadlessDialog_base: (new (...args: any[]) => import("@fusion
39
39
  * announces with its description as it opens. Set `dismissible` to draw a
40
40
  * close control, labeled by `dismiss-label`.
41
41
  *
42
+ * **Keyboard.** Opening moves focus to the first control in the dialog.
43
+ *
44
+ * | Key | What it does |
45
+ * |---|---|
46
+ * | Tab, Shift+Tab | Moves through the controls in the dialog, from the last round to the first and from the first round to the last. |
47
+ * | Escape | Asks to close the dialog. A `helix-before-close` listener can refuse, and `persistent` or `closedby="none"` keeps it open. |
48
+ * | Enter, Space | On the close control, closes the dialog. |
49
+ *
50
+ * Closing puts focus back on the element that had it before the dialog opened.
51
+ *
42
52
  * @since 0.1.0
43
53
  * @status stable
44
54
  * @category overlays
@@ -94,8 +104,14 @@ export declare class HelixHeadlessDialog extends HelixHeadlessDialog_base {
94
104
  * `closedby="any"` gains light dismiss without giving up the refusal, which is
95
105
  * the property this component exists to keep.
96
106
  *
107
+ * The press on the backdrop that this element handles itself follows it too:
108
+ * `closerequest` and `none` leave the dialog open on that press, as the
109
+ * platform does, and `none` also keeps it open on Escape in a browser without
110
+ * `closedby`.
111
+ *
97
112
  * Absent by default, which is the platform's `auto` and this library's rule
98
- * that a resting value is an attribute's absence.
113
+ * that a resting value is an attribute's absence. Absent, a press on the
114
+ * backdrop closes the dialog.
99
115
  */
100
116
  closedBy?: 'any' | 'closerequest' | 'none';
101
117
  /**
@@ -36,7 +36,7 @@ let HelixHeadlessDialog = class HelixHeadlessDialog extends MixinControllerOpen(
36
36
  this.returned = '';
37
37
  this.handleCancel = (event) => {
38
38
  event.preventDefault();
39
- if (this.persistent) {
39
+ if (this.persistent || this.closedBy === 'none') {
40
40
  return;
41
41
  }
42
42
  this.requestOpen(false);
@@ -84,7 +84,7 @@ let HelixHeadlessDialog = class HelixHeadlessDialog extends MixinControllerOpen(
84
84
  event.clientY <= box.bottom) {
85
85
  return;
86
86
  }
87
- if (this.persistent) {
87
+ if (this.persistent || this.closedBy === 'none' || this.closedBy === 'closerequest') {
88
88
  this.refuse();
89
89
  return;
90
90
  }
@@ -12,9 +12,10 @@ import { type PropertyValues, type TemplateResult } from 'lit';
12
12
  * **The line is a separator**, as `<hr>` is, and as Radix Separator, React
13
13
  * Aria Separator, and Web Awesome's divider are. It is named by any content
14
14
  * set into it, so a screen reader says "Or, separator" where you wrote "Or" on
15
- * the line. Set `decorative` on a rule that separates nothing, such as one
16
- * under a heading or between the facts of a byline, and it leaves the
17
- * accessibility tree.
15
+ * the line, and the words are read once, as that name. Set words into a
16
+ * separator and no controls, which are hidden with them. Set `decorative` on
17
+ * a rule that separates nothing, such as one under a heading or between the
18
+ * facts of a byline, and it leaves the accessibility tree.
18
19
  *
19
20
  * Use `orientation="vertical"` between items on one line. A vertical divider
20
21
  * stretches to fit its flex parent and sets no height of its own.
@@ -44,9 +45,10 @@ export declare class HelixHeadlessDivider extends HelixElement {
44
45
  * Whether screen readers announce the line as a separator, which they now do
45
46
  * by default.
46
47
  *
47
- * It dates from when the line was decorative by default. It is still read,
48
- * and it wins over `decorative`, so a divider written with it stays a
49
- * separator.
48
+ * @deprecated Remove it: a divider is a separator unless `decorative` is
49
+ * set. It dates from when the line was decorative by default, and it is
50
+ * still read and wins over `decorative`, so a divider written with it stays
51
+ * a separator.
50
52
  */
51
53
  announced: boolean;
52
54
  /**
@@ -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 { customElement, HelixElement, markRegion, toolbarAround, toolbarAxis, } from '@fusion.dev/helix-core';
7
+ import { customElement, HelixElement, markRegion, reportRuntime, toolbarAround, toolbarAxis, } from '@fusion.dev/helix-core';
8
8
  import { html, nothing } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
10
  let HelixHeadlessDivider = class HelixHeadlessDivider extends HelixElement {
@@ -38,6 +38,14 @@ let HelixHeadlessDivider = class HelixHeadlessDivider extends HelixElement {
38
38
  updated(changed) {
39
39
  super.updated(changed);
40
40
  this.markEmpty();
41
+ if (changed.has('announced') && this.announced && changed.get('announced') === undefined) {
42
+ reportRuntime({
43
+ kind: 'deprecated',
44
+ message: `<${this.localName} announced> is deprecated: remove it. A divider is a separator ` +
45
+ 'unless `decorative` is set.',
46
+ tag: this.localName,
47
+ });
48
+ }
41
49
  }
42
50
  render() {
43
51
  const separator = this.announced || !this.decorative;
@@ -47,7 +55,7 @@ let HelixHeadlessDivider = class HelixHeadlessDivider extends HelixElement {
47
55
  aria-labelledby=${separator ? 'content' : nothing}
48
56
  aria-orientation=${separator ? this.drawn : nothing}
49
57
  ></div>
50
- <span id="content" part="content"><slot @slotchange=${() => this.markEmpty()}></slot></span>
58
+ <span id="content" part="content" aria-hidden=${separator ? 'true' : nothing}><slot @slotchange=${() => this.markEmpty()}></slot></span>
51
59
  <div part="line" role="presentation"></div>`;
52
60
  }
53
61
  };
@@ -20,6 +20,11 @@ declare const HelixHeadlessDrawer_base: (new (...args: any[]) => import("@fusion
20
20
  * A drawer with `modal="false"` leaves the page usable and still closes on
21
21
  * Escape, in a browser without `closedby` as well.
22
22
  *
23
+ * Set `persistent` for a drawer that has to be answered: Escape, a press
24
+ * outside, and a swipe leave it open, and the close control and `close()`
25
+ * still close it. `closedby="none"` keeps it open on all three as well, and
26
+ * `closedby="closerequest"` keeps it open on a press outside.
27
+ *
23
28
  * **A touch or a pen swipes it away**, toward the edge it came from, resolved
24
29
  * against the writing direction. A swipe past halfway, or a quick one, closes
25
30
  * it through the same request as every other way out. `snap-points` lists the
@@ -40,7 +45,20 @@ declare const HelixHeadlessDrawer_base: (new (...args: any[]) => import("@fusion
40
45
  *
41
46
  * The panel's name is `label` when you set it. Otherwise it is the heading in
42
47
  * the `header` slot, or the elements an `aria-labelledby` on the host names,
43
- * and `fallback-label` where nothing names it.
48
+ * and `fallback-label` where nothing names it. A development build warns
49
+ * when a drawer opens with nothing but `fallback-label` to name it.
50
+ *
51
+ * The panel is described by the words at the start of its body, as `Dialog`
52
+ * is: the elements in the default slot before the first one that is or holds
53
+ * a control. An `aria-describedby` on the host replaces it.
54
+ *
55
+ * **Keyboard.** Opening moves focus to the first control in the panel.
56
+ *
57
+ * | Key | What it does |
58
+ * |---|---|
59
+ * | Tab, Shift+Tab | In a modal drawer, moves through the controls in the panel, from the last round to the first and from the first round to the last. In a drawer that is not modal, moves on to the page. |
60
+ * | Escape | Asks to close the drawer. A `helix-before-close` listener can refuse, and `persistent` or `closedby="none"` keeps it open. |
61
+ * | Enter, Space | On the close control, closes the drawer. |
44
62
  *
45
63
  * Content stays mounted when the drawer is closed, so it is in the server
46
64
  * markup and can animate. A closed `<dialog>` is already out of the tab order
@@ -114,6 +132,8 @@ export declare class HelixHeadlessDrawer extends HelixHeadlessDrawer_base {
114
132
  * way out.
115
133
  */
116
134
  dismissible: boolean;
135
+ /** Whether a request to dismiss the drawer, by Escape or a swipe, is refused. */
136
+ private get refusesDismissal();
117
137
  /**
118
138
  * The close control's accessible name. Translate it with the rest of your
119
139
  * interface.
@@ -129,7 +149,6 @@ export declare class HelixHeadlessDrawer extends HelixHeadlessDrawer_base {
129
149
  * `header` slot names the panel.
130
150
  */
131
151
  label: string;
132
- protected readonly aria: AriaForwardController;
133
152
  /**
134
153
  * Whether the drawer blocks the page behind it.
135
154
  *
@@ -144,6 +163,15 @@ export declare class HelixHeadlessDrawer extends HelixHeadlessDrawer_base {
144
163
  * not, rather than shown or hidden by CSS.
145
164
  */
146
165
  modal: boolean;
166
+ protected readonly aria: AriaForwardController;
167
+ /**
168
+ * Stops Escape, a press outside, and a swipe from closing the drawer, for a
169
+ * drawer that has to be answered.
170
+ *
171
+ * The close control and `close()` still close it, as on `Dialog`: a panel
172
+ * with no way out at all is a trap.
173
+ */
174
+ persistent: boolean;
147
175
  /**
148
176
  * Which edge the drawer comes from.
149
177
  *
@@ -281,6 +309,14 @@ export declare class HelixHeadlessDrawer extends HelixHeadlessDrawer_base {
281
309
  * reflection keeps the fallback name.
282
310
  */
283
311
  private nameByHeader;
312
+ /** Whether the body's words are what currently describe the panel. */
313
+ private describedByBody;
314
+ /**
315
+ * Describes the panel by the words at the start of its body, where nothing
316
+ * else describes it, as `Dialog` does. An `aria-describedby` on the host
317
+ * comes first.
318
+ */
319
+ private describeByBody;
284
320
  connectedCallback(): void;
285
321
  disconnectedCallback(): void;
286
322
  protected updated(changed: PropertyValues): void;
@@ -4,9 +4,10 @@ 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, CommandController, customElement, drawDismiss, drawDismissGlyph, edgeOffset, FocusScopeController, HelixElement, headingsIn, isRtl, lockScroll, MixinControllerOpen, nearest, OpenPhaseController, onUnlessFalse, parseSnapPoints, reflectElements, SwipeController, settleSwipe, snapTravel, swipeAxis, swipeEdge, towardEdge, unlockScroll, words, } from '@fusion.dev/helix-core';
7
+ import { AriaForwardController, auditName, CommandController, customElement, drawDismiss, drawDismissGlyph, edgeOffset, FocusScopeController, HelixElement, headingsIn, isRtl, lockScroll, MixinControllerOpen, nearest, OpenPhaseController, onUnlessFalse, parseSnapPoints, reflectElements, SwipeController, settleSwipe, snapTravel, swipeAxis, swipeEdge, towardEdge, unlockScroll, words, } from '@fusion.dev/helix-core';
8
8
  import { html, nothing } from 'lit';
9
9
  import { property } from 'lit/decorators.js';
10
+ import { wordsIn } from '../../dialog/description.js';
10
11
  import { beyondWindow } from '../reveal.js';
11
12
  import { startsSwipe } from '../swipe-start.js';
12
13
  let HelixHeadlessDrawer = class HelixHeadlessDrawer extends MixinControllerOpen(HelixElement) {
@@ -23,8 +24,9 @@ let HelixHeadlessDrawer = class HelixHeadlessDrawer extends MixinControllerOpen(
23
24
  this.dismissLabel = 'Close';
24
25
  this.fallbackLabel = 'Drawer';
25
26
  this.label = '';
26
- this.aria = new AriaForwardController(this, { target: () => this.panel });
27
27
  this.modal = true;
28
+ this.aria = new AriaForwardController(this, { target: () => this.panel });
29
+ this.persistent = false;
28
30
  this.snapPoints = '';
29
31
  this.swipe = true;
30
32
  this.returned = '';
@@ -63,7 +65,9 @@ let HelixHeadlessDrawer = class HelixHeadlessDrawer extends MixinControllerOpen(
63
65
  return;
64
66
  }
65
67
  event.preventDefault();
66
- this.requestOpen(false);
68
+ if (!this.refusesDismissal) {
69
+ this.requestOpen(false);
70
+ }
67
71
  };
68
72
  this.scope = new FocusScopeController(this, {
69
73
  contain: () => this.open && this.modal,
@@ -77,7 +81,9 @@ let HelixHeadlessDrawer = class HelixHeadlessDrawer extends MixinControllerOpen(
77
81
  });
78
82
  this.handleCancel = (event) => {
79
83
  event.preventDefault();
80
- this.requestOpen(false);
84
+ if (!this.refusesDismissal) {
85
+ this.requestOpen(false);
86
+ }
81
87
  };
82
88
  this.handleClose = (event) => {
83
89
  if (this.open) {
@@ -97,11 +103,18 @@ let HelixHeadlessDrawer = class HelixHeadlessDrawer extends MixinControllerOpen(
97
103
  event.clientY <= box.bottom) {
98
104
  return;
99
105
  }
106
+ if (this.refusesDismissal || this.closedBy === 'closerequest') {
107
+ return;
108
+ }
100
109
  this.requestOpen(false);
101
110
  };
102
111
  this.namedByHeader = false;
112
+ this.describedByBody = false;
103
113
  }
104
114
  static { this.words = 'helix-drawer'; }
115
+ get refusesDismissal() {
116
+ return this.persistent || this.closedBy === 'none';
117
+ }
105
118
  get returnValue() {
106
119
  return this.panel?.returnValue ?? this.returned;
107
120
  }
@@ -138,7 +151,7 @@ let HelixHeadlessDrawer = class HelixHeadlessDrawer extends MixinControllerOpen(
138
151
  travel: Math.max(0, this.rest + towardEdge(edge, dx, dy)),
139
152
  velocity: towardEdge(edge, vx, vy),
140
153
  });
141
- if (shown === 0 && this.requestOpen(false)) {
154
+ if (shown === 0 && !this.refusesDismissal && this.requestOpen(false)) {
142
155
  return;
143
156
  }
144
157
  if (shown !== 0) {
@@ -192,6 +205,11 @@ let HelixHeadlessDrawer = class HelixHeadlessDrawer extends MixinControllerOpen(
192
205
  : headingsIn(slot?.assignedElements({ flatten: true }) ?? []);
193
206
  this.namedByHeader = reflectElements(this.panel, 'ariaLabelledByElements', heading, this.namedByHeader);
194
207
  }
208
+ describeByBody() {
209
+ const slot = this.renderRoot.querySelector('slot:not([name])');
210
+ const said = this.hasAttribute('aria-describedby') ? [] : wordsIn(slot);
211
+ this.describedByBody = reflectElements(this.panel, 'ariaDescribedByElements', said, this.describedByBody);
212
+ }
195
213
  connectedCallback() {
196
214
  super.connectedCallback();
197
215
  this.addEventListener('focusin', this.handleFocusIn);
@@ -204,6 +222,7 @@ let HelixHeadlessDrawer = class HelixHeadlessDrawer extends MixinControllerOpen(
204
222
  updated(changed) {
205
223
  super.updated(changed);
206
224
  this.nameByHeader();
225
+ this.describeByBody();
207
226
  if (changed.has('open') && !this.open) {
208
227
  this.rest = 0;
209
228
  this.shown = 1;
@@ -221,6 +240,11 @@ let HelixHeadlessDrawer = class HelixHeadlessDrawer extends MixinControllerOpen(
221
240
  return;
222
241
  }
223
242
  if (this.open && !panel.open) {
243
+ auditName(this, () => null, 'Give it a `label`, or a heading in the `header` slot.', () => this.label !== '' ||
244
+ this.namedByHeader ||
245
+ headingsIn(this.renderRoot
246
+ .querySelector('slot[name="header"]')
247
+ ?.assignedElements({ flatten: true }) ?? []).length > 0);
224
248
  if (this.modal) {
225
249
  panel.showModal();
226
250
  }
@@ -245,7 +269,7 @@ let HelixHeadlessDrawer = class HelixHeadlessDrawer extends MixinControllerOpen(
245
269
  @keydown=${this.handleKeydown}
246
270
  >
247
271
  <div part="header"><slot name="header" @slotchange=${this.nameByHeader}></slot></div>
248
- <div part="content"><slot></slot></div>
272
+ <div part="content"><slot @slotchange=${this.describeByBody}></slot></div>
249
273
  <div part="footer"><slot name="footer"></slot></div>
250
274
  ${this.dismissible
251
275
  ? drawDismiss({
@@ -282,6 +306,9 @@ __decorate([
282
306
  __decorate([
283
307
  property({ converter: onUnlessFalse })
284
308
  ], HelixHeadlessDrawer.prototype, "modal", void 0);
309
+ __decorate([
310
+ property({ reflect: true, type: Boolean })
311
+ ], HelixHeadlessDrawer.prototype, "persistent", void 0);
285
312
  __decorate([
286
313
  property({ reflect: true })
287
314
  ], HelixHeadlessDrawer.prototype, "placement", void 0);
@@ -3,7 +3,9 @@ registerElementEvents({
3
3
  'helix-headless-accordion-item': ['helix-before-close', 'helix-before-open', 'toggle'],
4
4
  'helix-headless-announcement': ['helix-dismiss'],
5
5
  'helix-headless-app-header': ['helix-scrolled'],
6
+ 'helix-headless-area-chart': ['helix-reading-change', 'helix-reading-press'],
6
7
  'helix-headless-avatar': ['error', 'load'],
8
+ 'helix-headless-bar-chart': ['helix-reading-change', 'helix-reading-press'],
7
9
  'helix-headless-button': ['helix-press'],
8
10
  'helix-headless-button-group': ['change', 'helix-before-change'],
9
11
  'helix-headless-callout': ['helix-dismiss'],
@@ -132,6 +134,7 @@ registerElementEvents({
132
134
  'helix-before-open',
133
135
  'toggle',
134
136
  ],
137
+ 'helix-headless-line-chart': ['helix-reading-change', 'helix-reading-press'],
135
138
  'helix-headless-list': [
136
139
  'change',
137
140
  'helix-action',
@@ -208,6 +211,7 @@ registerElementEvents({
208
211
  'helix-preview',
209
212
  'input',
210
213
  ],
214
+ 'helix-headless-scatter-chart': ['helix-reading-change', 'helix-reading-press'],
211
215
  'helix-headless-scroll-area': ['scroll', 'scrollend'],
212
216
  'helix-headless-select': [
213
217
  'change',
@@ -23,6 +23,11 @@ import { type PropertyValues, type TemplateResult } from 'lit';
23
23
  * the shortcut with the control. A chord the page wrote there itself is left
24
24
  * alone, and the key takes back only what it added.
25
25
  *
26
+ * A chord is drawn with the platform's modifiers first, in its order, so one
27
+ * `keys` value is right everywhere: `mod+shift+p` is `⇧⌘P` on an Apple
28
+ * keyboard and `Ctrl+Shift+P` elsewhere. Where it is drawn in glyphs, it is
29
+ * read in words, such as `Shift Command P`.
30
+ *
26
31
  * `caps` draws a chord as a key per key: each is a `key` part and the `+`
27
32
  * between two a `separator` part, so a sheet can draw a cap for each.
28
33
  * `key-labels` renames any key, on every platform, such as
@@ -141,7 +146,8 @@ export declare class HelixHeadlessKbd extends HelixElement {
141
146
  /**
142
147
  * The chord under `caps`, a part for each key and for each `+` between two,
143
148
  * so a sheet can draw a cap per key. Written with no space between, so a
144
- * chord reads as the one label it was.
149
+ * chord reads as the one label it was. Where the chord is said in words,
150
+ * the parts are hidden from a screen reader, which reads the words.
145
151
  */
146
152
  protected renderChord(): TemplateResult;
147
153
  }
@@ -135,12 +135,16 @@ let HelixHeadlessKbd = class HelixHeadlessKbd extends HelixElement {
135
135
  if (this.keys === '') {
136
136
  return html `<kbd part="kbd"><slot></slot></kbd>`;
137
137
  }
138
- const { keys, separator } = this.label;
139
- return html `<kbd part="kbd">${this.caps ? this.renderChord() : keys.join(separator)}</kbd>`;
138
+ const { keys, separator, spoken } = this.label;
139
+ if (spoken === '') {
140
+ return html `<kbd part="kbd">${this.caps ? this.renderChord() : keys.join(separator)}</kbd>`;
141
+ }
142
+ return html `<kbd part="kbd">${this.caps ? this.renderChord() : html `<span aria-hidden="true">${keys.join(separator)}</span>`}<span data-spoken>${spoken}</span></kbd>`;
140
143
  }
141
144
  renderChord() {
142
- const { keys, separator } = this.label;
143
- return html `${keys.map((key, index) => html `${index > 0 && separator !== '' ? html `<span part="separator">${separator}</span>` : nothing}<span part="key">${key}</span>`)}`;
145
+ const { keys, separator, spoken } = this.label;
146
+ const hidden = spoken === '' ? nothing : 'true';
147
+ return html `${keys.map((key, index) => html `${index > 0 && separator !== '' ? html `<span part="separator" aria-hidden=${hidden}>${separator}</span>` : nothing}<span part="key" aria-hidden=${hidden}>${key}</span>`)}`;
144
148
  }
145
149
  };
146
150
  __decorate([
@@ -1,4 +1,4 @@
1
- import { CommandController, HelixElement, SwipeController } from '@fusion.dev/helix-core';
1
+ import { CommandController, FocusScopeController, HelixElement, SwipeController } from '@fusion.dev/helix-core';
2
2
  import { type PropertyValues, type TemplateResult } from 'lit';
3
3
  declare const HelixHeadlessLightbox_base: (new (...args: any[]) => import("@fusion.dev/helix-core").MixinControllerActiveInterface) & (new (...args: any[]) => import("@fusion.dev/helix-core").MixinControllerOpenInterface) & typeof HelixElement;
4
4
  /**
@@ -41,6 +41,25 @@ declare const HelixHeadlessLightbox_base: (new (...args: any[]) => import("@fusi
41
41
  * swiped across the picture shows the next or the previous item, the way the
42
42
  * line of text runs.
43
43
  *
44
+ * A press on the empty area around the item closes the viewer, as a press on
45
+ * the backdrop does. Under `max-zoom`, a press beside the item zooms on a
46
+ * double press and closes nothing, and only the space outside the viewport
47
+ * closes the viewer. A press that dragged closes nothing.
48
+ *
49
+ * **Keyboard.** Opening moves focus to the viewer itself, which a screen
50
+ * reader names.
51
+ *
52
+ * | Key | What it does |
53
+ * |---|---|
54
+ * | Right, Down | Shows the next item. On a right-to-left page, Left does. While the item is zoomed, the arrows move about it instead. |
55
+ * | Left, Up | Shows the previous item. |
56
+ * | Home, End | Shows the first or the last item. |
57
+ * | `+`, `-`, `0` | Zooms in, zooms out, and puts the item back at its own size, where `max-zoom` allows zooming. |
58
+ * | Tab, Shift+Tab | Moves through the controls in the viewer, from the last round to the first and from the first round to the last. |
59
+ * | Escape | Closes the viewer. A `helix-before-close` listener can refuse. |
60
+ *
61
+ * A development build warns when a viewer opens with no `label`.
62
+ *
44
63
  * The chrome hides when there is no input, through `IdleController`, and comes
45
64
  * back on a pointer move, a key press, or a focus. It stays while focus is on a
46
65
  * control in the header or the footer, so a keyboard reader is never left on
@@ -205,6 +224,7 @@ export declare class HelixHeadlessLightbox extends HelixHeadlessLightbox_base {
205
224
  /** Whether the zoom is being put back because the item changed, which says nothing. */
206
225
  private quietZoom;
207
226
  protected readonly swipes: SwipeController;
227
+ protected readonly scope: FocusScopeController;
208
228
  private readonly zoomer;
209
229
  /** Writes the zoom on the viewport, and nothing at the item's own size. */
210
230
  private drawZoom;
@@ -287,6 +307,11 @@ export declare class HelixHeadlessLightbox extends HelixHeadlessLightbox_base {
287
307
  private readonly handleKeydown;
288
308
  /** `+` and `-` zoom in and out by half again, and `0` puts the item back. Returns whether it was one. */
289
309
  private zoomKey;
310
+ /**
311
+ * The arrow keys on a zoomed item, which move about it rather than to
312
+ * another item. Returns whether it was one.
313
+ */
314
+ private panKey;
290
315
  /** Zooms in by half again, as `+` and the zoom-in control do. */
291
316
  private zoomIn;
292
317
  /** Zooms out by as much, as `-` and the zoom-out control do. */
@@ -296,14 +321,27 @@ export declare class HelixHeadlessLightbox extends HelixHeadlessLightbox_base {
296
321
  /** The item on screen: the one `active` names, or the first where it names none. */
297
322
  private get shown();
298
323
  /**
299
- * A press on the backdrop, which lands on the dialog element itself.
324
+ * A press on the backdrop or on the empty area around the item.
300
325
  *
301
- * The point is compared against the panel's box rather than trusting the
302
- * target alone, for the reason `Dialog` gives: a modal `<dialog>` attributes
303
- * backdrop presses to itself, so any padding the styled tier gives the panel
304
- * is a strip where a press on the panel looks like a press outside it.
326
+ * A modal `<dialog>` attributes backdrop presses to itself, and the styled
327
+ * tier draws the viewer over the whole screen, so a press beside the item
328
+ * lands on the dialog, the viewport, or the item's own box. Any of those is
329
+ * a reader done with the viewer, as a press on a dark room around a picture
330
+ * is. Not a press that dragged, which was a swipe or a pan.
305
331
  */
306
332
  private readonly handleBackdrop;
333
+ /** Where the last press began, so a press that dragged closes nothing. */
334
+ private pressedAt;
335
+ private readonly handlePointerDown;
336
+ /** Whether the press moved further than a click does, as a swipe or a pan does. */
337
+ private dragged;
338
+ /**
339
+ * Whether a press landed on the empty area around the item: the viewer's
340
+ * own surface, the viewport, or an item's box beside what it holds. The
341
+ * last two not where `max-zoom` lets a reader zoom, since a double press
342
+ * there zooms.
343
+ */
344
+ private onEmptyArea;
307
345
  protected willUpdate(changed: PropertyValues): void;
308
346
  disconnectedCallback(): void;
309
347
  protected updated(changed: PropertyValues): void;
@@ -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 { CommandController, customElement, DelayController, deepActiveElement, deepContains, drawDismiss, drawDismissGlyph, FullscreenController, formatWords, HelixElement, hostAria, IdleController, inlineStep, isRtl, keyClaimed, lockScroll, MixinControllerActive, MixinControllerOpen, resolveActiveItem, SwipeController, swipeStep, unlockScroll, whenHydrated, words, } from '@fusion.dev/helix-core';
7
+ import { auditName, CommandController, customElement, DelayController, deepActiveElement, deepContains, drawDismiss, drawDismissGlyph, FocusScopeController, FullscreenController, formatWords, HelixElement, hostAria, IdleController, inlineStep, isRtl, keyClaimed, lockScroll, MixinControllerActive, MixinControllerOpen, resolveActiveItem, SwipeController, swipeStep, unlockScroll, whenHydrated, words, } from '@fusion.dev/helix-core';
8
8
  import { caretLeft, caretRight, minus, plus } from '@fusion.dev/helix-icons';
9
9
  import { html, nothing } from 'lit';
10
10
  import { property, state } from 'lit/decorators.js';
@@ -53,6 +53,16 @@ let HelixHeadlessLightbox = class HelixHeadlessLightbox extends MixinControllerA
53
53
  start: (_target, event) => event.pointerType !== 'mouse',
54
54
  surface: () => this.dialog,
55
55
  });
56
+ this.scope = new FocusScopeController(this, {
57
+ contain: () => this.open,
58
+ contents: () => [
59
+ ...['header', '', 'footer'].flatMap((name) => this.renderRoot
60
+ ?.querySelector(name ? `slot[name="${name}"]` : 'slot:not([name])')
61
+ ?.assignedElements({ flatten: true }) ?? []),
62
+ ...[...(this.dialog?.children ?? [])].filter((one) => one.localName === 'button'),
63
+ ],
64
+ surface: () => this.dialog,
65
+ });
56
66
  this.zoomer = new ZoomController(this, {
57
67
  max: () => (this.open ? this.maxZoom : 1),
58
68
  onChange: (scale, pan) => this.drawZoom(scale, pan),
@@ -89,7 +99,7 @@ let HelixHeadlessLightbox = class HelixHeadlessLightbox extends MixinControllerA
89
99
  if (keyClaimed(event) || event.altKey || event.ctrlKey || event.metaKey) {
90
100
  return;
91
101
  }
92
- if (this.zoomKey(event.key)) {
102
+ if (this.zoomKey(event.key) || this.panKey(event.key)) {
93
103
  event.preventDefault();
94
104
  return;
95
105
  }
@@ -112,19 +122,14 @@ let HelixHeadlessLightbox = class HelixHeadlessLightbox extends MixinControllerA
112
122
  }
113
123
  };
114
124
  this.handleBackdrop = (event) => {
115
- const panel = this.dialog;
116
- if (!panel || event.target !== panel) {
117
- return;
118
- }
119
- const box = panel.getBoundingClientRect();
120
- if (event.clientX >= box.left &&
121
- event.clientX <= box.right &&
122
- event.clientY >= box.top &&
123
- event.clientY <= box.bottom) {
125
+ if (this.dragged(event) || !this.onEmptyArea(event)) {
124
126
  return;
125
127
  }
126
128
  this.requestOpen(false);
127
129
  };
130
+ this.handlePointerDown = (event) => {
131
+ this.pressedAt = { x: event.clientX, y: event.clientY };
132
+ };
128
133
  }
129
134
  static { this.words = 'helix-lightbox'; }
130
135
  drawZoom(scale, pan) {
@@ -217,6 +222,25 @@ let HelixHeadlessLightbox = class HelixHeadlessLightbox extends MixinControllerA
217
222
  }
218
223
  return true;
219
224
  }
225
+ panKey(key) {
226
+ if (this.zoomer.scale <= 1) {
227
+ return false;
228
+ }
229
+ const box = this.partNamed('viewport')?.getBoundingClientRect();
230
+ const across = Math.max(40, (box?.width ?? 0) / 10);
231
+ const down = Math.max(40, (box?.height ?? 0) / 10);
232
+ const by = {
233
+ ArrowDown: { x: 0, y: -down },
234
+ ArrowLeft: { x: across, y: 0 },
235
+ ArrowRight: { x: -across, y: 0 },
236
+ ArrowUp: { x: 0, y: down },
237
+ }[key];
238
+ if (!by) {
239
+ return false;
240
+ }
241
+ this.zoomer.panBy(by);
242
+ return true;
243
+ }
220
244
  zoomIn() {
221
245
  this.zoomer.zoomTo(this.zoomer.scale * 1.5);
222
246
  }
@@ -232,6 +256,28 @@ let HelixHeadlessLightbox = class HelixHeadlessLightbox extends MixinControllerA
232
256
  get shown() {
233
257
  return resolveActiveItem(this.active ?? '', this.items);
234
258
  }
259
+ dragged(event) {
260
+ const from = this.pressedAt;
261
+ this.pressedAt = undefined;
262
+ return from !== undefined && Math.hypot(event.clientX - from.x, event.clientY - from.y) > 8;
263
+ }
264
+ onEmptyArea(event) {
265
+ const pressed = event.composedPath()[0];
266
+ if (!(pressed instanceof Element)) {
267
+ return false;
268
+ }
269
+ if (pressed === this.dialog) {
270
+ return true;
271
+ }
272
+ if (this.maxZoom > 1) {
273
+ return false;
274
+ }
275
+ const root = pressed.getRootNode();
276
+ const inItem = root instanceof ShadowRoot &&
277
+ root.host instanceof HelixHeadlessLightboxItem &&
278
+ pressed.getAttribute('part') === 'item';
279
+ return inItem || pressed === this.partNamed('viewport');
280
+ }
235
281
  willUpdate(changed) {
236
282
  super.willUpdate(changed);
237
283
  hostAria(this.internals, { label: this.label });
@@ -263,6 +309,7 @@ let HelixHeadlessLightbox = class HelixHeadlessLightbox extends MixinControllerA
263
309
  return;
264
310
  }
265
311
  if (this.open && !dialog.open) {
312
+ auditName(this, () => null, 'Give it a `label`, such as "Project photos".', () => this.label !== '');
266
313
  dialog.showModal();
267
314
  dialog.focus({ preventScroll: true });
268
315
  this.idle.stir();
@@ -334,6 +381,7 @@ let HelixHeadlessLightbox = class HelixHeadlessLightbox extends MixinControllerA
334
381
  @close=${this.handleClose}
335
382
  @focusin=${this.idle.stir}
336
383
  @keydown=${this.handleKeydown}
384
+ @pointerdown=${this.handlePointerDown}
337
385
  @pointerleave=${this.idle.leave}
338
386
  @pointermove=${this.idle.stir}
339
387
  >