@waggylabs/yumekit 0.4.3-beta.45 → 0.4.3-beta.46

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
@@ -46,6 +46,10 @@ Delete any empty sections before publishing.
46
46
  - `y-tabs`: `leftIcon` and `rightIcon` properties on option objects — set a `<y-icon>` name directly in the options JSON to render icons without requiring extra child elements or named slots.
47
47
  - `y-tabs`: `tab-content-{id}` slot — place any content (icons, badges, custom markup) inside the tab button itself by targeting this slot. Takes full precedence over `leftIcon`/`rightIcon` and the default label rendering.
48
48
 
49
+ ### Fixed
50
+
51
+ - `y-dialog`, `y-drawer`, `y-menu`: anchor lookup now tolerates DOM insertion races. Previously, setting the `anchor` attribute before the anchor element was in the DOM (common with React portals and async / lazy mounts) left the component without a click listener. Resolution now tries `getElementById` synchronously, retries once on the next animation frame, and falls back to a `MutationObserver` that fires when the anchor appears. `y-dialog` also now cleans up its anchor listener on disconnect.
52
+
49
53
  ### Deprecated
50
54
 
51
55
  - `y-tabs`: `left-icon-{id}` and `right-icon-{id}` slots — these slots still function but emit a `console.warn` directing users to the `leftIcon`/`rightIcon` option properties or the `tab-content-{id}` slot. They will be removed before the release of version 1.0.
@@ -1,4 +1,4 @@
1
- import { contrastTextColor } from '../../modules/helpers.js';
1
+ import { contrastTextColor, resolveAnchor } from '../../modules/helpers.js';
2
2
  import { getIcon } from '../../icons/registry.js';
3
3
 
4
4
  class YumeButton extends HTMLElement {
@@ -1100,19 +1100,24 @@ class YumeMenu extends HTMLElement {
1100
1100
 
1101
1101
  _setupAnchor() {
1102
1102
  const id = this.anchor;
1103
- if (id) {
1104
- const root = this.getRootNode();
1105
- const el = root?.getElementById
1106
- ? root.getElementById(id)
1107
- : document.getElementById(id);
1108
- if (el) {
1103
+ if (!id) return;
1104
+ const root = this.getRootNode();
1105
+ this._anchorResolveDispose = resolveAnchor(
1106
+ this,
1107
+ id,
1108
+ (el) => {
1109
1109
  this._anchorEl = el;
1110
- this._anchorEl.addEventListener("click", this._onAnchorClick);
1111
- }
1112
- }
1110
+ el.addEventListener("click", this._onAnchorClick);
1111
+ },
1112
+ root && root.getElementById ? root : document,
1113
+ );
1113
1114
  }
1114
1115
 
1115
1116
  _teardownAnchor() {
1117
+ if (this._anchorResolveDispose) {
1118
+ this._anchorResolveDispose();
1119
+ this._anchorResolveDispose = null;
1120
+ }
1116
1121
  if (this._anchorEl) {
1117
1122
  this._anchorEl.removeEventListener("click", this._onAnchorClick);
1118
1123
  this._anchorEl = null;
@@ -1,40 +1 @@
1
- declare class YumeDialog extends HTMLElement {
2
- static get observedAttributes(): string[];
3
- _onKeyDown(e: any): void;
4
- _onAnchorClick(): void;
5
- connectedCallback(): void;
6
- attributeChangedCallback(name: any, oldValue: any, newValue: any): void;
7
- set animate(val: boolean);
8
- /** Whether the dialog uses an entrance animation. */
9
- get animate(): boolean;
10
- set anchor(id: string);
11
- /** The id of the element that toggles this dialog on click. */
12
- get anchor(): string;
13
- set closable(val: boolean);
14
- /** Whether the dialog renders a close button in the header. */
15
- get closable(): boolean;
16
- set position(val: string);
17
- /** Screen position of the dialog. One of: center (default), top-left, top-center, top-right, left, right, bottom-left, bottom-center, bottom-right. */
18
- get position(): string;
19
- set showBackdrop(val: boolean);
20
- /** Whether to apply a blurred backdrop behind the dialog. */
21
- get showBackdrop(): boolean;
22
- set visible(val: boolean);
23
- /** Whether the dialog is currently displayed. */
24
- get visible(): boolean;
25
- /** Closes the dialog. */
26
- hide(): void;
27
- /** Rebuilds the shadow DOM. */
28
- render(): void;
29
- /** Opens the dialog and focuses it. */
30
- show(): void;
31
- _buildCloseButton(): HTMLElement;
32
- _buildDialog(): HTMLDivElement;
33
- _buildHeader(): HTMLDivElement;
34
- _buildSection(slotName: any, partName: any): HTMLDivElement;
35
- _buildStyles(): HTMLStyleElement;
36
- _focusDialog(): void;
37
- _hideIfEmpty(wrapper: any): void;
38
- _setupAnchor(): void;
39
- _anchorEl: HTMLElement;
40
- }
1
+ export {};
@@ -1,40 +1 @@
1
- declare class YumeDialog extends HTMLElement {
2
- static get observedAttributes(): string[];
3
- _onKeyDown(e: any): void;
4
- _onAnchorClick(): void;
5
- connectedCallback(): void;
6
- attributeChangedCallback(name: any, oldValue: any, newValue: any): void;
7
- set animate(val: boolean);
8
- /** Whether the dialog uses an entrance animation. */
9
- get animate(): boolean;
10
- set anchor(id: string);
11
- /** The id of the element that toggles this dialog on click. */
12
- get anchor(): string;
13
- set closable(val: boolean);
14
- /** Whether the dialog renders a close button in the header. */
15
- get closable(): boolean;
16
- set position(val: string);
17
- /** Screen position of the dialog. One of: center (default), top-left, top-center, top-right, left, right, bottom-left, bottom-center, bottom-right. */
18
- get position(): string;
19
- set showBackdrop(val: boolean);
20
- /** Whether to apply a blurred backdrop behind the dialog. */
21
- get showBackdrop(): boolean;
22
- set visible(val: boolean);
23
- /** Whether the dialog is currently displayed. */
24
- get visible(): boolean;
25
- /** Closes the dialog. */
26
- hide(): void;
27
- /** Rebuilds the shadow DOM. */
28
- render(): void;
29
- /** Opens the dialog and focuses it. */
30
- show(): void;
31
- _buildCloseButton(): HTMLElement;
32
- _buildDialog(): HTMLDivElement;
33
- _buildHeader(): HTMLDivElement;
34
- _buildSection(slotName: any, partName: any): HTMLDivElement;
35
- _buildStyles(): HTMLStyleElement;
36
- _focusDialog(): void;
37
- _hideIfEmpty(wrapper: any): void;
38
- _setupAnchor(): void;
39
- _anchorEl: HTMLElement;
40
- }
1
+ export {};
@@ -1,3 +1,5 @@
1
+ import { resolveAnchor } from '../../modules/helpers.js';
2
+
1
3
  class YumeDialog extends HTMLElement {
2
4
  static get observedAttributes() {
3
5
  return ["visible", "anchor", "closable", "show-backdrop", "animate", "position"];
@@ -20,10 +22,17 @@ class YumeDialog extends HTMLElement {
20
22
  if (this.visible) this.show();
21
23
  }
22
24
 
25
+ disconnectedCallback() {
26
+ this._teardownAnchor();
27
+ }
28
+
23
29
  attributeChangedCallback(name, oldValue, newValue) {
24
30
  if (oldValue === newValue) return;
25
31
  if (name === "visible") this.visible ? this.show() : this.hide();
26
- if (name === "anchor") this._setupAnchor();
32
+ if (name === "anchor") {
33
+ this._teardownAnchor();
34
+ this._setupAnchor();
35
+ }
27
36
  if (name === "closable") this.render();
28
37
  }
29
38
 
@@ -259,15 +268,22 @@ class YumeDialog extends HTMLElement {
259
268
  }
260
269
 
261
270
  _setupAnchor() {
271
+ const id = this.anchor;
272
+ if (!id) return;
273
+ this._anchorResolveDispose = resolveAnchor(this, id, (el) => {
274
+ this._anchorEl = el;
275
+ el.addEventListener("click", this._onAnchorClick);
276
+ });
277
+ }
278
+
279
+ _teardownAnchor() {
280
+ if (this._anchorResolveDispose) {
281
+ this._anchorResolveDispose();
282
+ this._anchorResolveDispose = null;
283
+ }
262
284
  if (this._anchorEl) {
263
285
  this._anchorEl.removeEventListener("click", this._onAnchorClick);
264
- }
265
- if (this.anchor) {
266
- const el = document.getElementById(this.anchor);
267
- if (el) {
268
- this._anchorEl = el;
269
- el.addEventListener("click", this._onAnchorClick);
270
- }
286
+ this._anchorEl = null;
271
287
  }
272
288
  }
273
289
  }
@@ -1,3 +1,5 @@
1
+ import { resolveAnchor } from '../../modules/helpers.js';
2
+
1
3
  var ellipsisV = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <circle cx=\"12\" cy=\"5\" r=\"1.5\"/>\n <circle cx=\"12\" cy=\"12\" r=\"1.5\"/>\n <circle cx=\"12\" cy=\"19\" r=\"1.5\"/>\n</svg>\n";
2
4
 
3
5
  var ellipsisH = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n <circle cx=\"5\" cy=\"12\" r=\"1.5\"/>\n <circle cx=\"12\" cy=\"12\" r=\"1.5\"/>\n <circle cx=\"19\" cy=\"12\" r=\"1.5\"/>\n</svg>\n";
@@ -427,13 +429,11 @@ class YumeDrawer extends HTMLElement {
427
429
 
428
430
  _setupAnchor() {
429
431
  const id = this.anchor;
430
- if (id) {
431
- const el = document.getElementById(id);
432
- if (el) {
433
- this._anchorEl = el;
434
- this._anchorEl.addEventListener("click", this._onAnchorClick);
435
- }
436
- }
432
+ if (!id) return;
433
+ this._anchorResolveDispose = resolveAnchor(this, id, (el) => {
434
+ this._anchorEl = el;
435
+ el.addEventListener("click", this._onAnchorClick);
436
+ });
437
437
  }
438
438
 
439
439
  _show() {
@@ -453,6 +453,10 @@ class YumeDrawer extends HTMLElement {
453
453
  }
454
454
 
455
455
  _teardownAnchor() {
456
+ if (this._anchorResolveDispose) {
457
+ this._anchorResolveDispose();
458
+ this._anchorResolveDispose = null;
459
+ }
456
460
  if (this._anchorEl) {
457
461
  this._anchorEl.removeEventListener("click", this._onAnchorClick);
458
462
  this._anchorEl = null;
@@ -1,3 +1,5 @@
1
+ import { resolveAnchor } from '../../modules/helpers.js';
2
+
1
3
  var chevronRight = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <polyline points=\"9 18 15 12 9 6\"/>\n</svg>\n";
2
4
 
3
5
  class YumeMenu extends HTMLElement {
@@ -308,19 +310,24 @@ class YumeMenu extends HTMLElement {
308
310
 
309
311
  _setupAnchor() {
310
312
  const id = this.anchor;
311
- if (id) {
312
- const root = this.getRootNode();
313
- const el = root?.getElementById
314
- ? root.getElementById(id)
315
- : document.getElementById(id);
316
- if (el) {
313
+ if (!id) return;
314
+ const root = this.getRootNode();
315
+ this._anchorResolveDispose = resolveAnchor(
316
+ this,
317
+ id,
318
+ (el) => {
317
319
  this._anchorEl = el;
318
- this._anchorEl.addEventListener("click", this._onAnchorClick);
319
- }
320
- }
320
+ el.addEventListener("click", this._onAnchorClick);
321
+ },
322
+ root && root.getElementById ? root : document,
323
+ );
321
324
  }
322
325
 
323
326
  _teardownAnchor() {
327
+ if (this._anchorResolveDispose) {
328
+ this._anchorResolveDispose();
329
+ this._anchorResolveDispose = null;
330
+ }
324
331
  if (this._anchorEl) {
325
332
  this._anchorEl.removeEventListener("click", this._onAnchorClick);
326
333
  this._anchorEl = null;
package/dist/index.d.ts CHANGED
@@ -11,6 +11,7 @@ export * from "./components/y-color/y-color.js";
11
11
  export * from "./components/y-colorpicker/y-colorpicker.js";
12
12
  export * from "./components/y-date/y-date.js";
13
13
  export * from "./components/y-datepicker/y-datepicker.js";
14
+ export * from "./components/y-dialog/y-dialog.js";
14
15
  export * from "./components/y-drawer/y-drawer.js";
15
16
  export * from "./components/y-dock/y-dock.js";
16
17
  export * from "./components/y-gallery/y-gallery.js";
package/dist/index.js CHANGED
@@ -810,6 +810,64 @@ function manageLabelVisibility(labelWrapper) {
810
810
  });
811
811
  }
812
812
 
813
+ /**
814
+ * Resolve an anchor element by id for a web component, tolerating DOM insertion
815
+ * races. A synchronous lookup runs first; if it misses, one
816
+ * `requestAnimationFrame` retry covers the common React portal commit-ordering
817
+ * case, and a final `MutationObserver` fallback covers async / lazy mounts.
818
+ *
819
+ * The callback fires at most once. If the host disconnects before the anchor
820
+ * appears, resolution is aborted. Callers must invoke the returned dispose
821
+ * function on teardown (disconnectedCallback, attribute change, etc.) to
822
+ * cancel any pending rAF or observer.
823
+ *
824
+ * @param {HTMLElement} host — component instance; resolution aborts if it disconnects.
825
+ * @param {string} id — the anchor element's id.
826
+ * @param {(el: HTMLElement) => void} onFound — invoked once when the anchor resolves.
827
+ * @param {Document|ShadowRoot} [root=document] — root to search within.
828
+ * @returns {() => void} — dispose function.
829
+ */
830
+ function resolveAnchor(host, id, onFound, root = document) {
831
+ let disposed = false;
832
+ let rafId = null;
833
+ let observer = null;
834
+
835
+ const dispose = () => {
836
+ disposed = true;
837
+ if (rafId != null) cancelAnimationFrame(rafId);
838
+ if (observer) observer.disconnect();
839
+ rafId = null;
840
+ observer = null;
841
+ };
842
+
843
+ const find = () => {
844
+ if (disposed) return true;
845
+ const el = root.getElementById
846
+ ? root.getElementById(id)
847
+ : document.getElementById(id);
848
+ if (el) {
849
+ dispose();
850
+ onFound(el);
851
+ return true;
852
+ }
853
+ return false;
854
+ };
855
+
856
+ if (find()) return dispose;
857
+ if (!host.isConnected) return dispose;
858
+
859
+ rafId = requestAnimationFrame(() => {
860
+ rafId = null;
861
+ if (find()) return;
862
+ const target = root === document ? document.body : root;
863
+ if (!target) return;
864
+ observer = new MutationObserver(find);
865
+ observer.observe(target, { childList: true, subtree: true });
866
+ });
867
+
868
+ return dispose;
869
+ }
870
+
813
871
  // =============================================================================
814
872
  // Slot utilities
815
873
  // =============================================================================
@@ -1931,19 +1989,24 @@ class YumeMenu extends HTMLElement {
1931
1989
 
1932
1990
  _setupAnchor() {
1933
1991
  const id = this.anchor;
1934
- if (id) {
1935
- const root = this.getRootNode();
1936
- const el = root?.getElementById
1937
- ? root.getElementById(id)
1938
- : document.getElementById(id);
1939
- if (el) {
1992
+ if (!id) return;
1993
+ const root = this.getRootNode();
1994
+ this._anchorResolveDispose = resolveAnchor(
1995
+ this,
1996
+ id,
1997
+ (el) => {
1940
1998
  this._anchorEl = el;
1941
- this._anchorEl.addEventListener("click", this._onAnchorClick);
1942
- }
1943
- }
1999
+ el.addEventListener("click", this._onAnchorClick);
2000
+ },
2001
+ root && root.getElementById ? root : document,
2002
+ );
1944
2003
  }
1945
2004
 
1946
2005
  _teardownAnchor() {
2006
+ if (this._anchorResolveDispose) {
2007
+ this._anchorResolveDispose();
2008
+ this._anchorResolveDispose = null;
2009
+ }
1947
2010
  if (this._anchorEl) {
1948
2011
  this._anchorEl.removeEventListener("click", this._onAnchorClick);
1949
2012
  this._anchorEl = null;
@@ -9725,10 +9788,17 @@ class YumeDialog extends HTMLElement {
9725
9788
  if (this.visible) this.show();
9726
9789
  }
9727
9790
 
9791
+ disconnectedCallback() {
9792
+ this._teardownAnchor();
9793
+ }
9794
+
9728
9795
  attributeChangedCallback(name, oldValue, newValue) {
9729
9796
  if (oldValue === newValue) return;
9730
9797
  if (name === "visible") this.visible ? this.show() : this.hide();
9731
- if (name === "anchor") this._setupAnchor();
9798
+ if (name === "anchor") {
9799
+ this._teardownAnchor();
9800
+ this._setupAnchor();
9801
+ }
9732
9802
  if (name === "closable") this.render();
9733
9803
  }
9734
9804
 
@@ -9964,15 +10034,22 @@ class YumeDialog extends HTMLElement {
9964
10034
  }
9965
10035
 
9966
10036
  _setupAnchor() {
10037
+ const id = this.anchor;
10038
+ if (!id) return;
10039
+ this._anchorResolveDispose = resolveAnchor(this, id, (el) => {
10040
+ this._anchorEl = el;
10041
+ el.addEventListener("click", this._onAnchorClick);
10042
+ });
10043
+ }
10044
+
10045
+ _teardownAnchor() {
10046
+ if (this._anchorResolveDispose) {
10047
+ this._anchorResolveDispose();
10048
+ this._anchorResolveDispose = null;
10049
+ }
9967
10050
  if (this._anchorEl) {
9968
10051
  this._anchorEl.removeEventListener("click", this._onAnchorClick);
9969
- }
9970
- if (this.anchor) {
9971
- const el = document.getElementById(this.anchor);
9972
- if (el) {
9973
- this._anchorEl = el;
9974
- el.addEventListener("click", this._onAnchorClick);
9975
- }
10052
+ this._anchorEl = null;
9976
10053
  }
9977
10054
  }
9978
10055
  }
@@ -10406,13 +10483,11 @@ class YumeDrawer extends HTMLElement {
10406
10483
 
10407
10484
  _setupAnchor() {
10408
10485
  const id = this.anchor;
10409
- if (id) {
10410
- const el = document.getElementById(id);
10411
- if (el) {
10412
- this._anchorEl = el;
10413
- this._anchorEl.addEventListener("click", this._onAnchorClick);
10414
- }
10415
- }
10486
+ if (!id) return;
10487
+ this._anchorResolveDispose = resolveAnchor(this, id, (el) => {
10488
+ this._anchorEl = el;
10489
+ el.addEventListener("click", this._onAnchorClick);
10490
+ });
10416
10491
  }
10417
10492
 
10418
10493
  _show() {
@@ -10432,6 +10507,10 @@ class YumeDrawer extends HTMLElement {
10432
10507
  }
10433
10508
 
10434
10509
  _teardownAnchor() {
10510
+ if (this._anchorResolveDispose) {
10511
+ this._anchorResolveDispose();
10512
+ this._anchorResolveDispose = null;
10513
+ }
10435
10514
  if (this._anchorEl) {
10436
10515
  this._anchorEl.removeEventListener("click", this._onAnchorClick);
10437
10516
  this._anchorEl = null;
@@ -91,6 +91,24 @@ export function createElement(tag: string, attrs?: any, children?: Array<string
91
91
  * @param {HTMLElement} labelWrapper — the wrapper element containing the slot
92
92
  */
93
93
  export function manageLabelVisibility(labelWrapper: HTMLElement): void;
94
+ /**
95
+ * Resolve an anchor element by id for a web component, tolerating DOM insertion
96
+ * races. A synchronous lookup runs first; if it misses, one
97
+ * `requestAnimationFrame` retry covers the common React portal commit-ordering
98
+ * case, and a final `MutationObserver` fallback covers async / lazy mounts.
99
+ *
100
+ * The callback fires at most once. If the host disconnects before the anchor
101
+ * appears, resolution is aborted. Callers must invoke the returned dispose
102
+ * function on teardown (disconnectedCallback, attribute change, etc.) to
103
+ * cancel any pending rAF or observer.
104
+ *
105
+ * @param {HTMLElement} host — component instance; resolution aborts if it disconnects.
106
+ * @param {string} id — the anchor element's id.
107
+ * @param {(el: HTMLElement) => void} onFound — invoked once when the anchor resolves.
108
+ * @param {Document|ShadowRoot} [root=document] — root to search within.
109
+ * @returns {() => void} — dispose function.
110
+ */
111
+ export function resolveAnchor(host: HTMLElement, id: string, onFound: (el: HTMLElement) => void, root?: Document | ShadowRoot): () => void;
94
112
  /**
95
113
  * Resolve a CSS custom-property value to a concrete color string.
96
114
  * Reads from the given element's computed style.
@@ -340,6 +340,64 @@ export function manageLabelVisibility(labelWrapper) {
340
340
  });
341
341
  }
342
342
 
343
+ /**
344
+ * Resolve an anchor element by id for a web component, tolerating DOM insertion
345
+ * races. A synchronous lookup runs first; if it misses, one
346
+ * `requestAnimationFrame` retry covers the common React portal commit-ordering
347
+ * case, and a final `MutationObserver` fallback covers async / lazy mounts.
348
+ *
349
+ * The callback fires at most once. If the host disconnects before the anchor
350
+ * appears, resolution is aborted. Callers must invoke the returned dispose
351
+ * function on teardown (disconnectedCallback, attribute change, etc.) to
352
+ * cancel any pending rAF or observer.
353
+ *
354
+ * @param {HTMLElement} host — component instance; resolution aborts if it disconnects.
355
+ * @param {string} id — the anchor element's id.
356
+ * @param {(el: HTMLElement) => void} onFound — invoked once when the anchor resolves.
357
+ * @param {Document|ShadowRoot} [root=document] — root to search within.
358
+ * @returns {() => void} — dispose function.
359
+ */
360
+ export function resolveAnchor(host, id, onFound, root = document) {
361
+ let disposed = false;
362
+ let rafId = null;
363
+ let observer = null;
364
+
365
+ const dispose = () => {
366
+ disposed = true;
367
+ if (rafId != null) cancelAnimationFrame(rafId);
368
+ if (observer) observer.disconnect();
369
+ rafId = null;
370
+ observer = null;
371
+ };
372
+
373
+ const find = () => {
374
+ if (disposed) return true;
375
+ const el = root.getElementById
376
+ ? root.getElementById(id)
377
+ : document.getElementById(id);
378
+ if (el) {
379
+ dispose();
380
+ onFound(el);
381
+ return true;
382
+ }
383
+ return false;
384
+ };
385
+
386
+ if (find()) return dispose;
387
+ if (!host.isConnected) return dispose;
388
+
389
+ rafId = requestAnimationFrame(() => {
390
+ rafId = null;
391
+ if (find()) return;
392
+ const target = root === document ? document.body : root;
393
+ if (!target) return;
394
+ observer = new MutationObserver(find);
395
+ observer.observe(target, { childList: true, subtree: true });
396
+ });
397
+
398
+ return dispose;
399
+ }
400
+
343
401
  /**
344
402
  * Resolve a CSS custom-property value to a concrete color string.
345
403
  * Reads from the given element's computed style.