@fusion.dev/helix-headless 0.2.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
|
@@ -7,122 +7,37 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
7
7
|
import { AriaForwardController, customElement, HelixFormElement, MixinControllerChecked, MixinField, } from '@fusion.dev/helix-core';
|
|
8
8
|
import { html, nothing } from 'lit';
|
|
9
9
|
import { property } from 'lit/decorators.js';
|
|
10
|
-
/**
|
|
11
|
-
* @summary A box that is ticked, or not, or neither.
|
|
12
|
-
*
|
|
13
|
-
* @description
|
|
14
|
-
* Renders a real `<input type="checkbox">`, so the space key, the focus ring,
|
|
15
|
-
* and the indeterminate glyph come from the platform. `delegatesFocus` puts
|
|
16
|
-
* focus on it, and it serializes into the server's markup, so the control is
|
|
17
|
-
* reachable before any script loads.
|
|
18
|
-
*
|
|
19
|
-
* **The input inside a shadow root submits nothing**, because the form is on
|
|
20
|
-
* the other side of the boundary. The host is form-associated instead and
|
|
21
|
-
* reports its own value through `ElementInternals`, which is also what carries
|
|
22
|
-
* validity and what answers a form reset.
|
|
23
|
-
*
|
|
24
|
-
* A checked box submits its `value`, and an unchecked one submits nothing at
|
|
25
|
-
* all. That is what a native checkbox does, and a form built around one reads
|
|
26
|
-
* the absence rather than a `false`.
|
|
27
|
-
*
|
|
28
|
-
* `indeterminate` is presentation and nothing else. It is neither checked nor
|
|
29
|
-
* unchecked to a reader, it never submits, and the first click resolves it: a
|
|
30
|
-
* third state that survives interaction is a control nobody can finish.
|
|
31
|
-
*
|
|
32
|
-
* @since 0.1.0
|
|
33
|
-
* @status experimental
|
|
34
|
-
* @category forms
|
|
35
|
-
* @aliases Tickbox, Toggle
|
|
36
|
-
* @documentation https://helix.fusion.dev/components/checkbox
|
|
37
|
-
*
|
|
38
|
-
* @slot - The label.
|
|
39
|
-
*
|
|
40
|
-
* @event {Event} change - Relayed from the input once the state has changed.
|
|
41
|
-
* @event {HelixBeforeToggleEvent} helix-before-toggle - Fired before the state changes, and cancelable.
|
|
42
|
-
* @event {Event} input - Announced once the state has moved.
|
|
43
|
-
*
|
|
44
|
-
* @csspart box - The frame the input is painted as, and the mark sits over.
|
|
45
|
-
* @csspart control - The input.
|
|
46
|
-
* @csspart feedback - What is wrong with it.
|
|
47
|
-
* @csspart field - The row the box and its label share, and the surface the card appearance draws.
|
|
48
|
-
* @csspart hint - Help under the control, while nothing is wrong.
|
|
49
|
-
* @csspart label - The label beside it.
|
|
50
|
-
* @csspart mark - The tick, or the dash when the box is indeterminate.
|
|
51
|
-
*/
|
|
52
10
|
let HelixHeadlessCheckbox = class HelixHeadlessCheckbox extends MixinControllerChecked(MixinField(HelixFormElement)) {
|
|
53
11
|
constructor() {
|
|
54
12
|
super(...arguments);
|
|
55
|
-
/* A consumer's `aria-describedby` names ids in their own tree and the control
|
|
56
|
-
is in this shadow root, so the reference has to be carried down as an
|
|
57
|
-
element rather than copied as an id. See `AriaForwardController`: measured
|
|
58
|
-
in Chrome's own tree, the host held the description and the control the
|
|
59
|
-
reader is actually on was exposed without it. */
|
|
60
13
|
this.aria = new AriaForwardController(this, {
|
|
61
14
|
target: () => this.renderRoot?.querySelector('[part="control"]') ?? null,
|
|
62
15
|
});
|
|
63
|
-
/**
|
|
64
|
-
* Whether the box shows as neither checked nor unchecked.
|
|
65
|
-
*
|
|
66
|
-
* Presentation only. It never submits, and the first change resolves it.
|
|
67
|
-
*/
|
|
68
16
|
this.indeterminate = false;
|
|
69
|
-
/**
|
|
70
|
-
* Value submitted when the box is checked.
|
|
71
|
-
*/
|
|
72
17
|
this.value = 'on';
|
|
73
18
|
this.handleChange = (event) => {
|
|
74
19
|
const input = event.target;
|
|
75
|
-
// The input has already flipped itself. Where the change is refused it is
|
|
76
|
-
// put back to whatever the element resolves to, so the box never shows a
|
|
77
|
-
// state the element does not hold.
|
|
78
20
|
const next = input.checked;
|
|
79
|
-
/* The dash goes only where the change is taken. Clearing it
|
|
80
|
-
* unconditionally leaves a refused change holding the checked state and
|
|
81
|
-
* losing the third one: a box neither ticked nor dashed, which is a state
|
|
82
|
-
* nobody asked for. Refusing leaves everything as it was, which is the
|
|
83
|
-
* argument the line below already makes for `checked`. */
|
|
84
21
|
if (this.requestChecked(next)) {
|
|
85
22
|
this.indeterminate = false;
|
|
86
23
|
}
|
|
87
24
|
input.checked = this.checked;
|
|
88
25
|
this.relayNativeEvent(event);
|
|
89
26
|
};
|
|
90
|
-
/**
|
|
91
|
-
* Whether there is a label to leave room for, written to the host.
|
|
92
|
-
*
|
|
93
|
-
* The row is a flex line with a gap in it, so a label span rendered empty is
|
|
94
|
-
* still a box and still takes that gap: an unlabelled box came out with eight
|
|
95
|
-
* pixels of nothing after it. The input answers this by not rendering the
|
|
96
|
-
* element, which cannot work here, because a checkbox is named by its own
|
|
97
|
-
* content and the property is only that slot's fallback. Gating on the
|
|
98
|
-
* property would unlabel every `<helix-checkbox>Text</helix-checkbox>` ever
|
|
99
|
-
* written.
|
|
100
|
-
*
|
|
101
|
-
* So the slot is asked instead, the same way `Tooltip` asks its own: with
|
|
102
|
-
* `flatten`, because here the fallback counts, being where the property
|
|
103
|
-
* renders. Client only, so a server-rendered unlabelled checkbox carries the
|
|
104
|
-
* gap for one frame. That is a control with no accessible name, which has a
|
|
105
|
-
* worse problem than its spacing.
|
|
106
|
-
*/
|
|
107
27
|
this.handleLabelChange = () => {
|
|
108
28
|
this.syncLabelPresence();
|
|
109
29
|
};
|
|
110
30
|
}
|
|
111
|
-
/** Focus is delegated to the input, so the host is never the focus target. */
|
|
112
31
|
static { this.shadowRootOptions = {
|
|
113
32
|
...HelixFormElement.shadowRootOptions,
|
|
114
33
|
delegatesFocus: true,
|
|
115
34
|
}; }
|
|
116
|
-
/** Reports the value and the validity the host stands behind. */
|
|
117
35
|
sync() {
|
|
118
36
|
this.internals.setFormValue(this.checked ? this.value : null);
|
|
119
37
|
this.reportRequired(!this.checked, 'Tick this box to continue.', 'control');
|
|
120
38
|
}
|
|
121
|
-
/** Returns the box to the state its markup declared. */
|
|
122
39
|
formResetCallback() {
|
|
123
40
|
this.indeterminate = false;
|
|
124
|
-
// `resetChecked` drops both halves, the consumer's and the element's, so
|
|
125
|
-
// there is nothing to clear before it.
|
|
126
41
|
this.resetChecked();
|
|
127
42
|
}
|
|
128
43
|
willUpdate(changed) {
|
|
@@ -134,41 +49,13 @@ let HelixHeadlessCheckbox = class HelixHeadlessCheckbox extends MixinControllerC
|
|
|
134
49
|
this.sync();
|
|
135
50
|
this.syncLabelPresence();
|
|
136
51
|
}
|
|
137
|
-
/**
|
|
138
|
-
* Asks the slot, and records the answer as a state.
|
|
139
|
-
*
|
|
140
|
-
* Called on `slotchange` and after every update, and the second is not
|
|
141
|
-
* belt-and-braces: **`slotchange` does not fire when nothing is assigned.**
|
|
142
|
-
* Without it a checkbox labeled only by its property, which is most of them,
|
|
143
|
-
* never hears about its own label and renders without one.
|
|
144
|
-
*
|
|
145
|
-
* **`:state(has-label)` and not `data-has-label`.** The answer comes from
|
|
146
|
-
* `assignedNodes` and a server has no tree to ask, so an attribute would
|
|
147
|
-
* appear on every labeled checkbox the instant it upgraded and on none that
|
|
148
|
-
* any server sent. See `HelixElement.toggleState`.
|
|
149
|
-
*/
|
|
150
52
|
syncLabelPresence() {
|
|
151
53
|
const slot = this.renderRoot.querySelector('[part="label"] slot');
|
|
152
54
|
const filled = (slot?.assignedNodes({ flatten: true }) ?? []).some((node) => node.textContent?.trim() !== '' || node.nodeType === Node.ELEMENT_NODE);
|
|
153
55
|
this.toggleState('has-label', filled);
|
|
154
|
-
/* And `hidden` on the region itself, because the state is on the host and
|
|
155
|
-
this tier has no sheet to turn one into the other, so a checkbox with no
|
|
156
|
-
label would keep an empty label box beside it. The styled rule stays,
|
|
157
|
-
since it hides through a descendant selector this cannot replace. */
|
|
158
56
|
this.renderRoot.querySelector('[part="label"]')?.toggleAttribute('hidden', !filled);
|
|
159
57
|
}
|
|
160
58
|
render() {
|
|
161
|
-
/* A real `<label>` around both, which is the whole of what makes the text a
|
|
162
|
-
target. A `<span>` beside the input leaves pressing the words doing
|
|
163
|
-
nothing.
|
|
164
|
-
|
|
165
|
-
Labeling is behavior, which is what earns an element here rather than a
|
|
166
|
-
bare slot: the wrapper is not decoration and there is no other way to get
|
|
167
|
-
it. The browser handles the forwarding, so there is no click handler to
|
|
168
|
-
write and none to get wrong.
|
|
169
|
-
|
|
170
|
-
The one line of description goes under that row, which is where a field
|
|
171
|
-
puts it too. */
|
|
172
59
|
return html `<label part="field">
|
|
173
60
|
<span part="box">
|
|
174
61
|
<input
|
|
@@ -8,99 +8,21 @@ import { customElement, DisabledCascadeController, HelixBeforeChangeEvent, Helix
|
|
|
8
8
|
import { html } from 'lit';
|
|
9
9
|
import { property } from 'lit/decorators.js';
|
|
10
10
|
import { HelixHeadlessCheckbox } from '../../checkbox/root/checkbox-root.js';
|
|
11
|
-
/**
|
|
12
|
-
* @summary Groups related checkboxes, and reports them as one set.
|
|
13
|
-
*
|
|
14
|
-
* @description
|
|
15
|
-
* The host is the group, so `role="group"` sits on it through internals and
|
|
16
|
-
* `label` names it. A run of checkboxes with no name is announced as separate
|
|
17
|
-
* controls, which is what the grouping exists to say it is not.
|
|
18
|
-
*
|
|
19
|
-
* **Each checkbox still submits itself.** They are form-associated already and
|
|
20
|
-
* a native group is several inputs sharing a name, so this adds no form value
|
|
21
|
-
* of its own: it would submit the same answers twice. What it adds is the
|
|
22
|
-
* grouping, a shared `name` so it is written once, and one place to read and
|
|
23
|
-
* write the whole set.
|
|
24
|
-
*
|
|
25
|
-
* `value` is the checked values, comma separated, in the order the boxes are
|
|
26
|
-
* written. Setting it ticks exactly those and unticks the rest.
|
|
27
|
-
*
|
|
28
|
-
* **Its message is always given, never derived.** The group is form-associated
|
|
29
|
-
* for validity alone and never calls `setFormValue`, so `feedback` is what shows
|
|
30
|
-
* here: the browser has nothing to say about a set it does not submit.
|
|
31
|
-
*
|
|
32
|
-
* @since 0.1.0
|
|
33
|
-
* @status stable
|
|
34
|
-
* @category forms
|
|
35
|
-
* @aliases Checkbox list, Multi-select
|
|
36
|
-
* @documentation https://helix.fusion.dev/components/checkbox-group
|
|
37
|
-
*
|
|
38
|
-
* @slot - The checkboxes.
|
|
39
|
-
* @slot hint - Richer than a string.
|
|
40
|
-
* @slot label - Richer than a string, where the label is more than one.
|
|
41
|
-
*
|
|
42
|
-
* @event {Event} change - Announced once the value has moved.
|
|
43
|
-
* @event {HelixBeforeChangeEvent} helix-before-change - Fired before any box changes, with the whole set. Cancelable: refusing it takes the decision and nothing moves.
|
|
44
|
-
* @event {Event} input - Announced once the value has moved.
|
|
45
|
-
*
|
|
46
|
-
* @csspart feedback - What is wrong with it.
|
|
47
|
-
* @csspart hint - Help under the set.
|
|
48
|
-
* @csspart items - The list itself, which is what `orientation` lays out.
|
|
49
|
-
* @csspart label - The group's name, over the set.
|
|
50
|
-
*/
|
|
51
11
|
let HelixHeadlessCheckboxGroup = class HelixHeadlessCheckboxGroup extends MixinField(HelixFormElement) {
|
|
52
12
|
constructor() {
|
|
53
|
-
/* `name` is inherited from `HelixFormElement`, reflected and with the
|
|
54
|
-
converter that makes an empty one absent rather than `name=""`. What it
|
|
55
|
-
means differs slightly and the property does not: the group submits nothing
|
|
56
|
-
under it, and hands it to every box that has none. */
|
|
57
13
|
super(...arguments);
|
|
58
|
-
/**
|
|
59
|
-
* The checked values, comma separated.
|
|
60
|
-
*/
|
|
61
14
|
this.value = '';
|
|
62
|
-
/* Stated once, and the group never thinks about it again. The controller is
|
|
63
|
-
what stops the cascade being hand-called from whichever methods a component
|
|
64
|
-
happens to have. */
|
|
65
15
|
this.cascade = new DisabledCascadeController(this, {
|
|
66
16
|
items: () => this.boxes,
|
|
67
17
|
});
|
|
68
|
-
/**
|
|
69
|
-
* Reads a box's requested change into `value`, and says so once.
|
|
70
|
-
*
|
|
71
|
-
* The box is found from the event's target, which does two things. It tells
|
|
72
|
-
* the group which box asked, where the detail carries only the shared name.
|
|
73
|
-
* And it ignores the group's own dispatch, which lands on this same listener
|
|
74
|
-
* because the group is what it is dispatched from: without the guard the
|
|
75
|
-
* group answers itself until the stack gives out.
|
|
76
|
-
*
|
|
77
|
-
* The requested state comes from the event rather than from the box. Driving
|
|
78
|
-
* the boxes from `value` makes them controlled, so a controlled box applies
|
|
79
|
-
* nothing on its own and reports what it was asked for. Reading `checked`
|
|
80
|
-
* here would read the state it still holds and nothing would ever change.
|
|
81
|
-
*/
|
|
82
18
|
this.handleToggle = (event) => {
|
|
83
19
|
const target = event.target;
|
|
84
20
|
const changed = this.boxes.find((box) => box === target);
|
|
85
21
|
if (!changed) {
|
|
86
22
|
return;
|
|
87
23
|
}
|
|
88
|
-
/* A box's `helix-before-toggle` bubbles, and the group is the single voice
|
|
89
|
-
for the set: a consumer listening here wants the whole answer, not one
|
|
90
|
-
box's half of it. */
|
|
91
24
|
event.stopImmediatePropagation();
|
|
92
25
|
const wanted = event.detail.checked;
|
|
93
|
-
/* Announced before it is written, never after: dispatching once the group's
|
|
94
|
-
value has moved leaves `preventDefault()` refusing a set that has already
|
|
95
|
-
changed.
|
|
96
|
-
|
|
97
|
-
Hand-rolled rather than through `MixinControllerValue`, and that is a
|
|
98
|
-
decision rather than an omission. The group's `value` is a derived read of
|
|
99
|
-
the boxes rather than state it owns, so there is nothing for the mixin to
|
|
100
|
-
control.
|
|
101
|
-
|
|
102
|
-
The boxes have already moved either way. Each answered the reader before
|
|
103
|
-
this ran, so refusing here stops only the group's value from following. */
|
|
104
26
|
const next = this.boxes
|
|
105
27
|
.filter((box) => (box === changed ? wanted : box.checked))
|
|
106
28
|
.map((box) => box.value)
|
|
@@ -109,50 +31,18 @@ let HelixHeadlessCheckboxGroup = class HelixHeadlessCheckboxGroup extends MixinF
|
|
|
109
31
|
return;
|
|
110
32
|
}
|
|
111
33
|
this.value = next;
|
|
112
|
-
/* Under the platform's own name, after the value has moved, so a consumer
|
|
113
|
-
binding `onChange` reads the group's new value off the target.
|
|
114
|
-
|
|
115
|
-
`input` beside it, because this is a discrete control and that is the
|
|
116
|
-
pair a native `<select>` sends and the pair every other discrete control
|
|
117
|
-
here sends through `MixinControllerValue`. */
|
|
118
34
|
this.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
|
|
119
35
|
this.dispatchEvent(new Event('change', { bubbles: true, composed: false }));
|
|
120
36
|
};
|
|
121
|
-
/**
|
|
122
|
-
* Stops a box's own announcement, for the reason the request above is stopped.
|
|
123
|
-
*
|
|
124
|
-
* A box announces `input` and `change` from its own host, which is what makes
|
|
125
|
-
* `onChange` work on a checkbox, and both bubble up through this group:
|
|
126
|
-
* measured, one click produced `change` from the group and `input` and
|
|
127
|
-
* `change` from the box, all three at a consumer listening here, two of them
|
|
128
|
-
* describing one box rather than the set.
|
|
129
|
-
*
|
|
130
|
-
* `stopImmediatePropagation` and not `stopPropagation`: the box's event and
|
|
131
|
-
* this group's own carry one name from what a consumer sees as one element,
|
|
132
|
-
* so stopping propagation alone still leaves every other listener on this
|
|
133
|
-
* host receiving both.
|
|
134
|
-
*
|
|
135
|
-
* A listener on the box itself is unaffected, which is where per-box handling
|
|
136
|
-
* belongs. Each box is form-associated and submits itself; what this stops is
|
|
137
|
-
* one box answering a question asked of the set.
|
|
138
|
-
*/
|
|
139
37
|
this.handleMemberNative = (event) => {
|
|
140
38
|
if (event.target !== this) {
|
|
141
39
|
event.stopImmediatePropagation();
|
|
142
40
|
}
|
|
143
41
|
};
|
|
144
42
|
}
|
|
145
|
-
/** The boxes, in light-DOM order. Client only: the server has no tree. */
|
|
146
43
|
get boxes() {
|
|
147
44
|
return [...this.children].filter((child) => child instanceof HelixHeadlessCheckbox);
|
|
148
45
|
}
|
|
149
|
-
/**
|
|
150
|
-
* Pushes the group's name and value onto the boxes.
|
|
151
|
-
*
|
|
152
|
-
* As properties rather than attributes. A `name` written onto a box is a
|
|
153
|
-
* change to markup the consumer rendered, which a React consumer's hydration
|
|
154
|
-
* reports and does not patch up. A box that names itself keeps its own.
|
|
155
|
-
*/
|
|
156
46
|
sync() {
|
|
157
47
|
const wanted = new Set(this.value
|
|
158
48
|
.split(',')
|
|
@@ -165,21 +55,6 @@ let HelixHeadlessCheckboxGroup = class HelixHeadlessCheckboxGroup extends MixinF
|
|
|
165
55
|
box.checked = wanted.has(box.value);
|
|
166
56
|
}
|
|
167
57
|
}
|
|
168
|
-
/**
|
|
169
|
-
* Reads the boxes' own state into `value`, once, before anything is pushed.
|
|
170
|
-
*
|
|
171
|
-
* Seeding, and never a guard in `sync` refusing to write while `value` is
|
|
172
|
-
* empty. The guard would answer the same case, a group written with no `value`
|
|
173
|
-
* whose boxes declare their own `checked` being cleared on the first update,
|
|
174
|
-
* and it also makes `group.value = ''` do nothing at all: the obvious way to
|
|
175
|
-
* clear a group would silently not, and the last box could not be unticked
|
|
176
|
-
* through the group either.
|
|
177
|
-
*
|
|
178
|
-
* The markup decides the starting set, and from then on `value` is what the
|
|
179
|
-
* boxes follow, including when it is empty. The group's `value` is a derived
|
|
180
|
-
* read of the boxes rather than state it owns, so reading it out of them at
|
|
181
|
-
* the start is what it already means.
|
|
182
|
-
*/
|
|
183
58
|
seed() {
|
|
184
59
|
const checked = this.boxes.filter((box) => box.checked).map((box) => box.value);
|
|
185
60
|
if (checked.length > 0) {
|
|
@@ -200,39 +75,15 @@ let HelixHeadlessCheckboxGroup = class HelixHeadlessCheckboxGroup extends MixinF
|
|
|
200
75
|
}
|
|
201
76
|
willUpdate(changed) {
|
|
202
77
|
super.willUpdate(changed);
|
|
203
|
-
/* The string half, which is what a server can write. `linkHost` adds the
|
|
204
|
-
element references on the client, and those win where a consumer slotted
|
|
205
|
-
a label richer than a string. */
|
|
206
78
|
hostAria(this.internals, { label: this.label, role: 'group' });
|
|
207
79
|
this.internals.ariaRequired = this.required ? 'true' : null;
|
|
208
80
|
}
|
|
209
81
|
firstUpdated(changed) {
|
|
210
82
|
super.firstUpdated(changed);
|
|
211
|
-
/* Before the first `sync`, and only ever once: after this the group's value
|
|
212
|
-
is what the boxes follow. Client only, the server having no boxes to
|
|
213
|
-
read. */
|
|
214
83
|
if (this.value === '') {
|
|
215
84
|
this.seed();
|
|
216
85
|
}
|
|
217
86
|
}
|
|
218
|
-
/**
|
|
219
|
-
* Refuses an empty set where one was required.
|
|
220
|
-
*
|
|
221
|
-
* **The group is form-associated, and submits nothing.** Nothing here calls
|
|
222
|
-
* `setFormValue`, so the set that reaches the server is exactly the boxes,
|
|
223
|
-
* each of which submits itself. What form association buys is the other half
|
|
224
|
-
* of participation, validity, and there is no other mechanism for it:
|
|
225
|
-
* `setValidity` throws `NotSupportedError` on an element the platform does not
|
|
226
|
-
* know is a control. happy-dom's shim does not throw, so a spec passes against
|
|
227
|
-
* code that fails on every update in Chromium.
|
|
228
|
-
*
|
|
229
|
-
* It is also what makes the rest work: the form refuses to submit, which fires
|
|
230
|
-
* `invalid`, which is what marks the field touched and puts the message on
|
|
231
|
-
* screen.
|
|
232
|
-
*
|
|
233
|
-
* Anchored on the list, because the browser needs somewhere to point the
|
|
234
|
-
* bubble.
|
|
235
|
-
*/
|
|
236
87
|
validate() {
|
|
237
88
|
this.reportRequired(this.value === '', 'Choose at least one to continue.', 'items');
|
|
238
89
|
}
|
|
@@ -243,16 +94,9 @@ let HelixHeadlessCheckboxGroup = class HelixHeadlessCheckboxGroup extends MixinF
|
|
|
243
94
|
this.linkHost();
|
|
244
95
|
}
|
|
245
96
|
render() {
|
|
246
|
-
/* The list is an element of its own, because the label and the hint are the
|
|
247
|
-
host's children too and `orientation` lays out whatever is in the flex
|
|
248
|
-
container. On the host, a horizontal group would have put its own label
|
|
249
|
-
in the row beside the first box. */
|
|
250
97
|
return html `${this.renderGroupLabel()}
|
|
251
98
|
<div part="items">
|
|
252
|
-
${
|
|
253
|
-
/* A box arriving after first render has neither the shared name nor
|
|
254
|
-
the value's checked state until something pushes them onto it. */
|
|
255
|
-
''}
|
|
99
|
+
${''}
|
|
256
100
|
<slot @slotchange=${() => this.requestUpdate()}></slot>
|
|
257
101
|
</div>
|
|
258
102
|
${this.renderHint()}${this.renderFeedback()}`;
|
|
@@ -6,32 +6,6 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
6
6
|
};
|
|
7
7
|
import { customElement, HelixElement } from '@fusion.dev/helix-core';
|
|
8
8
|
import { html } from 'lit';
|
|
9
|
-
/**
|
|
10
|
-
* @summary A fragment of code inside a line of prose.
|
|
11
|
-
*
|
|
12
|
-
* @description
|
|
13
|
-
* **A real `<code>`, because the element is the meaning.** A styled span reads
|
|
14
|
-
* as code and is not: a screen reader announces nothing about it, a translation
|
|
15
|
-
* tool will happily translate it, and a search that understands documents
|
|
16
|
-
* cannot tell it from emphasis. The tag is the whole point, which is also why
|
|
17
|
-
* this component is so small.
|
|
18
|
-
*
|
|
19
|
-
* **Inline, and only inline.** A block of code is `CodeBlock`, a separate
|
|
20
|
-
* family rather than a variant here. The two differ in more than presentation:
|
|
21
|
-
* one flows with a sentence and must never scroll, the other is a figure with
|
|
22
|
-
* its own box, its own overflow and something to copy. Namespacing them
|
|
23
|
-
* together would say `CodeBlock` is part of `Code`, and it is not.
|
|
24
|
-
*
|
|
25
|
-
* @since 0.1.0
|
|
26
|
-
* @status stable
|
|
27
|
-
* @category typography
|
|
28
|
-
* @aliases Inline code, Kbd
|
|
29
|
-
* @documentation https://helix.fusion.dev/components/code
|
|
30
|
-
*
|
|
31
|
-
* @slot - The code.
|
|
32
|
-
*
|
|
33
|
-
* @csspart code - The `<code>` element.
|
|
34
|
-
*/
|
|
35
9
|
let HelixHeadlessCode = class HelixHeadlessCode extends HelixElement {
|
|
36
10
|
render() {
|
|
37
11
|
return html `<code part="code"><slot></slot></code>`;
|
|
@@ -9,126 +9,26 @@ import { nothing } from 'lit';
|
|
|
9
9
|
import { property } from 'lit/decorators.js';
|
|
10
10
|
import { html } from 'lit/static-html.js';
|
|
11
11
|
import '../../copy-button/index.js';
|
|
12
|
-
/**
|
|
13
|
-
* @summary A block of code, with somewhere to put its name and a way to take it.
|
|
14
|
-
*
|
|
15
|
-
* @description
|
|
16
|
-
* **`<pre><code>` and nothing cleverer.** The pair is what the platform means
|
|
17
|
-
* by a block of code: `pre` keeps the whitespace, `code` says what the
|
|
18
|
-
* whitespace is for, and losing either one loses something no styling puts
|
|
19
|
-
* back. Indentation in a `div` is a paragraph with odd spacing.
|
|
20
|
-
*
|
|
21
|
-
* **It scrolls rather than wraps.** A wrapped line of code is a lie about where
|
|
22
|
-
* the line ends, and in a language where a line break can be significant it is
|
|
23
|
-
* a lie that costs someone an afternoon. The box takes its own scrollbar.
|
|
24
|
-
*
|
|
25
|
-
* **The copy button is here because the alternative is selecting it by hand.**
|
|
26
|
-
* Every consumer would otherwise put one there, slightly differently, and the
|
|
27
|
-
* one that forgot would be the one with the longest snippet. It is on unless
|
|
28
|
-
* `no-copy` says otherwise, and takes its text from the slot rather than from a
|
|
29
|
-
* property, so there is no second copy of the code to keep in step.
|
|
30
|
-
*
|
|
31
|
-
* A `Code` for a fragment inside a sentence. Separate families, because one
|
|
32
|
-
* flows with prose and the other is a figure.
|
|
33
|
-
*
|
|
34
|
-
* @since 0.1.0
|
|
35
|
-
* @status stable
|
|
36
|
-
* @category typography
|
|
37
|
-
* @aliases Code block, Snippet, Pre
|
|
38
|
-
* @documentation https://helix.fusion.dev/components/code-block
|
|
39
|
-
*
|
|
40
|
-
* @slot - The code.
|
|
41
|
-
*
|
|
42
|
-
* @csspart block - The scrolling `<pre>`.
|
|
43
|
-
* @csspart code - The `<code>` inside it.
|
|
44
|
-
* @csspart copy - The copy button.
|
|
45
|
-
* @csspart copy-button - The inner button.
|
|
46
|
-
* @csspart copy-mark - The clipboard glyph, and the confirmation that replaces it.
|
|
47
|
-
* @csspart copy-tooltip - The bubble, forwarded from the tooltip inside.
|
|
48
|
-
* @csspart copy-tooltip-arrow - The point, resting against the trigger.
|
|
49
|
-
* @csspart frame - The code and the copy button beside it.
|
|
50
|
-
* @csspart header - The bar carrying the name, where there is one.
|
|
51
|
-
* @csspart label - The file name or language, where one is given.
|
|
52
|
-
*/
|
|
53
12
|
let HelixHeadlessCodeBlock = class HelixHeadlessCodeBlock extends HelixElement {
|
|
54
13
|
constructor() {
|
|
55
14
|
super(...arguments);
|
|
56
|
-
/**
|
|
57
|
-
* What to call this block, usually a file name or a language.
|
|
58
|
-
*/
|
|
59
15
|
this.label = '';
|
|
60
|
-
/**
|
|
61
|
-
* Whether to leave the copy button off.
|
|
62
|
-
*
|
|
63
|
-
* Negative, which is not a style choice. A boolean attribute is true whenever
|
|
64
|
-
* it is *present*, so a `copyable` defaulting to true could never be turned
|
|
65
|
-
* off from markup: `copyable="false"` is the string "false", which is
|
|
66
|
-
* present, which is true. Measured, and it is why this is named the other way
|
|
67
|
-
* round. The platform already writes booleans like this for the same reason,
|
|
68
|
-
* `novalidate` and `nomodule` and `noreferrer` among them.
|
|
69
|
-
*
|
|
70
|
-
* **Reflected, because the gutter is a stylesheet rule.** `code-block.css`
|
|
71
|
-
* reserves the copy button's width with `:host(:not([no-copy]))`, so the
|
|
72
|
-
* attribute is what decides whether the block pays for a control. Unreflected,
|
|
73
|
-
* setting it from script takes the button away (`render` reads the property)
|
|
74
|
-
* and leaves the 40px it stood in reserved.
|
|
75
|
-
*
|
|
76
|
-
* A false boolean reflects as the attribute's absence, so this costs no
|
|
77
|
-
* `no-copy=""` on hosts that never asked for one, and the rule about a
|
|
78
|
-
* reflected property taking no default is satisfied by the default being
|
|
79
|
-
* `false`.
|
|
80
|
-
*/
|
|
81
16
|
this.noCopy = false;
|
|
82
17
|
}
|
|
83
|
-
/**
|
|
84
|
-
* The code as text, read from the slot rather than stored.
|
|
85
|
-
*
|
|
86
|
-
* Client only, and it has to be: the server has no children to read. The
|
|
87
|
-
* copy button is a client-side affordance anyway, so nothing is lost, and
|
|
88
|
-
* taking the text from a property instead would be a second copy of the code
|
|
89
|
-
* that goes stale the moment the slot changes.
|
|
90
|
-
*/
|
|
91
18
|
get text() {
|
|
92
19
|
const slot = this.renderRoot?.querySelector('slot');
|
|
93
20
|
return (slot?.assignedNodes() ?? []).map((node) => node.textContent ?? '').join('');
|
|
94
21
|
}
|
|
95
|
-
/**
|
|
96
|
-
* One more render, once there is a slot to read.
|
|
97
|
-
*
|
|
98
|
-
* The first render has no shadow children yet, so `text` is empty and the
|
|
99
|
-
* copy button is bound to nothing. `slotchange` normally schedules the render
|
|
100
|
-
* that fixes it, and leaning on that alone was measured to leave the button
|
|
101
|
-
* empty through four update cycles: the event is not guaranteed to have
|
|
102
|
-
* arrived, and in one environment never arrives at all. A component should not
|
|
103
|
-
* need an event to show its own content, so it asks for the second render
|
|
104
|
-
* itself and `slotchange` is left to handle the case it is actually for, which
|
|
105
|
-
* is content changing later.
|
|
106
|
-
*/
|
|
107
22
|
firstUpdated() {
|
|
108
23
|
this.requestUpdate();
|
|
109
24
|
}
|
|
110
25
|
render() {
|
|
111
|
-
/* The copy button sits beside the code, not in a bar above it. In the
|
|
112
|
-
header, a block with nothing to call itself still draws a full-width bar
|
|
113
|
-
to hold one button, so a `Usage` section of one-line imports is a run of
|
|
114
|
-
empty strips. Beside the code, the header exists only when there is a
|
|
115
|
-
name to put in it. */
|
|
116
26
|
return html `${this.label ? html `<div part="header"><span part="label">${this.label}</span></div>` : nothing}
|
|
117
27
|
<div part="frame">
|
|
118
28
|
<pre part="block"><code part="code"><slot @slotchange=${() => this.requestUpdate()}></slot></code></pre>
|
|
119
29
|
${this.noCopy ? nothing : this.renderCopyButton()}
|
|
120
30
|
</div>`;
|
|
121
31
|
}
|
|
122
|
-
/**
|
|
123
|
-
* The button that takes the code, behind a seam the styled tier can move.
|
|
124
|
-
*
|
|
125
|
-
* A method rather than a fragment inline, so a tier above can render its own
|
|
126
|
-
* copy control, wrap this one, or pass it something the base does not know
|
|
127
|
-
* about, without reimplementing the whole template to do it. The tag is
|
|
128
|
-
* interpolated rather than written, so `tier` resolves it: this renders the
|
|
129
|
-
* base button, and a styled subclass that sets `tier` renders the styled one
|
|
130
|
-
* from the same template.
|
|
131
|
-
*/
|
|
132
32
|
renderCopyButton() {
|
|
133
33
|
return html `<${this.tag('copy-button')}
|
|
134
34
|
part="copy"
|