@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 {
|
|
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
|
|
25
|
-
function
|
|
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
|
|
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
|
|
36
|
+
function D(e, t) {
|
|
36
37
|
for (let n of e) if (n instanceof HTMLElement) {
|
|
37
|
-
if (
|
|
38
|
+
if (E(n)) return n === t;
|
|
38
39
|
if (n === t) break;
|
|
39
40
|
}
|
|
40
41
|
return !0;
|
|
41
42
|
}
|
|
42
|
-
function
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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:${++
|
|
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 =
|
|
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 &&
|
|
99
|
+
if (t instanceof HTMLElement && T(t)) return;
|
|
120
100
|
let n = this.shadowRoot?.querySelector(".main");
|
|
121
|
-
if (!n || !
|
|
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 ||
|
|
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] ??
|
|
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 =
|
|
366
|
-
if (n && t.some((e) =>
|
|
367
|
-
let r =
|
|
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 =
|
|
370
|
-
r && i &&
|
|
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()
|
|
377
|
-
if (
|
|
378
|
-
let
|
|
379
|
-
|
|
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 =
|
|
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)],
|
|
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 {
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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"
|
|
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 |
|