@iyulab/modern-app 0.42.0 → 0.42.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.42.2] - 2026-10-07
4
+
5
+ ### Fixed
6
+
7
+ - The sidebar layout registers the `u-progress-bar` it renders with an explicit import. It imported `UProgressBar` for
8
+ a type only, and the published module kept the import by accident (decorator metadata).
9
+
10
+ ## [0.42.1] - 2026-10-07
11
+
12
+ ### Fixed
13
+
14
+ - **A long screen with no controls can be scrolled from the keyboard.** The sidebar layout's content region carries
15
+ `tabindex="-1"` for programmatic focus (skip link, route change) — and a negative tabindex is the explicit opt-out
16
+ from the browser's own rule that makes a scroller with nothing focusable inside a Tab stop (Chrome 130). So a
17
+ keyboard user who tabbed past a document or notice screen had no way to scroll it (axe
18
+ `scrollable-region-focusable`). The region is now a Tab stop exactly when it overflows and holds nothing reachable
19
+ by Tab (shadow roots included), and re-judges when the content's size or slotted elements change; screens with
20
+ controls keep `-1`.
21
+
3
22
  ## [0.42.0] - 2026-10-07
4
23
 
5
24
  ### Added
@@ -30,6 +30,16 @@ export declare class SidebarLayout extends StyledElement<SidebarParts> {
30
30
  * 숨어 이름 없는 진행 막대가 모든 화면의 접근성 트리에 떠 있었다(axe `aria-progressbar-name`).
31
31
  */
32
32
  private routeLoading;
33
+ /**
34
+ * 본문이 탭 정지인가 — 넘쳐서 스크롤되는데 안에 탭으로 닿을 것이 하나도 없을 때만.
35
+ *
36
+ * 브라우저 규칙(«키보드 포커스 가능한 스크롤러», Chrome 130)을 셸이 재현한다: 그런 스크롤러는 브라우저가 스스로
37
+ * 탭 정지로 만들지만, `tabindex="-1"` 이 그 규칙의 명시적 opt-out 이다. 셸은 프로그램 포커스(skip link · 라우트
38
+ * 완료) 때문에 `-1` 을 달았고, 그래서 컨트롤 없는 긴 화면(문서 · 안내)을 Tab 으로 지나치면 스크롤할 길이 없었다
39
+ * (axe `scrollable-region-focusable`). 컨트롤이 있는 화면은 그 컨트롤에서 스크롤 키가 듣는다 — 정지를 더하지 않는다.
40
+ */
41
+ private mainTabStop;
42
+ private mainSizeObserver?;
33
43
  /**
34
44
  * 오버레이를 연 컨트롤 — 닫힐 때 포커스를 되돌릴 곳.
35
45
  * ★슬롯 배정 «시점» 에 잡는다. 렌더가 본문에 `inert` 를 걸면 그 컨트롤이 쥐던 포커스는
@@ -129,6 +139,17 @@ export declare class SidebarLayout extends StyledElement<SidebarParts> {
129
139
  private handleRouteProgress;
130
140
  /** 라우트 변경 완료 핸들러 */
131
141
  private handleRouteDone;
142
+ /**
143
+ * 본문과 그 내용의 크기가 바뀌면(넘침 · 내용이 그려짐) 탭 정지를 다시 판정한다. 본문이 처음 그려질 때 한 번 건다.
144
+ * ⚠`.main-content` 는 `display: contents` 라 상자가 없다 — 관찰 대상은 슬롯에 배정된 요소들이고, 배정이 바뀔 때마다 다시 건다.
145
+ */
146
+ private observeMainSize;
147
+ private observedContent;
148
+ private observeContentSize;
149
+ private handleMainSlotChange;
150
+ private syncMainTabStop;
151
+ /** 공지 슬롯에 배정된 요소들 */
152
+ private noticeContent;
132
153
  /** 라우트 본문(기본 슬롯)에 배정된 요소들 */
133
154
  private routeContent;
134
155
  /**
@@ -24,16 +24,16 @@ import "@iyulab/components/dist/components/menu-item/UMenuItem.js";
24
24
  import { createDevWarner as v } from "@iyulab/components/dist/utilities/devWarning.js";
25
25
  import { isFocusCandidate as y, querySelectorDeep as b } from "@iyulab/components/dist/utilities/elements.js";
26
26
  import { OverlayManager as x } from "@iyulab/components/dist/utilities/OverlayManager.js";
27
- import { UProgressBar as S } from "@iyulab/components/dist/components/progress-bar/UProgressBar.js";
27
+ import "@iyulab/components/dist/components/progress-bar/UProgressBar.js";
28
28
  //#region src/layouts/SidebarLayout.ts
29
- var C, w = v("@iyulab/modern-app"), T = 0;
30
- function E(e) {
29
+ var S, C = v("@iyulab/modern-app"), w = 0;
30
+ function T(e) {
31
31
  let t = e.tagName;
32
32
  if (t === "INPUT" || t === "TEXTAREA" || t === "SELECT" || e.isContentEditable) return !0;
33
33
  let n = e.getAttribute("role");
34
34
  return n === "textbox" || n === "searchbox" || n === "combobox" || n === "spinbutton";
35
35
  }
36
- var D = /* @__PURE__ */ new Set([
36
+ var E = /* @__PURE__ */ new Set([
37
37
  "button",
38
38
  "link",
39
39
  "checkbox",
@@ -46,27 +46,27 @@ var D = /* @__PURE__ */ new Set([
46
46
  "tab",
47
47
  "treeitem"
48
48
  ]);
49
- function O(e) {
49
+ function D(e) {
50
50
  let t = e.tagName;
51
- return t === "BUTTON" || t === "SUMMARY" || t === "A" && e.hasAttribute("href") ? !0 : D.has(e.getAttribute("role") ?? "");
51
+ return t === "BUTTON" || t === "SUMMARY" || t === "A" && e.hasAttribute("href") ? !0 : E.has(e.getAttribute("role") ?? "");
52
52
  }
53
- function k(e) {
53
+ function O(e) {
54
54
  let t = getComputedStyle(e);
55
55
  return (t.overflowY === "auto" || t.overflowY === "scroll") && e.scrollHeight > e.clientHeight;
56
56
  }
57
- function A(e, t) {
57
+ function k(e, t) {
58
58
  for (let n of e) if (n instanceof HTMLElement) {
59
- if (k(n)) return n === t;
59
+ if (O(n)) return n === t;
60
60
  if (n === t) break;
61
61
  }
62
62
  return !0;
63
63
  }
64
- function j() {
64
+ function A() {
65
65
  let e = document.activeElement;
66
66
  for (; e?.shadowRoot?.activeElement;) e = e.shadowRoot.activeElement;
67
67
  return e;
68
68
  }
69
- function M(e, t) {
69
+ function j(e, t) {
70
70
  let n = t;
71
71
  for (; n;) {
72
72
  if (n === e) return !0;
@@ -74,12 +74,12 @@ function M(e, t) {
74
74
  }
75
75
  return !1;
76
76
  }
77
- var N = class extends i {
77
+ var M = class extends i {
78
78
  static {
79
- C = this;
79
+ S = this;
80
80
  }
81
81
  constructor(...t) {
82
- super(...t), this.state = "default", this.counts = {}, this.locale = "", this.hasOverlay = !1, this.openMenu = null, this.menuIds = /* @__PURE__ */ new WeakMap(), this.hasNotice = !1, this.routeLoading = !1, this.overlayTrigger = null, this.internals = typeof this.attachInternals == "function" ? this.attachInternals() : void 0, this.currentPath = null, this.unsizedWarnKey = `unsized:${++T}`, this.isMatchedLink = (e) => this.currentPath === null || !e ? !1 : (e = typeof e == "string" ? new URLPattern(e, window.location.origin) : e, e.test(this.currentPath, window.location.origin)), this.handleToggleButtonClick = () => {
82
+ super(...t), this.state = "default", this.counts = {}, this.locale = "", this.hasOverlay = !1, this.openMenu = null, this.menuIds = /* @__PURE__ */ new WeakMap(), this.hasNotice = !1, this.routeLoading = !1, this.mainTabStop = !1, this.overlayTrigger = null, this.internals = typeof this.attachInternals == "function" ? this.attachInternals() : void 0, this.currentPath = null, this.unsizedWarnKey = `unsized:${++w}`, this.isMatchedLink = (e) => this.currentPath === null || !e ? !1 : (e = typeof e == "string" ? new URLPattern(e, window.location.origin) : e, e.test(this.currentPath, window.location.origin)), this.handleToggleButtonClick = () => {
83
83
  let t = this.screenSize ?? e.screen ?? "large";
84
84
  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);
85
85
  }, this.handleBackdropClick = () => {
@@ -93,7 +93,14 @@ var N = class extends i {
93
93
  this.routeLoading = !1;
94
94
  }, 300);
95
95
  let t = this.shadowRoot?.querySelector(".main");
96
- t && (this.config?.scrollBehavior?.(e.context, t), this.focusRoute(t));
96
+ t && (this.config?.scrollBehavior?.(e.context, t), this.focusRoute(t), requestAnimationFrame(this.syncMainTabStop));
97
+ }, this.observedContent = [], this.handleMainSlotChange = () => {
98
+ this.observeContentSize(), this.syncMainTabStop();
99
+ }, this.syncMainTabStop = () => {
100
+ let e = this.shadowRoot?.querySelector(".main");
101
+ if (!e) return;
102
+ let t = e.scrollHeight > e.clientHeight + 1 || e.scrollWidth > e.clientWidth + 1, n = t && !!b([...this.noticeContent(), ...this.routeContent()], "*", (e) => y(e) && e.tabIndex >= 0);
103
+ this.mainTabStop = t && !n;
97
104
  }, this.handleSkipLinkClick = (e) => {
98
105
  e.preventDefault();
99
106
  let t = this.shadowRoot?.querySelector(".main");
@@ -105,12 +112,12 @@ var N = class extends i {
105
112
  }, this.handleOverlaySlotChange = (e) => {
106
113
  let t = o(e.target);
107
114
  if (t && !this.hasOverlay) {
108
- let e = j();
115
+ let e = A();
109
116
  this.overlayTrigger = e && e !== document.body ? e : null;
110
117
  }
111
118
  this.hasOverlay = t;
112
119
  }, this.handleNoticeSlotChange = (e) => {
113
- this.hasNotice = o(e.target);
120
+ this.hasNotice = o(e.target), this.handleMainSlotChange();
114
121
  }, this.handleOverlayEscape = (e) => {
115
122
  let t = this.shadowRoot?.querySelector(".overlay");
116
123
  return !this.hasOverlay || !t || !e.composedPath().includes(t) ? !1 : (this.handleOverlayClose(), !0);
@@ -119,9 +126,9 @@ var N = class extends i {
119
126
  }, this._handleMainKeydown = (e) => {
120
127
  if (e.defaultPrevented) return;
121
128
  let t = e.composedPath()[0];
122
- if (t instanceof HTMLElement && E(t) || e.key === " " && t instanceof HTMLElement && O(t)) return;
129
+ if (t instanceof HTMLElement && T(t) || e.key === " " && t instanceof HTMLElement && D(t)) return;
123
130
  let n = this.shadowRoot?.querySelector(".main");
124
- if (!n || !A(e.composedPath(), n)) return;
131
+ if (!n || !k(e.composedPath(), n)) return;
125
132
  let r = n.clientHeight;
126
133
  switch (e.key) {
127
134
  case " ":
@@ -164,13 +171,13 @@ var N = class extends i {
164
171
  super.connectedCallback(), this.currentPath = l(), window.addEventListener("route-begin", this.handleRouteBegin), window.addEventListener("route-done", this.handleRouteDone), window.addEventListener("route-progress", this.handleRouteProgress), window.addEventListener("route-error", this.handleRouteError), window.addEventListener("screen-resize", this.handleScreenResize);
165
172
  }
166
173
  disconnectedCallback() {
167
- window.removeEventListener("route-begin", this.handleRouteBegin), window.removeEventListener("route-done", this.handleRouteDone), window.removeEventListener("route-progress", this.handleRouteProgress), window.removeEventListener("route-error", this.handleRouteError), window.removeEventListener("screen-resize", this.handleScreenResize), x.closeLayer(this), super.disconnectedCallback();
174
+ window.removeEventListener("route-begin", this.handleRouteBegin), window.removeEventListener("route-done", this.handleRouteDone), window.removeEventListener("route-progress", this.handleRouteProgress), window.removeEventListener("route-error", this.handleRouteError), window.removeEventListener("screen-resize", this.handleScreenResize), x.closeLayer(this), this.mainSizeObserver?.disconnect(), this.mainSizeObserver = void 0, super.disconnectedCallback();
168
175
  }
169
176
  willUpdate(e) {
170
177
  super.willUpdate(e), e.has("config") && (this.styles = this.config?.styles);
171
178
  }
172
179
  updated(e) {
173
- super.updated(e), e.has("hasOverlay") && (this.internals?.states?.[this.hasOverlay ? "add" : "delete"]("overlay"), this.hasOverlay ? (x.openLayer(this, this.handleOverlayEscape), this.focusOverlay()) : e.get("hasOverlay") === !0 && (x.closeLayer(this), this.restoreOverlayFocus()));
180
+ super.updated(e), this.observeMainSize(), e.has("hasOverlay") && (this.internals?.states?.[this.hasOverlay ? "add" : "delete"]("overlay"), this.hasOverlay ? (x.openLayer(this, this.handleOverlayEscape), this.focusOverlay()) : e.get("hasOverlay") === !0 && (x.closeLayer(this), this.restoreOverlayFocus()));
174
181
  }
175
182
  firstUpdated(e) {
176
183
  super.firstUpdated(e), this.warnIfUnsized();
@@ -179,7 +186,7 @@ var N = class extends i {
179
186
  process.env.NODE_ENV !== "production" && requestAnimationFrame(() => {
180
187
  if (!this.isConnected) return;
181
188
  let e = this.getBoundingClientRect().height;
182
- e >= 200 || w(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).`);
189
+ 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).`);
183
190
  });
184
191
  }
185
192
  render() {
@@ -237,7 +244,7 @@ var N = class extends i {
237
244
 
238
245
  <!-- Main Content -->
239
246
  <div class="main-region">
240
- <main class="main" part="main" scrollable tabindex="-1" @keydown=${this._handleMainKeydown}>
247
+ <main class="main" part="main" scrollable tabindex=${this.mainTabStop ? "0" : "-1"} @keydown=${this._handleMainKeydown}>
241
248
  <u-progress-bar part="progress" class=${this.routeLoading ? "loading" : ""}
242
249
  aria-hidden=${this.routeLoading ? d : "true"}
243
250
  aria-label=${a(this.locale || void 0).pageLoading}></u-progress-bar>
@@ -248,7 +255,7 @@ var N = class extends i {
248
255
  aria-label=${this.config.noticesAriaLabel ?? d}>
249
256
  <slot name="notice" @slotchange=${this.handleNoticeSlotChange}></slot>
250
257
  </div>
251
- <slot></slot>
258
+ <slot @slotchange=${this.handleMainSlotChange}></slot>
252
259
  </div>
253
260
  </main>
254
261
 
@@ -270,7 +277,7 @@ var N = class extends i {
270
277
  }
271
278
  renderMenu(e) {
272
279
  let t = this.menuIds.get(e);
273
- t || (t = `u-sidebar-menu-${++C.menuSeq}`, this.menuIds.set(e, t));
280
+ t || (t = `u-sidebar-menu-${++S.menuSeq}`, this.menuIds.set(e, t));
274
281
  let n = t, r = this.state === "mobile" || this.state === "mobile-open" ? "bottom-start" : "right-start";
275
282
  return u`
276
283
  <u-sidebar-button
@@ -305,8 +312,8 @@ var N = class extends i {
305
312
  }
306
313
  handleMenuHide(e, t) {
307
314
  this.openMenu === t && (this.openMenu = null);
308
- let n = e.currentTarget, r = j();
309
- r && M(n, r) && this.shadowRoot?.querySelector(`#${t}`)?.focus();
315
+ let n = e.currentTarget, r = A();
316
+ r && j(n, r) && this.shadowRoot?.querySelector(`#${t}`)?.focus();
310
317
  }
311
318
  renderItem(e) {
312
319
  if (!e) return d;
@@ -379,7 +386,7 @@ var N = class extends i {
379
386
  };
380
387
  }
381
388
  icon(e) {
382
- return this.config?.icons?.[e] ?? C.DEFAULT_ICONS[e];
389
+ return this.config?.icons?.[e] ?? S.DEFAULT_ICONS[e];
383
390
  }
384
391
  renderLogo() {
385
392
  let e = this.config?.logo, t = this.config?.title ?? "Home";
@@ -410,20 +417,36 @@ var N = class extends i {
410
417
  </u-link>
411
418
  `;
412
419
  }
420
+ observeMainSize() {
421
+ if (this.mainSizeObserver || typeof ResizeObserver > "u") return;
422
+ let e = this.shadowRoot?.querySelector(".main");
423
+ e && (this.mainSizeObserver = new ResizeObserver(() => this.syncMainTabStop()), this.mainSizeObserver.observe(e), this.observeContentSize());
424
+ }
425
+ observeContentSize() {
426
+ let e = this.mainSizeObserver;
427
+ if (e) {
428
+ for (let t of this.observedContent) e.unobserve(t);
429
+ this.observedContent = [...this.noticeContent(), ...this.routeContent()];
430
+ for (let t of this.observedContent) e.observe(t);
431
+ }
432
+ }
433
+ noticeContent() {
434
+ return (this.shadowRoot?.querySelector("slot[name=\"notice\"]"))?.assignedElements({ flatten: !0 }) ?? [];
435
+ }
413
436
  routeContent() {
414
437
  return (this.shadowRoot?.querySelector(".main-content > slot:not([name])"))?.assignedElements({ flatten: !0 }) ?? [];
415
438
  }
416
439
  async focusRoute(e) {
417
440
  let t = this.routeContent();
418
441
  if (await Promise.all(t.map((e) => e.updateComplete)), this.hasOverlay || !e.isConnected) return;
419
- let n = j();
420
- n && t.some((e) => M(e, n)) || this.focusContentStart(t, e);
442
+ let n = A();
443
+ n && t.some((e) => j(e, n)) || this.focusContentStart(t, e);
421
444
  }
422
445
  focusContentStart(e, t) {
423
446
  let n = b(e, "[autofocus]", y);
424
447
  n?.focus();
425
- let r = j();
426
- n && r && M(n, r) || t.focus({ preventScroll: !0 });
448
+ let r = A();
449
+ n && r && j(n, r) || t.focus({ preventScroll: !0 });
427
450
  }
428
451
  overlayPanels() {
429
452
  return (this.shadowRoot?.querySelector("slot[name=\"overlay\"]"))?.assignedElements({ flatten: !0 }) ?? [];
@@ -431,19 +454,19 @@ var N = class extends i {
431
454
  async focusOverlay() {
432
455
  let e = this.overlayPanels();
433
456
  if (await Promise.all(e.map((e) => e.updateComplete)), !this.hasOverlay) return;
434
- let t = j();
435
- t && e.some((e) => M(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();
457
+ let t = A();
458
+ t && e.some((e) => j(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();
436
459
  }
437
460
  restoreOverlayFocus() {
438
461
  let e = this.overlayTrigger;
439
462
  this.overlayTrigger = null;
440
- let t = j();
463
+ let t = A();
441
464
  t && t !== document.body || (e?.isConnected ? e.focus() : this.mainElement?.focus({ preventScroll: !0 }));
442
465
  }
443
466
  };
444
467
  r([p({
445
468
  type: String,
446
469
  reflect: !0
447
- }), n("design:type", Object)], N.prototype, "state", void 0), r([p({ type: Object }), n("design:type", Object)], N.prototype, "config", void 0), r([p({ type: Object }), n("design:type", typeof Record > "u" ? Object : Record)], N.prototype, "counts", void 0), r([p({ type: String }), n("design:type", Object)], N.prototype, "locale", void 0), r([m("u-progress-bar"), n("design:type", S === void 0 ? Object : S)], N.prototype, "progressBarEl", void 0), r([h(), n("design:type", Object)], N.prototype, "hasOverlay", void 0), r([h(), n("design:type", Object)], N.prototype, "openMenu", void 0), r([h(), n("design:type", Object)], N.prototype, "hasNotice", void 0), r([h(), n("design:type", Object)], N.prototype, "routeLoading", void 0), r([h(), n("design:type", Object)], N.prototype, "currentPath", void 0), N = C = r([f("u-sidebar-layout")], N);
470
+ }), n("design:type", Object)], M.prototype, "state", void 0), r([p({ type: Object }), n("design:type", Object)], M.prototype, "config", void 0), r([p({ type: Object }), n("design:type", typeof Record > "u" ? Object : Record)], M.prototype, "counts", void 0), r([p({ type: String }), n("design:type", Object)], M.prototype, "locale", void 0), r([m("u-progress-bar"), n("design:type", Object)], M.prototype, "progressBarEl", void 0), r([h(), n("design:type", Object)], M.prototype, "hasOverlay", void 0), r([h(), n("design:type", Object)], M.prototype, "openMenu", void 0), r([h(), n("design:type", Object)], M.prototype, "hasNotice", void 0), r([h(), n("design:type", Object)], M.prototype, "routeLoading", void 0), r([h(), n("design:type", Object)], M.prototype, "mainTabStop", void 0), r([h(), n("design:type", Object)], M.prototype, "currentPath", void 0), M = S = r([f("u-sidebar-layout")], M);
448
471
  //#endregion
449
- export { N as SidebarLayout };
472
+ export { M 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.42.0",
4
+ "version": "0.42.2",
5
5
  "keywords": [
6
6
  "iyulab",
7
7
  "web-framework",