kempo-ui 0.4.25 → 0.4.27

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.
Files changed (91) hide show
  1. package/dist/components/Context.js +1 -1
  2. package/dist/utils/closestAcrossShadow.js +1 -0
  3. package/docs/components/accordion.html +3 -0
  4. package/docs/components/aside.html +3 -0
  5. package/docs/components/button.html +3 -0
  6. package/docs/components/calendar.html +3 -0
  7. package/docs/components/card.html +3 -0
  8. package/docs/components/chat.html +3 -0
  9. package/docs/components/code-editor.html +3 -0
  10. package/docs/components/color-picker.html +3 -0
  11. package/docs/components/combobox.html +3 -0
  12. package/docs/components/content-slider.html +3 -0
  13. package/docs/components/context.html +20 -0
  14. package/docs/components/dialog.html +3 -0
  15. package/docs/components/dropdown.html +3 -0
  16. package/docs/components/filter-list.html +3 -0
  17. package/docs/components/focus-capture.html +3 -0
  18. package/docs/components/html-editor.html +3 -0
  19. package/docs/components/hybrid-component.html +3 -0
  20. package/docs/components/icon.html +3 -0
  21. package/docs/components/import.html +3 -0
  22. package/docs/components/light-component.html +3 -0
  23. package/docs/components/markdown-editor.html +3 -0
  24. package/docs/components/markdown.html +3 -0
  25. package/docs/components/nav-spacer.html +3 -0
  26. package/docs/components/nav.html +3 -0
  27. package/docs/components/pagination.html +3 -0
  28. package/docs/components/photo-viewer.html +3 -0
  29. package/docs/components/progress.html +3 -0
  30. package/docs/components/resize.html +3 -0
  31. package/docs/components/segmented-control.html +3 -0
  32. package/docs/components/shadow-component.html +3 -0
  33. package/docs/components/show-more.html +3 -0
  34. package/docs/components/slider.html +3 -0
  35. package/docs/components/sortable.html +3 -0
  36. package/docs/components/speech-to-text.html +3 -0
  37. package/docs/components/spinner.html +3 -0
  38. package/docs/components/split.html +3 -0
  39. package/docs/components/table.html +3 -0
  40. package/docs/components/tableControls.html +3 -0
  41. package/docs/components/tableCustomFields.html +3 -0
  42. package/docs/components/tableFetchRecords.html +3 -0
  43. package/docs/components/tableFieldSortHide.html +3 -0
  44. package/docs/components/tablePagination.html +3 -0
  45. package/docs/components/tablePlaceholder.html +3 -0
  46. package/docs/components/tableRecordEditing.html +3 -0
  47. package/docs/components/tableRecordFiltering.html +3 -0
  48. package/docs/components/tableRecordHiding.html +3 -0
  49. package/docs/components/tableRecordSearching.html +3 -0
  50. package/docs/components/tableRecordSelection.html +3 -0
  51. package/docs/components/tableRowControls.html +3 -0
  52. package/docs/components/tableServerSync.html +3 -0
  53. package/docs/components/tableSorting.html +3 -0
  54. package/docs/components/tabs.html +3 -0
  55. package/docs/components/tags.html +3 -0
  56. package/docs/components/text-to-speech.html +3 -0
  57. package/docs/components/theme-select.html +3 -0
  58. package/docs/components/theme-switcher.html +3 -0
  59. package/docs/components/time.html +3 -0
  60. package/docs/components/timestamp.html +3 -0
  61. package/docs/components/toast.html +3 -0
  62. package/docs/components/toggle.html +3 -0
  63. package/docs/components/tree.html +3 -0
  64. package/docs/components/voice-selector.html +3 -0
  65. package/docs/index.html +3 -0
  66. package/docs/src/components/Context.js +1 -1
  67. package/docs/src/utils/closestAcrossShadow.js +1 -0
  68. package/docs/utils/closestAcrossShadow.html +549 -0
  69. package/docs/utils/context.html +3 -0
  70. package/docs/utils/cookie.html +3 -0
  71. package/docs/utils/debounce.html +3 -0
  72. package/docs/utils/drag.html +3 -0
  73. package/docs/utils/elevation.html +3 -0
  74. package/docs/utils/formatTimestamp.html +3 -0
  75. package/docs/utils/object.html +3 -0
  76. package/docs/utils/propConverters.html +3 -0
  77. package/docs/utils/sanitizeHtml.html +3 -0
  78. package/docs/utils/string.html +3 -0
  79. package/docs/utils/theme.html +3 -0
  80. package/docs/utils/toTitleCase.html +3 -0
  81. package/docs/utils/type.html +3 -0
  82. package/docs/utils/voice.html +3 -0
  83. package/docs/utils/wait.html +3 -0
  84. package/docs-src/components/context.page.html +17 -0
  85. package/docs-src/nav.fragment.html +3 -0
  86. package/docs-src/utils/closestAcrossShadow.page.html +49 -0
  87. package/package.json +1 -1
  88. package/src/components/Context.js +44 -1
  89. package/src/utils/closestAcrossShadow.js +23 -0
  90. package/tests/components/Context.browser-test.js +74 -0
  91. package/tests/utils/closestAcrossShadow.browser-test.js +90 -0
@@ -228,6 +228,8 @@
228
228
  <k-filter-item filter-keywords="button base component keyboard disabled accessibility"><a
229
229
  href="../components/button.html"
230
230
  >Button<br><small>Base Component</small></a></k-filter-item>
231
+ <k-filter-item filter-keywords="closestAcrossShadow closest shadow dom utils utility"><a
232
+ href="../utils/closestAcrossShadow.html">closestAcrossShadow<br><small>Utility</small></a></k-filter-item>
231
233
  <k-filter-item filter-keywords="cookie utils utility"><a
232
234
  href="../utils/cookie.html">cookie<br><small>Utility</small></a></k-filter-item>
233
235
  <k-filter-item filter-keywords="context utils utility"><a
@@ -360,6 +362,7 @@
360
362
 
361
363
  <h5 class="mb0">Utils</h5>
362
364
  <div class="pl mb">
365
+ <a href="../utils/closestAcrossShadow.html">closestAcrossShadow</a>
363
366
  <a href="../utils/cookie.html">cookie</a>
364
367
  <a href="../utils/context.html">context</a>
365
368
  <a href="../utils/debounce.html">debounce</a>
@@ -228,6 +228,8 @@
228
228
  <k-filter-item filter-keywords="button base component keyboard disabled accessibility"><a
229
229
  href="../components/button.html"
230
230
  >Button<br><small>Base Component</small></a></k-filter-item>
231
+ <k-filter-item filter-keywords="closestAcrossShadow closest shadow dom utils utility"><a
232
+ href="../utils/closestAcrossShadow.html">closestAcrossShadow<br><small>Utility</small></a></k-filter-item>
231
233
  <k-filter-item filter-keywords="cookie utils utility"><a
232
234
  href="../utils/cookie.html">cookie<br><small>Utility</small></a></k-filter-item>
233
235
  <k-filter-item filter-keywords="context utils utility"><a
@@ -360,6 +362,7 @@
360
362
 
361
363
  <h5 class="mb0">Utils</h5>
362
364
  <div class="pl mb">
365
+ <a href="../utils/closestAcrossShadow.html">closestAcrossShadow</a>
363
366
  <a href="../utils/cookie.html">cookie</a>
364
367
  <a href="../utils/context.html">context</a>
365
368
  <a href="../utils/debounce.html">debounce</a>
@@ -228,6 +228,8 @@
228
228
  <k-filter-item filter-keywords="button base component keyboard disabled accessibility"><a
229
229
  href="../components/button.html"
230
230
  >Button<br><small>Base Component</small></a></k-filter-item>
231
+ <k-filter-item filter-keywords="closestAcrossShadow closest shadow dom utils utility"><a
232
+ href="../utils/closestAcrossShadow.html">closestAcrossShadow<br><small>Utility</small></a></k-filter-item>
231
233
  <k-filter-item filter-keywords="cookie utils utility"><a
232
234
  href="../utils/cookie.html">cookie<br><small>Utility</small></a></k-filter-item>
233
235
  <k-filter-item filter-keywords="context utils utility"><a
@@ -360,6 +362,7 @@
360
362
 
361
363
  <h5 class="mb0">Utils</h5>
362
364
  <div class="pl mb">
365
+ <a href="../utils/closestAcrossShadow.html">closestAcrossShadow</a>
363
366
  <a href="../utils/cookie.html">cookie</a>
364
367
  <a href="../utils/context.html">context</a>
365
368
  <a href="../utils/debounce.html">debounce</a>
@@ -228,6 +228,8 @@
228
228
  <k-filter-item filter-keywords="button base component keyboard disabled accessibility"><a
229
229
  href="../components/button.html"
230
230
  >Button<br><small>Base Component</small></a></k-filter-item>
231
+ <k-filter-item filter-keywords="closestAcrossShadow closest shadow dom utils utility"><a
232
+ href="../utils/closestAcrossShadow.html">closestAcrossShadow<br><small>Utility</small></a></k-filter-item>
231
233
  <k-filter-item filter-keywords="cookie utils utility"><a
232
234
  href="../utils/cookie.html">cookie<br><small>Utility</small></a></k-filter-item>
233
235
  <k-filter-item filter-keywords="context utils utility"><a
@@ -360,6 +362,7 @@
360
362
 
361
363
  <h5 class="mb0">Utils</h5>
362
364
  <div class="pl mb">
365
+ <a href="../utils/closestAcrossShadow.html">closestAcrossShadow</a>
363
366
  <a href="../utils/cookie.html">cookie</a>
364
367
  <a href="../utils/context.html">context</a>
365
368
  <a href="../utils/debounce.html">debounce</a>
@@ -228,6 +228,8 @@
228
228
  <k-filter-item filter-keywords="button base component keyboard disabled accessibility"><a
229
229
  href="../components/button.html"
230
230
  >Button<br><small>Base Component</small></a></k-filter-item>
231
+ <k-filter-item filter-keywords="closestAcrossShadow closest shadow dom utils utility"><a
232
+ href="../utils/closestAcrossShadow.html">closestAcrossShadow<br><small>Utility</small></a></k-filter-item>
231
233
  <k-filter-item filter-keywords="cookie utils utility"><a
232
234
  href="../utils/cookie.html">cookie<br><small>Utility</small></a></k-filter-item>
233
235
  <k-filter-item filter-keywords="context utils utility"><a
@@ -360,6 +362,7 @@
360
362
 
361
363
  <h5 class="mb0">Utils</h5>
362
364
  <div class="pl mb">
365
+ <a href="../utils/closestAcrossShadow.html">closestAcrossShadow</a>
363
366
  <a href="../utils/cookie.html">cookie</a>
364
367
  <a href="../utils/context.html">context</a>
365
368
  <a href="../utils/debounce.html">debounce</a>
@@ -228,6 +228,8 @@
228
228
  <k-filter-item filter-keywords="button base component keyboard disabled accessibility"><a
229
229
  href="../components/button.html"
230
230
  >Button<br><small>Base Component</small></a></k-filter-item>
231
+ <k-filter-item filter-keywords="closestAcrossShadow closest shadow dom utils utility"><a
232
+ href="../utils/closestAcrossShadow.html">closestAcrossShadow<br><small>Utility</small></a></k-filter-item>
231
233
  <k-filter-item filter-keywords="cookie utils utility"><a
232
234
  href="../utils/cookie.html">cookie<br><small>Utility</small></a></k-filter-item>
233
235
  <k-filter-item filter-keywords="context utils utility"><a
@@ -360,6 +362,7 @@
360
362
 
361
363
  <h5 class="mb0">Utils</h5>
362
364
  <div class="pl mb">
365
+ <a href="../utils/closestAcrossShadow.html">closestAcrossShadow</a>
363
366
  <a href="../utils/cookie.html">cookie</a>
364
367
  <a href="../utils/context.html">context</a>
365
368
  <a href="../utils/debounce.html">debounce</a>
@@ -228,6 +228,8 @@
228
228
  <k-filter-item filter-keywords="button base component keyboard disabled accessibility"><a
229
229
  href="../components/button.html"
230
230
  >Button<br><small>Base Component</small></a></k-filter-item>
231
+ <k-filter-item filter-keywords="closestAcrossShadow closest shadow dom utils utility"><a
232
+ href="../utils/closestAcrossShadow.html">closestAcrossShadow<br><small>Utility</small></a></k-filter-item>
231
233
  <k-filter-item filter-keywords="cookie utils utility"><a
232
234
  href="../utils/cookie.html">cookie<br><small>Utility</small></a></k-filter-item>
233
235
  <k-filter-item filter-keywords="context utils utility"><a
@@ -360,6 +362,7 @@
360
362
 
361
363
  <h5 class="mb0">Utils</h5>
362
364
  <div class="pl mb">
365
+ <a href="../utils/closestAcrossShadow.html">closestAcrossShadow</a>
363
366
  <a href="../utils/cookie.html">cookie</a>
364
367
  <a href="../utils/context.html">context</a>
365
368
  <a href="../utils/debounce.html">debounce</a>
@@ -228,6 +228,8 @@
228
228
  <k-filter-item filter-keywords="button base component keyboard disabled accessibility"><a
229
229
  href="../components/button.html"
230
230
  >Button<br><small>Base Component</small></a></k-filter-item>
231
+ <k-filter-item filter-keywords="closestAcrossShadow closest shadow dom utils utility"><a
232
+ href="../utils/closestAcrossShadow.html">closestAcrossShadow<br><small>Utility</small></a></k-filter-item>
231
233
  <k-filter-item filter-keywords="cookie utils utility"><a
232
234
  href="../utils/cookie.html">cookie<br><small>Utility</small></a></k-filter-item>
233
235
  <k-filter-item filter-keywords="context utils utility"><a
@@ -360,6 +362,7 @@
360
362
 
361
363
  <h5 class="mb0">Utils</h5>
362
364
  <div class="pl mb">
365
+ <a href="../utils/closestAcrossShadow.html">closestAcrossShadow</a>
363
366
  <a href="../utils/cookie.html">cookie</a>
364
367
  <a href="../utils/context.html">context</a>
365
368
  <a href="../utils/debounce.html">debounce</a>
@@ -228,6 +228,8 @@
228
228
  <k-filter-item filter-keywords="button base component keyboard disabled accessibility"><a
229
229
  href="../components/button.html"
230
230
  >Button<br><small>Base Component</small></a></k-filter-item>
231
+ <k-filter-item filter-keywords="closestAcrossShadow closest shadow dom utils utility"><a
232
+ href="../utils/closestAcrossShadow.html">closestAcrossShadow<br><small>Utility</small></a></k-filter-item>
231
233
  <k-filter-item filter-keywords="cookie utils utility"><a
232
234
  href="../utils/cookie.html">cookie<br><small>Utility</small></a></k-filter-item>
233
235
  <k-filter-item filter-keywords="context utils utility"><a
@@ -360,6 +362,7 @@
360
362
 
361
363
  <h5 class="mb0">Utils</h5>
362
364
  <div class="pl mb">
365
+ <a href="../utils/closestAcrossShadow.html">closestAcrossShadow</a>
363
366
  <a href="../utils/cookie.html">cookie</a>
364
367
  <a href="../utils/context.html">context</a>
365
368
  <a href="../utils/debounce.html">debounce</a>
@@ -228,6 +228,8 @@
228
228
  <k-filter-item filter-keywords="button base component keyboard disabled accessibility"><a
229
229
  href="../components/button.html"
230
230
  >Button<br><small>Base Component</small></a></k-filter-item>
231
+ <k-filter-item filter-keywords="closestAcrossShadow closest shadow dom utils utility"><a
232
+ href="../utils/closestAcrossShadow.html">closestAcrossShadow<br><small>Utility</small></a></k-filter-item>
231
233
  <k-filter-item filter-keywords="cookie utils utility"><a
232
234
  href="../utils/cookie.html">cookie<br><small>Utility</small></a></k-filter-item>
233
235
  <k-filter-item filter-keywords="context utils utility"><a
@@ -360,6 +362,7 @@
360
362
 
361
363
  <h5 class="mb0">Utils</h5>
362
364
  <div class="pl mb">
365
+ <a href="../utils/closestAcrossShadow.html">closestAcrossShadow</a>
363
366
  <a href="../utils/cookie.html">cookie</a>
364
367
  <a href="../utils/context.html">context</a>
365
368
  <a href="../utils/debounce.html">debounce</a>
@@ -228,6 +228,8 @@
228
228
  <k-filter-item filter-keywords="button base component keyboard disabled accessibility"><a
229
229
  href="../components/button.html"
230
230
  >Button<br><small>Base Component</small></a></k-filter-item>
231
+ <k-filter-item filter-keywords="closestAcrossShadow closest shadow dom utils utility"><a
232
+ href="../utils/closestAcrossShadow.html">closestAcrossShadow<br><small>Utility</small></a></k-filter-item>
231
233
  <k-filter-item filter-keywords="cookie utils utility"><a
232
234
  href="../utils/cookie.html">cookie<br><small>Utility</small></a></k-filter-item>
233
235
  <k-filter-item filter-keywords="context utils utility"><a
@@ -360,6 +362,7 @@
360
362
 
361
363
  <h5 class="mb0">Utils</h5>
362
364
  <div class="pl mb">
365
+ <a href="../utils/closestAcrossShadow.html">closestAcrossShadow</a>
363
366
  <a href="../utils/cookie.html">cookie</a>
364
367
  <a href="../utils/context.html">context</a>
365
368
  <a href="../utils/debounce.html">debounce</a>
@@ -9,6 +9,7 @@
9
9
  <a href="#basicUsage">Basic Usage</a><br />
10
10
  <a href="#events">Listening to Events</a><br />
11
11
  <a href="#usingInComponents">Using in Components</a><br />
12
+ <a href="#persistence">Persistence</a><br />
12
13
  <a href="#interactiveDemo">Interactive Demo</a><br />
13
14
 
14
15
  <h6 class="mt"><a href="#jsRef" class="no-link">JavaScript Reference</a></h6>
@@ -69,6 +70,17 @@
69
70
  </div>
70
71
  <p>To react to context changes from inside a shadow-DOM component, listen on the context element directly — events bubble and are <code>composed: true</code> so they also cross shadow boundaries.</p>
71
72
 
73
+ <h3 id="persistence"><a href="#persistence" class="no-link">Persistence</a></h3>
74
+ <p>Set a <code>persistent-id</code> to automatically save the store to <code>localStorage</code> and restore it on the next page load — the same convention used by <code>Split</code>, <code>Accordion</code>, <code>Tabs</code> and <code>Aside</code>. The data is reloaded in <code>connectedCallback</code> (before descendants read it) and re-saved on every <code>set()</code>, <code>delete()</code> and <code>clear()</code>.</p>
75
+ <div class="row -mx">
76
+ <div class="col m-span-12 px">
77
+ <k-card label="HTML">
78
+ <pre><code class="hljs html"><span class="hljs-tag">&lt;<span class="hljs-name">k-context</span> <span class="hljs-attr">persistent-id</span>=<span class="hljs-string">&quot;app-settings&quot;</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">k-context</span>&gt;</span></code></pre>
79
+ </k-card>
80
+ </div>
81
+ </div>
82
+ <p>Because the entire store is JSON-serialized into a single value, keep persisted contexts to small, serializable state (settings, selections, ids) — not large or non-JSON data. Scope what you persist by giving config its own <code>persistent-id</code> context and leaving transient state in a separate, non-persistent <code>k-context</code>.</p>
83
+
72
84
  <h3 id="interactiveDemo"><a href="#interactiveDemo" class="no-link">Interactive Demo</a></h3>
73
85
  <div class="row -mx">
74
86
  <div class="col m-span-12 t-span-6 px">
@@ -134,6 +146,11 @@
134
146
  <td><code>String</code> (JSON)</td>
135
147
  <td>A JSON string representing the initial store state. Reflects back to the attribute as state changes via the component's methods. Defaults to <code>'{}'</code>.</td>
136
148
  </tr>
149
+ <tr>
150
+ <td><code>persistent-id</code></td>
151
+ <td><code>String</code></td>
152
+ <td>When set, the store is auto-saved to <code>localStorage</code> (key <code>context-persistent-id-&lt;id&gt;</code>) on every change and restored on connect. Defaults to <code>null</code> (no persistence).</td>
153
+ </tr>
137
154
  </tbody>
138
155
  </table>
139
156
  </div>
@@ -212,6 +212,8 @@
212
212
  <k-filter-item filter-keywords="button base component keyboard disabled accessibility"><a
213
213
  href="{{pathToRoot}}components/button.html"
214
214
  >Button<br><small>Base Component</small></a></k-filter-item>
215
+ <k-filter-item filter-keywords="closestAcrossShadow closest shadow dom utils utility"><a
216
+ href="{{pathToRoot}}utils/closestAcrossShadow.html">closestAcrossShadow<br><small>Utility</small></a></k-filter-item>
215
217
  <k-filter-item filter-keywords="cookie utils utility"><a
216
218
  href="{{pathToRoot}}utils/cookie.html">cookie<br><small>Utility</small></a></k-filter-item>
217
219
  <k-filter-item filter-keywords="context utils utility"><a
@@ -344,6 +346,7 @@
344
346
 
345
347
  <h5 class="mb0">Utils</h5>
346
348
  <div class="pl mb">
349
+ <a href="{{pathToRoot}}utils/closestAcrossShadow.html">closestAcrossShadow</a>
347
350
  <a href="{{pathToRoot}}utils/cookie.html">cookie</a>
348
351
  <a href="{{pathToRoot}}utils/context.html">context</a>
349
352
  <a href="{{pathToRoot}}utils/debounce.html">debounce</a>
@@ -0,0 +1,49 @@
1
+ <page pageName="closestAcrossShadow" title="closestAcrossShadow - Kempo UI">
2
+ <content location="header"></content>
3
+ <content>
4
+ <h1>closestAcrossShadow</h1>
5
+ <k-accordion persistent-id="toc" class="b r mb">
6
+ <k-accordion-header for-panel="toc-panel">Table of Contents</k-accordion-header>
7
+ <k-accordion-panel name="toc-panel">
8
+ <div class="m ">
9
+ <h6>Usage</h6>
10
+ <a href="#basicUsage">Basic Usage</a><br />
11
+ <a href="#parameters">Parameters</a><br />
12
+ <a href="#examples">Examples</a><br />
13
+ </div>
14
+ </k-accordion-panel>
15
+ </k-accordion>
16
+
17
+ <h3>Description</h3>
18
+ <p>The <code>closestAcrossShadow</code> utility works like the native
19
+ <code>Element.closest()</code>, but it does not stop at shadow-DOM boundaries.
20
+ When the current tree yields no match it continues from the host of the enclosing
21
+ shadow root and keeps walking up toward the document, returning the nearest matching
22
+ ancestor (or the element itself), or <code>null</code> if there is none.</p>
23
+ <p>This is useful when a component nested inside another component's shadow root needs
24
+ to locate a shared ancestor — for example a <a href="{{pathToRoot}}components/context.html"><code>k-context</code></a> —
25
+ which native <code>closest()</code> cannot reach because it never crosses shadow boundaries.</p>
26
+
27
+ <h3 id="basicUsage"><a href="#basicUsage" class="no-link">Basic Usage</a></h3>
28
+ <pre><code class="hljs javascript"><span class="hljs-keyword">import</span> closestAcrossShadow <span class="hljs-keyword">from</span> <span class="hljs-string">'./utils/closestAcrossShadow.js'</span>;<br /><br /><span class="hljs-comment">// From inside a deeply nested component, reach a shared k-context:</span><br /><span class="hljs-keyword">const</span> ctx = closestAcrossShadow(<span class="hljs-keyword">this</span>, <span class="hljs-string">'k-context'</span>);<br /><span class="hljs-keyword">const</span> settings = ctx?.get(<span class="hljs-string">'settings'</span>);</code></pre>
29
+
30
+ <h3 id="parameters"><a href="#parameters" class="no-link">Parameters</a></h3>
31
+ <h5><code>element<i>: Element</i></code></h5>
32
+ <p>The element to start searching from. The element itself is included in the match, just like <code>closest()</code>.</p>
33
+ <h5><code>selector<i>: string</i></code></h5>
34
+ <p>A CSS selector to match ancestors against.</p>
35
+ <h5><code><i>returns</i>: Element | null</code></h5>
36
+ <p>The nearest matching ancestor across shadow boundaries, or <code>null</code> if none match.</p>
37
+
38
+ <h3 id="examples"><a href="#examples" class="no-link">Examples</a></h3>
39
+
40
+ <h4>Reading a context from a nested shadow component</h4>
41
+ <pre><code class="hljs javascript"><span class="hljs-keyword">import</span> closestAcrossShadow <span class="hljs-keyword">from</span> <span class="hljs-string">'./utils/closestAcrossShadow.js'</span>;<br /><br />connectedCallback() {<br /> <span class="hljs-keyword">super</span>.connectedCallback();<br /> <span class="hljs-keyword">const</span> ctx = closestAcrossShadow(<span class="hljs-keyword">this</span>, <span class="hljs-string">'k-context'</span>);<br /> ctx?.addEventListener(<span class="hljs-string">'context:set'</span>, () =&gt; <span class="hljs-keyword">this</span>.requestUpdate());<br />}</code></pre>
42
+
43
+ <h4>Falls back to null</h4>
44
+ <pre><code class="hljs javascript"><span class="hljs-comment">// No matching ancestor in any tree:</span><br>closestAcrossShadow(someDetachedNode, <span class="hljs-string">'.missing'</span>); <span class="hljs-comment">// =&gt; null</span></code></pre>
45
+ </content>
46
+ <content location="scripts">
47
+ <script type="module" src="{{pathToRoot}}src/components/Accordion.js"></script>
48
+ </content>
49
+ </page>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kempo-ui",
3
- "version": "0.4.25",
3
+ "version": "0.4.27",
4
4
  "type": "module",
5
5
  "description": "A Lit based web-component library",
6
6
  "main": "index.js",
@@ -4,18 +4,51 @@
4
4
  A non-rendering state container for sharing data between sibling and descendant
5
5
  components, similar in purpose to React's useContext. Place it as an ancestor
6
6
  of components that need shared state; children locate it via closest('k-context').
7
+
8
+ Set a `persistent-id` to auto-save/load the data to localStorage (the same
9
+ convention used by Split, Accordion, Tabs and Aside).
7
10
  */
8
11
  import { html } from '../lit-all.min.js';
9
12
  import LightComponent from './LightComponent.js';
10
13
 
14
+ /*
15
+ Symbols
16
+ */
17
+ const persist = Symbol('persist');
18
+
11
19
  export default class Context extends LightComponent {
20
+ /*
21
+ Reactive Properties / Attributes
22
+ */
12
23
  static properties = {
13
- data: { type: String, reflect: true }
24
+ data: { type: String, reflect: true },
25
+ persistentId: { type: String, reflect: true, attribute: 'persistent-id' }
14
26
  };
15
27
 
28
+ /*
29
+ Constructor
30
+ */
16
31
  constructor() {
17
32
  super();
33
+
34
+ /*
35
+ Private Methods
36
+ */
37
+ // Mirror the data blob into localStorage whenever a persistent-id is set, so the
38
+ // next page load can restore it. Guarded so a full/blocked store (private mode,
39
+ // quota) never throws out of a set/delete.
40
+ this[persist] = () => {
41
+ if(!this.persistentId || !window?.localStorage) return;
42
+ try {
43
+ window.localStorage.setItem(`context-persistent-id-${this.persistentId}`, this.data);
44
+ } catch { /* storage unavailable — keep working in-memory */ }
45
+ };
46
+
47
+ /*
48
+ Init Props
49
+ */
18
50
  this.data = '{}';
51
+ this.persistentId = null;
19
52
  }
20
53
 
21
54
  /*
@@ -24,6 +57,14 @@ export default class Context extends LightComponent {
24
57
  connectedCallback() {
25
58
  super.connectedCallback();
26
59
  this.style.display = 'contents';
60
+ // Restore persisted state up front, before descendants read it on their own
61
+ // connect. Setting data directly (rather than per-key set) intentionally skips
62
+ // events: the context is an ancestor, so children read the loaded values via
63
+ // closest('k-context').get() when they first render.
64
+ if(this.persistentId && window?.localStorage){
65
+ const stored = window.localStorage.getItem(`context-persistent-id-${this.persistentId}`);
66
+ if(stored) this.data = stored;
67
+ }
27
68
  }
28
69
 
29
70
  /*
@@ -35,6 +76,7 @@ export default class Context extends LightComponent {
35
76
  const oldValue = obj[key];
36
77
  obj[key] = value;
37
78
  this.data = JSON.stringify(obj);
79
+ this[persist]();
38
80
  this.dispatchEvent(new CustomEvent(isNew ? 'context:create' : 'context:set', {
39
81
  detail: isNew ? { key, value } : { key, value, oldValue },
40
82
  bubbles: true,
@@ -56,6 +98,7 @@ export default class Context extends LightComponent {
56
98
  const value = obj[key];
57
99
  delete obj[key];
58
100
  this.data = JSON.stringify(obj);
101
+ this[persist]();
59
102
  this.dispatchEvent(new CustomEvent('context:delete', {
60
103
  detail: { key, value },
61
104
  bubbles: true,
@@ -0,0 +1,23 @@
1
+ /*
2
+ closestAcrossShadow
3
+
4
+ Like Element.closest(), but it does not stop at shadow-DOM boundaries. When the
5
+ current tree yields no match, it continues from the host of the enclosing shadow
6
+ root and keeps walking up toward the document. Returns the nearest matching
7
+ ancestor (or the element itself), or null if there is none.
8
+
9
+ Useful for components nested inside another component's shadow root that need to
10
+ locate a shared ancestor — e.g. a k-context — which native closest() can't reach.
11
+ */
12
+ export const closestAcrossShadow = (element, selector) => {
13
+ let node = element;
14
+ while(node){
15
+ const match = node.closest?.(selector);
16
+ if(match) return match;
17
+ const root = node.getRootNode();
18
+ node = root instanceof ShadowRoot ? root.host : null;
19
+ }
20
+ return null;
21
+ };
22
+
23
+ export default closestAcrossShadow;
@@ -10,6 +10,17 @@ const createContext = async () => {
10
10
  return { container, el };
11
11
  };
12
12
 
13
+ const createPersistentContext = async (persistentId) => {
14
+ const container = document.createElement('div');
15
+ container.innerHTML = `<k-context persistent-id="${persistentId}"></k-context>`;
16
+ document.body.appendChild(container);
17
+ const el = container.querySelector('k-context');
18
+ await el.updateComplete;
19
+ return { container, el };
20
+ };
21
+
22
+ const storageKey = (id) => `context-persistent-id-${id}`;
23
+
13
24
  const cleanup = (container) => {
14
25
  if(container && container.parentNode){
15
26
  container.parentNode.removeChild(container);
@@ -234,5 +245,68 @@ export default {
234
245
  }
235
246
  cleanup(container);
236
247
  pass('getData() returns a shallow copy of all data');
248
+ },
249
+
250
+ /*
251
+ persistent-id (localStorage auto save/load)
252
+ */
253
+ 'set() writes data to localStorage when persistent-id is set': async ({pass, fail}) => {
254
+ const id = 'persist-set-' + Date.now();
255
+ window.localStorage.removeItem(storageKey(id));
256
+ const { container, el } = await createPersistentContext(id);
257
+ el.set('theme', 'dark');
258
+ let stored;
259
+ try { stored = JSON.parse(window.localStorage.getItem(storageKey(id))); } catch(e) {
260
+ cleanup(container); window.localStorage.removeItem(storageKey(id));
261
+ return fail('localStorage value is not valid JSON');
262
+ }
263
+ cleanup(container); window.localStorage.removeItem(storageKey(id));
264
+ if(!stored || stored.theme !== 'dark'){
265
+ return fail(`Expected stored {theme:'dark'}, got ${JSON.stringify(stored)}`);
266
+ }
267
+ pass('set() persisted to localStorage');
268
+ },
269
+
270
+ 'restores data from localStorage on connect': async ({pass, fail}) => {
271
+ const id = 'persist-load-' + Date.now();
272
+ window.localStorage.setItem(storageKey(id), JSON.stringify({ count: 7 }));
273
+ const { container, el } = await createPersistentContext(id);
274
+ const value = el.get('count');
275
+ cleanup(container); window.localStorage.removeItem(storageKey(id));
276
+ if(value !== 7){
277
+ return fail(`Expected restored count to be 7, got ${value}`);
278
+ }
279
+ pass('data restored from localStorage on connect');
280
+ },
281
+
282
+ 'delete() updates localStorage': async ({pass, fail}) => {
283
+ const id = 'persist-delete-' + Date.now();
284
+ window.localStorage.removeItem(storageKey(id));
285
+ const { container, el } = await createPersistentContext(id);
286
+ el.set('a', 1);
287
+ el.set('b', 2);
288
+ el.delete('a');
289
+ let stored;
290
+ try { stored = JSON.parse(window.localStorage.getItem(storageKey(id))); } catch(e) {
291
+ cleanup(container); window.localStorage.removeItem(storageKey(id));
292
+ return fail('localStorage value is not valid JSON');
293
+ }
294
+ cleanup(container); window.localStorage.removeItem(storageKey(id));
295
+ if(!stored || 'a' in stored || stored.b !== 2){
296
+ return fail(`Expected stored to drop 'a' and keep b:2, got ${JSON.stringify(stored)}`);
297
+ }
298
+ pass('delete() updated localStorage');
299
+ },
300
+
301
+ 'does not write to localStorage without a persistent-id': async ({pass, fail}) => {
302
+ const before = window.localStorage.length;
303
+ const { container, el } = await createContext();
304
+ el.set('x', 1);
305
+ const after = window.localStorage.length;
306
+ cleanup(container);
307
+ if(after !== before){
308
+ return fail('A context without persistent-id should not touch localStorage');
309
+ }
310
+ pass('no persistent-id means no localStorage writes');
237
311
  }
238
312
  };
@@ -0,0 +1,90 @@
1
+ import closestAcrossShadow, { closestAcrossShadow as named } from '../../src/utils/closestAcrossShadow.js';
2
+
3
+ const cleanup = (el) => {
4
+ if(el && el.parentNode){
5
+ el.parentNode.removeChild(el);
6
+ }
7
+ };
8
+
9
+ export default {
10
+ 'exports the function as default and named export': ({pass, fail}) => {
11
+ if(typeof closestAcrossShadow === 'function' && typeof named === 'function'){
12
+ pass('exported correctly as both default and named export');
13
+ } else {
14
+ fail('not exported correctly');
15
+ }
16
+ },
17
+
18
+ 'finds an ancestor within the same tree (like closest)': ({pass, fail}) => {
19
+ const root = document.createElement('div');
20
+ root.className = 'target';
21
+ root.innerHTML = '<section><span id="leaf"></span></section>';
22
+ document.body.appendChild(root);
23
+ const found = closestAcrossShadow(root.querySelector('#leaf'), '.target');
24
+ cleanup(root);
25
+ if(found === root){
26
+ pass('found ancestor in the same tree');
27
+ } else {
28
+ fail('did not find ancestor in the same tree');
29
+ }
30
+ },
31
+
32
+ 'returns the element itself when it matches': ({pass, fail}) => {
33
+ const el = document.createElement('div');
34
+ el.className = 'me';
35
+ document.body.appendChild(el);
36
+ const found = closestAcrossShadow(el, '.me');
37
+ cleanup(el);
38
+ if(found === el){
39
+ pass('returned the element itself');
40
+ } else {
41
+ fail('did not return the element itself');
42
+ }
43
+ },
44
+
45
+ 'crosses a shadow boundary to find an ancestor': ({pass, fail}) => {
46
+ const host = document.createElement('div');
47
+ host.className = 'target';
48
+ document.body.appendChild(host);
49
+ const inner = document.createElement('span');
50
+ host.attachShadow({ mode: 'open' }).appendChild(inner);
51
+ const found = closestAcrossShadow(inner, '.target');
52
+ cleanup(host);
53
+ if(found === host){
54
+ pass('crossed a shadow boundary');
55
+ } else {
56
+ fail('did not cross the shadow boundary');
57
+ }
58
+ },
59
+
60
+ 'crosses multiple nested shadow boundaries': ({pass, fail}) => {
61
+ const host = document.createElement('div');
62
+ host.className = 'target';
63
+ document.body.appendChild(host);
64
+ const mid = document.createElement('div');
65
+ host.attachShadow({ mode: 'open' }).appendChild(mid);
66
+ const leaf = document.createElement('span');
67
+ mid.attachShadow({ mode: 'open' }).appendChild(leaf);
68
+ const found = closestAcrossShadow(leaf, '.target');
69
+ cleanup(host);
70
+ if(found === host){
71
+ pass('crossed multiple nested shadow boundaries');
72
+ } else {
73
+ fail('did not cross multiple nested shadow boundaries');
74
+ }
75
+ },
76
+
77
+ 'returns null when there is no match anywhere': ({pass, fail}) => {
78
+ const host = document.createElement('div');
79
+ document.body.appendChild(host);
80
+ const inner = document.createElement('span');
81
+ host.attachShadow({ mode: 'open' }).appendChild(inner);
82
+ const found = closestAcrossShadow(inner, '.nope');
83
+ cleanup(host);
84
+ if(found === null){
85
+ pass('returned null when no match');
86
+ } else {
87
+ fail(`expected null, got ${found}`);
88
+ }
89
+ }
90
+ };