@iyulab/modern-app 0.30.0 → 0.30.1

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,16 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.30.1] - 2026-09-28
4
+
5
+ ### Fixed
6
+
7
+ - **A route screen can declare its first focus.** On every route change the shell moved focus to
8
+ the content area unconditionally, so a screen that focused its own input on entry — a scan field,
9
+ a search box — had that focus taken back, and `autofocus` in the screen did nothing. Route
10
+ changes now follow the overlay panel's rule: focus the screen already moved into the content
11
+ stays; else an `[autofocus]` element in the content (through shadow roots, after the screen's
12
+ first render) receives it; else the content area, as before.
13
+
3
14
  ## [0.30.0] - 2026-09-27
4
15
 
5
16
  ### Added
@@ -98,6 +98,19 @@ export declare class SidebarLayout extends StyledElement<SidebarParts> {
98
98
  private handleRouteProgress;
99
99
  /** 라우트 변경 완료 핸들러 */
100
100
  private handleRouteDone;
101
+ /** 라우트 본문(기본 슬롯)에 배정된 요소들 */
102
+ private routeContent;
103
+ /**
104
+ * 라우트 완료 후 포커스 — 오버레이 패널과 같은 규칙이다.
105
+ * ① 화면이 이미 본문 안으로 옮겨 두었으면 건드리지 않는다
106
+ * ② 본문 안의 `[autofocus]`(섀도 경계 안쪽까지)
107
+ * ③ 그 밖에는 `.main` — 마우스 없이 키보드로 스크롤할 수 있고, 새 화면의 시작이 된다.
108
+ * 오버레이가 열려 있으면 아무것도 하지 않는다 — 아래 라우트는 inert 이고 사용자의 포커스는 패널에 있다.
109
+ *
110
+ * 판정은 본문 요소들의 첫 렌더를 기다린 뒤에 한다 — `route-done` 은 화면이 자기 템플릿을
111
+ * 그리기 전에 올 수 있고, 그때 찾으면 `[autofocus]` 가 아직 없다.
112
+ */
113
+ private focusRoute;
101
114
  /** 라우트 에러 핸들러 */
102
115
  private handleRouteError;
103
116
  private handleOverlaySlotChange;
@@ -52,7 +52,28 @@ function D(e, t) {
52
52
  }
53
53
  return !1;
54
54
  }
55
- var O = class extends i {
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
77
  static {
57
78
  b = this;
58
79
  }
@@ -71,7 +92,7 @@ var O = class extends i {
71
92
  this.progressBarEl.removeAttribute("visible");
72
93
  }, 300);
73
94
  let t = this.shadowRoot?.querySelector(".main");
74
- t && (this.config?.scrollBehavior?.(e.context, t), this.hasOverlay || t.focus({ preventScroll: !0 }));
95
+ t && (this.config?.scrollBehavior?.(e.context, t), this.focusRoute(t));
75
96
  }, this.handleRouteError = (e) => {
76
97
  this.progressBarEl.status = "error", this.progressBarEl.value = 100, setTimeout(() => {
77
98
  this.progressBarEl.removeAttribute("visible"), this.progressBarEl.status = "default";
@@ -335,6 +356,19 @@ var O = class extends i {
335
356
  </u-link>
336
357
  `;
337
358
  }
359
+ routeContent() {
360
+ return (this.shadowRoot?.querySelector(".main-content > slot:not([name])"))?.assignedElements({ flatten: !0 }) ?? [];
361
+ }
362
+ async focusRoute(e) {
363
+ let t = this.routeContent();
364
+ 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]");
368
+ r?.focus();
369
+ let i = E();
370
+ r && i && D(r, i) || e.focus({ preventScroll: !0 });
371
+ }
338
372
  overlayPanels() {
339
373
  return (this.shadowRoot?.querySelector("slot[name=\"overlay\"]"))?.assignedElements({ flatten: !0 }) ?? [];
340
374
  }
@@ -361,6 +395,6 @@ var O = class extends i {
361
395
  r([f({
362
396
  type: String,
363
397
  reflect: !0
364
- }), n("design:type", Object)], O.prototype, "state", void 0), r([f({ type: Object }), n("design:type", Object)], O.prototype, "config", void 0), r([f({ type: String }), n("design:type", Object)], O.prototype, "locale", void 0), r([p("u-progress-bar"), n("design:type", y === void 0 ? Object : y)], O.prototype, "progressBarEl", void 0), r([m(), n("design:type", Object)], O.prototype, "hasOverlay", void 0), r([m(), n("design:type", Object)], O.prototype, "hasNotice", void 0), r([m(), n("design:type", Object)], O.prototype, "context", void 0), O = b = r([d("u-sidebar-layout")], O);
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);
365
399
  //#endregion
366
- export { O as SidebarLayout };
400
+ export { k 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.0",
4
+ "version": "0.30.1",
5
5
  "keywords": [
6
6
  "iyulab",
7
7
  "web-framework",
@@ -27,7 +27,7 @@ interface SidebarLayoutConfig {
27
27
  /** Permission filter — hides items (and emptied section/groups) whose requirement fails. Unset shows everything. See "권한 기반 메뉴 필터" below. */
28
28
  hasPermission?: (code: string) => boolean;
29
29
 
30
- /** Called on `route-done` (before focus moves to the container) with the new route's `RouteContext` and the scroll container itself — implement reset/save/restore here. Unset does nothing (same default as Vue Router's unset `scrollBehavior`). Also reachable outside the hook via the element's `.mainElement` accessor. */
30
+ /** Called on `route-done` (before the shell places focus — see *Focus when a route finishes*) with the new route's `RouteContext` and the scroll container itself — implement reset/save/restore here. Unset does nothing (same default as Vue Router's unset `scrollBehavior`). Also reachable outside the hook via the element's `.mainElement` accessor. */
31
31
  scrollBehavior?: (context: RouteContext, main: HTMLElement) => void;
32
32
 
33
33
  /** Shell chrome icon source and names — `{ lib?, menu?, close?, sidebarToggle?, overlayClose? }`, only the keys you set replace the defaults. Defaults come from the bundled `internal` set, so the shell needs no network to draw its own toggles; give `lib` **and** the names to point it at your own set. */
@@ -333,6 +333,23 @@ told apart in CSS; notices are rare and short-lived.
333
333
  While the overlay is open, `part="main-content"` becomes a box at the area's height, because a
334
334
  stacking context needs one — route content is inert and under the overlay then.
335
335
 
336
+ ### Focus when a route finishes
337
+
338
+ On every `route-done` the shell places focus so the keyboard works on the new screen without a
339
+ click — by the same rule as the overlay panel (see *What the shell owns, and what it does not*):
340
+
341
+ 1. **If your screen already moved focus into the route content** (a `focus()` in `firstUpdated`,
342
+ say), the shell leaves it there.
343
+ 2. **Else an `[autofocus]` element in the route content** — found through shadow roots, so an input
344
+ inside your page component's own template counts. The shell waits for the screen's first render
345
+ before looking.
346
+ 3. **Else `part="main"`**, the scroll container — arrow keys and Page Down scroll the new screen,
347
+ and it marks where the new screen starts.
348
+
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
351
+ underneath is inert and focus stays in the panel.
352
+
336
353
  ## Printing
337
354
 
338
355
  On print media the shell drops its app chrome and lets the content flow across pages: the