@jungherz-de/notionkit-elements 1.18.0 → 1.20.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 +48 -0
- package/README.md +11 -7
- package/SKILL.md +42 -37
- package/dist/components/base.js +2 -0
- package/dist/components/nk-calendar.js +6 -5
- package/dist/components/nk-cmdk.js +7 -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-sheet.js +5 -5
- package/dist/components/nk-sidebar.js +1 -1
- package/dist/components/nk-theme-toggle.js +163 -22
- 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 +357 -69
- package/dist/notionkit-elements.esm.min.js +1 -1
- package/dist/notionkit-elements.esm.min.js.map +1 -1
- package/dist/notionkit-elements.js +357 -69
- package/dist/notionkit-elements.min.js +1 -1
- package/package.json +3 -3
- package/src/components/forms/nk-calendar.js +5 -4
- 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-theme-toggle.js +163 -22
- package/src/util/focus.js +6 -0
- package/src/util/layers.js +33 -0
- package/src/util/strings.js +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jungherz-de/notionkit-elements",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.20.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.20.0"
|
|
67
67
|
},
|
|
68
68
|
"devDependencies": {
|
|
69
|
-
"@jungherz-de/notionkit": "^1.
|
|
69
|
+
"@jungherz-de/notionkit": "^1.20.0",
|
|
70
70
|
"@playwright/test": "^1.50.0",
|
|
71
71
|
"@rollup/plugin-node-resolve": "^15.0.0",
|
|
72
72
|
"@rollup/plugin-terser": "^0.4.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;
|
|
@@ -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) {
|
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
import { NkElement } from '../../base.js';
|
|
2
|
+
import { deepActiveElement, containsDeep } from '../../util/focus.js';
|
|
2
3
|
|
|
3
4
|
// <nk-toast id="toast"></nk-toast> … toast.show('Saved');
|
|
4
|
-
//
|
|
5
|
+
// toast.show('Moved to trash', { action: { label: 'Undo', value: 'undo' } });
|
|
6
|
+
// → <div class="nk-toast show">✓ Moved to trash<button class="t-action">Undo</button><button class="t-close">×</button></div>
|
|
5
7
|
// One node, bottom-centred, auto-hides after `duration` ms (default 2200).
|
|
6
8
|
// Above a tab bar at the bottom of the screen it rises 12px over the bar:
|
|
7
9
|
// notionkit.css does that for class markup with :has(), which cannot see
|
|
8
10
|
// into shadow roots, so the element measures the bar when it opens.
|
|
11
|
+
// An action (1.19.0) – from show() or a <button slot="action"> – makes it
|
|
12
|
+
// Notion's "Moved to trash · Undo": a button, and an × that closes. Such a
|
|
13
|
+
// toast stays until one of them is used, unless show() names a duration,
|
|
14
|
+
// and waits while the pointer or the focus is on it. The action fires
|
|
15
|
+
// nk-action { action, value, label } and, unless cancelled, closes it;
|
|
16
|
+
// Escape closes it while the focus is inside.
|
|
17
|
+
const CLOSE_ICON = '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12"/></svg>';
|
|
18
|
+
|
|
9
19
|
class NkToast extends NkElement {
|
|
10
20
|
static get observedAttributes() { return ['open', 'duration', 'icon']; }
|
|
11
21
|
|
|
@@ -13,19 +23,37 @@ class NkToast extends NkElement {
|
|
|
13
23
|
this._box = this.createElement('div', ['nk-toast'], { role: 'status', 'aria-live': 'polite' });
|
|
14
24
|
this._icon = document.createElement('span');
|
|
15
25
|
this._msg = document.createElement('span');
|
|
16
|
-
this.
|
|
26
|
+
this._action = this.createElement('button', ['t-action'], { type: 'button' });
|
|
27
|
+
this._actionSlot = this.createElement('slot', [], { name: 'action' });
|
|
28
|
+
this._close = this.createElement('button', ['t-close'], { type: 'button' });
|
|
29
|
+
this._close.innerHTML = CLOSE_ICON;
|
|
30
|
+
this._box.append(this._icon, this._msg, document.createElement('slot'), this._actionSlot);
|
|
17
31
|
this._wrapper.appendChild(this._box);
|
|
18
32
|
this._sync();
|
|
19
33
|
}
|
|
20
34
|
|
|
21
35
|
_sync() {
|
|
22
|
-
|
|
23
|
-
|
|
36
|
+
const open = this.getBoolAttr('open');
|
|
37
|
+
if (open) this._lift();
|
|
38
|
+
this._box.classList.toggle('show', open);
|
|
24
39
|
const icon = this.getAttribute('icon') ?? '✓';
|
|
25
40
|
this._icon.textContent = icon;
|
|
26
41
|
this._icon.style.display = icon ? '' : 'none';
|
|
42
|
+
// An empty message takes no place – with slotted text it added a second gap.
|
|
43
|
+
this._msg.style.display = this._msg.textContent ? '' : 'none';
|
|
44
|
+
// The action from show() and the × sit in the tree only when there is an
|
|
45
|
+
// action; a closed toast keeps them for its fade, out of the tab order.
|
|
46
|
+
const own = this._act, slotted = this._slotted();
|
|
47
|
+
if (own) { this._action.textContent = own.label; if (!this._action.isConnected) this._actionSlot.before(this._action); }
|
|
48
|
+
else this._action.remove();
|
|
49
|
+
if (own || slotted) { if (!this._close.isConnected) this._box.appendChild(this._close); }
|
|
50
|
+
else this._close.remove();
|
|
51
|
+
this._close.setAttribute('aria-label', this.str('close'));
|
|
52
|
+
for (const b of [this._action, this._close]) open ? b.removeAttribute('tabindex') : b.setAttribute('tabindex', '-1');
|
|
27
53
|
}
|
|
28
54
|
|
|
55
|
+
_slotted() { return this._actionSlot?.assignedElements()[0] ?? null; }
|
|
56
|
+
|
|
29
57
|
// A bar counts when it is rendered and ends near the bottom of the viewport –
|
|
30
58
|
// the app's own bar, fixed, sticky or floating. A tab bar shown as a preview
|
|
31
59
|
// somewhere in a page does not lift the toast.
|
|
@@ -40,16 +68,83 @@ class NkToast extends NkElement {
|
|
|
40
68
|
this._box.style.bottom = rect ? `${Math.round(innerHeight - rect.top + 12)}px` : '';
|
|
41
69
|
}
|
|
42
70
|
|
|
43
|
-
/**
|
|
44
|
-
|
|
71
|
+
/**
|
|
72
|
+
* Shows `message` (or the slotted content when omitted) and hides it again
|
|
73
|
+
* after `duration` ms – the attribute's, else 2200; with an `action`
|
|
74
|
+
* ({ label, value }) it stays until the action or the × is used.
|
|
75
|
+
*/
|
|
76
|
+
show(message, { duration, action } = {}) {
|
|
45
77
|
if (message !== undefined) this._msg.textContent = message;
|
|
78
|
+
this._shown = (this._shown ?? 0) + 1;
|
|
79
|
+
this._act = action ? { label: String(action.label ?? action.value ?? ''), value: action.value ?? action.label } : null;
|
|
80
|
+
const attr = this.getAttribute('duration'), set = attr !== null && attr.trim() !== '' && Number.isFinite(Number(attr));
|
|
81
|
+
// Number(null) is 0: without the attribute a toast used to stay for good.
|
|
82
|
+
this._ms = duration ?? (this._act || this._slotted() ? 0 : set ? Number(attr) : 2200);
|
|
83
|
+
if (this.getBoolAttr('open')) this._sync(); else this.setBoolAttr('open', true);
|
|
84
|
+
this._arm();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
close() {
|
|
46
88
|
clearTimeout(this._timer);
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
if (
|
|
89
|
+
// The focus leaves with the toast, back to where it came from.
|
|
90
|
+
const active = deepActiveElement();
|
|
91
|
+
if (active && containsDeep(this, active)) {
|
|
92
|
+
this._from?.isConnected && this._from.focus({ preventScroll: true });
|
|
93
|
+
if (containsDeep(this, deepActiveElement())) active.blur();
|
|
94
|
+
}
|
|
95
|
+
this._from = null;
|
|
96
|
+
this.setBoolAttr('open', false);
|
|
50
97
|
}
|
|
51
98
|
|
|
52
|
-
|
|
99
|
+
_arm() {
|
|
100
|
+
clearTimeout(this._timer);
|
|
101
|
+
if (this._ms > 0 && !this._held && this.getBoolAttr('open')) this._timer = setTimeout(() => this.close(), this._ms);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
_take(act) {
|
|
105
|
+
if (!act) return;
|
|
106
|
+
// A handler that shows the next message – "Restored" – keeps it open.
|
|
107
|
+
const shown = this._shown;
|
|
108
|
+
if (this.emit('nk-action', { action: act.value, value: act.value, label: act.label }) && this._shown === shown) this.close();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
setupEvents() {
|
|
112
|
+
this._onClick = (e) => {
|
|
113
|
+
const path = e.composedPath();
|
|
114
|
+
if (path.includes(this._close)) { this.close(); return; }
|
|
115
|
+
if (path.includes(this._action)) { this._take(this._act); return; }
|
|
116
|
+
const slotted = this._slotted();
|
|
117
|
+
if (slotted && path.includes(slotted)) this._take({ value: slotted.value || slotted.getAttribute('value') || slotted.textContent.trim(), label: slotted.textContent.trim() });
|
|
118
|
+
};
|
|
119
|
+
// While the pointer or the focus is on it, a toast with a duration waits.
|
|
120
|
+
this._onHold = (e) => {
|
|
121
|
+
if (e.type === 'focusin' && !this._from) this._from = e.relatedTarget ?? null;
|
|
122
|
+
this._held = true; clearTimeout(this._timer);
|
|
123
|
+
};
|
|
124
|
+
this._onRelease = (e) => {
|
|
125
|
+
if (e.relatedTarget && containsDeep(this, e.relatedTarget)) return;
|
|
126
|
+
this._held = false; this._arm();
|
|
127
|
+
};
|
|
128
|
+
this._onKey = (e) => { if (e.key === 'Escape') { e.stopPropagation(); this.close(); } };
|
|
129
|
+
this._onSlot = () => this._sync();
|
|
130
|
+
this._box.addEventListener('click', this._onClick);
|
|
131
|
+
this._box.addEventListener('pointerover', this._onHold);
|
|
132
|
+
this._box.addEventListener('pointerout', this._onRelease);
|
|
133
|
+
this._box.addEventListener('focusin', this._onHold);
|
|
134
|
+
this._box.addEventListener('focusout', this._onRelease);
|
|
135
|
+
this._box.addEventListener('keydown', this._onKey);
|
|
136
|
+
this._actionSlot.addEventListener('slotchange', this._onSlot);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
teardownEvents() {
|
|
140
|
+
this._box?.removeEventListener('click', this._onClick);
|
|
141
|
+
this._box?.removeEventListener('pointerover', this._onHold);
|
|
142
|
+
this._box?.removeEventListener('pointerout', this._onRelease);
|
|
143
|
+
this._box?.removeEventListener('focusin', this._onHold);
|
|
144
|
+
this._box?.removeEventListener('focusout', this._onRelease);
|
|
145
|
+
this._box?.removeEventListener('keydown', this._onKey);
|
|
146
|
+
this._actionSlot?.removeEventListener('slotchange', this._onSlot);
|
|
147
|
+
}
|
|
53
148
|
|
|
54
149
|
onAttributeChanged(name) {
|
|
55
150
|
this._sync();
|
|
@@ -61,7 +156,8 @@ class NkToast extends NkElement {
|
|
|
61
156
|
get open() { return this.getBoolAttr('open'); }
|
|
62
157
|
set open(v) { this.setBoolAttr('open', v); }
|
|
63
158
|
get message() { return this._msg?.textContent ?? ''; }
|
|
64
|
-
set message(v) { if (this._msg) this._msg.textContent = v; }
|
|
159
|
+
set message(v) { if (this._msg) { this._msg.textContent = v; this._sync(); } }
|
|
160
|
+
onStringsChanged() { if (this._box) this._sync(); }
|
|
65
161
|
}
|
|
66
162
|
|
|
67
163
|
customElements.define('nk-toast', NkToast);
|