@iyulab/modern-app 0.30.1 → 0.31.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
@@ -1,5 +1,38 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.31.0] - 2026-09-28
4
+
5
+ ### Changed
6
+
7
+ - **Depends on `@iyulab/router` `^0.17.0`**, where `@lit/react` is an optional peer. Apps that import
8
+ `@iyulab/modern-app/react` already install `@lit/react` (an optional peer here too); nothing else
9
+ changes.
10
+
11
+ ### Fixed
12
+
13
+ - **`document.querySelector('u-sidebar-layout')` is typed as `SidebarLayout`.** The shell was the
14
+ one element without an `HTMLElementTagNameMap` entry, so reaching `mainElement` from a query
15
+ needed a cast.
16
+
17
+ ## [0.30.2] - 2026-09-28
18
+
19
+ ### Fixed
20
+
21
+ - **An overlay panel that is a component now gets focus inside it.** The shell looked for the
22
+ panel's `[autofocus]` element and first input control with `querySelector`, which stops at shadow
23
+ roots — so when the panel was a custom element rendering its controls in its own template, the
24
+ shell found neither and fell back to the close button. The search now goes through shadow roots
25
+ and waits for the panel's first render, as route focus does since 0.30.1.
26
+ - **Route and overlay focus skip a `u-select`'s closed popover.** Searching through shadow roots
27
+ (0.30.1) also reached the popover every `u-select` keeps inside, which carries an `autofocus`
28
+ attribute of its own — a screen with a `u-select` before its `autofocus` field got the content
29
+ area instead. Only elements that can actually take focus are candidates now.
30
+
31
+ ### Changed
32
+
33
+ - **Requires `@iyulab/components` 1.47.1 or later** (peer) — the shell now uses its
34
+ `querySelectorDeep` / `isFocusCandidate`, the same lookup `u-dialog` and `u-drawer` use.
35
+
3
36
  ## [0.30.1] - 2026-09-28
4
37
 
5
38
  ### 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
  /**
@@ -151,3 +152,8 @@ export declare class SidebarLayout extends StyledElement<SidebarParts> {
151
152
  /** 화면 크기 변경에 따른 사이드바 상태 업데이트 */
152
153
  private handleScreenResize;
153
154
  }
155
+ declare global {
156
+ interface HTMLElementTagNameMap {
157
+ 'u-sidebar-layout': SidebarLayout;
158
+ }
159
+ }
@@ -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.31.0",
5
5
  "keywords": [
6
6
  "iyulab",
7
7
  "web-framework",
@@ -59,12 +59,12 @@
59
59
  "test:react-types": "tsc -p tsconfig.react-smoke.json"
60
60
  },
61
61
  "dependencies": {
62
- "@iyulab/router": "^0.16.0",
62
+ "@iyulab/router": "^0.17.0",
63
63
  "i18next": "^26.3.6",
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
  },
@@ -77,18 +77,19 @@
77
77
  }
78
78
  },
79
79
  "devDependencies": {
80
+ "@iyulab/components": ">=1.47.1",
80
81
  "@lit/react": "^1.0.8",
81
82
  "@types/node": "^26.1.1",
82
83
  "@types/react": "^19.2.14",
83
84
  "@types/react-dom": "^19.2.3",
84
85
  "@vitest/browser-playwright": "^5.0.0",
86
+ "glob": "^13.0.6",
85
87
  "happy-dom": "^20.10.6",
86
88
  "react": "^19.2.7",
87
89
  "react-dom": "^19.2.7",
88
90
  "typescript": "^6.0.2",
89
91
  "vite": "^8.1.4",
90
92
  "vite-plugin-dts": "^5.0.3",
91
- "vitest": "^5.0.0",
92
- "@iyulab/components": ">=1.45.2"
93
+ "vitest": "^5.0.0"
93
94
  }
94
95
  }
@@ -347,7 +347,9 @@ click — by the same rule as the overlay panel (see *What the shell owns, and w
347
347
  and it marks where the new screen starts.
348
348
 
349
349
  So a screen that must receive input on entry — a barcode scan field, a search box — only needs
350
- `autofocus` on that control. While the overlay is open the shell does none of this: the route
350
+ `autofocus` on that control. The same holds for a React screen (`autoFocus` on a native input, or `autofocus`
351
+ on a component such as `UInput`) — `@iyulab/router` 0.16.1 and later report `route-done` only after
352
+ React has committed the screen. While the overlay is open the shell does none of this: the route
351
353
  underneath is inert and focus stays in the panel.
352
354
 
353
355
  ## Printing
@@ -414,7 +416,7 @@ from either re-implementing what the shell already does, or dropping what it doe
414
416
  | Making route content non-interactive | shell | `inert` on `part="main-content"`, which propagates through the slot into your route content |
415
417
  | A close affordance | shell | `part="overlay-close"`, firing the non-cancelable `overlay-close` event |
416
418
  | 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 |
419
+ | 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
420
  | 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
421
  | 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
422
  | **A backdrop / dimmed scrim** | **consumer** | the panel is opaque and full-bleed by design; add a scrim inside your panel if you want one |