@fusion.dev/helix-headless 0.1.0 → 0.3.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 +234 -234
- package/dist/accordion/item/accordion-item.js +0 -108
- package/dist/accordion/root/accordion-root.js +0 -33
- package/dist/announcement/root/announcement-root.js +1 -76
- package/dist/app-header/root/app-header-root.js +0 -84
- package/dist/avatar/group/avatar-group.js +0 -77
- package/dist/avatar/root/avatar-root.js +0 -72
- package/dist/badge/root/badge-root.js +0 -67
- package/dist/bar-chart/root/bar-chart-root.js +3 -263
- package/dist/blockquote/root/blockquote-root.js +0 -46
- package/dist/breadcrumbs/breadcrumb/breadcrumb.js +0 -72
- package/dist/breadcrumbs/root/breadcrumbs-root.js +0 -52
- package/dist/button/root/button-root.js +0 -215
- package/dist/button-group/root/button-group-root.js +0 -30
- package/dist/callout/root/callout-root.js +0 -52
- package/dist/card/root/card-root.js +0 -41
- package/dist/carousel/root/carousel-root.d.ts +2 -2
- package/dist/carousel/root/carousel-root.js +2 -362
- package/dist/chat/composer/chat-composer.js +0 -131
- package/dist/chat/message/chat-message.js +0 -137
- package/dist/chat/root/chat-root.js +0 -193
- package/dist/checkbox/root/checkbox-root.js +0 -113
- package/dist/checkbox-group/root/checkbox-group-root.js +1 -157
- package/dist/code/root/code-root.js +0 -26
- package/dist/code-block/root/code-block-root.js +0 -100
- package/dist/color-input/root/color-input-root.js +0 -217
- package/dist/color-picker/color.js +0 -69
- package/dist/color-picker/root/color-picker-root.js +0 -272
- package/dist/combobox/root/combobox-root.js +0 -428
- package/dist/command/root/command-root.js +0 -177
- package/dist/copy-button/root/copy-button-root.js +0 -134
- package/dist/data-table/root/data-table-root.js +0 -336
- package/dist/data-table/root/order.js +0 -100
- package/dist/date-input/root/date-input-root.js +1 -398
- package/dist/date-picker/root/date-picker-root.js +0 -358
- package/dist/dialog/root/dialog-root.js +0 -189
- package/dist/divider/root/divider-root.js +0 -71
- package/dist/donut-chart/root/donut-chart-root.js +0 -214
- package/dist/drawer/root/drawer-root.js +0 -116
- package/dist/drilldown/item/drilldown-item.js +0 -82
- package/dist/drilldown/root/drilldown-root.js +0 -91
- package/dist/element-events.generated.js +0 -5
- package/dist/empty/root/empty-root.js +0 -35
- package/dist/file-input/root/file-input-root.js +0 -232
- package/dist/format-date/root/format-date-root.js +0 -95
- package/dist/heatmap/root/buckets.js +0 -57
- package/dist/heatmap/root/heatmap-root.js +0 -313
- package/dist/icon/root/icon-root.js +0 -48
- package/dist/icon-button/root/icon-button-root.js +0 -30
- package/dist/image/root/image-root.js +0 -174
- package/dist/index.js +0 -9
- package/dist/input/root/input-root.js +0 -228
- package/dist/kbd/root/kbd-root.js +0 -117
- package/dist/legend/root/legend-root.js +0 -73
- package/dist/line-chart/root/line-chart-root.js +0 -172
- package/dist/list/item/list-item.js +0 -92
- package/dist/list/root/list-root.js +0 -26
- package/dist/listbox/root/listbox-root.js +2 -429
- package/dist/loader/root/loader-root.js +0 -40
- package/dist/megamenu/root/megamenu-root.js +0 -284
- package/dist/menu/item/menu-item.js +1 -221
- package/dist/menu/root/menu-root.js +1 -171
- package/dist/meter/root/meter-root.js +1 -152
- package/dist/navigation/root/navigation-root.js +0 -56
- package/dist/notifications/notification/notification.js +0 -159
- package/dist/notifications/root/notifications-root.js +0 -148
- package/dist/number-input/root/number-input-root.js +1 -263
- package/dist/option/group/option-group.js +0 -26
- package/dist/option/root/option-root.js +0 -157
- package/dist/outline/root/outline-root.js +0 -292
- package/dist/pagination/root/pagination-root.js +0 -361
- package/dist/pagination/size/pagination-size.js +0 -104
- package/dist/pagination/summary/pagination-summary.js +0 -90
- package/dist/pin-input/root/pin-input-root.js +0 -276
- package/dist/popover/root/popover-root.js +0 -103
- package/dist/progress/root/progress-root.js +1 -133
- package/dist/qr-code/root/qr-code-root.js +0 -96
- package/dist/quote/root/quote-root.js +0 -25
- package/dist/radio/group/radio-group.js +0 -81
- package/dist/radio/root/radio-root.js +0 -85
- package/dist/rating/root/rating-root.js +0 -220
- package/dist/scroll-area/root/scroll-area-root.js +0 -100
- package/dist/select/root/select-root.js +2 -218
- package/dist/skeleton/root/skeleton-root.js +0 -20
- package/dist/slider/root/slider-root.js +1 -267
- package/dist/sparkline/root/sparkline-root.js +3 -164
- package/dist/speed-dial/item/speed-dial-item.js +0 -52
- package/dist/speed-dial/root/speed-dial-root.js +1 -116
- package/dist/splitter/root/splitter-root.js +0 -489
- package/dist/stat/group/stat-group.js +0 -18
- package/dist/stat/root/stat-root.js +0 -23
- package/dist/steps/root/steps-root.js +0 -62
- package/dist/steps/step/step.js +0 -131
- package/dist/switch/root/switch-root.js +0 -80
- package/dist/table/cell/table-cell.js +0 -23
- package/dist/table/column/table-column.js +0 -158
- package/dist/table/root/table-root.js +0 -82
- package/dist/table/row/table-row.js +0 -21
- package/dist/tabs/panel/tab-panel.js +0 -49
- package/dist/tabs/root/tabs-root.js +0 -98
- package/dist/tabs/tab/tab.js +0 -59
- package/dist/tag/root/tag-root.js +0 -31
- package/dist/textarea/root/textarea-root.js +0 -162
- package/dist/time-input/root/time-input-root.js +0 -283
- package/dist/time-picker/root/time-picker-root.js +0 -326
- package/dist/timeline/item/timeline-item.js +0 -66
- package/dist/timeline/root/timeline-root.js +0 -41
- package/dist/toast/item/toast-item.js +0 -165
- package/dist/toast/root/toast-root.js +0 -51
- package/dist/toggle/group/toggle-group.js +0 -150
- package/dist/toggle/root/toggle-root.js +0 -78
- package/dist/toolbar/root/toolbar-root.js +0 -116
- package/dist/tooltip/root/tooltip-root.js +0 -427
- package/dist/tour/root/tour-root.js +0 -130
- package/dist/tour/stop/tour-stop.js +1 -127
- package/dist/tree/item/tree-item.js +0 -198
- package/dist/tree/root/tree-root.js +0 -97
- package/dist/trend/root/trend-root.js +0 -53
- package/dist/video/root/video-root.d.ts +8 -12
- package/dist/video/root/video-root.js +5 -756
- package/dist/video-playlist/item/video-playlist-item.js +1 -90
- package/dist/video-playlist/menu/video-playlist-menu.js +1 -132
- package/dist/video-playlist/root/video-playlist-root.js +0 -113
- package/dist/virtual-list/root/virtual-list-root.js +0 -105
- package/jsx.d.ts +1408 -2576
- package/package.json +6 -6
- package/vue.d.ts +1 -1
|
@@ -10,64 +10,14 @@ import { html, nothing } from 'lit';
|
|
|
10
10
|
import { property } from 'lit/decorators.js';
|
|
11
11
|
import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
|
|
12
12
|
import { HelixHeadlessAccordion } from '../root/accordion-root.js';
|
|
13
|
-
/** Shadow-DOM id linking the trigger's aria-controls to the content region. */
|
|
14
13
|
const CONTENT_ID = 'content';
|
|
15
|
-
/**
|
|
16
|
-
* Shadow-DOM id of the trigger, used to name the content region.
|
|
17
|
-
*
|
|
18
|
-
* A `role="region"` with no accessible name is not exposed as a landmark by
|
|
19
|
-
* screen readers and is flagged by every automated audit, so the region borrows
|
|
20
|
-
* the trigger's text. Both ids live in the same shadow root, so the reference
|
|
21
|
-
* resolves; an IDREF pointing at the light DOM never would.
|
|
22
|
-
*/
|
|
23
14
|
const TRIGGER_ID = 'trigger';
|
|
24
|
-
/**
|
|
25
|
-
* @summary One collapsible section of an accordion.
|
|
26
|
-
*
|
|
27
|
-
* @description
|
|
28
|
-
* Carries its own open state, which is why it is an element rather than a slot.
|
|
29
|
-
* Used alone, outside an accordion, it is a disclosure.
|
|
30
|
-
*
|
|
31
|
-
* Content stays mounted when closed, so it can animate and so the text is
|
|
32
|
-
* present in server-rendered HTML. Closed content carries both `hidden` and
|
|
33
|
-
* `inert`: the first collapses it with no stylesheet in play, the second keeps
|
|
34
|
-
* it out of the tab order and the accessibility tree once the styled tier's
|
|
35
|
-
* `display` has overridden the first.
|
|
36
|
-
*
|
|
37
|
-
* @since 0.1.0
|
|
38
|
-
* @status experimental
|
|
39
|
-
* @aliases Collapsible, Expander, Disclosure
|
|
40
|
-
* @documentation https://helix.fusion.dev/components/accordion
|
|
41
|
-
*
|
|
42
|
-
* @slot - The content revealed when open.
|
|
43
|
-
* @slot summary - The always-visible trigger content.
|
|
44
|
-
*
|
|
45
|
-
* @event {HelixBeforeCloseEvent} helix-before-close - Fired before the section closes, and cancelable.
|
|
46
|
-
* @event {HelixBeforeOpenEvent} helix-before-open - Fired before the section opens. Cancelable: refusing it takes the decision and nothing moves.
|
|
47
|
-
* @event {Event} toggle - Says the state moved, carrying `newState` and `oldState`.
|
|
48
|
-
*
|
|
49
|
-
* @csspart chevron - The open and close indicator.
|
|
50
|
-
* @csspart container - The outer wrapper.
|
|
51
|
-
* @csspart content - The revealed region.
|
|
52
|
-
* @csspart content-inner - The clipping box inside the animated region.
|
|
53
|
-
* @csspart trigger - The clickable summary row.
|
|
54
|
-
*/
|
|
55
15
|
let HelixHeadlessAccordionItem = class HelixHeadlessAccordionItem extends MixinControllerOpen(HelixElement) {
|
|
56
16
|
constructor() {
|
|
57
17
|
super(...arguments);
|
|
58
|
-
/**
|
|
59
|
-
* Opens or closes this item, and enforces the accordion's mode.
|
|
60
|
-
*
|
|
61
|
-
* The item that was clicked is the coordinator. It refuses first, then
|
|
62
|
-
* announces, then applies, then closes its peers, in that order: closing a
|
|
63
|
-
* peer before knowing whether this change was even allowed would leave the
|
|
64
|
-
* set collapsed after a listener refused it.
|
|
65
|
-
*/
|
|
66
18
|
this.handleToggle = () => {
|
|
67
19
|
const next = !this.open;
|
|
68
20
|
const mode = this.accordion?.mode;
|
|
69
|
-
// `single` keeps one open at all times, so the last one cannot be closed.
|
|
70
|
-
// `single-collapsible` is the same rule without that floor.
|
|
71
21
|
if (!next && mode === 'single') {
|
|
72
22
|
return;
|
|
73
23
|
}
|
|
@@ -77,15 +27,12 @@ let HelixHeadlessAccordionItem = class HelixHeadlessAccordionItem extends MixinC
|
|
|
77
27
|
if (next && (mode !== undefined || (this.name !== undefined && this.name !== ''))) {
|
|
78
28
|
for (const peer of this.peers) {
|
|
79
29
|
if (peer !== this && peer.open) {
|
|
80
|
-
// A peer whose consumer refuses the request stays open, which can
|
|
81
|
-
// legitimately leave two open.
|
|
82
30
|
peer.requestOpen(false);
|
|
83
31
|
}
|
|
84
32
|
}
|
|
85
33
|
}
|
|
86
34
|
};
|
|
87
35
|
}
|
|
88
|
-
/** The chevron, or nothing when suppressed. */
|
|
89
36
|
renderChevron() {
|
|
90
37
|
if (this.chevronPlacement === 'none') {
|
|
91
38
|
return nothing;
|
|
@@ -94,30 +41,11 @@ let HelixHeadlessAccordionItem = class HelixHeadlessAccordionItem extends MixinC
|
|
|
94
41
|
${unsafeSVG(caretDown.content)}
|
|
95
42
|
</svg>`;
|
|
96
43
|
}
|
|
97
|
-
/**
|
|
98
|
-
* The nearest accordion above this item, if there is one.
|
|
99
|
-
*
|
|
100
|
-
* Resolved on demand rather than held, so an item moved between accordions
|
|
101
|
-
* coordinates with the one it is in now. `closest` crosses no shadow
|
|
102
|
-
* boundary, which is correct: an item inside another item's content is in
|
|
103
|
-
* that item's light DOM and finds the same accordion, so the check below is
|
|
104
|
-
* what keeps it independent.
|
|
105
|
-
*/
|
|
106
44
|
get accordion() {
|
|
107
45
|
return closestInstance(this, HelixHeadlessAccordion);
|
|
108
46
|
}
|
|
109
|
-
/**
|
|
110
|
-
* The items this one coordinates with: its own siblings, and only those.
|
|
111
|
-
*
|
|
112
|
-
* Direct children only. An accordion nested inside an item's content is a
|
|
113
|
-
* separate set, and opening a section in it must not close anything outside.
|
|
114
|
-
*/
|
|
115
47
|
get peers() {
|
|
116
48
|
if (this.name !== undefined && this.name !== '') {
|
|
117
|
-
/* By name, across the whole root rather than one accordion, which is what
|
|
118
|
-
`<details name>` means. `getRootNode()` so an item inside a shadow tree
|
|
119
|
-
finds its group there rather than reaching into the document, which is
|
|
120
|
-
the same boundary every IDREF in this library respects. */
|
|
121
49
|
const root = this.getRootNode();
|
|
122
50
|
return [
|
|
123
51
|
...root.querySelectorAll(`${this.localName}[name="${CSS.escape(this.name)}"]`),
|
|
@@ -129,45 +57,9 @@ let HelixHeadlessAccordionItem = class HelixHeadlessAccordionItem extends MixinC
|
|
|
129
57
|
}
|
|
130
58
|
return accordion.items;
|
|
131
59
|
}
|
|
132
|
-
/**
|
|
133
|
-
* Moves focus to this item's trigger.
|
|
134
|
-
*
|
|
135
|
-
* The parent accordion drives arrow-key navigation and cannot reach the
|
|
136
|
-
* trigger itself, because it is inside this item's shadow root. Exposing the
|
|
137
|
-
* intent as a method keeps the parent out of the child's internals.
|
|
138
|
-
*/
|
|
139
60
|
focusTrigger() {
|
|
140
61
|
this.renderRoot.querySelector(`#${TRIGGER_ID}`)?.focus();
|
|
141
62
|
}
|
|
142
|
-
/**
|
|
143
|
-
* The item, with `hidden` doing the collapsing.
|
|
144
|
-
*
|
|
145
|
-
* **A closed section has to be closed with no stylesheet at all**, which is
|
|
146
|
-
* the whole claim `headless` makes. `inert` alone is not that: it takes the
|
|
147
|
-
* region out of the accessibility tree and off the tab order and leaves every
|
|
148
|
-
* pixel of it on the screen, so a consumer taking this tier gets sections of
|
|
149
|
-
* text permanently open under a trigger saying `aria-expanded="false"`, and a
|
|
150
|
-
* sighted reader sees content a screen reader is told is not there. Leaving
|
|
151
|
-
* the collapse to the styled tier's `grid-template-rows: 0fr` puts the state
|
|
152
|
-
* in a stylesheet, where only one of the two tiers has it.
|
|
153
|
-
*
|
|
154
|
-
* **`hidden` costs the styled tier nothing**, which is what makes this the
|
|
155
|
-
* cheap fix rather than a trade. The UA sheet hides `[hidden]` from the UA
|
|
156
|
-
* origin, and `accordion-item.css` already sets `display: grid` on this part
|
|
157
|
-
* unconditionally, so the author rule wins and the 0fr transition runs
|
|
158
|
-
* exactly as before. Measured on both tiers.
|
|
159
|
-
*
|
|
160
|
-
* **Not `hidden="until-found"`**, which would be the better accordion and is
|
|
161
|
-
* a different piece of work. It hides through `content-visibility` rather
|
|
162
|
-
* than `display`, so the styled tier's `display: grid` would no longer
|
|
163
|
-
* override it and a collapsed section would animate open to nothing. It also
|
|
164
|
-
* needs a `beforematch` handler to set `open`, or find-in-page reveals text
|
|
165
|
-
* while the trigger still reports collapsed.
|
|
166
|
-
*
|
|
167
|
-
* `inert` stays. `hidden` answers the reader who can see and `inert` answers
|
|
168
|
-
* the one tabbing through, and in the styled tier, where `display` is
|
|
169
|
-
* overridden, `inert` is the only one of the two still in force.
|
|
170
|
-
*/
|
|
171
63
|
render() {
|
|
172
64
|
const open = this.open;
|
|
173
65
|
return html `
|
|
@@ -8,38 +8,7 @@ import { customElement, HelixElement } from '@fusion.dev/helix-core';
|
|
|
8
8
|
import { html } from 'lit';
|
|
9
9
|
import { property } from 'lit/decorators.js';
|
|
10
10
|
import { HelixHeadlessAccordionItem } from '../item/accordion-item.js';
|
|
11
|
-
/** Keys that move focus between triggers, per the APG accordion pattern. */
|
|
12
11
|
const NAVIGATION = new Set(['ArrowDown', 'ArrowUp', 'Home', 'End']);
|
|
13
|
-
/**
|
|
14
|
-
* @summary Groups collapsible sections, one open at a time by default.
|
|
15
|
-
*
|
|
16
|
-
* @description
|
|
17
|
-
* Holds the mode and nothing else. The item that was clicked is what
|
|
18
|
-
* coordinates: it walks up to the nearest accordion, reads the mode off it, and
|
|
19
|
-
* closes its peers itself.
|
|
20
|
-
*
|
|
21
|
-
* Coordination is structural, not a subscription. `helix-before-open` does not bubble
|
|
22
|
-
* and is not composed, because overlays nest and an inner one's lifecycle must
|
|
23
|
-
* never read as the outer one's, so an accordion cannot learn that a child
|
|
24
|
-
* opened by listening for it.
|
|
25
|
-
*
|
|
26
|
-
* Membership resolves on demand, so there is no registry to keep in step: an
|
|
27
|
-
* item added a second after render coordinates on its first click.
|
|
28
|
-
*
|
|
29
|
-
* Items are matched with `instanceof HelixHeadlessAccordionItem`, never by tag
|
|
30
|
-
* name. Matching `helix-accordion-item` would tie this tier to the styled one.
|
|
31
|
-
*
|
|
32
|
-
* Every listener is attached on connect, so none of it runs on the server,
|
|
33
|
-
* where there is no tree to walk and no events to hear.
|
|
34
|
-
*
|
|
35
|
-
* @since 0.1.0
|
|
36
|
-
* @status experimental
|
|
37
|
-
* @category layout
|
|
38
|
-
* @aliases Collapse, Expander group
|
|
39
|
-
* @documentation https://helix.fusion.dev/components/accordion
|
|
40
|
-
*
|
|
41
|
-
* @slot - The accordion's items.
|
|
42
|
-
*/
|
|
43
12
|
let HelixHeadlessAccordion = class HelixHeadlessAccordion extends HelixElement {
|
|
44
13
|
constructor() {
|
|
45
14
|
super(...arguments);
|
|
@@ -66,12 +35,10 @@ let HelixHeadlessAccordion = class HelixHeadlessAccordion extends HelixElement {
|
|
|
66
35
|
else {
|
|
67
36
|
next = last;
|
|
68
37
|
}
|
|
69
|
-
// Only once a move is certain, so an unhandled key still scrolls the page.
|
|
70
38
|
event.preventDefault();
|
|
71
39
|
items[next]?.focusTrigger();
|
|
72
40
|
};
|
|
73
41
|
}
|
|
74
|
-
/** Direct items, in light-DOM order. Client only: the server has no tree. */
|
|
75
42
|
get items() {
|
|
76
43
|
return [...this.children].filter((child) => child instanceof HelixHeadlessAccordionItem);
|
|
77
44
|
}
|
|
@@ -9,50 +9,9 @@ import { x } from '@fusion.dev/helix-icons';
|
|
|
9
9
|
import { html, nothing } from 'lit';
|
|
10
10
|
import { property } from 'lit/decorators.js';
|
|
11
11
|
import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
|
|
12
|
-
/**
|
|
13
|
-
* @summary A page-level message above the masthead, which can be dismissed.
|
|
14
|
-
*
|
|
15
|
-
* @description
|
|
16
|
-
* Chrome rather than content. An announcement sits above the header and speaks
|
|
17
|
-
* for the whole page: a trial ending, a service degraded, a change of terms. A
|
|
18
|
-
* passage that interrupts a document to set itself apart is a `Callout`, which
|
|
19
|
-
* is read where it sits and cannot be dismissed. Neither is a variant of the
|
|
20
|
-
* other.
|
|
21
|
-
*
|
|
22
|
-
* **Always a polite live region.** `role="alert"` is assertive and interrupts
|
|
23
|
-
* whatever is being read, which is right for something that appears mid-session
|
|
24
|
-
* and wrong for something that was in the served HTML: an assertive
|
|
25
|
-
* announcement would cut across a screen-reader user on every page load, before
|
|
26
|
-
* they had read anything. Politeness costs a moment and is never wrong; the tone
|
|
27
|
-
* does not change that, because an error that was always there is still not an
|
|
28
|
-
* interruption.
|
|
29
|
-
*
|
|
30
|
-
* Dismissing is a request. The announcement does not remove itself, because the
|
|
31
|
-
* consumer owns the markup and taking the node out from under them would put
|
|
32
|
-
* their state and the document out of step. `preventDefault()` on the
|
|
33
|
-
* `helix-dismiss` is a refusal.
|
|
34
|
-
*
|
|
35
|
-
* @since 0.1.0
|
|
36
|
-
* @status stable
|
|
37
|
-
* @category feedback
|
|
38
|
-
* @aliases Banner, Notification bar, Alert bar
|
|
39
|
-
* @documentation https://helix.fusion.dev/components/announcement
|
|
40
|
-
*
|
|
41
|
-
* @slot - The message.
|
|
42
|
-
* @slot actions - What to do about it, usually a link or a button.
|
|
43
|
-
* @slot icon - Replaces the glyph inside the dismiss control.
|
|
44
|
-
*
|
|
45
|
-
* @event {HelixDismissEvent} helix-dismiss - Fired when the reader asks to close it. Cancelable: refusing it takes the decision and nothing moves.
|
|
46
|
-
*
|
|
47
|
-
* @csspart actions - The trailing action region.
|
|
48
|
-
* @csspart content - The message region.
|
|
49
|
-
* @csspart dismiss - The control that asks to close.
|
|
50
|
-
* @csspart dismiss-mark - The glyph on the dismiss control, where none is slotted.
|
|
51
|
-
*/
|
|
52
12
|
let HelixHeadlessAnnouncement = class HelixHeadlessAnnouncement extends MixinDismissible(HelixElement) {
|
|
53
13
|
willUpdate(changed) {
|
|
54
14
|
super.willUpdate(changed);
|
|
55
|
-
// On the host, and always polite. See the note above on `role="alert"`.
|
|
56
15
|
hostAria(this.internals, { role: 'status' });
|
|
57
16
|
this.internals.ariaLive = 'polite';
|
|
58
17
|
}
|
|
@@ -60,39 +19,12 @@ let HelixHeadlessAnnouncement = class HelixHeadlessAnnouncement extends MixinDis
|
|
|
60
19
|
super.updated(changed);
|
|
61
20
|
this.markEmpty();
|
|
62
21
|
}
|
|
63
|
-
/**
|
|
64
|
-
* Says on the `actions` region whether anything is in it.
|
|
65
|
-
*
|
|
66
|
-
* An empty region is not free: it is still a flex item, so it still takes the
|
|
67
|
-
* container's gap. There is no selector that can ask, `slot:empty` being
|
|
68
|
-
* about a slot's fallback rather than its assigned nodes, so the component
|
|
69
|
-
* asks and says so, on the region inside this shadow root rather than on the
|
|
70
|
-
* host: an attribute written onto the host is a change to markup the consumer
|
|
71
|
-
* rendered, which React reports on hydration and does not patch up.
|
|
72
|
-
*/
|
|
73
22
|
markEmpty() {
|
|
74
23
|
const region = this.renderRoot?.querySelector('[part="actions"]');
|
|
75
24
|
const empty = !slotFilled(this.renderRoot?.querySelector('slot[name="actions"]'));
|
|
76
25
|
region?.toggleAttribute('data-empty', empty);
|
|
77
|
-
/* `hidden` beside the mirror, because `data-empty` is a hook the styled
|
|
78
|
-
sheet reads and this tier has no sheet: an empty region left in the box
|
|
79
|
-
tree is a gap a consumer cannot close without knowing to look for it. The
|
|
80
|
-
styled rule stays, because that sheet sets `display` on this part and
|
|
81
|
-
therefore outranks the UA one. */
|
|
82
26
|
region?.toggleAttribute('hidden', empty);
|
|
83
27
|
}
|
|
84
|
-
/**
|
|
85
|
-
* The cross inside the dismiss button, behind a seam.
|
|
86
|
-
*
|
|
87
|
-
* Slot fallback, so a consumer replaces it by slotting; and a method, so the
|
|
88
|
-
* tier above replaces it by overriding. Those answer different people: the
|
|
89
|
-
* slot is for the page and the method is for whoever is building the styled
|
|
90
|
-
* layer, and a fallback with only the first is one a tier can restyle and
|
|
91
|
-
* cannot change.
|
|
92
|
-
*
|
|
93
|
-
* Not `renderIcon`, which is what `Toast` and `Callout` call the method that
|
|
94
|
-
* draws a tone mark. An announcement has no tone mark at all.
|
|
95
|
-
*/
|
|
96
28
|
renderDismissIcon() {
|
|
97
29
|
return html `<slot name="icon">
|
|
98
30
|
<svg part="dismiss-mark" viewBox=${x.viewBox} fill="currentColor" aria-hidden="true">
|
|
@@ -103,14 +35,7 @@ let HelixHeadlessAnnouncement = class HelixHeadlessAnnouncement extends MixinDis
|
|
|
103
35
|
render() {
|
|
104
36
|
return html `<div part="content"><slot></slot></div>
|
|
105
37
|
<div part="actions"><slot name="actions"></slot></div>
|
|
106
|
-
${
|
|
107
|
-
/* A plain `<button>`, the way `Menu` renders its way back and every
|
|
108
|
-
other in-shadow control in the library. Never a nested component
|
|
109
|
-
here: `[part="dismiss"]` would select the host and stop at its shadow
|
|
110
|
-
boundary, so the styled tier could not reach the inner control at
|
|
111
|
-
all, and the parent would depend invisibly on the child being
|
|
112
|
-
registered. */
|
|
113
|
-
this.dismissible
|
|
38
|
+
${this.dismissible
|
|
114
39
|
? html `<button
|
|
115
40
|
part="dismiss"
|
|
116
41
|
type="button"
|
|
@@ -7,87 +7,10 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
7
7
|
import { customElement, HelixElement, HelixScrolledEvent, hostAria } from '@fusion.dev/helix-core';
|
|
8
8
|
import { html } from 'lit';
|
|
9
9
|
import { property } from 'lit/decorators.js';
|
|
10
|
-
/**
|
|
11
|
-
* @summary Page-level banner holding brand, navigation, and actions.
|
|
12
|
-
*
|
|
13
|
-
* @description
|
|
14
|
-
* Tracks whether the page has scrolled and carries it as the custom state
|
|
15
|
-
* `scrolled`, so the styled tier can lift the bar off the page without a scroll
|
|
16
|
-
* handler of its own. Scroll position is the one piece of state the server
|
|
17
|
-
* genuinely cannot know, so it is read on connect rather than derived: a fresh
|
|
18
|
-
* load starts at the top, which is what the server rendered, and a reload
|
|
19
|
-
* part-way down corrects on hydration.
|
|
20
|
-
*
|
|
21
|
-
* **A custom state and never `data-scrolled`.** An attribute the element writes
|
|
22
|
-
* onto its own host is one the server did not send, so a hydrating framework
|
|
23
|
-
* reports the tree as mismatched, intermittently: only when the reader reloads
|
|
24
|
-
* far enough down the page to pass the threshold. A consumer can answer that
|
|
25
|
-
* only by suppressing hydration warnings on an element they did not write.
|
|
26
|
-
* `ElementInternals.states` is deliberately not in the DOM, so there is nothing
|
|
27
|
-
* for a hydration pass to disagree with, and `:state(scrolled)` selects it. The
|
|
28
|
-
* base class made the same move for `data-settled` and its note explains the
|
|
29
|
-
* rest.
|
|
30
|
-
*
|
|
31
|
-
* The listener is passive and reads `scrollY` only, so it never blocks
|
|
32
|
-
* scrolling.
|
|
33
|
-
*
|
|
34
|
-
* Three regions, each in its own wrapper, because the row is a grid and a grid
|
|
35
|
-
* needs three items to put the middle one in the middle. A bare slot is
|
|
36
|
-
* `display: contents`, so its assigned elements become grid items themselves:
|
|
37
|
-
* two actions would take two tracks and push the navigation off center. These
|
|
38
|
-
* wrappers earn their boxes, which is the test; they carry no inset or fill of
|
|
39
|
-
* their own, so an empty one still paints nothing.
|
|
40
|
-
*
|
|
41
|
-
* The host is the banner and spans the viewport; the row inside it is what gets
|
|
42
|
-
* centered. A banner that is itself narrowed leaves its background stopping
|
|
43
|
-
* short of the window edges, which reads as a floating bar rather than as the
|
|
44
|
-
* top of the page. That needs one element and one row, not a wrapper as well:
|
|
45
|
-
* the host carries the material and `content` carries the measure.
|
|
46
|
-
*
|
|
47
|
-
* @since 0.1.0
|
|
48
|
-
* @status stable
|
|
49
|
-
* @category navigation
|
|
50
|
-
* @aliases Masthead, Navbar, TopBar
|
|
51
|
-
* @documentation https://helix.fusion.dev/components/app-header
|
|
52
|
-
*
|
|
53
|
-
* @slot - The middle region, usually a `Navigation`.
|
|
54
|
-
* @slot end - Trailing content.
|
|
55
|
-
* @slot start - Leading content, usually a wordmark.
|
|
56
|
-
*
|
|
57
|
-
* @event {HelixScrolledEvent} helix-scrolled - Fired when the page crosses the
|
|
58
|
-
* threshold, with the new state. A custom state cannot be observed the way an
|
|
59
|
-
* attribute could, so the header says when it changes rather than leaving
|
|
60
|
-
* consumers to run a second scroll listener and disagree about the hysteresis.
|
|
61
|
-
*
|
|
62
|
-
* @csspart content - The middle region.
|
|
63
|
-
* @csspart end - The trailing region.
|
|
64
|
-
* @csspart row - The centered row inside the banner.
|
|
65
|
-
* @csspart start - The leading region.
|
|
66
|
-
*/
|
|
67
10
|
let HelixHeadlessAppHeader = class HelixHeadlessAppHeader extends HelixElement {
|
|
68
11
|
constructor() {
|
|
69
12
|
super(...arguments);
|
|
70
|
-
/* Whether the bar currently believes the page has moved, kept alongside the
|
|
71
|
-
custom state so a scroll that changes nothing writes nothing. */
|
|
72
13
|
this.scrolled = false;
|
|
73
|
-
/**
|
|
74
|
-
* A band rather than a line, so the fill cannot flicker at the boundary.
|
|
75
|
-
*
|
|
76
|
-
* **`scrollY > 0` is a threshold with no width, and a threshold with no width
|
|
77
|
-
* is a switch sitting exactly where scroll positions cluster.** Zero is not a
|
|
78
|
-
* quiet place: rubber-banding overshoots past it and comes back, scroll
|
|
79
|
-
* anchoring nudges by a pixel when an image or a font lands, and a
|
|
80
|
-
* `scroll-behavior: smooth` jump settles by oscillating around its target.
|
|
81
|
-
* Every one of those crossings toggles the state, and the fill and the blur
|
|
82
|
-
* are both transitioned, so each toggle starts a fresh animation in the
|
|
83
|
-
* opposite direction. What a reader sees is a bar that shimmers, and text
|
|
84
|
-
* legible through it for as long as the fill is part way in.
|
|
85
|
-
*
|
|
86
|
-
* Eight pixels to take the fill, two to give it back. Asymmetric on purpose:
|
|
87
|
-
* the bar should look attached to the top of the page for as long as it
|
|
88
|
-
* honestly is, so leaving is nearly the whole way back, and arriving needs
|
|
89
|
-
* enough travel to mean the reader moved rather than the layout did.
|
|
90
|
-
*/
|
|
91
14
|
this.handleScroll = () => {
|
|
92
15
|
const y = window.scrollY;
|
|
93
16
|
const next = this.scrolled ? y > 2 : y > 8;
|
|
@@ -95,23 +18,16 @@ let HelixHeadlessAppHeader = class HelixHeadlessAppHeader extends HelixElement {
|
|
|
95
18
|
return;
|
|
96
19
|
}
|
|
97
20
|
this.scrolled = next;
|
|
98
|
-
/* The state is what CSS selects; the event is what anything outside this
|
|
99
|
-
element can hear. Both, because neither covers the other: a state cannot
|
|
100
|
-
be observed and an event cannot be styled. */
|
|
101
21
|
this.toggleState('scrolled', next);
|
|
102
22
|
this.dispatchEvent(new HelixScrolledEvent({ scrolled: next }));
|
|
103
23
|
};
|
|
104
24
|
}
|
|
105
25
|
willUpdate(changed) {
|
|
106
26
|
super.willUpdate(changed);
|
|
107
|
-
// On the host, since the host is the banner.
|
|
108
27
|
hostAria(this.internals, { role: 'banner' });
|
|
109
28
|
}
|
|
110
29
|
connectedCallback() {
|
|
111
30
|
super.connectedCallback();
|
|
112
|
-
// Never reached on the server, which is correct: there is no scroll
|
|
113
|
-
// position to read there, and the un-scrolled markup it emits is what a
|
|
114
|
-
// fresh load actually shows.
|
|
115
31
|
window.addEventListener('scroll', this.handleScroll, { passive: true });
|
|
116
32
|
this.handleScroll();
|
|
117
33
|
}
|
|
@@ -7,89 +7,16 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
7
7
|
import { customElement, HelixElement, hostAria } from '@fusion.dev/helix-core';
|
|
8
8
|
import { html } from 'lit';
|
|
9
9
|
import { property } from 'lit/decorators.js';
|
|
10
|
-
/**
|
|
11
|
-
* The largest `max` the styled tier can hide past.
|
|
12
|
-
*
|
|
13
|
-
* The fold is `:host([max="N"]) ::slotted(:nth-child(n + N+1))`, one selector
|
|
14
|
-
* per value, because `An+B` takes a literal and no custom property can be read
|
|
15
|
-
* into it. So the range is finite and this is where it is written down, in the
|
|
16
|
-
* tier that has to agree with it: a count that folds and a stylesheet that does
|
|
17
|
-
* not would put a "+5" beside five avatars that are still on screen.
|
|
18
|
-
*/
|
|
19
10
|
export const FOLDS_UP_TO = 12;
|
|
20
|
-
/**
|
|
21
|
-
* @summary Overlapping avatars, with a count for the ones not shown.
|
|
22
|
-
*
|
|
23
|
-
* @description
|
|
24
|
-
* A part of `Avatar` rather than a family of its own: it arranges avatars and
|
|
25
|
-
* is about nothing else, and its member is a complete `Avatar` the group did
|
|
26
|
-
* not have to explain.
|
|
27
|
-
*
|
|
28
|
-
* **The overflow count is the group's, not a slot.** Which avatars are hidden
|
|
29
|
-
* is a fact the group works out from `max` and how many it was given, so a
|
|
30
|
-
* consumer writing the "+3" themselves would be writing a number that has to
|
|
31
|
-
* agree with a list they do not control.
|
|
32
|
-
*
|
|
33
|
-
* **Hidden, not removed.** Past `max`, the extra avatars stay in the DOM and
|
|
34
|
-
* stop being shown, so nothing re-renders when `max` changes and the accessible
|
|
35
|
-
* name can still be built from all of them. The count is what a sighted reader
|
|
36
|
-
* gets; the group's own name is what everyone else gets, and "Seven people" is
|
|
37
|
-
* more useful than seven names read in a row.
|
|
38
|
-
*
|
|
39
|
-
* **And hidden by the stylesheet, not by this class.** Hiding from `updated` is
|
|
40
|
-
* one update too late and one element too far out: Lit renders on a microtask,
|
|
41
|
-
* so an avatar appended past the limit is laid out and painted before anything
|
|
42
|
-
* runs to hide it, and marking each one means writing attributes onto markup a
|
|
43
|
-
* consumer owns. The styled tier's
|
|
44
|
-
* `:host([max="N"]) ::slotted(:nth-child(n + N+1))` is right at the first style
|
|
45
|
-
* resolution and writes nothing.
|
|
46
|
-
*
|
|
47
|
-
* **The fold is finite, and the count agrees with it.** `An+B` takes a literal,
|
|
48
|
-
* so there is one selector per value of `max` and the range stops at twelve.
|
|
49
|
-
* Past that the group shows everyone, counts nothing and says so, because
|
|
50
|
-
* showing everyone is a defensible answer and showing everyone beside a "+5" is
|
|
51
|
-
* not. `max` above twelve is a list the consumer should be slicing themselves.
|
|
52
|
-
*
|
|
53
|
-
* **It is a list.** A row of faces is a set of separate people, and a group
|
|
54
|
-
* with no role announces as one run of text. `role="list"` needs its children
|
|
55
|
-
* to be `listitem`, which they cannot be told to become without writing an
|
|
56
|
-
* attribute onto a consumer's element, so the group carries `role="group"` and
|
|
57
|
-
* its name instead: one announcement, correct, and nothing written to anyone
|
|
58
|
-
* else's markup.
|
|
59
|
-
*
|
|
60
|
-
* @since 0.1.0
|
|
61
|
-
* @status stable
|
|
62
|
-
* @aliases Avatar stack, Facepile
|
|
63
|
-
* @documentation https://helix.fusion.dev/components/avatar
|
|
64
|
-
*
|
|
65
|
-
* @slot - The avatars, in the order they should be read.
|
|
66
|
-
*
|
|
67
|
-
* @csspart overflow - The count of avatars past `max`.
|
|
68
|
-
*/
|
|
69
11
|
let HelixHeadlessAvatarGroup = class HelixHeadlessAvatarGroup extends HelixElement {
|
|
70
12
|
constructor() {
|
|
71
13
|
super(...arguments);
|
|
72
|
-
/**
|
|
73
|
-
* Accessible name for the set, such as "Reviewers".
|
|
74
|
-
*/
|
|
75
14
|
this.label = '';
|
|
76
|
-
/** Said once per element, because a re-render is not a second mistake. */
|
|
77
15
|
this.warned = false;
|
|
78
16
|
}
|
|
79
|
-
/** The avatars, in light-DOM order. Empty on the server, which has no tree. */
|
|
80
17
|
get avatars() {
|
|
81
|
-
/* `?? []` is the server: Lit's element shim carries attributes and not a
|
|
82
|
-
tree, so `children` is undefined there and spreading it throws. */
|
|
83
18
|
return [...(this.children ?? [])].filter((child) => child instanceof HTMLElement);
|
|
84
19
|
}
|
|
85
|
-
/**
|
|
86
|
-
* How many avatars are past the limit, and so not shown.
|
|
87
|
-
*
|
|
88
|
-
* Not `hidden`, which is `HTMLElement`'s own boolean: a getter of that name
|
|
89
|
-
* returning a number is a type error, and had it been assignable it would
|
|
90
|
-
* have been worse, because a group with three extra members would have set
|
|
91
|
-
* the attribute that hides the whole group.
|
|
92
|
-
*/
|
|
93
20
|
get overflow() {
|
|
94
21
|
const limit = this.max ?? 0;
|
|
95
22
|
const limited = limit > 0 && limit <= FOLDS_UP_TO;
|
|
@@ -101,10 +28,6 @@ let HelixHeadlessAvatarGroup = class HelixHeadlessAvatarGroup extends HelixEleme
|
|
|
101
28
|
}
|
|
102
29
|
updated(changed) {
|
|
103
30
|
super.updated(changed);
|
|
104
|
-
/* Nothing hides anything here: the stylesheet does it, from `max` on the
|
|
105
|
-
host. What is left is saying so when a `max` is set that no rule can
|
|
106
|
-
match, because showing everyone is a defensible answer and showing
|
|
107
|
-
everyone beside a "+5" is not. */
|
|
108
31
|
const limit = this.max ?? 0;
|
|
109
32
|
if (limit > FOLDS_UP_TO && this.avatars.length > limit && !this.warned) {
|
|
110
33
|
this.warned = true;
|
|
@@ -7,72 +7,21 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
7
7
|
import { customElement, HelixElement, hostAria } from '@fusion.dev/helix-core';
|
|
8
8
|
import { html } from 'lit';
|
|
9
9
|
import { property, state } from 'lit/decorators.js';
|
|
10
|
-
/** Initials for a name: first letter of the first and last words. */
|
|
11
10
|
export const initialsOf = (name) => {
|
|
12
11
|
const words = name.trim().split(/\s+/).filter(Boolean);
|
|
13
12
|
if (words.length === 0) {
|
|
14
13
|
return '';
|
|
15
14
|
}
|
|
16
|
-
/* By code point, not by code unit.
|
|
17
|
-
*
|
|
18
|
-
* `word[0]` indexes UTF-16, so a name beginning with anything outside the
|
|
19
|
-
* basic plane, an emoji or most of the world's less common scripts, yields a
|
|
20
|
-
* lone surrogate and the avatar paints U+FFFD. Spreading a string iterates
|
|
21
|
-
* code points, which is what a first letter means. */
|
|
22
15
|
const initial = (word) => [...(word ?? '')][0] ?? '';
|
|
23
16
|
const first = initial(words[0]);
|
|
24
17
|
const last = words.length > 1 ? initial(words[words.length - 1]) : '';
|
|
25
18
|
return (first + last).toUpperCase();
|
|
26
19
|
};
|
|
27
|
-
/**
|
|
28
|
-
* @summary Represents a person or entity with an image or their initials.
|
|
29
|
-
*
|
|
30
|
-
* @description
|
|
31
|
-
* Shows `src` until it fails to load. Without an image, or after a failure,
|
|
32
|
-
* the default slot is shown; its own fallback is the initials.
|
|
33
|
-
*
|
|
34
|
-
* The host carries the accessible name, taken from `name`, so the `<img>` has an
|
|
35
|
-
* empty `alt` and the initials are hidden from assistive technology. Announcing
|
|
36
|
-
* either would repeat the name. An `aria-label` on the host overrides `name`,
|
|
37
|
-
* for an avatar standing for something a person's name does not describe.
|
|
38
|
-
*
|
|
39
|
-
* `load` and `error` are relayed from the host. They are not composed, so they
|
|
40
|
-
* stop at the shadow boundary otherwise.
|
|
41
|
-
*
|
|
42
|
-
* @since 0.1.0
|
|
43
|
-
* @status stable
|
|
44
|
-
* @category media
|
|
45
|
-
* @aliases Profile picture, Gravatar
|
|
46
|
-
* @documentation https://helix.fusion.dev/components/avatar
|
|
47
|
-
*
|
|
48
|
-
* @slot - Shown when there is no usable image, in place of the initials.
|
|
49
|
-
*
|
|
50
|
-
* @event {Event} error - Relayed from the image when it fails.
|
|
51
|
-
* @event {Event} load - Relayed from the image once it has loaded.
|
|
52
|
-
*
|
|
53
|
-
* @csspart image - The image, when one is loading or loaded.
|
|
54
|
-
* @csspart initials - The initials shown when there is no usable image.
|
|
55
|
-
*/
|
|
56
20
|
let HelixHeadlessAvatar = class HelixHeadlessAvatar extends HelixElement {
|
|
57
21
|
constructor() {
|
|
58
22
|
super(...arguments);
|
|
59
|
-
/**
|
|
60
|
-
* Name of the person or entity.
|
|
61
|
-
*
|
|
62
|
-
* Used for the label and the initials.
|
|
63
|
-
*/
|
|
64
23
|
this.name = '';
|
|
65
|
-
/**
|
|
66
|
-
* Image to show.
|
|
67
|
-
*
|
|
68
|
-
* Falls back to the slot, or to initials, if it fails.
|
|
69
|
-
*/
|
|
70
24
|
this.src = '';
|
|
71
|
-
/**
|
|
72
|
-
* Whether the image failed.
|
|
73
|
-
*
|
|
74
|
-
* The element's own observation, never set.
|
|
75
|
-
*/
|
|
76
25
|
this.failed = false;
|
|
77
26
|
this.handleError = (event) => {
|
|
78
27
|
this.failed = true;
|
|
@@ -84,22 +33,9 @@ let HelixHeadlessAvatar = class HelixHeadlessAvatar extends HelixElement {
|
|
|
84
33
|
}
|
|
85
34
|
willUpdate(changed) {
|
|
86
35
|
super.willUpdate(changed);
|
|
87
|
-
/* A new source is a new attempt.
|
|
88
|
-
*
|
|
89
|
-
* `failed` is an observation about the image that was loading, so it has to
|
|
90
|
-
* be cleared with the source. Latched, an avatar that failed once falls back
|
|
91
|
-
* to initials for the life of the element, and a consumer swapping a broken
|
|
92
|
-
* URL for a good one, or a list reusing an avatar for the next person, gets
|
|
93
|
-
* the initials forever with no error to say why. */
|
|
94
36
|
if (changed.has('src')) {
|
|
95
37
|
this.failed = false;
|
|
96
38
|
}
|
|
97
|
-
/* A role only where there is a name to carry.
|
|
98
|
-
*
|
|
99
|
-
* An unconditional `role="img"` makes an avatar with no `name` an unnamed
|
|
100
|
-
* image: a screen reader announces "image" and stops, which is worse than
|
|
101
|
-
* announcing nothing, because nothing is what a decorative mark should be.
|
|
102
|
-
* With a name it is content and needs the role to hold it. */
|
|
103
39
|
hostAria(this.internals, {
|
|
104
40
|
label: this.name,
|
|
105
41
|
role: this.name === '' ? '' : 'img',
|
|
@@ -117,14 +53,6 @@ let HelixHeadlessAvatar = class HelixHeadlessAvatar extends HelixElement {
|
|
|
117
53
|
}
|
|
118
54
|
return this.renderInitials();
|
|
119
55
|
}
|
|
120
|
-
/**
|
|
121
|
-
* The mark shown where no image loaded, behind a seam a tier can move.
|
|
122
|
-
*
|
|
123
|
-
* A consumer replaces it by slotting something; a tier above replaces it by
|
|
124
|
-
* overriding this, and those answer different people. Written inline it
|
|
125
|
-
* answers only the first, and a styled tier wanting initials drawn its own way
|
|
126
|
-
* has to reimplement the whole template to reach three elements.
|
|
127
|
-
*/
|
|
128
56
|
renderInitials() {
|
|
129
57
|
return html `<slot>
|
|
130
58
|
<span part="initials" aria-hidden="true">${initialsOf(this.name)}</span>
|