@fusion.dev/helix-headless 0.153.0 → 0.155.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 +1885 -140
- package/dist/accordion/item/accordion-item.d.ts +15 -0
- package/dist/accordion/item/accordion-item.js +12 -1
- package/dist/accordion/root/accordion-root.d.ts +2 -1
- package/dist/accordion/root/accordion-root.js +1 -0
- package/dist/badge/root/badge-root.d.ts +15 -2
- package/dist/badge/root/badge-root.js +20 -3
- package/dist/breadcrumbs/breadcrumb/breadcrumb.d.ts +36 -3
- package/dist/breadcrumbs/breadcrumb/breadcrumb.js +42 -2
- package/dist/breadcrumbs/root/breadcrumbs-root.d.ts +15 -5
- package/dist/breadcrumbs/root/breadcrumbs-root.js +29 -4
- package/dist/button-group/root/button-group-root.d.ts +2 -0
- package/dist/button-group/root/button-group-root.js +1 -0
- package/dist/card/root/card-root.d.ts +31 -11
- package/dist/card/root/card-root.js +25 -11
- package/dist/code/root/code-root.d.ts +7 -0
- package/dist/code/root/code-root.js +1 -1
- package/dist/code-block/root/code-block-root.d.ts +37 -1
- package/dist/code-block/root/code-block-root.js +38 -5
- package/dist/color-input/root/color-input-root.d.ts +9 -3
- package/dist/color-input/root/color-input-root.js +3 -1
- package/dist/color-picker/channels.d.ts +5 -3
- package/dist/color-picker/channels.js +79 -21
- package/dist/color-picker/root/color-picker-root.d.ts +58 -5
- package/dist/color-picker/root/color-picker-root.js +75 -13
- package/dist/date-input/root/date-input-root.d.ts +1 -0
- package/dist/date-input/root/date-input-root.js +2 -1
- package/dist/date-picker/root/date-picker-root.d.ts +43 -13
- package/dist/date-picker/root/date-picker-root.js +54 -31
- package/dist/element-events.generated.js +2 -1
- package/dist/empty/root/empty-root.d.ts +19 -1
- package/dist/empty/root/empty-root.js +13 -4
- package/dist/icon/fetch.d.ts +20 -0
- package/dist/icon/fetch.js +70 -0
- package/dist/icon/root/icon-root.d.ts +27 -5
- package/dist/icon/root/icon-root.js +41 -17
- package/dist/navigation/root/navigation-root.d.ts +26 -0
- package/dist/navigation/root/navigation-root.js +58 -1
- package/dist/quote/root/quote-root.d.ts +17 -1
- package/dist/quote/root/quote-root.js +15 -2
- package/dist/select/root/select-root.d.ts +29 -3
- package/dist/select/root/select-root.js +34 -9
- package/dist/skeleton/root/skeleton-root.d.ts +7 -0
- package/dist/skeleton/root/skeleton-root.js +4 -1
- package/dist/slider/root/slider-root.d.ts +71 -9
- package/dist/slider/root/slider-root.js +129 -42
- package/dist/speed-dial/item/speed-dial-item.d.ts +29 -1
- package/dist/speed-dial/item/speed-dial-item.js +48 -7
- package/dist/speed-dial/root/speed-dial-root.d.ts +32 -6
- package/dist/speed-dial/root/speed-dial-root.js +50 -5
- package/dist/splitter/root/splitter-root.d.ts +22 -0
- package/dist/splitter/root/splitter-root.js +36 -7
- package/dist/stat/group/stat-group.d.ts +23 -4
- package/dist/stat/group/stat-group.js +13 -1
- package/dist/stat/root/stat-root.d.ts +46 -1
- package/dist/stat/root/stat-root.js +29 -5
- package/dist/steps/reach.d.ts +5 -1
- package/dist/steps/reach.js +2 -2
- package/dist/steps/root/steps-root.d.ts +37 -2
- package/dist/steps/root/steps-root.js +49 -7
- package/dist/steps/step/step.d.ts +6 -0
- package/dist/steps/step/step.js +4 -0
- package/dist/tabs/root/tabs-root.d.ts +37 -0
- package/dist/tabs/root/tabs-root.js +92 -2
- package/dist/tabs/tab/tab.d.ts +38 -2
- package/dist/tabs/tab/tab.js +45 -4
- package/dist/tag/group/tag-group.d.ts +29 -3
- package/dist/tag/group/tag-group.js +42 -11
- package/dist/toggle/root/toggle-root.d.ts +2 -0
- package/dist/toggle/root/toggle-root.js +1 -1
- package/dist/tree/item/tree-item.d.ts +10 -0
- package/dist/tree/item/tree-item.js +12 -0
- package/dist/tree/root/tree-root.d.ts +40 -1
- package/dist/tree/root/tree-root.js +120 -4
- package/dist/tree/selection.d.ts +6 -0
- package/dist/tree/selection.js +11 -0
- package/lint.json +1 -1
- package/package.json +5 -5
- package/types/jsx.d.ts +1412 -207
- package/types/vue.d.ts +294 -65
- package/types/words.d.ts +49 -1
|
@@ -6,6 +6,7 @@ registerElementEvents({
|
|
|
6
6
|
'helix-headless-area-chart': ['helix-reading-change', 'helix-reading-press'],
|
|
7
7
|
'helix-headless-avatar': ['error', 'load'],
|
|
8
8
|
'helix-headless-bar-chart': ['helix-reading-change', 'helix-reading-press'],
|
|
9
|
+
'helix-headless-breadcrumb': ['helix-select'],
|
|
9
10
|
'helix-headless-button': ['helix-press'],
|
|
10
11
|
'helix-headless-button-group': ['change', 'helix-before-change'],
|
|
11
12
|
'helix-headless-callout': ['helix-dismiss'],
|
|
@@ -241,7 +242,7 @@ registerElementEvents({
|
|
|
241
242
|
'helix-headless-step': ['helix-select'],
|
|
242
243
|
'helix-headless-steps': ['helix-activate', 'helix-before-activate', 'helix-select'],
|
|
243
244
|
'helix-headless-switch': ['change', 'helix-before-toggle', 'input'],
|
|
244
|
-
'helix-headless-tab': ['helix-select'],
|
|
245
|
+
'helix-headless-tab': ['helix-dismiss', 'helix-select'],
|
|
245
246
|
'helix-headless-table': [
|
|
246
247
|
'helix-before-reorder',
|
|
247
248
|
'helix-reorder',
|
|
@@ -17,7 +17,16 @@ import { type PropertyValues, type TemplateResult } from 'lit';
|
|
|
17
17
|
*
|
|
18
18
|
* Every region is a bare slot. There is no `empty-state-heading` or
|
|
19
19
|
* `empty-state-actions` element: assign your own heading and actions to the
|
|
20
|
-
* slots.
|
|
20
|
+
* slots. The icon, the message and the actions slots are parts of the same
|
|
21
|
+
* names, as Card's regions are. A slot draws no box, so a part passes
|
|
22
|
+
* inherited properties, such as color and font, to what is slotted, and a
|
|
23
|
+
* page wanting a box there gives it one with `display`, such as a row of
|
|
24
|
+
* actions with `display: flex`.
|
|
25
|
+
*
|
|
26
|
+
* **The view says which regions it holds**, as `:state(has-icon)` and
|
|
27
|
+
* `:state(has-actions)`, so a sheet can space a view with no icon or no
|
|
28
|
+
* actions differently. Read in the browser, so a served view carries neither
|
|
29
|
+
* until the script runs.
|
|
21
30
|
*
|
|
22
31
|
* **Or give the heading as words.** `heading` draws them in a `heading` part
|
|
23
32
|
* that is a heading at `heading-level`, 3 where it is not set, so a page
|
|
@@ -42,8 +51,14 @@ import { type PropertyValues, type TemplateResult } from 'lit';
|
|
|
42
51
|
* @slot heading - The heading, which names what is missing. A heading element, such as an `<h3>`, keeps its level; any other element is read as a heading at `heading-level`. Replaces the one `heading` draws.
|
|
43
52
|
* @slot icon - An icon above the heading.
|
|
44
53
|
*
|
|
54
|
+
* @csspart actions - The slot the actions arrive through. A slot draws no box, so it passes inherited properties to the actions; `display: flex` lays them out in a row.
|
|
45
55
|
* @csspart heading - The heading at `heading-level`: the one `heading` draws while nothing is slotted into `heading`, or the one around a slotted element that is no heading.
|
|
56
|
+
* @csspart icon - The slot the icon arrives through, drawing no box, as `actions` does.
|
|
46
57
|
* @csspart live - A copy of the heading and the message, in a live region that screen readers announce when the view appears. Visually hidden in the styled tier.
|
|
58
|
+
* @csspart message - The slot the message arrives through, drawing no box, as `actions` does.
|
|
59
|
+
*
|
|
60
|
+
* @cssstate has-actions - Something is slotted into `actions`.
|
|
61
|
+
* @cssstate has-icon - Something is slotted into `icon`.
|
|
47
62
|
*/
|
|
48
63
|
export declare class HelixHeadlessEmpty extends HelixElement {
|
|
49
64
|
/**
|
|
@@ -75,6 +90,9 @@ export declare class HelixHeadlessEmpty extends HelixElement {
|
|
|
75
90
|
private looseHeading;
|
|
76
91
|
protected willUpdate(changed: PropertyValues): void;
|
|
77
92
|
protected firstUpdated(changed: PropertyValues): void;
|
|
93
|
+
protected updated(changed: PropertyValues): void;
|
|
94
|
+
/** Says which regions hold something, one call apiece so each pairs with its `@cssstate`. */
|
|
95
|
+
private readonly readRegions;
|
|
78
96
|
protected readonly arrival: ArrivalController;
|
|
79
97
|
/**
|
|
80
98
|
* The `heading` slot, with the heading `heading` draws as its fallback, so
|
|
@@ -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 { ArrivalController, customElement, HelixElement, headingIn, headingLevel, slottedWords, } from '@fusion.dev/helix-core';
|
|
7
|
+
import { ArrivalController, customElement, HelixElement, headingIn, headingLevel, slotFilled, slottedWords, } from '@fusion.dev/helix-core';
|
|
8
8
|
import { html, nothing } from 'lit';
|
|
9
9
|
import { property } from 'lit/decorators.js';
|
|
10
10
|
let HelixHeadlessEmpty = class HelixHeadlessEmpty extends HelixElement {
|
|
@@ -13,6 +13,11 @@ let HelixHeadlessEmpty = class HelixHeadlessEmpty extends HelixElement {
|
|
|
13
13
|
this.heading = '';
|
|
14
14
|
this.noAnnounce = false;
|
|
15
15
|
this.looseHeading = false;
|
|
16
|
+
this.readRegions = () => {
|
|
17
|
+
const filled = (name) => slotFilled(this.renderRoot?.querySelector(`slot[name="${name}"]`));
|
|
18
|
+
this.toggleState('has-icon', filled('icon'));
|
|
19
|
+
this.toggleState('has-actions', filled('actions'));
|
|
20
|
+
};
|
|
16
21
|
this.arrival = new ArrivalController(this, {
|
|
17
22
|
arriving: () => !this.serverRendered && document.readyState === 'complete',
|
|
18
23
|
text: () => [slottedWords(this, ['heading']) || this.heading.trim(), slottedWords(this, [''])]
|
|
@@ -31,6 +36,10 @@ let HelixHeadlessEmpty = class HelixHeadlessEmpty extends HelixElement {
|
|
|
31
36
|
super.firstUpdated(changed);
|
|
32
37
|
this.finishHydration();
|
|
33
38
|
}
|
|
39
|
+
updated(changed) {
|
|
40
|
+
super.updated(changed);
|
|
41
|
+
this.readRegions();
|
|
42
|
+
}
|
|
34
43
|
renderHeading(hidden) {
|
|
35
44
|
const level = headingLevel(this.headingLevel);
|
|
36
45
|
const slot = html `<slot
|
|
@@ -58,10 +67,10 @@ let HelixHeadlessEmpty = class HelixHeadlessEmpty extends HelixElement {
|
|
|
58
67
|
render() {
|
|
59
68
|
const say = () => this.arrival.say();
|
|
60
69
|
const held = this.arrival.said !== '' && !this.noAnnounce;
|
|
61
|
-
return html `<slot name="icon"></slot>
|
|
70
|
+
return html `<slot name="icon" part="icon" @slotchange=${this.readRegions}></slot>
|
|
62
71
|
${this.renderHeading(held)}
|
|
63
|
-
<slot aria-hidden=${held ? 'true' : nothing} @slotchange=${say}></slot>
|
|
64
|
-
<slot name="actions"></slot>
|
|
72
|
+
<slot aria-hidden=${held ? 'true' : nothing} part="message" @slotchange=${say}></slot>
|
|
73
|
+
<slot name="actions" part="actions" @slotchange=${this.readRegions}></slot>
|
|
65
74
|
${this.noAnnounce
|
|
66
75
|
? nothing
|
|
67
76
|
: html `<span part="live" role="status">${this.arrival.said}</span>`}`;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { HelixIcon } from '@fusion.dev/helix-icons';
|
|
2
|
+
/**
|
|
3
|
+
* Reads an SVG file's text as an icon, or `undefined` where it is no SVG.
|
|
4
|
+
*
|
|
5
|
+
* **What could run is taken out**, because the markup is drawn as it is: every
|
|
6
|
+
* `<script>` and `<foreignObject>`, every `on…` attribute, every link to a
|
|
7
|
+
* `javascript:` URL, and every animation that writes a link or a handler. The
|
|
8
|
+
* root's paint attributes are kept on a group around the shapes, and its
|
|
9
|
+
* `viewBox`, or its width and height, is the box.
|
|
10
|
+
*
|
|
11
|
+
* @param text The file's text.
|
|
12
|
+
* @param name What the icon is known by, its URL.
|
|
13
|
+
*/
|
|
14
|
+
export declare const readIconFile: (text: string, name: string) => HelixIcon | undefined;
|
|
15
|
+
/**
|
|
16
|
+
* The icon in the SVG file at `src`: at once where it has been fetched, or a
|
|
17
|
+
* promise of it the first time, fetched once a page however many elements ask.
|
|
18
|
+
* `undefined` where the file could not be fetched or is no SVG.
|
|
19
|
+
*/
|
|
20
|
+
export declare const fetchIcon: (src: string) => HelixIcon | Promise<HelixIcon | undefined> | undefined;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
const PAINT = [
|
|
2
|
+
'fill',
|
|
3
|
+
'fill-rule',
|
|
4
|
+
'stroke',
|
|
5
|
+
'stroke-linecap',
|
|
6
|
+
'stroke-linejoin',
|
|
7
|
+
'stroke-miterlimit',
|
|
8
|
+
'stroke-width',
|
|
9
|
+
];
|
|
10
|
+
const KEY = Symbol.for('helix.icon-fetched');
|
|
11
|
+
const fetched = () => {
|
|
12
|
+
const held = globalThis;
|
|
13
|
+
let found = held[KEY];
|
|
14
|
+
if (!found) {
|
|
15
|
+
found = { answers: new Map() };
|
|
16
|
+
held[KEY] = found;
|
|
17
|
+
}
|
|
18
|
+
return found;
|
|
19
|
+
};
|
|
20
|
+
export const readIconFile = (text, name) => {
|
|
21
|
+
const parsed = new DOMParser().parseFromString(text, 'image/svg+xml');
|
|
22
|
+
const root = parsed.documentElement;
|
|
23
|
+
if (root?.localName !== 'svg' || parsed.getElementsByTagName('parsererror').length > 0) {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
for (const unsafe of [...root.querySelectorAll('script, foreignObject')]) {
|
|
27
|
+
unsafe.remove();
|
|
28
|
+
}
|
|
29
|
+
for (const animation of [...root.querySelectorAll('animate, set')]) {
|
|
30
|
+
const target = (animation.getAttribute('attributeName') ?? '').toLowerCase();
|
|
31
|
+
if (target.endsWith('href') || target.startsWith('on')) {
|
|
32
|
+
animation.remove();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
for (const element of [root, ...root.querySelectorAll('*')]) {
|
|
36
|
+
for (const attribute of [...element.attributes]) {
|
|
37
|
+
const link = attribute.localName === 'href' && /^\s*javascript:/i.test(attribute.value);
|
|
38
|
+
if (attribute.localName.toLowerCase().startsWith('on') || link) {
|
|
39
|
+
element.removeAttribute(attribute.name);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const width = Number.parseFloat(root.getAttribute('width') ?? '') || 24;
|
|
44
|
+
const height = Number.parseFloat(root.getAttribute('height') ?? '') || width;
|
|
45
|
+
const viewBox = root.getAttribute('viewBox') ?? `0 0 ${width} ${height}`;
|
|
46
|
+
const paint = PAINT.filter((one) => root.hasAttribute(one))
|
|
47
|
+
.map((one) => `${one}="${(root.getAttribute(one) ?? '').replaceAll('"', '"')}"`)
|
|
48
|
+
.join(' ');
|
|
49
|
+
const shapes = [...root.childNodes]
|
|
50
|
+
.map((node) => (node instanceof Element ? new XMLSerializer().serializeToString(node) : ''))
|
|
51
|
+
.join('');
|
|
52
|
+
return { content: paint ? `<g ${paint}>${shapes}</g>` : shapes, name, viewBox };
|
|
53
|
+
};
|
|
54
|
+
export const fetchIcon = (src) => {
|
|
55
|
+
const held = fetched();
|
|
56
|
+
if (held.answers.has(src)) {
|
|
57
|
+
return held.answers.get(src);
|
|
58
|
+
}
|
|
59
|
+
const pending = fetch(src)
|
|
60
|
+
.then((response) => (response.ok ? response.text() : undefined))
|
|
61
|
+
.then((text) => (text === undefined ? undefined : readIconFile(text, src)))
|
|
62
|
+
.catch(() => undefined);
|
|
63
|
+
held.answers.set(src, pending);
|
|
64
|
+
void pending.then((icon) => {
|
|
65
|
+
if (held.answers.get(src) === pending) {
|
|
66
|
+
held.answers.set(src, icon);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
return pending;
|
|
70
|
+
};
|
|
@@ -27,8 +27,16 @@ import { nothing, type PropertyValues, type TemplateResult } from 'lit';
|
|
|
27
27
|
* a glyph that came later is drawn. A glyph registered after the element
|
|
28
28
|
* rendered is drawn then.
|
|
29
29
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
30
|
+
* **Or point `src` at an SVG file**, such as `src="/icons/rocket.svg"`, and
|
|
31
|
+
* the element fetches it and draws its shapes in `currentColor` where the file
|
|
32
|
+
* does not color them. A file is fetched once a page, however many icons name
|
|
33
|
+
* it, and `load` fires when it is drawn. Scripts, event handlers and
|
|
34
|
+
* `javascript:` links in the file are taken out before it is drawn. A server
|
|
35
|
+
* fetches nothing, so a served icon with a `src` is drawn once the script
|
|
36
|
+
* runs.
|
|
37
|
+
*
|
|
38
|
+
* A name with no match, or a `src` that is no SVG, renders nothing, and the
|
|
39
|
+
* element fires `error`, so a typo in a name can be heard. It waits until the page has loaded and painted
|
|
32
40
|
* first, so a library a later module registers is not taken for a typo.
|
|
33
41
|
*
|
|
34
42
|
* An icon is decorative by default and hidden from assistive technology. Set
|
|
@@ -47,8 +55,8 @@ import { nothing, type PropertyValues, type TemplateResult } from 'lit';
|
|
|
47
55
|
* @aliases Glyph, Symbol
|
|
48
56
|
* @documentation https://fusion.dev/helix/components/icon
|
|
49
57
|
*
|
|
50
|
-
* @event {Event} error - `name` matches no registered icon and no library answers it, so nothing is drawn.
|
|
51
|
-
* @event {Event} load - A glyph a library gave later for `name` is drawn.
|
|
58
|
+
* @event {Event} error - `name` matches no registered icon and no library answers it, or `src` is no SVG, so nothing is drawn.
|
|
59
|
+
* @event {Event} load - A glyph a library gave later for `name`, or the file at `src`, is drawn.
|
|
52
60
|
*
|
|
53
61
|
* @csspart svg - The rendered SVG.
|
|
54
62
|
*/
|
|
@@ -97,6 +105,13 @@ export declare class HelixHeadlessIcon extends HelixElement {
|
|
|
97
105
|
* prefix.
|
|
98
106
|
*/
|
|
99
107
|
name: string;
|
|
108
|
+
/**
|
|
109
|
+
* The URL of an SVG file to draw, fetched once a page and drawn in
|
|
110
|
+
* `currentColor` where the file does not color its shapes.
|
|
111
|
+
*
|
|
112
|
+
* Wins over `name`; `icon` wins over both. Absent draws `name`.
|
|
113
|
+
*/
|
|
114
|
+
src: string;
|
|
100
115
|
/**
|
|
101
116
|
* Hears an `aria-label` written on the element, which no property mirrors.
|
|
102
117
|
*
|
|
@@ -120,11 +135,18 @@ export declare class HelixHeadlessIcon extends HelixElement {
|
|
|
120
135
|
private arrived;
|
|
121
136
|
/** Stops listening to the registry. */
|
|
122
137
|
private unwatch;
|
|
138
|
+
/** What the glyph is asked for by: `src` where it is set, and `name` otherwise. */
|
|
139
|
+
private get asked();
|
|
123
140
|
connectedCallback(): void;
|
|
124
141
|
disconnectedCallback(): void;
|
|
125
|
-
/**
|
|
142
|
+
/**
|
|
143
|
+
* What `src` or the registry gives now, starting the wait for a late glyph.
|
|
144
|
+
* A file is fetched only where the page can be read: a server fetches
|
|
145
|
+
* nothing, and a hydrating render draws what the server sent.
|
|
146
|
+
*/
|
|
126
147
|
private lookup;
|
|
127
148
|
protected willUpdate(changed: PropertyValues): void;
|
|
149
|
+
protected firstUpdated(changed: PropertyValues): void;
|
|
128
150
|
protected updated(changed: PropertyValues): void;
|
|
129
151
|
/** Hands the drawn `<svg>` to the mutator of the library `name` asks, where it has one. */
|
|
130
152
|
private mutate;
|
|
@@ -8,6 +8,7 @@ import { auditName, customElement, HelixElement, hostAria, reportRuntime, } from
|
|
|
8
8
|
import { html, nothing } from 'lit';
|
|
9
9
|
import { property } from 'lit/decorators.js';
|
|
10
10
|
import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
|
|
11
|
+
import { fetchIcon } from '../fetch.js';
|
|
11
12
|
import { iconMutator, resolveIcon, watchIcons } from '../registry.js';
|
|
12
13
|
import { afterPageSettles } from '../settle.js';
|
|
13
14
|
let HelixHeadlessIcon = class HelixHeadlessIcon extends HelixElement {
|
|
@@ -16,6 +17,7 @@ let HelixHeadlessIcon = class HelixHeadlessIcon extends HelixElement {
|
|
|
16
17
|
this.label = '';
|
|
17
18
|
this.mirror = false;
|
|
18
19
|
this.name = '';
|
|
20
|
+
this.src = '';
|
|
19
21
|
this.missed = '';
|
|
20
22
|
this.waiting = false;
|
|
21
23
|
this.arrived = '';
|
|
@@ -30,10 +32,13 @@ let HelixHeadlessIcon = class HelixHeadlessIcon extends HelixElement {
|
|
|
30
32
|
this.requestUpdate();
|
|
31
33
|
}
|
|
32
34
|
}
|
|
35
|
+
get asked() {
|
|
36
|
+
return this.src || this.name;
|
|
37
|
+
}
|
|
33
38
|
connectedCallback() {
|
|
34
39
|
super.connectedCallback();
|
|
35
40
|
this.unwatch = watchIcons(() => {
|
|
36
|
-
if (this.icon === undefined && this.name !== '') {
|
|
41
|
+
if (this.icon === undefined && this.src === '' && this.name !== '') {
|
|
37
42
|
this.requestUpdate();
|
|
38
43
|
}
|
|
39
44
|
});
|
|
@@ -44,15 +49,21 @@ let HelixHeadlessIcon = class HelixHeadlessIcon extends HelixElement {
|
|
|
44
49
|
this.unwatch = undefined;
|
|
45
50
|
}
|
|
46
51
|
lookup() {
|
|
47
|
-
const
|
|
48
|
-
|
|
52
|
+
const asked = this.asked;
|
|
53
|
+
const found = this.src !== ''
|
|
54
|
+
? this.childrenKnown
|
|
55
|
+
? fetchIcon(this.src)
|
|
56
|
+
: undefined
|
|
57
|
+
: this.name === ''
|
|
58
|
+
? undefined
|
|
59
|
+
: resolveIcon(this.name);
|
|
60
|
+
this.waiting = found instanceof Promise || (this.src !== '' && !this.childrenKnown);
|
|
49
61
|
if (!(found instanceof Promise)) {
|
|
50
62
|
return found;
|
|
51
63
|
}
|
|
52
|
-
const name = this.name;
|
|
53
64
|
void found.then(() => {
|
|
54
|
-
if (this.
|
|
55
|
-
this.arrived =
|
|
65
|
+
if (this.asked === asked && this.isConnected) {
|
|
66
|
+
this.arrived = asked;
|
|
56
67
|
this.requestUpdate();
|
|
57
68
|
}
|
|
58
69
|
});
|
|
@@ -64,19 +75,26 @@ let HelixHeadlessIcon = class HelixHeadlessIcon extends HelixElement {
|
|
|
64
75
|
hostAria(this.internals, { role: named ? 'img' : '' });
|
|
65
76
|
this.drawn = this.icon ?? this.lookup();
|
|
66
77
|
}
|
|
78
|
+
firstUpdated(changed) {
|
|
79
|
+
super.firstUpdated(changed);
|
|
80
|
+
if (this.src !== '') {
|
|
81
|
+
this.finishHydration();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
67
84
|
updated(changed) {
|
|
68
85
|
super.updated(changed);
|
|
69
86
|
if (this.drawn !== undefined) {
|
|
70
87
|
this.mutate();
|
|
71
88
|
this.auditControl();
|
|
72
89
|
}
|
|
73
|
-
|
|
90
|
+
const asked = this.asked;
|
|
91
|
+
if (this.arrived === asked && this.drawn !== undefined) {
|
|
74
92
|
this.dispatchEvent(new Event('load', { bubbles: false, cancelable: false, composed: false }));
|
|
75
93
|
}
|
|
76
94
|
this.arrived = '';
|
|
77
|
-
if (this.drawn === undefined &&
|
|
78
|
-
if (this.missed !==
|
|
79
|
-
this.pending =
|
|
95
|
+
if (this.drawn === undefined && asked !== '' && !this.waiting) {
|
|
96
|
+
if (this.missed !== asked && this.pending !== asked) {
|
|
97
|
+
this.pending = asked;
|
|
80
98
|
afterPageSettles(() => this.reportMissing());
|
|
81
99
|
}
|
|
82
100
|
}
|
|
@@ -86,7 +104,7 @@ let HelixHeadlessIcon = class HelixHeadlessIcon extends HelixElement {
|
|
|
86
104
|
}
|
|
87
105
|
mutate() {
|
|
88
106
|
const svg = this.renderRoot.querySelector('svg');
|
|
89
|
-
const mutator = this.icon === undefined ? iconMutator(this.name) : undefined;
|
|
107
|
+
const mutator = this.icon === undefined && this.src === '' ? iconMutator(this.name) : undefined;
|
|
90
108
|
if (svg && mutator) {
|
|
91
109
|
mutator(svg);
|
|
92
110
|
}
|
|
@@ -102,18 +120,21 @@ let HelixHeadlessIcon = class HelixHeadlessIcon extends HelixElement {
|
|
|
102
120
|
}
|
|
103
121
|
}
|
|
104
122
|
reportMissing() {
|
|
105
|
-
const
|
|
123
|
+
const asked = this.pending;
|
|
106
124
|
this.pending = '';
|
|
107
|
-
if (this.
|
|
125
|
+
if (this.asked !== asked || this.drawn !== undefined || this.waiting || !this.isConnected) {
|
|
108
126
|
return;
|
|
109
127
|
}
|
|
110
|
-
this.missed =
|
|
128
|
+
this.missed = asked;
|
|
111
129
|
this.dispatchEvent(new Event('error', { bubbles: false, cancelable: false, composed: false }));
|
|
112
130
|
reportRuntime({
|
|
113
131
|
kind: 'unknown-icon',
|
|
114
|
-
message:
|
|
115
|
-
|
|
116
|
-
|
|
132
|
+
message: this.src === ''
|
|
133
|
+
? `<${this.localName} name="${asked}"> names no registered icon, so it draws ` +
|
|
134
|
+
'nothing. Check the name, register the glyph with `registerIcons`, or import it ' +
|
|
135
|
+
'and pass it as `icon`.'
|
|
136
|
+
: `<${this.localName} src="${asked}"> could not be fetched as an SVG file, so it ` +
|
|
137
|
+
'draws nothing. Check the URL, and that the file is an `<svg>`.',
|
|
117
138
|
tag: this.localName,
|
|
118
139
|
});
|
|
119
140
|
}
|
|
@@ -148,6 +169,9 @@ __decorate([
|
|
|
148
169
|
__decorate([
|
|
149
170
|
property()
|
|
150
171
|
], HelixHeadlessIcon.prototype, "name", void 0);
|
|
172
|
+
__decorate([
|
|
173
|
+
property()
|
|
174
|
+
], HelixHeadlessIcon.prototype, "src", void 0);
|
|
151
175
|
HelixHeadlessIcon = __decorate([
|
|
152
176
|
customElement('helix-headless-icon')
|
|
153
177
|
], HelixHeadlessIcon);
|
|
@@ -36,6 +36,15 @@ import { type PropertyValues, type TemplateResult } from 'lit';
|
|
|
36
36
|
* links and not a single control, and the keys across the other axis are
|
|
37
37
|
* left to the page and to an open panel.
|
|
38
38
|
*
|
|
39
|
+
* While a `Megamenu`, or another disclosure among the destinations, is open,
|
|
40
|
+
* the navigation is `:state(open)`, so a page can dim what lies under the
|
|
41
|
+
* panel, as Radix's navigation menu marks its open state.
|
|
42
|
+
*
|
|
43
|
+
* A screen reader lists the page's navigation landmarks by name, so two named
|
|
44
|
+
* alike cannot be told apart. Where another navigation in the same document
|
|
45
|
+
* has the same name, "Main" for two with no `label`, the later one warns once
|
|
46
|
+
* with the kind `duplicate-name`.
|
|
47
|
+
*
|
|
39
48
|
* @since 0.1.0
|
|
40
49
|
* @status stable
|
|
41
50
|
* @category navigation
|
|
@@ -47,6 +56,8 @@ import { type PropertyValues, type TemplateResult } from 'lit';
|
|
|
47
56
|
*
|
|
48
57
|
* @csspart indicator - The marker that moves to the current destination. It carries the destination's place and size as `--indicator-x`, `--indicator-y`, `--indicator-inline-size` and `--indicator-block-size`, and `data-placed` once it has been placed.
|
|
49
58
|
* @csspart nav - The navigation landmark.
|
|
59
|
+
*
|
|
60
|
+
* @cssstate open - While a megamenu or another disclosure among the destinations is open.
|
|
50
61
|
*/
|
|
51
62
|
export declare class HelixHeadlessNavigation extends HelixElement {
|
|
52
63
|
/**
|
|
@@ -80,6 +91,21 @@ export declare class HelixHeadlessNavigation extends HelixElement {
|
|
|
80
91
|
*/
|
|
81
92
|
orientation?: 'vertical';
|
|
82
93
|
private current;
|
|
94
|
+
/** The disclosures among the destinations that are open. */
|
|
95
|
+
private readonly opened;
|
|
96
|
+
/**
|
|
97
|
+
* Hears a disclosure among the destinations open or close. In capture,
|
|
98
|
+
* because `toggle` does not bubble.
|
|
99
|
+
*/
|
|
100
|
+
private readonly handleToggle;
|
|
101
|
+
/** The landmark's name as a screen reader lists it. */
|
|
102
|
+
private get landmarkName();
|
|
103
|
+
/**
|
|
104
|
+
* Warns once where another navigation in the same document has the same
|
|
105
|
+
* name, a task after the first render, so the page's other landmarks have
|
|
106
|
+
* rendered too.
|
|
107
|
+
*/
|
|
108
|
+
private auditTwin;
|
|
83
109
|
/**
|
|
84
110
|
* The destinations the consumer slotted, and the items of a slotted list in
|
|
85
111
|
* place of the list.
|
|
@@ -4,7 +4,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
4
4
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
5
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
6
|
};
|
|
7
|
-
import { AriaForwardController, customElement, focusableElements, focusItemOf, HelixElement, IndicatorController, KeyboardNavigationController, words, } from '@fusion.dev/helix-core';
|
|
7
|
+
import { AriaForwardController, customElement, focusableElements, focusItemOf, HelixElement, IndicatorController, KeyboardNavigationController, reportRuntime, words, } from '@fusion.dev/helix-core';
|
|
8
8
|
import { html } from 'lit';
|
|
9
9
|
import { property } from 'lit/decorators.js';
|
|
10
10
|
const FOCUSABLE = 'a[href], button, input, select, textarea, [tabindex]:not([tabindex="-1"])';
|
|
@@ -23,6 +23,8 @@ const navigable = (item) => {
|
|
|
23
23
|
},
|
|
24
24
|
});
|
|
25
25
|
};
|
|
26
|
+
const connected = new Set();
|
|
27
|
+
const warned = new WeakSet();
|
|
26
28
|
let HelixHeadlessNavigation = class HelixHeadlessNavigation extends HelixElement {
|
|
27
29
|
constructor() {
|
|
28
30
|
super(...arguments);
|
|
@@ -53,8 +55,56 @@ let HelixHeadlessNavigation = class HelixHeadlessNavigation extends HelixElement
|
|
|
53
55
|
label: true,
|
|
54
56
|
target: () => this.partNamed('nav'),
|
|
55
57
|
});
|
|
58
|
+
this.opened = new Set();
|
|
59
|
+
this.handleToggle = (event) => {
|
|
60
|
+
const target = event.target;
|
|
61
|
+
if (target === this || target === null) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (event.newState === 'open') {
|
|
65
|
+
this.opened.add(target);
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
this.opened.delete(target);
|
|
69
|
+
}
|
|
70
|
+
this.toggleState('open', this.opened.size > 0);
|
|
71
|
+
};
|
|
56
72
|
}
|
|
57
73
|
static { this.words = 'helix-navigation'; }
|
|
74
|
+
get landmarkName() {
|
|
75
|
+
const labeled = this.getAttribute('aria-labelledby');
|
|
76
|
+
if (labeled) {
|
|
77
|
+
const root = this.getRootNode();
|
|
78
|
+
return labeled
|
|
79
|
+
.split(/\s+/)
|
|
80
|
+
.map((id) => root.getElementById?.(id)?.textContent?.trim() ?? '')
|
|
81
|
+
.join(' ')
|
|
82
|
+
.trim();
|
|
83
|
+
}
|
|
84
|
+
return (this.getAttribute('aria-label') ?? this.label).trim();
|
|
85
|
+
}
|
|
86
|
+
auditTwin() {
|
|
87
|
+
setTimeout(() => {
|
|
88
|
+
if (!this.isConnected || warned.has(this)) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const name = this.landmarkName;
|
|
92
|
+
const root = this.getRootNode();
|
|
93
|
+
const twin = [...connected].some((other) => other !== this &&
|
|
94
|
+
other.isConnected &&
|
|
95
|
+
(other.compareDocumentPosition(this) & Node.DOCUMENT_POSITION_FOLLOWING) !== 0 &&
|
|
96
|
+
other.getRootNode() === root &&
|
|
97
|
+
other.landmarkName === name);
|
|
98
|
+
if (twin) {
|
|
99
|
+
warned.add(this);
|
|
100
|
+
reportRuntime({
|
|
101
|
+
kind: 'duplicate-name',
|
|
102
|
+
message: `<${this.localName}> has the name "${name}", which another navigation on the page has too, so a screen reader lists the two alike. Give each a \`label\` that says what it holds, such as "Footer".`,
|
|
103
|
+
tag: this.localName,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
}
|
|
58
108
|
get items() {
|
|
59
109
|
const slot = this.renderRoot.querySelector('slot:not([name])');
|
|
60
110
|
return (slot?.assignedElements() ?? []).flatMap((one) => isList(one) ? [...one.children] : [one]);
|
|
@@ -69,17 +119,24 @@ let HelixHeadlessNavigation = class HelixHeadlessNavigation extends HelixElement
|
|
|
69
119
|
}
|
|
70
120
|
connectedCallback() {
|
|
71
121
|
super.connectedCallback();
|
|
122
|
+
connected.add(this);
|
|
123
|
+
this.addEventListener('toggle', this.handleToggle, true);
|
|
72
124
|
if (this.hasUpdated) {
|
|
73
125
|
this.watch();
|
|
74
126
|
}
|
|
75
127
|
}
|
|
76
128
|
disconnectedCallback() {
|
|
77
129
|
super.disconnectedCallback();
|
|
130
|
+
connected.delete(this);
|
|
131
|
+
this.removeEventListener('toggle', this.handleToggle, true);
|
|
132
|
+
this.opened.clear();
|
|
133
|
+
this.toggleState('open', false);
|
|
78
134
|
this.current?.disconnect();
|
|
79
135
|
}
|
|
80
136
|
firstUpdated(changed) {
|
|
81
137
|
super.firstUpdated(changed);
|
|
82
138
|
this.watch();
|
|
139
|
+
this.auditTwin();
|
|
83
140
|
}
|
|
84
141
|
render() {
|
|
85
142
|
return html `<nav part="nav" aria-label=${this.label}>
|
|
@@ -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 A short quotation, set inline with the text around it.
|
|
5
5
|
*
|
|
@@ -12,6 +12,12 @@ import { type PropertyValues, type TemplateResult } from 'lit';
|
|
|
12
12
|
* element, and its marks follow it: `lang="de"` sets „…“ and `lang="fr"` «…»
|
|
13
13
|
* inside an English page.
|
|
14
14
|
*
|
|
15
|
+
* **`no-marks` leaves the marks off**, for a quotation set where the page draws
|
|
16
|
+
* its own, such as a pull quote with a large decorative mark beside it or a
|
|
17
|
+
* table of sayings. The quotation still counts toward nesting, so a quote
|
|
18
|
+
* inside it takes the inner marks it would have had. Text with marks typed
|
|
19
|
+
* into it raises no warning under `no-marks`.
|
|
20
|
+
*
|
|
15
21
|
* For a passage set off from the surrounding text, use a blockquote.
|
|
16
22
|
*
|
|
17
23
|
* @since 0.1.0
|
|
@@ -25,10 +31,20 @@ import { type PropertyValues, type TemplateResult } from 'lit';
|
|
|
25
31
|
* @csspart quote - The quotation.
|
|
26
32
|
*/
|
|
27
33
|
export declare class HelixHeadlessQuote extends HelixElement {
|
|
34
|
+
static styles: CSSResultGroup;
|
|
28
35
|
/**
|
|
29
36
|
* URL identifying where the quotation came from.
|
|
30
37
|
*/
|
|
31
38
|
cite: string;
|
|
39
|
+
/**
|
|
40
|
+
* Whether to leave the quotation marks off, for a quotation set where the
|
|
41
|
+
* page draws its own.
|
|
42
|
+
*
|
|
43
|
+
* Negative, because the marks are on by default and a boolean attribute is
|
|
44
|
+
* true whenever it is present. Reflected, because a stylesheet takes the
|
|
45
|
+
* marks off on it.
|
|
46
|
+
*/
|
|
47
|
+
noMarks: boolean;
|
|
32
48
|
/** Whether this quote has said its text carries typed marks, so it says so once. */
|
|
33
49
|
private warned;
|
|
34
50
|
/**
|
|
@@ -5,20 +5,21 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
5
5
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
6
|
};
|
|
7
7
|
import { customElement, HelixElement, reportRuntime } from '@fusion.dev/helix-core';
|
|
8
|
-
import { html, nothing } from 'lit';
|
|
8
|
+
import { css, html, nothing, } from 'lit';
|
|
9
9
|
import { property } from 'lit/decorators.js';
|
|
10
10
|
import { typedMarks } from '../marks.js';
|
|
11
11
|
let HelixHeadlessQuote = class HelixHeadlessQuote extends HelixElement {
|
|
12
12
|
constructor() {
|
|
13
13
|
super(...arguments);
|
|
14
14
|
this.cite = '';
|
|
15
|
+
this.noMarks = false;
|
|
15
16
|
this.warned = false;
|
|
16
17
|
this.readText = () => {
|
|
17
18
|
const text = [...this.childNodes]
|
|
18
19
|
.filter((node) => !(node instanceof Element) || !node.slot)
|
|
19
20
|
.map((node) => node.textContent ?? '')
|
|
20
21
|
.join('');
|
|
21
|
-
if (this.warned || !typedMarks(text)) {
|
|
22
|
+
if (this.warned || this.noMarks || !typedMarks(text)) {
|
|
22
23
|
return;
|
|
23
24
|
}
|
|
24
25
|
this.warned = true;
|
|
@@ -29,6 +30,15 @@ let HelixHeadlessQuote = class HelixHeadlessQuote extends HelixElement {
|
|
|
29
30
|
});
|
|
30
31
|
};
|
|
31
32
|
}
|
|
33
|
+
static { this.styles = css `
|
|
34
|
+
:host([no-marks]) [part='quote']::before {
|
|
35
|
+
content: no-open-quote;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
:host([no-marks]) [part='quote']::after {
|
|
39
|
+
content: no-close-quote;
|
|
40
|
+
}
|
|
41
|
+
`; }
|
|
32
42
|
firstUpdated(changed) {
|
|
33
43
|
super.firstUpdated(changed);
|
|
34
44
|
this.readText();
|
|
@@ -41,6 +51,9 @@ let HelixHeadlessQuote = class HelixHeadlessQuote extends HelixElement {
|
|
|
41
51
|
__decorate([
|
|
42
52
|
property()
|
|
43
53
|
], HelixHeadlessQuote.prototype, "cite", void 0);
|
|
54
|
+
__decorate([
|
|
55
|
+
property({ attribute: 'no-marks', reflect: true, type: Boolean })
|
|
56
|
+
], HelixHeadlessQuote.prototype, "noMarks", void 0);
|
|
44
57
|
HelixHeadlessQuote = __decorate([
|
|
45
58
|
customElement('helix-headless-quote')
|
|
46
59
|
], HelixHeadlessQuote);
|