@fusion.dev/helix-headless 0.2.0 → 0.4.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,127 +7,33 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
7
7
|
import { customElement, HelixElement, HelixReadEvent, hostAria, MixinCollection, slotFilled, } from '@fusion.dev/helix-core';
|
|
8
8
|
import { html, nothing } from 'lit';
|
|
9
9
|
import { property } from 'lit/decorators.js';
|
|
10
|
-
/**
|
|
11
|
-
* @summary A feed a reader comes back to, made of notifications they may not have seen.
|
|
12
|
-
*
|
|
13
|
-
* @description
|
|
14
|
-
* **Slotted children rather than a data property.** A notification is a small
|
|
15
|
-
* piece of somebody's own markup, with an avatar and a link and a button in it,
|
|
16
|
-
* and there are tens of them rather than thousands, so writing them out lets an
|
|
17
|
-
* item hold anything.
|
|
18
|
-
*
|
|
19
|
-
* **This does not virtualize, and it cannot.** Windowing needs the list to own
|
|
20
|
-
* the data, and here the consumer owns it: N notifications is N elements by
|
|
21
|
-
* construction. A feed that really does hold thousands wants a data table with a
|
|
22
|
-
* custom cell, and this component will not grow into one.
|
|
23
|
-
*
|
|
24
|
-
* **Read state is reported, never invented.** Marking read is usually a round
|
|
25
|
-
* trip: this element asks its items and each item asks the consumer, so a
|
|
26
|
-
* refused `helix-read` leaves everything alone and a consumer who owns `read`
|
|
27
|
-
* sees nothing change until they say so. `unread` is derived from the items and
|
|
28
|
-
* written to an attribute, so a stylesheet and a badge can both read it without
|
|
29
|
-
* either of them counting.
|
|
30
|
-
*
|
|
31
|
-
* @since 0.1.0
|
|
32
|
-
* @status experimental
|
|
33
|
-
* @category feedback
|
|
34
|
-
* @aliases Notification feed, Inbox, Activity
|
|
35
|
-
* @documentation https://helix.fusion.dev/components/notifications
|
|
36
|
-
*
|
|
37
|
-
* @slot - The notifications, as `helix-notification` children.
|
|
38
|
-
* @slot footer - Below the list, for a link to everything.
|
|
39
|
-
* @slot header - Above the list, for a title and the mark-all control.
|
|
40
|
-
*
|
|
41
|
-
* @event {HelixReadEvent} helix-read - Fired by an item, and by this element when everything is marked read. Cancelable.
|
|
42
|
-
*
|
|
43
|
-
* @csspart empty - Shown when there is nothing in the feed.
|
|
44
|
-
* @csspart footer - The region below the list.
|
|
45
|
-
* @csspart header - The region above the list.
|
|
46
|
-
* @csspart list - The list itself.
|
|
47
|
-
* @csspart loading - Shown while the feed is still on its way.
|
|
48
|
-
* @csspart mark-all - The control that marks everything read.
|
|
49
|
-
*/
|
|
50
10
|
let HelixHeadlessNotifications = class HelixHeadlessNotifications extends MixinCollection(HelixElement) {
|
|
51
11
|
constructor() {
|
|
52
12
|
super(...arguments);
|
|
53
|
-
/**
|
|
54
|
-
* Accessible name for the feed.
|
|
55
|
-
*/
|
|
56
13
|
this.label = '';
|
|
57
|
-
/**
|
|
58
|
-
* Whether to draw a control that marks everything read.
|
|
59
|
-
*/
|
|
60
14
|
this.markAll = false;
|
|
61
|
-
/**
|
|
62
|
-
* Label for that control.
|
|
63
|
-
*/
|
|
64
15
|
this.markAllLabel = 'Mark all as read';
|
|
65
|
-
/** Items arrive from the light DOM, which changes no property here. */
|
|
66
16
|
this.handleSlotChange = () => {
|
|
67
17
|
this.assign();
|
|
68
18
|
this.requestUpdate();
|
|
69
19
|
};
|
|
70
|
-
/**
|
|
71
|
-
* An item's own read event, heard on the way past, so the count follows.
|
|
72
|
-
*
|
|
73
|
-
* `unread` is derived from the items and nothing about an item changing tells
|
|
74
|
-
* Lit to look again. Not stopped: the event is the consumer's to hear, and
|
|
75
|
-
* this element is a bystander that happens to need to redraw.
|
|
76
|
-
*/
|
|
77
20
|
this.handleRead = () => {
|
|
78
|
-
/* After the item has applied it, not before: read synchronously the count
|
|
79
|
-
would be the one from before the change and the badge would trail by one
|
|
80
|
-
every time. */
|
|
81
21
|
queueMicrotask(() => this.requestUpdate());
|
|
82
22
|
};
|
|
83
23
|
this.handleMarkAll = () => {
|
|
84
24
|
this.markEveryoneRead();
|
|
85
25
|
};
|
|
86
26
|
}
|
|
87
|
-
/**
|
|
88
|
-
* Nothing is assigned by name, so the items land in a list and the rest does not.
|
|
89
|
-
*
|
|
90
|
-
* `role="listitem"` under anything but a `list` is a list item belonging to
|
|
91
|
-
* nothing, and the items are ordinary children a consumer writes with no
|
|
92
|
-
* `slot`. Manual assignment is the only way to wrap them without editing the
|
|
93
|
-
* consumer's markup, which is a change a React consumer's hydration reports
|
|
94
|
-
* and does not patch up.
|
|
95
|
-
*/
|
|
96
27
|
static { this.shadowRootOptions = {
|
|
97
28
|
...HelixElement.shadowRootOptions,
|
|
98
29
|
slotAssignment: 'manual',
|
|
99
30
|
}; }
|
|
100
|
-
/**
|
|
101
|
-
* How many items are unread, counted from the items themselves.
|
|
102
|
-
*
|
|
103
|
-
* Read-only because it is derived: a count the consumer could set is a second
|
|
104
|
-
* source of truth for something the items already know.
|
|
105
|
-
*
|
|
106
|
-
* **The attribute is written in `updated`, never reflected.**
|
|
107
|
-
* `@property({ reflect: true })` on a getter with no setter reflects once: Lit
|
|
108
|
-
* reflects what is in `changedProperties`, and nothing puts a derived getter
|
|
109
|
-
* there after the first update. Measured with two items, one marked read: the
|
|
110
|
-
* property answered 1 and the attribute still said 2, so a badge reading the
|
|
111
|
-
* attribute showed a count for an item the reader had just opened.
|
|
112
|
-
*/
|
|
113
31
|
get unread() {
|
|
114
32
|
return this.items.filter((item) => !item.read).length;
|
|
115
33
|
}
|
|
116
|
-
/** The notifications declared as children, in the order they are written. */
|
|
117
34
|
get items() {
|
|
118
|
-
/* `?? []` is the server. Lit's element shim carries attributes and not a
|
|
119
|
-
tree, so `children` is undefined there, and this getter is reached from
|
|
120
|
-
`unread` during the first update. */
|
|
121
35
|
return [...(this.children ?? [])].filter((child) => 'isNotification' in child && child.isNotification === true);
|
|
122
36
|
}
|
|
123
|
-
/**
|
|
124
|
-
* Marks every unread item read, and reports how many actually moved.
|
|
125
|
-
*
|
|
126
|
-
* **Through each item's own request rather than by writing its property**,
|
|
127
|
-
* which is what makes one refusal refuse one item: a consumer who owns some
|
|
128
|
-
* items and not others gets the right answer for each, and a server that
|
|
129
|
-
* rejects one does not silently mark the rest.
|
|
130
|
-
*/
|
|
131
37
|
markEveryoneRead() {
|
|
132
38
|
if (!this.dispatchEvent(new HelixReadEvent({ read: true }))) {
|
|
133
39
|
return 0;
|
|
@@ -166,56 +72,13 @@ let HelixHeadlessNotifications = class HelixHeadlessNotifications extends MixinC
|
|
|
166
72
|
super.willUpdate(changed);
|
|
167
73
|
hostAria(this.internals, { label: this.label || 'Notifications', role: 'region' });
|
|
168
74
|
}
|
|
169
|
-
/**
|
|
170
|
-
* Says whether anything in the feed has media, so every item can hold the
|
|
171
|
-
* column open whether or not it has one of its own.
|
|
172
|
-
*
|
|
173
|
-
* **A fact no item can answer for itself**, which is why it is here. An item
|
|
174
|
-
* knows about its own media slot and nothing about its neighbors, so a feed
|
|
175
|
-
* where some rows have an avatar and some do not draws two different left
|
|
176
|
-
* edges for its text: measured on a four item feed, the rows without media
|
|
177
|
-
* started their text 32 pixels to the left of the rows with it. The styled
|
|
178
|
-
* tier turns this into a width; this tier ships no stylesheet and is
|
|
179
|
-
* unaffected.
|
|
180
|
-
*
|
|
181
|
-
* Read off the light DOM rather than off the items, so it does not depend on
|
|
182
|
-
* which children have upgraded by the time this runs.
|
|
183
|
-
*/
|
|
184
75
|
markMedia() {
|
|
185
76
|
this.toggleState('media', this.items.some((item) => item.querySelector(':scope > [slot="media"]') !== null));
|
|
186
77
|
}
|
|
187
|
-
/**
|
|
188
|
-
* Says on each region whether anything is in it.
|
|
189
|
-
*
|
|
190
|
-
* **A stylesheet cannot ask this, and the obvious selector fails twice over.**
|
|
191
|
-
* `[part="header"]:not(:has(> slot:has(*)))`: `:has()` may not be nested
|
|
192
|
-
* inside `:has()`, so the selector fails to parse and the whole rule is
|
|
193
|
-
* discarded; and a slot's assigned nodes are not its DOM children, so
|
|
194
|
-
* `slot:has(*)` reports whether the slot has *fallback* rather than whether a
|
|
195
|
-
* consumer filled it, and would have been false either way. Both verified in a
|
|
196
|
-
* browser: `insertRule` throws a `SyntaxError` on the first, and an assigned
|
|
197
|
-
* element still leaves `slot.matches(':has(*)')` false.
|
|
198
|
-
*
|
|
199
|
-
* `slotFilled` is what the rest of the library already asks with. It is said
|
|
200
|
-
* on the region inside this shadow root rather than on the host, because an
|
|
201
|
-
* attribute written onto the host is a change to markup the consumer rendered,
|
|
202
|
-
* which React reports on hydration and does not patch up.
|
|
203
|
-
*/
|
|
204
78
|
markEmpty() {
|
|
205
|
-
/* The header is not empty while this element is drawing its own control into
|
|
206
|
-
it. */
|
|
207
79
|
this.markRegion('header', ['header'], '[part="mark-all"]');
|
|
208
80
|
this.markRegion('footer', ['footer']);
|
|
209
81
|
}
|
|
210
|
-
/**
|
|
211
|
-
* One region, hidden when its slot holds nothing and nothing of ours fills it.
|
|
212
|
-
*
|
|
213
|
-
* `hidden` beside the `data-empty` mirror: `data-empty` is the hook the styled
|
|
214
|
-
* sheet reads, and this tier ships no sheet, so an empty region left in the box
|
|
215
|
-
* tree would be a gap a headless consumer cannot close without knowing to look
|
|
216
|
-
* for it. The styled rule stays too, because that sheet sets `display` on these
|
|
217
|
-
* parts and therefore outranks the UA's `[hidden]` one.
|
|
218
|
-
*/
|
|
219
82
|
markRegion(part, slots, occupied) {
|
|
220
83
|
const region = this.renderRoot?.querySelector(`[part="${part}"]`);
|
|
221
84
|
const empty = !slots.some((name) => slotFilled(this.renderRoot?.querySelector(`slot[name="${name}"]`))) && !(occupied && this.renderRoot?.querySelector(occupied));
|
|
@@ -224,20 +87,9 @@ let HelixHeadlessNotifications = class HelixHeadlessNotifications extends MixinC
|
|
|
224
87
|
}
|
|
225
88
|
updated(changed) {
|
|
226
89
|
super.updated(changed);
|
|
227
|
-
/* Before `markEmpty`, which reads what is assigned: on a feed with no items
|
|
228
|
-
nothing else ever schedules a second update, so a header asked about
|
|
229
|
-
before its slot was filled stays hidden for the life of the element. The
|
|
230
|
-
empty feed is the one state where the header is the only thing on screen. */
|
|
231
90
|
this.assign();
|
|
232
91
|
this.markEmpty();
|
|
233
92
|
this.markMedia();
|
|
234
|
-
/* Written after the render rather than reflected during it, so it follows
|
|
235
|
-
every change rather than only the first.
|
|
236
|
-
|
|
237
|
-
**Zero is the absence of the attribute, not `unread="0"`.** Writing it at
|
|
238
|
-
rest puts an attribute on the host that a consumer's server never sent,
|
|
239
|
-
and `updated` is no later than hydration: it is the same microtask
|
|
240
|
-
checkpoint `firstUpdated` runs in. Only a departure from resting writes. */
|
|
241
93
|
const unread = this.unread > 0 ? String(this.unread) : null;
|
|
242
94
|
if (this.getAttribute('unread') !== unread) {
|
|
243
95
|
if (unread === null) {
|
|
@@ -9,154 +9,24 @@ import { minus, plus } 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
|
-
* How far a pointer travels for one step of the value, in pixels.
|
|
14
|
-
*
|
|
15
|
-
* Four, measured against the alternatives rather than picked: at one the value
|
|
16
|
-
* runs away from the pointer and a field with `step="1"` crosses a hundred in
|
|
17
|
-
* the width of a word, and at ten a reader has to cross the viewport to reach
|
|
18
|
-
* a useful number. Four is roughly a character's width at the sizes this is set
|
|
19
|
-
* in, so a step reads as "about one glyph of travel".
|
|
20
|
-
*/
|
|
21
12
|
const SCRUB_PITCH = 4;
|
|
22
|
-
/**
|
|
23
|
-
* And the same step over four times the travel, which is what fine means here.
|
|
24
|
-
*
|
|
25
|
-
* Fine lengthens the distance rather than shrinking the increment. A drag that
|
|
26
|
-
* produced values between the steps would be the one input in the library that
|
|
27
|
-
* can reach a number its own arrow keys cannot, and `step` would stop being the
|
|
28
|
-
* grid the control is on.
|
|
29
|
-
*/
|
|
30
13
|
const SCRUB_FINE = 16;
|
|
31
|
-
/**
|
|
32
|
-
* @summary A number a reader types or steps to.
|
|
33
|
-
*
|
|
34
|
-
* @description
|
|
35
|
-
* **A family rather than `Input`'s `type="number"`, and rather than a part of
|
|
36
|
-
* it.** `Input` already takes that type and will keep it: a bare numeric field
|
|
37
|
-
* in a form is a text field with a different keyboard, and that is all it needs
|
|
38
|
-
* to be. This is a different control. It renders two buttons, binds four keys
|
|
39
|
-
* that `Input` does not, and owns clamping and stepping, so it is an alternative
|
|
40
|
-
* and not a variant. Namespacing it under `Input` would say it is contained by
|
|
41
|
-
* one, which is what `IconButton` is top-level for.
|
|
42
|
-
*
|
|
43
|
-
* **The field is `type="text"` with `inputmode="decimal"`, and that is a
|
|
44
|
-
* measurement rather than a preference.** Asked of a real browser, `type=number`
|
|
45
|
-
* fails three ways this control cannot live with:
|
|
46
|
-
*
|
|
47
|
-
* ```
|
|
48
|
-
* typed "12e" into type=number → .value === ""
|
|
49
|
-
* focused, wheeled over it → 5 became 4
|
|
50
|
-
* selectionStart on type=number → null
|
|
51
|
-
* same keystrokes into type=text → .value === "12e"
|
|
52
|
-
* ```
|
|
53
|
-
*
|
|
54
|
-
* The first is fatal on its own: the element cannot see what the reader typed,
|
|
55
|
-
* so a controlled value is destroyed by a keystroke and there is nothing left to
|
|
56
|
-
* correct or to report. The second changes a value because a page scrolled past.
|
|
57
|
-
* The third means the caret cannot be managed at all, which any correcting or
|
|
58
|
-
* formatting field needs. `inputmode` is what keeps the phone keypad, which is
|
|
59
|
-
* the one thing `type=number` was giving.
|
|
60
|
-
*
|
|
61
|
-
* Validity is still the platform's, through `min`, `max` and `step` mirrored
|
|
62
|
-
* onto the host's own `setValidity`. What is not mirrored is the field's
|
|
63
|
-
* `type`, because a text field has no opinion about numbers.
|
|
64
|
-
*
|
|
65
|
-
* **Stepping is separate from typing.** A reader mid-way through "1" of "12"
|
|
66
|
-
* has not asked for anything to be clamped, so the value is only pulled into
|
|
67
|
-
* range when they step, blur, or the form asks. Clamping on every keystroke is
|
|
68
|
-
* what makes a field that cannot be typed backwards into.
|
|
69
|
-
*
|
|
70
|
-
* @since 0.1.0
|
|
71
|
-
* @status experimental
|
|
72
|
-
* @category forms
|
|
73
|
-
* @aliases NumberField, Stepper, InputNumber
|
|
74
|
-
* @documentation https://helix.fusion.dev/components/number-input
|
|
75
|
-
*
|
|
76
|
-
* @slot end - After it.
|
|
77
|
-
* @slot start - Before the number, for a unit or an icon.
|
|
78
|
-
*
|
|
79
|
-
* @event {Event} change - Relayed from the field once the reader has finished.
|
|
80
|
-
* @event {HelixBeforeChangeEvent} helix-before-change - Fired as the value changes, and cancelable.
|
|
81
|
-
* @event {Event} input - Announced once the value has moved.
|
|
82
|
-
*
|
|
83
|
-
* @csspart control - The field itself.
|
|
84
|
-
* @csspart decrement - The control that steps down.
|
|
85
|
-
* @csspart decrement-mark - The glyph on the step down control.
|
|
86
|
-
* @csspart feedback - What is wrong with it.
|
|
87
|
-
* @csspart field - The box holding everything.
|
|
88
|
-
* @csspart hint - Help under the field, while nothing is wrong.
|
|
89
|
-
* @csspart increment - The control that steps up.
|
|
90
|
-
* @csspart increment-mark - The glyph on the step up control.
|
|
91
|
-
* @csspart label - The name over the field, which is also the scrub grip.
|
|
92
|
-
*/
|
|
93
14
|
let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinControllerValue(MixinField(HelixFormElement)) {
|
|
94
15
|
constructor() {
|
|
95
16
|
super(...arguments);
|
|
96
|
-
/* A consumer's `aria-describedby` names ids in their own tree and the control
|
|
97
|
-
is in this shadow root, so the reference has to be carried down as an
|
|
98
|
-
element rather than copied as an id. See `AriaForwardController`: measured
|
|
99
|
-
in Chrome's own tree, the host held the description and the control the
|
|
100
|
-
reader is actually on was exposed without it. */
|
|
101
17
|
this.aria = new AriaForwardController(this, {
|
|
102
18
|
target: () => this.renderRoot?.querySelector('[part="control"]') ?? null,
|
|
103
19
|
});
|
|
104
|
-
/** Typed or dragged, so every edit is announced. See `announcesInput`. */
|
|
105
20
|
this.announcesInput = true;
|
|
106
|
-
/**
|
|
107
|
-
* What the browser may autofill here, as one of the platform's own tokens.
|
|
108
|
-
*
|
|
109
|
-
* Empty leaves the attribute off, which is the browser's default and is what
|
|
110
|
-
* lets autofill work at all. Hardcoding `off` here is what stops a reader
|
|
111
|
-
* having a birthday or a postal code filled in for them, with no way for a
|
|
112
|
-
* consumer to ask for it.
|
|
113
|
-
*/
|
|
114
21
|
this.autocomplete = '';
|
|
115
|
-
/**
|
|
116
|
-
* Accessible name for the control that steps down.
|
|
117
|
-
*/
|
|
118
22
|
this.decrementLabel = 'Decrease';
|
|
119
|
-
/**
|
|
120
|
-
* Accessible name for the control that steps up.
|
|
121
|
-
*/
|
|
122
23
|
this.incrementLabel = 'Increase';
|
|
123
|
-
/**
|
|
124
|
-
* Shown while the field is empty.
|
|
125
|
-
*
|
|
126
|
-
* Not a label: it goes when typing starts.
|
|
127
|
-
*/
|
|
128
24
|
this.placeholder = '';
|
|
129
|
-
/**
|
|
130
|
-
* Whether the field refuses input while still submitting its value.
|
|
131
|
-
*/
|
|
132
25
|
this.readOnly = false;
|
|
133
|
-
/**
|
|
134
|
-
* Whether a grip is shown that changes the value when it is dragged.
|
|
135
|
-
*
|
|
136
|
-
* The label is what is dragged, so this adds no chrome at all. Still opt-in,
|
|
137
|
-
* because it changes what a press on the label does and a reader who drags
|
|
138
|
-
* across a word expecting to select it should get that everywhere it has not
|
|
139
|
-
* been asked for. Where it earns its place is a value a reader tunes rather
|
|
140
|
-
* than states: a duration, a weight, a count they are looking for rather than
|
|
141
|
-
* one they already know.
|
|
142
|
-
*/
|
|
143
26
|
this.scrub = false;
|
|
144
|
-
/**
|
|
145
|
-
* How far one step moves.
|
|
146
|
-
*/
|
|
147
27
|
this.step = 1;
|
|
148
|
-
/** Where the pointer went down, and what the value was there. */
|
|
149
28
|
this.scrubFrom = 0;
|
|
150
29
|
this.scrubValue = 0;
|
|
151
|
-
/**
|
|
152
|
-
* Press, and the value follows the pointer.
|
|
153
|
-
*
|
|
154
|
-
* Captured, so a drag that leaves the grip keeps going: a reader tuning a
|
|
155
|
-
* number moves further than a 20 pixel handle and the gesture ending because
|
|
156
|
-
* they overshot it would be the control refusing the very thing it offered.
|
|
157
|
-
* `preventDefault` because a press inside a field otherwise starts a text
|
|
158
|
-
* selection, which drags a blue wash across the label at the same time.
|
|
159
|
-
*/
|
|
160
30
|
this.handleScrubDown = (event) => {
|
|
161
31
|
if (this.isDisabled || this.readOnly) {
|
|
162
32
|
return;
|
|
@@ -167,15 +37,6 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
|
|
|
167
37
|
this.scrubValue = this.numeric ?? this.min ?? 0;
|
|
168
38
|
event.preventDefault();
|
|
169
39
|
};
|
|
170
|
-
/**
|
|
171
|
-
* Read from where the press began, never from the last frame.
|
|
172
|
-
*
|
|
173
|
-
* Accumulating a delta per move drifts: every rounding to a step throws away
|
|
174
|
-
* the remainder, and a slow drag across a hundred events throws it away a
|
|
175
|
-
* hundred times, so the value and the pointer disagree by more the longer
|
|
176
|
-
* anyone holds. From the origin the arithmetic is the same on every frame and
|
|
177
|
-
* the value can only be where the pointer says it is.
|
|
178
|
-
*/
|
|
179
40
|
this.handleScrubMove = (event) => {
|
|
180
41
|
const grip = event.currentTarget;
|
|
181
42
|
if (!grip.hasPointerCapture(event.pointerId)) {
|
|
@@ -186,10 +47,6 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
|
|
|
186
47
|
const pitch = event.altKey ? SCRUB_FINE : SCRUB_PITCH;
|
|
187
48
|
const steps = Math.round((event.clientX - this.scrubFrom) / pitch);
|
|
188
49
|
const next = String(this.clamp(this.scrubValue + steps * unit));
|
|
189
|
-
/* Compared before it is sent. A drag produces a move event per frame and
|
|
190
|
-
most of them land on the step the value is already on, and announcing
|
|
191
|
-
those would be `helix-input` firing sixty times a second saying nothing
|
|
192
|
-
changed. */
|
|
193
50
|
if (next === this.value) {
|
|
194
51
|
return;
|
|
195
52
|
}
|
|
@@ -199,43 +56,18 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
|
|
|
199
56
|
restoreValue(control, this.value);
|
|
200
57
|
}
|
|
201
58
|
};
|
|
202
|
-
/** Let go, and the capture goes with it. */
|
|
203
59
|
this.handleScrubEnd = (event) => {
|
|
204
60
|
const grip = event.currentTarget;
|
|
205
61
|
if (grip.hasPointerCapture(event.pointerId)) {
|
|
206
62
|
grip.releasePointerCapture(event.pointerId);
|
|
207
63
|
}
|
|
208
64
|
};
|
|
209
|
-
/**
|
|
210
|
-
* Every keystroke, cancelable, and put back where it is refused.
|
|
211
|
-
*
|
|
212
|
-
* Not clamped here. A reader typing "12" is at "1" for one keystroke, and a
|
|
213
|
-
* field with `min="10"` that corrected them at that moment could never be
|
|
214
|
-
* typed into at all. Range is settled on blur, on a step, and at submit.
|
|
215
|
-
*/
|
|
216
65
|
this.handleInput = (event) => {
|
|
217
|
-
// Stopped for the reason `Input.handleInput` gives: the inner control's own
|
|
218
|
-
// `input` is composed and would reach a consumer beside this element's.
|
|
219
66
|
event.stopPropagation();
|
|
220
67
|
const control = event.target;
|
|
221
68
|
this.requestValue(control.value);
|
|
222
69
|
restoreValue(control, this.value);
|
|
223
70
|
};
|
|
224
|
-
/**
|
|
225
|
-
* Leaving the field is where a half-typed number becomes a number.
|
|
226
|
-
*
|
|
227
|
-
* Blank is left blank, because an empty optional field is a legitimate answer
|
|
228
|
-
* and filling it with `min` would put a number in a form the reader never
|
|
229
|
-
* gave.
|
|
230
|
-
*
|
|
231
|
-
* **And text that is not a number is left alone.** Clearing it means typing
|
|
232
|
-
* `2.7e` and tabbing away empties the field: the reader looks back and their
|
|
233
|
-
* entry is gone with nothing saying why, and the likeliest cause is a fat
|
|
234
|
-
* finger on a number they had almost finished. Nothing is lost by keeping it,
|
|
235
|
-
* because `sync` has already marked it `badInput`, so the field shows as wrong
|
|
236
|
-
* and the form refuses to submit. Destroying the evidence of a mistake is not
|
|
237
|
-
* the same as preventing one.
|
|
238
|
-
*/
|
|
239
71
|
this.handleBlur = () => {
|
|
240
72
|
const control = this.control;
|
|
241
73
|
const parsed = this.numeric;
|
|
@@ -245,18 +77,10 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
|
|
|
245
77
|
this.requestValue(String(this.clamp(parsed)));
|
|
246
78
|
restoreValue(control, this.value);
|
|
247
79
|
};
|
|
248
|
-
/** Relayed, because `change` does not cross a shadow boundary on its own. */
|
|
249
80
|
this.handleChange = (event) => {
|
|
250
81
|
event.stopPropagation();
|
|
251
82
|
this.dispatchEvent(new Event('change', { bubbles: true, composed: false }));
|
|
252
83
|
};
|
|
253
|
-
/**
|
|
254
|
-
* The four keys a spinner answers, which `Input` does not bind.
|
|
255
|
-
*
|
|
256
|
-
* `Home` and `End` only where there is a bound to go to: without one they are
|
|
257
|
-
* the text field's own keys and moving the caret to the start of the number is
|
|
258
|
-
* what a reader pressing them meant.
|
|
259
|
-
*/
|
|
260
84
|
this.handleKeydown = (event) => {
|
|
261
85
|
if (event.key === 'ArrowUp') {
|
|
262
86
|
this.stepUp();
|
|
@@ -269,9 +93,6 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
|
|
|
269
93
|
}
|
|
270
94
|
else if (event.key === 'PageDown') {
|
|
271
95
|
this.stepDown(true);
|
|
272
|
-
/* Guarded the way `nudge` is. A read-only field still receives keydown,
|
|
273
|
-
so an unguarded Home on one with a `min` silently rewrites the value
|
|
274
|
-
that the arrows beside it correctly refuse to touch. */
|
|
275
96
|
}
|
|
276
97
|
else if (event.key === 'Home' &&
|
|
277
98
|
this.min !== undefined &&
|
|
@@ -288,11 +109,9 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
|
|
|
288
109
|
else {
|
|
289
110
|
return;
|
|
290
111
|
}
|
|
291
|
-
// Or the arrows scroll the page and Home jumps to the top of it.
|
|
292
112
|
event.preventDefault();
|
|
293
113
|
};
|
|
294
114
|
}
|
|
295
|
-
/** Focus is delegated to the field, so the host is never the focus target. */
|
|
296
115
|
static { this.shadowRootOptions = {
|
|
297
116
|
...HelixFormElement.shadowRootOptions,
|
|
298
117
|
delegatesFocus: true,
|
|
@@ -300,7 +119,6 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
|
|
|
300
119
|
get control() {
|
|
301
120
|
return this.renderRoot.querySelector('[part="control"]');
|
|
302
121
|
}
|
|
303
|
-
/** The value as a number, or `null` where the field holds nothing usable. */
|
|
304
122
|
get numeric() {
|
|
305
123
|
if (this.value.trim() === '') {
|
|
306
124
|
return null;
|
|
@@ -308,33 +126,15 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
|
|
|
308
126
|
const parsed = Number(this.value);
|
|
309
127
|
return Number.isFinite(parsed) ? parsed : null;
|
|
310
128
|
}
|
|
311
|
-
/**
|
|
312
|
-
* A value pulled into range and onto the step grid.
|
|
313
|
-
*
|
|
314
|
-
* The grid is anchored at `min` where there is one, so `min="1" step="2"`
|
|
315
|
-
* gives 1, 3, 5 rather than 0, 2, 4. Rounded to the step's own precision,
|
|
316
|
-
* because stepping 0.1 in binary floating point otherwise produces
|
|
317
|
-
* `0.30000000000000004` and puts it in the field.
|
|
318
|
-
*/
|
|
319
129
|
clamp(value) {
|
|
320
130
|
const step = Math.abs(this.step) || 1;
|
|
321
131
|
const anchor = this.min ?? 0;
|
|
322
132
|
const stepped = anchor + Math.round((value - anchor) / step) * step;
|
|
323
133
|
const floored = this.min === undefined ? stepped : Math.max(this.min, stepped);
|
|
324
134
|
const capped = this.max === undefined ? floored : Math.min(this.max, floored);
|
|
325
|
-
/* Decimal places from the step and the anchor together: a step of 0.5 from
|
|
326
|
-
an anchor of 0.25 lands on 0.75, which needs two rather than the one the
|
|
327
|
-
step alone would give. */
|
|
328
135
|
const places = Math.max(decimals(step), decimals(anchor));
|
|
329
136
|
return Number(capped.toFixed(places));
|
|
330
137
|
}
|
|
331
|
-
/**
|
|
332
|
-
* Moves by whole steps, from where the reader is.
|
|
333
|
-
*
|
|
334
|
-
* From `min` when the field is empty, so the first press of an arrow lands
|
|
335
|
-
* somewhere meaningful rather than on zero: an empty field with `min="1"`
|
|
336
|
-
* stepping to 0 would be out of range before the reader had typed anything.
|
|
337
|
-
*/
|
|
338
138
|
nudge(steps) {
|
|
339
139
|
if (this.isDisabled || this.readOnly) {
|
|
340
140
|
return;
|
|
@@ -348,37 +148,12 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
|
|
|
348
148
|
restoreValue(control, this.value);
|
|
349
149
|
}
|
|
350
150
|
}
|
|
351
|
-
/** One step up, or `PageUp`'s larger one. */
|
|
352
151
|
stepUp(large = false) {
|
|
353
152
|
this.nudge(large ? (this.largeStep ?? 10) : 1);
|
|
354
153
|
}
|
|
355
|
-
/**
|
|
356
|
-
* Steps the value down, by `largeStep` when `large` is set.
|
|
357
|
-
*
|
|
358
|
-
* The same path a press of the down arrow takes, so a consumer driving this
|
|
359
|
-
* from their own control gets the clamping and the announcement rather than
|
|
360
|
-
* having to repeat both.
|
|
361
|
-
*/
|
|
362
154
|
stepDown(large = false) {
|
|
363
155
|
this.nudge(large ? -(this.largeStep ?? 10) : -1);
|
|
364
156
|
}
|
|
365
|
-
/**
|
|
366
|
-
* The label is the surface, which is where the convention already is.
|
|
367
|
-
*
|
|
368
|
-
* Not a grip of its own at the leading edge, which is the obvious answer and
|
|
369
|
-
* is a second control in a field that already has three. Every tool that does
|
|
370
|
-
* this well drags the *name* of the value: the label is present on every
|
|
371
|
-
* field, costs no chrome, and a reader who wants to know what they are
|
|
372
|
-
* dragging is already looking at it.
|
|
373
|
-
*
|
|
374
|
-
* Overridden rather than handled on the host, because the handlers have to
|
|
375
|
-
* sit on the element that takes the capture. The markup is the mixin's, said
|
|
376
|
-
* again here, which is the duplication #76 exists to remove.
|
|
377
|
-
*
|
|
378
|
-
* A label with nothing in it is still nothing. `scrub` on a field whose label
|
|
379
|
-
* is empty leaves no surface, which is honest: there is nowhere to put the
|
|
380
|
-
* gesture and inventing somewhere is how the grip happened.
|
|
381
|
-
*/
|
|
382
157
|
renderLabel() {
|
|
383
158
|
if (!this.scrub) {
|
|
384
159
|
return super.renderLabel();
|
|
@@ -398,15 +173,6 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
|
|
|
398
173
|
><slot name="label">${this.label || hidden}</slot></label
|
|
399
174
|
>`;
|
|
400
175
|
}
|
|
401
|
-
/**
|
|
402
|
-
* Reports the value and a verdict on it.
|
|
403
|
-
*
|
|
404
|
-
* Computed here rather than copied off the field, which is the one place this
|
|
405
|
-
* differs from `Input`: the field is text, so the browser has no opinion about
|
|
406
|
-
* whether it is a number in range. The flags are the platform's own names, so
|
|
407
|
-
* a consumer reading `validity.rangeOverflow` finds what they expect and the
|
|
408
|
-
* message is what a native spinner would have said.
|
|
409
|
-
*/
|
|
410
176
|
sync() {
|
|
411
177
|
const control = this.control;
|
|
412
178
|
this.internals.setFormValue(this.value);
|
|
@@ -429,23 +195,16 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
|
|
|
429
195
|
else {
|
|
430
196
|
this.internals.setValidity({}, '', control);
|
|
431
197
|
}
|
|
432
|
-
// Straight after, because validity moved without any property moving.
|
|
433
198
|
this.refreshFeedback();
|
|
434
199
|
}
|
|
435
|
-
/** Puts the caret in the field, the way a label pointing at one would. */
|
|
436
200
|
focus(options) {
|
|
437
201
|
this.control?.focus(options);
|
|
438
202
|
}
|
|
439
|
-
/** Selects the number, so typing replaces it. */
|
|
440
203
|
select() {
|
|
441
204
|
this.control?.select();
|
|
442
205
|
}
|
|
443
206
|
willUpdate(changed) {
|
|
444
207
|
super.willUpdate(changed);
|
|
445
|
-
/* Never before the first update, which is the one the server does: there is
|
|
446
|
-
no render root to query there, and `querySelector` is not merely empty on
|
|
447
|
-
Lit's element shim, it is undefined. There is also no caret to keep before
|
|
448
|
-
anything has rendered. */
|
|
449
208
|
if (!this.hasUpdated) {
|
|
450
209
|
return;
|
|
451
210
|
}
|
|
@@ -479,13 +238,7 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
|
|
|
479
238
|
</svg>
|
|
480
239
|
</button>
|
|
481
240
|
<slot name="start"></slot>
|
|
482
|
-
${
|
|
483
|
-
/* `role="spinbutton"` with the range on it, which is what tells a
|
|
484
|
-
screen reader this is a number in a range rather than a line of
|
|
485
|
-
text. The three `aria-value*` attributes are what the native
|
|
486
|
-
`type=number` would have carried, and they have to be written here
|
|
487
|
-
because the field is text. */
|
|
488
|
-
''}
|
|
241
|
+
${''}
|
|
489
242
|
<input
|
|
490
243
|
part="control"
|
|
491
244
|
id="control"
|
|
@@ -524,13 +277,6 @@ let HelixHeadlessNumberInput = class HelixHeadlessNumberInput extends MixinContr
|
|
|
524
277
|
</div>
|
|
525
278
|
${this.renderHint()}${this.renderFeedback()}`;
|
|
526
279
|
}
|
|
527
|
-
/**
|
|
528
|
-
* The value as a number, or `NaN` while the control is empty.
|
|
529
|
-
*
|
|
530
|
-
* `NaN` and not zero, which is the trap the platform already avoids: `Number('')`
|
|
531
|
-
* is zero, so an empty control would report a number it does not hold and a
|
|
532
|
-
* form would submit it.
|
|
533
|
-
*/
|
|
534
280
|
get valueAsNumber() {
|
|
535
281
|
return asNumber(this.value);
|
|
536
282
|
}
|
|
@@ -572,14 +318,6 @@ HelixHeadlessNumberInput = __decorate([
|
|
|
572
318
|
customElement('helix-headless-number-input')
|
|
573
319
|
], HelixHeadlessNumberInput);
|
|
574
320
|
export { HelixHeadlessNumberInput };
|
|
575
|
-
/**
|
|
576
|
-
* How many decimal places a number is written with.
|
|
577
|
-
*
|
|
578
|
-
* Read off the string rather than computed, because the question is about how
|
|
579
|
-
* the consumer wrote the step and not about what the binary can represent.
|
|
580
|
-
* `1e-7` is handled separately: its string form has no dot at all, so counting
|
|
581
|
-
* after one would say zero and round every result to a whole number.
|
|
582
|
-
*/
|
|
583
321
|
const decimals = (value) => {
|
|
584
322
|
const text = String(value);
|
|
585
323
|
const exponent = /e-(\d+)$/.exec(text);
|
|
@@ -7,35 +7,9 @@ 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
|
-
* @summary A named run of options inside a listbox.
|
|
12
|
-
*
|
|
13
|
-
* @description
|
|
14
|
-
* A heading over a set, and nothing else: it holds no selection and no value,
|
|
15
|
-
* because grouping is presentation over a list whose owner is the listbox. Its
|
|
16
|
-
* options are still the listbox's options, which is what keeps the keyboard
|
|
17
|
-
* moving through a grouped list exactly as it moves through a flat one.
|
|
18
|
-
*
|
|
19
|
-
* `role="group"` named by the heading, so a screen reader announces which run
|
|
20
|
-
* an option is in without the heading becoming something to land on.
|
|
21
|
-
*
|
|
22
|
-
* @since 0.1.0
|
|
23
|
-
* @status experimental
|
|
24
|
-
* @aliases OptGroup, Section
|
|
25
|
-
* @documentation https://helix.fusion.dev/components/option-group
|
|
26
|
-
*
|
|
27
|
-
* @slot - The options in this run.
|
|
28
|
-
* @slot label - Richer than a string.
|
|
29
|
-
*
|
|
30
|
-
* @csspart items - The run itself.
|
|
31
|
-
* @csspart label - The heading.
|
|
32
|
-
*/
|
|
33
10
|
let HelixHeadlessOptionGroup = class HelixHeadlessOptionGroup extends HelixElement {
|
|
34
11
|
constructor() {
|
|
35
12
|
super(...arguments);
|
|
36
|
-
/**
|
|
37
|
-
* The heading over the run.
|
|
38
|
-
*/
|
|
39
13
|
this.label = '';
|
|
40
14
|
}
|
|
41
15
|
willUpdate(changed) {
|