@alacris/ui 0.4.0 → 0.4.1

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
@@ -47,9 +47,9 @@ The published package is plain ESM. Point an import map at a pinned CDN build of
47
47
  {
48
48
  "imports": {
49
49
  "@alacris/core": "https://cdn.jsdelivr.net/npm/@alacris/core@0.11.3/dist/alacris.js",
50
- "@alacris/ui": "https://cdn.jsdelivr.net/npm/@alacris/ui@0.4.0/src/index.js",
51
- "@alacris/ui/theme": "https://cdn.jsdelivr.net/npm/@alacris/ui@0.4.0/src/theme/index.js",
52
- "@alacris/ui/components/": "https://cdn.jsdelivr.net/npm/@alacris/ui@0.4.0/src/components/"
50
+ "@alacris/ui": "https://cdn.jsdelivr.net/npm/@alacris/ui@0.4.1/src/index.js",
51
+ "@alacris/ui/theme": "https://cdn.jsdelivr.net/npm/@alacris/ui@0.4.1/src/theme/index.js",
52
+ "@alacris/ui/components/": "https://cdn.jsdelivr.net/npm/@alacris/ui@0.4.1/src/components/"
53
53
  }
54
54
  }
55
55
  </script>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alacris/ui",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Themeable design system for Alacris — Material defaults, sixty-eight custom elements, ESM-only, no build step.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -185,7 +185,7 @@ const styles = css`
185
185
 
186
186
  .panel {
187
187
  position: fixed;
188
- z-index: ${sys.z.modal};
188
+ z-index: ${sys.z.popup};
189
189
  min-inline-size: 112px;
190
190
  max-block-size: 40vh;
191
191
  overflow: auto;
@@ -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.modal};
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.modal};
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
  },
@@ -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', {
@@ -135,7 +135,7 @@ define('ui-drawer', {
135
135
 
136
136
  const surfaceRef = (el) => {
137
137
  surfaceEl = el;
138
- animate(el, slideIn(), { duration: 'medium2', easing: 'emphasizedDecelerate' });
138
+ releaseFill(animate(el, slideIn(), { duration: 'medium2', easing: 'emphasizedDecelerate' }));
139
139
  };
140
140
 
141
141
  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.modal};
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.modal};
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.modal};
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', {
@@ -163,7 +163,7 @@ define('ui-sheet', {
163
163
  aria-labelledby=${() => (hasHeadline() ? 'headline' : null)}
164
164
  aria-label=${() => (hasHeadline() ? null : (label() || 'Sheet'))}
165
165
  tabindex="-1"
166
- ref=${(el) => { surfaceEl = el; animate(el, fx.sheetIn, { duration: 'medium2', easing: 'emphasizedDecelerate' }); }}>
166
+ ref=${(el) => { surfaceEl = el; releaseFill(animate(el, fx.sheetIn, { duration: 'medium2', easing: 'emphasizedDecelerate' })); }}>
167
167
  <div class="handle" part="handle" aria-hidden="true"></div>
168
168
  <div class="headline" part="headline" id="headline"
169
169
  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
 
@@ -158,7 +158,7 @@ define('ui-side-sheet', {
158
158
 
159
159
  const surfaceRef = (el) => {
160
160
  surfaceEl = el;
161
- animate(el, slideIn(), { duration: 'medium2', easing: 'emphasizedDecelerate' });
161
+ releaseFill(animate(el, slideIn(), { duration: 'medium2', easing: 'emphasizedDecelerate' }));
162
162
  };
163
163
 
164
164
  const hasSlot = (el, set) => {
@@ -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.modal};
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';
@@ -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
@@ -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
@@ -65,6 +65,7 @@ export const sys = {
65
65
  appBar: v('z-app-bar'),
66
66
  drawer: v('z-drawer'),
67
67
  modal: v('z-modal'),
68
+ popup: v('z-popup'),
68
69
  snackbar: v('z-snackbar'),
69
70
  tooltip: v('z-tooltip'),
70
71
  },
@@ -103,6 +103,7 @@ export function zTokens() {
103
103
  'z-app-bar': '1100',
104
104
  'z-drawer': '1200',
105
105
  'z-modal': '1300',
106
+ 'z-popup': '1350',
106
107
  'z-snackbar': '1400',
107
108
  'z-tooltip': '1500',
108
109
  };
@@ -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;