@jungherz-de/notionkit-elements 1.20.0 → 1.21.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 CHANGED
@@ -4,6 +4,65 @@ 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.21.0] – 2026-10-18
8
+
9
+ Built against NotionKit CSS 1.21.0 (peer `>= 1.21.0`).
10
+
11
+ ### Added
12
+ - **Select all from the header of a checkbox column (#4).** A `checkbox`
13
+ column with `selectAll: true` is a selection column: its header holds the
14
+ cells' own checkbox instead of a name – empty when no row is chosen, a dash
15
+ for some, a check for all (`th.select-all`, NotionKit 1.21.0) – and sorts
16
+ nothing. A click or Space sets the column for every row the view has – a
17
+ dash selects all, as in Notion –, renders the cells again and fires **one**
18
+ `nk-change { key, value, all: true, rows }`, so an app that keeps its own
19
+ selection follows with one update instead of one per row; Space leaves the
20
+ focus on the header box. A row's own box keeps the header in step without
21
+ losing its focus. Without rows the header box is disabled. What "all" means
22
+ beyond the view's rows – more pages, a filter on the server – stays with
23
+ the app. Its accessible name is the new built-in text `selectAll`
24
+ ("Select all" / "Alle auswählen"). Requested by Mailverse for its
25
+ quarantine list.
26
+ - `<nk-check indeterminate>` finally shows its mixed state – a dash in the
27
+ accent box (NotionKit 1.21.0); it looked empty before. The docs show it
28
+ next to the table's selection column.
29
+
30
+ ### Fixed
31
+ - Through NotionKit 1.21.0: a table row with a checkbox cell was 39.5px
32
+ instead of 36px; a selection column would have grown every row.
33
+
34
+ ### Tests
35
+ - The parity test runs a script in the class markup too, for what only a
36
+ script sets – an `indeterminate` checkbox – as the docs page does.
37
+
38
+ ## [1.20.1] – 2026-10-14
39
+
40
+ Elements only; NotionKit CSS stays at 1.20.0 (peer `>= 1.20.0`).
41
+
42
+ ### Fixed
43
+ - **`<nk-tooltip>` follows the pointer and the focus inside a shadow root
44
+ (#3).** Moving from one `[data-tooltip]` to the next in the same shadow
45
+ root never updated the tooltip – the first one stayed, or none appeared –
46
+ for the pointer and for Tab alike; `<nk-copy-field icons secret>` kept
47
+ saying "Show" over Copy, an app's toolbar or chart bars in one root did
48
+ the same. `pointerover`, `focusin` and `focusout` carry a `relatedTarget`,
49
+ and when target and `relatedTarget` share a shadow tree the DOM standard
50
+ ends the event path at its host, so the listeners on the document never
51
+ heard of the move. The tooltip now also reads `pointermove` (no
52
+ `relatedTarget`, it always reaches the document) and acts only when the
53
+ element under the pointer changes, so a hand resting on the page leaves a
54
+ tooltip the keyboard or `show(rect)` opened alone; and it catches a key
55
+ that moved the focus – Tab, Option+Tab in Safari, the arrow keys of a
56
+ roving tab stop that moves the focus in its own `keydown` – on `keyup`,
57
+ against the focus recorded before the key went down. Nested shadow roots
58
+ are followed too. A focus move by script alone inside one shadow root
59
+ stays unseen. Reported by Mailverse.
60
+ - **Leaving the window** over a `[data-tooltip]` left its tooltip standing,
61
+ and one still waiting appeared after the pointer had gone. It now goes
62
+ with the pointer (`pointerout` without a `relatedTarget`); a tooltip the
63
+ keyboard or `show(rect)` opened is not the pointer's and stays. Found
64
+ while testing #3.
65
+
7
66
  ## [1.20.0] – 2026-10-12
8
67
 
9
68
  Built against NotionKit CSS 1.20.0 (peer `>= 1.20.0`).
@@ -778,6 +837,8 @@ built and tested against NotionKit CSS 1.1.1 (peer range `>= 1.0.0`).
778
837
  import, never in the core bundle – shadow-less, adding `nk-block-host` to
779
838
  itself so the foundation's editor adapter rules apply.
780
839
 
840
+ [1.21.0]: https://github.com/JUNGHERZ/NotionKit-Elements/releases/tag/v1.21.0
841
+ [1.20.1]: https://github.com/JUNGHERZ/NotionKit-Elements/releases/tag/v1.20.1
781
842
  [1.20.0]: https://github.com/JUNGHERZ/NotionKit-Elements/releases/tag/v1.20.0
782
843
  [1.19.0]: https://github.com/JUNGHERZ/NotionKit-Elements/releases/tag/v1.19.0
783
844
  [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.21.0-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.21.0-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
 
@@ -71,8 +71,8 @@ It is the **app layer** of the NotionKit family – three layers, one design lan
71
71
  <!DOCTYPE html>
72
72
  <html lang="en" data-theme="light">
73
73
  <head>
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>
74
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit@1.21.0/notionkit.min.css">
75
+ <script src="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit-elements@1.21.0/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">
@@ -104,7 +104,7 @@ Without a build step, an import map can point at the minified module instead: `"
104
104
 
105
105
  Do not mix the full bundle with the per-component files: each brings its own `NkElement`. With the bundle, take `componentsSheet` and `NkElement` from the bundle (`NotionKitElements.componentsSheet` from the `<script>` build).
106
106
 
107
- Two prerequisites, always: `notionkit.css` on the document (the peer dependency, `>= 1.19.1`; 1.19.1 keeps a toast with an × at its height, 1.19.0 brings the toast with an action, 1.18.0 brings a bar and its label in one row, stacked properties that keep to their card on a phone and icons in the copy field, 1.17.0 brings a step's label and a gallery card as links, 1.16.0 brings `--nk-block-space` and `flush` for blocks in a column, keeps a wide progress bar and a stretched button's label in shape and lays floating menus above dialogs, 1.15.0 brings table text with a tone and a second line, 1.14.0 brings the gallery view and a banner action that stands at the right edge again, 1.13.0 keeps fields inside narrow panels, leaves a page without icon its padding under the cover and makes cover heights tokens, 1.12.0 brings a tag or button beside a panel's title, lists and grids without outer margin, text values, steps with states in any order and in a row, scrolling tabs, a cap for full pages and buttons per table row, 1.11.0 keeps a floating menu inside an open modal or dialog in place and inside the window and brings the banner's action as a button, fields that share a row and tooltips of several lines, 1.10.1 lets a disabled button show its tooltip, 1.10.0 brings the collapsing sidebar, the resizable side peek, the dialog and the tooltip, 1.9.0 the date picker and the calendar view, 1.8.0 the side peek, the bookmark, the copy field and pictures on the profile row, 1.7.0 the sheet, menus that float in and become sheets on a phone, the sidebar drawer, the database toolbar with filter pills and the steps, 1.7.1 minified files that carry all of it, 1.6.0 page properties, page options, list view, panels, prose and the avatar, 1.5.1 the safe areas, 1.5.0 the Notion-2025 tokens, the nine tag colours and the filled inputs; from 1.5.0 on, Elements and the foundation share one version number – the elements are built and tested against the same release) and `class="nk-body"` on `<body>`. The elements ship no visual CSS of their own – every rule comes from the foundation. The bundle injects the design tokens once as `@layer notionkit-defaults`, so any plain `:root { --nk-accent: … }` of yours re-brands every element in both themes.
107
+ Two prerequisites, always: `notionkit.css` on the document (the peer dependency, `>= 1.21.0`; 1.21.0 brings the select-all header of a table and the dash of an indeterminate checkbox and keeps a row with a checkbox at 36px, 1.20.0 brings the appearance glyph of a three-state theme toggle, 1.19.1 keeps a toast with an × at its height, 1.19.0 brings the toast with an action, 1.18.0 brings a bar and its label in one row, stacked properties that keep to their card on a phone and icons in the copy field, 1.17.0 brings a step's label and a gallery card as links, 1.16.0 brings `--nk-block-space` and `flush` for blocks in a column, keeps a wide progress bar and a stretched button's label in shape and lays floating menus above dialogs, 1.15.0 brings table text with a tone and a second line, 1.14.0 brings the gallery view and a banner action that stands at the right edge again, 1.13.0 keeps fields inside narrow panels, leaves a page without icon its padding under the cover and makes cover heights tokens, 1.12.0 brings a tag or button beside a panel's title, lists and grids without outer margin, text values, steps with states in any order and in a row, scrolling tabs, a cap for full pages and buttons per table row, 1.11.0 keeps a floating menu inside an open modal or dialog in place and inside the window and brings the banner's action as a button, fields that share a row and tooltips of several lines, 1.10.1 lets a disabled button show its tooltip, 1.10.0 brings the collapsing sidebar, the resizable side peek, the dialog and the tooltip, 1.9.0 the date picker and the calendar view, 1.8.0 the side peek, the bookmark, the copy field and pictures on the profile row, 1.7.0 the sheet, menus that float in and become sheets on a phone, the sidebar drawer, the database toolbar with filter pills and the steps, 1.7.1 minified files that carry all of it, 1.6.0 page properties, page options, list view, panels, prose and the avatar, 1.5.1 the safe areas, 1.5.0 the Notion-2025 tokens, the nine tag colours and the filled inputs; from 1.5.0 on, Elements and the foundation share one version number – the elements are built and tested against the same release) and `class="nk-body"` on `<body>`. The elements ship no visual CSS of their own – every rule comes from the foundation. The bundle injects the design tokens once as `@layer notionkit-defaults`, so any plain `:root { --nk-accent: … }` of yours re-brands every element in both themes.
108
108
 
109
109
  ## Elements
110
110
 
@@ -161,6 +161,7 @@ The elements on the page take new texts at once, after `setStrings()` and when `
161
161
  | `commandPlaceholder` | `nk-cmdk` | `placeholder` | Search or type a command … | Suchen oder Befehl eingeben … |
162
162
  | `emojiPlaceholder` | `nk-emoji-picker` | `placeholder` | Search… | Suchen … |
163
163
  | `toggleTheme` | `nk-theme-toggle` | `title` | Toggle light / dark | Hell / Dunkel |
164
+ | `selectAll` | `nk-table-view` (a `checkbox` column with `selectAll`) | – | Select all | Alle auswählen |
164
165
  | `appearance` | `nk-theme-toggle` (`modes` with `auto`) | `title` | Appearance: {mode} – click: {next} | Darstellung: {mode} – Klick: {next} |
165
166
  | `themeAuto` | `nk-theme-toggle` (`modes` with `auto`) | – | automatic (follows the system) | automatisch (wie das System) |
166
167
  | `themeLight` | `nk-theme-toggle` (`modes` with `auto`) | – | light | hell |
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.21.0) wrapping NotionKit CSS v1.21.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
@@ -21,8 +21,8 @@ description: NotionKit Elements is a vanilla-JS Web Components library (v1.20.0)
21
21
  <head>
22
22
  <meta charset="UTF-8">
23
23
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
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>
24
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit@1.21.0/notionkit.min.css">
25
+ <script src="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit-elements@1.21.0/dist/notionkit-elements.min.js"></script>
26
26
  </head>
27
27
  <body class="nk-body">
28
28
  <nk-btn variant="primary">Save</nk-btn>
@@ -412,9 +412,10 @@ Renders `button.nk-switch[role=switch]`; the stylesheet keys the knob on `aria-c
412
412
 
413
413
  ### 3.9 `<nk-check>` – Checkbox
414
414
 
415
- A `label.nk-check` with a custom-drawn checkbox; the label text is slotted, so clicking it toggles the box.
415
+ A `label.nk-check` with a custom-drawn checkbox; the label text is slotted, so clicking it toggles the box. `indeterminate` shows a dash (NotionKit 1.21.0) – for a box over a group partly chosen.
416
416
 
417
417
  ```html
418
+ <nk-check indeterminate>All</nk-check>
418
419
  <nk-check name="digest" value="weekly" checked>Weekly digest</nk-check>
419
420
  <nk-check name="digest" value="mentions">Mentions only</nk-check>
420
421
  ```
@@ -437,6 +438,7 @@ A `label.nk-check` with a custom-drawn checkbox; the label text is slotted, so c
437
438
 
438
439
  ```html
439
440
  <!-- equivalent class markup -->
441
+ <label class="nk-check"><input type="checkbox">All</label><script>document.currentScript.previousElementSibling.querySelector('input').indeterminate = true</script>
440
442
  <label class="nk-check"><input type="checkbox" name="digest" value="weekly" checked>Weekly digest</label>
441
443
  <label class="nk-check"><input type="checkbox" name="digest" value="mentions">Mentions only</label>
442
444
  ```
@@ -2562,7 +2564,7 @@ One inverted pill at the bottom centre, above every overlay; over a tab bar at t
2562
2564
 
2563
2565
  ### 3.76 `<nk-tooltip>` – Tooltip
2564
2566
 
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.
2567
+ 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
2568
 
2567
2569
  ```html
2568
2570
  <div style="display:flex;gap:16px;align-items:center;padding:4px 0 44px">
@@ -2659,13 +2661,14 @@ The view switcher with Notion’s toolbar: child views (`nk-table-view`, `nk-boa
2659
2661
 
2660
2662
  ### 3.78 `<nk-table-view>` – Table view
2661
2663
 
2662
- Renders `columns` × `rows` as the NotionKit table. Cells are polymorphic (`text`, `select`, `multi-select`, `date`, `person`, `checkbox`, `url`, `number`, `progress`) and rendered as plain markup by the exported `renderPropertyCell()` – every cell rule starts with `.nk-table`, so a cell element of its own would never be styled. Header clicks sort with `sortable`. A `number` column stands right-aligned in figures of equal width, formatted by its `locale` and `format` (Intl.NumberFormat options); a person’s `color` takes one of the nine names. A column of type `actions` (NotionKit 1.12.0) sets buttons in each row from `column.actions` – `[{ action, label, icon, danger, disabled, tooltip }]` –, and a row’s value – a list of action names – picks which of them it shows; a click fires `nk-action { action, row, id, anchor }` instead of selecting the row. A `url` value may be `{ href, label, target }`: a link of your own, to another page of the app, with its own text. A text value may be `{ text, desc, color, tooltip }` (NotionKit 1.15.0): a quiet second line, one of Notion’s nine text colours – `orange` for an error –, the whole text in a tooltip. Sorting goes by the value, not the text shown: dates by their time, selects by the order of their options, empty cells last; `sortKey` on a column sorts by another field of the row, a `sort` in a cell’s object by that value. A `date` column with `format` – `'short'`, `'relative'` or the options of Intl.DateTimeFormat – shows ISO dates and date-times formatted in its `locale`.
2664
+ Renders `columns` × `rows` as the NotionKit table. Cells are polymorphic (`text`, `select`, `multi-select`, `date`, `person`, `checkbox`, `url`, `number`, `progress`) and rendered as plain markup by the exported `renderPropertyCell()` – every cell rule starts with `.nk-table`, so a cell element of its own would never be styled. Header clicks sort with `sortable`. A `number` column stands right-aligned in figures of equal width, formatted by its `locale` and `format` (Intl.NumberFormat options); a person’s `color` takes one of the nine names. A column of type `actions` (NotionKit 1.12.0) sets buttons in each row from `column.actions` – `[{ action, label, icon, danger, disabled, tooltip }]` –, and a row’s value – a list of action names – picks which of them it shows; a click fires `nk-action { action, row, id, anchor }` instead of selecting the row. A `url` value may be `{ href, label, target }`: a link of your own, to another page of the app, with its own text. A text value may be `{ text, desc, color, tooltip }` (NotionKit 1.15.0): a quiet second line, one of Notion’s nine text colours – `orange` for an error –, the whole text in a tooltip. Sorting goes by the value, not the text shown: dates by their time, selects by the order of their options, empty cells last; `sortKey` on a column sorts by another field of the row, a `sort` in a cell’s object by that value. A `date` column with `format` – `'short'`, `'relative'` or the options of Intl.DateTimeFormat – shows ISO dates and date-times formatted in its `locale`. A `checkbox` column with `selectAll: true` (1.21.0) is a selection column: its header is a checkbox – empty, a dash for some rows, a check for all – that sorts nothing; a click or Space sets the column for every row the view has (a dash selects all, as in Notion) and fires one `nk-change { key, value, all: true, rows }`. What “all” means beyond these rows – more pages, a filter on the server – stays with the app. Its name comes from the texts (`selectAll`).
2663
2665
 
2664
2666
  ```html
2665
2667
  <nk-table-view new-row sortable></nk-table-view>
2666
2668
  <script>{
2667
2669
  const db = document.currentScript.previousElementSibling;
2668
2670
  db.columns = [
2671
+ { key: 'selected', type: 'checkbox', selectAll: true },
2669
2672
  { key: 'name', label: 'Name', type: 'text', icon: '📄', title: true },
2670
2673
  { key: 'status', label: 'Status', type: 'select', icon: '◉', options: [
2671
2674
  { value: 'planned', label: 'Planned', color: 'orange' }, { value: 'progress', label: 'In progress', color: 'blue' }, { value: 'done', label: 'Done', color: 'green' } ] },
@@ -2676,9 +2679,9 @@ Renders `columns` × `rows` as the NotionKit table. Cells are polymorphic (`text
2676
2679
  { key: 'actions', type: 'actions', actions: [{ action: 'open', label: 'Open' }, { action: 'archive', label: 'Archive', danger: true }] },
2677
2680
  ];
2678
2681
  db.rows = [
2679
- { id: 1, icon: '🧭', name: { text: 'App shell & sidebar', desc: 'Three open questions' }, status: 'done', owner: { name: 'Marcel', initials: 'MK', color: 'purple' }, due: '08.05.2026', progress: 100, effort: 6, cover: '/covers/aurora.svg' },
2682
+ { id: 1, icon: '🧭', name: { text: 'App shell & sidebar', desc: 'Three open questions' }, status: 'done', owner: { name: 'Marcel', initials: 'MK', color: 'purple' }, due: '08.05.2026', progress: 100, effort: 6, cover: '/covers/aurora.svg', selected: true },
2680
2683
  { id: 2, icon: '📄', name: 'Page shell & typography', status: 'done', owner: { name: 'Marcel', initials: 'MK', color: 'purple' }, due: '10.05.2026', progress: 100, effort: 4.5, cover: '/covers/dunes.svg' },
2681
- { id: 3, icon: '🗃️', name: 'Database table view', status: 'progress', owner: { name: 'Marcel', initials: 'MK', color: 'purple' }, due: '20.05.2026', progress: 65, effort: 12.5, cover: '/covers/meadow.svg' },
2684
+ { id: 3, icon: '🗃️', name: 'Database table view', status: 'progress', owner: { name: 'Marcel', initials: 'MK', color: 'purple' }, due: '20.05.2026', progress: 65, effort: 12.5, cover: '/covers/meadow.svg', selected: true },
2682
2685
  { id: 4, icon: '▤', name: 'Board view & drag-and-drop', status: 'planned', due: '02.06.2026', progress: 0, effort: 8, actions: ['open'], cover: '/covers/tide.svg' },
2683
2686
  ];
2684
2687
  }</script>
@@ -2697,23 +2700,23 @@ Renders `columns` × `rows` as the NotionKit table. Cells are polymorphic (`text
2697
2700
  | `sort-dir` | asc | desc | – | Direction. |
2698
2701
  | `wrap` | boolean | – | Cell text may break (Notion's "wrap column"). |
2699
2702
 
2700
- **Events:** `nk-select` `{ row, id, key, cell }` – Row clicked. · `nk-change` `{ row, key, value }` – Checkbox cell toggled (row updated in place). · `nk-action` `{ action: 'sort' | 'new-row' | an actions column's action, key?, value?, row?, id?, anchor? }` – Header, add row or a row’s button clicked.
2703
+ **Events:** `nk-select` `{ row, id, key, cell }` – Row clicked. · `nk-change` `{ row, key, value } · selectAll: { key, value, all: true, rows }` – Checkbox cell toggled (row updated in place); from a selectAll header once for every row. · `nk-action` `{ action: 'sort' | 'new-row' | an actions column's action, key?, value?, row?, id?, anchor? }` – Header, add row or a row’s button clicked.
2701
2704
 
2702
2705
  **Properties:** `columns`, `rows`, `data` · **Methods:** `refresh()`
2703
2706
 
2704
- **Replaces:** `.nk-table-wrap`, `.nk-table`, `.wrap`, `.th-icon`, `.row-title`, `.date-cell`, `.person-cell`, `.nk-avatar`, `.num`, `.row-actions`, `.actions`, `.td-text`, `.td-desc`, `.nk-new-row`
2707
+ **Replaces:** `.nk-table-wrap`, `.nk-table`, `.wrap`, `.th-icon`, `.row-title`, `.date-cell`, `.person-cell`, `.nk-avatar`, `.num`, `.row-actions`, `.actions`, `.select-all`, `.nk-check`, `.td-text`, `.td-desc`, `.nk-new-row`
2705
2708
 
2706
2709
  ```html
2707
2710
  <!-- equivalent class markup -->
2708
2711
  <div class="nk-table-wrap"><table class="nk-table">
2709
- <thead><tr><th><span class="th-icon">📄</span>Name</th><th><span class="th-icon">◉</span>Status</th><th><span class="th-icon">👤</span>Owner</th><th><span class="th-icon">📅</span>Due</th><th><span class="th-icon">▰</span>Progress</th><th><span class="th-icon">#</span>Effort (h)</th><th class="actions"></th></tr></thead>
2712
+ <thead><tr><th class="select-all"><label class="nk-check"><input type="checkbox" aria-label="Select all"></label></th><th><span class="th-icon">📄</span>Name</th><th><span class="th-icon">◉</span>Status</th><th><span class="th-icon">👤</span>Owner</th><th><span class="th-icon">📅</span>Due</th><th><span class="th-icon">▰</span>Progress</th><th><span class="th-icon">#</span>Effort (h)</th><th class="actions"></th></tr></thead>
2710
2713
  <tbody>
2711
- <tr><td><span class="row-title">🧭 App shell & sidebar</span><span class="td-desc">Three open questions</span></td><td><span class="nk-tag green">Done</span></td><td><span class="person-cell"><span class="nk-avatar small purple">MK</span> Marcel</span></td><td><span class="date-cell">08.05.2026</span></td><td><span class="nk-progress"><i style="width:100%"></i></span><span class="nk-progress-label">100%</span></td><td class="num"><span>6.0</span></td><td><span class="row-actions"><button class="nk-btn secondary small">Open</button><button class="nk-btn danger small">Archive</button></span></td></tr>
2712
- <tr><td><span class="row-title">📄 Page shell & typography</span></td><td><span class="nk-tag green">Done</span></td><td><span class="person-cell"><span class="nk-avatar small purple">MK</span> Marcel</span></td><td><span class="date-cell">10.05.2026</span></td><td><span class="nk-progress"><i style="width:100%"></i></span><span class="nk-progress-label">100%</span></td><td class="num"><span>4.5</span></td><td><span class="row-actions"><button class="nk-btn secondary small">Open</button><button class="nk-btn danger small">Archive</button></span></td></tr>
2713
- <tr><td><span class="row-title">🗃️ Database table view</span></td><td><span class="nk-tag blue">In progress</span></td><td><span class="person-cell"><span class="nk-avatar small purple">MK</span> Marcel</span></td><td><span class="date-cell">20.05.2026</span></td><td><span class="nk-progress"><i style="width:65%"></i></span><span class="nk-progress-label">65%</span></td><td class="num"><span>12.5</span></td><td><span class="row-actions"><button class="nk-btn secondary small">Open</button><button class="nk-btn danger small">Archive</button></span></td></tr>
2714
- <tr><td><span class="row-title">▤ Board view & drag-and-drop</span></td><td><span class="nk-tag orange">Planned</span></td><td><span class="person-cell">—</span></td><td><span class="date-cell">02.06.2026</span></td><td><span class="nk-progress"><i style="width:0%"></i></span><span class="nk-progress-label">0%</span></td><td class="num"><span>8.0</span></td><td><span class="row-actions"><button class="nk-btn secondary small">Open</button></span></td></tr>
2714
+ <tr><td><label class="nk-check"><input type="checkbox" checked></label></td><td><span class="row-title">🧭 App shell & sidebar</span><span class="td-desc">Three open questions</span></td><td><span class="nk-tag green">Done</span></td><td><span class="person-cell"><span class="nk-avatar small purple">MK</span> Marcel</span></td><td><span class="date-cell">08.05.2026</span></td><td><span class="nk-progress"><i style="width:100%"></i></span><span class="nk-progress-label">100%</span></td><td class="num"><span>6.0</span></td><td><span class="row-actions"><button class="nk-btn secondary small">Open</button><button class="nk-btn danger small">Archive</button></span></td></tr>
2715
+ <tr><td><label class="nk-check"><input type="checkbox"></label></td><td><span class="row-title">📄 Page shell & typography</span></td><td><span class="nk-tag green">Done</span></td><td><span class="person-cell"><span class="nk-avatar small purple">MK</span> Marcel</span></td><td><span class="date-cell">10.05.2026</span></td><td><span class="nk-progress"><i style="width:100%"></i></span><span class="nk-progress-label">100%</span></td><td class="num"><span>4.5</span></td><td><span class="row-actions"><button class="nk-btn secondary small">Open</button><button class="nk-btn danger small">Archive</button></span></td></tr>
2716
+ <tr><td><label class="nk-check"><input type="checkbox" checked></label></td><td><span class="row-title">🗃️ Database table view</span></td><td><span class="nk-tag blue">In progress</span></td><td><span class="person-cell"><span class="nk-avatar small purple">MK</span> Marcel</span></td><td><span class="date-cell">20.05.2026</span></td><td><span class="nk-progress"><i style="width:65%"></i></span><span class="nk-progress-label">65%</span></td><td class="num"><span>12.5</span></td><td><span class="row-actions"><button class="nk-btn secondary small">Open</button><button class="nk-btn danger small">Archive</button></span></td></tr>
2717
+ <tr><td><label class="nk-check"><input type="checkbox"></label></td><td><span class="row-title">▤ Board view & drag-and-drop</span></td><td><span class="nk-tag orange">Planned</span></td><td><span class="person-cell">—</span></td><td><span class="date-cell">02.06.2026</span></td><td><span class="nk-progress"><i style="width:0%"></i></span><span class="nk-progress-label">0%</span></td><td class="num"><span>8.0</span></td><td><span class="row-actions"><button class="nk-btn secondary small">Open</button></span></td></tr>
2715
2718
  </tbody>
2716
- </table><div class="nk-new-row">+ New page</div></div>
2719
+ </table><div class="nk-new-row">+ New page</div></div><script>document.currentScript.previousElementSibling.querySelector('th.select-all input').indeterminate = true</script>
2717
2720
  ```
2718
2721
 
2719
2722
  **Small screens:** Scrolls horizontally inside `.nk-table-wrap`.
@@ -3152,8 +3155,8 @@ Eight skeletons, one per app shape, mirroring the NotionKit CSS SKILL.md. Copy o
3152
3155
  <html lang="en" data-theme="light">
3153
3156
  <head>
3154
3157
  <meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
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>
3158
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit@1.21.0/notionkit.min.css">
3159
+ <script src="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit-elements@1.21.0/dist/notionkit-elements.min.js"></script>
3157
3160
  </head>
3158
3161
  <body class="nk-body">
3159
3162
  <nk-app>
@@ -3234,8 +3237,8 @@ Rules of the shell: `nk-sidebar`, `nk-topbar` and `nk-page` are `display: conten
3234
3237
  <html lang="en" data-theme="light">
3235
3238
  <head>
3236
3239
  <meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
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>
3240
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit@1.21.0/notionkit.min.css">
3241
+ <script src="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit-elements@1.21.0/dist/notionkit-elements.min.js"></script>
3239
3242
  </head>
3240
3243
  <body class="nk-body">
3241
3244
  <nk-app>
@@ -3342,8 +3345,8 @@ Data contract: `columns` describe the properties (`type`: text | select | multi-
3342
3345
  <html lang="en" data-theme="light">
3343
3346
  <head>
3344
3347
  <meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
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>
3348
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit@1.21.0/notionkit.min.css">
3349
+ <script src="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit-elements@1.21.0/dist/notionkit-elements.min.js"></script>
3347
3350
  </head>
3348
3351
  <body class="nk-body">
3349
3352
  <nk-app>
@@ -3402,8 +3405,8 @@ Data contract: `columns` describe the properties (`type`: text | select | multi-
3402
3405
  <html lang="en" data-theme="light">
3403
3406
  <head>
3404
3407
  <meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
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>
3408
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit@1.21.0/notionkit.min.css">
3409
+ <script src="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit-elements@1.21.0/dist/notionkit-elements.min.js"></script>
3407
3410
  </head>
3408
3411
  <body class="nk-body">
3409
3412
  <!-- your app -->
@@ -3480,8 +3483,8 @@ The open/close contract is one attribute: `settings.open = true`, `settings.show
3480
3483
  <html lang="en" data-theme="light">
3481
3484
  <head>
3482
3485
  <meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
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>
3486
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit@1.21.0/notionkit.min.css">
3487
+ <script src="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit-elements@1.21.0/dist/notionkit-elements.min.js"></script>
3485
3488
  </head>
3486
3489
  <body class="nk-body">
3487
3490
  <div class="nk-page" style="padding-top:48px">
@@ -3527,8 +3530,8 @@ The open/close contract is one attribute: `settings.open = true`, `settings.show
3527
3530
  <html lang="en" data-theme="light">
3528
3531
  <head>
3529
3532
  <meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
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>
3533
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit@1.21.0/notionkit.min.css">
3534
+ <script src="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit-elements@1.21.0/dist/notionkit-elements.min.js"></script>
3532
3535
  </head>
3533
3536
  <body class="nk-body">
3534
3537
  <nk-page narrow icon="📘" cover>
@@ -3572,8 +3575,8 @@ Note `narrow`: the page is the document, so there is no inner scroll wrapper –
3572
3575
  <html lang="en" data-theme="light">
3573
3576
  <head>
3574
3577
  <meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
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>
3578
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit@1.21.0/notionkit.min.css">
3579
+ <script src="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit-elements@1.21.0/dist/notionkit-elements.min.js"></script>
3577
3580
  </head>
3578
3581
  <body class="nk-body">
3579
3582
  <nk-app>
@@ -3627,8 +3630,8 @@ Note `narrow`: the page is the document, so there is no inner scroll wrapper –
3627
3630
  <html lang="en" data-theme="light">
3628
3631
  <head>
3629
3632
  <meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
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>
3633
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit@1.21.0/notionkit.min.css">
3634
+ <script src="https://cdn.jsdelivr.net/npm/@jungherz-de/notionkit-elements@1.21.0/dist/notionkit-elements.min.js"></script>
3632
3635
  </head>
3633
3636
  <body class="nk-body">
3634
3637
  <!-- A narrow column, centred: layout is yours, so it is inline. -->
@@ -3839,10 +3842,10 @@ Form controls additionally re-dispatch a native, bubbling `change` event, so `fo
3839
3842
  | 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
3843
  | Components | `src/components/{forms,content,shell,page,overlays,data}/nk-*.js`, one tag per file, `customElements.define` at the bottom |
3841
3844
  | 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 |
3845
+ | Peer | `@jungherz-de/notionkit >= 1.21.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 |
3843
3846
 
3844
3847
  Lifecycle: construct (attach shadow, adopt sheets) → first connect (wrapper + `render()`) → every connect (`setupEvents()`, theme registration, light-DOM observer) → `attributeChangedCallback` → `onAttributeChanged` → disconnect (`teardownEvents()`, unregister).
3845
3848
 
3846
3849
 
3847
3850
  ---
3848
- *NotionKit Elements v1.20.0 · wrapping NotionKit CSS v1.20.0 · MIT · Jungherz GmbH*
3851
+ *NotionKit Elements v1.21.0 · wrapping NotionKit CSS v1.21.0 · MIT · Jungherz GmbH*
@@ -21,7 +21,7 @@ const BUILT_IN = {
21
21
  appearance: 'Appearance: {mode} – click: {next}', themeAuto: 'automatic (follows the system)', themeLight: 'light', themeDark: 'dark',
22
22
  message: 'Message', breadcrumb: 'Breadcrumb', commandPalette: 'Command palette', comment: 'Comment',
23
23
  searchEmoji: 'Search emoji', settings: 'Settings', title: 'Title', changeIcon: 'Change icon',
24
- add: 'Add', more: 'More',
24
+ add: 'Add', more: 'More', selectAll: 'Select all',
25
25
  },
26
26
  de: {
27
27
  copy: 'Kopieren', copied: 'Kopiert', show: 'Zeigen', hide: 'Verbergen',
@@ -36,7 +36,7 @@ const BUILT_IN = {
36
36
  appearance: 'Darstellung: {mode} – Klick: {next}', themeAuto: 'automatisch (wie das System)', themeLight: 'hell', themeDark: 'dunkel',
37
37
  message: 'Nachricht', breadcrumb: 'Pfad', commandPalette: 'Befehlspalette', comment: 'Kommentar',
38
38
  searchEmoji: 'Emoji suchen', settings: 'Einstellungen', title: 'Titel', changeIcon: 'Symbol ändern',
39
- add: 'Hinzufügen', more: 'Mehr',
39
+ add: 'Hinzufügen', more: 'Mehr', selectAll: 'Alle auswählen',
40
40
  },
41
41
  };
42
42
  const custom = {};
@@ -16,6 +16,15 @@ import './shared/dates-a32DcW1l.js';
16
16
  // type 'actions' (NotionKit 1.12.0) sets buttons in each row; a click on one
17
17
  // fires nk-action { action, row, id, anchor } instead of selecting the row,
18
18
  // and its header sorts nothing.
19
+ //
20
+ // A checkbox column with `selectAll: true` (1.21.0) is a selection column:
21
+ // its header holds a checkbox instead of a name – empty, a dash for some
22
+ // rows, a check for all – and sorts nothing (th.select-all, NotionKit
23
+ // 1.21.0). A click or Space sets the column for every row the view has –
24
+ // a dash selects all, as Notion does –, renders again and fires one
25
+ // nk-change { key, value, all: true, rows }, so an app that keeps its own
26
+ // selection follows with one update. What "all" means beyond these rows –
27
+ // more pages, a filter on the server – stays with the app.
19
28
  class NkTableView extends NkElement {
20
29
  static get observedAttributes() { return ['name', 'label', 'badge', 'count', 'new-row', 'new-row-label', 'sortable', 'sort-key', 'sort-dir', 'wrap']; }
21
30
 
@@ -45,6 +54,7 @@ class NkTableView extends NkElement {
45
54
  this._table.classList.toggle('wrap', this.getBoolAttr('wrap'));
46
55
  const tr = document.createElement('tr');
47
56
  for (const col of this._columns) {
57
+ if (col.type === 'checkbox' && col.selectAll) { tr.appendChild(this._selectAllHeader(col)); continue; }
48
58
  const actions = col.type === 'actions';
49
59
  const th = this.createElement('th', actions ? ['actions'] : [], actions ? { scope: 'col' } : { 'data-key': col.key, scope: 'col' });
50
60
  if (col.icon) { const i = this.createElement('span', ['th-icon']); i.textContent = col.icon; th.appendChild(i); }
@@ -68,6 +78,40 @@ class NkTableView extends NkElement {
68
78
  }
69
79
  this._newRow.textContent = this.getAttribute('new-row-label') || this.str('newPage');
70
80
  this._newRow.style.display = this.getBoolAttr('new-row') ? '' : 'none';
81
+ this._syncSelectAll();
82
+ }
83
+
84
+ /** The header of a selection column: the cells' own checkbox, named "Select all". */
85
+ _selectAllHeader(col) {
86
+ const th = this.createElement('th', ['select-all'], { scope: 'col' });
87
+ const label = this.createElement('label', ['nk-check']);
88
+ label.appendChild(this.createElement('input', [], { type: 'checkbox', 'aria-label': this.str('selectAll'), 'data-select-all': col.key }));
89
+ th.appendChild(label);
90
+ if (col.width) th.style.width = col.width;
91
+ return th;
92
+ }
93
+
94
+ /** Every header box shows its column: checked for all rows, a dash for some, empty for none – and nothing to choose without rows. */
95
+ _syncSelectAll() {
96
+ for (const input of this._thead?.querySelectorAll('input[data-select-all]') ?? []) {
97
+ const key = input.dataset.selectAll, on = this._rows.filter(r => r[key]).length;
98
+ input.checked = on > 0 && on === this._rows.length;
99
+ input.indeterminate = on > 0 && on < this._rows.length;
100
+ input.disabled = !this._rows.length;
101
+ }
102
+ }
103
+
104
+ /** A click or Space on a header box: the column for every row, one event. */
105
+ _selectAll(input) {
106
+ const key = input.dataset.selectAll;
107
+ // A dash selects all, as Notion does; only a full column clears.
108
+ const value = !(this._rows.length && this._rows.every(r => r[key]));
109
+ for (const row of this._rows) row[key] = value;
110
+ const focused = this._shadow.activeElement === input;
111
+ this._render();
112
+ // Space should leave the focus where it was – on the new header box.
113
+ if (focused) [...this._thead.querySelectorAll('input[data-select-all]')].find(i => i.dataset.selectAll === key)?.focus();
114
+ this.emit('nk-change', { key, value, all: true, rows: this._rows });
71
115
  }
72
116
 
73
117
  setupEvents() {
@@ -98,8 +142,10 @@ class NkTableView extends NkElement {
98
142
  this._onChange = (e) => {
99
143
  const input = e.target;
100
144
  if (input.type !== 'checkbox') return;
145
+ if (input.dataset.selectAll != null) { this._selectAll(input); return; }
101
146
  const row = this._rowById(input.dataset.rowId);
102
147
  if (row) row[input.dataset.key] = input.checked;
148
+ this._syncSelectAll();
103
149
  this.emit('nk-change', { row, key: input.dataset.key, value: input.checked });
104
150
  };
105
151
  this._onKey = (e) => { if ((e.key === 'Enter' || e.key === ' ') && e.target === this._newRow) { e.preventDefault(); this.emit('nk-action', { action: 'new-row' }); } };
@@ -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(); }