@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.
- package/CHANGELOG.md +30 -0
- package/README.md +7 -7
- package/SKILL.md +53 -42
- package/dist/components/nk-calendar.js +6 -5
- package/dist/components/nk-cmdk.js +7 -5
- package/dist/components/nk-copy-field.js +33 -5
- package/dist/components/nk-dialog.js +5 -5
- package/dist/components/nk-menu.js +19 -6
- package/dist/components/nk-modal.js +7 -6
- package/dist/components/nk-peek.js +5 -4
- package/dist/components/nk-pop.js +3 -3
- package/dist/components/nk-progress.js +6 -3
- package/dist/components/nk-sheet.js +5 -5
- package/dist/components/nk-sidebar.js +1 -1
- package/dist/components/nk-tab-bar.js +10 -4
- package/dist/components/nk-toast.js +107 -11
- package/dist/components/nk-tooltip.js +1 -1
- package/dist/components/shared/{focus-C4tbSNND.js → focus-D55JGjRA.js} +7 -1
- package/dist/components/shared/layers-D9YYYA5g.js +35 -0
- package/dist/notionkit-elements.esm.js +241 -59
- package/dist/notionkit-elements.esm.min.js +1 -1
- package/dist/notionkit-elements.esm.min.js.map +1 -1
- package/dist/notionkit-elements.js +241 -59
- package/dist/notionkit-elements.min.js +1 -1
- package/package.json +3 -3
- package/src/components/content/nk-progress.js +6 -3
- package/src/components/forms/nk-calendar.js +5 -4
- package/src/components/forms/nk-copy-field.js +33 -5
- package/src/components/overlays/nk-cmdk.js +7 -5
- package/src/components/overlays/nk-dialog.js +4 -4
- package/src/components/overlays/nk-menu.js +19 -6
- package/src/components/overlays/nk-modal.js +7 -6
- package/src/components/overlays/nk-peek.js +4 -3
- package/src/components/overlays/nk-pop.js +3 -3
- package/src/components/overlays/nk-sheet.js +4 -4
- package/src/components/overlays/nk-toast.js +107 -11
- package/src/components/shell/nk-tab-bar.js +10 -4
- package/src/util/focus.js +6 -0
- 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.
|
|
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.
|
|
66
|
+
"@jungherz-de/notionkit": ">=1.19.1"
|
|
67
67
|
},
|
|
68
68
|
"devDependencies": {
|
|
69
|
-
"@jungherz-de/notionkit": "^1.
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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"
|
|
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
|
-
|
|
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.
|
|
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 =
|
|
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();
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|