@fusion.dev/helix-headless 0.153.0 → 0.154.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 +459 -53
- package/dist/badge/root/badge-root.d.ts +15 -2
- package/dist/badge/root/badge-root.js +20 -3
- 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/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/quote/root/quote-root.d.ts +17 -1
- package/dist/quote/root/quote-root.js +15 -2
- package/dist/skeleton/root/skeleton-root.d.ts +7 -0
- package/dist/skeleton/root/skeleton-root.js +4 -1
- 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/lint.json +1 -1
- package/package.json +5 -5
- package/types/jsx.d.ts +428 -56
- package/types/vue.d.ts +73 -16
- package/types/words.d.ts +13 -0
|
@@ -35,6 +35,12 @@ import { type PropertyValues, type TemplateResult } from 'lit';
|
|
|
35
35
|
* button "Inbox 8 unread", and stays current as the count changes. The styled
|
|
36
36
|
* tier's `placement="inline"` draws the mark in the line for that case.
|
|
37
37
|
*
|
|
38
|
+
* **An icon goes inside the mark** through the `icon` slot, before the count,
|
|
39
|
+
* as Web Awesome's badge takes one at its start. With no `value` the icon is
|
|
40
|
+
* the whole mark, such as a check on an avatar, drawn where the dot would be.
|
|
41
|
+
* The icon is hidden from screen readers: the count and `label` are what the
|
|
42
|
+
* mark says.
|
|
43
|
+
*
|
|
38
44
|
* @since 0.1.0
|
|
39
45
|
* @status stable
|
|
40
46
|
* @category markers
|
|
@@ -42,12 +48,15 @@ import { type PropertyValues, type TemplateResult } from 'lit';
|
|
|
42
48
|
* @documentation https://fusion.dev/helix/components/badge
|
|
43
49
|
*
|
|
44
50
|
* @slot - The element being decorated.
|
|
51
|
+
* @slot icon - An icon inside the mark, before the count, or the whole mark when there is no value. Hidden from screen readers.
|
|
45
52
|
*
|
|
46
|
-
* @csspart
|
|
53
|
+
* @csspart icon - The slot the icon arrives through, inside the indicator. A slot draws no box, so it passes inherited properties, such as color, to the icon.
|
|
54
|
+
* @csspart indicator - The count, or the dot when there is no value. Carries `data-dot` when there is no value, `data-single` when the count is one character, and `data-blank` when the value is zero.
|
|
47
55
|
*
|
|
48
56
|
* @cssstate blank - The value is zero, so there is no mark.
|
|
49
57
|
* @cssstate capped - The count is past `max`, so the mark shows `{max}+`.
|
|
50
|
-
* @cssstate dot - There is no value, so the mark is a dot.
|
|
58
|
+
* @cssstate dot - There is no value and no icon, so the mark is a dot.
|
|
59
|
+
* @cssstate has-icon - Something is slotted into `icon`, so it is drawn inside the mark.
|
|
51
60
|
*/
|
|
52
61
|
export declare class HelixHeadlessBadge extends HelixElement {
|
|
53
62
|
/**
|
|
@@ -98,6 +107,10 @@ export declare class HelixHeadlessBadge extends HelixElement {
|
|
|
98
107
|
* is showing a count must not lose the count for being handed one.
|
|
99
108
|
*/
|
|
100
109
|
value: number | string;
|
|
110
|
+
/** Whether something is slotted into `icon`, read from the children. */
|
|
111
|
+
private hasIcon;
|
|
112
|
+
/** Reads whether an icon is slotted, from the children where they can be known. */
|
|
113
|
+
private readonly readIcon;
|
|
101
114
|
/** The value as text, whatever it was set to. */
|
|
102
115
|
private get given();
|
|
103
116
|
/** Whether the value counts nothing, in which case there is no mark. */
|
|
@@ -6,7 +6,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
6
6
|
};
|
|
7
7
|
import { customElement, focusTarget, HelixElement, wordsLocale } from '@fusion.dev/helix-core';
|
|
8
8
|
import { html, nothing } from 'lit';
|
|
9
|
-
import { property } from 'lit/decorators.js';
|
|
9
|
+
import { property, state } from 'lit/decorators.js';
|
|
10
10
|
import { badgeBlank, badgeCapped, badgeText, badgeWords } from '../count.js';
|
|
11
11
|
let HelixHeadlessBadge = class HelixHeadlessBadge extends HelixElement {
|
|
12
12
|
constructor() {
|
|
@@ -14,6 +14,10 @@ let HelixHeadlessBadge = class HelixHeadlessBadge extends HelixElement {
|
|
|
14
14
|
this.label = '';
|
|
15
15
|
this.locale = '';
|
|
16
16
|
this.value = '';
|
|
17
|
+
this.hasIcon = false;
|
|
18
|
+
this.readIcon = () => {
|
|
19
|
+
this.hasIcon = this.knownChildren.some((child) => child.slot === 'icon');
|
|
20
|
+
};
|
|
17
21
|
this.handleSlotchange = () => {
|
|
18
22
|
this.describe();
|
|
19
23
|
};
|
|
@@ -81,9 +85,13 @@ let HelixHeadlessBadge = class HelixHeadlessBadge extends HelixElement {
|
|
|
81
85
|
}
|
|
82
86
|
willUpdate(changed) {
|
|
83
87
|
super.willUpdate(changed);
|
|
88
|
+
if (!this.hasUpdated) {
|
|
89
|
+
this.readIcon();
|
|
90
|
+
}
|
|
84
91
|
this.toggleState('capped', this.capped);
|
|
85
|
-
this.toggleState('dot', this.given.trim() === '');
|
|
92
|
+
this.toggleState('dot', this.given.trim() === '' && !this.hasIcon);
|
|
86
93
|
this.toggleState('blank', this.blank);
|
|
94
|
+
this.toggleState('has-icon', this.hasIcon);
|
|
87
95
|
}
|
|
88
96
|
updated(changed) {
|
|
89
97
|
super.updated(changed);
|
|
@@ -97,10 +105,16 @@ let HelixHeadlessBadge = class HelixHeadlessBadge extends HelixElement {
|
|
|
97
105
|
<span
|
|
98
106
|
part="indicator"
|
|
99
107
|
data-blank=${blank ? '' : nothing}
|
|
108
|
+
data-dot=${this.given.trim() === '' ? '' : nothing}
|
|
100
109
|
data-single=${text.length <= 1 ? '' : nothing}
|
|
101
110
|
role="status"
|
|
102
111
|
aria-label=${announced ? this.words : nothing}
|
|
103
|
-
|
|
112
|
+
><slot
|
|
113
|
+
aria-hidden="true"
|
|
114
|
+
name="icon"
|
|
115
|
+
part="icon"
|
|
116
|
+
@slotchange=${this.readIcon}
|
|
117
|
+
></slot>${blank ? html `<span aria-hidden="true">${text}</span>` : text}</span
|
|
104
118
|
>`;
|
|
105
119
|
}
|
|
106
120
|
};
|
|
@@ -119,6 +133,9 @@ __decorate([
|
|
|
119
133
|
__decorate([
|
|
120
134
|
property()
|
|
121
135
|
], HelixHeadlessBadge.prototype, "value", void 0);
|
|
136
|
+
__decorate([
|
|
137
|
+
state()
|
|
138
|
+
], HelixHeadlessBadge.prototype, "hasIcon", void 0);
|
|
122
139
|
HelixHeadlessBadge = __decorate([
|
|
123
140
|
customElement('helix-headless-badge')
|
|
124
141
|
], HelixHeadlessBadge);
|
|
@@ -13,13 +13,17 @@ import { type PropertyValues, type TemplateResult } from 'lit';
|
|
|
13
13
|
* through with `::part`.
|
|
14
14
|
*
|
|
15
15
|
* Name the card with `label`, as a Stat Group is named, or with `aria-label`
|
|
16
|
-
* or `aria-labelledby`.
|
|
17
|
-
*
|
|
18
|
-
* server has none. `label` is set through `ElementInternals`, so it writes no
|
|
19
|
-
* attribute onto the card. **A named card is a group**, so a
|
|
16
|
+
* or `aria-labelledby`. `label` is set through `ElementInternals`, so it
|
|
17
|
+
* writes no attribute onto the card. **A named card is a group**, so a
|
|
20
18
|
* reader entering it hears its name; an unnamed one has no role, since a group
|
|
21
19
|
* with no name only adds "group" to what a reader says.
|
|
22
20
|
*
|
|
21
|
+
* **With none of those, a heading in `header` names the card**, such as the
|
|
22
|
+
* `<h2>` of its title, by reference, so the name follows the heading's words.
|
|
23
|
+
* Reading a heading needs a DOM, so a served card is named by it once the
|
|
24
|
+
* script runs; `label`, `aria-label` and `aria-labelledby` are the names a
|
|
25
|
+
* server writes.
|
|
26
|
+
*
|
|
23
27
|
* Each region's slot is a part of the same name, `body` for the main content,
|
|
24
28
|
* as Web Awesome's card names its regions. A slot draws no box, so a part
|
|
25
29
|
* passes inherited properties to what is slotted, and a page wanting a box
|
|
@@ -29,7 +33,9 @@ import { type PropertyValues, type TemplateResult } from 'lit';
|
|
|
29
33
|
* header's and the footer's line, as Web Awesome's card does. While one holds
|
|
30
34
|
* something, its line is a `header-row` or `footer-row` part; otherwise there
|
|
31
35
|
* is no wrapper and every region is a bare slot. A server cannot see slotted
|
|
32
|
-
* content, so served
|
|
36
|
+
* content, so a served card has no row; the styled tier sets served actions at
|
|
37
|
+
* the end of their line from the stylesheet alone, so they do not move when
|
|
38
|
+
* the script runs.
|
|
33
39
|
*
|
|
34
40
|
* **The card says which regions it holds**, as `:state(has-media)`,
|
|
35
41
|
* `:state(has-header)` and `:state(has-footer)`, so a sheet can draw a card
|
|
@@ -87,20 +93,34 @@ export declare class HelixHeadlessCard extends HelixElement {
|
|
|
87
93
|
*/
|
|
88
94
|
mediaPlacement?: 'end';
|
|
89
95
|
/**
|
|
90
|
-
*
|
|
91
|
-
*
|
|
96
|
+
* Hears a name written on the card, which no property mirrors.
|
|
97
|
+
*
|
|
98
|
+
* Lit observes the attributes of its own properties only, and `ariaLabel`
|
|
99
|
+
* and `ariaLabelledByElements` belong to the platform. Observed here rather
|
|
100
|
+
* than through a property, so the manifest lists no attribute of the card's
|
|
101
|
+
* own for them.
|
|
92
102
|
*/
|
|
93
|
-
|
|
103
|
+
static get observedAttributes(): string[];
|
|
94
104
|
/**
|
|
95
|
-
*
|
|
96
|
-
*
|
|
105
|
+
* Called by the browser when an observed attribute changes. A name written
|
|
106
|
+
* or taken off asks for an update, because it decides whether the card is a
|
|
107
|
+
* group and whether its heading names it.
|
|
97
108
|
*/
|
|
98
|
-
|
|
109
|
+
attributeChangedCallback(name: string, old: string | null, value: string | null): void;
|
|
99
110
|
/** Whether `footer-actions` holds something, read from the slot. */
|
|
100
111
|
private footerActions;
|
|
101
112
|
/** Whether `header-actions` holds something, read from the slot. */
|
|
102
113
|
private headerActions;
|
|
103
114
|
protected willUpdate(changed: PropertyValues): void;
|
|
115
|
+
/**
|
|
116
|
+
* Says what the card is and what names it. On the host, since the host is
|
|
117
|
+
* the box, and from `willUpdate`, which the server calls too.
|
|
118
|
+
*
|
|
119
|
+
* **A heading in `header` is the name only where nothing else is**, and it
|
|
120
|
+
* names the card by reference, so a renamed heading renames the card. An
|
|
121
|
+
* engine without element references hears its words.
|
|
122
|
+
*/
|
|
123
|
+
private writeName;
|
|
104
124
|
protected updated(changed: PropertyValues): void;
|
|
105
125
|
protected firstUpdated(changed: PropertyValues): void;
|
|
106
126
|
private readonly readRegions;
|
|
@@ -4,15 +4,13 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
4
4
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
5
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
6
|
};
|
|
7
|
-
import { customElement, HelixElement, hostAria, slotFilled } from '@fusion.dev/helix-core';
|
|
7
|
+
import { customElement, HelixElement, headingIn, hostAria, slotFilled, } from '@fusion.dev/helix-core';
|
|
8
8
|
import { html } from 'lit';
|
|
9
9
|
import { property, state } from 'lit/decorators.js';
|
|
10
10
|
let HelixHeadlessCard = class HelixHeadlessCard extends HelixElement {
|
|
11
11
|
constructor() {
|
|
12
12
|
super(...arguments);
|
|
13
13
|
this.label = '';
|
|
14
|
-
this.labelledBy = null;
|
|
15
|
-
this.namedBy = null;
|
|
16
14
|
this.footerActions = false;
|
|
17
15
|
this.headerActions = false;
|
|
18
16
|
this.readRegions = () => {
|
|
@@ -22,12 +20,34 @@ let HelixHeadlessCard = class HelixHeadlessCard extends HelixElement {
|
|
|
22
20
|
this.toggleState('has-media', filled('media'));
|
|
23
21
|
this.toggleState('has-header', this.headerActions || filled('header'));
|
|
24
22
|
this.toggleState('has-footer', this.footerActions || filled('footer'));
|
|
23
|
+
this.writeName();
|
|
25
24
|
};
|
|
26
25
|
}
|
|
26
|
+
static get observedAttributes() {
|
|
27
|
+
return [...super.observedAttributes, 'aria-label', 'aria-labelledby'];
|
|
28
|
+
}
|
|
29
|
+
attributeChangedCallback(name, old, value) {
|
|
30
|
+
super.attributeChangedCallback(name, old, value);
|
|
31
|
+
if (name === 'aria-label' || name === 'aria-labelledby') {
|
|
32
|
+
this.requestUpdate();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
27
35
|
willUpdate(changed) {
|
|
28
36
|
super.willUpdate(changed);
|
|
29
|
-
|
|
30
|
-
|
|
37
|
+
this.writeName();
|
|
38
|
+
}
|
|
39
|
+
writeName() {
|
|
40
|
+
const attribute = (name) => Boolean(this.getAttribute?.(name)?.trim());
|
|
41
|
+
const named = this.label.trim() !== '' || attribute('aria-label') || attribute('aria-labelledby');
|
|
42
|
+
const heading = named
|
|
43
|
+
? null
|
|
44
|
+
: headingIn(this.knownChildren.filter((child) => child.slot === 'header'));
|
|
45
|
+
const references = 'ariaLabelledByElements' in this.internals;
|
|
46
|
+
hostAria(this.internals, {
|
|
47
|
+
label: heading && !references ? (heading.textContent ?? '').trim() : this.label,
|
|
48
|
+
labelledBy: heading ? [heading] : [],
|
|
49
|
+
role: named || heading ? 'group' : '',
|
|
50
|
+
});
|
|
31
51
|
}
|
|
32
52
|
updated(changed) {
|
|
33
53
|
super.updated(changed);
|
|
@@ -64,12 +84,6 @@ __decorate([
|
|
|
64
84
|
__decorate([
|
|
65
85
|
property({ attribute: 'media-placement', reflect: true })
|
|
66
86
|
], HelixHeadlessCard.prototype, "mediaPlacement", void 0);
|
|
67
|
-
__decorate([
|
|
68
|
-
property({ attribute: 'aria-labelledby' })
|
|
69
|
-
], HelixHeadlessCard.prototype, "labelledBy", void 0);
|
|
70
|
-
__decorate([
|
|
71
|
-
property({ attribute: 'aria-label' })
|
|
72
|
-
], HelixHeadlessCard.prototype, "namedBy", void 0);
|
|
73
87
|
__decorate([
|
|
74
88
|
state()
|
|
75
89
|
], HelixHeadlessCard.prototype, "footerActions", void 0);
|
|
@@ -8,6 +8,13 @@ import { type TemplateResult } from 'lit';
|
|
|
8
8
|
* tools and document search. A styled span carries no such meaning, and a
|
|
9
9
|
* screen reader announces nothing about it.
|
|
10
10
|
*
|
|
11
|
+
* **Set `translate="no"` on the element to keep a page translator off the
|
|
12
|
+
* code.** A translator that rewrites prose would also rewrite an identifier.
|
|
13
|
+
* The slotted text takes its translation mode from the element, its parent in
|
|
14
|
+
* the page, and the element writes nothing onto itself, so the attribute is
|
|
15
|
+
* yours to set. The `<code>` inside carries `translate="no"` for anything the
|
|
16
|
+
* element draws itself.
|
|
17
|
+
*
|
|
11
18
|
* `Code` is inline only and never scrolls. For a block of code with its own
|
|
12
19
|
* box, its own overflow, and copying, use `CodeBlock`, which is a separate
|
|
13
20
|
* family.
|
|
@@ -8,7 +8,7 @@ import { customElement, HelixElement } from '@fusion.dev/helix-core';
|
|
|
8
8
|
import { html } from 'lit';
|
|
9
9
|
let HelixHeadlessCode = class HelixHeadlessCode extends HelixElement {
|
|
10
10
|
render() {
|
|
11
|
-
return html `<code part="code"><slot></slot></code>`;
|
|
11
|
+
return html `<code part="code" translate="no"><slot></slot></code>`;
|
|
12
12
|
}
|
|
13
13
|
};
|
|
14
14
|
HelixHeadlessCode = __decorate([
|
|
@@ -38,6 +38,18 @@ import '../../copy-button/index.js';
|
|
|
38
38
|
* and its copy button copies the new version of the file. `variant="split"`
|
|
39
39
|
* shows the old version beside the new, each removal opposite its replacement.
|
|
40
40
|
*
|
|
41
|
+
* **A block that scrolls is a tab stop.** While its lines run past its width or
|
|
42
|
+
* its cap, the scroller is a region named for the block, such as "Code,
|
|
43
|
+
* app.ts", that Tab reaches, so a keyboard scrolls it with the arrow keys in
|
|
44
|
+
* every browser. Chromium and Firefox make a scrolling box focusable on their
|
|
45
|
+
* own, and Safari does not. A block that fits is no stop at all.
|
|
46
|
+
*
|
|
47
|
+
* **The code the block draws is marked untranslated**, `translate="no"` on
|
|
48
|
+
* its `<pre>`, so a page translator leaves highlighted lines and a diff as
|
|
49
|
+
* written. Code in the slot takes its translation mode from the element, which
|
|
50
|
+
* writes nothing onto itself, so set `translate="no"` on the element to keep a
|
|
51
|
+
* translator off plain code too.
|
|
52
|
+
*
|
|
41
53
|
* On the server, the slot renders as ordinary code, so a diff first appears as
|
|
42
54
|
* the patch text in a `<pre>`. The client makes the same first render, which
|
|
43
55
|
* keeps hydration clean, and draws the diff once it can read the content. Diff
|
|
@@ -95,6 +107,18 @@ export declare class HelixHeadlessCodeBlock extends HelixElement {
|
|
|
95
107
|
* `registerWords('de', { 'helix-code-block': { copyLabel: 'Code kopieren' } })`.
|
|
96
108
|
*/
|
|
97
109
|
static words: string;
|
|
110
|
+
/**
|
|
111
|
+
* The name of a block that scrolls, which is a tab stop while it does.
|
|
112
|
+
*
|
|
113
|
+
* Where the block has a name, the scroller says it too, such as "Code,
|
|
114
|
+
* app.ts", as `code-name-label` puts it.
|
|
115
|
+
*/
|
|
116
|
+
codeLabel: string;
|
|
117
|
+
/**
|
|
118
|
+
* The name of a block that scrolls, where the block has a name. `{code}` is
|
|
119
|
+
* `code-label` and `{name}` the block's name.
|
|
120
|
+
*/
|
|
121
|
+
codeNameLabel: string;
|
|
98
122
|
/**
|
|
99
123
|
* The expand button's words while the block shows every line.
|
|
100
124
|
*/
|
|
@@ -249,7 +273,7 @@ export declare class HelixHeadlessCodeBlock extends HelixElement {
|
|
|
249
273
|
private capped;
|
|
250
274
|
/** Whether the expand button has shown every line. */
|
|
251
275
|
private expanded;
|
|
252
|
-
/** Watches the block's size
|
|
276
|
+
/** Watches the block's size. */
|
|
253
277
|
private sizes;
|
|
254
278
|
/**
|
|
255
279
|
* Reads whether the cap hides lines: the scroller is taller inside than it
|
|
@@ -258,6 +282,18 @@ export declare class HelixHeadlessCodeBlock extends HelixElement {
|
|
|
258
282
|
* everything.
|
|
259
283
|
*/
|
|
260
284
|
private readonly measure;
|
|
285
|
+
/**
|
|
286
|
+
* Makes each box that scrolls a tab stop and a region named for its block,
|
|
287
|
+
* and takes both away from a box that fits.
|
|
288
|
+
*
|
|
289
|
+
* **A reader on a keyboard scrolls the code from the box itself.** Chromium
|
|
290
|
+
* and Firefox make a scrolling box focusable on their own, and Safari does
|
|
291
|
+
* not, so there the arrow keys could not reach the end of a long line. A
|
|
292
|
+
* box counts while its sheet lets it scroll and its content is wider or
|
|
293
|
+
* taller than it is drawn: an unstyled `<pre>` overflows without scrolling,
|
|
294
|
+
* and a stop on it would scroll nothing.
|
|
295
|
+
*/
|
|
296
|
+
private markScrollers;
|
|
261
297
|
protected updated(changed: PropertyValues): void;
|
|
262
298
|
connectedCallback(): void;
|
|
263
299
|
disconnectedCallback(): void;
|
|
@@ -16,6 +16,8 @@ import { pairLines, parsePatch } from '../patch.js';
|
|
|
16
16
|
let HelixHeadlessCodeBlock = class HelixHeadlessCodeBlock extends HelixElement {
|
|
17
17
|
constructor() {
|
|
18
18
|
super(...arguments);
|
|
19
|
+
this.codeLabel = 'Code';
|
|
20
|
+
this.codeNameLabel = '{code}, {name}';
|
|
19
21
|
this.collapseLabel = 'Show fewer lines';
|
|
20
22
|
this.copiedLabel = 'Copied';
|
|
21
23
|
this.copyLabel = 'Copy code';
|
|
@@ -31,6 +33,7 @@ let HelixHeadlessCodeBlock = class HelixHeadlessCodeBlock extends HelixElement {
|
|
|
31
33
|
this.capped = false;
|
|
32
34
|
this.expanded = false;
|
|
33
35
|
this.measure = () => {
|
|
36
|
+
this.markScrollers();
|
|
34
37
|
if (!this.expanded) {
|
|
35
38
|
this.capped =
|
|
36
39
|
this.expandable &&
|
|
@@ -60,9 +63,31 @@ let HelixHeadlessCodeBlock = class HelixHeadlessCodeBlock extends HelixElement {
|
|
|
60
63
|
.map((node) => node.textContent ?? '')
|
|
61
64
|
.join('');
|
|
62
65
|
}
|
|
66
|
+
markScrollers() {
|
|
67
|
+
for (const box of this.renderRoot.querySelectorAll('[part="block"], [part="code"], [part="old"], [part="new"]')) {
|
|
68
|
+
const style = getComputedStyle(box);
|
|
69
|
+
const scrolls = (value) => value === 'auto' || value === 'scroll';
|
|
70
|
+
const overflows = (scrolls(style.overflowX) && box.scrollWidth > box.clientWidth + 1) ||
|
|
71
|
+
(scrolls(style.overflowY) && box.scrollHeight > box.clientHeight + 1);
|
|
72
|
+
if (overflows) {
|
|
73
|
+
const name = box.closest('[part="file"]')?.querySelector('[part="label"]')?.textContent;
|
|
74
|
+
const named = name ?? (this.label || this.language);
|
|
75
|
+
box.tabIndex = 0;
|
|
76
|
+
box.setAttribute('role', 'region');
|
|
77
|
+
box.setAttribute('aria-label', named
|
|
78
|
+
? formatWords(this, 'codeNameLabel', { code: this.codeLabel, name: named })
|
|
79
|
+
: this.codeLabel);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
box.removeAttribute('tabindex');
|
|
83
|
+
box.removeAttribute('role');
|
|
84
|
+
box.removeAttribute('aria-label');
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
63
88
|
updated(changed) {
|
|
64
89
|
super.updated(changed);
|
|
65
|
-
if (
|
|
90
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
66
91
|
this.sizes ??= new ResizeObserver(this.measure);
|
|
67
92
|
this.sizes.observe(this);
|
|
68
93
|
}
|
|
@@ -114,7 +139,7 @@ let HelixHeadlessCodeBlock = class HelixHeadlessCodeBlock extends HelixElement {
|
|
|
114
139
|
const name = this.label || this.language;
|
|
115
140
|
return html `${this.renderHeader(name)}
|
|
116
141
|
<div part="frame">
|
|
117
|
-
<pre id="block" part="block">${gutter}${code}</pre>
|
|
142
|
+
<pre id="block" part="block" translate="no">${gutter}${code}</pre>
|
|
118
143
|
${name ? nothing : this.renderCopy()}
|
|
119
144
|
</div>${this.renderExpand()}`;
|
|
120
145
|
}
|
|
@@ -179,8 +204,8 @@ let HelixHeadlessCodeBlock = class HelixHeadlessCodeBlock extends HelixElement {
|
|
|
179
204
|
const hunks = file.hunks.map((hunk) => ({ hunk, rows: pairLines(hunk.lines) }));
|
|
180
205
|
const filler = html `<span part="filler" aria-hidden="true">\n</span>`;
|
|
181
206
|
return html `<div part="split">
|
|
182
|
-
<pre part="old"><code part="code">${hunks.map(({ hunk, rows }) => html `${this.renderHunk(hunk)}${rows.map(({ left }) => left ? this.renderLine(left, width, 'old') : filler)}`)}</code></pre>
|
|
183
|
-
<pre part="new"><code part="code">${hunks.map(({ hunk, rows }) => html `${this.renderHunk(hunk, true)}${rows.map(({ right }) => right ? this.renderLine(right, width, 'new') : filler)}`)}</code></pre>
|
|
207
|
+
<pre part="old" translate="no"><code part="code">${hunks.map(({ hunk, rows }) => html `${this.renderHunk(hunk)}${rows.map(({ left }) => left ? this.renderLine(left, width, 'old') : filler)}`)}</code></pre>
|
|
208
|
+
<pre part="new" translate="no"><code part="code">${hunks.map(({ hunk, rows }) => html `${this.renderHunk(hunk, true)}${rows.map(({ right }) => right ? this.renderLine(right, width, 'new') : filler)}`)}</code></pre>
|
|
184
209
|
</div>`;
|
|
185
210
|
}
|
|
186
211
|
renderDiff(files) {
|
|
@@ -197,13 +222,21 @@ let HelixHeadlessCodeBlock = class HelixHeadlessCodeBlock extends HelixElement {
|
|
|
197
222
|
<div part="frame">
|
|
198
223
|
${this.variant === 'split'
|
|
199
224
|
? this.renderSplit(file, width)
|
|
200
|
-
: html `<pre part="block"><code part="code">${file.hunks.map((hunk) => html `${this.renderHunk(hunk)}${hunk.lines.map((line) => this.renderLine(line, width))}`)}</code></pre>`}
|
|
225
|
+
: html `<pre part="block" translate="no"><code part="code">${file.hunks.map((hunk) => html `${this.renderHunk(hunk)}${hunk.lines.map((line) => this.renderLine(line, width))}`)}</code></pre>`}
|
|
201
226
|
${name ? nothing : this.renderCopy(now)}
|
|
202
227
|
</div>
|
|
203
228
|
</div>`;
|
|
204
229
|
})}<slot hidden @slotchange=${() => this.requestUpdate()}></slot>`;
|
|
205
230
|
}
|
|
206
231
|
};
|
|
232
|
+
__decorate([
|
|
233
|
+
words,
|
|
234
|
+
property({ attribute: 'code-label' })
|
|
235
|
+
], HelixHeadlessCodeBlock.prototype, "codeLabel", void 0);
|
|
236
|
+
__decorate([
|
|
237
|
+
words,
|
|
238
|
+
property({ attribute: 'code-name-label' })
|
|
239
|
+
], HelixHeadlessCodeBlock.prototype, "codeNameLabel", void 0);
|
|
207
240
|
__decorate([
|
|
208
241
|
words,
|
|
209
242
|
property({ attribute: 'collapse-label' })
|
|
@@ -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
|
+
};
|