@jungherz-de/notionkit-elements 1.17.0 → 1.19.0

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.
Files changed (39) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +7 -7
  3. package/SKILL.md +53 -42
  4. package/dist/components/nk-calendar.js +6 -5
  5. package/dist/components/nk-cmdk.js +7 -5
  6. package/dist/components/nk-copy-field.js +33 -5
  7. package/dist/components/nk-dialog.js +5 -5
  8. package/dist/components/nk-menu.js +19 -6
  9. package/dist/components/nk-modal.js +7 -6
  10. package/dist/components/nk-peek.js +5 -4
  11. package/dist/components/nk-pop.js +3 -3
  12. package/dist/components/nk-progress.js +6 -3
  13. package/dist/components/nk-sheet.js +5 -5
  14. package/dist/components/nk-sidebar.js +1 -1
  15. package/dist/components/nk-tab-bar.js +10 -4
  16. package/dist/components/nk-toast.js +107 -11
  17. package/dist/components/nk-tooltip.js +1 -1
  18. package/dist/components/shared/{focus-C4tbSNND.js → focus-D55JGjRA.js} +7 -1
  19. package/dist/components/shared/layers-D9YYYA5g.js +35 -0
  20. package/dist/notionkit-elements.esm.js +241 -59
  21. package/dist/notionkit-elements.esm.min.js +1 -1
  22. package/dist/notionkit-elements.esm.min.js.map +1 -1
  23. package/dist/notionkit-elements.js +241 -59
  24. package/dist/notionkit-elements.min.js +1 -1
  25. package/package.json +3 -3
  26. package/src/components/content/nk-progress.js +6 -3
  27. package/src/components/forms/nk-calendar.js +5 -4
  28. package/src/components/forms/nk-copy-field.js +33 -5
  29. package/src/components/overlays/nk-cmdk.js +7 -5
  30. package/src/components/overlays/nk-dialog.js +4 -4
  31. package/src/components/overlays/nk-menu.js +19 -6
  32. package/src/components/overlays/nk-modal.js +7 -6
  33. package/src/components/overlays/nk-peek.js +4 -3
  34. package/src/components/overlays/nk-pop.js +3 -3
  35. package/src/components/overlays/nk-sheet.js +4 -4
  36. package/src/components/overlays/nk-toast.js +107 -11
  37. package/src/components/shell/nk-tab-bar.js +10 -4
  38. package/src/util/focus.js +6 -0
  39. package/src/util/layers.js +33 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jungherz-de/notionkit-elements",
3
- "version": "1.17.0",
3
+ "version": "1.19.0",
4
4
  "description": "Vanilla JavaScript Web Components wrapping the NotionKit CSS foundation – <nk-*> custom elements with Shadow DOM, automatic theme sync and native form participation.",
5
5
  "license": "MIT",
6
6
  "author": "Jungherz GmbH <info@jungherz.com> (https://www.jungherz.com)",
@@ -63,10 +63,10 @@
63
63
  "dark-mode"
64
64
  ],
65
65
  "peerDependencies": {
66
- "@jungherz-de/notionkit": ">=1.17.0"
66
+ "@jungherz-de/notionkit": ">=1.19.1"
67
67
  },
68
68
  "devDependencies": {
69
- "@jungherz-de/notionkit": "^1.17.0",
69
+ "@jungherz-de/notionkit": "^1.19.1",
70
70
  "@playwright/test": "^1.50.0",
71
71
  "@rollup/plugin-node-resolve": "^15.0.0",
72
72
  "@rollup/plugin-terser": "^0.4.0",
@@ -3,6 +3,9 @@ import { NkElement } from '../../base.js';
3
3
  // <nk-progress value="72" label="72%"></nk-progress>
4
4
  // <nk-progress value="72" label="72 %" wide></nk-progress> → fills its row, the label stays beside it
5
5
  // The bar is `.nk-progress > i` — a child combinator, so both nodes live here.
6
+ // With a label the two form one .nk-progress-row (1.18.0): the label beside
7
+ // the bar in a flex column too – a panel – where the host, display: contents,
8
+ // would let them stand one under the other.
6
9
  class NkProgress extends NkElement {
7
10
  static get observedAttributes() { return ['value', 'max', 'label', 'wide']; }
8
11
 
@@ -11,8 +14,7 @@ class NkProgress extends NkElement {
11
14
  this._fill = document.createElement('i');
12
15
  this._bar.appendChild(this._fill);
13
16
  this._label = this.createElement('span', ['nk-progress-label']);
14
- this._wrapper.appendChild(this._bar);
15
- this._wrapper.appendChild(this._label);
17
+ this._row = this.createElement('span', ['nk-progress-row']);
16
18
  this._sync();
17
19
  }
18
20
 
@@ -29,7 +31,8 @@ class NkProgress extends NkElement {
29
31
  this._bar.setAttribute('aria-valuemax', max);
30
32
  const label = this.getAttribute('label');
31
33
  this._label.textContent = label ?? '';
32
- this._label.style.display = label === null ? 'none' : '';
34
+ if (label === null) { if (this._bar.parentNode !== this._wrapper) this._wrapper.replaceChildren(this._bar); }
35
+ else if (this._bar.parentNode !== this._row) { this._row.replaceChildren(this._bar, this._label); this._wrapper.replaceChildren(this._row); }
33
36
  }
34
37
 
35
38
  get value() { return Number(this.getAttribute('value')) || 0; }
@@ -2,6 +2,7 @@ import { NkFormElement } from '../../base.js';
2
2
  import { isoOf, parseDay, addDays, isoWeek, resolveLocale, firstWeekday, weekdayNames, monthGrid, stepMonth, weekLabel, CHEVRONS } from '../../util/dates.js';
3
3
  import { placeUnder } from '../../util/floating.js';
4
4
  import { deepActiveElement } from '../../util/focus.js';
5
+ import { openLayer, closeLayer } from '../../util/layers.js';
5
6
 
6
7
  // <nk-calendar name="due" value="2026-06-02" weeks weekend="6,0"></nk-calendar>
7
8
  // <nk-calendar range value="2026-06-08/2026-06-12"></nk-calendar>
@@ -271,14 +272,13 @@ class NkCalendar extends NkFormElement {
271
272
  this._draw();
272
273
  this.emit('nk-change', { value: '', start: null, end: null, time: null });
273
274
  };
274
- // Floating: a tap outside closes it and reaches nothing else; Escape closes (captured, before a modal around it).
275
+ // Floating: a tap outside closes it and reaches nothing else; Escape closes it, before a dialog around it (util/layers.js).
275
276
  this._onOutside = (e) => {
276
277
  const path = e.composedPath();
277
278
  if (path.includes(this) || (this._anchor && path.includes(this._anchor))) return;
278
279
  e.preventDefault(); e.stopPropagation();
279
280
  this.close();
280
281
  };
281
- this._onEscape = (e) => { if (e.key === 'Escape' && this.getBoolAttr('floating') && this.getBoolAttr('open')) { e.stopPropagation(); this.close(); } };
282
282
  this._grid.addEventListener('click', this._onGrid);
283
283
  this._grid.addEventListener('keydown', this._onKey);
284
284
  this._todayBtn.addEventListener('click', this._onToday);
@@ -286,7 +286,6 @@ class NkCalendar extends NkFormElement {
286
286
  this._next.addEventListener('click', this._onNext);
287
287
  this._timeInput.addEventListener('change', this._onTime);
288
288
  this._clear.addEventListener('click', this._onClear);
289
- document.addEventListener('keydown', this._onEscape, true);
290
289
  if (this.getBoolAttr('open')) document.addEventListener('click', this._onOutside, true);
291
290
  }
292
291
 
@@ -298,7 +297,7 @@ class NkCalendar extends NkFormElement {
298
297
  this._next?.removeEventListener('click', this._onNext);
299
298
  this._timeInput?.removeEventListener('change', this._onTime);
300
299
  this._clear?.removeEventListener('click', this._onClear);
301
- document.removeEventListener('keydown', this._onEscape, true);
300
+ closeLayer(this);
302
301
  document.removeEventListener('click', this._onOutside, true);
303
302
  }
304
303
 
@@ -322,7 +321,9 @@ class NkCalendar extends NkFormElement {
322
321
  this._returnFocus = deepActiveElement();
323
322
  if (this._returnFocus?.matches(':focus-visible')) requestAnimationFrame(() => this.focusDay());
324
323
  document.addEventListener('click', this._onOutside, true);
324
+ if (this.getBoolAttr('floating')) openLayer(this, () => this.close());
325
325
  } else {
326
+ closeLayer(this);
326
327
  document.removeEventListener('click', this._onOutside, true);
327
328
  const back = this._returnFocus;
328
329
  this._returnFocus = null;
@@ -3,7 +3,8 @@ import { NkElement } from '../../base.js';
3
3
  // <nk-copy-field value="https://monahilft.notionkit.app"></nk-copy-field>
4
4
  // <nk-copy-field value="ntn_4f2…" secret mono></nk-copy-field>
5
5
  // → <div class="nk-copy-field mono"><span class="cf-value">…</span>
6
- // <button class="cf-btn">Show</button><button class="cf-btn">Copy</button></div>
6
+ // <button class="cf-btn" aria-label="Show"><svg>…</svg><span>Show</span></button>
7
+ // <button class="cf-btn" aria-label="Copy"><svg>…</svg><span>Copy</span></button></div>
7
8
  // A value to take along – a link, an address, a key (NotionKit 1.8.0). Copy
8
9
  // writes it to the clipboard and says "Copied" in green for a moment; where
9
10
  // the clipboard is not allowed, the value is shown and selected instead, for
@@ -12,8 +13,21 @@ import { NkElement } from '../../base.js';
12
13
  // lands in the markup. Fires nk-action { action: 'copy', value, ok }. The
13
14
  // texts come in English or German after the language (1.17.0, setStrings()
14
15
  // for others); copy-label, copied-label, show-label, hide-label set them here.
16
+ // Each button carries an icon beside its word (1.18.0): on a phone, and with
17
+ // `icons` anywhere, the icon stands in for the word – two overlapping squares,
18
+ // an eye, a check in green for the moment after – and a masked key keeps a
19
+ // dozen characters more. The word stays the button's name, and while the
20
+ // icons show, its tooltip for <nk-tooltip>.
21
+ const ICONS = {
22
+ copy: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M15 9V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h4"/><rect x="9" y="9" width="12" height="12" rx="2"/></svg>',
23
+ copied: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg>',
24
+ show: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7S2 12 2 12Z"/><circle cx="12" cy="12" r="3"/></svg>',
25
+ hide: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7S2 12 2 12Z"/><circle cx="12" cy="12" r="3"/><path d="m3 3 18 18"/></svg>',
26
+ };
27
+ const PHONE = typeof matchMedia === 'function' ? matchMedia('(max-width: 860px)') : null;
28
+
15
29
  class NkCopyField extends NkElement {
16
- static get observedAttributes() { return ['value', 'mono', 'wrap', 'wide', 'secret', 'copy-label', 'copied-label', 'show-label', 'hide-label']; }
30
+ static get observedAttributes() { return ['value', 'mono', 'wrap', 'wide', 'secret', 'icons', 'copy-label', 'copied-label', 'show-label', 'hide-label']; }
17
31
 
18
32
  render() {
19
33
  this._val ??= this.getAttribute('value') ?? '';
@@ -28,29 +42,41 @@ class NkCopyField extends NkElement {
28
42
 
29
43
  _sync() {
30
44
  if (!this._box) return;
31
- for (const m of ['mono', 'wrap', 'wide']) this._box.classList.toggle(m, this.getBoolAttr(m));
45
+ for (const m of ['mono', 'wrap', 'wide', 'icons']) this._box.classList.toggle(m, this.getBoolAttr(m));
32
46
  const secret = this.getBoolAttr('secret');
33
47
  this._text.textContent = secret && !this._revealed ? '•'.repeat(Math.min(this._val.length, 24)) : this._val;
34
48
  // Not `hidden`: .cf-btn sets no display of its own that could lose to it,
35
49
  // but a button that is not there cannot be tabbed to either.
36
50
  if (secret && !this._show.isConnected) this._copy.before(this._show);
37
51
  if (!secret) this._show.remove();
38
- this._show.textContent = this._revealed ? (this.getAttribute('hide-label') || this.str('hide')) : (this.getAttribute('show-label') || this.str('show'));
52
+ const tip = this.getBoolAttr('icons') || !!PHONE?.matches;
53
+ this._button(this._show, this._revealed ? 'hide' : 'show', this._revealed ? (this.getAttribute('hide-label') || this.str('hide')) : (this.getAttribute('show-label') || this.str('show')), tip);
39
54
  this._show.setAttribute('aria-pressed', String(!!this._revealed));
40
- this._copy.textContent = this._copied ? (this.getAttribute('copied-label') || this.str('copied')) : (this.getAttribute('copy-label') || this.str('copy'));
55
+ this._button(this._copy, this._copied ? 'copied' : 'copy', this._copied ? (this.getAttribute('copied-label') || this.str('copied')) : (this.getAttribute('copy-label') || this.str('copy')), tip);
41
56
  this._copy.classList.toggle('copied', !!this._copied);
42
57
  }
43
58
 
59
+ /** The icon and the word of a button; the word is its name, and its tooltip while the icon stands in for it. */
60
+ _button(btn, icon, word, tip) {
61
+ if (btn.dataset.icon !== icon) { btn.innerHTML = `${ICONS[icon]}<span></span>`; btn.dataset.icon = icon; }
62
+ btn.lastChild.textContent = word;
63
+ btn.setAttribute('aria-label', word);
64
+ if (tip) btn.setAttribute('data-tooltip', word); else btn.removeAttribute('data-tooltip');
65
+ }
66
+
44
67
  setupEvents() {
45
68
  this._onCopy = () => this.copy();
46
69
  this._onShow = () => { this._revealed = !this._revealed; this._sync(); };
70
+ this._onPhone = () => this._sync();
47
71
  this._copy.addEventListener('click', this._onCopy);
48
72
  this._show.addEventListener('click', this._onShow);
73
+ PHONE?.addEventListener('change', this._onPhone);
49
74
  }
50
75
 
51
76
  teardownEvents() {
52
77
  this._copy?.removeEventListener('click', this._onCopy);
53
78
  this._show?.removeEventListener('click', this._onShow);
79
+ PHONE?.removeEventListener('change', this._onPhone);
54
80
  clearTimeout(this._timer);
55
81
  }
56
82
 
@@ -83,6 +109,8 @@ class NkCopyField extends NkElement {
83
109
  set value(v) { this._val = v == null ? '' : String(v); this._sync(); }
84
110
  get secret() { return this.getBoolAttr('secret'); }
85
111
  set secret(v) { this.setBoolAttr('secret', v); }
112
+ get icons() { return this.getBoolAttr('icons'); }
113
+ set icons(v) { this.setBoolAttr('icons', v); }
86
114
  }
87
115
 
88
116
  customElements.define('nk-copy-field', NkCopyField);
@@ -1,5 +1,6 @@
1
1
  import { NkElement } from '../../base.js';
2
- import { lockScroll, unlockScroll, inertOutside } from '../../util/focus.js';
2
+ import { lockScroll, unlockScroll, inertOutside, deepActiveElement } from '../../util/focus.js';
3
+ import { openLayer, closeLayer } from '../../util/layers.js';
3
4
  import { fuzzyScore } from '../../util/fuzzy.js';
4
5
 
5
6
  // <nk-cmdk id="palette" placeholder="Search or type a command …"></nk-cmdk>
@@ -133,14 +134,16 @@ class NkCmdk extends NkElement {
133
134
  if (open === this._wasOpen) return;
134
135
  this._wasOpen = open;
135
136
  if (open) {
136
- this._returnFocus = document.activeElement;
137
+ this._returnFocus = deepActiveElement();
137
138
  this._input.value = '';
138
139
  this._index = 0;
139
140
  this._renderList();
140
141
  lockScroll();
141
142
  this._undoInert = inertOutside(this);
143
+ openLayer(this, () => this.close());
142
144
  requestAnimationFrame(() => this._input.focus({ preventScroll: true }));
143
145
  } else {
146
+ closeLayer(this);
144
147
  unlockScroll();
145
148
  this._undoInert?.(); this._undoInert = null;
146
149
  const back = this._returnFocus;
@@ -151,8 +154,7 @@ class NkCmdk extends NkElement {
151
154
 
152
155
  setupEvents() {
153
156
  this._onDocKey = (e) => {
154
- if (this._matchesHotkey(e)) { e.preventDefault(); this.toggle(); return; }
155
- if (e.key === 'Escape' && this.getBoolAttr('open')) { e.stopPropagation(); this.close(); }
157
+ if (this._matchesHotkey(e)) { e.preventDefault(); this.toggle(); }
156
158
  };
157
159
  this._onInput = () => { this._index = 0; this._renderList(); };
158
160
  this._onInputKey = (e) => {
@@ -181,7 +183,7 @@ class NkCmdk extends NkElement {
181
183
  this._list?.removeEventListener('mousemove', this._onListMove);
182
184
  this._list?.removeEventListener('mousedown', this._onListDown);
183
185
  this._backdrop?.removeEventListener('click', this._onBackdrop);
184
- if (this._wasOpen) { unlockScroll(); this._undoInert?.(); this._undoInert = null; this._wasOpen = false; }
186
+ if (this._wasOpen) { closeLayer(this); unlockScroll(); this._undoInert?.(); this._undoInert = null; this._wasOpen = false; }
185
187
  }
186
188
 
187
189
  onAttributeChanged(name) {
@@ -1,5 +1,6 @@
1
1
  import { NkElement } from '../../base.js';
2
2
  import { lockScroll, unlockScroll, inertOutside, firstFocusable, deepActiveElement } from '../../util/focus.js';
3
+ import { openLayer, closeLayer } from '../../util/layers.js';
3
4
 
4
5
  // <nk-dialog id="trash" title="Move “Roadmap” to trash?" alert>
5
6
  // The page and its sub-pages can be restored from Trash for 30 days.
@@ -79,8 +80,10 @@ class NkDialog extends NkElement {
79
80
  this.inert = false;
80
81
  lockScroll();
81
82
  this._undoInert = inertOutside(this);
83
+ openLayer(this, () => this.close(''));
82
84
  requestAnimationFrame(() => (this.querySelector('[autofocus]') || firstFocusable(this) || this._box).focus({ preventScroll: true }));
83
85
  } else {
86
+ closeLayer(this);
84
87
  unlockScroll();
85
88
  this._undoInert?.(); this._undoInert = null;
86
89
  this.inert = this._wasInert;
@@ -92,7 +95,6 @@ class NkDialog extends NkElement {
92
95
 
93
96
  setupEvents() {
94
97
  this._onBackdrop = (e) => { if (e.target === this._backdrop) this.close(''); };
95
- this._onKey = (e) => { if (e.key === 'Escape' && this.getBoolAttr('open')) { e.stopPropagation(); this.close(''); } };
96
98
  // A button in the actions with a value closes it, and one with data-close
97
99
  // anywhere inside; a submit button is the form's.
98
100
  this._onClick = (e) => {
@@ -112,7 +114,6 @@ class NkDialog extends NkElement {
112
114
  this.addEventListener('click', this._onClick);
113
115
  this.addEventListener('submit', this._onSubmit);
114
116
  this._actionsSlot.addEventListener('slotchange', this._onSlot);
115
- document.addEventListener('keydown', this._onKey, true);
116
117
  this._onSlot();
117
118
  }
118
119
 
@@ -121,8 +122,7 @@ class NkDialog extends NkElement {
121
122
  this.removeEventListener('click', this._onClick);
122
123
  this.removeEventListener('submit', this._onSubmit);
123
124
  this._actionsSlot?.removeEventListener('slotchange', this._onSlot);
124
- document.removeEventListener('keydown', this._onKey, true);
125
- if (this._wasOpen) { unlockScroll(); this._undoInert?.(); this._undoInert = null; this._wasOpen = false; }
125
+ if (this._wasOpen) { closeLayer(this); unlockScroll(); this._undoInert?.(); this._undoInert = null; this._wasOpen = false; }
126
126
  }
127
127
 
128
128
  onAttributeChanged(name, oldValue, value) {
@@ -1,5 +1,6 @@
1
1
  import { NkElement } from '../../base.js';
2
- import { deepActiveElement } from '../../util/focus.js';
2
+ import { deepActiveElement, containsDeep } from '../../util/focus.js';
3
+ import { openLayer, closeLayer } from '../../util/layers.js';
3
4
  import { placeUnder } from '../../util/floating.js';
4
5
 
5
6
  // <nk-menu>
@@ -48,15 +49,30 @@ class NkMenu extends NkElement {
48
49
  this._returnFocus = deepActiveElement();
49
50
  if (this._returnFocus?.matches(':focus-visible')) requestAnimationFrame(() => this.focusFirst());
50
51
  document.addEventListener('click', this._onOutside, true);
52
+ if (this.getBoolAttr('floating')) { openLayer(this, () => this.close()); this._scrollToStart(); }
51
53
  } else {
54
+ closeLayer(this);
52
55
  document.removeEventListener('click', this._onOutside, true);
53
56
  const back = this._returnFocus;
54
57
  this._returnFocus = null;
55
- if (back?.isConnected && this.contains(document.activeElement)) back.focus({ preventScroll: true });
58
+ // The focus behind shadow hosts: document.activeElement stops at the
59
+ // host of a view that holds the menu (1.19.0).
60
+ const active = deepActiveElement();
61
+ if (back?.isConnected && active && (containsDeep(this, active) || active === document.body)) back.focus({ preventScroll: true });
56
62
  }
57
63
  this.emit('nk-toggle', { open });
58
64
  }
59
65
 
66
+ // A long menu opens at its top again, or at its first checked item (1.19.0):
67
+ // it kept where it had been scrolled to, its heading out of sight.
68
+ _scrollToStart() {
69
+ this._box.scrollTop = 0;
70
+ const item = this.items.find(i => i.hasAttribute('checked'))?.shadowRoot?.querySelector('.nk-menu-item');
71
+ if (!item) return;
72
+ const box = this._box.getBoundingClientRect(), r = item.getBoundingClientRect();
73
+ if (r.bottom > box.bottom) this._box.scrollTop += r.top - box.top - (box.height - r.height) / 2;
74
+ }
75
+
60
76
  /** Opens under `anchor` (any element, display: contents hosts included). */
61
77
  show(anchor) {
62
78
  if (anchor) {
@@ -94,17 +110,14 @@ class NkMenu extends NkElement {
94
110
  e.preventDefault(); e.stopPropagation();
95
111
  this.close();
96
112
  };
97
- // Captured, so a menu inside a modal or sheet closes before they do.
98
- this._onEscape = (e) => { if (e.key === 'Escape' && this.getBoolAttr('floating') && this.getBoolAttr('open')) { e.stopPropagation(); this.close(); } };
99
113
  this._onSelect = () => { if (this.getBoolAttr('floating')) this.close(); };
100
- document.addEventListener('keydown', this._onEscape, true);
101
114
  this.addEventListener('nk-select', this._onSelect);
102
115
  if (this.getBoolAttr('open')) document.addEventListener('click', this._onOutside, true);
103
116
  }
104
117
 
105
118
  teardownEvents() {
106
119
  this.removeEventListener('keydown', this._onKey);
107
- document.removeEventListener('keydown', this._onEscape, true);
120
+ closeLayer(this);
108
121
  this.removeEventListener('nk-select', this._onSelect);
109
122
  document.removeEventListener('click', this._onOutside, true);
110
123
  }
@@ -1,5 +1,6 @@
1
1
  import { NkElement } from '../../base.js';
2
- import { lockScroll, unlockScroll, inertOutside, firstFocusable } from '../../util/focus.js';
2
+ import { lockScroll, unlockScroll, inertOutside, firstFocusable, deepActiveElement } from '../../util/focus.js';
3
+ import { openLayer, closeLayer } from '../../util/layers.js';
3
4
 
4
5
  // <nk-modal id="settings">
5
6
  // <nk-settings-user slot="user" name="…" mail="…"></nk-settings-user>
@@ -93,14 +94,17 @@ class NkModal extends NkElement {
93
94
  if (open === this._wasOpen) return;
94
95
  this._wasOpen = open;
95
96
  if (open) {
96
- this._returnFocus = document.activeElement;
97
+ // Behind shadow hosts too: a trigger in a view's shadow root gets the focus back.
98
+ this._returnFocus = deepActiveElement();
97
99
  lockScroll();
98
100
  this._undoInert = inertOutside(this);
101
+ openLayer(this, () => this.close());
99
102
  requestAnimationFrame(() => {
100
103
  const target = this._rows.querySelector('.nk-tree-item.active') || firstFocusable(this) || this._box;
101
104
  target.focus({ preventScroll: true });
102
105
  });
103
106
  } else {
107
+ closeLayer(this);
104
108
  unlockScroll();
105
109
  this._undoInert?.(); this._undoInert = null;
106
110
  const back = this._returnFocus;
@@ -111,7 +115,6 @@ class NkModal extends NkElement {
111
115
 
112
116
  setupEvents() {
113
117
  this._onBackdrop = (e) => { if (e.target === this._backdrop) this.close(); };
114
- this._onKey = (e) => { if (e.key === 'Escape' && this.getBoolAttr('open')) { e.stopPropagation(); this.close(); } };
115
118
  this._onNavClick = (e) => { const row = e.target.closest('[data-pane]'); if (row) { row.focus(); this.pane = row.dataset.pane; } };
116
119
  this._onNavKey = (e) => {
117
120
  const row = e.target.closest?.('[data-pane]');
@@ -125,7 +128,6 @@ class NkModal extends NkElement {
125
128
  };
126
129
  this._onSlot = () => this.requestNav();
127
130
  this._backdrop.addEventListener('click', this._onBackdrop);
128
- document.addEventListener('keydown', this._onKey);
129
131
  this._rows.addEventListener('click', this._onNavClick);
130
132
  this._rows.addEventListener('keydown', this._onNavKey);
131
133
  this._slot.addEventListener('slotchange', this._onSlot);
@@ -133,11 +135,10 @@ class NkModal extends NkElement {
133
135
 
134
136
  teardownEvents() {
135
137
  this._backdrop?.removeEventListener('click', this._onBackdrop);
136
- document.removeEventListener('keydown', this._onKey);
137
138
  this._rows?.removeEventListener('click', this._onNavClick);
138
139
  this._rows?.removeEventListener('keydown', this._onNavKey);
139
140
  this._slot?.removeEventListener('slotchange', this._onSlot);
140
- if (this._wasOpen) { unlockScroll(); this._undoInert?.(); this._undoInert = null; this._wasOpen = false; }
141
+ if (this._wasOpen) { closeLayer(this); unlockScroll(); this._undoInert?.(); this._undoInert = null; this._wasOpen = false; }
141
142
  }
142
143
 
143
144
  onStringsChanged() { this._nav.setAttribute('aria-label', this.str('settings')); }
@@ -1,5 +1,6 @@
1
1
  import { NkElement } from '../../base.js';
2
2
  import { lockScroll, unlockScroll, inertOutside, deepActiveElement } from '../../util/focus.js';
3
+ import { openLayer, closeLayer } from '../../util/layers.js';
3
4
 
4
5
  // <nk-peek id="peek" label="Database Table-View">
5
6
  // <nk-btn slot="actions" variant="topbar" aria-label="Open as page"><svg …></svg></nk-btn>
@@ -108,8 +109,10 @@ class NkPeek extends NkElement {
108
109
  lockScroll();
109
110
  this._undoInert = inertOutside(this);
110
111
  }
112
+ openLayer(this, () => this.close());
111
113
  requestAnimationFrame(() => this._box.focus({ preventScroll: true }));
112
114
  } else {
115
+ closeLayer(this);
113
116
  if (this._modal) { this._modal = false; this._box.removeAttribute('aria-modal'); unlockScroll(); this._undoInert?.(); this._undoInert = null; }
114
117
  const active = deepActiveElement();
115
118
  const inside = active && (this.contains(active) || this._shadow.contains(active) || active === document.body);
@@ -122,7 +125,6 @@ class NkPeek extends NkElement {
122
125
  setupEvents() {
123
126
  this._onClose = () => this.close();
124
127
  this._onBackdrop = (e) => { if (e.target === this._backdrop) this.close(); };
125
- this._onKey = (e) => { if (e.key === 'Escape' && this.getBoolAttr('open')) this.close(); };
126
128
  // Bubble phase, after the page's own handlers: a click that showed the
127
129
  // peek again (another row) must not close it.
128
130
  this._onOutside = (e) => {
@@ -156,7 +158,6 @@ class NkPeek extends NkElement {
156
158
  this._handle.addEventListener('pointermove', this._onResizeMove);
157
159
  this._handle.addEventListener('lostpointercapture', this._onResizeEnd);
158
160
  this._handle.addEventListener('keydown', this._onResizeKey);
159
- document.addEventListener('keydown', this._onKey);
160
161
  document.addEventListener('click', this._onOutside);
161
162
  addEventListener('resize', this._onViewport);
162
163
  }
@@ -168,7 +169,7 @@ class NkPeek extends NkElement {
168
169
  this._handle?.removeEventListener('pointermove', this._onResizeMove);
169
170
  this._handle?.removeEventListener('lostpointercapture', this._onResizeEnd);
170
171
  this._handle?.removeEventListener('keydown', this._onResizeKey);
171
- document.removeEventListener('keydown', this._onKey);
172
+ closeLayer(this);
172
173
  document.removeEventListener('click', this._onOutside);
173
174
  removeEventListener('resize', this._onViewport);
174
175
  if (this._insetApp) { this._app()?.removeAttribute('peek-inset'); this._insetApp = false; }
@@ -1,4 +1,5 @@
1
1
  import { NkElement } from '../../base.js';
2
+ import { openLayer, closeLayer } from '../../util/layers.js';
2
3
 
3
4
  // <nk-pop placement="bottom-start">
4
5
  // <nk-btn slot="trigger" variant="secondary">Options</nk-btn>
@@ -48,12 +49,10 @@ class NkPop extends NkElement {
48
49
  this.toggle();
49
50
  };
50
51
  this._onDocClick = (e) => { if (this.getBoolAttr('open') && !e.composedPath().includes(this)) this.close(); };
51
- this._onKey = (e) => { if (e.key === 'Escape' && this.getBoolAttr('open')) this.close(); };
52
52
  this._onSelect = (e) => { if (e.target !== this) this.close(); };
53
53
  this._onSlot = () => this._syncSurface();
54
54
  this.addEventListener('click', this._onTrigger);
55
55
  document.addEventListener('click', this._onDocClick);
56
- document.addEventListener('keydown', this._onKey);
57
56
  this._float.addEventListener('nk-select', this._onSelect);
58
57
  this._slot.addEventListener('slotchange', this._onSlot);
59
58
  }
@@ -61,7 +60,7 @@ class NkPop extends NkElement {
61
60
  teardownEvents() {
62
61
  this.removeEventListener('click', this._onTrigger);
63
62
  document.removeEventListener('click', this._onDocClick);
64
- document.removeEventListener('keydown', this._onKey);
63
+ closeLayer(this);
65
64
  this._float?.removeEventListener('nk-select', this._onSelect);
66
65
  this._slot?.removeEventListener('slotchange', this._onSlot);
67
66
  }
@@ -70,6 +69,7 @@ class NkPop extends NkElement {
70
69
  if (name === 'bare') this._syncSurface();
71
70
  if (name === 'open') {
72
71
  const open = this.getBoolAttr('open');
72
+ if (open) openLayer(this, () => this.close()); else closeLayer(this);
73
73
  for (const el of this._triggerSlot.assignedElements()) el.setAttribute('aria-expanded', open ? 'true' : 'false');
74
74
  this.emit('nk-toggle', { open });
75
75
  }
@@ -1,5 +1,6 @@
1
1
  import { NkElement } from '../../base.js';
2
2
  import { lockScroll, unlockScroll, inertOutside, firstFocusable, deepActiveElement } from '../../util/focus.js';
3
+ import { openLayer, closeLayer } from '../../util/layers.js';
3
4
 
4
5
  // <nk-sheet id="more" title="More">
5
6
  // <nk-tree manual>
@@ -52,8 +53,10 @@ class NkSheet extends NkElement {
52
53
  this._returnFocus = deepActiveElement();
53
54
  lockScroll();
54
55
  this._undoInert = inertOutside(this);
56
+ openLayer(this, () => this.close());
55
57
  requestAnimationFrame(() => (firstFocusable(this) || this._box).focus({ preventScroll: true }));
56
58
  } else {
59
+ closeLayer(this);
57
60
  unlockScroll();
58
61
  this._undoInert?.(); this._undoInert = null;
59
62
  const back = this._returnFocus;
@@ -64,15 +67,12 @@ class NkSheet extends NkElement {
64
67
 
65
68
  setupEvents() {
66
69
  this._onBackdrop = (e) => { if (e.target === this._backdrop) this.close(); };
67
- this._onKey = (e) => { if (e.key === 'Escape' && this.getBoolAttr('open')) { e.stopPropagation(); this.close(); } };
68
70
  this._backdrop.addEventListener('click', this._onBackdrop);
69
- document.addEventListener('keydown', this._onKey);
70
71
  }
71
72
 
72
73
  teardownEvents() {
73
74
  this._backdrop?.removeEventListener('click', this._onBackdrop);
74
- document.removeEventListener('keydown', this._onKey);
75
- if (this._wasOpen) { unlockScroll(); this._undoInert?.(); this._undoInert = null; this._wasOpen = false; }
75
+ if (this._wasOpen) { closeLayer(this); unlockScroll(); this._undoInert?.(); this._undoInert = null; this._wasOpen = false; }
76
76
  }
77
77
 
78
78
  onAttributeChanged(name, oldValue, value) {