@iyulab/modern-app 0.32.0 → 0.33.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 +34 -0
- package/README.md +4 -0
- package/dist/components/SidebarLink.js +14 -12
- package/dist/components/SidebarLink.styles.js +2 -0
- package/dist/internals/locale.d.ts +4 -0
- package/dist/internals/locale.js +6 -0
- package/dist/layouts/SidebarLayout.d.ts +4 -0
- package/dist/layouts/SidebarLayout.js +18 -7
- package/dist/layouts/SidebarLayout.styles.js +29 -0
- package/package.json +3 -3
- package/skills/modern-app/references/layout.md +10 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.33.1] - 2026-09-30
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **Sidebar links with `target: '_blank'` now say so to screen readers** — their accessible name ends
|
|
8
|
+
with a visually hidden "(opens in a new tab)", in the collapsed sidebar too. The text is the new
|
|
9
|
+
`opensInNewTab` string of the `modern-app` locale namespace (English and Korean built in).
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- Optional React peers promise only the tested majors: `@lit/react ^1.0.8` and
|
|
14
|
+
`react ^18.0.0 || ^19.0.0` (were `>=` ranges with no upper bound).
|
|
15
|
+
|
|
16
|
+
### Documentation
|
|
17
|
+
|
|
18
|
+
- README: the Accessibility section links the KWCAG 2.2 table in `@iyulab/components`.
|
|
19
|
+
|
|
20
|
+
## [0.33.0] - 2026-09-30
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- **«Skip to main content» link in the sidebar layout** — the shell's first Tab stop, shown only while
|
|
25
|
+
it has focus (`part="skip-link"`). Activating it moves focus the way a finished route does: to the
|
|
26
|
+
screen's `[autofocus]` element, else to the route area. A keyboard user no longer tabs through the
|
|
27
|
+
whole menu on every screen (WCAG 2.4.1 Bypass Blocks). The text is the new `skipToContent` key of
|
|
28
|
+
`modernAppLocale` (English and Korean built in).
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
|
|
32
|
+
- **The route area (`part="main"`) is a `<main>` element**, so screen readers list it as the main
|
|
33
|
+
landmark. It was a `<div>`. Styling through `::part(main)` and `layout.styles` is unchanged. If your
|
|
34
|
+
route screens render their own `<main>`, change it to a `<div>` or `<section>` — a page should have
|
|
35
|
+
one main landmark.
|
|
36
|
+
|
|
3
37
|
## [0.32.0] - 2026-09-28
|
|
4
38
|
|
|
5
39
|
### Changed
|
package/README.md
CHANGED
|
@@ -276,11 +276,15 @@ conformance claim for the success criteria it does not list.
|
|
|
276
276
|
|---|---|---|
|
|
277
277
|
| SC 2.5.8 Target Size (Minimum) | Every pointer target this package renders itself — sidebar links, buttons and group headers (expanded and compact), the sidebar logo and toggle, wizard steps, the page header's back link — is at least 24×24 CSS px or meets the spacing exception (24px between centers), and is actually hit at that position | `tests/browser/target-size.browser.test.ts` (real Chromium) |
|
|
278
278
|
| SC 2.1.1 Keyboard (pointer-cursor check) | Nothing this package renders shows a pointer cursor without being an interactive element — the sidebar logo is a link | `tests/browser/target-size.browser.test.ts` |
|
|
279
|
+
| SC 2.4.1 Bypass Blocks | The sidebar layout's first Tab stop is a «Skip to main content» link, visible when focused and at least 24px tall; it moves focus to the screen's `[autofocus]` element or to the route area, which is the page's single `<main>` landmark | `tests/browser/sidebar-layout-skip-link.browser.test.ts` |
|
|
280
|
+
| SC 3.2.5 Change on Request (new tabs) | A sidebar link with `target: '_blank'` ends its accessible name with a localized, visually hidden "(opens in a new tab)" — expanded and collapsed | `tests/browser/sidebar-link-new-tab-name.browser.test.ts` |
|
|
279
281
|
|
|
280
282
|
Buttons placed through slots or rendered as `u-button` without size overrides (wizard back/next, the
|
|
281
283
|
master-detail close button) follow `@iyulab/components`, which measures them in its own gate. Color
|
|
282
284
|
contrast likewise comes from that package's tokens.
|
|
283
285
|
|
|
286
|
+
For **KWCAG 2.2** (the Korean web accessibility standard), the `@iyulab/components` README has a table of all 33 check items — which are guaranteed by a test across the sibling packages, which are shared with the app, and which do not apply: [KWCAG 2.2 대응표](https://github.com/iyulab/node-components#kwcag-22-대응표).
|
|
287
|
+
|
|
284
288
|
## Documentation
|
|
285
289
|
|
|
286
290
|
| Guide | Description |
|
|
@@ -1,45 +1,47 @@
|
|
|
1
1
|
import e from "../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorateMetadata.js";
|
|
2
2
|
import t from "../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorate.js";
|
|
3
3
|
import { StyledElement as n } from "../internals/StyledElement.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { getLocaleStrings as r } from "../internals/locale.js";
|
|
5
|
+
import { DEFAULT_NAV_ICON as i } from "../internals/nav-icon.js";
|
|
6
|
+
import { styles as a } from "./SidebarLink.styles.js";
|
|
6
7
|
import "@iyulab/router";
|
|
7
|
-
import { html as
|
|
8
|
-
import { customElement as
|
|
8
|
+
import { html as o, nothing as s } from "lit";
|
|
9
|
+
import { customElement as c, property as l } from "lit/decorators.js";
|
|
9
10
|
import "@iyulab/components/dist/components/icon/UIcon.js";
|
|
10
11
|
//#region src/components/SidebarLink.ts
|
|
11
|
-
var
|
|
12
|
+
var u = class extends n {
|
|
12
13
|
constructor(...e) {
|
|
13
14
|
super(...e), this.selected = !1, this.compact = !1;
|
|
14
15
|
}
|
|
15
16
|
static {
|
|
16
|
-
this.styles = [super.styles,
|
|
17
|
+
this.styles = [super.styles, a];
|
|
17
18
|
}
|
|
18
19
|
render() {
|
|
19
|
-
return
|
|
20
|
+
return o`
|
|
20
21
|
<u-link
|
|
21
22
|
.href=${this.href || "#"}
|
|
22
23
|
.navigate=${this.navigate}
|
|
23
24
|
.target=${this.target}
|
|
24
|
-
aria-current=${this.selected ? "page" :
|
|
25
|
+
aria-current=${this.selected ? "page" : s}
|
|
25
26
|
>
|
|
26
27
|
<div class="container" part="base" ?compact=${this.compact}>
|
|
27
28
|
<u-icon part="icon"
|
|
28
29
|
.lib=${this.lib}
|
|
29
30
|
.name=${this.icon}
|
|
30
|
-
.fallback=${
|
|
31
|
+
.fallback=${i}
|
|
31
32
|
></u-icon>
|
|
32
33
|
<span part="label" ?compact=${this.compact}>
|
|
33
34
|
${this.label}
|
|
34
35
|
</span>
|
|
36
|
+
${this.target === "_blank" ? o`<span class="new-tab-hint">${r().opensInNewTab}</span>` : s}
|
|
35
37
|
</div>
|
|
36
38
|
</u-link>
|
|
37
39
|
`;
|
|
38
40
|
}
|
|
39
41
|
};
|
|
40
|
-
t([
|
|
42
|
+
t([l({
|
|
41
43
|
type: Boolean,
|
|
42
44
|
reflect: !0
|
|
43
|
-
}), e("design:type", Object)],
|
|
45
|
+
}), e("design:type", Object)], u.prototype, "selected", void 0), t([l({ type: Boolean }), e("design:type", Object)], u.prototype, "compact", void 0), t([l({ type: String }), e("design:type", String)], u.prototype, "icon", void 0), t([l({ type: String }), e("design:type", String)], u.prototype, "lib", void 0), t([l({ type: String }), e("design:type", Object)], u.prototype, "label", void 0), t([l({ type: String }), e("design:type", String)], u.prototype, "href", void 0), t([l({ type: String }), e("design:type", Object)], u.prototype, "pattern", void 0), t([l({ type: String }), e("design:type", String)], u.prototype, "navigate", void 0), t([l({ type: String }), e("design:type", String)], u.prototype, "target", void 0), u = t([c("u-sidebar-link")], u);
|
|
44
46
|
//#endregion
|
|
45
|
-
export {
|
|
47
|
+
export { u as SidebarLink };
|
|
@@ -74,6 +74,8 @@ var t = e`
|
|
|
74
74
|
* 접힌 상태의 라벨 - 시각적으로만 숨긴다. 접근성 트리에는 남아 항목의 이름이 된다
|
|
75
75
|
* (hidden 은 트리에서도 빼서 이름 없는 항목을 만들었다). 절대배치라 flex 배치와 gap 에 끼지 않는다.
|
|
76
76
|
*/
|
|
77
|
+
/* 새 창 알림 — 화면에는 없고 링크의 접근성 이름에만 붙는다(KWCAG 7.2.1). */
|
|
78
|
+
.new-tab-hint,
|
|
77
79
|
[part~='label'][compact] {
|
|
78
80
|
position: absolute;
|
|
79
81
|
width: 1px;
|
|
@@ -13,6 +13,10 @@ export interface ModernAppLocaleStrings {
|
|
|
13
13
|
toggleMobileMenu: string;
|
|
14
14
|
/** Sidebar layout — accessible label for the sidebar-header collapse/expand toggle button. */
|
|
15
15
|
toggleSidebar: string;
|
|
16
|
+
/** Sidebar layout — the «skip to main content» link, the first Tab stop of the shell. */
|
|
17
|
+
skipToContent: string;
|
|
18
|
+
/** Sidebar links with `target: '_blank'` — appended to the link's accessible name (visually hidden). */
|
|
19
|
+
opensInNewTab: string;
|
|
16
20
|
/** Wizard — default Back/Next action labels ("Submit" wording is the consumer's call). */
|
|
17
21
|
wizardBack: string;
|
|
18
22
|
wizardNext: string;
|
package/dist/internals/locale.js
CHANGED
|
@@ -9,6 +9,8 @@ var t = {
|
|
|
9
9
|
detailClose: "Close",
|
|
10
10
|
toggleMobileMenu: "Toggle menu",
|
|
11
11
|
toggleSidebar: "Toggle sidebar",
|
|
12
|
+
skipToContent: "Skip to main content",
|
|
13
|
+
opensInNewTab: "(opens in a new tab)",
|
|
12
14
|
wizardBack: "Back",
|
|
13
15
|
wizardNext: "Next",
|
|
14
16
|
wizardStepAnnouncement: "Step {index} of {total}: {label}"
|
|
@@ -22,6 +24,8 @@ n.register("en", t), n.register("ko", {
|
|
|
22
24
|
detailClose: "닫기",
|
|
23
25
|
toggleMobileMenu: "메뉴 열고 닫기",
|
|
24
26
|
toggleSidebar: "사이드바 열고 닫기",
|
|
27
|
+
skipToContent: "본문으로 건너뛰기",
|
|
28
|
+
opensInNewTab: "(새 창에서 열림)",
|
|
25
29
|
wizardBack: "이전",
|
|
26
30
|
wizardNext: "다음",
|
|
27
31
|
wizardStepAnnouncement: "{total}단계 중 {index}단계: {label}"
|
|
@@ -53,6 +57,8 @@ function l(e) {
|
|
|
53
57
|
detailClose: t("detailClose"),
|
|
54
58
|
toggleMobileMenu: t("toggleMobileMenu"),
|
|
55
59
|
toggleSidebar: t("toggleSidebar"),
|
|
60
|
+
skipToContent: t("skipToContent"),
|
|
61
|
+
opensInNewTab: t("opensInNewTab"),
|
|
56
62
|
wizardBack: t("wizardBack"),
|
|
57
63
|
wizardNext: t("wizardNext"),
|
|
58
64
|
wizardStepAnnouncement: (t, n, r) => {
|
|
@@ -111,6 +111,10 @@ export declare class SidebarLayout extends StyledElement<SidebarParts> {
|
|
|
111
111
|
* 그리기 전에 올 수 있고, 그때 찾으면 `[autofocus]` 가 아직 없다.
|
|
112
112
|
*/
|
|
113
113
|
private focusRoute;
|
|
114
|
+
/** 본문의 시작으로 포커스 — 화면이 선언한 `[autofocus]`, 없으면 본문(`main`) 자체. */
|
|
115
|
+
private focusContentStart;
|
|
116
|
+
/** «본문 바로가기» — 라우트 완료와 같은 규칙으로 본문의 시작에 포커스를 둔다(`#517`). */
|
|
117
|
+
private handleSkipLinkClick;
|
|
114
118
|
/** 라우트 에러 핸들러 */
|
|
115
119
|
private handleRouteError;
|
|
116
120
|
private handleOverlaySlotChange;
|
|
@@ -74,6 +74,10 @@ var j = class extends i {
|
|
|
74
74
|
}, 300);
|
|
75
75
|
let t = this.shadowRoot?.querySelector(".main");
|
|
76
76
|
t && (this.config?.scrollBehavior?.(e.context, t), this.focusRoute(t));
|
|
77
|
+
}, this.handleSkipLinkClick = (e) => {
|
|
78
|
+
e.preventDefault();
|
|
79
|
+
let t = this.shadowRoot?.querySelector(".main");
|
|
80
|
+
t && this.focusContentStart(this.hasOverlay ? [] : this.routeContent(), t);
|
|
77
81
|
}, this.handleRouteError = (e) => {
|
|
78
82
|
this.progressBarEl.status = "error", this.progressBarEl.value = 100, setTimeout(() => {
|
|
79
83
|
this.progressBarEl.removeAttribute("visible"), this.progressBarEl.status = "default";
|
|
@@ -156,6 +160,11 @@ var j = class extends i {
|
|
|
156
160
|
}
|
|
157
161
|
render() {
|
|
158
162
|
return this.config ? l`
|
|
163
|
+
<!-- 반복 영역 건너뛰기(WCAG 2.4.1) — 셸의 첫 Tab. 포커스될 때만 보인다. -->
|
|
164
|
+
<a class="skip-link" part="skip-link" href="#" @click=${this.handleSkipLinkClick}>
|
|
165
|
+
${a(this.locale || void 0).skipToContent}
|
|
166
|
+
</a>
|
|
167
|
+
|
|
159
168
|
<!-- Mobile Header -->
|
|
160
169
|
<div class="mobile-header" part="mobile-header" ?hidden="${!this.state.startsWith("mobile")}">
|
|
161
170
|
${this.renderLogo()}
|
|
@@ -204,7 +213,7 @@ var j = class extends i {
|
|
|
204
213
|
|
|
205
214
|
<!-- Main Content -->
|
|
206
215
|
<div class="main-region">
|
|
207
|
-
<
|
|
216
|
+
<main class="main" part="main" scrollable tabindex="-1" @keydown=${this._handleMainKeydown}>
|
|
208
217
|
<u-progress-bar part="progress"></u-progress-bar>
|
|
209
218
|
|
|
210
219
|
<div class="main-content" part="main-content" ?inert=${this.hasOverlay}>
|
|
@@ -215,7 +224,7 @@ var j = class extends i {
|
|
|
215
224
|
</div>
|
|
216
225
|
<slot></slot>
|
|
217
226
|
</div>
|
|
218
|
-
</
|
|
227
|
+
</main>
|
|
219
228
|
|
|
220
229
|
<div class="overlay ${this.hasOverlay ? "" : "empty"}" part="overlay">
|
|
221
230
|
<u-button class="overlay-close" part="overlay-close" variant="ghost"
|
|
@@ -341,11 +350,13 @@ var j = class extends i {
|
|
|
341
350
|
let t = this.routeContent();
|
|
342
351
|
if (await Promise.all(t.map((e) => e.updateComplete)), this.hasOverlay || !e.isConnected) return;
|
|
343
352
|
let n = k();
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
let
|
|
348
|
-
|
|
353
|
+
n && t.some((e) => A(e, n)) || this.focusContentStart(t, e);
|
|
354
|
+
}
|
|
355
|
+
focusContentStart(e, t) {
|
|
356
|
+
let n = b(e, "[autofocus]", y);
|
|
357
|
+
n?.focus();
|
|
358
|
+
let r = k();
|
|
359
|
+
n && r && A(n, r) || t.focus({ preventScroll: !0 });
|
|
349
360
|
}
|
|
350
361
|
overlayPanels() {
|
|
351
362
|
return (this.shadowRoot?.querySelector("slot[name=\"overlay\"]"))?.assignedElements({ flatten: !0 }) ?? [];
|
|
@@ -72,6 +72,35 @@ var t = e`
|
|
|
72
72
|
color: var(--u-txt-color-hover, #1565C0);
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
/* 본문 바로가기 — 포커스될 때만 보인다(보이지 않을 때는 자리도 차지하지 않는다). */
|
|
76
|
+
.skip-link {
|
|
77
|
+
position: absolute;
|
|
78
|
+
top: 8px;
|
|
79
|
+
left: 8px;
|
|
80
|
+
z-index: calc(var(--u-layer-shell, 1000) + 2);
|
|
81
|
+
width: 1px;
|
|
82
|
+
height: 1px;
|
|
83
|
+
overflow: hidden;
|
|
84
|
+
clip-path: inset(50%);
|
|
85
|
+
white-space: nowrap;
|
|
86
|
+
}
|
|
87
|
+
.skip-link:focus {
|
|
88
|
+
width: auto;
|
|
89
|
+
height: auto;
|
|
90
|
+
min-height: 24px;
|
|
91
|
+
overflow: visible;
|
|
92
|
+
clip-path: none;
|
|
93
|
+
display: inline-flex;
|
|
94
|
+
align-items: center;
|
|
95
|
+
padding: 6px 12px;
|
|
96
|
+
border-radius: var(--u-radius-md, 4px);
|
|
97
|
+
background: var(--u-bg-color, #FFFFFF);
|
|
98
|
+
color: var(--u-txt-color, #212121);
|
|
99
|
+
box-shadow: 0 0 0 2px var(--u-primary-color, #1976D2);
|
|
100
|
+
text-decoration: none;
|
|
101
|
+
outline: none;
|
|
102
|
+
}
|
|
103
|
+
|
|
75
104
|
/* Sidebar Container */
|
|
76
105
|
.sidebar {
|
|
77
106
|
position: relative;
|
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.33.1",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"iyulab",
|
|
7
7
|
"web-framework",
|
|
@@ -65,8 +65,8 @@
|
|
|
65
65
|
},
|
|
66
66
|
"peerDependencies": {
|
|
67
67
|
"@iyulab/components": ">=1.49.0",
|
|
68
|
-
"@lit/react": "
|
|
69
|
-
"react": "
|
|
68
|
+
"@lit/react": "^1.0.8",
|
|
69
|
+
"react": "^18.0.0 || ^19.0.0"
|
|
70
70
|
},
|
|
71
71
|
"peerDependenciesMeta": {
|
|
72
72
|
"@lit/react": {
|
|
@@ -257,6 +257,7 @@ Parts available for `styles` overrides on the root layout:
|
|
|
257
257
|
| Part | Element |
|
|
258
258
|
|------|---------|
|
|
259
259
|
| `host` | Outer layout shell |
|
|
260
|
+
| `skip-link` | «Skip to main content» link — the shell's first Tab stop, shown only while focused |
|
|
260
261
|
| `mobile-header` | Top bar shown on mobile |
|
|
261
262
|
| `sidebar` | Sidebar panel |
|
|
262
263
|
| `sidebar-header` | Logo + title area |
|
|
@@ -333,6 +334,15 @@ told apart in CSS; notices are rare and short-lived.
|
|
|
333
334
|
While the overlay is open, `part="main-content"` becomes a box at the area's height, because a
|
|
334
335
|
stacking context needs one — route content is inert and under the overlay then.
|
|
335
336
|
|
|
337
|
+
### Skip link and main landmark
|
|
338
|
+
|
|
339
|
+
The route area is a `<main>` landmark (`part="main"`), so a screen reader can jump to it. The shell's
|
|
340
|
+
first Tab stop is a **«Skip to main content»** link (`part="skip-link"`, localized — `skipToContent` in
|
|
341
|
+
`modernAppLocale`), visible only while it has focus. Activating it moves focus by the same rule as a
|
|
342
|
+
finished route (steps 2 and 3 below): your screen's `[autofocus]` element, else `main` itself — so a
|
|
343
|
+
keyboard user does not tab through the whole menu on every screen (WCAG 2.4.1 Bypass Blocks). Do not put
|
|
344
|
+
another `<main>` in your route screens; the shell already provides it.
|
|
345
|
+
|
|
336
346
|
### Focus when a route finishes
|
|
337
347
|
|
|
338
348
|
On every `route-done` the shell places focus so the keyboard works on the new screen without a
|