@fusion.dev/helix-headless 0.151.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 (41) hide show
  1. package/custom-elements.json +648 -28
  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/breadcrumbs/breadcrumb/breadcrumb.d.ts +11 -2
  5. package/dist/breadcrumbs/breadcrumb/breadcrumb.js +11 -2
  6. package/dist/breadcrumbs/root/breadcrumbs-root.d.ts +14 -2
  7. package/dist/breadcrumbs/root/breadcrumbs-root.js +16 -3
  8. package/dist/dialog/description.d.ts +10 -0
  9. package/dist/dialog/description.js +22 -8
  10. package/dist/dialog/root/dialog-root.d.ts +17 -1
  11. package/dist/dialog/root/dialog-root.js +2 -2
  12. package/dist/drawer/root/drawer-root.d.ts +38 -2
  13. package/dist/drawer/root/drawer-root.js +33 -6
  14. package/dist/lightbox/root/lightbox-root.d.ts +44 -6
  15. package/dist/lightbox/root/lightbox-root.js +59 -11
  16. package/dist/lightbox/zoom-controller.d.ts +5 -0
  17. package/dist/lightbox/zoom-controller.js +8 -0
  18. package/dist/menu/root/menu-root.js +2 -0
  19. package/dist/navigation/root/navigation-root.d.ts +11 -1
  20. package/dist/navigation/root/navigation-root.js +10 -5
  21. package/dist/notifications/root/notifications-root.d.ts +17 -0
  22. package/dist/notifications/root/notifications-root.js +21 -1
  23. package/dist/popover/root/popover-root.d.ts +18 -7
  24. package/dist/popover/root/popover-root.js +8 -1
  25. package/dist/splitter/root/splitter-root.d.ts +41 -0
  26. package/dist/splitter/root/splitter-root.js +77 -0
  27. package/dist/tabs/root/tabs-root.d.ts +25 -0
  28. package/dist/tabs/root/tabs-root.js +28 -1
  29. package/dist/tour/naming.d.ts +9 -3
  30. package/dist/tour/naming.js +9 -3
  31. package/dist/tour/root/tour-root.d.ts +29 -5
  32. package/dist/tour/root/tour-root.js +13 -1
  33. package/dist/tour/stop/tour-stop.d.ts +2 -1
  34. package/dist/tour/stop/tour-stop.js +4 -1
  35. package/dist/tree/root/tree-root.d.ts +13 -0
  36. package/dist/tree/root/tree-root.js +42 -2
  37. package/lint.json +1 -1
  38. package/package.json +5 -5
  39. package/types/jsx.d.ts +450 -30
  40. package/types/vue.d.ts +53 -16
  41. package/types/words.d.ts +8 -0
@@ -19,6 +19,20 @@ import { type PropertyValues, type TemplateResult } from 'lit';
19
19
  * The page is the window unless `scroller` names the element that scrolls, for
20
20
  * an app shell whose content scrolls inside a container.
21
21
  *
22
+ * **Set `hide-on-scroll` for a bar that moves out of the way.** Scrolling down
23
+ * past the bar's own height hides it, and scrolling up shows it again, as does
24
+ * focus moving into it. The custom state `hidden` is on the host while it is
25
+ * hidden, and the styled tier slides a `sticky` bar up out of view.
26
+ *
27
+ * **Set `fold-below` to fold the middle region on a narrow screen.** Below that
28
+ * many pixels of the bar's own width, the middle region is hidden behind a
29
+ * toggle at the end of the bar, named by `menu-label`. The toggle says whether
30
+ * the region is open with `aria-expanded`, and the region opens below the row.
31
+ * Escape closes it and puts focus back on the toggle. The custom state
32
+ * `folded` is on the host while the bar is that narrow, and `open` while the
33
+ * folded region is open. A server serves the bar unfolded, and the browser
34
+ * folds it once it has measured it.
35
+ *
22
36
  * The host is the page's `banner` landmark. Placed inside `main`, `article`,
23
37
  * `aside`, `nav` or `section`, it is a plain box instead, as a native
24
38
  * `<header>` is there, so a header inside a card or a pane does not add a
@@ -36,6 +50,7 @@ import { type PropertyValues, type TemplateResult } from 'lit';
36
50
  *
37
51
  * @slot - The middle region, usually a `Navigation`.
38
52
  * @slot end - Trailing content.
53
+ * @slot menu-icon - Replaces the glyph inside the toggle that opens a folded middle region.
39
54
  * @slot start - Leading content, usually a wordmark.
40
55
  *
41
56
  * @event {HelixScrolledEvent} helix-scrolled - Fired when the page crosses the
@@ -48,10 +63,20 @@ import { type PropertyValues, type TemplateResult } from 'lit';
48
63
  * @csspart end - The trailing region.
49
64
  * @csspart row - The centered row inside the banner.
50
65
  * @csspart start - The leading region.
66
+ * @csspart toggle - The control that opens and closes the folded middle region, drawn while the bar is narrower than `fold-below`.
67
+ * @csspart toggle-mark - The glyph on the toggle, where none is slotted.
51
68
  *
69
+ * @cssstate folded - While the bar is narrower than `fold-below`, and the middle region is behind the toggle.
70
+ * @cssstate hidden - While `hide-on-scroll` has the bar out of the way, after a scroll down.
71
+ * @cssstate open - While the folded middle region is open.
52
72
  * @cssstate scrolled - While the page is scrolled down from the top, which is when the bar lifts off it.
53
73
  */
54
74
  export declare class HelixHeadlessAppHeader extends HelixElement {
75
+ /**
76
+ * The key this family's words are registered under, its styled tag:
77
+ * `registerWords('de', { 'helix-app-header': { menuLabel: 'Menü' } })`.
78
+ */
79
+ static words: string;
55
80
  /**
56
81
  * Where the middle region sits in the bar.
57
82
  *
@@ -68,6 +93,47 @@ export declare class HelixHeadlessAppHeader extends HelixElement {
68
93
  * the property carries the part's name, `content`.
69
94
  */
70
95
  contentAlign?: 'start';
96
+ /**
97
+ * The width of the bar, in pixels, below which the middle region folds behind
98
+ * a toggle. Unset, it never folds.
99
+ */
100
+ foldBelow?: number;
101
+ /**
102
+ * Hides the bar as the page scrolls down past it, and shows it again as the
103
+ * page scrolls up.
104
+ */
105
+ hideOnScroll: boolean;
106
+ /** Whether the bar is narrower than `fold-below`, which only the observer sets. */
107
+ private folded;
108
+ /** Whether the folded middle region is open. */
109
+ private unfolded;
110
+ /** Watches the bar's own width while `fold-below` is set. */
111
+ private widths;
112
+ /** Starts or stops watching the width, according to `fold-below`. */
113
+ private watchWidth;
114
+ /** Folds the middle region where the bar is narrower than `fold-below`. */
115
+ private fold;
116
+ /** Opens or closes the folded middle region. */
117
+ private readonly handleToggle;
118
+ /** Escape in the open region closes it and puts focus back on the toggle. */
119
+ private readonly handleKeydown;
120
+ /** Whether the bar is out of the way, under `hide-on-scroll`. */
121
+ private away;
122
+ /** Where the page was last scrolled to, to tell down from up. */
123
+ private lastY;
124
+ /** Shows a hidden bar again when focus moves into it. */
125
+ private readonly handleFocusIn;
126
+ /** Hides the bar or shows it again, under `hide-on-scroll`. */
127
+ private hide;
128
+ /**
129
+ * Hides the bar on a scroll down past its own height, and shows it on a
130
+ * scroll up. A few pixels either way are not a direction, so a bar does not
131
+ * flicker under a hand resting on a trackpad. Not while focus is in the bar
132
+ * or its folded region is open, which a reader is using.
133
+ */
134
+ private follow;
135
+ /** The name of the toggle that opens a folded middle region. */
136
+ menuLabel: string;
71
137
  /**
72
138
  * The id of the element whose scrolling lifts the bar, for an app shell that
73
139
  * scrolls a container instead of the window. Unset is the window.
@@ -111,7 +177,16 @@ export declare class HelixHeadlessAppHeader extends HelixElement {
111
177
  /** Whether this header is the page's banner, read from where it sits. */
112
178
  private readLandmark;
113
179
  disconnectedCallback(): void;
180
+ /** The `fold-below` the width watch was started for, so a change is measured at once. */
181
+ private watchedFor;
182
+ protected firstUpdated(changed: PropertyValues): void;
114
183
  protected updated(changed: PropertyValues): void;
184
+ /**
185
+ * The glyph on the toggle that opens a folded middle region, behind a seam:
186
+ * slot fallback, so a consumer replaces it by slotting, and a method, so a
187
+ * tier above replaces it by overriding.
188
+ */
189
+ protected renderMenuIcon(): TemplateResult;
115
190
  protected render(): TemplateResult;
116
191
  }
117
192
  declare global {
@@ -4,18 +4,41 @@ 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, elementById, HelixElement, HelixScrolledEvent, hostAria, } from '@fusion.dev/helix-core';
8
- import { html } from 'lit';
9
- import { property } from 'lit/decorators.js';
7
+ import { customElement, deepActiveElement, deepContains, elementById, HelixElement, HelixScrolledEvent, hostAria, words, } from '@fusion.dev/helix-core';
8
+ import { list } from '@fusion.dev/helix-icons';
9
+ import { html, nothing } from 'lit';
10
+ import { property, state } from 'lit/decorators.js';
11
+ import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
10
12
  import { inSectioningContent } from '../sectioning.js';
11
13
  let HelixHeadlessAppHeader = class HelixHeadlessAppHeader extends HelixElement {
12
14
  constructor() {
13
15
  super(...arguments);
16
+ this.hideOnScroll = false;
17
+ this.folded = false;
18
+ this.unfolded = false;
19
+ this.handleToggle = () => {
20
+ this.unfolded = !this.unfolded;
21
+ };
22
+ this.handleKeydown = (event) => {
23
+ if (event.key !== 'Escape' || !this.unfolded || event.defaultPrevented) {
24
+ return;
25
+ }
26
+ event.preventDefault();
27
+ this.unfolded = false;
28
+ void this.updateComplete.then(() => this.partNamed('toggle')?.focus());
29
+ };
30
+ this.away = false;
31
+ this.lastY = 0;
32
+ this.handleFocusIn = () => {
33
+ this.hide(false);
34
+ };
35
+ this.menuLabel = 'Menu';
14
36
  this.landmark = 'banner';
15
37
  this.scrolled = false;
16
38
  this.handleScroll = () => {
17
39
  const source = this.source ?? window;
18
40
  const y = source instanceof Element ? source.scrollTop : window.scrollY;
41
+ this.follow(y);
19
42
  const next = this.scrolled ? y > 2 : y > 8;
20
43
  if (next === this.scrolled) {
21
44
  return;
@@ -25,6 +48,57 @@ let HelixHeadlessAppHeader = class HelixHeadlessAppHeader extends HelixElement {
25
48
  this.dispatchEvent(new HelixScrolledEvent({ scrolled: next }));
26
49
  };
27
50
  }
51
+ static { this.words = 'helix-app-header'; }
52
+ watchWidth() {
53
+ if (!this.foldBelow || typeof ResizeObserver === 'undefined') {
54
+ this.widths?.disconnect();
55
+ this.widths = undefined;
56
+ this.folded = false;
57
+ return;
58
+ }
59
+ if (this.widths) {
60
+ return;
61
+ }
62
+ this.widths = new ResizeObserver(([entry]) => {
63
+ const width = entry?.borderBoxSize?.[0]?.inlineSize ?? entry?.contentRect.width ?? 0;
64
+ if (width > 0) {
65
+ this.fold(width);
66
+ }
67
+ });
68
+ this.widths.observe(this);
69
+ }
70
+ fold(width) {
71
+ const folded = this.foldBelow !== undefined && width < this.foldBelow;
72
+ if (folded !== this.folded) {
73
+ this.folded = folded;
74
+ this.unfolded = false;
75
+ }
76
+ }
77
+ hide(away) {
78
+ if (away === this.away) {
79
+ return;
80
+ }
81
+ this.away = away;
82
+ this.toggleState('hidden', away);
83
+ }
84
+ follow(y) {
85
+ const moved = y - this.lastY;
86
+ this.lastY = y;
87
+ if (!this.hideOnScroll) {
88
+ this.hide(false);
89
+ return;
90
+ }
91
+ const height = this.getBoundingClientRect().height;
92
+ if (y <= height) {
93
+ this.hide(false);
94
+ }
95
+ else if (moved > 4 && !this.unfolded && !deepContains(this, deepActiveElement())) {
96
+ this.hide(true);
97
+ }
98
+ else if (moved < -4) {
99
+ this.hide(false);
100
+ }
101
+ }
28
102
  findSource() {
29
103
  return (this.scroller ? elementById(this, this.scroller) : null) ?? window;
30
104
  }
@@ -44,6 +118,8 @@ let HelixHeadlessAppHeader = class HelixHeadlessAppHeader extends HelixElement {
44
118
  willUpdate(changed) {
45
119
  super.willUpdate(changed);
46
120
  hostAria(this.internals, { role: this.landmark });
121
+ this.toggleState('folded', this.folded);
122
+ this.toggleState('open', this.folded && this.unfolded);
47
123
  }
48
124
  connectedCallback() {
49
125
  super.connectedCallback();
@@ -54,6 +130,11 @@ let HelixHeadlessAppHeader = class HelixHeadlessAppHeader extends HelixElement {
54
130
  }
55
131
  });
56
132
  this.listen();
133
+ this.addEventListener('focusin', this.handleFocusIn);
134
+ this.addEventListener('keydown', this.handleKeydown);
135
+ if (this.hasUpdated) {
136
+ this.watchWidth();
137
+ }
57
138
  }
58
139
  readLandmark() {
59
140
  this.landmark = inSectioningContent(this) ? '' : 'banner';
@@ -63,19 +144,57 @@ let HelixHeadlessAppHeader = class HelixHeadlessAppHeader extends HelixElement {
63
144
  super.disconnectedCallback();
64
145
  this.source?.removeEventListener('scroll', this.handleScroll);
65
146
  this.source = undefined;
147
+ this.removeEventListener('focusin', this.handleFocusIn);
148
+ this.removeEventListener('keydown', this.handleKeydown);
149
+ this.widths?.disconnect();
150
+ this.widths = undefined;
151
+ }
152
+ firstUpdated(changed) {
153
+ super.firstUpdated(changed);
154
+ this.watchedFor = this.foldBelow;
155
+ this.watchWidth();
66
156
  }
67
157
  updated(changed) {
68
158
  super.updated(changed);
69
159
  if (changed.has('scroller') && this.isConnected) {
70
160
  this.listen();
71
161
  }
162
+ if (this.foldBelow !== this.watchedFor) {
163
+ this.watchedFor = this.foldBelow;
164
+ this.watchWidth();
165
+ const width = this.getBoundingClientRect().width;
166
+ if (this.widths && width > 0) {
167
+ this.fold(width);
168
+ }
169
+ }
170
+ if (changed.has('hideOnScroll') && !this.hideOnScroll) {
171
+ this.hide(false);
172
+ }
173
+ }
174
+ renderMenuIcon() {
175
+ return html `<slot name="menu-icon">
176
+ <svg part="toggle-mark" fill="currentColor" viewBox=${list.viewBox} aria-hidden="true">
177
+ ${unsafeSVG(list.content)}
178
+ </svg>
179
+ </slot>`;
72
180
  }
73
181
  render() {
74
182
  return html `
75
183
  <div part="row">
76
184
  <div part="start"><slot name="start"></slot></div>
77
- <div part="content"><slot></slot></div>
78
- <div part="end"><slot name="end"></slot></div>
185
+ <div part="content" id="content" ?hidden=${this.folded && !this.unfolded}><slot></slot></div>
186
+ <div part="end"><slot name="end"></slot>${this.folded
187
+ ? html `<button
188
+ part="toggle"
189
+ type="button"
190
+ aria-controls="content"
191
+ aria-expanded=${this.unfolded ? 'true' : 'false'}
192
+ aria-label=${this.menuLabel}
193
+ @click=${this.handleToggle}
194
+ >
195
+ ${this.renderMenuIcon()}
196
+ </button>`
197
+ : nothing}</div>
79
198
  </div>
80
199
  `;
81
200
  }
@@ -83,6 +202,22 @@ let HelixHeadlessAppHeader = class HelixHeadlessAppHeader extends HelixElement {
83
202
  __decorate([
84
203
  property({ attribute: 'content-align', reflect: true })
85
204
  ], HelixHeadlessAppHeader.prototype, "contentAlign", void 0);
205
+ __decorate([
206
+ property({ attribute: 'fold-below', type: Number })
207
+ ], HelixHeadlessAppHeader.prototype, "foldBelow", void 0);
208
+ __decorate([
209
+ property({ attribute: 'hide-on-scroll', type: Boolean })
210
+ ], HelixHeadlessAppHeader.prototype, "hideOnScroll", void 0);
211
+ __decorate([
212
+ state()
213
+ ], HelixHeadlessAppHeader.prototype, "folded", void 0);
214
+ __decorate([
215
+ state()
216
+ ], HelixHeadlessAppHeader.prototype, "unfolded", void 0);
217
+ __decorate([
218
+ words,
219
+ property({ attribute: 'menu-label' })
220
+ ], HelixHeadlessAppHeader.prototype, "menuLabel", void 0);
86
221
  __decorate([
87
222
  property()
88
223
  ], HelixHeadlessAppHeader.prototype, "scroller", void 0);
@@ -1,5 +1,5 @@
1
1
  import { HelixElement } from '@fusion.dev/helix-core';
2
- import { type PropertyValues, type TemplateResult } from 'lit';
2
+ import { type CSSResultGroup, type PropertyValues, type TemplateResult } from 'lit';
3
3
  /**
4
4
  * @summary One step in a breadcrumb trail.
5
5
  *
@@ -18,7 +18,10 @@ import { type PropertyValues, type TemplateResult } from 'lit';
18
18
  * including your own anchor.
19
19
  *
20
20
  * The current page is plain text, and `aria-current="page"` on the host conveys
21
- * the position.
21
+ * the position. In a trail where no crumb is `current`, the last one is said
22
+ * to be the current page, and keeps its link. An `aria-current` you write on
23
+ * the host, such as `aria-current="location"`, replaces the one the crumb
24
+ * gives itself.
22
25
  *
23
26
  * @since 0.1.0
24
27
  * @status stable
@@ -42,6 +45,7 @@ import { type PropertyValues, type TemplateResult } from 'lit';
42
45
  * the label and what follows it into one control.
43
46
  */
44
47
  export declare class HelixHeadlessBreadcrumb extends HelixElement {
48
+ static styles: CSSResultGroup;
45
49
  /**
46
50
  * Marks this item as the page you are on.
47
51
  */
@@ -65,6 +69,11 @@ export declare class HelixHeadlessBreadcrumb extends HelixElement {
65
69
  * of `breadcrumbs/links` lost its padding at once.
66
70
  */
67
71
  href?: string;
72
+ /**
73
+ * @internal Set by the trail: this is its last crumb and no crumb is
74
+ * `current`, so this one is said to be the current page.
75
+ */
76
+ implied: boolean;
68
77
  /**
69
78
  * The link's `rel` value, which describes how the linked page relates to this
70
79
  * one.
@@ -6,23 +6,29 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
6
6
  };
7
7
  import { customElement, HelixElement, hostAria, slotFilled, slotNamed, } from '@fusion.dev/helix-core';
8
8
  import { caretRight } from '@fusion.dev/helix-icons';
9
- import { html, nothing } from 'lit';
9
+ import { css, html, nothing, } from 'lit';
10
10
  import { property } from 'lit/decorators.js';
11
11
  import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
12
12
  let HelixHeadlessBreadcrumb = class HelixHeadlessBreadcrumb extends HelixElement {
13
13
  constructor() {
14
14
  super(...arguments);
15
15
  this.current = false;
16
+ this.implied = false;
16
17
  this.rel = '';
17
18
  this.target = '';
18
19
  this.handleEnd = () => {
19
20
  this.markEnd();
20
21
  };
21
22
  }
23
+ static { this.styles = css `
24
+ :host(:first-child) [part='separator'] {
25
+ display: none;
26
+ }
27
+ `; }
22
28
  willUpdate(changed) {
23
29
  super.willUpdate(changed);
24
30
  hostAria(this.internals, { role: 'listitem' });
25
- this.internals.ariaCurrent = this.current ? 'page' : null;
31
+ this.internals.ariaCurrent = this.current || this.implied ? 'page' : null;
26
32
  }
27
33
  updated(changed) {
28
34
  super.updated(changed);
@@ -60,6 +66,9 @@ __decorate([
60
66
  __decorate([
61
67
  property({ reflect: true })
62
68
  ], HelixHeadlessBreadcrumb.prototype, "href", void 0);
69
+ __decorate([
70
+ property({ attribute: false })
71
+ ], HelixHeadlessBreadcrumb.prototype, "implied", void 0);
63
72
  __decorate([
64
73
  property()
65
74
  ], HelixHeadlessBreadcrumb.prototype, "rel", void 0);
@@ -1,6 +1,5 @@
1
1
  import { HelixElement } from '@fusion.dev/helix-core';
2
2
  import { nothing, type PropertyValues, type TemplateResult } from 'lit';
3
- import '../breadcrumb/breadcrumb.js';
4
3
  /** One step of a trail written as data, for `items`. */
5
4
  export interface BreadcrumbItem {
6
5
  /** The URL the step links to. The last step is the current page and takes none. */
@@ -26,7 +25,8 @@ export interface BreadcrumbItem {
26
25
  * markers.
27
26
  *
28
27
  * The items are slotted directly, so they are `<ol>` children in the flat tree,
29
- * and each item sets `role="listitem"` on itself.
28
+ * and each item sets `role="listitem"` on itself. Where no slotted crumb is
29
+ * `current`, the last one is said to be the current page.
30
30
  *
31
31
  * The trail does not measure itself, and you write its overflow in markup: put
32
32
  * the crumb that holds the folded items where the gap should appear.
@@ -41,6 +41,11 @@ export interface BreadcrumbItem {
41
41
  * to the first crumb it brought back. A new `items` folds again. Slotted crumbs
42
42
  * do not fold; write the folded crumb where the gap should be.
43
43
  *
44
+ * **Keyboard.** Each crumb with an `href` is a link and one tab stop, and
45
+ * Enter follows it. The current page and the separators are not in the tab
46
+ * order. In a folded trail, Enter or Space on the fold's button shows the
47
+ * whole trail and moves focus to the first crumb it brought back.
48
+ *
44
49
  * @since 0.1.0
45
50
  * @status stable
46
51
  * @category navigation
@@ -96,8 +101,15 @@ export declare class HelixHeadlessBreadcrumbs extends HelixElement {
96
101
  /** Shows the whole trail and moves focus to the first crumb it brought back. */
97
102
  private readonly expand;
98
103
  protected willUpdate(changed: PropertyValues): void;
104
+ protected updated(changed: PropertyValues): void;
99
105
  /** How many crumbs drawn from `items` are folded away, or 0. */
100
106
  private get folded();
107
+ /**
108
+ * Says the last slotted crumb is the current page where none is marked
109
+ * `current`, as React Aria's trail does. Through a property the crumb does
110
+ * not reflect, so nothing is written onto your markup.
111
+ */
112
+ private readonly markLast;
101
113
  /** The crumbs `items` describes, each a crumb of this tier. */
102
114
  protected renderItems(): TemplateResult | typeof nothing;
103
115
  protected render(): TemplateResult;
@@ -4,12 +4,12 @@ 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, formatWords, HelixElement, hostAria, words } from '@fusion.dev/helix-core';
7
+ import { customElement, formatWords, HelixElement, hostAria, whenHydrated, words, } from '@fusion.dev/helix-core';
8
8
  import { nothing } from 'lit';
9
9
  import { property, state } from 'lit/decorators.js';
10
10
  import { repeat } from 'lit/directives/repeat.js';
11
11
  import { html } from 'lit/static-html.js';
12
- import '../breadcrumb/breadcrumb.js';
12
+ import { HelixHeadlessBreadcrumb } from '../breadcrumb/breadcrumb.js';
13
13
  let HelixHeadlessBreadcrumbs = class HelixHeadlessBreadcrumbs extends HelixElement {
14
14
  constructor() {
15
15
  super(...arguments);
@@ -24,6 +24,15 @@ let HelixHeadlessBreadcrumbs = class HelixHeadlessBreadcrumbs extends HelixEleme
24
24
  crumb?.shadowRoot?.querySelector('a, [part="link"]')?.focus();
25
25
  });
26
26
  };
27
+ this.markLast = () => {
28
+ const crumbs = (this.renderRoot.querySelector('slot:not([name])')?.assignedElements() ?? []).filter((one) => one instanceof HelixHeadlessBreadcrumb);
29
+ const marked = crumbs.some((one) => one.current);
30
+ crumbs.forEach((crumb, index) => {
31
+ whenHydrated(crumb, () => {
32
+ crumb.implied = !marked && this.items.length === 0 && index === crumbs.length - 1;
33
+ });
34
+ });
35
+ };
27
36
  }
28
37
  static { this.words = 'helix-breadcrumbs'; }
29
38
  willUpdate(changed) {
@@ -33,6 +42,10 @@ let HelixHeadlessBreadcrumbs = class HelixHeadlessBreadcrumbs extends HelixEleme
33
42
  this.expanded = false;
34
43
  }
35
44
  }
45
+ updated(changed) {
46
+ super.updated(changed);
47
+ this.markLast();
48
+ }
36
49
  get folded() {
37
50
  const most = this.maxItems ?? 0;
38
51
  const count = this.items.length;
@@ -77,7 +90,7 @@ let HelixHeadlessBreadcrumbs = class HelixHeadlessBreadcrumbs extends HelixEleme
77
90
  }
78
91
  render() {
79
92
  return html `<ol part="list" role="list">
80
- ${this.renderItems()}<slot></slot>
93
+ ${this.renderItems()}<slot @slotchange=${this.markLast}></slot>
81
94
  </ol>`;
82
95
  }
83
96
  };
@@ -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
  }