@iyulab/modern-app 0.29.0 → 0.30.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,26 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.30.0] - 2026-09-27
4
+
5
+ ### Added
6
+
7
+ - **`noticesAriaLabel`** in the sidebar layout config. When set, the app-level notice stack
8
+ (`slot="notice"`) is a `role="region"` landmark with that name, so screen-reader users can jump
9
+ back to the notices currently shown. Without it, the only way to name the stack was to wrap the
10
+ notices in your own labelled section — which moved the stack's spacing and full width from each
11
+ notice onto the wrapper. The same shape as `mainAriaLabel`; unset keeps today's unnamed container,
12
+ and an empty stack is not rendered, so no empty landmark appears.
13
+
14
+ ### Fixed
15
+
16
+ - **Escape inside the overlay now closes a drawer or dialog opened in the panel before the
17
+ overlay.** 0.28.0 promised that an Escape consumed inside the panel closes only that layer, but
18
+ that held only for layers listening before the shell: the shell listens on `window` from the
19
+ moment the overlay opens, so a `u-drawer` or `u-dialog` opened afterwards listened later. The
20
+ shell judged first, closed the overlay and marked the key consumed — and the drawer, seeing it
21
+ consumed, stayed open: the outer layer closed and the inner one remained. The shell now decides
22
+ after every layer has, and no longer marks the Escape consumed.
23
+
3
24
  ## [0.29.0] - 2026-09-27
4
25
 
5
26
  ### Fixed
@@ -118,9 +118,17 @@ export declare class SidebarLayout extends StyledElement<SidebarParts> {
118
118
  */
119
119
  private restoreOverlayFocus;
120
120
  /**
121
- * 패널 안의 Escape 는 닫기 버튼과 같은 `overlay-close` 를 낸다.
122
- * window 버블 단계에서 받는다 — 패널 안의 목록·팝오버가 자기 층을 닫으며 먹은 키
123
- * (`defaultPrevented`)는 그 층의 몫이다. 한 번의 Escape 는 한 층만 닫는다.
121
+ * 패널 안의 Escape 는 닫기 버튼과 같은 `overlay-close` 를 낸다 — 단, **안쪽 층이 모두 판정한 뒤에**.
122
+ *
123
+ * 셸의 오버레이는 가장 바깥 층이다. 그런데 window 의 같은 단계 리스너는 «등록 순서» 로 돌고,
124
+ * 셸은 오버레이가 열릴 때 등록하므로 패널 안에서 «나중에» 연 층(`u-drawer`·`u-dialog` — 역시
125
+ * window 에서 받는다)보다 먼저 돈다. 거기서 판정하면 바깥이 닫히고 안쪽이 남는다(거꾸로다).
126
+ * ⇒ 리스너에서는 판정하지 않고 **디스패치가 끝난 뒤**(다음 태스크) `defaultPrevented` 를 본다 —
127
+ * 그때는 어느 순서로 등록된 층이든 이미 자기 몫을 가져갔다. 한 번의 Escape 는 한 층만 닫는다.
128
+ * ⚠마이크로태스크로는 부족하다 — 네이티브 이벤트에서는 리스너 사이마다 마이크로태스크가 비워져,
129
+ * 뒤에 등록된 층보다 먼저 돈다.
130
+ * ⚠셸은 `preventDefault` 하지 않는다 — 가장 바깥 층이 키를 «먹었다» 고 표시하면 안쪽 층의 가드
131
+ * (`defaultPrevented` 면 닫지 않는다)를 오염시킨다.
124
132
  * 경로에 패널이 없으면(사이드바, 패널 위에 띄운 대화상자) 받지 않는다.
125
133
  */
126
134
  private handleOverlayEscape;
@@ -86,9 +86,11 @@ var O = class extends i {
86
86
  }, this.handleNoticeSlotChange = (e) => {
87
87
  this.hasNotice = o(e.target);
88
88
  }, this.handleOverlayEscape = (e) => {
89
- if (e.key !== "Escape" || e.defaultPrevented || e.isComposing || !this.hasOverlay) return;
89
+ if (e.key !== "Escape" || e.isComposing || !this.hasOverlay) return;
90
90
  let t = this.shadowRoot?.querySelector(".overlay");
91
- t && e.composedPath().includes(t) && (e.preventDefault(), this.handleOverlayClose());
91
+ t && e.composedPath().includes(t) && setTimeout(() => {
92
+ !e.defaultPrevented && this.hasOverlay && this.handleOverlayClose();
93
+ }, 0);
92
94
  }, this.handleOverlayClose = () => {
93
95
  this.fire("overlay-close", { cancelable: !1 });
94
96
  }, this._handleMainKeydown = (e) => {
@@ -207,7 +209,9 @@ var O = class extends i {
207
209
  <u-progress-bar part="progress"></u-progress-bar>
208
210
 
209
211
  <div class="main-content" part="main-content" ?inert=${this.hasOverlay}>
210
- <div class="notices ${this.hasNotice ? "" : "empty"}" part="notices">
212
+ <div class="notices ${this.hasNotice ? "" : "empty"}" part="notices"
213
+ role=${this.config.noticesAriaLabel ? "region" : u}
214
+ aria-label=${this.config.noticesAriaLabel ?? u}>
211
215
  <slot name="notice" @slotchange=${this.handleNoticeSlotChange}></slot>
212
216
  </div>
213
217
  <slot></slot>
@@ -78,6 +78,13 @@ export interface SidebarLayoutConfig {
78
78
  * 별개 값이다. 미지정 시 종전과 동일하게 랜드마크 이름이 없다(breaking 아님).
79
79
  */
80
80
  mainAriaLabel?: string;
81
+ /**
82
+ * 앱 수준 공지 스택(`slot="notice"`)의 접근 가능한 이름. 지정하면 스택이 `role="region"` +
83
+ * `aria-label` 인 랜드마크가 되어, 스크린리더 사용자가 «지금 떠 있는 공지들» 로 건너뛰어
84
+ * 다시 읽을 수 있다(`region` 은 이름이 있을 때만 랜드마크다). 공지가 없으면 스택 자체가 없으므로
85
+ * 빈 랜드마크는 생기지 않는다. 미지정 시 종전과 같이 이름 없는 컨테이너다.
86
+ */
87
+ noticesAriaLabel?: string;
81
88
  /**
82
89
  * 메뉴 항목 권한 필터 판정. 지정하면 `requirePermission`/`requireAnyPermission` 를 만족하지
83
90
  * 않는 항목을 숨기고, 항목이 모두 걸러진 section/group 은 통째로 숨긴다.
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.29.0",
4
+ "version": "0.30.0",
5
5
  "keywords": [
6
6
  "iyulab",
7
7
  "web-framework",
@@ -21,6 +21,9 @@ interface SidebarLayoutConfig {
21
21
  /** Accessible name for the main nav landmark (`<nav class="sidebar-main">`), reflected as `aria-label`. Unset by default. */
22
22
  mainAriaLabel?: string;
23
23
 
24
+ /** Accessible name for the app-level notice stack (`slot="notice"`). When set, the stack is a `role="region"` landmark with this `aria-label`. Unset by default. */
25
+ noticesAriaLabel?: string;
26
+
24
27
  /** Permission filter — hides items (and emptied section/groups) whose requirement fails. Unset shows everything. See "권한 기반 메뉴 필터" below. */
25
28
  hasPermission?: (code: string) => boolean;
26
29
 
@@ -396,7 +399,7 @@ from either re-implementing what the shell already does, or dropping what it doe
396
399
  | Keeping the route mounted underneath | shell | the overlay is independent of routing |
397
400
  | Moving focus into the panel when it opens | shell | an `[autofocus]` element in your panel, else its first input control, else `part="overlay-close"`. If you already moved focus into the panel, the shell leaves it there |
398
401
  | 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"` |
399
- | Escape to close | shell | Escape inside the panel fires the same `overlay-close` as the button — you still empty the slot. An Escape already consumed inside the panel (an open list or popover calling `preventDefault()`) closes only that layer |
402
+ | 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 |
400
403
  | **A backdrop / dimmed scrim** | **consumer** | the panel is opaque and full-bleed by design; add a scrim inside your panel if you want one |
401
404
  | **Announcing the panel to assistive tech** | **consumer** | put `role`/`aria-label` (or `aria-modal`, if you have made it modal) on *your* panel — the shell does not know what it holds |
402
405
 
@@ -435,6 +438,10 @@ Put them in `slot="notice"`; the shell stacks them at the top of the route conte
435
438
  - **Full width.** Each notice takes the width of the content box, whatever its text length.
436
439
  - **Empty takes no space** — the stack has no margin while nothing is slotted.
437
440
  - **Inert with the route content** while the overlay is open.
441
+ - **A named landmark on request.** Set `noticesAriaLabel` in the layout config and the stack
442
+ becomes a `role="region"` with that name, so screen-reader users can jump back to the notices
443
+ currently shown. Put the notices in the slot directly — wrapping them in your own named section
444
+ puts the stack's spacing and width on the wrapper instead of on each notice.
438
445
  - Which surface a notice is (toast, banner or modal) is decided by *who ends it* — a notice the time
439
446
  ends is a toast (`app.success`), not a slotted banner.
440
447