@jungherz-de/notionkit-elements 1.9.0 → 1.10.1

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.
@@ -0,0 +1,138 @@
1
+ import { NkElement } from './base.js';
2
+ import { a as placeNear } from './shared/floating-CxHxy-Cb.js';
3
+ import { d as deepActiveElement } from './shared/focus-BNAChOXO.js';
4
+ import '@jungherz-de/notionkit/notionkit-styles.js';
5
+
6
+ // <nk-tooltip></nk-tooltip> one for the page: every [data-tooltip]
7
+ // <button data-tooltip="Close sidebar" data-tooltip-key="⌘\">«</button>
8
+ // <nk-tooltip for="saveBtn" shortcut="⌘S">Save the page</nk-tooltip>
9
+ // tip.show(barOrRect, 'Shell · 12.10.–30.11.'); tip.hide();
10
+ // → <div class="nk-tooltip open" role="tooltip">Close sidebar<span class="tt-key">⌘\</span></div>
11
+ //
12
+ // Notion's hover hint (NotionKit 1.10.0). Without `for` it serves every
13
+ // element with data-tooltip – and data-tooltip-key for a shortcut – on the
14
+ // page, inside shadow roots too: the tooltips of a whole toolbar in one
15
+ // element. With `for` it belongs to the element with that id and shows its
16
+ // own content and `shortcut`. It appears after `delay` ms (400) under the
17
+ // pointer and at once on keyboard focus, never on touch; leaving, a press,
18
+ // blur, scrolling and Escape hide it. It sits 6px below its target and
19
+ // centred, above it where the window ends – placement="top" prefers above –
20
+ // and 8px inside the window (placeNear in util/floating.js). show(target,
21
+ // text, shortcut) takes an element or a rect, for what has no element of
22
+ // its own: the bars of a Gantt chart. While it shows, the target's
23
+ // aria-describedby names it (in the same document or shadow root).
24
+ const hostSheet = new CSSStyleSheet();
25
+ hostSheet.replaceSync(`:host { display: block; position: fixed; top: 0; left: 0; z-index: 130; pointer-events: none; }`);
26
+ let count = 0;
27
+
28
+ class NkTooltip extends NkElement {
29
+ static get hostStyles() { return hostSheet; }
30
+ static get observedAttributes() { return ['for', 'shortcut']; }
31
+
32
+ render() {
33
+ this._bubble = this.createElement('div', ['nk-tooltip'], { role: 'tooltip' });
34
+ this._slot = document.createElement('slot');
35
+ this._text = document.createTextNode('');
36
+ this._key = this.createElement('span', ['tt-key']);
37
+ this._bubble.append(this._slot, this._text, this._key);
38
+ this._wrapper.appendChild(this._bubble);
39
+ this._fill(null);
40
+ }
41
+
42
+ get _delay() { const d = Number(this.getAttribute('delay')); return this.hasAttribute('delay') && d >= 0 ? d : 400; }
43
+
44
+ /** Own content (text null) or a given text and shortcut. */
45
+ _fill(text, key = this.getAttribute('shortcut')) {
46
+ this._slot.style.display = text == null ? '' : 'none';
47
+ this._text.data = text ?? '';
48
+ this._key.textContent = key || '';
49
+ this._key.hidden = !key;
50
+ }
51
+
52
+ /** Shows it at `target` – an element or a rect – with `text` (else its own content) and `shortcut`. */
53
+ show(target, text, shortcut) {
54
+ clearTimeout(this._timer);
55
+ if (!target) return;
56
+ this._fill(text ?? null, shortcut);
57
+ this._describe(target);
58
+ placeNear(this, target, this.getAttribute('placement') === 'top' ? 'top' : 'bottom');
59
+ this._bubble.classList.add('open');
60
+ }
61
+
62
+ hide() {
63
+ clearTimeout(this._timer);
64
+ this._bubble?.classList.remove('open');
65
+ this._describe(null);
66
+ this._target = null;
67
+ }
68
+
69
+ get open() { return !!this._bubble?.classList.contains('open'); }
70
+
71
+ _describe(target) {
72
+ if (this._described) {
73
+ const ids = (this._described.getAttribute('aria-describedby') || '').split(/\s+/).filter(id => id && id !== this.id);
74
+ ids.length ? this._described.setAttribute('aria-describedby', ids.join(' ')) : this._described.removeAttribute('aria-describedby');
75
+ this._described = null;
76
+ }
77
+ if (!(target instanceof Element) || target.getRootNode() !== this.getRootNode()) return;
78
+ this.id ||= `nk-tooltip-${++count}`;
79
+ const ids = (target.getAttribute('aria-describedby') || '').split(/\s+/).filter(Boolean);
80
+ if (!ids.includes(this.id)) target.setAttribute('aria-describedby', [...ids, this.id].join(' '));
81
+ this._described = target;
82
+ }
83
+
84
+ /** The element this tooltip serves for an event: its `for` target, or the nearest [data-tooltip]. */
85
+ _targetOf(e) {
86
+ const path = e.composedPath();
87
+ const own = this.getAttribute('for');
88
+ if (own) { const el = this.getRootNode().getElementById?.(own); return el && path.includes(el) ? el : null; }
89
+ return path.find(n => n instanceof Element && n.hasAttribute('data-tooltip')) ?? null;
90
+ }
91
+
92
+ _showFor(el) {
93
+ const own = this.hasAttribute('for');
94
+ this.show(el, own ? null : el.getAttribute('data-tooltip'), own ? this.getAttribute('shortcut') : el.getAttribute('data-tooltip-key'));
95
+ }
96
+
97
+ setupEvents() {
98
+ this._onOver = (e) => {
99
+ const el = e.pointerType === 'touch' ? null : this._targetOf(e);
100
+ if (el === this._target) return;
101
+ this.hide();
102
+ if (!el) return;
103
+ this._target = el;
104
+ this._timer = setTimeout(() => this._showFor(el), this._delay);
105
+ };
106
+ this._onFocus = (e) => {
107
+ const el = this._targetOf(e);
108
+ if (!el || !deepActiveElement()?.matches(':focus-visible')) return;
109
+ this.hide();
110
+ this._target = el;
111
+ this._showFor(el);
112
+ };
113
+ this._onHide = () => this.hide();
114
+ this._onKey = (e) => { if (e.key === 'Escape' && this.open) this.hide(); };
115
+ document.addEventListener('pointerover', this._onOver);
116
+ document.addEventListener('focusin', this._onFocus);
117
+ document.addEventListener('focusout', this._onHide);
118
+ document.addEventListener('pointerdown', this._onHide, true);
119
+ document.addEventListener('scroll', this._onHide, true);
120
+ document.addEventListener('keydown', this._onKey);
121
+ }
122
+
123
+ teardownEvents() {
124
+ document.removeEventListener('pointerover', this._onOver);
125
+ document.removeEventListener('focusin', this._onFocus);
126
+ document.removeEventListener('focusout', this._onHide);
127
+ document.removeEventListener('pointerdown', this._onHide, true);
128
+ document.removeEventListener('scroll', this._onHide, true);
129
+ document.removeEventListener('keydown', this._onKey);
130
+ this.hide();
131
+ }
132
+
133
+ onAttributeChanged(name) { if (name === 'shortcut' && !this._text.data) this._fill(null); if (name === 'for') this.hide(); }
134
+ }
135
+
136
+ customElements.define('nk-tooltip', NkTooltip);
137
+
138
+ export { NkTooltip };
@@ -9,7 +9,7 @@ import '@jungherz-de/notionkit/notionkit-styles.js';
9
9
  // row; clicking outside or pressing Escape closes it.
10
10
  const menuSheet = new CSSStyleSheet();
11
11
  menuSheet.replaceSync(`
12
- .nk-ws-anchor { position: relative; }
12
+ .nk-ws-anchor { position: relative; flex: 1 1 auto; min-width: 0; }
13
13
  .nk-ws-menu { display: none; position: absolute; left: 8px; top: calc(100% - 2px); z-index: 50; }
14
14
  :host([open]) .nk-ws-menu { display: block; }
15
15
  `);
@@ -11,6 +11,21 @@ function boxOf(el) {
11
11
  return el.getBoundingClientRect();
12
12
  }
13
13
 
14
+ /**
15
+ * Puts `host` 6px below `target` – an element or a rect ({ left, top, right,
16
+ * bottom }, a DOMRect) – centred on it: above with side="top", or where the
17
+ * window ends below; kept 8px inside the window. For <nk-tooltip>.
18
+ */
19
+ function placeNear(host, target, side = 'bottom', gap = 6) {
20
+ const r = target instanceof Element ? boxOf(target) : target;
21
+ const { width: w, height: h } = host.getBoundingClientRect();
22
+ const below = r.bottom + gap, above = r.top - gap - h;
23
+ const top = side === 'top' ? (above < 8 ? below : above) : (below + h > innerHeight - 8 ? above : below);
24
+ host.style.top = `${Math.round(top)}px`;
25
+ host.style.left = `${Math.round(Math.min(Math.max(8, (r.left + r.right) / 2 - w / 2), innerWidth - 8 - w))}px`;
26
+ host.style.right = '';
27
+ }
28
+
14
29
  /** Puts `host` 6px under `anchor`, right edges aligned – left edges with align="start". */
15
30
  function placeUnder(host, anchor, align) {
16
31
  const r = boxOf(anchor);
@@ -19,4 +34,4 @@ function placeUnder(host, anchor, align) {
19
34
  else { host.style.right = `${Math.max(8, innerWidth - r.right)}px`; host.style.left = ''; }
20
35
  }
21
36
 
22
- export { placeUnder as p };
37
+ export { placeNear as a, placeUnder as p };