@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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { NkElement } from './base.js';
|
|
2
|
-
import { d as deepActiveElement, l as lockScroll, i as inertOutside, f as firstFocusable, u as unlockScroll } from './shared/focus-
|
|
2
|
+
import { d as deepActiveElement, l as lockScroll, i as inertOutside, f as firstFocusable, u as unlockScroll } from './shared/focus-D55JGjRA.js';
|
|
3
|
+
import { o as openLayer, c as closeLayer } from './shared/layers-D9YYYA5g.js';
|
|
3
4
|
import '@jungherz-de/notionkit/notionkit-styles.js';
|
|
4
5
|
|
|
5
6
|
// <nk-dialog id="trash" title="Move “Roadmap” to trash?" alert>
|
|
@@ -80,8 +81,10 @@ class NkDialog extends NkElement {
|
|
|
80
81
|
this.inert = false;
|
|
81
82
|
lockScroll();
|
|
82
83
|
this._undoInert = inertOutside(this);
|
|
84
|
+
openLayer(this, () => this.close(''));
|
|
83
85
|
requestAnimationFrame(() => (this.querySelector('[autofocus]') || firstFocusable(this) || this._box).focus({ preventScroll: true }));
|
|
84
86
|
} else {
|
|
87
|
+
closeLayer(this);
|
|
85
88
|
unlockScroll();
|
|
86
89
|
this._undoInert?.(); this._undoInert = null;
|
|
87
90
|
this.inert = this._wasInert;
|
|
@@ -93,7 +96,6 @@ class NkDialog extends NkElement {
|
|
|
93
96
|
|
|
94
97
|
setupEvents() {
|
|
95
98
|
this._onBackdrop = (e) => { if (e.target === this._backdrop) this.close(''); };
|
|
96
|
-
this._onKey = (e) => { if (e.key === 'Escape' && this.getBoolAttr('open')) { e.stopPropagation(); this.close(''); } };
|
|
97
99
|
// A button in the actions with a value closes it, and one with data-close
|
|
98
100
|
// anywhere inside; a submit button is the form's.
|
|
99
101
|
this._onClick = (e) => {
|
|
@@ -113,7 +115,6 @@ class NkDialog extends NkElement {
|
|
|
113
115
|
this.addEventListener('click', this._onClick);
|
|
114
116
|
this.addEventListener('submit', this._onSubmit);
|
|
115
117
|
this._actionsSlot.addEventListener('slotchange', this._onSlot);
|
|
116
|
-
document.addEventListener('keydown', this._onKey, true);
|
|
117
118
|
this._onSlot();
|
|
118
119
|
}
|
|
119
120
|
|
|
@@ -122,8 +123,7 @@ class NkDialog extends NkElement {
|
|
|
122
123
|
this.removeEventListener('click', this._onClick);
|
|
123
124
|
this.removeEventListener('submit', this._onSubmit);
|
|
124
125
|
this._actionsSlot?.removeEventListener('slotchange', this._onSlot);
|
|
125
|
-
|
|
126
|
-
if (this._wasOpen) { unlockScroll(); this._undoInert?.(); this._undoInert = null; this._wasOpen = false; }
|
|
126
|
+
if (this._wasOpen) { closeLayer(this); unlockScroll(); this._undoInert?.(); this._undoInert = null; this._wasOpen = false; }
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
onAttributeChanged(name, oldValue, value) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { NkElement } from './base.js';
|
|
2
|
-
import { d as deepActiveElement } from './shared/focus-
|
|
2
|
+
import { d as deepActiveElement, c as containsDeep } from './shared/focus-D55JGjRA.js';
|
|
3
|
+
import { o as openLayer, c as closeLayer } from './shared/layers-D9YYYA5g.js';
|
|
3
4
|
import { p as placeUnder } from './shared/floating-DOK73b_D.js';
|
|
4
5
|
import '@jungherz-de/notionkit/notionkit-styles.js';
|
|
5
6
|
|
|
@@ -49,15 +50,30 @@ class NkMenu extends NkElement {
|
|
|
49
50
|
this._returnFocus = deepActiveElement();
|
|
50
51
|
if (this._returnFocus?.matches(':focus-visible')) requestAnimationFrame(() => this.focusFirst());
|
|
51
52
|
document.addEventListener('click', this._onOutside, true);
|
|
53
|
+
if (this.getBoolAttr('floating')) { openLayer(this, () => this.close()); this._scrollToStart(); }
|
|
52
54
|
} else {
|
|
55
|
+
closeLayer(this);
|
|
53
56
|
document.removeEventListener('click', this._onOutside, true);
|
|
54
57
|
const back = this._returnFocus;
|
|
55
58
|
this._returnFocus = null;
|
|
56
|
-
|
|
59
|
+
// The focus behind shadow hosts: document.activeElement stops at the
|
|
60
|
+
// host of a view that holds the menu (1.19.0).
|
|
61
|
+
const active = deepActiveElement();
|
|
62
|
+
if (back?.isConnected && active && (containsDeep(this, active) || active === document.body)) back.focus({ preventScroll: true });
|
|
57
63
|
}
|
|
58
64
|
this.emit('nk-toggle', { open });
|
|
59
65
|
}
|
|
60
66
|
|
|
67
|
+
// A long menu opens at its top again, or at its first checked item (1.19.0):
|
|
68
|
+
// it kept where it had been scrolled to, its heading out of sight.
|
|
69
|
+
_scrollToStart() {
|
|
70
|
+
this._box.scrollTop = 0;
|
|
71
|
+
const item = this.items.find(i => i.hasAttribute('checked'))?.shadowRoot?.querySelector('.nk-menu-item');
|
|
72
|
+
if (!item) return;
|
|
73
|
+
const box = this._box.getBoundingClientRect(), r = item.getBoundingClientRect();
|
|
74
|
+
if (r.bottom > box.bottom) this._box.scrollTop += r.top - box.top - (box.height - r.height) / 2;
|
|
75
|
+
}
|
|
76
|
+
|
|
61
77
|
/** Opens under `anchor` (any element, display: contents hosts included). */
|
|
62
78
|
show(anchor) {
|
|
63
79
|
if (anchor) {
|
|
@@ -95,17 +111,14 @@ class NkMenu extends NkElement {
|
|
|
95
111
|
e.preventDefault(); e.stopPropagation();
|
|
96
112
|
this.close();
|
|
97
113
|
};
|
|
98
|
-
// Captured, so a menu inside a modal or sheet closes before they do.
|
|
99
|
-
this._onEscape = (e) => { if (e.key === 'Escape' && this.getBoolAttr('floating') && this.getBoolAttr('open')) { e.stopPropagation(); this.close(); } };
|
|
100
114
|
this._onSelect = () => { if (this.getBoolAttr('floating')) this.close(); };
|
|
101
|
-
document.addEventListener('keydown', this._onEscape, true);
|
|
102
115
|
this.addEventListener('nk-select', this._onSelect);
|
|
103
116
|
if (this.getBoolAttr('open')) document.addEventListener('click', this._onOutside, true);
|
|
104
117
|
}
|
|
105
118
|
|
|
106
119
|
teardownEvents() {
|
|
107
120
|
this.removeEventListener('keydown', this._onKey);
|
|
108
|
-
|
|
121
|
+
closeLayer(this);
|
|
109
122
|
this.removeEventListener('nk-select', this._onSelect);
|
|
110
123
|
document.removeEventListener('click', this._onOutside, true);
|
|
111
124
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { NkElement } from './base.js';
|
|
2
|
-
import { l as lockScroll, i as inertOutside, f as firstFocusable, u as unlockScroll } from './shared/focus-
|
|
2
|
+
import { d as deepActiveElement, l as lockScroll, i as inertOutside, f as firstFocusable, u as unlockScroll } from './shared/focus-D55JGjRA.js';
|
|
3
|
+
import { o as openLayer, c as closeLayer } from './shared/layers-D9YYYA5g.js';
|
|
3
4
|
import '@jungherz-de/notionkit/notionkit-styles.js';
|
|
4
5
|
|
|
5
6
|
// <nk-modal id="settings">
|
|
@@ -94,14 +95,17 @@ class NkModal extends NkElement {
|
|
|
94
95
|
if (open === this._wasOpen) return;
|
|
95
96
|
this._wasOpen = open;
|
|
96
97
|
if (open) {
|
|
97
|
-
|
|
98
|
+
// Behind shadow hosts too: a trigger in a view's shadow root gets the focus back.
|
|
99
|
+
this._returnFocus = deepActiveElement();
|
|
98
100
|
lockScroll();
|
|
99
101
|
this._undoInert = inertOutside(this);
|
|
102
|
+
openLayer(this, () => this.close());
|
|
100
103
|
requestAnimationFrame(() => {
|
|
101
104
|
const target = this._rows.querySelector('.nk-tree-item.active') || firstFocusable(this) || this._box;
|
|
102
105
|
target.focus({ preventScroll: true });
|
|
103
106
|
});
|
|
104
107
|
} else {
|
|
108
|
+
closeLayer(this);
|
|
105
109
|
unlockScroll();
|
|
106
110
|
this._undoInert?.(); this._undoInert = null;
|
|
107
111
|
const back = this._returnFocus;
|
|
@@ -112,7 +116,6 @@ class NkModal extends NkElement {
|
|
|
112
116
|
|
|
113
117
|
setupEvents() {
|
|
114
118
|
this._onBackdrop = (e) => { if (e.target === this._backdrop) this.close(); };
|
|
115
|
-
this._onKey = (e) => { if (e.key === 'Escape' && this.getBoolAttr('open')) { e.stopPropagation(); this.close(); } };
|
|
116
119
|
this._onNavClick = (e) => { const row = e.target.closest('[data-pane]'); if (row) { row.focus(); this.pane = row.dataset.pane; } };
|
|
117
120
|
this._onNavKey = (e) => {
|
|
118
121
|
const row = e.target.closest?.('[data-pane]');
|
|
@@ -126,7 +129,6 @@ class NkModal extends NkElement {
|
|
|
126
129
|
};
|
|
127
130
|
this._onSlot = () => this.requestNav();
|
|
128
131
|
this._backdrop.addEventListener('click', this._onBackdrop);
|
|
129
|
-
document.addEventListener('keydown', this._onKey);
|
|
130
132
|
this._rows.addEventListener('click', this._onNavClick);
|
|
131
133
|
this._rows.addEventListener('keydown', this._onNavKey);
|
|
132
134
|
this._slot.addEventListener('slotchange', this._onSlot);
|
|
@@ -134,11 +136,10 @@ class NkModal extends NkElement {
|
|
|
134
136
|
|
|
135
137
|
teardownEvents() {
|
|
136
138
|
this._backdrop?.removeEventListener('click', this._onBackdrop);
|
|
137
|
-
document.removeEventListener('keydown', this._onKey);
|
|
138
139
|
this._rows?.removeEventListener('click', this._onNavClick);
|
|
139
140
|
this._rows?.removeEventListener('keydown', this._onNavKey);
|
|
140
141
|
this._slot?.removeEventListener('slotchange', this._onSlot);
|
|
141
|
-
if (this._wasOpen) { unlockScroll(); this._undoInert?.(); this._undoInert = null; this._wasOpen = false; }
|
|
142
|
+
if (this._wasOpen) { closeLayer(this); unlockScroll(); this._undoInert?.(); this._undoInert = null; this._wasOpen = false; }
|
|
142
143
|
}
|
|
143
144
|
|
|
144
145
|
onStringsChanged() { this._nav.setAttribute('aria-label', this.str('settings')); }
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { NkElement } from './base.js';
|
|
2
|
-
import { d as deepActiveElement, l as lockScroll, i as inertOutside, u as unlockScroll } from './shared/focus-
|
|
2
|
+
import { d as deepActiveElement, l as lockScroll, i as inertOutside, u as unlockScroll } from './shared/focus-D55JGjRA.js';
|
|
3
|
+
import { o as openLayer, c as closeLayer } from './shared/layers-D9YYYA5g.js';
|
|
3
4
|
import '@jungherz-de/notionkit/notionkit-styles.js';
|
|
4
5
|
|
|
5
6
|
// <nk-peek id="peek" label="Database Table-View">
|
|
@@ -109,8 +110,10 @@ class NkPeek extends NkElement {
|
|
|
109
110
|
lockScroll();
|
|
110
111
|
this._undoInert = inertOutside(this);
|
|
111
112
|
}
|
|
113
|
+
openLayer(this, () => this.close());
|
|
112
114
|
requestAnimationFrame(() => this._box.focus({ preventScroll: true }));
|
|
113
115
|
} else {
|
|
116
|
+
closeLayer(this);
|
|
114
117
|
if (this._modal) { this._modal = false; this._box.removeAttribute('aria-modal'); unlockScroll(); this._undoInert?.(); this._undoInert = null; }
|
|
115
118
|
const active = deepActiveElement();
|
|
116
119
|
const inside = active && (this.contains(active) || this._shadow.contains(active) || active === document.body);
|
|
@@ -123,7 +126,6 @@ class NkPeek extends NkElement {
|
|
|
123
126
|
setupEvents() {
|
|
124
127
|
this._onClose = () => this.close();
|
|
125
128
|
this._onBackdrop = (e) => { if (e.target === this._backdrop) this.close(); };
|
|
126
|
-
this._onKey = (e) => { if (e.key === 'Escape' && this.getBoolAttr('open')) this.close(); };
|
|
127
129
|
// Bubble phase, after the page's own handlers: a click that showed the
|
|
128
130
|
// peek again (another row) must not close it.
|
|
129
131
|
this._onOutside = (e) => {
|
|
@@ -157,7 +159,6 @@ class NkPeek extends NkElement {
|
|
|
157
159
|
this._handle.addEventListener('pointermove', this._onResizeMove);
|
|
158
160
|
this._handle.addEventListener('lostpointercapture', this._onResizeEnd);
|
|
159
161
|
this._handle.addEventListener('keydown', this._onResizeKey);
|
|
160
|
-
document.addEventListener('keydown', this._onKey);
|
|
161
162
|
document.addEventListener('click', this._onOutside);
|
|
162
163
|
addEventListener('resize', this._onViewport);
|
|
163
164
|
}
|
|
@@ -169,7 +170,7 @@ class NkPeek extends NkElement {
|
|
|
169
170
|
this._handle?.removeEventListener('pointermove', this._onResizeMove);
|
|
170
171
|
this._handle?.removeEventListener('lostpointercapture', this._onResizeEnd);
|
|
171
172
|
this._handle?.removeEventListener('keydown', this._onResizeKey);
|
|
172
|
-
|
|
173
|
+
closeLayer(this);
|
|
173
174
|
document.removeEventListener('click', this._onOutside);
|
|
174
175
|
removeEventListener('resize', this._onViewport);
|
|
175
176
|
if (this._insetApp) { this._app()?.removeAttribute('peek-inset'); this._insetApp = false; }
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { NkElement } from './base.js';
|
|
2
|
+
import { c as closeLayer, o as openLayer } from './shared/layers-D9YYYA5g.js';
|
|
2
3
|
import '@jungherz-de/notionkit/notionkit-styles.js';
|
|
3
4
|
|
|
4
5
|
// <nk-pop placement="bottom-start">
|
|
@@ -49,12 +50,10 @@ class NkPop extends NkElement {
|
|
|
49
50
|
this.toggle();
|
|
50
51
|
};
|
|
51
52
|
this._onDocClick = (e) => { if (this.getBoolAttr('open') && !e.composedPath().includes(this)) this.close(); };
|
|
52
|
-
this._onKey = (e) => { if (e.key === 'Escape' && this.getBoolAttr('open')) this.close(); };
|
|
53
53
|
this._onSelect = (e) => { if (e.target !== this) this.close(); };
|
|
54
54
|
this._onSlot = () => this._syncSurface();
|
|
55
55
|
this.addEventListener('click', this._onTrigger);
|
|
56
56
|
document.addEventListener('click', this._onDocClick);
|
|
57
|
-
document.addEventListener('keydown', this._onKey);
|
|
58
57
|
this._float.addEventListener('nk-select', this._onSelect);
|
|
59
58
|
this._slot.addEventListener('slotchange', this._onSlot);
|
|
60
59
|
}
|
|
@@ -62,7 +61,7 @@ class NkPop extends NkElement {
|
|
|
62
61
|
teardownEvents() {
|
|
63
62
|
this.removeEventListener('click', this._onTrigger);
|
|
64
63
|
document.removeEventListener('click', this._onDocClick);
|
|
65
|
-
|
|
64
|
+
closeLayer(this);
|
|
66
65
|
this._float?.removeEventListener('nk-select', this._onSelect);
|
|
67
66
|
this._slot?.removeEventListener('slotchange', this._onSlot);
|
|
68
67
|
}
|
|
@@ -71,6 +70,7 @@ class NkPop extends NkElement {
|
|
|
71
70
|
if (name === 'bare') this._syncSurface();
|
|
72
71
|
if (name === 'open') {
|
|
73
72
|
const open = this.getBoolAttr('open');
|
|
73
|
+
if (open) openLayer(this, () => this.close()); else closeLayer(this);
|
|
74
74
|
for (const el of this._triggerSlot.assignedElements()) el.setAttribute('aria-expanded', open ? 'true' : 'false');
|
|
75
75
|
this.emit('nk-toggle', { open });
|
|
76
76
|
}
|
|
@@ -4,6 +4,9 @@ import '@jungherz-de/notionkit/notionkit-styles.js';
|
|
|
4
4
|
// <nk-progress value="72" label="72%"></nk-progress>
|
|
5
5
|
// <nk-progress value="72" label="72 %" wide></nk-progress> → fills its row, the label stays beside it
|
|
6
6
|
// The bar is `.nk-progress > i` — a child combinator, so both nodes live here.
|
|
7
|
+
// With a label the two form one .nk-progress-row (1.18.0): the label beside
|
|
8
|
+
// the bar in a flex column too – a panel – where the host, display: contents,
|
|
9
|
+
// would let them stand one under the other.
|
|
7
10
|
class NkProgress extends NkElement {
|
|
8
11
|
static get observedAttributes() { return ['value', 'max', 'label', 'wide']; }
|
|
9
12
|
|
|
@@ -12,8 +15,7 @@ class NkProgress extends NkElement {
|
|
|
12
15
|
this._fill = document.createElement('i');
|
|
13
16
|
this._bar.appendChild(this._fill);
|
|
14
17
|
this._label = this.createElement('span', ['nk-progress-label']);
|
|
15
|
-
this.
|
|
16
|
-
this._wrapper.appendChild(this._label);
|
|
18
|
+
this._row = this.createElement('span', ['nk-progress-row']);
|
|
17
19
|
this._sync();
|
|
18
20
|
}
|
|
19
21
|
|
|
@@ -30,7 +32,8 @@ class NkProgress extends NkElement {
|
|
|
30
32
|
this._bar.setAttribute('aria-valuemax', max);
|
|
31
33
|
const label = this.getAttribute('label');
|
|
32
34
|
this._label.textContent = label ?? '';
|
|
33
|
-
|
|
35
|
+
if (label === null) { if (this._bar.parentNode !== this._wrapper) this._wrapper.replaceChildren(this._bar); }
|
|
36
|
+
else if (this._bar.parentNode !== this._row) { this._row.replaceChildren(this._bar, this._label); this._wrapper.replaceChildren(this._row); }
|
|
34
37
|
}
|
|
35
38
|
|
|
36
39
|
get value() { return Number(this.getAttribute('value')) || 0; }
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { NkElement } from './base.js';
|
|
2
|
-
import { d as deepActiveElement, l as lockScroll, i as inertOutside, f as firstFocusable, u as unlockScroll } from './shared/focus-
|
|
2
|
+
import { d as deepActiveElement, l as lockScroll, i as inertOutside, f as firstFocusable, u as unlockScroll } from './shared/focus-D55JGjRA.js';
|
|
3
|
+
import { o as openLayer, c as closeLayer } from './shared/layers-D9YYYA5g.js';
|
|
3
4
|
import '@jungherz-de/notionkit/notionkit-styles.js';
|
|
4
5
|
|
|
5
6
|
// <nk-sheet id="more" title="More">
|
|
@@ -53,8 +54,10 @@ class NkSheet extends NkElement {
|
|
|
53
54
|
this._returnFocus = deepActiveElement();
|
|
54
55
|
lockScroll();
|
|
55
56
|
this._undoInert = inertOutside(this);
|
|
57
|
+
openLayer(this, () => this.close());
|
|
56
58
|
requestAnimationFrame(() => (firstFocusable(this) || this._box).focus({ preventScroll: true }));
|
|
57
59
|
} else {
|
|
60
|
+
closeLayer(this);
|
|
58
61
|
unlockScroll();
|
|
59
62
|
this._undoInert?.(); this._undoInert = null;
|
|
60
63
|
const back = this._returnFocus;
|
|
@@ -65,15 +68,12 @@ class NkSheet extends NkElement {
|
|
|
65
68
|
|
|
66
69
|
setupEvents() {
|
|
67
70
|
this._onBackdrop = (e) => { if (e.target === this._backdrop) this.close(); };
|
|
68
|
-
this._onKey = (e) => { if (e.key === 'Escape' && this.getBoolAttr('open')) { e.stopPropagation(); this.close(); } };
|
|
69
71
|
this._backdrop.addEventListener('click', this._onBackdrop);
|
|
70
|
-
document.addEventListener('keydown', this._onKey);
|
|
71
72
|
}
|
|
72
73
|
|
|
73
74
|
teardownEvents() {
|
|
74
75
|
this._backdrop?.removeEventListener('click', this._onBackdrop);
|
|
75
|
-
|
|
76
|
-
if (this._wasOpen) { unlockScroll(); this._undoInert?.(); this._undoInert = null; this._wasOpen = false; }
|
|
76
|
+
if (this._wasOpen) { closeLayer(this); unlockScroll(); this._undoInert?.(); this._undoInert = null; this._wasOpen = false; }
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
onAttributeChanged(name, oldValue, value) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { NkElement } from './base.js';
|
|
2
|
-
import { d as deepActiveElement } from './shared/focus-
|
|
2
|
+
import { d as deepActiveElement } from './shared/focus-D55JGjRA.js';
|
|
3
3
|
import '@jungherz-de/notionkit/notionkit-styles.js';
|
|
4
4
|
|
|
5
5
|
// <nk-sidebar slot="sidebar">
|
|
@@ -14,8 +14,12 @@ import '@jungherz-de/notionkit/notionkit-styles.js';
|
|
|
14
14
|
// stylesheet hides it above 860px – there the sidebar is the navigation –
|
|
15
15
|
// and shows it below; `always` shows it at every width (previews, phone
|
|
16
16
|
// frames), `fixed` pins it to the viewport (standalone PWAs) with a spacer
|
|
17
|
-
// holding its place, `floating` makes it a capsule.
|
|
18
|
-
// `drawer` item opens the sidebar instead of
|
|
17
|
+
// holding its place, `floating` makes it a capsule. The item whose value is
|
|
18
|
+
// the bar's `value` is `active`; a `drawer` item opens the sidebar instead of
|
|
19
|
+
// becoming active when clicked. A value no item has – a page without a tab
|
|
20
|
+
// of its own, opened from the drawer – marks the drawer item, as iOS marks
|
|
21
|
+
// "More" (1.18.0); without one no item is active. Without a value the item
|
|
22
|
+
// marked `active`, else the first, is the one.
|
|
19
23
|
class NkTabBar extends NkElement {
|
|
20
24
|
static get observedAttributes() { return ['value', 'always', 'fixed', 'floating', 'label']; }
|
|
21
25
|
|
|
@@ -46,16 +50,18 @@ class NkTabBar extends NkElement {
|
|
|
46
50
|
}
|
|
47
51
|
|
|
48
52
|
_sync() {
|
|
49
|
-
const items = this.items.filter(i => !i.hasAttribute('drawer'));
|
|
53
|
+
const items = this.items.filter(i => !i.hasAttribute('drawer')), drawers = this.items.filter(i => i.hasAttribute('drawer'));
|
|
50
54
|
if (!items.length) return;
|
|
51
55
|
let value = this.getAttribute('value');
|
|
52
|
-
if (value === null
|
|
56
|
+
if (value === null) {
|
|
53
57
|
const preset = items.find(i => i.hasAttribute('active')) || items[0];
|
|
54
58
|
value = this._valueOf(preset);
|
|
55
59
|
}
|
|
60
|
+
const known = items.some(i => this._valueOf(i) === value);
|
|
56
61
|
for (const item of items) {
|
|
57
62
|
if (this._valueOf(item) === value) item.setAttribute('active', ''); else item.removeAttribute('active');
|
|
58
63
|
}
|
|
64
|
+
drawers.forEach((item, n) => { if (!known && n === 0) item.setAttribute('active', ''); else item.removeAttribute('active'); });
|
|
59
65
|
this._value = value;
|
|
60
66
|
}
|
|
61
67
|
|
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
import { NkElement } from './base.js';
|
|
2
|
+
import { d as deepActiveElement, c as containsDeep } from './shared/focus-D55JGjRA.js';
|
|
2
3
|
import '@jungherz-de/notionkit/notionkit-styles.js';
|
|
3
4
|
|
|
4
5
|
// <nk-toast id="toast"></nk-toast> … toast.show('Saved');
|
|
5
|
-
//
|
|
6
|
+
// toast.show('Moved to trash', { action: { label: 'Undo', value: 'undo' } });
|
|
7
|
+
// → <div class="nk-toast show">✓ Moved to trash<button class="t-action">Undo</button><button class="t-close">×</button></div>
|
|
6
8
|
// One node, bottom-centred, auto-hides after `duration` ms (default 2200).
|
|
7
9
|
// Above a tab bar at the bottom of the screen it rises 12px over the bar:
|
|
8
10
|
// notionkit.css does that for class markup with :has(), which cannot see
|
|
9
11
|
// into shadow roots, so the element measures the bar when it opens.
|
|
12
|
+
// An action (1.19.0) – from show() or a <button slot="action"> – makes it
|
|
13
|
+
// Notion's "Moved to trash · Undo": a button, and an × that closes. Such a
|
|
14
|
+
// toast stays until one of them is used, unless show() names a duration,
|
|
15
|
+
// and waits while the pointer or the focus is on it. The action fires
|
|
16
|
+
// nk-action { action, value, label } and, unless cancelled, closes it;
|
|
17
|
+
// Escape closes it while the focus is inside.
|
|
18
|
+
const CLOSE_ICON = '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12"/></svg>';
|
|
19
|
+
|
|
10
20
|
class NkToast extends NkElement {
|
|
11
21
|
static get observedAttributes() { return ['open', 'duration', 'icon']; }
|
|
12
22
|
|
|
@@ -14,19 +24,37 @@ class NkToast extends NkElement {
|
|
|
14
24
|
this._box = this.createElement('div', ['nk-toast'], { role: 'status', 'aria-live': 'polite' });
|
|
15
25
|
this._icon = document.createElement('span');
|
|
16
26
|
this._msg = document.createElement('span');
|
|
17
|
-
this.
|
|
27
|
+
this._action = this.createElement('button', ['t-action'], { type: 'button' });
|
|
28
|
+
this._actionSlot = this.createElement('slot', [], { name: 'action' });
|
|
29
|
+
this._close = this.createElement('button', ['t-close'], { type: 'button' });
|
|
30
|
+
this._close.innerHTML = CLOSE_ICON;
|
|
31
|
+
this._box.append(this._icon, this._msg, document.createElement('slot'), this._actionSlot);
|
|
18
32
|
this._wrapper.appendChild(this._box);
|
|
19
33
|
this._sync();
|
|
20
34
|
}
|
|
21
35
|
|
|
22
36
|
_sync() {
|
|
23
|
-
|
|
24
|
-
|
|
37
|
+
const open = this.getBoolAttr('open');
|
|
38
|
+
if (open) this._lift();
|
|
39
|
+
this._box.classList.toggle('show', open);
|
|
25
40
|
const icon = this.getAttribute('icon') ?? '✓';
|
|
26
41
|
this._icon.textContent = icon;
|
|
27
42
|
this._icon.style.display = icon ? '' : 'none';
|
|
43
|
+
// An empty message takes no place – with slotted text it added a second gap.
|
|
44
|
+
this._msg.style.display = this._msg.textContent ? '' : 'none';
|
|
45
|
+
// The action from show() and the × sit in the tree only when there is an
|
|
46
|
+
// action; a closed toast keeps them for its fade, out of the tab order.
|
|
47
|
+
const own = this._act, slotted = this._slotted();
|
|
48
|
+
if (own) { this._action.textContent = own.label; if (!this._action.isConnected) this._actionSlot.before(this._action); }
|
|
49
|
+
else this._action.remove();
|
|
50
|
+
if (own || slotted) { if (!this._close.isConnected) this._box.appendChild(this._close); }
|
|
51
|
+
else this._close.remove();
|
|
52
|
+
this._close.setAttribute('aria-label', this.str('close'));
|
|
53
|
+
for (const b of [this._action, this._close]) open ? b.removeAttribute('tabindex') : b.setAttribute('tabindex', '-1');
|
|
28
54
|
}
|
|
29
55
|
|
|
56
|
+
_slotted() { return this._actionSlot?.assignedElements()[0] ?? null; }
|
|
57
|
+
|
|
30
58
|
// A bar counts when it is rendered and ends near the bottom of the viewport –
|
|
31
59
|
// the app's own bar, fixed, sticky or floating. A tab bar shown as a preview
|
|
32
60
|
// somewhere in a page does not lift the toast.
|
|
@@ -41,16 +69,83 @@ class NkToast extends NkElement {
|
|
|
41
69
|
this._box.style.bottom = rect ? `${Math.round(innerHeight - rect.top + 12)}px` : '';
|
|
42
70
|
}
|
|
43
71
|
|
|
44
|
-
/**
|
|
45
|
-
|
|
72
|
+
/**
|
|
73
|
+
* Shows `message` (or the slotted content when omitted) and hides it again
|
|
74
|
+
* after `duration` ms – the attribute's, else 2200; with an `action`
|
|
75
|
+
* ({ label, value }) it stays until the action or the × is used.
|
|
76
|
+
*/
|
|
77
|
+
show(message, { duration, action } = {}) {
|
|
46
78
|
if (message !== undefined) this._msg.textContent = message;
|
|
79
|
+
this._shown = (this._shown ?? 0) + 1;
|
|
80
|
+
this._act = action ? { label: String(action.label ?? action.value ?? ''), value: action.value ?? action.label } : null;
|
|
81
|
+
const attr = this.getAttribute('duration'), set = attr !== null && attr.trim() !== '' && Number.isFinite(Number(attr));
|
|
82
|
+
// Number(null) is 0: without the attribute a toast used to stay for good.
|
|
83
|
+
this._ms = duration ?? (this._act || this._slotted() ? 0 : set ? Number(attr) : 2200);
|
|
84
|
+
if (this.getBoolAttr('open')) this._sync(); else this.setBoolAttr('open', true);
|
|
85
|
+
this._arm();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
close() {
|
|
47
89
|
clearTimeout(this._timer);
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
if (
|
|
90
|
+
// The focus leaves with the toast, back to where it came from.
|
|
91
|
+
const active = deepActiveElement();
|
|
92
|
+
if (active && containsDeep(this, active)) {
|
|
93
|
+
this._from?.isConnected && this._from.focus({ preventScroll: true });
|
|
94
|
+
if (containsDeep(this, deepActiveElement())) active.blur();
|
|
95
|
+
}
|
|
96
|
+
this._from = null;
|
|
97
|
+
this.setBoolAttr('open', false);
|
|
51
98
|
}
|
|
52
99
|
|
|
53
|
-
|
|
100
|
+
_arm() {
|
|
101
|
+
clearTimeout(this._timer);
|
|
102
|
+
if (this._ms > 0 && !this._held && this.getBoolAttr('open')) this._timer = setTimeout(() => this.close(), this._ms);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
_take(act) {
|
|
106
|
+
if (!act) return;
|
|
107
|
+
// A handler that shows the next message – "Restored" – keeps it open.
|
|
108
|
+
const shown = this._shown;
|
|
109
|
+
if (this.emit('nk-action', { action: act.value, value: act.value, label: act.label }) && this._shown === shown) this.close();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
setupEvents() {
|
|
113
|
+
this._onClick = (e) => {
|
|
114
|
+
const path = e.composedPath();
|
|
115
|
+
if (path.includes(this._close)) { this.close(); return; }
|
|
116
|
+
if (path.includes(this._action)) { this._take(this._act); return; }
|
|
117
|
+
const slotted = this._slotted();
|
|
118
|
+
if (slotted && path.includes(slotted)) this._take({ value: slotted.value || slotted.getAttribute('value') || slotted.textContent.trim(), label: slotted.textContent.trim() });
|
|
119
|
+
};
|
|
120
|
+
// While the pointer or the focus is on it, a toast with a duration waits.
|
|
121
|
+
this._onHold = (e) => {
|
|
122
|
+
if (e.type === 'focusin' && !this._from) this._from = e.relatedTarget ?? null;
|
|
123
|
+
this._held = true; clearTimeout(this._timer);
|
|
124
|
+
};
|
|
125
|
+
this._onRelease = (e) => {
|
|
126
|
+
if (e.relatedTarget && containsDeep(this, e.relatedTarget)) return;
|
|
127
|
+
this._held = false; this._arm();
|
|
128
|
+
};
|
|
129
|
+
this._onKey = (e) => { if (e.key === 'Escape') { e.stopPropagation(); this.close(); } };
|
|
130
|
+
this._onSlot = () => this._sync();
|
|
131
|
+
this._box.addEventListener('click', this._onClick);
|
|
132
|
+
this._box.addEventListener('pointerover', this._onHold);
|
|
133
|
+
this._box.addEventListener('pointerout', this._onRelease);
|
|
134
|
+
this._box.addEventListener('focusin', this._onHold);
|
|
135
|
+
this._box.addEventListener('focusout', this._onRelease);
|
|
136
|
+
this._box.addEventListener('keydown', this._onKey);
|
|
137
|
+
this._actionSlot.addEventListener('slotchange', this._onSlot);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
teardownEvents() {
|
|
141
|
+
this._box?.removeEventListener('click', this._onClick);
|
|
142
|
+
this._box?.removeEventListener('pointerover', this._onHold);
|
|
143
|
+
this._box?.removeEventListener('pointerout', this._onRelease);
|
|
144
|
+
this._box?.removeEventListener('focusin', this._onHold);
|
|
145
|
+
this._box?.removeEventListener('focusout', this._onRelease);
|
|
146
|
+
this._box?.removeEventListener('keydown', this._onKey);
|
|
147
|
+
this._actionSlot?.removeEventListener('slotchange', this._onSlot);
|
|
148
|
+
}
|
|
54
149
|
|
|
55
150
|
onAttributeChanged(name) {
|
|
56
151
|
this._sync();
|
|
@@ -62,7 +157,8 @@ class NkToast extends NkElement {
|
|
|
62
157
|
get open() { return this.getBoolAttr('open'); }
|
|
63
158
|
set open(v) { this.setBoolAttr('open', v); }
|
|
64
159
|
get message() { return this._msg?.textContent ?? ''; }
|
|
65
|
-
set message(v) { if (this._msg) this._msg.textContent = v; }
|
|
160
|
+
set message(v) { if (this._msg) { this._msg.textContent = v; this._sync(); } }
|
|
161
|
+
onStringsChanged() { if (this._box) this._sync(); }
|
|
66
162
|
}
|
|
67
163
|
|
|
68
164
|
customElements.define('nk-toast', NkToast);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { NkElement } from './base.js';
|
|
2
2
|
import { a as placeNear } from './shared/floating-DOK73b_D.js';
|
|
3
|
-
import { d as deepActiveElement } from './shared/focus-
|
|
3
|
+
import { d as deepActiveElement } from './shared/focus-D55JGjRA.js';
|
|
4
4
|
import '@jungherz-de/notionkit/notionkit-styles.js';
|
|
5
5
|
|
|
6
6
|
// <nk-tooltip></nk-tooltip> one for the page: every [data-tooltip]
|
|
@@ -44,6 +44,12 @@ function deepActiveElement() {
|
|
|
44
44
|
return el;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
/** Whether `node` lies inside `root`, across shadow roots – Node.contains() stops at them. */
|
|
48
|
+
function containsDeep(root, node) {
|
|
49
|
+
for (let n = node; n; n = n.parentNode ?? n.host) if (n === root) return true;
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
|
|
47
53
|
/** The first tabbable element inside a light-DOM subtree, looking into shadow roots. */
|
|
48
54
|
function firstFocusable(root) {
|
|
49
55
|
const SEL = 'a[href], button:not([disabled]), input:not([disabled]):not([type=hidden]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"]), [contenteditable="true"], [contenteditable="plaintext-only"]';
|
|
@@ -57,4 +63,4 @@ function firstFocusable(root) {
|
|
|
57
63
|
return walk(root);
|
|
58
64
|
}
|
|
59
65
|
|
|
60
|
-
export { deepActiveElement as d, firstFocusable as f, inertOutside as i, lockScroll as l, unlockScroll as u };
|
|
66
|
+
export { containsDeep as c, deepActiveElement as d, firstFocusable as f, inertOutside as i, lockScroll as l, unlockScroll as u };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Escape closes the layer that opened last (NotionKit Elements 1.19.0). The
|
|
2
|
+
// overlays – modal, sheet, dialog, side peek, palette, popover, a floating
|
|
3
|
+
// menu or date picker – join this stack while they are open, and one
|
|
4
|
+
// listener on the document, in the capture phase, hands Escape to the top
|
|
5
|
+
// of it only. Each used to listen for itself: a date picker opened from a
|
|
6
|
+
// dialog and the dialog both listened on the document in the same phase,
|
|
7
|
+
// where stopPropagation() does not part them, so one Escape closed both.
|
|
8
|
+
const stack = [];
|
|
9
|
+
let listening = false;
|
|
10
|
+
|
|
11
|
+
function onKey(e) {
|
|
12
|
+
if (e.key !== 'Escape') return;
|
|
13
|
+
// Escape with the focus in a toast is the toast's, not the dialog's below.
|
|
14
|
+
if (e.composedPath().some(n => n.localName === 'nk-toast')) return;
|
|
15
|
+
for (let i = stack.length - 1; i >= 0; i--) if (!stack[i].el.isConnected) stack.splice(i, 1);
|
|
16
|
+
const top = stack.at(-1);
|
|
17
|
+
if (!top) return;
|
|
18
|
+
e.stopPropagation();
|
|
19
|
+
top.close();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** `el` is open on top of the others; Escape calls `close` until closeLayer(el). */
|
|
23
|
+
function openLayer(el, close) {
|
|
24
|
+
closeLayer(el);
|
|
25
|
+
stack.push({ el, close });
|
|
26
|
+
if (!listening && typeof document !== 'undefined') { document.addEventListener('keydown', onKey, true); listening = true; }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** `el` is closed or gone. */
|
|
30
|
+
function closeLayer(el) {
|
|
31
|
+
const i = stack.findIndex(layer => layer.el === el);
|
|
32
|
+
if (i >= 0) stack.splice(i, 1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export { closeLayer as c, openLayer as o };
|