@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,116 +7,26 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
7
7
|
import { customElement, formatNumbers, HelixElement } from '@fusion.dev/helix-core';
|
|
8
8
|
import { html } from 'lit';
|
|
9
9
|
import { property } from 'lit/decorators.js';
|
|
10
|
-
/**
|
|
11
|
-
* @summary Reads out which items of how many are on show.
|
|
12
|
-
*
|
|
13
|
-
* @description
|
|
14
|
-
* Its own element rather than a part of `Pagination`, so a consumer decides
|
|
15
|
-
* where it goes. Inside the run it could only ever be at one end of one row;
|
|
16
|
-
* beside it, it goes above, below, in a table's caption, or on the other side of
|
|
17
|
-
* the page, and the direction and order of the whole footer are the consumer's.
|
|
18
|
-
*
|
|
19
|
-
* **It repeats `page`, `page-size` and `total`, and the cost of that is skew
|
|
20
|
-
* rather than verbosity.** Those three come from the consumer's own query, so
|
|
21
|
-
* writing them on two elements is one variable used twice; what it risks is the
|
|
22
|
-
* two disagreeing, and a count reading `76-100 of 248` beside a run showing page
|
|
23
|
-
* 7 is the same failure this element's own clamping exists to prevent, arriving
|
|
24
|
-
* from the outside instead.
|
|
25
|
-
*
|
|
26
|
-
* **Nothing removes it at this level, and that was measured rather than
|
|
27
|
-
* assumed.** A coordinating root can only reach children the consumer laid out
|
|
28
|
-
* by writing attributes onto them, which a framework reports as a hydration
|
|
29
|
-
* error, or through context, which is dead on a server: `@lit/context`
|
|
30
|
-
* subscribes from `connectedCallback`, and Lit SSR does not call it, so every
|
|
31
|
-
* value is `undefined` in exactly the render this element exists to be part of.
|
|
32
|
-
* Confirmed against Lit SSR directly. The fork is real: children in the light
|
|
33
|
-
* DOM mean the consumer owns the layout and no root can feed them; children in a
|
|
34
|
-
* shadow root mean a root feeds them by ordinary binding and owns the layout.
|
|
35
|
-
*
|
|
36
|
-
* **It resolves one level up, at a composite, and the state is why.** `page`,
|
|
37
|
-
* `page-size` and `total` are facts about a *result*, not about a pagination, so
|
|
38
|
-
* the element that should hold them once is the one that holds the result: a
|
|
39
|
-
* data table composing a pagination inside itself, rather than a pagination root
|
|
40
|
-
* invented to hold three pagination elements together. That root would have been
|
|
41
|
-
* plumbing; a table's paging state is the thing itself. And because a composite
|
|
42
|
-
* renders its parts into its own shadow root, it lands on the side of the fork
|
|
43
|
-
* where binding works and the server renders, at no cost to anyone.
|
|
44
|
-
*
|
|
45
|
-
* Renders nothing without a `total` and a `page-size`. A wrong count is worse
|
|
46
|
-
* than none: it is the one number in a footer a reader cannot check against
|
|
47
|
-
* anything else on screen.
|
|
48
|
-
*
|
|
49
|
-
* @since 0.1.0
|
|
50
|
-
* @status experimental
|
|
51
|
-
* @aliases Result count, Range
|
|
52
|
-
* @documentation https://helix.fusion.dev/components/pagination
|
|
53
|
-
*/
|
|
54
10
|
let HelixHeadlessPaginationSummary = class HelixHeadlessPaginationSummary extends HelixElement {
|
|
55
11
|
constructor() {
|
|
56
12
|
super(...arguments);
|
|
57
|
-
/**
|
|
58
|
-
* The reading, with `{from}`, `{to}` and `{total}` filled in.
|
|
59
|
-
*
|
|
60
|
-
* A template rather than a prefix and a suffix, because word order is not
|
|
61
|
-
* universal: `format="{total} 件中 {from}-{to} 件"` is one attribute here and
|
|
62
|
-
* three concatenations anywhere else.
|
|
63
|
-
*
|
|
64
|
-
* **`format`, not `label`, and the difference is worth the rename.** `label`
|
|
65
|
-
* across this library means the accessible name of a thing that has one:
|
|
66
|
-
* `Pagination.label` names the landmark and `Pagination.Size.label` is the
|
|
67
|
-
* words beside the control. This is neither. It is the whole visible content
|
|
68
|
-
* of the element, so calling it `label` put one word on three jobs inside one
|
|
69
|
-
* family. The root's templates are `pageLabel`, `statusLabel` and `gapLabel`,
|
|
70
|
-
* which each label something; this labels nothing, it *is* the thing.
|
|
71
|
-
*/
|
|
72
13
|
this.format = '{from}-{to} of {total}';
|
|
73
|
-
/**
|
|
74
|
-
* Locale for the numbers.
|
|
75
|
-
*
|
|
76
|
-
* Absent uses the runtime's own.
|
|
77
|
-
*/
|
|
78
14
|
this.locale = '';
|
|
79
|
-
/**
|
|
80
|
-
* The page on show, one-based.
|
|
81
|
-
*/
|
|
82
15
|
this.page = 1;
|
|
83
|
-
/**
|
|
84
|
-
* How many items are on a page.
|
|
85
|
-
*/
|
|
86
16
|
this.pageSize = 0;
|
|
87
|
-
/**
|
|
88
|
-
* How many items there are across every page.
|
|
89
|
-
*/
|
|
90
17
|
this.total = 0;
|
|
91
18
|
}
|
|
92
|
-
/** Held across every component asking for the same locale. */
|
|
93
19
|
get numbers() {
|
|
94
20
|
return formatNumbers(this.locale);
|
|
95
21
|
}
|
|
96
22
|
render() {
|
|
97
|
-
/* An empty template rather than `nothing`, so the element generates a box
|
|
98
|
-
with no text in it: a footer laid out as a grid keeps its column, and a
|
|
99
|
-
count that appears when the total loads does not move the controls beside
|
|
100
|
-
it. */
|
|
101
23
|
if (this.total <= 0 || this.pageSize <= 0) {
|
|
102
24
|
return html ``;
|
|
103
25
|
}
|
|
104
26
|
const size = Math.max(1, Math.floor(this.pageSize));
|
|
105
|
-
/* **The page is clamped to the range, not just the window inside it.**
|
|
106
|
-
Only the end was, so a `page` past the last one produced a start past the
|
|
107
|
-
total and an end pinned to it: at page 99 of a 10-page result this read
|
|
108
|
-
"2,451-248 of 248", a range running backwards. That is the worst kind of
|
|
109
|
-
wrong here, because a count is the one number in a footer a reader cannot
|
|
110
|
-
check against anything else on screen.
|
|
111
|
-
|
|
112
|
-
A page past the end is not far-fetched either: it is a bookmarked query
|
|
113
|
-
after the result shrank, and the run beside this clamps it silently, so
|
|
114
|
-
the two would disagree with only this one visibly broken. */
|
|
115
27
|
const pages = Math.max(1, Math.ceil(this.total / size));
|
|
116
28
|
const page = Math.min(pages, Math.max(1, Math.floor(this.page) || 1));
|
|
117
29
|
const from = (page - 1) * size + 1;
|
|
118
|
-
/* And the end is clamped to the total, because the last page is rarely
|
|
119
|
-
full: 248 items at 25 a page end at 248, not at 250. */
|
|
120
30
|
const to = Math.min(this.total, from + size - 1);
|
|
121
31
|
return html `${this.format
|
|
122
32
|
.replaceAll('{from}', this.numbers(from))
|
|
@@ -7,141 +7,27 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
7
7
|
import { AriaForwardController, customElement, HelixCompleteEvent, HelixFormElement, MixinControllerValue, 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 short code, one character to a box.
|
|
12
|
-
*
|
|
13
|
-
* @description
|
|
14
|
-
* **One `<input>` per cell, rather than one input drawn as cells.** A single
|
|
15
|
-
* field autofills perfectly and needs a drawn caret nobody can style well;
|
|
16
|
-
* separate fields give every box a real focus ring, a real caret and a real tab
|
|
17
|
-
* stop for free. Autofill's gap is closed below rather than lived with.
|
|
18
|
-
*
|
|
19
|
-
* **Anything longer than one character is treated as a paste, wherever it
|
|
20
|
-
* lands.** That covers the obvious case and two that are not: a browser
|
|
21
|
-
* autofilling an SMS code drops the whole thing into whichever box is focused,
|
|
22
|
-
* and a password manager does the same. So a six-character arrival in cell three
|
|
23
|
-
* fills from cell three rather than being truncated to its first character,
|
|
24
|
-
* which is what a per-cell `maxlength` would have done silently.
|
|
25
|
-
*
|
|
26
|
-
* **The code is one value to a form.** The cells are inside a shadow root and
|
|
27
|
-
* submit nothing on their own; the host is form-associated and reports the
|
|
28
|
-
* joined string, which is also what carries validity and answers a reset.
|
|
29
|
-
*
|
|
30
|
-
* `autocomplete="one-time-code"` is on the first cell only. It is a hint about
|
|
31
|
-
* the field as a whole, and repeating it on six boxes offers a browser six
|
|
32
|
-
* places to put one code.
|
|
33
|
-
*
|
|
34
|
-
* @since 0.1.0
|
|
35
|
-
* @status stable
|
|
36
|
-
* @category forms
|
|
37
|
-
* @aliases PinField, OTP, One-time code, Verification code, InputPin
|
|
38
|
-
* @documentation https://helix.fusion.dev/components/pin-input
|
|
39
|
-
*
|
|
40
|
-
* @event {Event} change - Announced with a filled code, and when an edited one loses focus.
|
|
41
|
-
* @event {HelixBeforeChangeEvent} helix-before-change - Fired as the code changes, and cancelable.
|
|
42
|
-
* @event {HelixCompleteEvent} helix-complete - Fired when the last box is filled.
|
|
43
|
-
* @event {Event} input - Announced once the value has moved.
|
|
44
|
-
*
|
|
45
|
-
* @csspart cell - One box.
|
|
46
|
-
* @csspart feedback - What is wrong with it.
|
|
47
|
-
* @csspart field - The row of boxes.
|
|
48
|
-
* @csspart hint - Help under the boxes, while nothing is wrong.
|
|
49
|
-
* @csspart label - The question, over the boxes.
|
|
50
|
-
*/
|
|
51
10
|
let HelixHeadlessPinInput = class HelixHeadlessPinInput extends MixinControllerValue(MixinField(HelixFormElement)) {
|
|
52
11
|
constructor() {
|
|
53
12
|
super(...arguments);
|
|
54
|
-
/* A consumer's `aria-describedby` names ids in their own tree and the control
|
|
55
|
-
is in this shadow root, so the reference has to be carried down as an
|
|
56
|
-
element rather than copied as an id. See `AriaForwardController`: measured
|
|
57
|
-
in Chrome's own tree, the host held the description and the control the
|
|
58
|
-
reader is actually on was exposed without it. */
|
|
59
13
|
this.aria = new AriaForwardController(this, {
|
|
60
14
|
target: () => this.renderRoot?.querySelector('[part="cell"]') ?? null,
|
|
61
15
|
});
|
|
62
|
-
/** Typed or dragged, so every edit is announced. See `announcesInput`. */
|
|
63
16
|
this.announcesInput = true;
|
|
64
|
-
/**
|
|
65
|
-
* What the browser may autofill, reaching the first cell only.
|
|
66
|
-
*
|
|
67
|
-
* `one-time-code` is why this defaults to anything at all: it is the token
|
|
68
|
-
* that lets a phone offer the code out of a message. Later cells stay `off`
|
|
69
|
-
* for the reason in the class comment above, that six cells each offering
|
|
70
|
-
* the same code is one offer made six times.
|
|
71
|
-
*/
|
|
72
17
|
this.autocomplete = 'one-time-code';
|
|
73
|
-
/**
|
|
74
|
-
* Accessible name for one box, with `{index}` and `{length}` filled in.
|
|
75
|
-
*/
|
|
76
18
|
this.cellLabel = 'Character {index} of {length}';
|
|
77
|
-
/**
|
|
78
|
-
* How many characters the code has.
|
|
79
|
-
*/
|
|
80
19
|
this.length = 6;
|
|
81
|
-
/**
|
|
82
|
-
* Whether the characters are hidden, the way a password is.
|
|
83
|
-
*/
|
|
84
20
|
this.mask = false;
|
|
85
|
-
/**
|
|
86
|
-
* What an empty box shows, usually a single dot.
|
|
87
|
-
*
|
|
88
|
-
* The platform's own placeholder, on each cell, rather than a character
|
|
89
|
-
* drawn behind the field: it clears the moment anything is typed, it is
|
|
90
|
-
* already excluded from the accessible value, and it works the same under
|
|
91
|
-
* `mask`, where the cell is a password input and a hand-drawn glyph would
|
|
92
|
-
* have to know that.
|
|
93
|
-
*
|
|
94
|
-
* Empty by default, because a row of boxes already reads as a row of boxes.
|
|
95
|
-
*/
|
|
96
21
|
this.placeholder = '';
|
|
97
|
-
/**
|
|
98
|
-
* Whether the boxes refuse input while still submitting their value.
|
|
99
|
-
*/
|
|
100
22
|
this.readOnly = false;
|
|
101
|
-
/**
|
|
102
|
-
* What a box accepts, which decides the keyboard a phone offers.
|
|
103
|
-
*
|
|
104
|
-
* `numeric` and `alphanumeric` also refuse anything else on the way in,
|
|
105
|
-
* because a code that is digits is digits: letting a letter into a box a
|
|
106
|
-
* reader cannot submit is a mistake they have to find and undo themselves.
|
|
107
|
-
* `text` refuses nothing and only trims, so it is the mode for a code with a
|
|
108
|
-
* hyphen in it.
|
|
109
|
-
*
|
|
110
|
-
* **`alphanumeric` is ASCII letters and digits**, which is narrower than the
|
|
111
|
-
* word is in general and is the right narrowness here. A code of this kind
|
|
112
|
-
* comes off a keypad, an SMS or an authenticator, and none of them emit `é`;
|
|
113
|
-
* a filter that accepted it would let through a character the service on the
|
|
114
|
-
* other end is going to reject. A consumer whose codes really do carry
|
|
115
|
-
* marked letters wants `text`, which judges nothing.
|
|
116
|
-
*
|
|
117
|
-
* **Not reflected**: nothing selects on it, so reflecting only writes
|
|
118
|
-
* `type="numeric"` onto every `<helix-pin-input>` ever rendered, an attribute
|
|
119
|
-
* no consumer's markup had. The area rules carry the sentence "reflect only
|
|
120
|
-
* what CSS selects on". The real inputs take it as a binding, which is what
|
|
121
|
-
* makes the keyboard follow.
|
|
122
|
-
*/
|
|
123
23
|
this.type = 'numeric';
|
|
124
24
|
this.handleInput = (event) => {
|
|
125
|
-
// Stopped for the reason `Input.handleInput` gives: the inner control's own
|
|
126
|
-
// `input` is composed and would reach a consumer beside this element's.
|
|
127
25
|
event.stopPropagation();
|
|
128
26
|
const cell = event.target;
|
|
129
27
|
const at = this.cells.indexOf(cell);
|
|
130
28
|
if (at === -1) {
|
|
131
29
|
return;
|
|
132
30
|
}
|
|
133
|
-
/* **An emptied box is a deletion, and without this branch it is
|
|
134
|
-
unreachable.** A Backspace in a *filled* box is the browser's own: the
|
|
135
|
-
keydown handler leaves it alone, the character goes, and `input` arrives
|
|
136
|
-
carrying "". That falls through to `write`, which returns early on empty,
|
|
137
|
-
and then `paint` puts the character straight back. Only real keystrokes
|
|
138
|
-
reproduce it: dispatched events do not, which is why the specs pass over
|
|
139
|
-
it.
|
|
140
|
-
|
|
141
|
-
The rest collapse left, which is what the model already requires and what
|
|
142
|
-
a single text field does: "123456" with the 2 deleted is "13456", not a
|
|
143
|
-
hole. Focus stays where the reader was, so the box under the caret now
|
|
144
|
-
holds what followed. */
|
|
145
31
|
if (cell.value === '') {
|
|
146
32
|
const characters = this.characters;
|
|
147
33
|
characters[at] = '';
|
|
@@ -152,11 +38,7 @@ let HelixHeadlessPinInput = class HelixHeadlessPinInput extends MixinControllerV
|
|
|
152
38
|
});
|
|
153
39
|
return;
|
|
154
40
|
}
|
|
155
|
-
/* Longer than one character is a paste or an autofill, whichever box it
|
|
156
|
-
landed in. A per-cell `maxlength` would have silently truncated it to the
|
|
157
|
-
first character and lost the rest. */
|
|
158
41
|
this.write(at, cell.value);
|
|
159
|
-
// And put the box back, in case what arrived was refused or filtered.
|
|
160
42
|
this.updateComplete.then(() => this.paint());
|
|
161
43
|
};
|
|
162
44
|
this.handlePaste = (event) => {
|
|
@@ -165,18 +47,9 @@ let HelixHeadlessPinInput = class HelixHeadlessPinInput extends MixinControllerV
|
|
|
165
47
|
if (at === -1) {
|
|
166
48
|
return;
|
|
167
49
|
}
|
|
168
|
-
// Taken here rather than left to `input`, so the browser does not first put
|
|
169
|
-
// the whole string into one box and fire a change we then have to undo.
|
|
170
50
|
event.preventDefault();
|
|
171
51
|
this.write(at, event.clipboardData?.getData('text') ?? '');
|
|
172
52
|
};
|
|
173
|
-
/**
|
|
174
|
-
* The keys that move between boxes, and the one that clears backwards.
|
|
175
|
-
*
|
|
176
|
-
* Backspace in an empty box goes back and clears the one it lands in, which
|
|
177
|
-
* is what makes a code correctable without the reader hunting for the box
|
|
178
|
-
* they meant. In a full box it does what it always does and is left alone.
|
|
179
|
-
*/
|
|
180
53
|
this.handleKeydown = (event) => {
|
|
181
54
|
const cell = event.target;
|
|
182
55
|
const at = this.cells.indexOf(cell);
|
|
@@ -205,63 +78,12 @@ let HelixHeadlessPinInput = class HelixHeadlessPinInput extends MixinControllerV
|
|
|
205
78
|
}
|
|
206
79
|
event.preventDefault();
|
|
207
80
|
};
|
|
208
|
-
/**
|
|
209
|
-
* The code as it stood when focus arrived, so a visit that changed nothing
|
|
210
|
-
* says nothing.
|
|
211
|
-
*
|
|
212
|
-
* **Baselined on arrival, not at the last commit**, which is the platform's
|
|
213
|
-
* reading. What the two do to a real browser, measured in Chromium against a
|
|
214
|
-
* native `<input>` with real keystrokes:
|
|
215
|
-
*
|
|
216
|
-
* | | native | last commit | on arrival |
|
|
217
|
-
* |---|---|---|---|
|
|
218
|
-
* | types, leaves | `change` | `change` | `change` |
|
|
219
|
-
* | script assigns unfocused, visit with no edit | silent | **`change`** | silent |
|
|
220
|
-
* | script assigns unfocused, then edits | `change` | `change` | `change` |
|
|
221
|
-
* | types a character and deletes it | silent | silent | silent |
|
|
222
|
-
*
|
|
223
|
-
* One row differed and it is the one that matters: a consumer who assigns
|
|
224
|
-
* `value` and then has a reader tab through the row was told the reader had
|
|
225
|
-
* entered a code. `change` on a field means "the reader had a go at this",
|
|
226
|
-
* `MixinField` treats it as exactly that, and announcing a value the consumer
|
|
227
|
-
* set themselves is a false positive rather than the safe half of a trade.
|
|
228
|
-
*
|
|
229
|
-
* **The narrower divergence that is left** is a script assigning while the row
|
|
230
|
-
* is focused, where the platform stays silent because no user edit happened
|
|
231
|
-
* and this commits on the way out. Closing it needs a dirty flag set from real
|
|
232
|
-
* cell input rather than a baseline, which is more machinery than the case has
|
|
233
|
-
* earned: a consumer writing to a field the reader is inside is already in a
|
|
234
|
-
* fight with them.
|
|
235
|
-
*
|
|
236
|
-
* Seeded in `firstUpdated`, a code in the markup being committed by whoever
|
|
237
|
-
* wrote it, and re-taken on every arrival from outside the row.
|
|
238
|
-
*/
|
|
239
81
|
this.committed = '';
|
|
240
|
-
/** Selecting on focus, so typing replaces rather than appending. */
|
|
241
82
|
this.handleFocus = (event) => {
|
|
242
83
|
;
|
|
243
84
|
event.target.select();
|
|
244
85
|
};
|
|
245
|
-
/**
|
|
246
|
-
* Whether focus is currently somewhere inside the row.
|
|
247
|
-
*
|
|
248
|
-
* **The arrival is a transition, not a `relatedTarget` reading**, which is
|
|
249
|
-
* what the exit still does. `relatedTarget` does not work on the way in for
|
|
250
|
-
* two reasons and the second decides it. Every character moves focus to the
|
|
251
|
-
* next box, so an unguarded `focusin` re-baselines on each keystroke and the
|
|
252
|
-
* row can never report anything: measured, the three commit specs went silent
|
|
253
|
-
* at once. And `focusin` carries `relatedTarget` in a browser and not in
|
|
254
|
-
* happy-dom, so a guard written on it holds in production and not in the
|
|
255
|
-
* specs, which is the worse half. This flag needs neither: entering is
|
|
256
|
-
* `focusin` while it is `false`, and nothing else is.
|
|
257
|
-
*/
|
|
258
86
|
this.within = false;
|
|
259
|
-
/**
|
|
260
|
-
* Takes the baseline when focus arrives from outside the row.
|
|
261
|
-
*
|
|
262
|
-
* The value at arrival is what the platform compares against, and the table
|
|
263
|
-
* on `committed` is why.
|
|
264
|
-
*/
|
|
265
87
|
this.handleFocusIn = () => {
|
|
266
88
|
if (this.within) {
|
|
267
89
|
return;
|
|
@@ -269,21 +91,6 @@ let HelixHeadlessPinInput = class HelixHeadlessPinInput extends MixinControllerV
|
|
|
269
91
|
this.within = true;
|
|
270
92
|
this.committed = this.value;
|
|
271
93
|
};
|
|
272
|
-
/**
|
|
273
|
-
* Commits when focus leaves the row.
|
|
274
|
-
*
|
|
275
|
-
* `change` is one of the two events `MixinField` reads as "the reader has had
|
|
276
|
-
* a go at this", so without this a code that was never completed is never
|
|
277
|
-
* `touched` and its own `tooShort` message cannot show: two boxes of four
|
|
278
|
-
* filled and tabbed away from is answered with nothing at all. `Input` gets
|
|
279
|
-
* this for free by relaying its control's own `change`; here the cells each
|
|
280
|
-
* fire one, none of them means the code is entered, and neither does any
|
|
281
|
-
* single one of them mean the reader has left.
|
|
282
|
-
*
|
|
283
|
-
* **Focus leaving the row, and not a cell.** Every character moves focus to
|
|
284
|
-
* the next box, so a per-cell blur would commit on every keystroke, which is
|
|
285
|
-
* the fault this exists to fix pointed the other way.
|
|
286
|
-
*/
|
|
287
94
|
this.handleFocusOut = (event) => {
|
|
288
95
|
if (this.cells.includes(event.relatedTarget)) {
|
|
289
96
|
return;
|
|
@@ -292,23 +99,19 @@ let HelixHeadlessPinInput = class HelixHeadlessPinInput extends MixinControllerV
|
|
|
292
99
|
this.commit();
|
|
293
100
|
};
|
|
294
101
|
}
|
|
295
|
-
/** Focus is delegated, so the host is never the focus target. */
|
|
296
102
|
static { this.shadowRootOptions = {
|
|
297
103
|
...HelixFormElement.shadowRootOptions,
|
|
298
104
|
delegatesFocus: true,
|
|
299
105
|
}; }
|
|
300
|
-
/** How many boxes there are, clamped once so every reader agrees. */
|
|
301
106
|
get count() {
|
|
302
107
|
return Math.max(1, Math.floor(this.length) || 1);
|
|
303
108
|
}
|
|
304
109
|
get cells() {
|
|
305
110
|
return [...this.renderRoot.querySelectorAll('[part="cell"]')];
|
|
306
111
|
}
|
|
307
|
-
/** The code as characters, padded to the row so every box has one to show. */
|
|
308
112
|
get characters() {
|
|
309
113
|
return Array.from({ length: this.count }, (_, at) => this.value[at] ?? '');
|
|
310
114
|
}
|
|
311
|
-
/** Everything this box would accept, with the rest dropped. */
|
|
312
115
|
clean(text) {
|
|
313
116
|
const trimmed = text.trim();
|
|
314
117
|
if (this.type === 'numeric') {
|
|
@@ -319,29 +122,11 @@ let HelixHeadlessPinInput = class HelixHeadlessPinInput extends MixinControllerV
|
|
|
319
122
|
}
|
|
320
123
|
return trimmed;
|
|
321
124
|
}
|
|
322
|
-
/**
|
|
323
|
-
* Writes a run of characters from one box onward.
|
|
324
|
-
*
|
|
325
|
-
* The single path for typing, pasting and autofill alike, because they are
|
|
326
|
-
* the same act at different lengths and splitting them is how the three
|
|
327
|
-
* disagree about trimming, about the cursor, and about what happens at the
|
|
328
|
-
* end.
|
|
329
|
-
*/
|
|
330
125
|
write(from, text) {
|
|
331
126
|
const cleaned = this.clean(text);
|
|
332
127
|
if (cleaned === '') {
|
|
333
128
|
return;
|
|
334
129
|
}
|
|
335
|
-
/* **A code has no holes in it**, so a character cannot land past the end of
|
|
336
|
-
what is already there. Clamped rather than allowed, because the value is
|
|
337
|
-
one compact string and a gap in the middle of it does not exist: unclamped,
|
|
338
|
-
the boxes and the value disagree about where the reader typed.
|
|
339
|
-
|
|
340
|
-
It reads correctly in both directions. Pasting a whole code while the
|
|
341
|
-
caret happens to sit in box three fills from the start, which is what a
|
|
342
|
-
reader pasting a code means. Pasting two characters into box three of a
|
|
343
|
-
field with two already in it fills from box three, because that is where
|
|
344
|
-
the end was. */
|
|
345
130
|
const start = Math.min(from, this.value.length);
|
|
346
131
|
const characters = this.characters;
|
|
347
132
|
for (let at = 0; at < cleaned.length && start + at < this.count; at += 1) {
|
|
@@ -356,21 +141,10 @@ let HelixHeadlessPinInput = class HelixHeadlessPinInput extends MixinControllerV
|
|
|
356
141
|
this.cells[filled]?.select();
|
|
357
142
|
if (next.length === this.count) {
|
|
358
143
|
this.dispatchEvent(new HelixCompleteEvent({ value: next }));
|
|
359
|
-
/* And a native `change`, because a filled code is this control's
|
|
360
|
-
commit. There is no inner control to relay one from: six boxes each
|
|
361
|
-
fire their own, and none of them means the code is entered. */
|
|
362
144
|
this.commit();
|
|
363
145
|
}
|
|
364
146
|
});
|
|
365
147
|
}
|
|
366
|
-
/**
|
|
367
|
-
* Puts the value back into the boxes.
|
|
368
|
-
*
|
|
369
|
-
* The cells are uncontrolled between renders, because Lit will not rewrite an
|
|
370
|
-
* input's `.value` when the bound string has not changed, and a refused
|
|
371
|
-
* keystroke leaves the box holding a character the element rejected. This is
|
|
372
|
-
* the same put-back `Input` does with `restoreValue`, once per box.
|
|
373
|
-
*/
|
|
374
148
|
paint() {
|
|
375
149
|
const characters = this.characters;
|
|
376
150
|
for (const [at, cell] of this.cells.entries()) {
|
|
@@ -380,21 +154,6 @@ let HelixHeadlessPinInput = class HelixHeadlessPinInput extends MixinControllerV
|
|
|
380
154
|
}
|
|
381
155
|
}
|
|
382
156
|
}
|
|
383
|
-
/**
|
|
384
|
-
* Announces the code as entered, from whichever moment reached it first.
|
|
385
|
-
*
|
|
386
|
-
* **One writer, because the fact has two moments and only one meaning.** A
|
|
387
|
-
* filled code and a row being left are both "the reader is done with this".
|
|
388
|
-
* Dispatching from either site directly is how a moment gets added that fires
|
|
389
|
-
* without baselining.
|
|
390
|
-
*
|
|
391
|
-
* Silent when nothing moved since the baseline, which is what makes it safe
|
|
392
|
-
* to call from both: completing a code that is already the committed one, or
|
|
393
|
-
* leaving a row nobody edited, says nothing.
|
|
394
|
-
*
|
|
395
|
-
* `composed: false`, like the native event this stands in for. It is
|
|
396
|
-
* dispatched on the host, so a consumer hears it.
|
|
397
|
-
*/
|
|
398
157
|
commit() {
|
|
399
158
|
if (this.value === this.committed) {
|
|
400
159
|
return;
|
|
@@ -402,32 +161,6 @@ let HelixHeadlessPinInput = class HelixHeadlessPinInput extends MixinControllerV
|
|
|
402
161
|
this.committed = this.value;
|
|
403
162
|
this.dispatchEvent(new Event('change', { bubbles: true, composed: false }));
|
|
404
163
|
}
|
|
405
|
-
/**
|
|
406
|
-
* Reports the code and a verdict on it.
|
|
407
|
-
*
|
|
408
|
-
* **A half-entered code is invalid whether or not it is required, and that is
|
|
409
|
-
* the platform's answer rather than ours.** `required` guards `valueMissing`
|
|
410
|
-
* and nothing guards `tooShort`, so an optional code is refused for being half
|
|
411
|
-
* filled. Measured in Chromium against a native `<input minlength="4">` with
|
|
412
|
-
* real keystrokes, which is the feature this is the same shape as:
|
|
413
|
-
*
|
|
414
|
-
* | | optional | required |
|
|
415
|
-
* |---|---|---|
|
|
416
|
-
* | typed `12` | `tooShort`, invalid | `tooShort`, invalid |
|
|
417
|
-
* | left empty | valid | `valueMissing` |
|
|
418
|
-
* | typed `1234` | valid | valid |
|
|
419
|
-
*
|
|
420
|
-
* So `required` decides whether *nothing* is acceptable, and the length rule
|
|
421
|
-
* applies to anything that is not nothing. That is the right split for a code
|
|
422
|
-
* as well: two of four characters is not an optional code omitted, it is a
|
|
423
|
-
* code got wrong, and submitting it would send a fragment. The gate on the
|
|
424
|
-
* message is `touched` rather than `required`, which is what stops a reader
|
|
425
|
-
* being told off before they have had a go.
|
|
426
|
-
*
|
|
427
|
-
* `execCommand('insertText')` is not enough to measure this: `tooShort`
|
|
428
|
-
* applies only to a value the user edited, and the first run of the probe
|
|
429
|
-
* reported every case valid because nothing had set that flag.
|
|
430
|
-
*/
|
|
431
164
|
sync() {
|
|
432
165
|
this.internals.setFormValue(this.value);
|
|
433
166
|
const first = this.cells[0];
|
|
@@ -443,16 +176,13 @@ let HelixHeadlessPinInput = class HelixHeadlessPinInput extends MixinControllerV
|
|
|
443
176
|
else {
|
|
444
177
|
this.internals.setValidity({}, '', first);
|
|
445
178
|
}
|
|
446
|
-
// Straight after, because validity moved without any property moving.
|
|
447
179
|
this.refreshFeedback();
|
|
448
180
|
}
|
|
449
|
-
/** Puts the caret in the first empty box, which is where a reader resumes. */
|
|
450
181
|
focus(options) {
|
|
451
182
|
const cells = this.cells;
|
|
452
183
|
const at = Math.min(this.value.length, cells.length - 1);
|
|
453
184
|
cells[at]?.focus(options);
|
|
454
185
|
}
|
|
455
|
-
/** Empties every box and returns to the first. */
|
|
456
186
|
clear() {
|
|
457
187
|
this.requestValue('');
|
|
458
188
|
this.updateComplete.then(() => {
|
|
@@ -471,12 +201,6 @@ let HelixHeadlessPinInput = class HelixHeadlessPinInput extends MixinControllerV
|
|
|
471
201
|
}
|
|
472
202
|
render() {
|
|
473
203
|
const characters = this.characters;
|
|
474
|
-
/* `renderGroupLabel` and not `renderLabel`, which is what every other
|
|
475
|
-
grouped control here uses. `renderLabel` emits `for="control"` and no
|
|
476
|
-
element in this template carries that id, because a pin input is a row of
|
|
477
|
-
cells and not one control: the visible label pointed at nothing, so
|
|
478
|
-
pressing it focused nothing and the group had no accessible name at all.
|
|
479
|
-
The set names itself through `hostAria` instead. */
|
|
480
204
|
return html `${this.renderGroupLabel()}
|
|
481
205
|
<div
|
|
482
206
|
part="field"
|