@alacris/ui 0.2.1 → 0.2.3
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
CHANGED
|
@@ -46,10 +46,10 @@ The published package is plain ESM. Point an import map at a pinned CDN build of
|
|
|
46
46
|
<script type="importmap">
|
|
47
47
|
{
|
|
48
48
|
"imports": {
|
|
49
|
-
"@alacris/core": "https://cdn.jsdelivr.net/npm/@alacris/core@0.11/dist/alacris.js",
|
|
50
|
-
"@alacris/ui": "https://cdn.jsdelivr.net/npm/@alacris/ui/src/index.js",
|
|
51
|
-
"@alacris/ui/theme": "https://cdn.jsdelivr.net/npm/@alacris/ui/src/theme/index.js",
|
|
52
|
-
"@alacris/ui/components/": "https://cdn.jsdelivr.net/npm/@alacris/ui/src/components/"
|
|
49
|
+
"@alacris/core": "https://cdn.jsdelivr.net/npm/@alacris/core@0.11.1/dist/alacris.js",
|
|
50
|
+
"@alacris/ui": "https://cdn.jsdelivr.net/npm/@alacris/ui@0.2.3/src/index.js",
|
|
51
|
+
"@alacris/ui/theme": "https://cdn.jsdelivr.net/npm/@alacris/ui@0.2.3/src/theme/index.js",
|
|
52
|
+
"@alacris/ui/components/": "https://cdn.jsdelivr.net/npm/@alacris/ui@0.2.3/src/components/"
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
</script>
|
|
@@ -61,7 +61,7 @@ The published package is plain ESM. Point an import map at a pinned CDN build of
|
|
|
61
61
|
<ui-button>Hello</ui-button>
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
-
|
|
64
|
+
Never mix two versions of `@alacris/core` on one page — two copies means two reactive graphs.
|
|
65
65
|
|
|
66
66
|
## Theming
|
|
67
67
|
|
package/package.json
CHANGED
|
@@ -28,6 +28,7 @@ import { define, html, css, vars, computed, signal, effect, onCleanup, each } fr
|
|
|
28
28
|
import { sys } from '../tokens/sys.js';
|
|
29
29
|
import { base } from './base.js';
|
|
30
30
|
import { formBind } from '../util/form.js';
|
|
31
|
+
import { escapeLayer } from '../util/keys.js';
|
|
31
32
|
import { presence } from '../motion/presence.js';
|
|
32
33
|
import { fx } from '../motion/animate.js';
|
|
33
34
|
import { autoUpdate } from '../util/position.js';
|
|
@@ -295,9 +296,15 @@ define('ui-autocomplete', {
|
|
|
295
296
|
if (showPanel() && activeOpt()) { e.preventDefault(); commit(activeOpt().value); }
|
|
296
297
|
else if (freeSolo()) commit(query().trim());
|
|
297
298
|
break;
|
|
298
|
-
case 'Escape': open.set(false); break;
|
|
299
299
|
}
|
|
300
300
|
};
|
|
301
|
+
// The panel owns Escape while it is up, so that closing it inside a
|
|
302
|
+
// dialog does not close the dialog as well.
|
|
303
|
+
effect(() => {
|
|
304
|
+
if (!showPanel()) return;
|
|
305
|
+
return escapeLayer(() => open.set(false));
|
|
306
|
+
});
|
|
307
|
+
|
|
301
308
|
const onFocus = () => { focused.set(true); open.set(true); };
|
|
302
309
|
const onBlur = () => {
|
|
303
310
|
focused.set(false);
|
|
@@ -38,6 +38,7 @@ import { formBind } from '../util/form.js';
|
|
|
38
38
|
import { presence } from '../motion/presence.js';
|
|
39
39
|
import { animate, fx } from '../motion/animate.js';
|
|
40
40
|
import { autoUpdate } from '../util/position.js';
|
|
41
|
+
import { escapeLayer } from '../util/keys.js';
|
|
41
42
|
import { focusTrap, scrollLock } from '../util/focus.js';
|
|
42
43
|
import './ui-icon-button.js';
|
|
43
44
|
import './ui-button.js';
|
|
@@ -607,12 +608,14 @@ define('ui-date-picker', {
|
|
|
607
608
|
if (e.composedPath().includes(host)) return;
|
|
608
609
|
closePanel();
|
|
609
610
|
};
|
|
610
|
-
|
|
611
|
+
// Capture at the document is not early enough: a dialog registers the
|
|
612
|
+
// same way when it opens, so it is already listening by the time this
|
|
613
|
+
// panel does and one Escape closes both.
|
|
614
|
+
const releaseEsc = escapeLayer(closePanel);
|
|
611
615
|
document.addEventListener('pointerdown', onDoc);
|
|
612
|
-
document.addEventListener('keydown', onEsc, true);
|
|
613
616
|
return () => {
|
|
614
617
|
document.removeEventListener('pointerdown', onDoc);
|
|
615
|
-
|
|
618
|
+
releaseEsc();
|
|
616
619
|
};
|
|
617
620
|
});
|
|
618
621
|
effect(() => {
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
// </ui-select>
|
|
8
8
|
//
|
|
9
9
|
// Keyboard (APG select-only combobox): Enter/Space/ArrowDown/ArrowUp open;
|
|
10
|
-
// arrows move the active option, Enter/Space selects it, Escape closes
|
|
10
|
+
// arrows move the active option, Enter/Space selects it, Escape closes the
|
|
11
|
+
// panel only — an enclosing dialog keeps its own Escape for a second press,
|
|
11
12
|
// typing jumps to the next option starting with that letter. The panel closes
|
|
12
13
|
// on outside pointerdown and returns focus to the field.
|
|
13
14
|
//
|
|
@@ -37,6 +38,7 @@ import { formBind } from '../util/form.js';
|
|
|
37
38
|
import { presence } from '../motion/presence.js';
|
|
38
39
|
import { fx } from '../motion/animate.js';
|
|
39
40
|
import { autoUpdate } from '../util/position.js';
|
|
41
|
+
import { escapeLayer } from '../util/keys.js';
|
|
40
42
|
import './ui-icon.js';
|
|
41
43
|
import './ui-option.js';
|
|
42
44
|
|
|
@@ -294,6 +296,18 @@ define('ui-select', {
|
|
|
294
296
|
fieldEl?.focus();
|
|
295
297
|
};
|
|
296
298
|
|
|
299
|
+
// Escape belongs to the panel while it is open, not to whatever encloses
|
|
300
|
+
// it. A dialog listens for the key in the capture phase at the document,
|
|
301
|
+
// so without claiming it a step earlier one press closes the panel and the
|
|
302
|
+
// dialog together.
|
|
303
|
+
effect(() => {
|
|
304
|
+
if (!open()) return;
|
|
305
|
+
return escapeLayer(() => {
|
|
306
|
+
closePanel();
|
|
307
|
+
fieldEl?.focus();
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
|
|
297
311
|
// Outside pointerdown closes (scrim-less popup).
|
|
298
312
|
effect(() => {
|
|
299
313
|
if (!open()) return;
|
|
@@ -362,7 +376,6 @@ define('ui-select', {
|
|
|
362
376
|
}
|
|
363
377
|
case 'Enter':
|
|
364
378
|
case ' ': e.preventDefault(); commit(opts[activeIndex()]); break;
|
|
365
|
-
case 'Escape': e.preventDefault(); closePanel(); break;
|
|
366
379
|
case 'Tab': closePanel(); break;
|
|
367
380
|
default:
|
|
368
381
|
if (e.key.length === 1 && e.key !== ' ') typeahead(e.key);
|
|
@@ -37,6 +37,11 @@ const t = vars('ui-text-field', {
|
|
|
37
37
|
radius: sys.radius.xs,
|
|
38
38
|
font: sys.type.bodyLg,
|
|
39
39
|
height: '56px',
|
|
40
|
+
|
|
41
|
+
// The lane reserved at the end of a number field for its stepper. It is a
|
|
42
|
+
// variable because the buttons are the browser's and their width is not the
|
|
43
|
+
// same on every engine — a consumer with a denser field can shorten it.
|
|
44
|
+
stepperWidth: '18px',
|
|
40
45
|
});
|
|
41
46
|
|
|
42
47
|
const styles = css`
|
|
@@ -217,6 +222,43 @@ const styles = css`
|
|
|
217
222
|
.filled.has-label textarea { padding-top: ${sys.space(7)}; }
|
|
218
223
|
.with-leading.multiline .field { padding-inline-start: ${sys.space(4)}; }
|
|
219
224
|
.with-leading.multiline textarea { padding-inline-start: 0; }
|
|
225
|
+
/* A number field's stepper gets its own lane.
|
|
226
|
+
|
|
227
|
+
The browser draws the spin buttons inside the input's content box, at the
|
|
228
|
+
inline end, and they are painted over whatever is already there: the
|
|
229
|
+
floating label, the placeholder, and the value itself once it is long
|
|
230
|
+
enough. On a narrow field it lands squarely on the label — a chevron
|
|
231
|
+
sitting on the word it is meant to sit beside.
|
|
232
|
+
|
|
233
|
+
So the end of the field is reserved for it. The input is padded by the
|
|
234
|
+
stepper's width, and the label and legend are shortened by the same
|
|
235
|
+
amount so a long label ellipsises before it reaches the buttons rather
|
|
236
|
+
than sliding underneath them. appearance:none does not help here: it
|
|
237
|
+
removes the field's own chrome and leaves the ::-webkit-*-spin-button
|
|
238
|
+
alone, which is why this needs saying explicitly. */
|
|
239
|
+
.numeric input { padding-inline-end: ${t.stepperWidth}; }
|
|
240
|
+
.numeric .label {
|
|
241
|
+
max-inline-size: calc(100% - ${sys.space(4)} - ${t.stepperWidth});
|
|
242
|
+
overflow: hidden;
|
|
243
|
+
text-overflow: ellipsis;
|
|
244
|
+
white-space: nowrap;
|
|
245
|
+
}
|
|
246
|
+
.numeric legend { max-inline-size: calc(100% - ${t.stepperWidth}); }
|
|
247
|
+
.numeric input::-webkit-outer-spin-button,
|
|
248
|
+
.numeric input::-webkit-inner-spin-button {
|
|
249
|
+
/* Held at the end of the reserved lane rather than tight against the
|
|
250
|
+
text, and always visible: a stepper that appears on hover is a control
|
|
251
|
+
nobody knows is there. */
|
|
252
|
+
margin: 0;
|
|
253
|
+
margin-inline-start: ${sys.space(2)};
|
|
254
|
+
opacity: 1;
|
|
255
|
+
}
|
|
256
|
+
/* Firefox draws no buttons at all unless asked, so the reserved lane would
|
|
257
|
+
be an empty gap. Asking for them makes the two engines agree. */
|
|
258
|
+
@supports (-moz-appearance: number-input) {
|
|
259
|
+
.numeric input { -moz-appearance: number-input; }
|
|
260
|
+
}
|
|
261
|
+
|
|
220
262
|
input::placeholder, textarea::placeholder { color: ${t.labelFg}; opacity: 0; transition: opacity ${sys.duration.short2} linear; }
|
|
221
263
|
.floating input::placeholder, .floating textarea::placeholder { opacity: 1; }
|
|
222
264
|
|
|
@@ -255,7 +297,8 @@ define('ui-text-field', {
|
|
|
255
297
|
const cls = computed(() =>
|
|
256
298
|
['root', variant(), floating() && 'floating', focused() && 'focused',
|
|
257
299
|
error() && 'error', disabled() && 'disabled', label() && 'has-label',
|
|
258
|
-
hasLeading() && 'with-leading', type() === 'textarea' && 'multiline'
|
|
300
|
+
hasLeading() && 'with-leading', type() === 'textarea' && 'multiline',
|
|
301
|
+
type() === 'number' && 'numeric'].filter(Boolean).join(' '));
|
|
259
302
|
|
|
260
303
|
const onInput = (e) => {
|
|
261
304
|
value.set(e.target.value);
|
package/src/util/keys.js
CHANGED
|
@@ -108,3 +108,34 @@ export function rovingTabindex(container, opts = {}) {
|
|
|
108
108
|
},
|
|
109
109
|
};
|
|
110
110
|
}
|
|
111
|
+
|
|
112
|
+
// escapeLayer — claim Escape for the innermost open layer.
|
|
113
|
+
//
|
|
114
|
+
// `ui-dialog` listens for Escape in the capture phase at the document, so that
|
|
115
|
+
// the key works wherever focus happens to be. That is right for a dialog and
|
|
116
|
+
// wrong for anything transient opened inside one: a select's panel, a menu, a
|
|
117
|
+
// date picker. Those handle Escape too, but the dialog has already seen it by
|
|
118
|
+
// then, so one press closes both — and choosing a format in a dialog looks
|
|
119
|
+
// like the dialog is broken rather than like an ordering problem nobody can
|
|
120
|
+
// see.
|
|
121
|
+
//
|
|
122
|
+
// Capture descends window → document → …, so a layer claims the key one step
|
|
123
|
+
// earlier than the dialog and stops it there. Nothing below ever runs.
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* escapeLayer(onEscape)
|
|
127
|
+
*
|
|
128
|
+
* Call while a transient layer is open; call the returned function when it
|
|
129
|
+
* closes. Only registers a listener while it is held, so a page with nothing
|
|
130
|
+
* open behaves exactly as before.
|
|
131
|
+
*/
|
|
132
|
+
export function escapeLayer(onEscape) {
|
|
133
|
+
const onKeydown = (e) => {
|
|
134
|
+
if (e.key !== 'Escape') return;
|
|
135
|
+
e.preventDefault();
|
|
136
|
+
e.stopPropagation();
|
|
137
|
+
onEscape(e);
|
|
138
|
+
};
|
|
139
|
+
window.addEventListener('keydown', onKeydown, true);
|
|
140
|
+
return () => window.removeEventListener('keydown', onKeydown, true);
|
|
141
|
+
}
|