@alacris/ui 0.2.1 → 0.2.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 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.0/dist/alacris.js",
50
+ "@alacris/ui": "https://cdn.jsdelivr.net/npm/@alacris/ui@0.2.2/src/index.js",
51
+ "@alacris/ui/theme": "https://cdn.jsdelivr.net/npm/@alacris/ui@0.2.2/src/theme/index.js",
52
+ "@alacris/ui/components/": "https://cdn.jsdelivr.net/npm/@alacris/ui@0.2.2/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
- Pin both packages in production.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alacris/ui",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
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": {
@@ -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
- const onEsc = (e) => { if (e.key === 'Escape') closePanel(); };
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
- document.removeEventListener('keydown', onEsc, true);
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);
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
+ }