@neumorphism-ui/elements 1.0.0-next.0 → 1.0.0-next.2
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 +12 -1
- package/cdn/neu.min.js +184 -51
- package/custom-elements.json +2232 -282
- package/dist/all.d.ts +8 -0
- package/dist/all.js +8 -0
- package/dist/checkbox.d.ts +3 -0
- package/dist/checkbox.js +4 -0
- package/dist/components/button/button.d.ts +0 -2
- package/dist/components/button/button.js +3 -36
- package/dist/components/checkbox/checkbox.d.ts +44 -0
- package/dist/components/checkbox/checkbox.def.d.ts +31 -0
- package/dist/components/checkbox/checkbox.def.js +37 -0
- package/dist/components/checkbox/checkbox.js +40 -0
- package/dist/components/checkbox/checkbox.styles.d.ts +1 -0
- package/dist/components/checkbox/checkbox.styles.js +28 -0
- package/dist/components/icon/system-icons.d.ts +10 -0
- package/dist/components/icon/system-icons.js +22 -11
- package/dist/components/number-field/number-field.d.ts +84 -0
- package/dist/components/number-field/number-field.def.d.ts +125 -0
- package/dist/components/number-field/number-field.def.js +62 -0
- package/dist/components/number-field/number-field.js +121 -0
- package/dist/components/number-field/number-field.styles.d.ts +1 -0
- package/dist/components/number-field/number-field.styles.js +15 -0
- package/dist/components/option/option-data.d.ts +8 -0
- package/dist/components/option/option-data.js +12 -0
- package/dist/components/option/option.d.ts +19 -0
- package/dist/components/option/option.def.d.ts +10 -0
- package/dist/components/option/option.def.js +19 -0
- package/dist/components/option/option.js +17 -0
- package/dist/components/option/option.styles.d.ts +2 -0
- package/dist/components/option/option.styles.js +5 -0
- package/dist/components/radio/radio-link.d.ts +11 -0
- package/dist/components/radio/radio-link.js +5 -0
- package/dist/components/radio/radio.d.ts +29 -0
- package/dist/components/radio/radio.def.d.ts +11 -0
- package/dist/components/radio/radio.def.js +25 -0
- package/dist/components/radio/radio.js +43 -0
- package/dist/components/radio/radio.styles.d.ts +1 -0
- package/dist/components/radio/radio.styles.js +21 -0
- package/dist/components/radio-group/radio-group.d.ts +77 -0
- package/dist/components/radio-group/radio-group.def.d.ts +40 -0
- package/dist/components/radio-group/radio-group.def.js +55 -0
- package/dist/components/radio-group/radio-group.js +184 -0
- package/dist/components/radio-group/radio-group.styles.d.ts +1 -0
- package/dist/components/radio-group/radio-group.styles.js +15 -0
- package/dist/components/select/select.d.ts +119 -0
- package/dist/components/select/select.def.d.ts +38 -0
- package/dist/components/select/select.def.js +59 -0
- package/dist/components/select/select.js +385 -0
- package/dist/components/select/select.styles.d.ts +1 -0
- package/dist/components/select/select.styles.js +39 -0
- package/dist/components/shared/checkable.d.ts +45 -0
- package/dist/components/shared/checkable.js +108 -0
- package/dist/components/shared/field.styles.d.ts +1 -0
- package/dist/components/shared/field.styles.js +36 -0
- package/dist/components/shared/press.d.ts +7 -0
- package/dist/components/shared/press.js +42 -0
- package/dist/components/shared/step.d.ts +2 -0
- package/dist/components/shared/step.js +2 -0
- package/dist/components/shared/text-control.d.ts +72 -0
- package/dist/components/shared/text-control.js +191 -0
- package/dist/components/slider/slider.d.ts +69 -0
- package/dist/components/slider/slider.def.d.ts +39 -0
- package/dist/components/slider/slider.def.js +76 -0
- package/dist/components/slider/slider.js +170 -0
- package/dist/components/slider/slider.styles.d.ts +1 -0
- package/dist/components/slider/slider.styles.js +16 -0
- package/dist/components/switch/switch.d.ts +4 -30
- package/dist/components/switch/switch.js +5 -117
- package/dist/components/text-field/implicit-submit.d.ts +5 -0
- package/dist/components/text-field/implicit-submit.js +8 -3
- package/dist/components/text-field/text-field.d.ts +9 -36
- package/dist/components/text-field/text-field.js +21 -178
- package/dist/components/text-field/text-field.styles.d.ts +1 -1
- package/dist/components/text-field/text-field.styles.js +2 -35
- package/dist/components/textarea/textarea.d.ts +98 -0
- package/dist/components/textarea/textarea.def.d.ts +139 -0
- package/dist/components/textarea/textarea.def.js +65 -0
- package/dist/components/textarea/textarea.js +101 -0
- package/dist/components/textarea/textarea.styles.d.ts +1 -0
- package/dist/components/textarea/textarea.styles.js +9 -0
- package/dist/core/define.d.ts +16 -1
- package/dist/core/form-control.js +3 -1
- package/dist/core/validation.d.ts +6 -2
- package/dist/core/validation.js +15 -8
- package/dist/number-field.d.ts +3 -0
- package/dist/number-field.js +4 -0
- package/dist/option.d.ts +3 -0
- package/dist/option.js +4 -0
- package/dist/radio-group.d.ts +4 -0
- package/dist/radio-group.js +6 -0
- package/dist/radio.d.ts +3 -0
- package/dist/radio.js +4 -0
- package/dist/select.d.ts +3 -0
- package/dist/select.js +4 -0
- package/dist/slider.d.ts +3 -0
- package/dist/slider.js +4 -0
- package/dist/ssr.d.ts +9 -5
- package/dist/ssr.js +25 -5
- package/dist/textarea.d.ts +3 -0
- package/dist/textarea.js +4 -0
- package/package.json +41 -1
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
import { dispatch, setOptionalAttribute, setText } from '../../core/dom.js';
|
|
2
|
+
import { FormControlElement } from '../../core/form-control.js';
|
|
3
|
+
import { requiredMessage } from '../../core/validation.js';
|
|
4
|
+
import { readOption } from '../option/option-data.js';
|
|
5
|
+
import { pickOption, selectDef } from './select.def.js';
|
|
6
|
+
/** Gap between the combobox and the listbox, in px. */
|
|
7
|
+
const GAP = 4;
|
|
8
|
+
/** How long typed characters keep adding to one typeahead search, in ms. */
|
|
9
|
+
const TYPEAHEAD_MS = 500;
|
|
10
|
+
export class NeuSelect extends FormControlElement {
|
|
11
|
+
static def = selectDef;
|
|
12
|
+
static extraProperties = ['defaultValue'];
|
|
13
|
+
/** The requested value, kept even while no option matches it (plan decision 8). */
|
|
14
|
+
current;
|
|
15
|
+
dirty = false;
|
|
16
|
+
/** The options as last read, which the listbox mirrors by index (`option-<index>`). */
|
|
17
|
+
opts = [];
|
|
18
|
+
observer;
|
|
19
|
+
opened = false;
|
|
20
|
+
/** The light-DOM option behind the active one, so a rebuild keeps it active. */
|
|
21
|
+
activeSource;
|
|
22
|
+
/** Whether the listbox was open when the pointer went down on the combobox. */
|
|
23
|
+
pressedOpen;
|
|
24
|
+
typed = '';
|
|
25
|
+
typedTimer;
|
|
26
|
+
constructor() {
|
|
27
|
+
super();
|
|
28
|
+
this.current = String(this.props.value);
|
|
29
|
+
}
|
|
30
|
+
get value() {
|
|
31
|
+
return pickOption(this.read(), this.current) ? this.current : '';
|
|
32
|
+
}
|
|
33
|
+
set value(next) {
|
|
34
|
+
this.dirty = true;
|
|
35
|
+
this.current = next === null || next === undefined ? '' : String(next);
|
|
36
|
+
this.sync();
|
|
37
|
+
}
|
|
38
|
+
get defaultValue() {
|
|
39
|
+
return String(this.props.value);
|
|
40
|
+
}
|
|
41
|
+
set defaultValue(next) {
|
|
42
|
+
this.setAttribute('value', String(next));
|
|
43
|
+
}
|
|
44
|
+
/** Reads the neu-option children from their attributes and text; they need not be upgraded. */
|
|
45
|
+
read() {
|
|
46
|
+
return [...this.querySelectorAll('neu-option')].map((el) => ({
|
|
47
|
+
...readOption(el),
|
|
48
|
+
source: el,
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
51
|
+
/** The value as of the last read: the requested one while an option matches it, else ''. */
|
|
52
|
+
chosen() {
|
|
53
|
+
return pickOption(this.opts, this.current) ? this.current : '';
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Reads the options again, then brings the display, an open listbox and the form in line.
|
|
57
|
+
* setText writes only when the text differs, so a server-rendered display is left alone.
|
|
58
|
+
*/
|
|
59
|
+
sync() {
|
|
60
|
+
this.opts = this.read();
|
|
61
|
+
const picked = pickOption(this.opts, this.current);
|
|
62
|
+
const display = this.ref('display');
|
|
63
|
+
setText(display, picked ? picked.label : String(this.props.placeholder));
|
|
64
|
+
display?.classList.toggle('placeholder', !picked);
|
|
65
|
+
if (this.opened)
|
|
66
|
+
this.render();
|
|
67
|
+
this.commit();
|
|
68
|
+
}
|
|
69
|
+
/** The first enabled option from `from` going `dir`, `from` included; -1 when there is none. */
|
|
70
|
+
step(from, dir) {
|
|
71
|
+
for (let i = from; i >= 0 && i < this.opts.length; i += dir) {
|
|
72
|
+
if (!this.opts[i].disabled)
|
|
73
|
+
return i;
|
|
74
|
+
}
|
|
75
|
+
return -1;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The enabled option nearest to `from` (clamped to the list) going `dir`, else turning back:
|
|
79
|
+
* a move or jump past either end stops at the last enabled option that way.
|
|
80
|
+
*/
|
|
81
|
+
near(from, dir) {
|
|
82
|
+
const start = Math.min(Math.max(from, 0), this.opts.length - 1);
|
|
83
|
+
const found = this.step(start, dir);
|
|
84
|
+
return found < 0 ? this.step(start, -dir) : found;
|
|
85
|
+
}
|
|
86
|
+
activeIndex() {
|
|
87
|
+
return this.opts.findIndex((option) => option.source === this.activeSource);
|
|
88
|
+
}
|
|
89
|
+
/** Builds the listbox from the options and places it; the active option stays if it can. */
|
|
90
|
+
render() {
|
|
91
|
+
const listbox = this.ref('listbox');
|
|
92
|
+
const picked = pickOption(this.opts, this.current);
|
|
93
|
+
listbox.replaceChildren(...this.opts.map((option, i) => {
|
|
94
|
+
const el = document.createElement('div');
|
|
95
|
+
el.className = 'option';
|
|
96
|
+
el.id = `option-${i}`;
|
|
97
|
+
el.setAttribute('part', 'option');
|
|
98
|
+
el.setAttribute('role', 'option');
|
|
99
|
+
el.setAttribute('aria-selected', String(option === picked));
|
|
100
|
+
if (option.disabled)
|
|
101
|
+
el.setAttribute('aria-disabled', 'true');
|
|
102
|
+
el.textContent = option.label;
|
|
103
|
+
return el;
|
|
104
|
+
}));
|
|
105
|
+
this.place();
|
|
106
|
+
const kept = this.activeIndex();
|
|
107
|
+
const selected = picked ? this.opts.indexOf(picked) : -1;
|
|
108
|
+
this.activate(kept >= 0 && !this.opts[kept].disabled ? kept : selected >= 0 ? selected : this.step(0, 1));
|
|
109
|
+
}
|
|
110
|
+
/** Makes option `i` the active one (none for -1) and scrolls it into view. */
|
|
111
|
+
activate(i) {
|
|
112
|
+
const listbox = this.ref('listbox');
|
|
113
|
+
this.activeSource = this.opts[i]?.source;
|
|
114
|
+
const items = [...listbox.children];
|
|
115
|
+
items.forEach((item, n) => item.classList.toggle('active', n === i));
|
|
116
|
+
const item = items[i];
|
|
117
|
+
setOptionalAttribute(this.ref('combobox'), 'aria-activedescendant', item ? item.id : '');
|
|
118
|
+
if (!item)
|
|
119
|
+
return;
|
|
120
|
+
// Keep the listbox padding in view too, so Home and End scroll to the very top and bottom.
|
|
121
|
+
const pad = items[0].offsetTop;
|
|
122
|
+
const bottom = item.offsetTop + item.offsetHeight + pad;
|
|
123
|
+
if (item.offsetTop - pad < listbox.scrollTop)
|
|
124
|
+
listbox.scrollTop = item.offsetTop - pad;
|
|
125
|
+
else if (bottom > listbox.scrollTop + listbox.clientHeight) {
|
|
126
|
+
listbox.scrollTop = bottom - listbox.clientHeight;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/** Puts the listbox under the combobox at its width, or above it when that has more room. */
|
|
130
|
+
place = () => {
|
|
131
|
+
const listbox = this.ref('listbox');
|
|
132
|
+
const box = this.ref('combobox')?.getBoundingClientRect();
|
|
133
|
+
if (!listbox || !box)
|
|
134
|
+
return;
|
|
135
|
+
listbox.style.left = `${box.left}px`;
|
|
136
|
+
listbox.style.width = `${box.width}px`;
|
|
137
|
+
const height = listbox.offsetHeight;
|
|
138
|
+
const below = innerHeight - box.bottom - GAP;
|
|
139
|
+
const above = box.top - GAP;
|
|
140
|
+
const up = height > below && above > below;
|
|
141
|
+
listbox.style.top = `${up ? box.top - GAP - height : box.bottom + GAP}px`;
|
|
142
|
+
};
|
|
143
|
+
open() {
|
|
144
|
+
const listbox = this.ref('listbox');
|
|
145
|
+
if (this.opened || this.isDisabled || !listbox)
|
|
146
|
+
return;
|
|
147
|
+
this.opened = true;
|
|
148
|
+
this.activeSource = undefined;
|
|
149
|
+
if (!listbox.matches(':popover-open'))
|
|
150
|
+
listbox.showPopover();
|
|
151
|
+
this.render();
|
|
152
|
+
this.ref('combobox').setAttribute('aria-expanded', 'true');
|
|
153
|
+
window.addEventListener('scroll', this.place, { capture: true, passive: true });
|
|
154
|
+
window.addEventListener('resize', this.place, { passive: true });
|
|
155
|
+
this.ownerDocument.addEventListener('focusin', this.focusLanded, true);
|
|
156
|
+
dispatch(this, 'neu-open');
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Focus that lands outside the select while the listbox is open. The combobox's focusout
|
|
160
|
+
* covers most moves; this covers focus set elsewhere after the window was left (the focusout
|
|
161
|
+
* then kept the listbox open, and Firefox sends no second one).
|
|
162
|
+
*/
|
|
163
|
+
focusLanded = () => {
|
|
164
|
+
if (!this.shadowRoot.activeElement)
|
|
165
|
+
this.close();
|
|
166
|
+
};
|
|
167
|
+
/** The one way the listbox closes: by key, by choice, by light dismiss (toggle), by focus leaving or when disabled. */
|
|
168
|
+
close() {
|
|
169
|
+
if (!this.opened)
|
|
170
|
+
return;
|
|
171
|
+
this.opened = false;
|
|
172
|
+
const listbox = this.ref('listbox');
|
|
173
|
+
if (listbox.matches(':popover-open'))
|
|
174
|
+
listbox.hidePopover();
|
|
175
|
+
const combobox = this.ref('combobox');
|
|
176
|
+
combobox.setAttribute('aria-expanded', 'false');
|
|
177
|
+
combobox.removeAttribute('aria-activedescendant');
|
|
178
|
+
window.removeEventListener('scroll', this.place, { capture: true });
|
|
179
|
+
window.removeEventListener('resize', this.place);
|
|
180
|
+
this.ownerDocument.removeEventListener('focusin', this.focusLanded, true);
|
|
181
|
+
dispatch(this, 'neu-close');
|
|
182
|
+
}
|
|
183
|
+
/** A choice by the user: enabled options only, and events only when the value changes. */
|
|
184
|
+
choose(i) {
|
|
185
|
+
const option = this.opts[i];
|
|
186
|
+
if (!option || option.disabled)
|
|
187
|
+
return;
|
|
188
|
+
const before = this.chosen();
|
|
189
|
+
this.dirty = true;
|
|
190
|
+
this.current = option.value;
|
|
191
|
+
this.sync();
|
|
192
|
+
if (option.value === before)
|
|
193
|
+
return;
|
|
194
|
+
this.markInteracted();
|
|
195
|
+
dispatch(this, 'input');
|
|
196
|
+
dispatch(this, 'change');
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Jumps to the next option whose label starts with the characters typed within TYPEAHEAD_MS.
|
|
200
|
+
* A buffer of one letter (typed once or repeated) searches that letter from the option after
|
|
201
|
+
* the active one, so repeating it cycles; any other buffer refines the search and starts at the
|
|
202
|
+
* active option, which may still match.
|
|
203
|
+
*/
|
|
204
|
+
typeahead(char) {
|
|
205
|
+
clearTimeout(this.typedTimer);
|
|
206
|
+
this.typedTimer = setTimeout(() => (this.typed = ''), TYPEAHEAD_MS);
|
|
207
|
+
const typed = (this.typed += char.toLowerCase());
|
|
208
|
+
const active = this.activeIndex();
|
|
209
|
+
const count = this.opts.length;
|
|
210
|
+
const find = (prefix, from) => {
|
|
211
|
+
for (let k = 0; k < count; k++) {
|
|
212
|
+
const i = (from + k) % count;
|
|
213
|
+
const option = this.opts[i];
|
|
214
|
+
if (!option.disabled && option.label.toLowerCase().startsWith(prefix))
|
|
215
|
+
return i;
|
|
216
|
+
}
|
|
217
|
+
return -1;
|
|
218
|
+
};
|
|
219
|
+
// Ruling R-8: one letter, typed once or repeated, moves on from the active option, so labels
|
|
220
|
+
// that start with a doubled letter ("AA Battery") cannot hold the cycle.
|
|
221
|
+
const found = [...typed].every((c) => c === typed[0])
|
|
222
|
+
? find(typed[0], active + 1)
|
|
223
|
+
: find(typed, Math.max(active, 0));
|
|
224
|
+
if (found >= 0)
|
|
225
|
+
this.activate(found);
|
|
226
|
+
}
|
|
227
|
+
onKey(event) {
|
|
228
|
+
if (this.isDisabled || event.ctrlKey || event.metaKey)
|
|
229
|
+
return;
|
|
230
|
+
const { key, altKey } = event;
|
|
231
|
+
const printable = key.length === 1 && key !== ' ' && !altKey;
|
|
232
|
+
const active = this.activeIndex();
|
|
233
|
+
const last = this.opts.length - 1;
|
|
234
|
+
const moves = {
|
|
235
|
+
ArrowDown: [active + 1, 1],
|
|
236
|
+
ArrowUp: [active - 1, -1],
|
|
237
|
+
Home: [0, 1],
|
|
238
|
+
End: [last, -1],
|
|
239
|
+
PageDown: [active + 10, 1],
|
|
240
|
+
PageUp: [active - 10, -1],
|
|
241
|
+
};
|
|
242
|
+
if (!this.opened) {
|
|
243
|
+
// Closed (§7.2): Down, Up, Enter and Space open; Home, End and typing open and move.
|
|
244
|
+
if (!printable && !['ArrowDown', 'ArrowUp', 'Enter', ' ', 'Home', 'End'].includes(key))
|
|
245
|
+
return;
|
|
246
|
+
event.preventDefault();
|
|
247
|
+
this.open();
|
|
248
|
+
if (key === 'Home' || key === 'End')
|
|
249
|
+
this.activate(this.near(...moves[key]));
|
|
250
|
+
else if (printable)
|
|
251
|
+
this.typeahead(key);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
if (key === 'Tab') {
|
|
255
|
+
// Choose and close, and let focus move on.
|
|
256
|
+
this.choose(active);
|
|
257
|
+
this.close();
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (key === 'Enter' || key === ' ' || (key === 'ArrowUp' && altKey)) {
|
|
261
|
+
this.choose(active);
|
|
262
|
+
this.close();
|
|
263
|
+
}
|
|
264
|
+
else if (key === 'Escape')
|
|
265
|
+
this.close();
|
|
266
|
+
else if (printable)
|
|
267
|
+
this.typeahead(key);
|
|
268
|
+
else if (moves[key])
|
|
269
|
+
this.activate(this.near(...moves[key]));
|
|
270
|
+
else
|
|
271
|
+
return;
|
|
272
|
+
event.preventDefault();
|
|
273
|
+
}
|
|
274
|
+
/** Index of the listbox option an event happened on, or -1. */
|
|
275
|
+
optionAt(event) {
|
|
276
|
+
const item = event.target.closest('.option');
|
|
277
|
+
return item ? Number(item.id.slice('option-'.length)) : -1;
|
|
278
|
+
}
|
|
279
|
+
rendered() {
|
|
280
|
+
const combobox = this.ref('combobox');
|
|
281
|
+
const listbox = this.ref('listbox');
|
|
282
|
+
combobox.addEventListener('keydown', (event) => this.onKey(event));
|
|
283
|
+
// The browser's light dismiss closes the listbox on the pointerup of a click on the combobox;
|
|
284
|
+
// remembering the state at pointerdown keeps that click from opening it again.
|
|
285
|
+
combobox.addEventListener('pointerdown', () => (this.pressedOpen = this.opened));
|
|
286
|
+
combobox.addEventListener('click', () => {
|
|
287
|
+
const wasOpen = this.pressedOpen ?? this.opened;
|
|
288
|
+
this.pressedOpen = undefined;
|
|
289
|
+
if (wasOpen)
|
|
290
|
+
this.close();
|
|
291
|
+
else
|
|
292
|
+
this.open();
|
|
293
|
+
});
|
|
294
|
+
this.ref('label').addEventListener('click', () => combobox.focus());
|
|
295
|
+
// Focus stays on the combobox (aria-activedescendant) while the pointer is on the listbox.
|
|
296
|
+
listbox.addEventListener('pointerdown', (event) => event.preventDefault());
|
|
297
|
+
listbox.addEventListener('pointermove', (event) => {
|
|
298
|
+
const i = this.optionAt(event);
|
|
299
|
+
if (i >= 0 && i !== this.activeIndex() && !this.opts[i]?.disabled)
|
|
300
|
+
this.activate(i);
|
|
301
|
+
});
|
|
302
|
+
listbox.addEventListener('click', (event) => {
|
|
303
|
+
const i = this.optionAt(event);
|
|
304
|
+
if (i < 0 || this.opts[i]?.disabled)
|
|
305
|
+
return;
|
|
306
|
+
this.choose(i);
|
|
307
|
+
this.close();
|
|
308
|
+
});
|
|
309
|
+
// Light dismiss and the browser's own Escape close the popover without us; follow it.
|
|
310
|
+
listbox.addEventListener('toggle', () => {
|
|
311
|
+
if (!listbox.matches(':popover-open'))
|
|
312
|
+
this.close();
|
|
313
|
+
});
|
|
314
|
+
// Focus moved elsewhere by a script, assistive tech or into a frame. Clicks on the listbox
|
|
315
|
+
// keep focus here (its pointerdown is cancelled), and leaving the window keeps it open.
|
|
316
|
+
combobox.addEventListener('focusout', (event) => {
|
|
317
|
+
const to = event.relatedTarget;
|
|
318
|
+
if (to ? !this.shadowRoot.contains(to) : document.hasFocus())
|
|
319
|
+
this.close();
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
willUpdate(changed) {
|
|
323
|
+
if (changed.has('value') && !this.dirty)
|
|
324
|
+
this.current = String(this.props.value);
|
|
325
|
+
}
|
|
326
|
+
update(changed) {
|
|
327
|
+
const combobox = this.ref('combobox');
|
|
328
|
+
if (changed.has('label'))
|
|
329
|
+
setText(this.ref('label'), String(this.props.label));
|
|
330
|
+
if (changed.has('helpText'))
|
|
331
|
+
setText(this.ref('help-text'), String(this.props.helpText));
|
|
332
|
+
if (changed.has('required')) {
|
|
333
|
+
setOptionalAttribute(combobox, 'aria-required', this.props.required ? 'true' : '');
|
|
334
|
+
}
|
|
335
|
+
if (changed.has('disabled')) {
|
|
336
|
+
const off = this.isDisabled;
|
|
337
|
+
setOptionalAttribute(combobox, 'aria-disabled', off ? 'true' : '');
|
|
338
|
+
setOptionalAttribute(combobox, 'tabindex', off ? '' : '0');
|
|
339
|
+
if (off)
|
|
340
|
+
this.close();
|
|
341
|
+
}
|
|
342
|
+
if (changed.has('value') || changed.has('placeholder'))
|
|
343
|
+
this.sync();
|
|
344
|
+
else if (changed.has('required') || changed.has('errorText'))
|
|
345
|
+
this.commit();
|
|
346
|
+
}
|
|
347
|
+
connected() {
|
|
348
|
+
this.observer ??= new MutationObserver(() => this.sync());
|
|
349
|
+
this.observer.observe(this, {
|
|
350
|
+
childList: true,
|
|
351
|
+
subtree: true,
|
|
352
|
+
characterData: true,
|
|
353
|
+
attributes: true,
|
|
354
|
+
attributeFilter: ['value', 'disabled'],
|
|
355
|
+
});
|
|
356
|
+
this.sync();
|
|
357
|
+
}
|
|
358
|
+
disconnected() {
|
|
359
|
+
this.observer?.disconnect();
|
|
360
|
+
this.close();
|
|
361
|
+
}
|
|
362
|
+
/** Like a native select: an option whose value is "" submits name=; no option, nothing. */
|
|
363
|
+
formValue() {
|
|
364
|
+
return pickOption(this.opts, this.current) ? this.current : null;
|
|
365
|
+
}
|
|
366
|
+
validate() {
|
|
367
|
+
if (this.props.required === true && this.chosen() === '') {
|
|
368
|
+
this.internals.setValidity({ valueMissing: true }, requiredMessage('select', 'Please select an item in the list.'), this.ref('combobox') ?? undefined);
|
|
369
|
+
}
|
|
370
|
+
else {
|
|
371
|
+
this.internals.setValidity({});
|
|
372
|
+
}
|
|
373
|
+
setText(this.ref('error-text'), String(this.props.errorText) || this.internals.validationMessage);
|
|
374
|
+
}
|
|
375
|
+
resetToDefault() {
|
|
376
|
+
this.dirty = false;
|
|
377
|
+
this.current = String(this.props.value);
|
|
378
|
+
this.sync();
|
|
379
|
+
}
|
|
380
|
+
restoreState(state) {
|
|
381
|
+
this.dirty = true;
|
|
382
|
+
this.current = state;
|
|
383
|
+
this.sync();
|
|
384
|
+
}
|
|
385
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const selectStyles = "\n:host {\n --_neu-bg: var(--neu-bg, #e0e5ec);\n --_neu-accent-c: var(--neu-accent, #6d5dfc);\n --_neu-d: var(--neu-distance, 6px);\n --_neu-k: var(--neu-intensity, 1);\n --_neu-r: var(--neu-radius, 12px);\n --_neu-danger: var(--neu-danger, #e5484d);\n --_neu-dur: var(--neu-duration, 150ms);\n --_neu-sd: var(--neu-shadow-dark, color-mix(in oklab, var(--_neu-bg), black min(100%, calc(var(--_neu-shade, 18%) * var(--_neu-k)))));\n --_neu-sl: var(--neu-shadow-light, color-mix(in oklab, var(--_neu-bg), white min(100%, calc(var(--_neu-light, 100%) * var(--_neu-k)))));\n --_neu-fg: var(--neu-fg, color-mix(in oklab, var(--_neu-bg), var(--_neu-ink, black) var(--_neu-fg-mix, 32%)));\n --_neu-fg-muted: var(--neu-fg-muted, color-mix(in oklab, var(--_neu-bg), var(--_neu-ink, black) var(--_neu-fg-muted-mix, 18%)));\n --_neu-accent-soft: color-mix(in oklab, var(--_neu-accent-c) 25%, var(--_neu-bg));\n --_neu-focus: 0 0 0 3px color-mix(in oklab, var(--_neu-accent-c) 45%, transparent);\n --_neu-raised: var(--_neu-d) var(--_neu-d) calc(var(--_neu-d) * 2) var(--_neu-sd), calc(var(--_neu-d) * -1) calc(var(--_neu-d) * -1) calc(var(--_neu-d) * 2) var(--_neu-sl);\n --_neu-raised-sm: calc(var(--_neu-d) * 0.5) calc(var(--_neu-d) * 0.5) var(--_neu-d) var(--_neu-sd), calc(var(--_neu-d) * -0.5) calc(var(--_neu-d) * -0.5) var(--_neu-d) var(--_neu-sl);\n --_neu-inset: inset var(--_neu-d) var(--_neu-d) calc(var(--_neu-d) * 2) var(--_neu-sd), inset calc(var(--_neu-d) * -1) calc(var(--_neu-d) * -1) calc(var(--_neu-d) * 2) var(--_neu-sl);\n --_neu-inset-sm: inset calc(var(--_neu-d) * 0.5) calc(var(--_neu-d) * 0.5) var(--_neu-d) var(--_neu-sd), inset calc(var(--_neu-d) * -0.5) calc(var(--_neu-d) * -0.5) var(--_neu-d) var(--_neu-sl);\n font-family: var(--neu-font);\n color: var(--_neu-fg);\n -webkit-tap-highlight-color: transparent;\n}\n:host(:not(:defined)) { visibility: inherit !important; min-block-size: auto !important; }\n:host([hidden]) { display: none !important; }\n:host(:focus) { outline: none; }\n*, *::before, *::after { box-sizing: border-box; }\n\n:host { display: block; }\n:host(:disabled), :host([disabled]) { cursor: not-allowed; }\n.base { display: flex; flex-direction: column; }\n.label-text, ::slotted([slot='label']) {\n display: block; margin-block-end: 0.375rem;\n font-size: 0.8125rem; font-weight: 600; line-height: 1rem; color: var(--_neu-fg-muted);\n}\n.label-text:empty { display: none; }\n.control {\n display: flex; align-items: center; min-block-size: 2.75rem; padding-inline: 0.875rem;\n border-radius: var(--_neu-r); background: var(--_neu-bg); box-shadow: var(--_neu-inset);\n transition: box-shadow var(--_neu-dur) ease;\n}\n:host([size='sm']) .control { min-block-size: 2.25rem; padding-inline: 0.75rem; box-shadow: var(--_neu-inset-sm); }\n:host([size='lg']) .control { min-block-size: 3.25rem; padding-inline: 1rem; }\n:host(:focus-within) .control { box-shadow: var(--_neu-inset), var(--_neu-focus); }\n:host(:state(user-invalid)) .control {\n box-shadow:\n inset var(--_neu-d) var(--_neu-d) calc(var(--_neu-d) * 2) color-mix(in oklab, var(--_neu-danger) 35%, var(--_neu-sd)),\n inset calc(var(--_neu-d) * -1) calc(var(--_neu-d) * -1) calc(var(--_neu-d) * 2) var(--_neu-sl);\n}\n:host(:disabled) .control, :host([disabled]) .control { box-shadow: var(--_neu-inset-sm); opacity: 0.6; }\n.input {\n flex: 1; min-inline-size: 0; align-self: stretch; padding: 0; border: none; outline: none;\n background: transparent; color: inherit; font: inherit; font-size: 0.9375rem;\n}\n.input::placeholder { color: var(--_neu-fg-muted); opacity: 1; }\n.help { font-size: 0.75rem; color: var(--_neu-fg-muted); }\n.help-text, ::slotted([slot='help-text']) { display: block; margin-block-start: 0.375rem; }\n.help-text:empty { display: none; }\n.error-text { display: none; margin-block-start: 0.375rem; color: var(--_neu-danger); }\n:host(:state(user-invalid)) slot[name='help-text'] { display: none; }\n:host(:state(user-invalid)) .error-text:not(:empty) { display: block; }\n\n.label {\n display: block; margin-block-end: 0.375rem;\n font-size: 0.8125rem; font-weight: 600; line-height: 1rem; color: var(--_neu-fg-muted);\n}\n.label:empty { display: none; }\n.combobox {\n display: flex; align-items: center; gap: 0.5rem; min-block-size: 2.75rem; padding-inline: 0.875rem 0.75rem;\n border-radius: var(--_neu-r); background: var(--_neu-bg); box-shadow: var(--_neu-raised-sm);\n font-size: 0.9375rem; cursor: pointer; user-select: none; outline: none;\n transition: box-shadow var(--_neu-dur) ease;\n}\n:host([size='sm']) .combobox { min-block-size: 2.25rem; padding-inline: 0.75rem 0.625rem; font-size: 0.875rem; }\n:host([size='lg']) .combobox { min-block-size: 3.25rem; padding-inline: 1rem 0.875rem; font-size: 1rem; }\n.combobox[aria-expanded='true'] { box-shadow: var(--_neu-inset-sm); }\n.combobox:focus-visible { box-shadow: var(--_neu-raised-sm), var(--_neu-focus); }\n.combobox[aria-expanded='true']:focus-visible { box-shadow: var(--_neu-inset-sm), var(--_neu-focus); }\n:host(:state(user-invalid)) .combobox {\n box-shadow:\n calc(var(--_neu-d) * 0.5) calc(var(--_neu-d) * 0.5) var(--_neu-d) color-mix(in oklab, var(--_neu-danger) 45%, var(--_neu-sd)),\n calc(var(--_neu-d) * -0.5) calc(var(--_neu-d) * -0.5) var(--_neu-d) var(--_neu-sl);\n}\n:host(:disabled) .combobox, :host([disabled]) .combobox { opacity: 0.6; cursor: not-allowed; }\n.display { flex: 1; min-inline-size: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }\n.placeholder { color: var(--_neu-fg-muted); }\n.chevron { display: inline-flex; flex: none; color: var(--_neu-fg-muted); transition: transform var(--_neu-dur) ease; }\n.chevron svg { inline-size: 1rem; block-size: 1rem; }\n.combobox[aria-expanded='true'] .chevron { transform: rotate(180deg); }\n.listbox {\n position: fixed; inset: auto; margin: 0; max-block-size: 16rem; overflow: auto;\n padding: 0.375rem; border: none; border-radius: var(--_neu-r);\n background: var(--_neu-bg); color: var(--_neu-fg); box-shadow: var(--_neu-raised); font-size: 0.9375rem;\n}\n.option { padding: 0.5rem 0.625rem; border-radius: calc(var(--_neu-r) * 0.75); cursor: pointer; user-select: none; }\n.option.active { box-shadow: var(--_neu-inset-sm); }\n.option[aria-selected='true'] { color: var(--_neu-accent-c); font-weight: 600; }\n.option[aria-disabled='true'] { color: var(--_neu-fg-muted); cursor: not-allowed; }\n";
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { fieldStyles } from '../shared/field.styles.js';
|
|
2
|
+
export const selectStyles = `${fieldStyles}
|
|
3
|
+
.label {
|
|
4
|
+
display: block; margin-block-end: 0.375rem;
|
|
5
|
+
font-size: 0.8125rem; font-weight: 600; line-height: 1rem; color: var(--_neu-fg-muted);
|
|
6
|
+
}
|
|
7
|
+
.label:empty { display: none; }
|
|
8
|
+
.combobox {
|
|
9
|
+
display: flex; align-items: center; gap: 0.5rem; min-block-size: 2.75rem; padding-inline: 0.875rem 0.75rem;
|
|
10
|
+
border-radius: var(--_neu-r); background: var(--_neu-bg); box-shadow: var(--_neu-raised-sm);
|
|
11
|
+
font-size: 0.9375rem; cursor: pointer; user-select: none; outline: none;
|
|
12
|
+
transition: box-shadow var(--_neu-dur) ease;
|
|
13
|
+
}
|
|
14
|
+
:host([size='sm']) .combobox { min-block-size: 2.25rem; padding-inline: 0.75rem 0.625rem; font-size: 0.875rem; }
|
|
15
|
+
:host([size='lg']) .combobox { min-block-size: 3.25rem; padding-inline: 1rem 0.875rem; font-size: 1rem; }
|
|
16
|
+
.combobox[aria-expanded='true'] { box-shadow: var(--_neu-inset-sm); }
|
|
17
|
+
.combobox:focus-visible { box-shadow: var(--_neu-raised-sm), var(--_neu-focus); }
|
|
18
|
+
.combobox[aria-expanded='true']:focus-visible { box-shadow: var(--_neu-inset-sm), var(--_neu-focus); }
|
|
19
|
+
:host(:state(user-invalid)) .combobox {
|
|
20
|
+
box-shadow:
|
|
21
|
+
calc(var(--_neu-d) * 0.5) calc(var(--_neu-d) * 0.5) var(--_neu-d) color-mix(in oklab, var(--_neu-danger) 45%, var(--_neu-sd)),
|
|
22
|
+
calc(var(--_neu-d) * -0.5) calc(var(--_neu-d) * -0.5) var(--_neu-d) var(--_neu-sl);
|
|
23
|
+
}
|
|
24
|
+
:host(:disabled) .combobox, :host([disabled]) .combobox { opacity: 0.6; cursor: not-allowed; }
|
|
25
|
+
.display { flex: 1; min-inline-size: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
|
26
|
+
.placeholder { color: var(--_neu-fg-muted); }
|
|
27
|
+
.chevron { display: inline-flex; flex: none; color: var(--_neu-fg-muted); transition: transform var(--_neu-dur) ease; }
|
|
28
|
+
.chevron svg { inline-size: 1rem; block-size: 1rem; }
|
|
29
|
+
.combobox[aria-expanded='true'] .chevron { transform: rotate(180deg); }
|
|
30
|
+
.listbox {
|
|
31
|
+
position: fixed; inset: auto; margin: 0; max-block-size: 16rem; overflow: auto;
|
|
32
|
+
padding: 0.375rem; border: none; border-radius: var(--_neu-r);
|
|
33
|
+
background: var(--_neu-bg); color: var(--_neu-fg); box-shadow: var(--_neu-raised); font-size: 0.9375rem;
|
|
34
|
+
}
|
|
35
|
+
.option { padding: 0.5rem 0.625rem; border-radius: calc(var(--_neu-r) * 0.75); cursor: pointer; user-select: none; }
|
|
36
|
+
.option.active { box-shadow: var(--_neu-inset-sm); }
|
|
37
|
+
.option[aria-selected='true'] { color: var(--_neu-accent-c); font-weight: 600; }
|
|
38
|
+
.option[aria-disabled='true'] { color: var(--_neu-fg-muted); cursor: not-allowed; }
|
|
39
|
+
`;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { FormControlElement } from '../../core/form-control.js';
|
|
2
|
+
import { type EnterMode } from './press.js';
|
|
3
|
+
/**
|
|
4
|
+
* Base for controls with a checked state (switch, checkbox): the checked/default split, the native
|
|
5
|
+
* click order, form value, required validity, reset and restore. Subclasses paint their own shadow.
|
|
6
|
+
*/
|
|
7
|
+
export declare abstract class CheckableElement extends FormControlElement {
|
|
8
|
+
static extraProperties: string[];
|
|
9
|
+
/** How Enter activates; Space always does. */
|
|
10
|
+
static enter: EnterMode;
|
|
11
|
+
name: string;
|
|
12
|
+
value: string;
|
|
13
|
+
disabled: boolean;
|
|
14
|
+
required: boolean;
|
|
15
|
+
size: 'sm' | 'md' | 'lg';
|
|
16
|
+
private current;
|
|
17
|
+
private dirty;
|
|
18
|
+
/** data-ref of the node that anchors validity messages. */
|
|
19
|
+
protected abstract readonly anchor: string;
|
|
20
|
+
/** Mirrors the current state into the shadow root's classes. */
|
|
21
|
+
protected abstract paint(): void;
|
|
22
|
+
constructor();
|
|
23
|
+
get checked(): boolean;
|
|
24
|
+
set checked(value: boolean);
|
|
25
|
+
get defaultChecked(): boolean;
|
|
26
|
+
set defaultChecked(value: boolean);
|
|
27
|
+
protected ariaCheckedValue(): 'true' | 'false' | 'mixed';
|
|
28
|
+
/** Changes the state for a click and returns the function that puts it back. */
|
|
29
|
+
protected flip(): () => void;
|
|
30
|
+
/**
|
|
31
|
+
* Native checkbox order: flip now, let click listeners see it, then input + change once the
|
|
32
|
+
* click has finished dispatching (within it, unless propagation was stopped before window).
|
|
33
|
+
*/
|
|
34
|
+
private onClick;
|
|
35
|
+
/** Pushes the state to host state, ARIA and the shadow root, without touching the form. */
|
|
36
|
+
protected sync(): void;
|
|
37
|
+
private setCurrent;
|
|
38
|
+
protected willUpdate(changed: ReadonlySet<string>): void;
|
|
39
|
+
protected update(changed: ReadonlySet<string>): void;
|
|
40
|
+
protected formValue(): string | null;
|
|
41
|
+
protected formState(): string;
|
|
42
|
+
protected validate(): void;
|
|
43
|
+
protected resetToDefault(): void;
|
|
44
|
+
protected restoreState(state: string): void;
|
|
45
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { afterDispatch, dispatch } from '../../core/dom.js';
|
|
2
|
+
import { FormControlElement } from '../../core/form-control.js';
|
|
3
|
+
import { requiredMessage } from '../../core/validation.js';
|
|
4
|
+
import { pressable } from './press.js';
|
|
5
|
+
/**
|
|
6
|
+
* Base for controls with a checked state (switch, checkbox): the checked/default split, the native
|
|
7
|
+
* click order, form value, required validity, reset and restore. Subclasses paint their own shadow.
|
|
8
|
+
*/
|
|
9
|
+
export class CheckableElement extends FormControlElement {
|
|
10
|
+
static extraProperties = ['defaultChecked'];
|
|
11
|
+
/** How Enter activates; Space always does. */
|
|
12
|
+
static enter = 'once';
|
|
13
|
+
current;
|
|
14
|
+
dirty = false;
|
|
15
|
+
constructor() {
|
|
16
|
+
super();
|
|
17
|
+
this.current = this.props.checked === true;
|
|
18
|
+
this.addEventListener('click', (event) => this.onClick(event));
|
|
19
|
+
// Like a native checkbox, Space toggles on keyup only after its own keydown.
|
|
20
|
+
pressable(this, this.internals.states, this.constructor.enter, () => this.isDisabled);
|
|
21
|
+
}
|
|
22
|
+
get checked() {
|
|
23
|
+
return this.current;
|
|
24
|
+
}
|
|
25
|
+
set checked(value) {
|
|
26
|
+
this.dirty = true;
|
|
27
|
+
this.setCurrent(Boolean(value));
|
|
28
|
+
}
|
|
29
|
+
get defaultChecked() {
|
|
30
|
+
return this.props.checked === true;
|
|
31
|
+
}
|
|
32
|
+
set defaultChecked(value) {
|
|
33
|
+
this.toggleAttribute('checked', Boolean(value));
|
|
34
|
+
}
|
|
35
|
+
ariaCheckedValue() {
|
|
36
|
+
return this.current ? 'true' : 'false';
|
|
37
|
+
}
|
|
38
|
+
/** Changes the state for a click and returns the function that puts it back. */
|
|
39
|
+
flip() {
|
|
40
|
+
const before = this.current;
|
|
41
|
+
this.setCurrent(!before);
|
|
42
|
+
return () => this.setCurrent(before);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Native checkbox order: flip now, let click listeners see it, then input + change once the
|
|
46
|
+
* click has finished dispatching (within it, unless propagation was stopped before window).
|
|
47
|
+
*/
|
|
48
|
+
onClick(event) {
|
|
49
|
+
this.dirty = true;
|
|
50
|
+
const undo = this.flip();
|
|
51
|
+
const flipped = this.ariaCheckedValue();
|
|
52
|
+
afterDispatch(event, () => {
|
|
53
|
+
if (event.defaultPrevented) {
|
|
54
|
+
undo();
|
|
55
|
+
}
|
|
56
|
+
else if (this.isConnected && this.ariaCheckedValue() === flipped) {
|
|
57
|
+
this.markInteracted();
|
|
58
|
+
dispatch(this, 'input');
|
|
59
|
+
dispatch(this, 'change');
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
/** Pushes the state to host state, ARIA and the shadow root, without touching the form. */
|
|
64
|
+
sync() {
|
|
65
|
+
this.toggleState('checked', this.current);
|
|
66
|
+
this.internals.ariaChecked = this.ariaCheckedValue();
|
|
67
|
+
this.paint();
|
|
68
|
+
}
|
|
69
|
+
setCurrent(value) {
|
|
70
|
+
this.current = value;
|
|
71
|
+
this.sync();
|
|
72
|
+
this.commit();
|
|
73
|
+
}
|
|
74
|
+
willUpdate(changed) {
|
|
75
|
+
if (changed.has('checked') && !this.dirty)
|
|
76
|
+
this.current = this.props.checked === true;
|
|
77
|
+
}
|
|
78
|
+
update(changed) {
|
|
79
|
+
if (changed.has('disabled'))
|
|
80
|
+
this.syncDisabledState();
|
|
81
|
+
if (changed.has('checked'))
|
|
82
|
+
this.setCurrent(this.current);
|
|
83
|
+
else if (changed.has('required') || changed.has('value'))
|
|
84
|
+
this.commit();
|
|
85
|
+
}
|
|
86
|
+
formValue() {
|
|
87
|
+
return this.current ? String(this.props.value) : null;
|
|
88
|
+
}
|
|
89
|
+
formState() {
|
|
90
|
+
return this.current ? 'checked' : 'unchecked';
|
|
91
|
+
}
|
|
92
|
+
validate() {
|
|
93
|
+
if (this.props.required === true && !this.current) {
|
|
94
|
+
this.internals.setValidity({ valueMissing: true }, requiredMessage('checkbox', 'Please check this box if you want to proceed.'), this.ref(this.anchor) ?? undefined);
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
this.internals.setValidity({});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
resetToDefault() {
|
|
101
|
+
this.dirty = false;
|
|
102
|
+
this.setCurrent(this.props.checked === true);
|
|
103
|
+
}
|
|
104
|
+
restoreState(state) {
|
|
105
|
+
this.dirty = true;
|
|
106
|
+
this.setCurrent(state === 'checked');
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const fieldStyles = "\n:host {\n --_neu-bg: var(--neu-bg, #e0e5ec);\n --_neu-accent-c: var(--neu-accent, #6d5dfc);\n --_neu-d: var(--neu-distance, 6px);\n --_neu-k: var(--neu-intensity, 1);\n --_neu-r: var(--neu-radius, 12px);\n --_neu-danger: var(--neu-danger, #e5484d);\n --_neu-dur: var(--neu-duration, 150ms);\n --_neu-sd: var(--neu-shadow-dark, color-mix(in oklab, var(--_neu-bg), black min(100%, calc(var(--_neu-shade, 18%) * var(--_neu-k)))));\n --_neu-sl: var(--neu-shadow-light, color-mix(in oklab, var(--_neu-bg), white min(100%, calc(var(--_neu-light, 100%) * var(--_neu-k)))));\n --_neu-fg: var(--neu-fg, color-mix(in oklab, var(--_neu-bg), var(--_neu-ink, black) var(--_neu-fg-mix, 32%)));\n --_neu-fg-muted: var(--neu-fg-muted, color-mix(in oklab, var(--_neu-bg), var(--_neu-ink, black) var(--_neu-fg-muted-mix, 18%)));\n --_neu-accent-soft: color-mix(in oklab, var(--_neu-accent-c) 25%, var(--_neu-bg));\n --_neu-focus: 0 0 0 3px color-mix(in oklab, var(--_neu-accent-c) 45%, transparent);\n --_neu-raised: var(--_neu-d) var(--_neu-d) calc(var(--_neu-d) * 2) var(--_neu-sd), calc(var(--_neu-d) * -1) calc(var(--_neu-d) * -1) calc(var(--_neu-d) * 2) var(--_neu-sl);\n --_neu-raised-sm: calc(var(--_neu-d) * 0.5) calc(var(--_neu-d) * 0.5) var(--_neu-d) var(--_neu-sd), calc(var(--_neu-d) * -0.5) calc(var(--_neu-d) * -0.5) var(--_neu-d) var(--_neu-sl);\n --_neu-inset: inset var(--_neu-d) var(--_neu-d) calc(var(--_neu-d) * 2) var(--_neu-sd), inset calc(var(--_neu-d) * -1) calc(var(--_neu-d) * -1) calc(var(--_neu-d) * 2) var(--_neu-sl);\n --_neu-inset-sm: inset calc(var(--_neu-d) * 0.5) calc(var(--_neu-d) * 0.5) var(--_neu-d) var(--_neu-sd), inset calc(var(--_neu-d) * -0.5) calc(var(--_neu-d) * -0.5) var(--_neu-d) var(--_neu-sl);\n font-family: var(--neu-font);\n color: var(--_neu-fg);\n -webkit-tap-highlight-color: transparent;\n}\n:host(:not(:defined)) { visibility: inherit !important; min-block-size: auto !important; }\n:host([hidden]) { display: none !important; }\n:host(:focus) { outline: none; }\n*, *::before, *::after { box-sizing: border-box; }\n\n:host { display: block; }\n:host(:disabled), :host([disabled]) { cursor: not-allowed; }\n.base { display: flex; flex-direction: column; }\n.label-text, ::slotted([slot='label']) {\n display: block; margin-block-end: 0.375rem;\n font-size: 0.8125rem; font-weight: 600; line-height: 1rem; color: var(--_neu-fg-muted);\n}\n.label-text:empty { display: none; }\n.control {\n display: flex; align-items: center; min-block-size: 2.75rem; padding-inline: 0.875rem;\n border-radius: var(--_neu-r); background: var(--_neu-bg); box-shadow: var(--_neu-inset);\n transition: box-shadow var(--_neu-dur) ease;\n}\n:host([size='sm']) .control { min-block-size: 2.25rem; padding-inline: 0.75rem; box-shadow: var(--_neu-inset-sm); }\n:host([size='lg']) .control { min-block-size: 3.25rem; padding-inline: 1rem; }\n:host(:focus-within) .control { box-shadow: var(--_neu-inset), var(--_neu-focus); }\n:host(:state(user-invalid)) .control {\n box-shadow:\n inset var(--_neu-d) var(--_neu-d) calc(var(--_neu-d) * 2) color-mix(in oklab, var(--_neu-danger) 35%, var(--_neu-sd)),\n inset calc(var(--_neu-d) * -1) calc(var(--_neu-d) * -1) calc(var(--_neu-d) * 2) var(--_neu-sl);\n}\n:host(:disabled) .control, :host([disabled]) .control { box-shadow: var(--_neu-inset-sm); opacity: 0.6; }\n.input {\n flex: 1; min-inline-size: 0; align-self: stretch; padding: 0; border: none; outline: none;\n background: transparent; color: inherit; font: inherit; font-size: 0.9375rem;\n}\n.input::placeholder { color: var(--_neu-fg-muted); opacity: 1; }\n.help { font-size: 0.75rem; color: var(--_neu-fg-muted); }\n.help-text, ::slotted([slot='help-text']) { display: block; margin-block-start: 0.375rem; }\n.help-text:empty { display: none; }\n.error-text { display: none; margin-block-start: 0.375rem; color: var(--_neu-danger); }\n:host(:state(user-invalid)) slot[name='help-text'] { display: none; }\n:host(:state(user-invalid)) .error-text:not(:empty) { display: block; }\n";
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { hostTheme } from '../../core/styles.js';
|
|
2
|
+
export const fieldStyles = `${hostTheme}
|
|
3
|
+
:host { display: block; }
|
|
4
|
+
:host(:disabled), :host([disabled]) { cursor: not-allowed; }
|
|
5
|
+
.base { display: flex; flex-direction: column; }
|
|
6
|
+
.label-text, ::slotted([slot='label']) {
|
|
7
|
+
display: block; margin-block-end: 0.375rem;
|
|
8
|
+
font-size: 0.8125rem; font-weight: 600; line-height: 1rem; color: var(--_neu-fg-muted);
|
|
9
|
+
}
|
|
10
|
+
.label-text:empty { display: none; }
|
|
11
|
+
.control {
|
|
12
|
+
display: flex; align-items: center; min-block-size: 2.75rem; padding-inline: 0.875rem;
|
|
13
|
+
border-radius: var(--_neu-r); background: var(--_neu-bg); box-shadow: var(--_neu-inset);
|
|
14
|
+
transition: box-shadow var(--_neu-dur) ease;
|
|
15
|
+
}
|
|
16
|
+
:host([size='sm']) .control { min-block-size: 2.25rem; padding-inline: 0.75rem; box-shadow: var(--_neu-inset-sm); }
|
|
17
|
+
:host([size='lg']) .control { min-block-size: 3.25rem; padding-inline: 1rem; }
|
|
18
|
+
:host(:focus-within) .control { box-shadow: var(--_neu-inset), var(--_neu-focus); }
|
|
19
|
+
:host(:state(user-invalid)) .control {
|
|
20
|
+
box-shadow:
|
|
21
|
+
inset var(--_neu-d) var(--_neu-d) calc(var(--_neu-d) * 2) color-mix(in oklab, var(--_neu-danger) 35%, var(--_neu-sd)),
|
|
22
|
+
inset calc(var(--_neu-d) * -1) calc(var(--_neu-d) * -1) calc(var(--_neu-d) * 2) var(--_neu-sl);
|
|
23
|
+
}
|
|
24
|
+
:host(:disabled) .control, :host([disabled]) .control { box-shadow: var(--_neu-inset-sm); opacity: 0.6; }
|
|
25
|
+
.input {
|
|
26
|
+
flex: 1; min-inline-size: 0; align-self: stretch; padding: 0; border: none; outline: none;
|
|
27
|
+
background: transparent; color: inherit; font: inherit; font-size: 0.9375rem;
|
|
28
|
+
}
|
|
29
|
+
.input::placeholder { color: var(--_neu-fg-muted); opacity: 1; }
|
|
30
|
+
.help { font-size: 0.75rem; color: var(--_neu-fg-muted); }
|
|
31
|
+
.help-text, ::slotted([slot='help-text']) { display: block; margin-block-start: 0.375rem; }
|
|
32
|
+
.help-text:empty { display: none; }
|
|
33
|
+
.error-text { display: none; margin-block-start: 0.375rem; color: var(--_neu-danger); }
|
|
34
|
+
:host(:state(user-invalid)) slot[name='help-text'] { display: none; }
|
|
35
|
+
:host(:state(user-invalid)) .error-text:not(:empty) { display: block; }
|
|
36
|
+
`;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** How Enter activates: on every keydown, only on the first, or not at all. */
|
|
2
|
+
export type EnterMode = 'repeat' | 'once' | 'none';
|
|
3
|
+
/**
|
|
4
|
+
* Native-like keyboard activation shared by button, switch and checkbox: Space arms on keydown and
|
|
5
|
+
* clicks on keyup, Enter clicks on keydown. `inactive` blocks both arming and clicking.
|
|
6
|
+
*/
|
|
7
|
+
export declare function pressable(host: HTMLElement, states: CustomStateSet, enter: EnterMode, inactive: () => boolean): void;
|