@alacris/ui 0.4.0 → 0.4.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 +4 -4
- package/package.json +1 -1
- package/src/components/base.js +1 -1
- package/src/components/ui-autocomplete.js +11 -1
- package/src/components/ui-date-picker.js +9 -8
- package/src/components/ui-dialog.js +4 -1
- package/src/components/ui-drawer.js +6 -3
- package/src/components/ui-fab-menu.js +5 -4
- package/src/components/ui-menu.js +6 -5
- package/src/components/ui-search.js +6 -5
- package/src/components/ui-select.js +9 -6
- package/src/components/ui-sheet.js +6 -3
- package/src/components/ui-side-sheet.js +6 -3
- package/src/components/ui-switch.js +1 -1
- package/src/components/ui-text-field.js +12 -0
- package/src/components/ui-time-picker.js +6 -5
- package/src/index.js +1 -1
- package/src/motion/animate.js +14 -0
- package/src/motion/index.js +1 -1
- package/src/tokens/sys.js +1 -0
- package/src/tokens/system.js +1 -0
- package/src/util/focus.js +2 -2
- package/src/util/popup.js +5 -0
- package/types/index.d.ts +1 -0
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.
|
|
50
|
-
"@alacris/ui": "https://cdn.jsdelivr.net/npm/@alacris/ui@0.4.
|
|
51
|
-
"@alacris/ui/theme": "https://cdn.jsdelivr.net/npm/@alacris/ui@0.4.
|
|
52
|
-
"@alacris/ui/components/": "https://cdn.jsdelivr.net/npm/@alacris/ui@0.4.
|
|
49
|
+
"@alacris/core": "https://cdn.jsdelivr.net/npm/@alacris/core@0.11.4/dist/alacris.js",
|
|
50
|
+
"@alacris/ui": "https://cdn.jsdelivr.net/npm/@alacris/ui@0.4.2/src/index.js",
|
|
51
|
+
"@alacris/ui/theme": "https://cdn.jsdelivr.net/npm/@alacris/ui@0.4.2/src/theme/index.js",
|
|
52
|
+
"@alacris/ui/components/": "https://cdn.jsdelivr.net/npm/@alacris/ui@0.4.2/src/components/"
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
</script>
|
package/package.json
CHANGED
package/src/components/base.js
CHANGED
|
@@ -13,7 +13,7 @@ export const base = css`
|
|
|
13
13
|
font-synthesis: none;
|
|
14
14
|
-webkit-tap-highlight-color: transparent;
|
|
15
15
|
}
|
|
16
|
-
:host([hidden]) { display: none; }
|
|
16
|
+
:host([hidden]) { display: none !important; }
|
|
17
17
|
@media (prefers-reduced-motion: reduce) {
|
|
18
18
|
*, *::before, *::after {
|
|
19
19
|
transition-duration: 0.01ms;
|
|
@@ -185,7 +185,7 @@ const styles = css`
|
|
|
185
185
|
|
|
186
186
|
.panel {
|
|
187
187
|
position: fixed;
|
|
188
|
-
z-index: ${sys.z.
|
|
188
|
+
z-index: ${sys.z.popup};
|
|
189
189
|
min-inline-size: 112px;
|
|
190
190
|
max-block-size: 40vh;
|
|
191
191
|
overflow: auto;
|
|
@@ -276,8 +276,16 @@ define('ui-autocomplete', {
|
|
|
276
276
|
open.set(false);
|
|
277
277
|
};
|
|
278
278
|
|
|
279
|
+
let isComposing = false;
|
|
280
|
+
const onCompositionStart = () => { isComposing = true; };
|
|
281
|
+
const onCompositionEnd = (e) => {
|
|
282
|
+
isComposing = false;
|
|
283
|
+
onInput(e);
|
|
284
|
+
};
|
|
285
|
+
|
|
279
286
|
const onInput = (e) => {
|
|
280
287
|
e.stopPropagation();
|
|
288
|
+
if (isComposing) return;
|
|
281
289
|
query.set(e.target.value);
|
|
282
290
|
open.set(true);
|
|
283
291
|
host.emit('input', { value: e.target.value });
|
|
@@ -358,6 +366,8 @@ define('ui-autocomplete', {
|
|
|
358
366
|
.value=${query}
|
|
359
367
|
placeholder=${() => placeholder() || null}
|
|
360
368
|
?disabled=${disabled} ?required=${required}
|
|
369
|
+
@compositionstart=${onCompositionStart}
|
|
370
|
+
@compositionend=${onCompositionEnd}
|
|
361
371
|
@input=${onInput} @change=${(e) => e.stopPropagation()} @keydown=${onKeydown}
|
|
362
372
|
@focus=${onFocus} @blur=${onBlur}>
|
|
363
373
|
<ui-icon class="arrow" name="arrow-drop-down"></ui-icon>
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
// @prop {string} placeholder=''
|
|
27
27
|
// @event change — committed; detail: { value } or { start, end, value } when range
|
|
28
28
|
// @event input — field keystroke; detail: { value } (the raw text)
|
|
29
|
-
// @event open — calendar visible (after the enter animation)
|
|
30
|
-
// @event close — calendar removed (after the exit animation)
|
|
29
|
+
// @event open — calendar visible (after the enter animation); does not bubble
|
|
30
|
+
// @event close — calendar removed (after the exit animation); does not bubble
|
|
31
31
|
// @part field, input, label, panel, day
|
|
32
32
|
// @vars see `t` below (`themeVars.names`)
|
|
33
33
|
|
|
@@ -39,6 +39,7 @@ import { presence } from '../motion/presence.js';
|
|
|
39
39
|
import { animate, fx } from '../motion/animate.js';
|
|
40
40
|
import { autoUpdate } from '../util/position.js';
|
|
41
41
|
import { escapeLayer } from '../util/keys.js';
|
|
42
|
+
import { popupEvent } from '../util/popup.js';
|
|
42
43
|
import { focusTrap, scrollLock } from '../util/focus.js';
|
|
43
44
|
import './ui-icon-button.js';
|
|
44
45
|
import './ui-button.js';
|
|
@@ -262,7 +263,7 @@ const styles = css`
|
|
|
262
263
|
|
|
263
264
|
.panel {
|
|
264
265
|
position: fixed;
|
|
265
|
-
z-index: ${sys.z.
|
|
266
|
+
z-index: ${sys.z.popup};
|
|
266
267
|
padding: ${sys.space(3)} ${sys.space(3)} ${sys.space(4)};
|
|
267
268
|
background: ${t.panelBg};
|
|
268
269
|
border-radius: ${t.panelRadius};
|
|
@@ -273,7 +274,7 @@ const styles = css`
|
|
|
273
274
|
.overlay {
|
|
274
275
|
position: fixed;
|
|
275
276
|
inset: 0;
|
|
276
|
-
z-index: ${sys.z.
|
|
277
|
+
z-index: ${sys.z.popup};
|
|
277
278
|
display: grid;
|
|
278
279
|
place-items: center;
|
|
279
280
|
}
|
|
@@ -758,16 +759,16 @@ define('ui-date-picker', {
|
|
|
758
759
|
exit: fx.scaleOut,
|
|
759
760
|
enterDuration: 'short4',
|
|
760
761
|
exitDuration: 'short4',
|
|
761
|
-
onEntered: () => host.emit('open'),
|
|
762
|
-
onExited: () => host.emit('close'),
|
|
762
|
+
onEntered: () => host.emit('open', null, popupEvent),
|
|
763
|
+
onExited: () => host.emit('close', null, popupEvent),
|
|
763
764
|
})}
|
|
764
765
|
${presence(() => open() && presentation() === 'modal', modalView, {
|
|
765
766
|
enter: fx.fadeIn,
|
|
766
767
|
exit: fx.fadeOut,
|
|
767
768
|
enterDuration: 'medium2',
|
|
768
769
|
exitDuration: 'short4',
|
|
769
|
-
onEntered: () => host.emit('open'),
|
|
770
|
-
onExited: () => host.emit('close'),
|
|
770
|
+
onEntered: () => host.emit('open', null, popupEvent),
|
|
771
|
+
onExited: () => host.emit('close', null, popupEvent),
|
|
771
772
|
})}
|
|
772
773
|
</div>`;
|
|
773
774
|
},
|
|
@@ -111,19 +111,22 @@ define('ui-dialog', {
|
|
|
111
111
|
};
|
|
112
112
|
|
|
113
113
|
// Trap focus + lock scroll exactly while open; presence handles the DOM.
|
|
114
|
+
let prevActive = null;
|
|
114
115
|
effect(() => {
|
|
115
116
|
if (open()) {
|
|
117
|
+
prevActive = document.activeElement;
|
|
116
118
|
document.addEventListener('keydown', onDocKeydown, true);
|
|
117
119
|
unlock = scrollLock();
|
|
118
120
|
// The surface renders synchronously with the signal write, but wait a
|
|
119
121
|
// microtask so slotted content is distributed before we look for it.
|
|
120
122
|
queueMicrotask(() => {
|
|
121
|
-
if (open() && !releaseTrap) releaseTrap = focusTrap(host);
|
|
123
|
+
if (open() && !releaseTrap) releaseTrap = focusTrap(host, { restore: prevActive });
|
|
122
124
|
});
|
|
123
125
|
} else {
|
|
124
126
|
document.removeEventListener('keydown', onDocKeydown, true);
|
|
125
127
|
releaseTrap?.();
|
|
126
128
|
releaseTrap = null;
|
|
129
|
+
prevActive = null;
|
|
127
130
|
unlock?.();
|
|
128
131
|
unlock = null;
|
|
129
132
|
if (surfaceEl?.isConnected) {
|
|
@@ -26,7 +26,7 @@ import { define, html, css, vars, effect, onCleanup } from '@alacris/core';
|
|
|
26
26
|
import { sys } from '../tokens/sys.js';
|
|
27
27
|
import { base } from './base.js';
|
|
28
28
|
import { presence } from '../motion/presence.js';
|
|
29
|
-
import { animate, fx } from '../motion/animate.js';
|
|
29
|
+
import { animate, fx, releaseFill } from '../motion/animate.js';
|
|
30
30
|
import { focusTrap, scrollLock } from '../util/focus.js';
|
|
31
31
|
|
|
32
32
|
const t = vars('ui-drawer', {
|
|
@@ -105,17 +105,20 @@ define('ui-drawer', {
|
|
|
105
105
|
};
|
|
106
106
|
|
|
107
107
|
// Trap focus + lock scroll exactly while the modal drawer is open.
|
|
108
|
+
let prevActive = null;
|
|
108
109
|
effect(() => {
|
|
109
110
|
if (open() && variant() === 'modal') {
|
|
111
|
+
prevActive = document.activeElement;
|
|
110
112
|
document.addEventListener('keydown', onDocKeydown, true);
|
|
111
113
|
if (!unlock) unlock = scrollLock();
|
|
112
114
|
queueMicrotask(() => {
|
|
113
|
-
if (open.peek() && !releaseTrap) releaseTrap = focusTrap(host);
|
|
115
|
+
if (open.peek() && !releaseTrap) releaseTrap = focusTrap(host, { restore: prevActive });
|
|
114
116
|
});
|
|
115
117
|
} else {
|
|
116
118
|
document.removeEventListener('keydown', onDocKeydown, true);
|
|
117
119
|
releaseTrap?.();
|
|
118
120
|
releaseTrap = null;
|
|
121
|
+
prevActive = null;
|
|
119
122
|
unlock?.();
|
|
120
123
|
unlock = null;
|
|
121
124
|
}
|
|
@@ -135,7 +138,7 @@ define('ui-drawer', {
|
|
|
135
138
|
|
|
136
139
|
const surfaceRef = (el) => {
|
|
137
140
|
surfaceEl = el;
|
|
138
|
-
animate(el, slideIn(), { duration: 'medium2', easing: 'emphasizedDecelerate' });
|
|
141
|
+
releaseFill(animate(el, slideIn(), { duration: 'medium2', easing: 'emphasizedDecelerate' }));
|
|
139
142
|
};
|
|
140
143
|
|
|
141
144
|
const overlay = () => html`
|
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
//
|
|
15
15
|
// @prop {boolean} open=false
|
|
16
16
|
// @prop {string} label='' — accessible name for the action list
|
|
17
|
-
// @event open — menu visible (after the enter animation)
|
|
18
|
-
// @event close — menu removed (after the exit animation)
|
|
17
|
+
// @event open — menu visible (after the enter animation); does not bubble
|
|
18
|
+
// @event close — menu removed (after the exit animation); does not bubble
|
|
19
19
|
// @slot trigger — the <ui-fab> that toggles the menu
|
|
20
20
|
// @slot (default) — related <ui-fab> actions
|
|
21
21
|
// @part actions, trigger
|
|
@@ -26,6 +26,7 @@ import { sys } from '../tokens/sys.js';
|
|
|
26
26
|
import { base } from './base.js';
|
|
27
27
|
import { presence } from '../motion/presence.js';
|
|
28
28
|
import { fx } from '../motion/animate.js';
|
|
29
|
+
import { popupEvent } from '../util/popup.js';
|
|
29
30
|
import './ui-fab.js';
|
|
30
31
|
|
|
31
32
|
const t = vars('ui-fab-menu', {
|
|
@@ -112,8 +113,8 @@ define('ui-fab-menu', {
|
|
|
112
113
|
exit: fx.scaleOut,
|
|
113
114
|
enterDuration: 'short4',
|
|
114
115
|
exitDuration: 'short2',
|
|
115
|
-
onEntered: () => host.emit('open'),
|
|
116
|
-
onExited: () => host.emit('close'),
|
|
116
|
+
onEntered: () => host.emit('open', null, popupEvent),
|
|
117
|
+
onExited: () => host.emit('close', null, popupEvent),
|
|
117
118
|
})}`;
|
|
118
119
|
},
|
|
119
120
|
});
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
// @prop {boolean} open=false
|
|
17
17
|
// @prop {string} placement='bottom-start' — side[-alignment] (see util/position.js)
|
|
18
18
|
// @event select — an item was chosen; detail: { value }
|
|
19
|
-
// @event open — panel visible (after the enter animation)
|
|
20
|
-
// @event close — panel removed (after the exit animation)
|
|
19
|
+
// @event open — panel visible (after the enter animation); does not bubble
|
|
20
|
+
// @event close — panel removed (after the exit animation); does not bubble
|
|
21
21
|
// @slot anchor — the trigger element
|
|
22
22
|
// @slot (default) — <ui-menu-item> children
|
|
23
23
|
// @part panel — the floating menu surface
|
|
@@ -30,6 +30,7 @@ import { presence } from '../motion/presence.js';
|
|
|
30
30
|
import { fx } from '../motion/animate.js';
|
|
31
31
|
import { autoUpdate } from '../util/position.js';
|
|
32
32
|
import { rovingTabindex } from '../util/keys.js';
|
|
33
|
+
import { popupEvent } from '../util/popup.js';
|
|
33
34
|
import './ui-menu-item.js';
|
|
34
35
|
|
|
35
36
|
const t = vars('ui-menu', {
|
|
@@ -47,7 +48,7 @@ const styles = css`
|
|
|
47
48
|
position: fixed;
|
|
48
49
|
inset-inline-start: 0;
|
|
49
50
|
inset-block-start: 0;
|
|
50
|
-
z-index: ${sys.z.
|
|
51
|
+
z-index: ${sys.z.popup};
|
|
51
52
|
min-inline-size: ${t.minWidth};
|
|
52
53
|
max-inline-size: ${t.maxWidth};
|
|
53
54
|
padding-block: ${sys.space(2)};
|
|
@@ -182,8 +183,8 @@ define('ui-menu', {
|
|
|
182
183
|
exitDuration: 'short2',
|
|
183
184
|
enterEasing: 'emphasizedDecelerate',
|
|
184
185
|
exitEasing: 'emphasizedAccelerate',
|
|
185
|
-
onEntered: () => host.emit('open'),
|
|
186
|
-
onExited: () => host.emit('close'),
|
|
186
|
+
onEntered: () => host.emit('open', null, popupEvent),
|
|
187
|
+
onExited: () => host.emit('close', null, popupEvent),
|
|
187
188
|
})}`;
|
|
188
189
|
},
|
|
189
190
|
});
|
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
// @event change — committed (blur/Enter); detail: { value }
|
|
29
29
|
// @event submit — Enter pressed; detail: { value }
|
|
30
30
|
// @event clear — the clear affordance was used
|
|
31
|
-
// @event open — suggestions visible (after the enter animation)
|
|
32
|
-
// @event close — suggestions removed (after the exit animation)
|
|
31
|
+
// @event open — suggestions visible (after the enter animation); does not bubble
|
|
32
|
+
// @event close — suggestions removed (after the exit animation); does not bubble
|
|
33
33
|
// @slot leading — replaces the search icon
|
|
34
34
|
// @slot trailing — after the clear button (avatar, extra actions)
|
|
35
35
|
// @slot (default) — suggestion rows (ui-list-item, …). The panel is a list
|
|
@@ -43,6 +43,7 @@ import { base } from './base.js';
|
|
|
43
43
|
import { formBind } from '../util/form.js';
|
|
44
44
|
import { presence } from '../motion/presence.js';
|
|
45
45
|
import { fx } from '../motion/animate.js';
|
|
46
|
+
import { popupEvent } from '../util/popup.js';
|
|
46
47
|
import './ui-icon.js';
|
|
47
48
|
import './ui-icon-button.js';
|
|
48
49
|
|
|
@@ -75,7 +76,7 @@ const styles = css`
|
|
|
75
76
|
}
|
|
76
77
|
.shell.open {
|
|
77
78
|
overflow: visible;
|
|
78
|
-
z-index: ${sys.z.
|
|
79
|
+
z-index: ${sys.z.popup};
|
|
79
80
|
background: ${t.panelBg};
|
|
80
81
|
border-start-start-radius: ${t.panelRadius};
|
|
81
82
|
border-start-end-radius: ${t.panelRadius};
|
|
@@ -307,8 +308,8 @@ define('ui-search', {
|
|
|
307
308
|
enterEasing: 'emphasizedDecelerate',
|
|
308
309
|
exitDuration: 'short4',
|
|
309
310
|
exitEasing: 'emphasizedAccelerate',
|
|
310
|
-
onEntered: () => host.emit('open'),
|
|
311
|
-
onExited: () => host.emit('close'),
|
|
311
|
+
onEntered: () => host.emit('open', null, popupEvent),
|
|
312
|
+
onExited: () => host.emit('close', null, popupEvent),
|
|
312
313
|
})}
|
|
313
314
|
</div>`;
|
|
314
315
|
},
|
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
// arrows move the active option, Enter/Space selects it, Escape closes the
|
|
11
11
|
// panel only — an enclosing dialog keeps its own Escape for a second press,
|
|
12
12
|
// typing jumps to the next option starting with that letter. The panel closes
|
|
13
|
-
// on outside pointerdown and returns focus to the field.
|
|
13
|
+
// on outside pointerdown and returns focus to the field. `open`/`close` do
|
|
14
|
+
// not bubble: they share those names with dialogs and sheets, and a bubbling
|
|
15
|
+
// select-close looks like the sheet dismissed itself.
|
|
14
16
|
//
|
|
15
17
|
// Past a handful of options the panel gets a filter field, because scrolling
|
|
16
18
|
// is not a way to find one entry among several hundred. It takes focus when
|
|
@@ -31,8 +33,8 @@
|
|
|
31
33
|
// @prop {number} searchThreshold=8
|
|
32
34
|
// @prop {string} searchPlaceholder='Search'
|
|
33
35
|
// @event change — an option was chosen; detail: { value }
|
|
34
|
-
// @event open — panel enter animation finished
|
|
35
|
-
// @event close — panel exit animation finished
|
|
36
|
+
// @event open — panel enter animation finished; does not bubble
|
|
37
|
+
// @event close — panel exit animation finished; does not bubble
|
|
36
38
|
// @slot (default) — <ui-option> children (projected into the panel)
|
|
37
39
|
// @part control — the field button (role="combobox")
|
|
38
40
|
// @part label, panel
|
|
@@ -50,6 +52,7 @@ import { presence } from '../motion/presence.js';
|
|
|
50
52
|
import { fx } from '../motion/animate.js';
|
|
51
53
|
import { autoUpdate } from '../util/position.js';
|
|
52
54
|
import { escapeLayer } from '../util/keys.js';
|
|
55
|
+
import { popupEvent } from '../util/popup.js';
|
|
53
56
|
import './ui-icon.js';
|
|
54
57
|
import './ui-option.js';
|
|
55
58
|
|
|
@@ -210,7 +213,7 @@ const styles = css`
|
|
|
210
213
|
|
|
211
214
|
.panel {
|
|
212
215
|
position: fixed;
|
|
213
|
-
z-index: ${sys.z.
|
|
216
|
+
z-index: ${sys.z.popup};
|
|
214
217
|
min-inline-size: 112px;
|
|
215
218
|
max-block-size: 40vh;
|
|
216
219
|
/* The filter field stays put while the options scroll under it, so the
|
|
@@ -553,8 +556,8 @@ define('ui-select', {
|
|
|
553
556
|
exitDuration: 'short2',
|
|
554
557
|
enterEasing: 'emphasizedDecelerate',
|
|
555
558
|
exitEasing: 'emphasizedAccelerate',
|
|
556
|
-
onEntered: () => host.emit('open'),
|
|
557
|
-
onExited: () => host.emit('close'),
|
|
559
|
+
onEntered: () => host.emit('open', null, popupEvent),
|
|
560
|
+
onExited: () => host.emit('close', null, popupEvent),
|
|
558
561
|
})}
|
|
559
562
|
</div>`;
|
|
560
563
|
},
|
|
@@ -29,7 +29,7 @@ import { define, html, css, vars, effect, onCleanup, signal } from '@alacris/cor
|
|
|
29
29
|
import { sys } from '../tokens/sys.js';
|
|
30
30
|
import { base } from './base.js';
|
|
31
31
|
import { presence } from '../motion/presence.js';
|
|
32
|
-
import { animate, fx } from '../motion/animate.js';
|
|
32
|
+
import { animate, fx, releaseFill } from '../motion/animate.js';
|
|
33
33
|
import { focusTrap, scrollLock } from '../util/focus.js';
|
|
34
34
|
|
|
35
35
|
const t = vars('ui-sheet', {
|
|
@@ -126,17 +126,20 @@ define('ui-sheet', {
|
|
|
126
126
|
if (e.key === 'Escape') requestClose('esc');
|
|
127
127
|
};
|
|
128
128
|
|
|
129
|
+
let prevActive = null;
|
|
129
130
|
effect(() => {
|
|
130
131
|
if (open() && variant() !== 'standard') {
|
|
132
|
+
prevActive = document.activeElement;
|
|
131
133
|
document.addEventListener('keydown', onDocKeydown, true);
|
|
132
134
|
unlock = scrollLock();
|
|
133
135
|
queueMicrotask(() => {
|
|
134
|
-
if (open() && !releaseTrap) releaseTrap = focusTrap(host);
|
|
136
|
+
if (open() && !releaseTrap) releaseTrap = focusTrap(host, { restore: prevActive });
|
|
135
137
|
});
|
|
136
138
|
} else {
|
|
137
139
|
document.removeEventListener('keydown', onDocKeydown, true);
|
|
138
140
|
releaseTrap?.();
|
|
139
141
|
releaseTrap = null;
|
|
142
|
+
prevActive = null;
|
|
140
143
|
unlock?.();
|
|
141
144
|
unlock = null;
|
|
142
145
|
if (surfaceEl?.isConnected) {
|
|
@@ -163,7 +166,7 @@ define('ui-sheet', {
|
|
|
163
166
|
aria-labelledby=${() => (hasHeadline() ? 'headline' : null)}
|
|
164
167
|
aria-label=${() => (hasHeadline() ? null : (label() || 'Sheet'))}
|
|
165
168
|
tabindex="-1"
|
|
166
|
-
ref=${(el) => { surfaceEl = el; animate(el, fx.sheetIn, { duration: 'medium2', easing: 'emphasizedDecelerate' }); }}>
|
|
169
|
+
ref=${(el) => { surfaceEl = el; releaseFill(animate(el, fx.sheetIn, { duration: 'medium2', easing: 'emphasizedDecelerate' })); }}>
|
|
167
170
|
<div class="handle" part="handle" aria-hidden="true"></div>
|
|
168
171
|
<div class="headline" part="headline" id="headline"
|
|
169
172
|
ref=${(el) => hasSlot(el.querySelector('slot'), (has) => {
|
|
@@ -29,7 +29,7 @@ import { define, html, css, vars, effect, onCleanup, signal } from '@alacris/cor
|
|
|
29
29
|
import { sys } from '../tokens/sys.js';
|
|
30
30
|
import { base } from './base.js';
|
|
31
31
|
import { presence } from '../motion/presence.js';
|
|
32
|
-
import { animate, fx } from '../motion/animate.js';
|
|
32
|
+
import { animate, fx, releaseFill } from '../motion/animate.js';
|
|
33
33
|
import { focusTrap, scrollLock } from '../util/focus.js';
|
|
34
34
|
import './ui-icon-button.js';
|
|
35
35
|
|
|
@@ -130,17 +130,20 @@ define('ui-side-sheet', {
|
|
|
130
130
|
if (e.key === 'Escape') requestClose('esc');
|
|
131
131
|
};
|
|
132
132
|
|
|
133
|
+
let prevActive = null;
|
|
133
134
|
effect(() => {
|
|
134
135
|
if (open() && variant() !== 'standard') {
|
|
136
|
+
prevActive = document.activeElement;
|
|
135
137
|
document.addEventListener('keydown', onDocKeydown, true);
|
|
136
138
|
unlock = scrollLock();
|
|
137
139
|
queueMicrotask(() => {
|
|
138
|
-
if (open() && !releaseTrap) releaseTrap = focusTrap(host);
|
|
140
|
+
if (open() && !releaseTrap) releaseTrap = focusTrap(host, { restore: prevActive });
|
|
139
141
|
});
|
|
140
142
|
} else {
|
|
141
143
|
document.removeEventListener('keydown', onDocKeydown, true);
|
|
142
144
|
releaseTrap?.();
|
|
143
145
|
releaseTrap = null;
|
|
146
|
+
prevActive = null;
|
|
144
147
|
unlock?.();
|
|
145
148
|
unlock = null;
|
|
146
149
|
}
|
|
@@ -158,7 +161,7 @@ define('ui-side-sheet', {
|
|
|
158
161
|
|
|
159
162
|
const surfaceRef = (el) => {
|
|
160
163
|
surfaceEl = el;
|
|
161
|
-
animate(el, slideIn(), { duration: 'medium2', easing: 'emphasizedDecelerate' });
|
|
164
|
+
releaseFill(animate(el, slideIn(), { duration: 'medium2', easing: 'emphasizedDecelerate' }));
|
|
162
165
|
};
|
|
163
166
|
|
|
164
167
|
const hasSlot = (el, set) => {
|
|
@@ -144,7 +144,7 @@ define('ui-switch', {
|
|
|
144
144
|
${() => (icons() ? html`<ui-icon name=${() => (checked() ? 'check' : 'close')}></ui-icon>` : null)}
|
|
145
145
|
</span>
|
|
146
146
|
</button>
|
|
147
|
-
${() => (label() ? html`<span id="label">${label}</span>` :
|
|
147
|
+
${() => (label() ? html`<span id="label" part="label">${label}</span>` : html`<slot></slot>`)}
|
|
148
148
|
</label>`;
|
|
149
149
|
},
|
|
150
150
|
});
|
|
@@ -320,7 +320,15 @@ define('ui-text-field', {
|
|
|
320
320
|
hasLeading() && 'with-leading', type() === 'textarea' && 'multiline',
|
|
321
321
|
type() === 'number' && 'numeric'].filter(Boolean).join(' '));
|
|
322
322
|
|
|
323
|
+
let isComposing = false;
|
|
324
|
+
const onCompositionStart = () => { isComposing = true; };
|
|
325
|
+
const onCompositionEnd = (e) => {
|
|
326
|
+
isComposing = false;
|
|
327
|
+
onInput(e);
|
|
328
|
+
};
|
|
329
|
+
|
|
323
330
|
const onInput = (e) => {
|
|
331
|
+
if (isComposing) return;
|
|
324
332
|
value.set(e.target.value);
|
|
325
333
|
host.emit('input', { value: value() });
|
|
326
334
|
};
|
|
@@ -350,6 +358,8 @@ define('ui-text-field', {
|
|
|
350
358
|
spellcheck=${attr(spellcheck)}
|
|
351
359
|
inputmode=${attr(inputmode)}
|
|
352
360
|
enterkeyhint=${attr(enterkeyhint)}
|
|
361
|
+
@compositionstart=${onCompositionStart}
|
|
362
|
+
@compositionend=${onCompositionEnd}
|
|
353
363
|
@input=${onInput} @change=${commit}
|
|
354
364
|
@focus=${() => focused.set(true)} @blur=${() => focused.set(false)}></textarea></span>`
|
|
355
365
|
: html`<input part="input" ref=${(el) => (input = el)}
|
|
@@ -364,6 +374,8 @@ define('ui-text-field', {
|
|
|
364
374
|
spellcheck=${attr(spellcheck)}
|
|
365
375
|
inputmode=${attr(inputmode)}
|
|
366
376
|
enterkeyhint=${attr(enterkeyhint)}
|
|
377
|
+
@compositionstart=${onCompositionStart}
|
|
378
|
+
@compositionend=${onCompositionEnd}
|
|
367
379
|
@input=${onInput} @change=${commit}
|
|
368
380
|
@focus=${() => focused.set(true)} @blur=${() => focused.set(false)}>`)}
|
|
369
381
|
${() => (clearable() && value() !== '' && !disabled()
|
|
@@ -21,8 +21,8 @@
|
|
|
21
21
|
// @prop {string} placeholder=''
|
|
22
22
|
// @event change — committed; detail: { value }
|
|
23
23
|
// @event input — field keystroke; detail: { value } (the raw text)
|
|
24
|
-
// @event open — panel visible (after the enter animation)
|
|
25
|
-
// @event close — panel removed (after the exit animation)
|
|
24
|
+
// @event open — panel visible (after the enter animation); does not bubble
|
|
25
|
+
// @event close — panel removed (after the exit animation); does not bubble
|
|
26
26
|
// @part field, input, label, panel, dial
|
|
27
27
|
// @vars see `t` below (`themeVars.names`)
|
|
28
28
|
|
|
@@ -34,6 +34,7 @@ import { presence } from '../motion/presence.js';
|
|
|
34
34
|
import { fx } from '../motion/animate.js';
|
|
35
35
|
import { autoUpdate } from '../util/position.js';
|
|
36
36
|
import { rovingTabindex } from '../util/keys.js';
|
|
37
|
+
import { popupEvent } from '../util/popup.js';
|
|
37
38
|
import './ui-icon-button.js';
|
|
38
39
|
|
|
39
40
|
const TIME = /^(\d{1,2}):(\d{2})$/;
|
|
@@ -215,7 +216,7 @@ const styles = css`
|
|
|
215
216
|
|
|
216
217
|
.panel {
|
|
217
218
|
position: fixed;
|
|
218
|
-
z-index: ${sys.z.
|
|
219
|
+
z-index: ${sys.z.popup};
|
|
219
220
|
display: flex;
|
|
220
221
|
flex-direction: column;
|
|
221
222
|
gap: ${sys.space(3)};
|
|
@@ -742,8 +743,8 @@ define('ui-time-picker', {
|
|
|
742
743
|
exit: fx.scaleOut,
|
|
743
744
|
enterDuration: 'short4',
|
|
744
745
|
exitDuration: 'short4',
|
|
745
|
-
onEntered: () => host.emit('open'),
|
|
746
|
-
onExited: () => host.emit('close'),
|
|
746
|
+
onEntered: () => host.emit('open', null, popupEvent),
|
|
747
|
+
onExited: () => host.emit('close', null, popupEvent),
|
|
747
748
|
})}
|
|
748
749
|
</div>`;
|
|
749
750
|
},
|
package/src/index.js
CHANGED
|
@@ -20,7 +20,7 @@ export {
|
|
|
20
20
|
} from './tokens/typography.js';
|
|
21
21
|
|
|
22
22
|
// Motion
|
|
23
|
-
export { animate, settled, duration, easing, fx, prefersReducedMotion } from './motion/animate.js';
|
|
23
|
+
export { animate, releaseFill, settled, duration, easing, fx, prefersReducedMotion } from './motion/animate.js';
|
|
24
24
|
export { presence } from './motion/presence.js';
|
|
25
25
|
export { withFlip } from './motion/flip.js';
|
|
26
26
|
export { ripple } from './motion/ripple.js';
|
package/src/motion/animate.js
CHANGED
|
@@ -104,6 +104,20 @@ export function animate(el, keyframes, opts = {}) {
|
|
|
104
104
|
/** Await an animation without throwing when it is cancelled mid-flight. */
|
|
105
105
|
export const settled = (anim) => anim.finished.catch(() => {});
|
|
106
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Drop fill:both once an enter animation has settled.
|
|
109
|
+
*
|
|
110
|
+
* A leftover transform is a containing block for `position:fixed`
|
|
111
|
+
* descendants — a select inside a side sheet is then placed in viewport
|
|
112
|
+
* pixels against the surface, so the list sits beside the sheet, under
|
|
113
|
+
* it, or clipped by the body's overflow. presence() already releases
|
|
114
|
+
* the overlay fade; surfaces animated directly need the same.
|
|
115
|
+
*/
|
|
116
|
+
export function releaseFill(anim) {
|
|
117
|
+
anim.finished.then(() => { try { anim.cancel(); } catch { /* already cancelled */ } });
|
|
118
|
+
return anim;
|
|
119
|
+
}
|
|
120
|
+
|
|
107
121
|
// ------------------------------------------------------------------ presets
|
|
108
122
|
//
|
|
109
123
|
// Keyframe presets tuned to the Material motion spec. Enter presets pair with
|
package/src/motion/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { animate, settled, duration, easing, fx, prefersReducedMotion } from './animate.js';
|
|
1
|
+
export { animate, releaseFill, settled, duration, easing, fx, prefersReducedMotion } from './animate.js';
|
|
2
2
|
export { presence } from './presence.js';
|
|
3
3
|
export { withFlip } from './flip.js';
|
|
4
4
|
export { ripple } from './ripple.js';
|
package/src/tokens/sys.js
CHANGED
package/src/tokens/system.js
CHANGED
package/src/util/focus.js
CHANGED
|
@@ -32,8 +32,8 @@ export function focusables(host) {
|
|
|
32
32
|
*
|
|
33
33
|
* const release = focusTrap(host, { initial: shadowCloseButton });
|
|
34
34
|
*/
|
|
35
|
-
export function focusTrap(host, { initial } = {}) {
|
|
36
|
-
const previous = document.activeElement;
|
|
35
|
+
export function focusTrap(host, { initial, restore } = {}) {
|
|
36
|
+
const previous = restore || document.activeElement;
|
|
37
37
|
|
|
38
38
|
const first = initial || focusables(host)[0] || host;
|
|
39
39
|
first.focus?.();
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// Popup lifecycle events share names with dialogs and sheets (`open` /
|
|
2
|
+
// `close`). They must not bubble: choosing a select option would otherwise
|
|
3
|
+
// look like the sheet dismissed itself. Listeners on the popup host still
|
|
4
|
+
// fire — emit dispatches on the host regardless of bubbles.
|
|
5
|
+
export const popupEvent = { bubbles: false };
|
package/types/index.d.ts
CHANGED
|
@@ -84,6 +84,7 @@ export interface AnimateOptions extends KeyframeAnimationOptions {
|
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
export function animate(el: Element, keyframes: Keyframes, opts?: AnimateOptions): Pick<Animation, 'finished' | 'cancel' | 'finish' | 'play' | 'pause'>;
|
|
87
|
+
export function releaseFill<T extends { finished: Promise<unknown>; cancel(): void }>(anim: T): T;
|
|
87
88
|
export function settled(anim: { finished: Promise<unknown> }): Promise<void>;
|
|
88
89
|
export function duration(key: number | string): number;
|
|
89
90
|
export function easing(key: string): string;
|