@iyulab/modern-app 0.42.0 → 0.42.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,17 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.42.1] - 2026-10-07
4
+
5
+ ### Fixed
6
+
7
+ - **A long screen with no controls can be scrolled from the keyboard.** The sidebar layout's content region carries
8
+ `tabindex="-1"` for programmatic focus (skip link, route change) — and a negative tabindex is the explicit opt-out
9
+ from the browser's own rule that makes a scroller with nothing focusable inside a Tab stop (Chrome 130). So a
10
+ keyboard user who tabbed past a document or notice screen had no way to scroll it (axe
11
+ `scrollable-region-focusable`). The region is now a Tab stop exactly when it overflows and holds nothing reachable
12
+ by Tab (shadow roots included), and re-judges when the content's size or slotted elements change; screens with
13
+ controls keep `-1`.
14
+
3
15
  ## [0.42.0] - 2026-10-07
4
16
 
5
17
  ### 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
  /**
@@ -79,7 +79,7 @@ var N = class extends i {
79
79
  C = 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:${++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 = () => {
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");
@@ -110,7 +117,7 @@ var N = class extends i {
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);
@@ -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();
@@ -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
 
@@ -410,6 +417,22 @@ 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
  }
@@ -444,6 +467,6 @@ var N = class extends i {
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)], 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, "mainTabStop", void 0), r([h(), n("design:type", Object)], N.prototype, "currentPath", void 0), N = C = r([f("u-sidebar-layout")], N);
448
471
  //#endregion
449
472
  export { N 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.1",
5
5
  "keywords": [
6
6
  "iyulab",
7
7
  "web-framework",