@iyulab/modern-app 0.30.1 → 0.30.2

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
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.30.2] - 2026-09-28
4
+
5
+ ### Fixed
6
+
7
+ - **An overlay panel that is a component now gets focus inside it.** The shell looked for the
8
+ panel's `[autofocus]` element and first input control with `querySelector`, which stops at shadow
9
+ roots — so when the panel was a custom element rendering its controls in its own template, the
10
+ shell found neither and fell back to the close button. The search now goes through shadow roots
11
+ and waits for the panel's first render, as route focus does since 0.30.1.
12
+ - **Route and overlay focus skip a `u-select`'s closed popover.** Searching through shadow roots
13
+ (0.30.1) also reached the popover every `u-select` keeps inside, which carries an `autofocus`
14
+ attribute of its own — a screen with a `u-select` before its `autofocus` field got the content
15
+ area instead. Only elements that can actually take focus are candidates now.
16
+
17
+ ### Changed
18
+
19
+ - **Requires `@iyulab/components` 1.47.1 or later** (peer) — the shell now uses its
20
+ `querySelectorDeep` / `isFocusCandidate`, the same lookup `u-dialog` and `u-drawer` use.
21
+
3
22
  ## [0.30.1] - 2026-09-28
4
23
 
5
24
  ### Fixed
@@ -122,6 +122,7 @@ export declare class SidebarLayout extends StyledElement<SidebarParts> {
122
122
  * `[autofocus]` → 첫 입력 컨트롤 → (없으면) 셸의 닫기 버튼.
123
123
  * 닫기 버튼으로 떨어지는 것은 의도다 — 포커스가 `<body>` 에 남는 것보다 항상 낫고,
124
124
  * 셸이 늘 가진 유일한 컨트롤이다. 소비자가 이미 패널 안으로 옮겨 두었으면 건드리지 않는다.
125
+ * 탐색은 섀도 경계 안쪽까지 한다 — 패널이 컴포넌트면 컨트롤은 그 섀도 루트에 있다.
125
126
  */
126
127
  private focusOverlay;
127
128
  /**
@@ -19,32 +19,33 @@ import { unsafeHTML as h } from "lit/directives/unsafe-html.js";
19
19
  import { repeat as g } from "lit/directives/repeat.js";
20
20
  import { ifDefined as _ } from "lit/directives/if-defined.js";
21
21
  import { createDevWarner as v } from "@iyulab/components/dist/utilities/devWarning.js";
22
- import { UProgressBar as y } from "@iyulab/components/dist/components/progress-bar/UProgressBar.js";
22
+ import { isFocusCandidate as y, querySelectorDeep as b } from "@iyulab/components/dist/utilities/elements.js";
23
+ import { UProgressBar as x } from "@iyulab/components/dist/components/progress-bar/UProgressBar.js";
23
24
  //#region src/layouts/SidebarLayout.ts
24
- var b, x = v("@iyulab/modern-app"), S = 0;
25
- function C(e) {
25
+ var S, C = v("@iyulab/modern-app"), w = 0;
26
+ function T(e) {
26
27
  let t = e.tagName;
27
28
  if (t === "INPUT" || t === "TEXTAREA" || t === "SELECT" || e.isContentEditable) return !0;
28
29
  let n = e.getAttribute("role");
29
30
  return n === "textbox" || n === "searchbox" || n === "combobox" || n === "spinbutton";
30
31
  }
31
- function w(e) {
32
+ function E(e) {
32
33
  let t = getComputedStyle(e);
33
34
  return (t.overflowY === "auto" || t.overflowY === "scroll") && e.scrollHeight > e.clientHeight;
34
35
  }
35
- function T(e, t) {
36
+ function D(e, t) {
36
37
  for (let n of e) if (n instanceof HTMLElement) {
37
- if (w(n)) return n === t;
38
+ if (E(n)) return n === t;
38
39
  if (n === t) break;
39
40
  }
40
41
  return !0;
41
42
  }
42
- function E() {
43
+ function O() {
43
44
  let e = document.activeElement;
44
45
  for (; e?.shadowRoot?.activeElement;) e = e.shadowRoot.activeElement;
45
46
  return e;
46
47
  }
47
- function D(e, t) {
48
+ function k(e, t) {
48
49
  let n = t;
49
50
  for (; n;) {
50
51
  if (n === e) return !0;
@@ -52,33 +53,12 @@ function D(e, t) {
52
53
  }
53
54
  return !1;
54
55
  }
55
- function O(e, t) {
56
- let n = (e) => {
57
- let r = e instanceof Element ? [e] : Array.from(e.children);
58
- for (let e of r) {
59
- let r = document.createTreeWalker(e, NodeFilter.SHOW_ELEMENT);
60
- for (let e = r.currentNode; e; e = r.nextNode()) {
61
- if (e.matches(t)) return e;
62
- if (e.shadowRoot) {
63
- let t = n(e.shadowRoot);
64
- if (t) return t;
65
- }
66
- }
67
- }
68
- return null;
69
- };
70
- for (let t of e) {
71
- let e = n(t);
72
- if (e) return e;
73
- }
74
- return null;
75
- }
76
- var k = class extends i {
56
+ var A = class extends i {
77
57
  static {
78
- b = this;
58
+ S = this;
79
59
  }
80
60
  constructor(...t) {
81
- super(...t), this.state = "default", this.locale = "", this.hasOverlay = !1, this.hasNotice = !1, this.overlayTrigger = null, this.internals = typeof this.attachInternals == "function" ? this.attachInternals() : void 0, this.context = null, this.unsizedWarnKey = `unsized:${++S}`, this.isMatchedLink = (e) => !this.context || !e ? !1 : (e = typeof e == "string" ? new URLPattern(e, window.location.origin) : e, e.test(this.context.path, window.location.origin)), this.handleToggleButtonClick = () => {
61
+ super(...t), this.state = "default", this.locale = "", this.hasOverlay = !1, this.hasNotice = !1, this.overlayTrigger = null, this.internals = typeof this.attachInternals == "function" ? this.attachInternals() : void 0, this.context = null, this.unsizedWarnKey = `unsized:${++w}`, this.isMatchedLink = (e) => !this.context || !e ? !1 : (e = typeof e == "string" ? new URLPattern(e, window.location.origin) : e, e.test(this.context.path, window.location.origin)), this.handleToggleButtonClick = () => {
82
62
  let t = this.screenSize ?? e.screen ?? "large";
83
63
  t === "large" ? this.state = this.state === "default" ? "slim" : "default" : t === "medium" ? this.state = this.state === "slim" ? "modal" : "slim" : t === "small" ? this.state = this.state === "mobile" ? "mobile-open" : "mobile" : console.warn("Unknown screen size:", t);
84
64
  }, this.handleBackdropClick = () => {
@@ -100,7 +80,7 @@ var k = class extends i {
100
80
  }, this.handleOverlaySlotChange = (e) => {
101
81
  let t = o(e.target);
102
82
  if (t && !this.hasOverlay) {
103
- let e = E();
83
+ let e = O();
104
84
  this.overlayTrigger = e && e !== document.body ? e : null;
105
85
  }
106
86
  this.hasOverlay = t;
@@ -116,9 +96,9 @@ var k = class extends i {
116
96
  this.fire("overlay-close", { cancelable: !1 });
117
97
  }, this._handleMainKeydown = (e) => {
118
98
  let t = e.composedPath()[0];
119
- if (t instanceof HTMLElement && C(t)) return;
99
+ if (t instanceof HTMLElement && T(t)) return;
120
100
  let n = this.shadowRoot?.querySelector(".main");
121
- if (!n || !T(e.composedPath(), n)) return;
101
+ if (!n || !D(e.composedPath(), n)) return;
122
102
  let r = n.clientHeight;
123
103
  switch (e.key) {
124
104
  case " ":
@@ -173,7 +153,7 @@ var k = class extends i {
173
153
  process.env.NODE_ENV !== "production" && requestAnimationFrame(() => {
174
154
  if (!this.isConnected) return;
175
155
  let e = this.getBoundingClientRect().height;
176
- e >= 200 || x(this.unsizedWarnKey, `u-sidebar-layout is only ${Math.round(e)}px tall — its height: 100% found no sized ancestor, so the shell sits at its own chrome height and the route area has almost no room. Give the root element a height (e.g. #app { height: 100vh } — app.load() does this for document.body).`);
156
+ e >= 200 || C(this.unsizedWarnKey, `u-sidebar-layout is only ${Math.round(e)}px tall — its height: 100% found no sized ancestor, so the shell sits at its own chrome height and the route area has almost no room. Give the root element a height (e.g. #app { height: 100vh } — app.load() does this for document.body).`);
177
157
  });
178
158
  }
179
159
  render() {
@@ -325,7 +305,7 @@ var k = class extends i {
325
305
  };
326
306
  }
327
307
  icon(e) {
328
- return this.config?.icons?.[e] ?? b.DEFAULT_ICONS[e];
308
+ return this.config?.icons?.[e] ?? S.DEFAULT_ICONS[e];
329
309
  }
330
310
  renderLogo() {
331
311
  let e = this.config?.logo, t = this.config?.title ?? "Home";
@@ -362,39 +342,32 @@ var k = class extends i {
362
342
  async focusRoute(e) {
363
343
  let t = this.routeContent();
364
344
  if (await Promise.all(t.map((e) => e.updateComplete)), this.hasOverlay || !e.isConnected) return;
365
- let n = E();
366
- if (n && t.some((e) => D(e, n))) return;
367
- let r = O(t, "[autofocus]");
345
+ let n = O();
346
+ if (n && t.some((e) => k(e, n))) return;
347
+ let r = b(t, "[autofocus]", y);
368
348
  r?.focus();
369
- let i = E();
370
- r && i && D(r, i) || e.focus({ preventScroll: !0 });
349
+ let i = O();
350
+ r && i && k(r, i) || e.focus({ preventScroll: !0 });
371
351
  }
372
352
  overlayPanels() {
373
353
  return (this.shadowRoot?.querySelector("slot[name=\"overlay\"]"))?.assignedElements({ flatten: !0 }) ?? [];
374
354
  }
375
- focusOverlay() {
376
- let e = this.overlayPanels(), t = E();
377
- if (t && e.some((e) => D(e, t))) return;
378
- let n = (t) => {
379
- for (let n of e) {
380
- if (n.matches(t)) return n;
381
- let e = n.querySelector(t);
382
- if (e) return e;
383
- }
384
- return null;
385
- };
386
- (n("[autofocus]") ?? n("input, select, textarea, u-input, u-textarea, u-select, u-checkbox, u-radio, u-switch, u-slider") ?? this.shadowRoot?.querySelector(".overlay-close"))?.focus();
355
+ async focusOverlay() {
356
+ let e = this.overlayPanels();
357
+ if (await Promise.all(e.map((e) => e.updateComplete)), !this.hasOverlay) return;
358
+ let t = O();
359
+ t && e.some((e) => k(e, t)) || (b(e, "[autofocus]", y) ?? b(e, "input, select, textarea, u-input, u-textarea, u-select, u-checkbox, u-radio, u-switch, u-slider", y) ?? this.shadowRoot?.querySelector(".overlay-close"))?.focus();
387
360
  }
388
361
  restoreOverlayFocus() {
389
362
  let e = this.overlayTrigger;
390
363
  this.overlayTrigger = null;
391
- let t = E();
364
+ let t = O();
392
365
  t && t !== document.body || (e?.isConnected ? e.focus() : this.mainElement?.focus({ preventScroll: !0 }));
393
366
  }
394
367
  };
395
368
  r([f({
396
369
  type: String,
397
370
  reflect: !0
398
- }), n("design:type", Object)], k.prototype, "state", void 0), r([f({ type: Object }), n("design:type", Object)], k.prototype, "config", void 0), r([f({ type: String }), n("design:type", Object)], k.prototype, "locale", void 0), r([p("u-progress-bar"), n("design:type", y === void 0 ? Object : y)], k.prototype, "progressBarEl", void 0), r([m(), n("design:type", Object)], k.prototype, "hasOverlay", void 0), r([m(), n("design:type", Object)], k.prototype, "hasNotice", void 0), r([m(), n("design:type", Object)], k.prototype, "context", void 0), k = b = r([d("u-sidebar-layout")], k);
371
+ }), n("design:type", Object)], A.prototype, "state", void 0), r([f({ type: Object }), n("design:type", Object)], A.prototype, "config", void 0), r([f({ type: String }), n("design:type", Object)], A.prototype, "locale", void 0), r([p("u-progress-bar"), n("design:type", x === void 0 ? Object : x)], A.prototype, "progressBarEl", void 0), r([m(), n("design:type", Object)], A.prototype, "hasOverlay", void 0), r([m(), n("design:type", Object)], A.prototype, "hasNotice", void 0), r([m(), n("design:type", Object)], A.prototype, "context", void 0), A = S = r([d("u-sidebar-layout")], A);
399
372
  //#endregion
400
- export { k as SidebarLayout };
373
+ export { A as SidebarLayout };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@iyulab/modern-app",
3
3
  "description": "web-framework by iyulab based on lit-element",
4
- "version": "0.30.1",
4
+ "version": "0.30.2",
5
5
  "keywords": [
6
6
  "iyulab",
7
7
  "web-framework",
@@ -64,7 +64,7 @@
64
64
  "lit": "^3.3.3"
65
65
  },
66
66
  "peerDependencies": {
67
- "@iyulab/components": ">=1.45.2",
67
+ "@iyulab/components": ">=1.47.1",
68
68
  "@lit/react": ">=1.0.8",
69
69
  "react": ">=18.0.0"
70
70
  },
@@ -89,6 +89,6 @@
89
89
  "vite": "^8.1.4",
90
90
  "vite-plugin-dts": "^5.0.3",
91
91
  "vitest": "^5.0.0",
92
- "@iyulab/components": ">=1.45.2"
92
+ "@iyulab/components": ">=1.47.1"
93
93
  }
94
94
  }
@@ -414,7 +414,7 @@ from either re-implementing what the shell already does, or dropping what it doe
414
414
  | Making route content non-interactive | shell | `inert` on `part="main-content"`, which propagates through the slot into your route content |
415
415
  | A close affordance | shell | `part="overlay-close"`, firing the non-cancelable `overlay-close` event |
416
416
  | Keeping the route mounted underneath | shell | the overlay is independent of routing |
417
- | Moving focus into the panel when it opens | shell | an `[autofocus]` element in your panel, else its first input control, else `part="overlay-close"`. If you already moved focus into the panel, the shell leaves it there |
417
+ | Moving focus into the panel when it opens | shell | an `[autofocus]` element in your panel, else its first input control, else `part="overlay-close"` — searched through shadow roots, after the panel's first render, so a panel that is itself a component works the same. If you already moved focus into the panel, the shell leaves it there |
418
418
  | Restoring focus when it closes | shell | back to the control that held focus when the panel opened — only if focus fell to `<body>`; if you moved it somewhere on purpose, that stands. With no such control (opened from code), focus goes to `part="main"` |
419
419
  | Escape to close | shell | Escape inside the panel fires the same `overlay-close` as the button — you still empty the slot. The shell decides **after** every layer inside the panel has: an Escape consumed there (`preventDefault()` — an open list, popover, drawer or dialog, whenever it was opened) closes only that layer. The shell does not mark the Escape consumed itself |
420
420
  | **A backdrop / dimmed scrim** | **consumer** | the panel is opaque and full-bleed by design; add a scrim inside your panel if you want one |