@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.
- package/custom-elements.json +648 -28
- package/dist/app-header/root/app-header-root.d.ts +75 -0
- package/dist/app-header/root/app-header-root.js +140 -5
- package/dist/breadcrumbs/breadcrumb/breadcrumb.d.ts +11 -2
- package/dist/breadcrumbs/breadcrumb/breadcrumb.js +11 -2
- package/dist/breadcrumbs/root/breadcrumbs-root.d.ts +14 -2
- package/dist/breadcrumbs/root/breadcrumbs-root.js +16 -3
- package/dist/dialog/description.d.ts +10 -0
- package/dist/dialog/description.js +22 -8
- package/dist/dialog/root/dialog-root.d.ts +17 -1
- package/dist/dialog/root/dialog-root.js +2 -2
- package/dist/drawer/root/drawer-root.d.ts +38 -2
- package/dist/drawer/root/drawer-root.js +33 -6
- package/dist/lightbox/root/lightbox-root.d.ts +44 -6
- package/dist/lightbox/root/lightbox-root.js +59 -11
- package/dist/lightbox/zoom-controller.d.ts +5 -0
- package/dist/lightbox/zoom-controller.js +8 -0
- package/dist/menu/root/menu-root.js +2 -0
- package/dist/navigation/root/navigation-root.d.ts +11 -1
- package/dist/navigation/root/navigation-root.js +10 -5
- package/dist/notifications/root/notifications-root.d.ts +17 -0
- package/dist/notifications/root/notifications-root.js +21 -1
- package/dist/popover/root/popover-root.d.ts +18 -7
- package/dist/popover/root/popover-root.js +8 -1
- package/dist/splitter/root/splitter-root.d.ts +41 -0
- package/dist/splitter/root/splitter-root.js +77 -0
- package/dist/tabs/root/tabs-root.d.ts +25 -0
- package/dist/tabs/root/tabs-root.js +28 -1
- package/dist/tour/naming.d.ts +9 -3
- package/dist/tour/naming.js +9 -3
- package/dist/tour/root/tour-root.d.ts +29 -5
- package/dist/tour/root/tour-root.js +13 -1
- package/dist/tour/stop/tour-stop.d.ts +2 -1
- package/dist/tour/stop/tour-stop.js +4 -1
- package/dist/tree/root/tree-root.d.ts +13 -0
- package/dist/tree/root/tree-root.js +42 -2
- package/lint.json +1 -1
- package/package.json +5 -5
- package/types/jsx.d.ts +450 -30
- package/types/vue.d.ts +53 -16
- 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 {
|
|
9
|
-
import {
|
|
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
|
|
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
|
|
17
|
+
export const wordsAmong = (elements, skip = null) => {
|
|
18
18
|
const words = [];
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
24
|
-
|
|
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
|
}
|