@jungherz-de/notionkit-elements 1.20.0 → 1.20.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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,34 @@ All notable changes to NotionKit Elements are documented here. The format follow
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow
5
5
  [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## [1.20.1] – 2026-10-14
8
+
9
+ Elements only; NotionKit CSS stays at 1.20.0 (peer `>= 1.20.0`).
10
+
11
+ ### Fixed
12
+ - **`<nk-tooltip>` follows the pointer and the focus inside a shadow root
13
+ (#3).** Moving from one `[data-tooltip]` to the next in the same shadow
14
+ root never updated the tooltip – the first one stayed, or none appeared –
15
+ for the pointer and for Tab alike; `<nk-copy-field icons secret>` kept
16
+ saying "Show" over Copy, an app's toolbar or chart bars in one root did
17
+ the same. `pointerover`, `focusin` and `focusout` carry a `relatedTarget`,
18
+ and when target and `relatedTarget` share a shadow tree the DOM standard
19
+ ends the event path at its host, so the listeners on the document never
20
+ heard of the move. The tooltip now also reads `pointermove` (no
21
+ `relatedTarget`, it always reaches the document) and acts only when the
22
+ element under the pointer changes, so a hand resting on the page leaves a
23
+ tooltip the keyboard or `show(rect)` opened alone; and it catches a key
24
+ that moved the focus – Tab, Option+Tab in Safari, the arrow keys of a
25
+ roving tab stop that moves the focus in its own `keydown` – on `keyup`,
26
+ against the focus recorded before the key went down. Nested shadow roots
27
+ are followed too. A focus move by script alone inside one shadow root
28
+ stays unseen. Reported by Mailverse.
29
+ - **Leaving the window** over a `[data-tooltip]` left its tooltip standing,
30
+ and one still waiting appeared after the pointer had gone. It now goes
31
+ with the pointer (`pointerout` without a `relatedTarget`); a tooltip the
32
+ keyboard or `show(rect)` opened is not the pointer's and stays. Found
33
+ while testing #3.
34
+
7
35
  ## [1.20.0] – 2026-10-12
8
36
 
9
37
  Built against NotionKit CSS 1.20.0 (peer `>= 1.20.0`).
@@ -778,6 +806,7 @@ built and tested against NotionKit CSS 1.1.1 (peer range `>= 1.0.0`).
778
806
  import, never in the core bundle – shadow-less, adding `nk-block-host` to
779
807
  itself so the foundation's editor adapter rules apply.
780
808
 
809
+ [1.20.1]: https://github.com/JUNGHERZ/NotionKit-Elements/releases/tag/v1.20.1
781
810
  [1.20.0]: https://github.com/JUNGHERZ/NotionKit-Elements/releases/tag/v1.20.0
782
811
  [1.19.0]: https://github.com/JUNGHERZ/NotionKit-Elements/releases/tag/v1.19.0
783
812
  [1.18.0]: https://github.com/JUNGHERZ/NotionKit-Elements/releases/tag/v1.18.0
package/README.md CHANGED
@@ -1,11 +1,11 @@
1
1
  <h1 align="center">🧩 NotionKit Elements</h1>
2
2
 
3
3
  <p align="center">
4
- <a href="https://www.npmjs.com/package/@jungherz-de/notionkit-elements"><img src="https://img.shields.io/badge/version-1.20.0-2383e2?style=flat-square" alt="Version"></a>
4
+ <a href="https://www.npmjs.com/package/@jungherz-de/notionkit-elements"><img src="https://img.shields.io/badge/version-1.20.1-2383e2?style=flat-square" alt="Version"></a>
5
5
  <a href="#"><img src="https://img.shields.io/badge/vanilla_JS-no_dependencies-448361?style=flat-square" alt="Vanilla JS"></a>
6
6
  <a href="#"><img src="https://img.shields.io/badge/elements-88-529cca?style=flat-square" alt="88 Elements"></a>
7
7
  <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-9065b0?style=flat-square" alt="MIT License"></a>
8
- <a href="CHANGELOG.md"><img src="https://img.shields.io/badge/changelog-v1.20.0-lightgrey?style=flat-square" alt="Changelog"></a>
8
+ <a href="CHANGELOG.md"><img src="https://img.shields.io/badge/changelog-v1.20.1-lightgrey?style=flat-square" alt="Changelog"></a>
9
9
  <a href="https://www.npmjs.com/package/@jungherz-de/notionkit-elements"><img src="https://img.shields.io/badge/npm-%40jungherz--de%2Fnotionkit--elements-cb3837?style=flat-square&logo=npm" alt="npm"></a>
10
10
  </p>
11
11
 
@@ -72,7 +72,7 @@ It is the **app layer** of the NotionKit family – three layers, one design lan
72
72
  <html lang="en" data-theme="light">
73
73
  <head>
74
74
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit@1.20.0/notionkit.min.css">
75
- <script src="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit-elements@1.20.0/dist/notionkit-elements.min.js"></script>
75
+ <script src="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit-elements@1.20.1/dist/notionkit-elements.min.js"></script>
76
76
  </head>
77
77
  <body class="nk-body">
78
78
  <nk-field label="Display name" desc="Shown next to your comments">
package/SKILL.md CHANGED
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: notionkit-elements
3
- description: NotionKit Elements is a vanilla-JS Web Components library (v1.20.0) wrapping NotionKit CSS v1.20.0 – the calm, document-centric design system in the Notion idiom. 88 custom elements with the `nk-` prefix, Shadow DOM, automatic light/dark sync via data-theme on <html>, and form-associated controls. Use this reference whenever generating HTML that uses <nk-*> tags to get attributes, slots, events and composition right.
3
+ description: NotionKit Elements is a vanilla-JS Web Components library (v1.20.1) wrapping NotionKit CSS v1.20.0 – the calm, document-centric design system in the Notion idiom. 88 custom elements with the `nk-` prefix, Shadow DOM, automatic light/dark sync via data-theme on <html>, and form-associated controls. Use this reference whenever generating HTML that uses <nk-*> tags to get attributes, slots, events and composition right.
4
4
  ---
5
5
 
6
6
  # NotionKit Elements – AI Component Reference
@@ -22,7 +22,7 @@ description: NotionKit Elements is a vanilla-JS Web Components library (v1.20.0)
22
22
  <meta charset="UTF-8">
23
23
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
24
24
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit@1.20.0/notionkit.min.css">
25
- <script src="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit-elements@1.20.0/dist/notionkit-elements.min.js"></script>
25
+ <script src="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit-elements@1.20.1/dist/notionkit-elements.min.js"></script>
26
26
  </head>
27
27
  <body class="nk-body">
28
28
  <nk-btn variant="primary">Save</nk-btn>
@@ -2562,7 +2562,7 @@ One inverted pill at the bottom centre, above every overlay; over a tab bar at t
2562
2562
 
2563
2563
  ### 3.76 `<nk-tooltip>` – Tooltip
2564
2564
 
2565
- Notion’s hover hint in the toast’s colours, a shortcut muted beside it. One `<nk-tooltip>` without `for` serves every element with `data-tooltip` (and `data-tooltip-key`) on the page, inside shadow roots too – a whole toolbar in one element; with `for` it belongs to one element and shows its own content and `shortcut`. It appears after `delay` ms (400) under the pointer and at once on keyboard focus, never on touch; leaving, a press, blur, scrolling and Escape hide it. It sits 6px below its target and centred, above it where the window ends, 8px inside the window – `placeNear` in `util/floating.js`. `show(target, text, shortcut)` takes an element or a rect, for what has no element of its own: the bars of a chart. The target’s `aria-describedby` names it while it shows. Hover the buttons. A tooltip shown with `show(rect, …)` stays until `hide()`, the pointer on another `[data-tooltip]`, a press or Escape; the wheel hides it too, also inside a shadow root. A line break in the text is kept.
2565
+ Notion’s hover hint in the toast’s colours, a shortcut muted beside it. One `<nk-tooltip>` without `for` serves every element with `data-tooltip` (and `data-tooltip-key`) on the page, inside shadow roots too – a whole toolbar in one element, and it follows pointer and keyboard from one such element to the next within the same shadow root; with `for` it belongs to one element and shows its own content and `shortcut`. It appears after `delay` ms (400) under the pointer and at once on keyboard focus, never on touch; leaving – the window too –, a press, blur, scrolling and Escape hide it. It sits 6px below its target and centred, above it where the window ends, 8px inside the window – `placeNear` in `util/floating.js`. `show(target, text, shortcut)` takes an element or a rect, for what has no element of its own: the bars of a chart. The target’s `aria-describedby` names it while it shows. Hover the buttons. A tooltip shown with `show(rect, …)` stays until `hide()`, the pointer on another `[data-tooltip]`, a press or Escape; the wheel hides it too, also inside a shadow root. A line break in the text is kept.
2566
2566
 
2567
2567
  ```html
2568
2568
  <div style="display:flex;gap:16px;align-items:center;padding:4px 0 44px">
@@ -3153,7 +3153,7 @@ Eight skeletons, one per app shape, mirroring the NotionKit CSS SKILL.md. Copy o
3153
3153
  <head>
3154
3154
  <meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
3155
3155
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit@1.20.0/notionkit.min.css">
3156
- <script src="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit-elements@1.20.0/dist/notionkit-elements.min.js"></script>
3156
+ <script src="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit-elements@1.20.1/dist/notionkit-elements.min.js"></script>
3157
3157
  </head>
3158
3158
  <body class="nk-body">
3159
3159
  <nk-app>
@@ -3235,7 +3235,7 @@ Rules of the shell: `nk-sidebar`, `nk-topbar` and `nk-page` are `display: conten
3235
3235
  <head>
3236
3236
  <meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
3237
3237
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit@1.20.0/notionkit.min.css">
3238
- <script src="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit-elements@1.20.0/dist/notionkit-elements.min.js"></script>
3238
+ <script src="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit-elements@1.20.1/dist/notionkit-elements.min.js"></script>
3239
3239
  </head>
3240
3240
  <body class="nk-body">
3241
3241
  <nk-app>
@@ -3343,7 +3343,7 @@ Data contract: `columns` describe the properties (`type`: text | select | multi-
3343
3343
  <head>
3344
3344
  <meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
3345
3345
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit@1.20.0/notionkit.min.css">
3346
- <script src="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit-elements@1.20.0/dist/notionkit-elements.min.js"></script>
3346
+ <script src="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit-elements@1.20.1/dist/notionkit-elements.min.js"></script>
3347
3347
  </head>
3348
3348
  <body class="nk-body">
3349
3349
  <nk-app>
@@ -3403,7 +3403,7 @@ Data contract: `columns` describe the properties (`type`: text | select | multi-
3403
3403
  <head>
3404
3404
  <meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
3405
3405
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit@1.20.0/notionkit.min.css">
3406
- <script src="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit-elements@1.20.0/dist/notionkit-elements.min.js"></script>
3406
+ <script src="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit-elements@1.20.1/dist/notionkit-elements.min.js"></script>
3407
3407
  </head>
3408
3408
  <body class="nk-body">
3409
3409
  <!-- your app -->
@@ -3481,7 +3481,7 @@ The open/close contract is one attribute: `settings.open = true`, `settings.show
3481
3481
  <head>
3482
3482
  <meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
3483
3483
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit@1.20.0/notionkit.min.css">
3484
- <script src="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit-elements@1.20.0/dist/notionkit-elements.min.js"></script>
3484
+ <script src="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit-elements@1.20.1/dist/notionkit-elements.min.js"></script>
3485
3485
  </head>
3486
3486
  <body class="nk-body">
3487
3487
  <div class="nk-page" style="padding-top:48px">
@@ -3528,7 +3528,7 @@ The open/close contract is one attribute: `settings.open = true`, `settings.show
3528
3528
  <head>
3529
3529
  <meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
3530
3530
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit@1.20.0/notionkit.min.css">
3531
- <script src="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit-elements@1.20.0/dist/notionkit-elements.min.js"></script>
3531
+ <script src="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit-elements@1.20.1/dist/notionkit-elements.min.js"></script>
3532
3532
  </head>
3533
3533
  <body class="nk-body">
3534
3534
  <nk-page narrow icon="📘" cover>
@@ -3573,7 +3573,7 @@ Note `narrow`: the page is the document, so there is no inner scroll wrapper –
3573
3573
  <head>
3574
3574
  <meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
3575
3575
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit@1.20.0/notionkit.min.css">
3576
- <script src="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit-elements@1.20.0/dist/notionkit-elements.min.js"></script>
3576
+ <script src="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit-elements@1.20.1/dist/notionkit-elements.min.js"></script>
3577
3577
  </head>
3578
3578
  <body class="nk-body">
3579
3579
  <nk-app>
@@ -3628,7 +3628,7 @@ Note `narrow`: the page is the document, so there is no inner scroll wrapper –
3628
3628
  <head>
3629
3629
  <meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
3630
3630
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit@1.20.0/notionkit.min.css">
3631
- <script src="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit-elements@1.20.0/dist/notionkit-elements.min.js"></script>
3631
+ <script src="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit-elements@1.20.1/dist/notionkit-elements.min.js"></script>
3632
3632
  </head>
3633
3633
  <body class="nk-body">
3634
3634
  <!-- A narrow column, centred: layout is yours, so it is inline. -->
@@ -3839,10 +3839,10 @@ Form controls additionally re-dispatch a native, bubbling `change` event, so `fo
3839
3839
  | Texts | `src/util/strings.js` – the English and German dictionary and `setStrings()`; `NkElement#str(key)` reads it in the element's language, `onStringsChanged()` runs after `setStrings()` and when `<html lang>` changes |
3840
3840
  | Components | `src/components/{forms,content,shell,page,overlays,data}/nk-*.js`, one tag per file, `customElements.define` at the bottom |
3841
3841
  | Build | Rollup: IIFE, minified IIFE, ESM, and per-component ESM entries on a stable `dist/components/base.js` that import NotionKit's sheet (`@jungherz-de/notionkit/notionkit-styles.js`) instead of inlining it; the full bundles inline it and export `componentsSheet` |
3842
- | Peer | `@jungherz-de/notionkit >= 1.20.0` – from 1.5.0 on the elements and the foundation share one version number; the bundle embeds that release's stylesheet, so keep them in step |
3842
+ | Peer | `@jungherz-de/notionkit >= 1.20.1` – from 1.5.0 on the elements and the foundation share one version number; the bundle embeds that release's stylesheet, so keep them in step |
3843
3843
 
3844
3844
  Lifecycle: construct (attach shadow, adopt sheets) → first connect (wrapper + `render()`) → every connect (`setupEvents()`, theme registration, light-DOM observer) → `attributeChangedCallback` → `onAttributeChanged` → disconnect (`teardownEvents()`, unregister).
3845
3845
 
3846
3846
 
3847
3847
  ---
3848
- *NotionKit Elements v1.20.0 · wrapping NotionKit CSS v1.20.0 · MIT · Jungherz GmbH*
3848
+ *NotionKit Elements v1.20.1 · wrapping NotionKit CSS v1.20.0 · MIT · Jungherz GmbH*
@@ -24,6 +24,17 @@ import '@jungherz-de/notionkit/notionkit-styles.js';
24
24
  // [data-tooltip], a press or Escape. A line break in the text is kept.
25
25
  // While it shows, the target's aria-describedby names it (in the same
26
26
  // document or shadow root).
27
+ //
28
+ // Following the pointer and the focus inside a shadow root (1.20.1):
29
+ // pointerover, focusin and focusout carry a relatedTarget, and when target
30
+ // and relatedTarget share a shadow tree the DOM standard ends the event path
31
+ // at its host – the document never hears of a move from one [data-tooltip]
32
+ // to the next in the same root. So the pointer is also read from
33
+ // pointermove, which has no relatedTarget, and acted on only when the
34
+ // element under it changes; and a key that moved the focus – Tab, the arrow
35
+ // keys of a group – is caught on keyup, comparing the focused element with
36
+ // the one before the key went down. A move by script alone inside one
37
+ // shadow root stays unseen.
27
38
  const hostSheet = new CSSStyleSheet();
28
39
  hostSheet.replaceSync(`:host { display: block; position: fixed; top: 0; left: 0; z-index: 130; pointer-events: none; }`);
29
40
  let count = 0;
@@ -101,14 +112,50 @@ class NkTooltip extends NkElement {
101
112
  this.show(el, own ? null : el.getAttribute('data-tooltip'), own ? this.getAttribute('shortcut') : el.getAttribute('data-tooltip-key'));
102
113
  }
103
114
 
115
+ /**
116
+ * The pointer is over `el` now (null: no [data-tooltip]). pointerover acts
117
+ * on every boundary it crosses, as it always has; pointermove – the only
118
+ * one that reaches the document from inside a shadow root – only when the
119
+ * element under the pointer changed, so a hand resting on the page leaves
120
+ * a tooltip the keyboard or show(rect) opened alone.
121
+ */
122
+ _pointer(e, move) {
123
+ const el = e.pointerType === 'touch' ? null : this._targetOf(e);
124
+ if (move && el === this._hover) return;
125
+ this._hover = el;
126
+ if (el === this._target) return;
127
+ this.hide();
128
+ if (!el) return;
129
+ this._target = el;
130
+ this._timer = setTimeout(() => this._showFor(el), this._delay);
131
+ }
132
+
104
133
  setupEvents() {
105
- this._onOver = (e) => {
106
- const el = e.pointerType === 'touch' ? null : this._targetOf(e);
107
- if (el === this._target) return;
134
+ this._onOver = (e) => this._pointer(e, false);
135
+ this._onMove = (e) => this._pointer(e, true);
136
+ // The pointer left the window (no relatedTarget): a tooltip – shown or
137
+ // still waiting – for what it was on goes with it. One the keyboard or
138
+ // show(rect) opened is not the pointer's and stays.
139
+ this._onOut = (e) => {
140
+ if (e.relatedTarget || e.pointerType === 'touch') return;
141
+ if (this._target && this._target === this._hover) this.hide();
142
+ this._hover = null;
143
+ };
144
+ // Capture, before an element's own keydown moves the focus (a roving tab
145
+ // stop does); not for a held key's repeats, so keyup compares with where
146
+ // the focus was before the key went down.
147
+ this._onKeyDown = (e) => { if (!e.repeat) this._focusBefore = deepActiveElement(); };
148
+ this._onKeyUp = (e) => {
149
+ const active = deepActiveElement();
150
+ if (active === this._focusBefore) return;
151
+ this._focusBefore = active;
152
+ // keyup goes to the focused element, so its path is the focus's path.
153
+ const el = this._targetOf(e);
154
+ if (el && el === this._target) return; // focusin got there first
108
155
  this.hide();
109
- if (!el) return;
156
+ if (!el || !active?.matches(':focus-visible')) return;
110
157
  this._target = el;
111
- this._timer = setTimeout(() => this._showFor(el), this._delay);
158
+ this._showFor(el);
112
159
  };
113
160
  this._onFocus = (e) => {
114
161
  const el = this._targetOf(e);
@@ -120,7 +167,11 @@ class NkTooltip extends NkElement {
120
167
  this._onHide = () => this.hide();
121
168
  this._onKey = (e) => { if (e.key === 'Escape' && this.open) this.hide(); };
122
169
  document.addEventListener('pointerover', this._onOver);
170
+ document.addEventListener('pointermove', this._onMove, { capture: true, passive: true });
171
+ document.addEventListener('pointerout', this._onOut);
123
172
  document.addEventListener('focusin', this._onFocus);
173
+ document.addEventListener('keydown', this._onKeyDown, true);
174
+ document.addEventListener('keyup', this._onKeyUp, true);
124
175
  document.addEventListener('focusout', this._onHide);
125
176
  document.addEventListener('pointerdown', this._onHide, true);
126
177
  document.addEventListener('scroll', this._onHide, true);
@@ -131,13 +182,18 @@ class NkTooltip extends NkElement {
131
182
 
132
183
  teardownEvents() {
133
184
  document.removeEventListener('pointerover', this._onOver);
185
+ document.removeEventListener('pointermove', this._onMove, { capture: true });
186
+ document.removeEventListener('pointerout', this._onOut);
134
187
  document.removeEventListener('focusin', this._onFocus);
188
+ document.removeEventListener('keydown', this._onKeyDown, true);
189
+ document.removeEventListener('keyup', this._onKeyUp, true);
135
190
  document.removeEventListener('focusout', this._onHide);
136
191
  document.removeEventListener('pointerdown', this._onHide, true);
137
192
  document.removeEventListener('scroll', this._onHide, true);
138
193
  document.removeEventListener('wheel', this._onHide, { capture: true });
139
194
  document.removeEventListener('keydown', this._onKey);
140
195
  this.hide();
196
+ this._hover = this._focusBefore = null;
141
197
  }
142
198
 
143
199
  onAttributeChanged(name) { if (name === 'shortcut' && !this._text.data) this._fill(null); if (name === 'for') this.hide(); }
@@ -5636,6 +5636,17 @@ customElements.define('nk-dialog', NkDialog);
5636
5636
  // [data-tooltip], a press or Escape. A line break in the text is kept.
5637
5637
  // While it shows, the target's aria-describedby names it (in the same
5638
5638
  // document or shadow root).
5639
+ //
5640
+ // Following the pointer and the focus inside a shadow root (1.20.1):
5641
+ // pointerover, focusin and focusout carry a relatedTarget, and when target
5642
+ // and relatedTarget share a shadow tree the DOM standard ends the event path
5643
+ // at its host – the document never hears of a move from one [data-tooltip]
5644
+ // to the next in the same root. So the pointer is also read from
5645
+ // pointermove, which has no relatedTarget, and acted on only when the
5646
+ // element under it changes; and a key that moved the focus – Tab, the arrow
5647
+ // keys of a group – is caught on keyup, comparing the focused element with
5648
+ // the one before the key went down. A move by script alone inside one
5649
+ // shadow root stays unseen.
5639
5650
  const hostSheet = new CSSStyleSheet();
5640
5651
  hostSheet.replaceSync(`:host { display: block; position: fixed; top: 0; left: 0; z-index: 130; pointer-events: none; }`);
5641
5652
  let count = 0;
@@ -5713,14 +5724,50 @@ class NkTooltip extends NkElement {
5713
5724
  this.show(el, own ? null : el.getAttribute('data-tooltip'), own ? this.getAttribute('shortcut') : el.getAttribute('data-tooltip-key'));
5714
5725
  }
5715
5726
 
5727
+ /**
5728
+ * The pointer is over `el` now (null: no [data-tooltip]). pointerover acts
5729
+ * on every boundary it crosses, as it always has; pointermove – the only
5730
+ * one that reaches the document from inside a shadow root – only when the
5731
+ * element under the pointer changed, so a hand resting on the page leaves
5732
+ * a tooltip the keyboard or show(rect) opened alone.
5733
+ */
5734
+ _pointer(e, move) {
5735
+ const el = e.pointerType === 'touch' ? null : this._targetOf(e);
5736
+ if (move && el === this._hover) return;
5737
+ this._hover = el;
5738
+ if (el === this._target) return;
5739
+ this.hide();
5740
+ if (!el) return;
5741
+ this._target = el;
5742
+ this._timer = setTimeout(() => this._showFor(el), this._delay);
5743
+ }
5744
+
5716
5745
  setupEvents() {
5717
- this._onOver = (e) => {
5718
- const el = e.pointerType === 'touch' ? null : this._targetOf(e);
5719
- if (el === this._target) return;
5746
+ this._onOver = (e) => this._pointer(e, false);
5747
+ this._onMove = (e) => this._pointer(e, true);
5748
+ // The pointer left the window (no relatedTarget): a tooltip – shown or
5749
+ // still waiting – for what it was on goes with it. One the keyboard or
5750
+ // show(rect) opened is not the pointer's and stays.
5751
+ this._onOut = (e) => {
5752
+ if (e.relatedTarget || e.pointerType === 'touch') return;
5753
+ if (this._target && this._target === this._hover) this.hide();
5754
+ this._hover = null;
5755
+ };
5756
+ // Capture, before an element's own keydown moves the focus (a roving tab
5757
+ // stop does); not for a held key's repeats, so keyup compares with where
5758
+ // the focus was before the key went down.
5759
+ this._onKeyDown = (e) => { if (!e.repeat) this._focusBefore = deepActiveElement(); };
5760
+ this._onKeyUp = (e) => {
5761
+ const active = deepActiveElement();
5762
+ if (active === this._focusBefore) return;
5763
+ this._focusBefore = active;
5764
+ // keyup goes to the focused element, so its path is the focus's path.
5765
+ const el = this._targetOf(e);
5766
+ if (el && el === this._target) return; // focusin got there first
5720
5767
  this.hide();
5721
- if (!el) return;
5768
+ if (!el || !active?.matches(':focus-visible')) return;
5722
5769
  this._target = el;
5723
- this._timer = setTimeout(() => this._showFor(el), this._delay);
5770
+ this._showFor(el);
5724
5771
  };
5725
5772
  this._onFocus = (e) => {
5726
5773
  const el = this._targetOf(e);
@@ -5732,7 +5779,11 @@ class NkTooltip extends NkElement {
5732
5779
  this._onHide = () => this.hide();
5733
5780
  this._onKey = (e) => { if (e.key === 'Escape' && this.open) this.hide(); };
5734
5781
  document.addEventListener('pointerover', this._onOver);
5782
+ document.addEventListener('pointermove', this._onMove, { capture: true, passive: true });
5783
+ document.addEventListener('pointerout', this._onOut);
5735
5784
  document.addEventListener('focusin', this._onFocus);
5785
+ document.addEventListener('keydown', this._onKeyDown, true);
5786
+ document.addEventListener('keyup', this._onKeyUp, true);
5736
5787
  document.addEventListener('focusout', this._onHide);
5737
5788
  document.addEventListener('pointerdown', this._onHide, true);
5738
5789
  document.addEventListener('scroll', this._onHide, true);
@@ -5743,13 +5794,18 @@ class NkTooltip extends NkElement {
5743
5794
 
5744
5795
  teardownEvents() {
5745
5796
  document.removeEventListener('pointerover', this._onOver);
5797
+ document.removeEventListener('pointermove', this._onMove, { capture: true });
5798
+ document.removeEventListener('pointerout', this._onOut);
5746
5799
  document.removeEventListener('focusin', this._onFocus);
5800
+ document.removeEventListener('keydown', this._onKeyDown, true);
5801
+ document.removeEventListener('keyup', this._onKeyUp, true);
5747
5802
  document.removeEventListener('focusout', this._onHide);
5748
5803
  document.removeEventListener('pointerdown', this._onHide, true);
5749
5804
  document.removeEventListener('scroll', this._onHide, true);
5750
5805
  document.removeEventListener('wheel', this._onHide, { capture: true });
5751
5806
  document.removeEventListener('keydown', this._onKey);
5752
5807
  this.hide();
5808
+ this._hover = this._focusBefore = null;
5753
5809
  }
5754
5810
 
5755
5811
  onAttributeChanged(name) { if (name === 'shortcut' && !this._text.data) this._fill(null); if (name === 'for') this.hide(); }