@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
|
-
|
|
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.
|
|
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)],
|
|
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 {
|
|
400
|
+
export { k as SidebarLayout };
|
package/package.json
CHANGED
|
@@ -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
|
|
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
|