@rak200/ui 0.2.6 → 0.2.8
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/README.md +10 -3
- package/dist/dialog.d.ts +71 -0
- package/dist/dialog.d.ts.map +1 -0
- package/dist/dialog.js +406 -0
- package/dist/dialog.js.map +1 -0
- package/dist/field.d.ts +4 -0
- package/dist/field.d.ts.map +1 -1
- package/dist/field.js +21 -1
- package/dist/field.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/input.d.ts +63 -0
- package/dist/input.d.ts.map +1 -0
- package/dist/input.js +159 -0
- package/dist/input.js.map +1 -0
- package/dist/tokens.d.ts +2 -2
- package/dist/tokens.d.ts.map +1 -1
- package/dist/tokens.js +52 -0
- package/dist/tokens.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -30,13 +30,20 @@ npm install @rak200/ui
|
|
|
30
30
|
|
|
31
31
|
<ui-field>
|
|
32
32
|
<label slot="label">Amount</label>
|
|
33
|
-
<input type="number"
|
|
33
|
+
<ui-input><input type="number" name="amount" /></ui-input>
|
|
34
34
|
<span slot="help">In BRL, two decimals.</span>
|
|
35
35
|
</ui-field>
|
|
36
36
|
|
|
37
37
|
<ui-button>Save</ui-button>
|
|
38
38
|
<ui-button variant="secondary">Cancel</ui-button>
|
|
39
39
|
<ui-button disabled>Unavailable</ui-button>
|
|
40
|
+
|
|
41
|
+
<ui-dialog>
|
|
42
|
+
<h2 slot="title">Delete account</h2>
|
|
43
|
+
<p>This cannot be undone.</p>
|
|
44
|
+
<ui-button slot="actions" variant="secondary">Cancel</ui-button>
|
|
45
|
+
<ui-button slot="actions">Delete</ui-button>
|
|
46
|
+
</ui-dialog>
|
|
40
47
|
```
|
|
41
48
|
|
|
42
49
|
Every visual decision is a CSS custom property, so a host restyles the kit without forking it:
|
|
@@ -64,8 +71,8 @@ components; `docs/` is what describes them.
|
|
|
64
71
|
|
|
65
72
|
## Status
|
|
66
73
|
|
|
67
|
-
**v0.**
|
|
68
|
-
the ecosystem's full quality bar rather than sketched: type-checked at the strictest available
|
|
74
|
+
**v0.** Five components — `<ui-button>`, `<ui-field>`, `<ui-dialog>`, `<ui-input>` and
|
|
75
|
+
`<ui-textarea>` — and the token layer under them, built to the ecosystem's full quality bar rather than sketched: type-checked at the strictest available
|
|
69
76
|
setting, formatted, tested in a real browser and **asserted against axe** for WCAG A/AA, 100%
|
|
70
77
|
coverage and **100% mutation score**, scanned, and every public symbol documented. The v0 surface in
|
|
71
78
|
RFC 0016 grows from here — see [ROADMAP.md](ROADMAP.md).
|
package/dist/dialog.d.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { LitElement, type TemplateResult } from 'lit';
|
|
2
|
+
/**
|
|
3
|
+
* A modal dialog.
|
|
4
|
+
*
|
|
5
|
+
* It delegates to a real `<dialog>` and opens it with `showModal()`, which is what makes
|
|
6
|
+
* the top layer, the inert background, the focus trap, <kbd>Esc</kbd> and — the half
|
|
7
|
+
* everyone forgets — **focus returning to whatever opened it** the platform's job rather
|
|
8
|
+
* than this file's. What is written here is the part the platform leaves out: the scroll
|
|
9
|
+
* lock, the accessible name across the shadow boundary, and an exit that can be seen.
|
|
10
|
+
*
|
|
11
|
+
* **This is where RFC 0016 expected Zag to arrive, and it does not.** Zag was adopted for
|
|
12
|
+
* dismissable layers and focus trapping, and its dialog machine implements both over a
|
|
13
|
+
* `<div>` — which means giving up the top layer and taking a JS focus trap in exchange for
|
|
14
|
+
* one the user agent enforces. *The platform owns what the platform is good at* is the
|
|
15
|
+
* older rule and the stronger one here. Zag arrives with the first component the platform
|
|
16
|
+
* gives nothing for; `ARCHITECTURE.md` carries that decision for a consumer, and
|
|
17
|
+
* `ROADMAP.md` names the component.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```html
|
|
21
|
+
* <ui-dialog>
|
|
22
|
+
* <h2 slot="title">Delete account</h2>
|
|
23
|
+
* <p>This cannot be undone.</p>
|
|
24
|
+
* <ui-button slot="actions" variant="secondary">Cancel</ui-button>
|
|
25
|
+
* <ui-button slot="actions">Delete</ui-button>
|
|
26
|
+
* </ui-dialog>
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export declare class UiDialog extends LitElement {
|
|
30
|
+
#private;
|
|
31
|
+
static readonly styles: import("lit").CSSResult;
|
|
32
|
+
static readonly properties: {
|
|
33
|
+
open: {
|
|
34
|
+
type: BooleanConstructor;
|
|
35
|
+
reflect: boolean;
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Whether the dialog is showing.
|
|
40
|
+
*
|
|
41
|
+
* **The single source of truth, and the only one.** {@link show} and {@link close} set
|
|
42
|
+
* it, <kbd>Esc</kbd> sets it, and the component reacts to it — so an attribute, a
|
|
43
|
+
* property and a method are three ways of saying one thing rather than three paths
|
|
44
|
+
* that can disagree. Reflected, so a host stylesheet can select on it.
|
|
45
|
+
*
|
|
46
|
+
* A plain field rather than the `accessor` keyword, for the reason `src/button.ts`
|
|
47
|
+
* states beside its own.
|
|
48
|
+
*/
|
|
49
|
+
open: boolean;
|
|
50
|
+
constructor();
|
|
51
|
+
connectedCallback(): void;
|
|
52
|
+
disconnectedCallback(): void;
|
|
53
|
+
firstUpdated(): void;
|
|
54
|
+
updated(): void;
|
|
55
|
+
render(): TemplateResult;
|
|
56
|
+
/**
|
|
57
|
+
* Opens the dialog.
|
|
58
|
+
*
|
|
59
|
+
* Sugar over `open = true` rather than a second way in — a method that opened by its
|
|
60
|
+
* own route would be a state the attribute does not describe.
|
|
61
|
+
*/
|
|
62
|
+
show(): void;
|
|
63
|
+
/** Closes the dialog, running the exit. Sugar over `open = false`, for the same reason. */
|
|
64
|
+
close(): void;
|
|
65
|
+
}
|
|
66
|
+
declare global {
|
|
67
|
+
interface HTMLElementTagNameMap {
|
|
68
|
+
'ui-dialog': UiDialog;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=dialog.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dialog.d.ts","sourceRoot":"","sources":["../src/dialog.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAa,KAAK,cAAc,EAAE,MAAM,KAAK,CAAC;AAoGjE;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,qBAAa,QAAS,SAAQ,UAAU;;IACpC,gBAAyB,MAAM,0BAwG7B;IAEF,gBAAyB,UAAU;;;;;MAEjC;IAEF;;;;;;;;;;OAUG;IACH,IAAI,UAAS;;IAyCJ,iBAAiB,IAAI,IAAI;IASzB,oBAAoB,IAAI,IAAI;IAU5B,YAAY,IAAI,IAAI;IAIpB,OAAO,IAAI,IAAI;IAiBf,MAAM,IAAI,cAAc;IAIjC;;;;;OAKG;IACH,IAAI,IAAI,IAAI;IAIZ,2FAA2F;IAC3F,KAAK,IAAI,IAAI;CA0FhB;AAQD,OAAO,CAAC,MAAM,CAAC;IACX,UAAU,qBAAqB;QAC3B,WAAW,EAAE,QAAQ,CAAC;KACzB;CACJ"}
|
package/dist/dialog.js
ADDED
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
import { LitElement, css, html } from 'lit';
|
|
2
|
+
import { reference } from './reference.js';
|
|
3
|
+
/** The class the exit is carried by, on the dialog for exactly as long as the exit runs. */
|
|
4
|
+
const closing = 'closing';
|
|
5
|
+
/** How many dialogs are holding the page still. */
|
|
6
|
+
let locks = 0;
|
|
7
|
+
/**
|
|
8
|
+
* The inline values the first hold displaced, given back by the last release.
|
|
9
|
+
*
|
|
10
|
+
* No initial value, deliberately: nothing reads it before {@link hold} writes it, and an
|
|
11
|
+
* initialiser here would be a value no test could ever observe.
|
|
12
|
+
*/
|
|
13
|
+
let displaced;
|
|
14
|
+
/**
|
|
15
|
+
* Holds the page still behind a modal, and gives it back when the last one leaves.
|
|
16
|
+
*
|
|
17
|
+
* `<dialog>` gives a modal the top layer, an inert background, a focus trap and focus
|
|
18
|
+
* restoration. It does not stop the page behind it scrolling, and that is the one piece of
|
|
19
|
+
* modality left to write.
|
|
20
|
+
*
|
|
21
|
+
* **The gutter is reserved rather than the scrollbar measured**, which is the difference
|
|
22
|
+
* between a rule and an arithmetic nothing can check. `overflow: hidden` takes the
|
|
23
|
+
* scrollbar away and the page jumps left by its width; the usual answer adds that width
|
|
24
|
+
* back as padding — and the browser this suite runs in has overlay scrollbars, so the
|
|
25
|
+
* width is `0`, every sign error in that arithmetic passes, and the check is theatre.
|
|
26
|
+
* `scrollbar-gutter: stable` asks the browser to keep the space it was already using,
|
|
27
|
+
* with no number to get wrong.
|
|
28
|
+
*
|
|
29
|
+
* Counted rather than flagged, because a dialog opening a dialog is ordinary — a confirm
|
|
30
|
+
* over a form — and the second one closing must not give the page back while the first is
|
|
31
|
+
* still up.
|
|
32
|
+
*/
|
|
33
|
+
function hold() {
|
|
34
|
+
locks += 1;
|
|
35
|
+
if (locks > 1) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const root = document.documentElement;
|
|
39
|
+
displaced = {
|
|
40
|
+
overflow: root.style.overflow,
|
|
41
|
+
gutter: root.style.getPropertyValue('scrollbar-gutter'),
|
|
42
|
+
};
|
|
43
|
+
// Only where there was a scrollbar to lose. `stable` on a page that never had one
|
|
44
|
+
// reserves space that was not there, which is the same layout shift in the other
|
|
45
|
+
// direction.
|
|
46
|
+
if (root.scrollHeight > root.clientHeight) {
|
|
47
|
+
root.style.setProperty('scrollbar-gutter', 'stable');
|
|
48
|
+
}
|
|
49
|
+
root.style.overflow = 'hidden';
|
|
50
|
+
}
|
|
51
|
+
/** Gives the page back, once the last dialog has closed. */
|
|
52
|
+
function release() {
|
|
53
|
+
locks -= 1;
|
|
54
|
+
if (locks > 0) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const root = document.documentElement;
|
|
58
|
+
// What the host had, not what this module would have chosen. A page that sets its own
|
|
59
|
+
// `overflow` or reserves its own gutter gets it back rather than being cleared.
|
|
60
|
+
root.style.overflow = displaced.overflow;
|
|
61
|
+
root.style.setProperty('scrollbar-gutter', displaced.gutter);
|
|
62
|
+
}
|
|
63
|
+
/** One of the dialog's slots — named, or the default one when the name is empty. */
|
|
64
|
+
function slot(name = '') {
|
|
65
|
+
return Object.assign(document.createElement('slot'), { name });
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The shadow content, which is static all the way down: a `<dialog>` and three slots.
|
|
69
|
+
*
|
|
70
|
+
* Built here rather than queried out of the render root, and the reason is the floors
|
|
71
|
+
* this repository holds. `renderRoot.querySelector('dialog')` is typed `| null`, and that
|
|
72
|
+
* null is unreachable in a component that renders the element unconditionally — an
|
|
73
|
+
* unreachable branch is one no test can cover and no mutant can be killed on, against a
|
|
74
|
+
* coverage floor of 100 and a mutation floor of 100. The honest way to satisfy both is to
|
|
75
|
+
* not create the branch.
|
|
76
|
+
*/
|
|
77
|
+
function build() {
|
|
78
|
+
const dialog = document.createElement('dialog');
|
|
79
|
+
dialog.setAttribute('part', 'dialog');
|
|
80
|
+
dialog.append(slot('title'), slot(), slot('actions'));
|
|
81
|
+
return dialog;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* A modal dialog.
|
|
85
|
+
*
|
|
86
|
+
* It delegates to a real `<dialog>` and opens it with `showModal()`, which is what makes
|
|
87
|
+
* the top layer, the inert background, the focus trap, <kbd>Esc</kbd> and — the half
|
|
88
|
+
* everyone forgets — **focus returning to whatever opened it** the platform's job rather
|
|
89
|
+
* than this file's. What is written here is the part the platform leaves out: the scroll
|
|
90
|
+
* lock, the accessible name across the shadow boundary, and an exit that can be seen.
|
|
91
|
+
*
|
|
92
|
+
* **This is where RFC 0016 expected Zag to arrive, and it does not.** Zag was adopted for
|
|
93
|
+
* dismissable layers and focus trapping, and its dialog machine implements both over a
|
|
94
|
+
* `<div>` — which means giving up the top layer and taking a JS focus trap in exchange for
|
|
95
|
+
* one the user agent enforces. *The platform owns what the platform is good at* is the
|
|
96
|
+
* older rule and the stronger one here. Zag arrives with the first component the platform
|
|
97
|
+
* gives nothing for; `ARCHITECTURE.md` carries that decision for a consumer, and
|
|
98
|
+
* `ROADMAP.md` names the component.
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* ```html
|
|
102
|
+
* <ui-dialog>
|
|
103
|
+
* <h2 slot="title">Delete account</h2>
|
|
104
|
+
* <p>This cannot be undone.</p>
|
|
105
|
+
* <ui-button slot="actions" variant="secondary">Cancel</ui-button>
|
|
106
|
+
* <ui-button slot="actions">Delete</ui-button>
|
|
107
|
+
* </ui-dialog>
|
|
108
|
+
* ```
|
|
109
|
+
*/
|
|
110
|
+
export class UiDialog extends LitElement {
|
|
111
|
+
static { this.styles = css `
|
|
112
|
+
/* The dialog is promoted to the top layer, so the host must take no space of its
|
|
113
|
+
own: a <ui-dialog> laid out in the flow would leave a gap on every page where
|
|
114
|
+
the dialog is closed. */
|
|
115
|
+
:host {
|
|
116
|
+
display: contents;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/* display is named under [open] and nowhere else, and that is not a style
|
|
120
|
+
choice. The user agent closes a dialog with display: none; a rule here that
|
|
121
|
+
names display unconditionally outranks it, and the closed dialog simply stays
|
|
122
|
+
on the page. The attribute is still there through the exit below, which is what
|
|
123
|
+
lets the transition run at all. */
|
|
124
|
+
dialog[open] {
|
|
125
|
+
display: flex;
|
|
126
|
+
flex-direction: column;
|
|
127
|
+
gap: calc(${reference('--ui-space')} * 2);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
dialog {
|
|
131
|
+
/* The user agent's own 2px border and white background are decisions this
|
|
132
|
+
component owns. Nothing is raised underneath: elevation arrives with the
|
|
133
|
+
component that needs to read as lifted, and a panel over a scrim is already
|
|
134
|
+
separated from the page by the scrim. */
|
|
135
|
+
border: none;
|
|
136
|
+
border-radius: ${reference('--ui-radius')};
|
|
137
|
+
padding: calc(${reference('--ui-space')} * 3);
|
|
138
|
+
background: ${reference('--ui-color-surface')};
|
|
139
|
+
color: ${reference('--ui-color-text')};
|
|
140
|
+
font-family: ${reference('--ui-font')};
|
|
141
|
+
|
|
142
|
+
/* A cap is a layout decision rather than a visual one, so it is not a token
|
|
143
|
+
and opens no category. A host that wants another size reaches
|
|
144
|
+
::part(dialog), which is the surface for what the token set does not name.
|
|
145
|
+
|
|
146
|
+
border-box, and the mutation floor is what found this: a dialog is
|
|
147
|
+
content-box by default, so the cap applied to the content alone and the
|
|
148
|
+
padding was added on top of it — the panel then ran wider than the cap by
|
|
149
|
+
six spacing steps, and the clearance the calc promises was not the
|
|
150
|
+
clearance it left. */
|
|
151
|
+
box-sizing: border-box;
|
|
152
|
+
max-inline-size: min(32rem, calc(100% - ${reference('--ui-space')} * 8));
|
|
153
|
+
|
|
154
|
+
opacity: 1;
|
|
155
|
+
transition: opacity ${reference('--ui-duration-state')}
|
|
156
|
+
${reference('--ui-easing-enter')};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/* Where the entrance comes *from*. Without it there is nothing to transition out
|
|
160
|
+
of: the dialog goes from display: none to laid out in a single frame, and a
|
|
161
|
+
transition needs a before-change style that an unrendered element does not
|
|
162
|
+
have. */
|
|
163
|
+
@starting-style {
|
|
164
|
+
dialog[open] {
|
|
165
|
+
opacity: 0;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
dialog::backdrop {
|
|
170
|
+
background: ${reference('--ui-color-scrim')};
|
|
171
|
+
opacity: 1;
|
|
172
|
+
transition: opacity ${reference('--ui-duration-state')}
|
|
173
|
+
${reference('--ui-easing-enter')};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
@starting-style {
|
|
177
|
+
dialog[open]::backdrop {
|
|
178
|
+
opacity: 0;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/* The exit, driven from the class rather than from [open] going away: the
|
|
183
|
+
platform removes a closed dialog in the same frame it is closed, so an exit
|
|
184
|
+
waiting on the attribute would never be seen. Both halves are declared together
|
|
185
|
+
because moving together is what lets #exit await one of them. */
|
|
186
|
+
dialog.closing,
|
|
187
|
+
dialog.closing::backdrop {
|
|
188
|
+
opacity: 0;
|
|
189
|
+
transition-timing-function: ${reference('--ui-easing-exit')};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/* showModal() focuses the dialog itself when nothing inside it can take focus,
|
|
193
|
+
and a focused thing with no ring is the most common way a component stops being
|
|
194
|
+
usable by keyboard. */
|
|
195
|
+
dialog:focus-visible {
|
|
196
|
+
outline: 2px solid ${reference('--ui-color-focus')};
|
|
197
|
+
outline-offset: 2px;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/* A heading's own margin would add to the padding and to the gap above it. This
|
|
201
|
+
resets the user agent's default and yields to a decision: for slotted content
|
|
202
|
+
the outer tree wins, so a host's own rule for the element still beats this. */
|
|
203
|
+
slot[name='title']::slotted(*) {
|
|
204
|
+
margin: 0;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/* A slot is display: contents until something says otherwise, which is what
|
|
208
|
+
makes this the line that turns three buttons into a row. */
|
|
209
|
+
slot[name='actions'] {
|
|
210
|
+
display: flex;
|
|
211
|
+
flex-wrap: wrap;
|
|
212
|
+
justify-content: flex-end;
|
|
213
|
+
gap: ${reference('--ui-space')};
|
|
214
|
+
}
|
|
215
|
+
`; }
|
|
216
|
+
static { this.properties = {
|
|
217
|
+
open: { type: Boolean, reflect: true },
|
|
218
|
+
}; }
|
|
219
|
+
/** The real `<dialog>` this component is a shell around. */
|
|
220
|
+
#dialog;
|
|
221
|
+
/**
|
|
222
|
+
* Watches the light DOM for a title that changed without the element changing.
|
|
223
|
+
*
|
|
224
|
+
* Built here rather than on connection so it is never absent: a disconnect that has to
|
|
225
|
+
* ask whether the observer exists is a branch no test can reach.
|
|
226
|
+
*/
|
|
227
|
+
#observer;
|
|
228
|
+
constructor() {
|
|
229
|
+
super();
|
|
230
|
+
/**
|
|
231
|
+
* Whether the dialog is showing.
|
|
232
|
+
*
|
|
233
|
+
* **The single source of truth, and the only one.** {@link show} and {@link close} set
|
|
234
|
+
* it, <kbd>Esc</kbd> sets it, and the component reacts to it — so an attribute, a
|
|
235
|
+
* property and a method are three ways of saying one thing rather than three paths
|
|
236
|
+
* that can disagree. Reflected, so a host stylesheet can select on it.
|
|
237
|
+
*
|
|
238
|
+
* A plain field rather than the `accessor` keyword, for the reason `src/button.ts`
|
|
239
|
+
* states beside its own.
|
|
240
|
+
*/
|
|
241
|
+
this.open = false;
|
|
242
|
+
/** The real `<dialog>` this component is a shell around. */
|
|
243
|
+
this.#dialog = build();
|
|
244
|
+
/**
|
|
245
|
+
* Watches the light DOM for a title that changed without the element changing.
|
|
246
|
+
*
|
|
247
|
+
* Built here rather than on connection so it is never absent: a disconnect that has to
|
|
248
|
+
* ask whether the observer exists is a branch no test can reach.
|
|
249
|
+
*/
|
|
250
|
+
this.#observer = new MutationObserver(() => {
|
|
251
|
+
this.#label();
|
|
252
|
+
});
|
|
253
|
+
this.#dialog.addEventListener('cancel', (event) => {
|
|
254
|
+
// Esc reaches a modal as `cancel`, and letting it through closes in the same
|
|
255
|
+
// frame — the exit this component just gained would never be seen. Prevented
|
|
256
|
+
// and routed back through `open`, so however a close was asked for it leaves
|
|
257
|
+
// by the one door.
|
|
258
|
+
event.preventDefault();
|
|
259
|
+
this.open = false;
|
|
260
|
+
});
|
|
261
|
+
this.#dialog.addEventListener('close', () => {
|
|
262
|
+
// The announcement only. The platform fires `close` as a *queued task* rather
|
|
263
|
+
// than synchronously, so this runs a turn after the dialog actually closed —
|
|
264
|
+
// which is fine for telling a host, and was wrong for giving the page back:
|
|
265
|
+
// the lock outlived the dialog by a task, and a second dialog opening inside
|
|
266
|
+
// that window counted against a page nobody was holding any more. Measured on
|
|
267
|
+
// Node 22 in CI and not on Node 24, which is what a timing dependency looks
|
|
268
|
+
// like from the outside. {@link UiDialog.#shut} owns the release instead, at
|
|
269
|
+
// both of the synchronous points where this element closes the dialog.
|
|
270
|
+
this.open = false;
|
|
271
|
+
this.dispatchEvent(new Event('ui-close', { bubbles: true, composed: true }));
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
connectedCallback() {
|
|
275
|
+
super.connectedCallback();
|
|
276
|
+
// The title may be replaced, or its text rewritten in place without any element
|
|
277
|
+
// changing, which is what a host's re-render looks like from here — so
|
|
278
|
+
// characterData matters as much as childList, and both can happen below a wrapper.
|
|
279
|
+
this.#observer.observe(this, { childList: true, characterData: true, subtree: true });
|
|
280
|
+
}
|
|
281
|
+
disconnectedCallback() {
|
|
282
|
+
this.#observer.disconnect();
|
|
283
|
+
// Removing an open dialog takes it out of the top layer and announces nothing, so
|
|
284
|
+
// the page would stay held by a dialog that is no longer anywhere.
|
|
285
|
+
this.#shut();
|
|
286
|
+
super.disconnectedCallback();
|
|
287
|
+
}
|
|
288
|
+
firstUpdated() {
|
|
289
|
+
this.#label();
|
|
290
|
+
}
|
|
291
|
+
updated() {
|
|
292
|
+
// Compared against the platform's own state rather than against Lit's changed
|
|
293
|
+
// properties, and it has to be: on the first update Lit reports every initialised
|
|
294
|
+
// property as changed, so a dialog that has never been open would run the closing
|
|
295
|
+
// path and announce a close that never happened. It is also what makes a re-render
|
|
296
|
+
// for any other reason a no-op instead of a second `showModal()`, which throws.
|
|
297
|
+
if (this.open === this.#dialog.open) {
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
if (this.open) {
|
|
301
|
+
this.#enter();
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
void this.#exit();
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
render() {
|
|
308
|
+
return html `${this.#dialog}`;
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Opens the dialog.
|
|
312
|
+
*
|
|
313
|
+
* Sugar over `open = true` rather than a second way in — a method that opened by its
|
|
314
|
+
* own route would be a state the attribute does not describe.
|
|
315
|
+
*/
|
|
316
|
+
show() {
|
|
317
|
+
this.open = true;
|
|
318
|
+
}
|
|
319
|
+
/** Closes the dialog, running the exit. Sugar over `open = false`, for the same reason. */
|
|
320
|
+
close() {
|
|
321
|
+
this.open = false;
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Puts the dialog in the top layer.
|
|
325
|
+
*
|
|
326
|
+
* `showModal()` rather than the `open` attribute, and the distance between the two is
|
|
327
|
+
* the whole component: the attribute opens a dialog *in the flow*, with no top layer,
|
|
328
|
+
* no inert background, no focus trap and no focus restoration. It is the one way to
|
|
329
|
+
* get something that looks modal and traps nobody — which is why this host's `open` is
|
|
330
|
+
* never bound onto the inner element's.
|
|
331
|
+
*/
|
|
332
|
+
#enter() {
|
|
333
|
+
this.#dialog.showModal();
|
|
334
|
+
hold();
|
|
335
|
+
}
|
|
336
|
+
/** Runs the exit, and closes once it has finished. */
|
|
337
|
+
async #exit() {
|
|
338
|
+
const dialog = this.#dialog;
|
|
339
|
+
dialog.classList.add(closing);
|
|
340
|
+
// `finished` rejects when an animation is cancelled, which is exactly what
|
|
341
|
+
// reopening mid-exit does — so what this waits for is settlement rather than
|
|
342
|
+
// fulfilment. `getAnimations` flushes the pending style change before it answers,
|
|
343
|
+
// so the transition the line above started is already in the list.
|
|
344
|
+
//
|
|
345
|
+
// The backdrop is not in that list: a pseudo-element needs `subtree: true`,
|
|
346
|
+
// measured. It does not need to be — the panel and the scrim move the same
|
|
347
|
+
// property over the same duration token, so they start together and end together,
|
|
348
|
+
// and awaiting one is awaiting both. `tests/dialog.test.ts` asserts that shared
|
|
349
|
+
// duration rather than leaving it to this paragraph.
|
|
350
|
+
await Promise.allSettled(dialog.getAnimations().map((animation) => animation.finished));
|
|
351
|
+
dialog.classList.remove(closing);
|
|
352
|
+
// Reopened while the exit was running. The dialog never left the top layer, so
|
|
353
|
+
// there is nothing to close, and taking the class back off is the whole of the
|
|
354
|
+
// way back.
|
|
355
|
+
if (this.open) {
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
this.#shut();
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Closes the dialog and gives the page back, in that order and in one place.
|
|
362
|
+
*
|
|
363
|
+
* Both ways out run through here — the exit above and the disconnect below — so the
|
|
364
|
+
* release is paired with the close rather than with the event the close will later
|
|
365
|
+
* fire. The guard is the platform's own state rather than a flag this element keeps:
|
|
366
|
+
* a dialog that is not open is holding nothing, so calling this twice gives the page
|
|
367
|
+
* back once.
|
|
368
|
+
*/
|
|
369
|
+
#shut() {
|
|
370
|
+
if (!this.#dialog.open) {
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
this.#dialog.close();
|
|
374
|
+
release();
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Names the dialog after its own title.
|
|
378
|
+
*
|
|
379
|
+
* **A copy of the text rather than a reference to the element, and that is forced.**
|
|
380
|
+
* APG names a modal with `aria-labelledby` pointing at its heading, and an IDREF does
|
|
381
|
+
* not cross a shadow boundary — so a `<dialog>` in this shadow root cannot point at a
|
|
382
|
+
* heading in the host's tree. `ui-field` answers the same constraint by leaving every
|
|
383
|
+
* associated element in the light DOM; a dialog cannot, because the element that has
|
|
384
|
+
* to reach the top layer is the one this component renders. What does cross is a
|
|
385
|
+
* string, so the name is copied and the observer above keeps the copy honest.
|
|
386
|
+
*/
|
|
387
|
+
#label() {
|
|
388
|
+
const title = this.querySelector(':scope > [slot="title"]');
|
|
389
|
+
const label = title?.textContent.trim() ?? '';
|
|
390
|
+
// A dialog with no title has no accessible name, and a blank `aria-label` would be
|
|
391
|
+
// worse than none: a screen reader announces "dialog" either way, and an attribute
|
|
392
|
+
// that is present and empty is the shape that makes an audit read as handled.
|
|
393
|
+
if (label === '') {
|
|
394
|
+
this.#dialog.removeAttribute('aria-label');
|
|
395
|
+
}
|
|
396
|
+
else {
|
|
397
|
+
this.#dialog.setAttribute('aria-label', label);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
// Stryker disable next-line StringLiteral: the registration runs once, at import, inside
|
|
402
|
+
// the warm process Stryker switches mutants in — so by the time a mutant on this line is
|
|
403
|
+
// active the element is already defined under the original name, and no test can observe
|
|
404
|
+
// the change. Outside the runner's reach, not an equivalent mutant.
|
|
405
|
+
customElements.define('ui-dialog', UiDialog);
|
|
406
|
+
//# sourceMappingURL=dialog.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dialog.js","sourceRoot":"","sources":["../src/dialog.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,GAAG,EAAE,IAAI,EAAuB,MAAM,KAAK,CAAC;AACjE,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAE3C,4FAA4F;AAC5F,MAAM,OAAO,GAAG,SAAS,CAAC;AAE1B,mDAAmD;AACnD,IAAI,KAAK,GAAG,CAAC,CAAC;AAEd;;;;;GAKG;AACH,IAAI,SAA+C,CAAC;AAEpD;;;;;;;;;;;;;;;;;;GAkBG;AACH,SAAS,IAAI;IACT,KAAK,IAAI,CAAC,CAAC;IAEX,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;QACZ,OAAO;IACX,CAAC;IAED,MAAM,IAAI,GAAG,QAAQ,CAAC,eAAe,CAAC;IAEtC,SAAS,GAAG;QACR,QAAQ,EAAE,IAAI,CAAC,KAAK,CAAC,QAAQ;QAC7B,MAAM,EAAE,IAAI,CAAC,KAAK,CAAC,gBAAgB,CAAC,kBAAkB,CAAC;KAC1D,CAAC;IAEF,kFAAkF;IAClF,iFAAiF;IACjF,aAAa;IACb,IAAI,IAAI,CAAC,YAAY,GAAG,IAAI,CAAC,YAAY,EAAE,CAAC;QACxC,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,kBAAkB,EAAE,QAAQ,CAAC,CAAC;IACzD,CAAC;IAED,IAAI,CAAC,KAAK,CAAC,QAAQ,GAAG,QAAQ,CAAC;AACnC,CAAC;AAED,4DAA4D;AAC5D,SAAS,OAAO;IACZ,KAAK,IAAI,CAAC,CAAC;IAEX,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;QACZ,OAAO;IACX,CAAC;IAED,MAAM,IAAI,GAAG,QAAQ,CAAC,eAAe,CAAC;IAEtC,sFAAsF;IACtF,gFAAgF;IAChF,IAAI,CAAC,KAAK,CAAC,QAAQ,GAAG,SAAS,CAAC,QAAQ,CAAC;IACzC,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,kBAAkB,EAAE,SAAS,CAAC,MAAM,CAAC,CAAC;AACjE,CAAC;AAED,oFAAoF;AACpF,SAAS,IAAI,CAAC,IAAI,GAAG,EAAE;IACnB,OAAO,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,aAAa,CAAC,MAAM,CAAC,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC;AACnE,CAAC;AAED;;;;;;;;;GASG;AACH,SAAS,KAAK;IACV,MAAM,MAAM,GAAG,QAAQ,CAAC,aAAa,CAAC,QAAQ,CAAC,CAAC;IAEhD,MAAM,CAAC,YAAY,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IACtC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC;IAEtD,OAAO,MAAM,CAAC;AAClB,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,OAAO,QAAS,SAAQ,UAAU;aACX,WAAM,GAAG,GAAG,CAAA;;;;;;;;;;;;;;;;wBAgBjB,SAAS,CAAC,YAAY,CAAC;;;;;;;;;6BASlB,SAAS,CAAC,aAAa,CAAC;4BACzB,SAAS,CAAC,YAAY,CAAC;0BACzB,SAAS,CAAC,oBAAoB,CAAC;qBACpC,SAAS,CAAC,iBAAiB,CAAC;2BACtB,SAAS,CAAC,WAAW,CAAC;;;;;;;;;;;;sDAYK,SAAS,CAAC,YAAY,CAAC;;;kCAG3C,SAAS,CAAC,qBAAqB,CAAC;kBAChD,SAAS,CAAC,mBAAmB,CAAC;;;;;;;;;;;;;;0BActB,SAAS,CAAC,kBAAkB,CAAC;;kCAErB,SAAS,CAAC,qBAAqB,CAAC;kBAChD,SAAS,CAAC,mBAAmB,CAAC;;;;;;;;;;;;;;;;0CAgBN,SAAS,CAAC,kBAAkB,CAAC;;;;;;;iCAOtC,SAAS,CAAC,kBAAkB,CAAC;;;;;;;;;;;;;;;;;mBAiB3C,SAAS,CAAC,YAAY,CAAC;;KAErC,AAxG8B,CAwG7B;aAEuB,eAAU,GAAG;QAClC,IAAI,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE;KACzC,AAFkC,CAEjC;IAeF,4DAA4D;IACnD,OAAO,CAAW;IAE3B;;;;;OAKG;IACM,SAAS,CAEf;IAEH;QACI,KAAK,EAAE,CAAC;QA3BZ;;;;;;;;;;WAUG;QACH,SAAI,GAAG,KAAK,CAAC;QAEb,4DAA4D;QACnD,YAAO,GAAG,KAAK,EAAE,CAAC;QAE3B;;;;;WAKG;QACM,cAAS,GAAG,IAAI,gBAAgB,CAAC,GAAG,EAAE;YAC3C,IAAI,CAAC,MAAM,EAAE,CAAC;QAClB,CAAC,CAAC,CAAC;QAKC,IAAI,CAAC,OAAO,CAAC,gBAAgB,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,EAAE;YAC9C,6EAA6E;YAC7E,6EAA6E;YAC7E,6EAA6E;YAC7E,mBAAmB;YACnB,KAAK,CAAC,cAAc,EAAE,CAAC;YACvB,IAAI,CAAC,IAAI,GAAG,KAAK,CAAC;QACtB,CAAC,CAAC,CAAC;QAEH,IAAI,CAAC,OAAO,CAAC,gBAAgB,CAAC,OAAO,EAAE,GAAG,EAAE;YACxC,8EAA8E;YAC9E,6EAA6E;YAC7E,4EAA4E;YAC5E,6EAA6E;YAC7E,8EAA8E;YAC9E,4EAA4E;YAC5E,6EAA6E;YAC7E,uEAAuE;YACvE,IAAI,CAAC,IAAI,GAAG,KAAK,CAAC;YAClB,IAAI,CAAC,aAAa,CAAC,IAAI,KAAK,CAAC,UAAU,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;QACjF,CAAC,CAAC,CAAC;IACP,CAAC;IAEQ,iBAAiB;QACtB,KAAK,CAAC,iBAAiB,EAAE,CAAC;QAE1B,gFAAgF;QAChF,uEAAuE;QACvE,mFAAmF;QACnF,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;IAC1F,CAAC;IAEQ,oBAAoB;QACzB,IAAI,CAAC,SAAS,CAAC,UAAU,EAAE,CAAC;QAE5B,kFAAkF;QAClF,mEAAmE;QACnE,IAAI,CAAC,KAAK,EAAE,CAAC;QAEb,KAAK,CAAC,oBAAoB,EAAE,CAAC;IACjC,CAAC;IAEQ,YAAY;QACjB,IAAI,CAAC,MAAM,EAAE,CAAC;IAClB,CAAC;IAEQ,OAAO;QACZ,8EAA8E;QAC9E,kFAAkF;QAClF,kFAAkF;QAClF,mFAAmF;QACnF,gFAAgF;QAChF,IAAI,IAAI,CAAC,IAAI,KAAK,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC;YAClC,OAAO;QACX,CAAC;QAED,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;YACZ,IAAI,CAAC,MAAM,EAAE,CAAC;QAClB,CAAC;aAAM,CAAC;YACJ,KAAK,IAAI,CAAC,KAAK,EAAE,CAAC;QACtB,CAAC;IACL,CAAC;IAEQ,MAAM;QACX,OAAO,IAAI,CAAA,GAAG,IAAI,CAAC,OAAO,EAAE,CAAC;IACjC,CAAC;IAED;;;;;OAKG;IACH,IAAI;QACA,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACrB,CAAC;IAED,2FAA2F;IAC3F,KAAK;QACD,IAAI,CAAC,IAAI,GAAG,KAAK,CAAC;IACtB,CAAC;IAED;;;;;;;;OAQG;IACH,MAAM;QACF,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,CAAC;QACzB,IAAI,EAAE,CAAC;IACX,CAAC;IAED,sDAAsD;IACtD,KAAK,CAAC,KAAK;QACP,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC;QAE5B,MAAM,CAAC,SAAS,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QAE9B,2EAA2E;QAC3E,6EAA6E;QAC7E,kFAAkF;QAClF,mEAAmE;QACnE,EAAE;QACF,4EAA4E;QAC5E,2EAA2E;QAC3E,kFAAkF;QAClF,gFAAgF;QAChF,qDAAqD;QACrD,MAAM,OAAO,CAAC,UAAU,CAAC,MAAM,CAAC,aAAa,EAAE,CAAC,GAAG,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,CAAC;QAExF,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QAEjC,+EAA+E;QAC/E,+EAA+E;QAC/E,YAAY;QACZ,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;YACZ,OAAO;QACX,CAAC;QAED,IAAI,CAAC,KAAK,EAAE,CAAC;IACjB,CAAC;IAED;;;;;;;;OAQG;IACH,KAAK;QACD,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC;YACrB,OAAO;QACX,CAAC;QAED,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;QACrB,OAAO,EAAE,CAAC;IACd,CAAC;IAED;;;;;;;;;;OAUG;IACH,MAAM;QACF,MAAM,KAAK,GAAG,IAAI,CAAC,aAAa,CAAC,yBAAyB,CAAC,CAAC;QAC5D,MAAM,KAAK,GAAG,KAAK,EAAE,WAAW,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC;QAE9C,mFAAmF;QACnF,mFAAmF;QACnF,8EAA8E;QAC9E,IAAI,KAAK,KAAK,EAAE,EAAE,CAAC;YACf,IAAI,CAAC,OAAO,CAAC,eAAe,CAAC,YAAY,CAAC,CAAC;QAC/C,CAAC;aAAM,CAAC;YACJ,IAAI,CAAC,OAAO,CAAC,YAAY,CAAC,YAAY,EAAE,KAAK,CAAC,CAAC;QACnD,CAAC;IACL,CAAC;;AAGL,yFAAyF;AACzF,yFAAyF;AACzF,yFAAyF;AACzF,oEAAoE;AACpE,cAAc,CAAC,MAAM,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC"}
|
package/dist/field.d.ts
CHANGED
|
@@ -13,6 +13,10 @@ import { LitElement, type TemplateResult } from 'lit';
|
|
|
13
13
|
* where the association is the platform's job — and this element only generates the ids
|
|
14
14
|
* and points them at each other.
|
|
15
15
|
*
|
|
16
|
+
* **The control may sit below a wrapper**, and the same constraint is why: `<ui-input>` is
|
|
17
|
+
* a box around a control that had to stay in the light DOM, so this element looks through
|
|
18
|
+
* it. See {@link UiField.#control}.
|
|
19
|
+
*
|
|
16
20
|
* @example
|
|
17
21
|
* ```html
|
|
18
22
|
* <ui-field>
|
package/dist/field.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"field.d.ts","sourceRoot":"","sources":["../src/field.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAa,KAAK,cAAc,EAAE,MAAM,KAAK,CAAC;AAMjE
|
|
1
|
+
{"version":3,"file":"field.d.ts","sourceRoot":"","sources":["../src/field.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAa,KAAK,cAAc,EAAE,MAAM,KAAK,CAAC;AAMjE;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,qBAAa,OAAQ,SAAQ,UAAU;;IACnC,gBAAyB,MAAM,0BAuB7B;IAeO,iBAAiB,IAAI,IAAI;IASzB,oBAAoB,IAAI,IAAI;IAK5B,YAAY,IAAI,IAAI;IAIpB,MAAM,IAAI,cAAc;CA2GpC;AAQD,OAAO,CAAC,MAAM,CAAC;IACX,UAAU,qBAAqB;QAC3B,UAAU,EAAE,OAAO,CAAC;KACvB;CACJ"}
|
package/dist/field.js
CHANGED
|
@@ -16,6 +16,10 @@ let sequence = 0;
|
|
|
16
16
|
* where the association is the platform's job — and this element only generates the ids
|
|
17
17
|
* and points them at each other.
|
|
18
18
|
*
|
|
19
|
+
* **The control may sit below a wrapper**, and the same constraint is why: `<ui-input>` is
|
|
20
|
+
* a box around a control that had to stay in the light DOM, so this element looks through
|
|
21
|
+
* it. See {@link UiField.#control}.
|
|
22
|
+
*
|
|
19
23
|
* @example
|
|
20
24
|
* ```html
|
|
21
25
|
* <ui-field>
|
|
@@ -92,6 +96,22 @@ export class UiField extends LitElement {
|
|
|
92
96
|
const found = this.querySelector(selector);
|
|
93
97
|
return found instanceof HTMLElement ? found : undefined;
|
|
94
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* The control this field is about, which a wrapper may stand in front of.
|
|
101
|
+
*
|
|
102
|
+
* `<ui-input>` is such a wrapper, and it is one because the control has to stay in the
|
|
103
|
+
* light DOM for the IDREFs below to resolve at all — so the box that styles it can
|
|
104
|
+
* only ever sit around it, never instead of it. The label points at the control and
|
|
105
|
+
* never at the box: `<label for>` aimed at a custom element labels nothing, which axe
|
|
106
|
+
* reports at critical impact.
|
|
107
|
+
*
|
|
108
|
+
* A control written as a direct child, which is every call site that predates the
|
|
109
|
+
* wrapper, matches nothing to descend into and is returned as it is.
|
|
110
|
+
*/
|
|
111
|
+
#control() {
|
|
112
|
+
const slotted = this.#slotted(null);
|
|
113
|
+
return slotted?.querySelector('input, textarea, select') ?? slotted;
|
|
114
|
+
}
|
|
95
115
|
/**
|
|
96
116
|
* Points the label, the help and the error at the control.
|
|
97
117
|
*
|
|
@@ -99,7 +119,7 @@ export class UiField extends LitElement {
|
|
|
99
119
|
* this element cannot see.
|
|
100
120
|
*/
|
|
101
121
|
#associate() {
|
|
102
|
-
const control = this.#
|
|
122
|
+
const control = this.#control();
|
|
103
123
|
if (control === undefined) {
|
|
104
124
|
return;
|
|
105
125
|
}
|
package/dist/field.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"field.js","sourceRoot":"","sources":["../src/field.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,GAAG,EAAE,IAAI,EAAuB,MAAM,KAAK,CAAC;AACjE,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAE3C,8DAA8D;AAC9D,IAAI,QAAQ,GAAG,CAAC,CAAC;AAEjB
|
|
1
|
+
{"version":3,"file":"field.js","sourceRoot":"","sources":["../src/field.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { UiButton, type ButtonVariant } from './button.js';
|
|
2
|
+
export { UiDialog } from './dialog.js';
|
|
2
3
|
export { UiField } from './field.js';
|
|
4
|
+
export { UiInput, UiTextarea } from './input.js';
|
|
3
5
|
export { tokens, derivedTokens, defaults, formulas, darkScheme, tokenStyleSheet, type Token, type DerivedToken, } from './tokens.js';
|
|
4
6
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AAC3D,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AACrC,OAAO,EACH,MAAM,EACN,aAAa,EACb,QAAQ,EACR,QAAQ,EACR,UAAU,EACV,eAAe,EACf,KAAK,KAAK,EACV,KAAK,YAAY,GACpB,MAAM,aAAa,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AAC3D,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AACrC,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACjD,OAAO,EACH,MAAM,EACN,aAAa,EACb,QAAQ,EACR,QAAQ,EACR,UAAU,EACV,eAAe,EACf,KAAK,KAAK,EACV,KAAK,YAAY,GACpB,MAAM,aAAa,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { UiButton } from './button.js';
|
|
2
|
+
export { UiDialog } from './dialog.js';
|
|
2
3
|
export { UiField } from './field.js';
|
|
4
|
+
export { UiInput, UiTextarea } from './input.js';
|
|
3
5
|
export { tokens, derivedTokens, defaults, formulas, darkScheme, tokenStyleSheet, } from './tokens.js';
|
|
4
6
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAsB,MAAM,aAAa,CAAC;AAC3D,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AACrC,OAAO,EACH,MAAM,EACN,aAAa,EACb,QAAQ,EACR,QAAQ,EACR,UAAU,EACV,eAAe,GAGlB,MAAM,aAAa,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAsB,MAAM,aAAa,CAAC;AAC3D,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AACrC,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACjD,OAAO,EACH,MAAM,EACN,aAAa,EACb,QAAQ,EACR,QAAQ,EACR,UAAU,EACV,eAAe,GAGlB,MAAM,aAAa,CAAC"}
|
package/dist/input.d.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { LitElement, type CSSResult, type TemplateResult } from 'lit';
|
|
2
|
+
/**
|
|
3
|
+
* A styled native form control.
|
|
4
|
+
*
|
|
5
|
+
* The control is the host's own, slotted in rather than rendered here, and that is the
|
|
6
|
+
* single decision this component is made of. {@link UiInput} and {@link UiTextarea} carry
|
|
7
|
+
* the appearance; the element inside carries everything else.
|
|
8
|
+
*/
|
|
9
|
+
declare class UiControl extends LitElement {
|
|
10
|
+
render(): TemplateResult;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* A text input, styled by the token layer rather than replaced.
|
|
14
|
+
*
|
|
15
|
+
* **The `<input>` is yours.** You write it, you set its attributes, and it stays in the
|
|
16
|
+
* light DOM — so `type`, `required`, `readonly`, `name` and `value` are the platform's
|
|
17
|
+
* business and there is no pass-through layer to fall out of step with them. It reaches a
|
|
18
|
+
* form submit for the same reason: a native control inside a `<form>` participates
|
|
19
|
+
* because it is a native control inside a `<form>`, with no `ElementInternals` and no
|
|
20
|
+
* value mirroring. That was the open question on the issue, and the answer is that the
|
|
21
|
+
* shape the accessibility rule forced happens to be the shape that answers it.
|
|
22
|
+
*
|
|
23
|
+
* Composes with {@link UiField}, which finds the control through this wrapper and wires
|
|
24
|
+
* the label, the help, the error and `aria-invalid` to it.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```html
|
|
28
|
+
* <ui-field>
|
|
29
|
+
* <label slot="label">Amount</label>
|
|
30
|
+
* <ui-input><input type="number" name="amount" /></ui-input>
|
|
31
|
+
* <span slot="help">In BRL, two decimals.</span>
|
|
32
|
+
* </ui-field>
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export declare class UiInput extends UiControl {
|
|
36
|
+
static readonly styles: CSSResult;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* A multi-line text control, styled by the token layer rather than replaced.
|
|
40
|
+
*
|
|
41
|
+
* Everything {@link UiInput} says applies. What differs is two rules: a height to start
|
|
42
|
+
* at, and resizing left on the vertical axis — a control that cannot grow is one people
|
|
43
|
+
* fight, and one that grows sideways breaks the layout around it.
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```html
|
|
47
|
+
* <ui-field>
|
|
48
|
+
* <label slot="label">Notes</label>
|
|
49
|
+
* <ui-textarea><textarea name="notes" rows="4"></textarea></ui-textarea>
|
|
50
|
+
* </ui-field>
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export declare class UiTextarea extends UiControl {
|
|
54
|
+
static readonly styles: CSSResult[];
|
|
55
|
+
}
|
|
56
|
+
declare global {
|
|
57
|
+
interface HTMLElementTagNameMap {
|
|
58
|
+
'ui-input': UiInput;
|
|
59
|
+
'ui-textarea': UiTextarea;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export {};
|
|
63
|
+
//# sourceMappingURL=input.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"input.d.ts","sourceRoot":"","sources":["../src/input.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAa,KAAK,SAAS,EAAE,KAAK,cAAc,EAAE,MAAM,KAAK,CAAC;AAyFjF;;;;;;GAMG;AACH,cAAM,SAAU,SAAQ,UAAU;IACrB,MAAM,IAAI,cAAc;CAGpC;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,qBAAa,OAAQ,SAAQ,SAAS;IAClC,gBAAyB,MAAM,EAAE,SAAS,CAAW;CACxD;AAED;;;;;;;;;;;;;;GAcG;AACH,qBAAa,UAAW,SAAQ,SAAS;IACrC,gBAAyB,MAAM,EAAE,SAAS,EAAE,CAQ1C;CACL;AAWD,OAAO,CAAC,MAAM,CAAC;IACX,UAAU,qBAAqB;QAC3B,UAAU,EAAE,OAAO,CAAC;QACpB,aAAa,EAAE,UAAU,CAAC;KAC7B;CACJ"}
|
package/dist/input.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { LitElement, css, html } from 'lit';
|
|
2
|
+
import { reference } from './reference.js';
|
|
3
|
+
/**
|
|
4
|
+
* What a styled native control looks like, which both elements share.
|
|
5
|
+
*
|
|
6
|
+
* One stylesheet naming both `input` and `textarea` rather than one per element: the two
|
|
7
|
+
* differ in what they accept and in one rule about resizing, and duplicating the whole
|
|
8
|
+
* box to express that would be two copies of a contract with nothing comparing them.
|
|
9
|
+
*/
|
|
10
|
+
const control = css `
|
|
11
|
+
:host {
|
|
12
|
+
display: block;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/* Everything below reaches a control the *host* wrote, through ::slotted. That is the
|
|
16
|
+
whole shape of this component and it is forced rather than preferred — the control
|
|
17
|
+
has to stay in the light DOM, where the label and the description ui-field writes
|
|
18
|
+
can resolve against it. Measured, in this suite's own instrument: the same control
|
|
19
|
+
rendered into a shadow root is an axe label violation at critical impact, and the
|
|
20
|
+
aria-describedby pointing at it dangles.
|
|
21
|
+
|
|
22
|
+
::slotted reaches a pseudo-element of a slotted node too, which is what makes the
|
|
23
|
+
placeholder rule below possible — measured, because the selector looks like it
|
|
24
|
+
should not work. */
|
|
25
|
+
::slotted(input),
|
|
26
|
+
::slotted(textarea) {
|
|
27
|
+
box-sizing: border-box;
|
|
28
|
+
inline-size: 100%;
|
|
29
|
+
font: inherit;
|
|
30
|
+
font-family: ${reference('--ui-font')};
|
|
31
|
+
color: ${reference('--ui-color-text')};
|
|
32
|
+
background: ${reference('--ui-color-surface')};
|
|
33
|
+
border: 1px solid ${reference('--ui-color-border')};
|
|
34
|
+
border-radius: ${reference('--ui-radius')};
|
|
35
|
+
padding: ${reference('--ui-space')};
|
|
36
|
+
/* Only the boundary moves. The focus ring is deliberately not in this list, for
|
|
37
|
+
the reason src/button.ts gives beside its own: delaying the affordance that
|
|
38
|
+
says *this is where you are* is the opposite of what it exists to do. */
|
|
39
|
+
transition: border-color ${reference('--ui-duration-state')}
|
|
40
|
+
${reference('--ui-easing-state')};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
::slotted(input)::placeholder,
|
|
44
|
+
::slotted(textarea)::placeholder {
|
|
45
|
+
color: ${reference('--ui-color-text-muted')};
|
|
46
|
+
/* Firefox lowers a placeholder's opacity by default, which would take the colour
|
|
47
|
+
below the 4.5:1 it was chosen to clear — a contrast floor undone by a rule
|
|
48
|
+
nobody wrote. */
|
|
49
|
+
opacity: 1;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/* The boundary completes the mix it already started: the resting border is the
|
|
53
|
+
surface half way to the text, and hovering finishes the trip. A hover colour of its
|
|
54
|
+
own would be a fourth name for a point on a line the token layer already draws.
|
|
55
|
+
|
|
56
|
+
The guards are measured rather than assumed, the same way ui-button's are: a
|
|
57
|
+
disabled control still matches :hover, and a readonly one accepts a pointer it will
|
|
58
|
+
do nothing with. */
|
|
59
|
+
::slotted(input:hover:not(:disabled):not([readonly])),
|
|
60
|
+
::slotted(textarea:hover:not(:disabled):not([readonly])) {
|
|
61
|
+
border-color: ${reference('--ui-color-text')};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/* A visible focus ring is not decoration: removing it is the single most common way a
|
|
65
|
+
component stops being usable by keyboard. */
|
|
66
|
+
::slotted(input:focus-visible),
|
|
67
|
+
::slotted(textarea:focus-visible) {
|
|
68
|
+
outline: 2px solid ${reference('--ui-color-focus')};
|
|
69
|
+
outline-offset: 2px;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
::slotted(input:disabled),
|
|
73
|
+
::slotted(textarea:disabled) {
|
|
74
|
+
cursor: not-allowed;
|
|
75
|
+
opacity: 0.5;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/* The error state is not this component's to decide, and it does not decide it:
|
|
79
|
+
ui-field sets aria-invalid on the control as part of the wiring it already
|
|
80
|
+
owns, and this rule reads it. So the message under the field and the box around it
|
|
81
|
+
cannot disagree about whether there is an error — there is one source and it is the
|
|
82
|
+
one a screen reader is already using. */
|
|
83
|
+
::slotted(input[aria-invalid='true']),
|
|
84
|
+
::slotted(textarea[aria-invalid='true']) {
|
|
85
|
+
border-color: ${reference('--ui-color-danger')};
|
|
86
|
+
}
|
|
87
|
+
`;
|
|
88
|
+
/**
|
|
89
|
+
* A styled native form control.
|
|
90
|
+
*
|
|
91
|
+
* The control is the host's own, slotted in rather than rendered here, and that is the
|
|
92
|
+
* single decision this component is made of. {@link UiInput} and {@link UiTextarea} carry
|
|
93
|
+
* the appearance; the element inside carries everything else.
|
|
94
|
+
*/
|
|
95
|
+
class UiControl extends LitElement {
|
|
96
|
+
render() {
|
|
97
|
+
return html `<slot></slot>`;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* A text input, styled by the token layer rather than replaced.
|
|
102
|
+
*
|
|
103
|
+
* **The `<input>` is yours.** You write it, you set its attributes, and it stays in the
|
|
104
|
+
* light DOM — so `type`, `required`, `readonly`, `name` and `value` are the platform's
|
|
105
|
+
* business and there is no pass-through layer to fall out of step with them. It reaches a
|
|
106
|
+
* form submit for the same reason: a native control inside a `<form>` participates
|
|
107
|
+
* because it is a native control inside a `<form>`, with no `ElementInternals` and no
|
|
108
|
+
* value mirroring. That was the open question on the issue, and the answer is that the
|
|
109
|
+
* shape the accessibility rule forced happens to be the shape that answers it.
|
|
110
|
+
*
|
|
111
|
+
* Composes with {@link UiField}, which finds the control through this wrapper and wires
|
|
112
|
+
* the label, the help, the error and `aria-invalid` to it.
|
|
113
|
+
*
|
|
114
|
+
* @example
|
|
115
|
+
* ```html
|
|
116
|
+
* <ui-field>
|
|
117
|
+
* <label slot="label">Amount</label>
|
|
118
|
+
* <ui-input><input type="number" name="amount" /></ui-input>
|
|
119
|
+
* <span slot="help">In BRL, two decimals.</span>
|
|
120
|
+
* </ui-field>
|
|
121
|
+
* ```
|
|
122
|
+
*/
|
|
123
|
+
export class UiInput extends UiControl {
|
|
124
|
+
static { this.styles = control; }
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* A multi-line text control, styled by the token layer rather than replaced.
|
|
128
|
+
*
|
|
129
|
+
* Everything {@link UiInput} says applies. What differs is two rules: a height to start
|
|
130
|
+
* at, and resizing left on the vertical axis — a control that cannot grow is one people
|
|
131
|
+
* fight, and one that grows sideways breaks the layout around it.
|
|
132
|
+
*
|
|
133
|
+
* @example
|
|
134
|
+
* ```html
|
|
135
|
+
* <ui-field>
|
|
136
|
+
* <label slot="label">Notes</label>
|
|
137
|
+
* <ui-textarea><textarea name="notes" rows="4"></textarea></ui-textarea>
|
|
138
|
+
* </ui-field>
|
|
139
|
+
* ```
|
|
140
|
+
*/
|
|
141
|
+
export class UiTextarea extends UiControl {
|
|
142
|
+
static { this.styles = [
|
|
143
|
+
control,
|
|
144
|
+
css `
|
|
145
|
+
::slotted(textarea) {
|
|
146
|
+
min-block-size: calc(${reference('--ui-space')} * 10);
|
|
147
|
+
resize: vertical;
|
|
148
|
+
}
|
|
149
|
+
`,
|
|
150
|
+
]; }
|
|
151
|
+
}
|
|
152
|
+
// Stryker disable next-line StringLiteral: the registration runs once, at import, inside
|
|
153
|
+
// the warm process Stryker switches mutants in — so by the time a mutant on this line is
|
|
154
|
+
// active the element is already defined under the original name, and no test can observe
|
|
155
|
+
// the change. Outside the runner's reach, not an equivalent mutant.
|
|
156
|
+
customElements.define('ui-input', UiInput);
|
|
157
|
+
// Stryker disable next-line StringLiteral: the same, for the same reason.
|
|
158
|
+
customElements.define('ui-textarea', UiTextarea);
|
|
159
|
+
//# sourceMappingURL=input.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"input.js","sourceRoot":"","sources":["../src/input.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,GAAG,EAAE,IAAI,EAAuC,MAAM,KAAK,CAAC;AACjF,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAE3C;;;;;;GAMG;AACH,MAAM,OAAO,GAAG,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;uBAoBI,SAAS,CAAC,WAAW,CAAC;iBAC5B,SAAS,CAAC,iBAAiB,CAAC;sBACvB,SAAS,CAAC,oBAAoB,CAAC;4BACzB,SAAS,CAAC,mBAAmB,CAAC;yBACjC,SAAS,CAAC,aAAa,CAAC;mBAC9B,SAAS,CAAC,YAAY,CAAC;;;;mCAIP,SAAS,CAAC,qBAAqB,CAAC;cACrD,SAAS,CAAC,mBAAmB,CAAC;;;;;iBAK3B,SAAS,CAAC,uBAAuB,CAAC;;;;;;;;;;;;;;;;wBAgB3B,SAAS,CAAC,iBAAiB,CAAC;;;;;;;6BAOvB,SAAS,CAAC,kBAAkB,CAAC;;;;;;;;;;;;;;;;;wBAiBlC,SAAS,CAAC,mBAAmB,CAAC;;CAErD,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,SAAU,SAAQ,UAAU;IACrB,MAAM;QACX,OAAO,IAAI,CAAA,eAAe,CAAC;IAC/B,CAAC;CACJ;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,OAAO,OAAQ,SAAQ,SAAS;aACT,WAAM,GAAc,OAAO,CAAC;;AAGzD;;;;;;;;;;;;;;GAcG;AACH,MAAM,OAAO,UAAW,SAAQ,SAAS;aACZ,WAAM,GAAgB;QAC3C,OAAO;QACP,GAAG,CAAA;;uCAE4B,SAAS,CAAC,YAAY,CAAC;;;SAGrD;KACJ,CAAC;;AAGN,yFAAyF;AACzF,yFAAyF;AACzF,yFAAyF;AACzF,oEAAoE;AACpE,cAAc,CAAC,MAAM,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;AAE3C,0EAA0E;AAC1E,cAAc,CAAC,MAAM,CAAC,aAAa,EAAE,UAAU,CAAC,CAAC"}
|
package/dist/tokens.d.ts
CHANGED
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
* it does break is code that **enumerates** the set, an exhaustive `Record<Token, string>`
|
|
32
32
|
* above all. The supported shape is the partial map, which is what a theme is anyway.
|
|
33
33
|
*/
|
|
34
|
-
export declare const tokens: readonly ["--ui-color-accent", "--ui-color-accent-contrast", "--ui-color-surface", "--ui-color-text", "--ui-color-focus", "--ui-color-danger", "--ui-radius", "--ui-space", "--ui-font", "--ui-duration-100", "--ui-easing-state"];
|
|
34
|
+
export declare const tokens: readonly ["--ui-color-accent", "--ui-color-accent-contrast", "--ui-color-surface", "--ui-color-text", "--ui-color-focus", "--ui-color-danger", "--ui-color-scrim", "--ui-radius", "--ui-space", "--ui-font", "--ui-duration-100", "--ui-easing-state", "--ui-easing-enter", "--ui-easing-exit"];
|
|
35
35
|
/** A CSS custom property this package defines and gives a default. */
|
|
36
36
|
export type Token = (typeof tokens)[number];
|
|
37
37
|
/**
|
|
@@ -44,7 +44,7 @@ export type Token = (typeof tokens)[number];
|
|
|
44
44
|
* what it buys is an override surface a host can hold in their head: change the accent and
|
|
45
45
|
* the hover and pressed colours follow, rather than being one more name each.
|
|
46
46
|
*/
|
|
47
|
-
export declare const derivedTokens: readonly ["--ui-duration-state", "--ui-color-hover", "--ui-color-pressed", "--ui-color-accent-hover", "--ui-color-accent-pressed"];
|
|
47
|
+
export declare const derivedTokens: readonly ["--ui-duration-state", "--ui-color-border", "--ui-color-text-muted", "--ui-color-hover", "--ui-color-pressed", "--ui-color-accent-hover", "--ui-color-accent-pressed"];
|
|
48
48
|
/** A CSS custom property this package computes rather than declares. */
|
|
49
49
|
export type DerivedToken = (typeof derivedTokens)[number];
|
|
50
50
|
/**
|
package/dist/tokens.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../src/tokens.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,MAAM,
|
|
1
|
+
{"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../src/tokens.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,MAAM,iSA6CT,CAAC;AAEX,sEAAsE;AACtE,MAAM,MAAM,KAAK,GAAG,CAAC,OAAO,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC;AAE5C;;;;;;;;;GASG;AACH,eAAO,MAAM,aAAa,kLAmBhB,CAAC;AAEX,wEAAwE;AACxE,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,aAAa,CAAC,CAAC,MAAM,CAAC,CAAC;AAE1D;;;;;GAKG;AACH,eAAO,MAAM,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,CAuCpD,CAAC;AAqBF;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,eAAO,MAAM,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,CAoB3D,CAAC;AAEF;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,UAAU,EAAE,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,CAW/D,CAAC;AAKF;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,eAAe,IAAI,MAAM,CAsBxC"}
|
package/dist/tokens.js
CHANGED
|
@@ -38,6 +38,19 @@ export const tokens = [
|
|
|
38
38
|
'--ui-color-text',
|
|
39
39
|
'--ui-color-focus',
|
|
40
40
|
'--ui-color-danger',
|
|
41
|
+
// The dim behind a modal, and a *ground* rather than a derivation even though every
|
|
42
|
+
// other neutral here is derived. A derived neutral mixes toward the text, which is
|
|
43
|
+
// what makes one formula right in both schemes — and it is exactly wrong for this
|
|
44
|
+
// one: on a dark page the text is the light pole, so the mix would *lighten* the
|
|
45
|
+
// page behind the dialog instead of dimming it. A scrim dims in both schemes, so it
|
|
46
|
+
// carries a literal and appears in neither `darkScheme` nor `formulas`.
|
|
47
|
+
//
|
|
48
|
+
// **This departs from RFC 0002**, whose survey of the queued components answered
|
|
49
|
+
// *text at an alpha* for this role. That table asks whether a role needs a new hue,
|
|
50
|
+
// and the answer to that question is still no — what it could not weigh, with no
|
|
51
|
+
// overlay yet written to judge against, is which pole the mix should run toward. The
|
|
52
|
+
// proposal says so in the same breath: read as a judgement, not a measurement.
|
|
53
|
+
'--ui-color-scrim',
|
|
41
54
|
'--ui-radius',
|
|
42
55
|
'--ui-space',
|
|
43
56
|
'--ui-font',
|
|
@@ -52,6 +65,17 @@ export const tokens = [
|
|
|
52
65
|
// ground and a component reads it directly.
|
|
53
66
|
'--ui-duration-100',
|
|
54
67
|
'--ui-easing-state',
|
|
68
|
+
// The pair `--ui-easing-state` promised, arriving with the overlay that has an enter
|
|
69
|
+
// and an exit to name — `ui-dialog`. A state change reverses mid-flight and wants a
|
|
70
|
+
// symmetric curve; an overlay does not reverse, it arrives and it leaves, and the two
|
|
71
|
+
// directions are asked to feel different on purpose.
|
|
72
|
+
//
|
|
73
|
+
// No duration steps come with them. The scale is ordinal with gaps precisely so a step
|
|
74
|
+
// can be inserted when something needs one, and the dialog needs no duration the state
|
|
75
|
+
// step does not already give it — inventing `--ui-duration-200` before a component
|
|
76
|
+
// judges it against something is the claim ROADMAP.md declines to make about Zag.
|
|
77
|
+
'--ui-easing-enter',
|
|
78
|
+
'--ui-easing-exit',
|
|
55
79
|
];
|
|
56
80
|
/**
|
|
57
81
|
* The roles computed from the grounds rather than declared beside them.
|
|
@@ -65,6 +89,16 @@ export const tokens = [
|
|
|
65
89
|
*/
|
|
66
90
|
export const derivedTokens = [
|
|
67
91
|
'--ui-duration-state',
|
|
92
|
+
// The boundary of a control, and the text inside one that is not a value yet. Both
|
|
93
|
+
// arrive with `ui-input`, and both are the same mix at different strengths — which is
|
|
94
|
+
// what makes them one category rather than two: a border and a placeholder are the
|
|
95
|
+
// surface travelling toward the text, stopped at the contrast each one owes.
|
|
96
|
+
//
|
|
97
|
+
// The percentages are read off a measurement rather than chosen. A control's boundary
|
|
98
|
+
// is what identifies the component, so WCAG 1.4.11 asks 3:1 against the surface, and
|
|
99
|
+
// a placeholder is text, so 1.4.3 asks 4.5:1. `tests/tokens.test.ts` holds both.
|
|
100
|
+
'--ui-color-border',
|
|
101
|
+
'--ui-color-text-muted',
|
|
68
102
|
// `hover` matches the pseudo-class it answers to; `pressed` deliberately does not —
|
|
69
103
|
// `--ui-color-active` would read as *the active item* as readily as *the pressed
|
|
70
104
|
// control*, and the role this implements was named `accent hover / pressed`.
|
|
@@ -94,6 +128,11 @@ export const defaults = {
|
|
|
94
128
|
// because no axe rule does.
|
|
95
129
|
'--ui-color-focus': '#b45309',
|
|
96
130
|
'--ui-color-danger': '#b91c1c',
|
|
131
|
+
// Half black. Enough to push the page behind a modal out of the reading order for the
|
|
132
|
+
// eye as well as for the accessibility tree, and not so much that the context a modal
|
|
133
|
+
// is *about* stops being visible. The alpha is the whole point, so this is the one
|
|
134
|
+
// default that is not an opaque hex.
|
|
135
|
+
'--ui-color-scrim': 'rgb(0 0 0 / 0.5)',
|
|
97
136
|
'--ui-radius': '0.375rem',
|
|
98
137
|
'--ui-space': '0.5rem',
|
|
99
138
|
'--ui-font': 'system-ui, sans-serif',
|
|
@@ -108,6 +147,11 @@ export const defaults = {
|
|
|
108
147
|
// for. `enter` will want an ease-out and `exit` an ease-in, and they arrive with the
|
|
109
148
|
// overlays that have an enter and an exit to name.
|
|
110
149
|
'--ui-easing-state': 'ease-in-out',
|
|
150
|
+
// Fast out of the gate and settling at the end, which is what makes an arriving
|
|
151
|
+
// overlay feel like it was already on its way. The exit is its mirror: slow to let go
|
|
152
|
+
// and quick to be gone, so a dismissal does not linger over a decision already made.
|
|
153
|
+
'--ui-easing-enter': 'ease-out',
|
|
154
|
+
'--ui-easing-exit': 'ease-in',
|
|
111
155
|
};
|
|
112
156
|
/**
|
|
113
157
|
* A ground as it is written inside a formula: the name, with its own default behind it.
|
|
@@ -164,6 +208,14 @@ export const formulas = {
|
|
|
164
208
|
'--ui-color-pressed': mix('--ui-color-text', 14, '--ui-color-surface'),
|
|
165
209
|
'--ui-color-accent-hover': mix('--ui-color-text', 12, '--ui-color-accent'),
|
|
166
210
|
'--ui-color-accent-pressed': mix('--ui-color-text', 22, '--ui-color-accent'),
|
|
211
|
+
// 3.39:1 on the light surface and 3.96:1 on the dark one, against a floor of 3. The
|
|
212
|
+
// step below clears neither — 45% is 2.94 in light, which is what a value chosen by
|
|
213
|
+
// eye would have shipped.
|
|
214
|
+
'--ui-color-border': mix('--ui-color-text', 50, '--ui-color-surface'),
|
|
215
|
+
// 5.24:1 and 6.07:1, against a floor of 4.5. Not the 60% that first cleared it: that
|
|
216
|
+
// is 4.52 in light, a rounding error from failing, and a default nobody could then
|
|
217
|
+
// retune without breaking a floor they were not thinking about.
|
|
218
|
+
'--ui-color-text-muted': mix('--ui-color-text', 65, '--ui-color-surface'),
|
|
167
219
|
};
|
|
168
220
|
/**
|
|
169
221
|
* The grounds whose value differs when the page is rendered dark.
|
package/dist/tokens.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tokens.js","sourceRoot":"","sources":["../src/tokens.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,MAAM,MAAM,GAAG;IAClB,mBAAmB;IACnB,4BAA4B;IAC5B,oBAAoB;IACpB,iBAAiB;IACjB,kBAAkB;IAClB,mBAAmB;IACnB,aAAa;IACb,YAAY;IACZ,WAAW;IACX,kFAAkF;IAClF,sFAAsF;IACtF,+CAA+C;IAC/C,EAAE;IACF,uFAAuF;IACvF,sFAAsF;IACtF,uFAAuF;IACvF,oFAAoF;IACpF,4CAA4C;IAC5C,mBAAmB;IACnB,mBAAmB;
|
|
1
|
+
{"version":3,"file":"tokens.js","sourceRoot":"","sources":["../src/tokens.ts"],"names":[],"mappings":"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"}
|