@wcstack/router 2.0.0 → 2.1.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/dist/index.d.ts CHANGED
@@ -185,10 +185,14 @@ interface IRouter extends IRouteChildContainer {
185
185
  commitNavigation(commit: IRouterCommit): void;
186
186
  /** `announce=` 用 live region。未生成なら null(docs/a11y-design.md §3-4) */
187
187
  readonly a11yRegion: HTMLElement | null;
188
- /** `<wcs-router focus=...>` の属性値(無ければ null) */
189
- readonly focusPolicy: string | null;
190
- /** `<wcs-router announce=...>` の属性値(無ければ null) */
191
- readonly announcePolicy: string | null;
188
+ /**
189
+ * `<wcs-router focus=...>` の正規化済みポリシー。有効値以外(属性なし・空文字・
190
+ * 未知値)はすべて null — 判定箇所ごとに生文字列の解釈が割れないよう、
191
+ * 解釈はこの getter の 1 箇所に閉じる(docs/a11y-design.md §3-5)。
192
+ */
193
+ readonly focusPolicy: "heading" | null;
194
+ /** `<wcs-router announce=...>` の正規化済みポリシー(focusPolicy と同じ規範) */
195
+ readonly announcePolicy: "title" | null;
192
196
  }
193
197
  interface IOutlet {
194
198
  routesNode: IRouter;
@@ -245,8 +249,14 @@ declare class Router extends HTMLElement implements IRouter {
245
249
  constructor();
246
250
  get connectedCallbackPromise(): Promise<void>;
247
251
  get a11yRegion(): HTMLElement | null;
248
- get focusPolicy(): string | null;
249
- get announcePolicy(): string | null;
252
+ /**
253
+ * `focus=` / `announce=` は有効値の union へ正規化して露出する。空文字やタイポは
254
+ * 「ポリシーなし」= null — intercept の focusReset 決定(_onNavigateFunc)と
255
+ * applyA11yPolicies の適用判定が同じ値を見るため、「manual だけ渡して実フォーカス
256
+ * 移動が無い」不整合が構造的に起きない(docs/a11y-design.md §3-5)。
257
+ */
258
+ get focusPolicy(): "heading" | null;
259
+ get announcePolicy(): "title" | null;
250
260
  /**
251
261
  * `announce=` 用 live region を <wcs-router> 直下に空のまま用意する
252
262
  * (docs/a11y-design.md §3-4)。
package/dist/index.esm.js CHANGED
@@ -1262,11 +1262,15 @@ async function parse(routerNode) {
1262
1262
  * - `announce="title"`: commit 時点の document.title のスナップショットを
1263
1263
  * live region へ書き込む(D2)。<wcs-head> の静的 title は mutate() 内で同期に
1264
1264
  * 差し替わるため、ここでは必ず新ルートの値が読める。バインド title の遅延窓・
1265
- * ナビゲーション外の title 変化には追従しない(README の明記された制限)。
1266
- * - `focus="heading"`: リーフ route が挿入した内容の最初の h1〜h6 に
1267
- * tabindex="-1" を付けて focus() する。見出し不在時は何もしない — 旧フォーカス
1268
- * 要素が遷移で消えていればブラウザが body へ落とすため、結果は仕様既定の
1269
- * focusReset と同等に収束する(§3-4 の規定)。
1265
+ * ナビゲーション外の title 変化・同一 title 連続遷移の再読み上げには追従しない
1266
+ * (README の明記された制限)。
1267
+ * - `focus="heading"`: リーフ route が挿入した内容の最初の可視 h1〜h6 に
1268
+ * tabindex="-1" を付けて focus() する。可視見出しが無ければ、仕様既定の
1269
+ * focusReset(after-transition)を自前で再現する — [autofocus] があればそこへ、
1270
+ * 無ければ blur で body へ落とす(§3-4 の規定)。focus= オプトインは Navigation
1271
+ * API 経路で focusReset: "manual" を渡してブラウザ既定を止めているため、旧
1272
+ * フォーカス要素(永続ナビのリンク等)が遷移後も生き残るケースでは、ここで
1273
+ * 落とさない限りフォーカスが前画面に取り残される。
1270
1274
  */
1271
1275
  function applyA11yPolicies(routerNode, matchResult) {
1272
1276
  if (routerNode.announcePolicy === "title") {
@@ -1285,13 +1289,28 @@ function applyA11yPolicies(routerNode, matchResult) {
1285
1289
  }
1286
1290
  heading.focus();
1287
1291
  }
1292
+ else {
1293
+ // 仕様既定の focusReset の再現(優先順も仕様と同じ: [autofocus] → body)。
1294
+ // フォールバック経路(pushState)にはブラウザのリセットが元々無いため、
1295
+ // ここで両経路の挙動が揃う(§3-2 の scroll と同じ構図)。
1296
+ const autofocus = document.querySelector("[autofocus]");
1297
+ if (autofocus !== null) {
1298
+ autofocus.focus();
1299
+ }
1300
+ else if (document.activeElement instanceof HTMLElement) {
1301
+ document.activeElement.blur();
1302
+ }
1303
+ }
1288
1304
  }
1289
1305
  }
1290
1306
  /**
1291
- * リーフ route のトップレベルノード列を document order で走査し、最初の見出しを
1292
- * 返す。祖先 route の内容へは遡らない — 読者が「新しい画面」と認識する単位は
1293
- * リーフである(docs/a11y-design.md §3-4)。ルート内容は Comment placeholder の
1294
- * 兄弟として挿入されるため安定した「箱」が無く、内容から探すのが唯一の現実解。
1307
+ * リーフ route のトップレベルノード列を document order で走査し、最初の可視の
1308
+ * 見出しを返す。祖先 route の内容へは遡らない — 読者が「新しい画面」と認識する
1309
+ * 単位はリーフである(docs/a11y-design.md §3-4)。ルート内容は Comment placeholder
1310
+ * の兄弟として挿入されるため安定した「箱」が無く、内容から探すのが唯一の現実解。
1311
+ *
1312
+ * 非表示(hidden / display:none 等)の見出しへの focus() は no-op になるため、
1313
+ * checkVisibility でスキップする。未実装環境(happy-dom)では可視扱い。
1295
1314
  */
1296
1315
  function findFirstHeading(nodes) {
1297
1316
  for (const node of nodes) {
@@ -1299,15 +1318,22 @@ function findFirstHeading(nodes) {
1299
1318
  continue;
1300
1319
  const element = node;
1301
1320
  if (/^H[1-6]$/.test(element.tagName)) {
1302
- return element;
1321
+ if (isVisible(element)) {
1322
+ return element;
1323
+ }
1324
+ continue;
1303
1325
  }
1304
- const descendant = element.querySelector("h1,h2,h3,h4,h5,h6");
1305
- if (descendant !== null) {
1306
- return descendant;
1326
+ for (const descendant of element.querySelectorAll("h1,h2,h3,h4,h5,h6")) {
1327
+ if (isVisible(descendant)) {
1328
+ return descendant;
1329
+ }
1307
1330
  }
1308
1331
  }
1309
1332
  return null;
1310
1333
  }
1334
+ function isVisible(element) {
1335
+ return element.checkVisibility?.() !== false;
1336
+ }
1311
1337
 
1312
1338
  function testPath(route, path, segments) {
1313
1339
  const params = {};
@@ -2247,11 +2273,17 @@ class Router extends HTMLElement {
2247
2273
  get a11yRegion() {
2248
2274
  return this._a11yRegion;
2249
2275
  }
2276
+ /**
2277
+ * `focus=` / `announce=` は有効値の union へ正規化して露出する。空文字やタイポは
2278
+ * 「ポリシーなし」= null — intercept の focusReset 決定(_onNavigateFunc)と
2279
+ * applyA11yPolicies の適用判定が同じ値を見るため、「manual だけ渡して実フォーカス
2280
+ * 移動が無い」不整合が構造的に起きない(docs/a11y-design.md §3-5)。
2281
+ */
2250
2282
  get focusPolicy() {
2251
- return this.getAttribute('focus');
2283
+ return this.getAttribute('focus') === 'heading' ? 'heading' : null;
2252
2284
  }
2253
2285
  get announcePolicy() {
2254
- return this.getAttribute('announce');
2286
+ return this.getAttribute('announce') === 'title' ? 'title' : null;
2255
2287
  }
2256
2288
  /**
2257
2289
  * `announce=` 用 live region を <wcs-router> 直下に空のまま用意する
@@ -2614,8 +2646,10 @@ class Router extends HTMLElement {
2614
2646
  // 契約の変更にあたる(docs/a11y-design.md §3-1)。same-match の扱いは
2615
2647
  // docs/router-state-contract-design.md §4.4 / D6b。
2616
2648
  scroll: sameMatchScrollManual ? "manual" : "after-transition",
2617
- // focus= 指定時のみ manual。渡さないと router のフォーカス移動とブラウザの
2618
- // after-transition リセットが二重処理になる(docs/a11y-design.md §3-5)。
2649
+ // focus= が有効値のときのみ manual。渡さないと router のフォーカス移動と
2650
+ // ブラウザの after-transition リセットが二重処理になる(docs/a11y-design.md §3-5)。
2651
+ // focusPolicy は正規化済み getter — 空文字・未知値は null になり仕様既定へ
2652
+ // 委譲されるため、「manual だけ渡して何もしない」状態は構造的に生じない。
2619
2653
  // same-match は常に manual — 1 打鍵ごとにフォーカスが body へ飛ぶ事故の防止。
2620
2654
  focusReset: sameMatch || routesNode.focusPolicy !== null ? "manual" : "after-transition",
2621
2655
  });
@@ -3091,10 +3125,12 @@ class Router extends HTMLElement {
3091
3125
  }
3092
3126
  }
3093
3127
 
3094
- // 生成 anchor へミラーする固定属性(docs/a11y-design.md §5)。`aria-*` は開集合なので
3095
- // observedAttributes には載せられず、anchor 生成時の一括コピーのみ(接続後の動的
3096
- // aria-* 変更 — data-wcs バインド経由を含む — には追従しない。README の明記された制限)。
3097
- const MIRRORED_ATTRIBUTES = ['title', 'rel', 'target', 'download', 'hreflang'];
3128
+ // 生成 anchor へミラーする固定属性(docs/a11y-design.md §5)。`lang` / `dir` は SR の
3129
+ // 読み上げ言語・方向に直結するため含める(多言語ナビで anchor 側に届かないと
3130
+ // 読み上げが崩れる)。`aria-*` は開集合なので observedAttributes には載せられず、
3131
+ // anchor 生成時の一括コピーのみ(接続後の動的 aria-* 変更 — data-wcs バインド経由を
3132
+ // 含む — には追従しない。README の明記された制限)。
3133
+ const MIRRORED_ATTRIBUTES = ['title', 'rel', 'target', 'download', 'hreflang', 'lang', 'dir'];
3098
3134
  class Link extends HTMLElement {
3099
3135
  static get observedAttributes() {
3100
3136
  return ['to', ...MIRRORED_ATTRIBUTES];
@@ -3236,7 +3272,7 @@ class Link extends HTMLElement {
3236
3272
  const nextSibling = this.nextSibling;
3237
3273
  link = document.createElement('a');
3238
3274
  this._setAnchorHref(link, this._path);
3239
- // ホスト属性の転送: `aria-*` prefix + 固定 5 名の一括コピー。
3275
+ // ホスト属性の転送: `aria-*` prefix + 固定 7 名の一括コピー。
3240
3276
  // to / style / class は除外 — ホストは display:none であり、class は active 契約を持つ。
3241
3277
  for (const attr of Array.from(this.attributes)) {
3242
3278
  if (attr.name.startsWith('aria-') || MIRRORED_ATTRIBUTES.includes(attr.name)) {
@@ -3642,7 +3678,7 @@ function bootstrapRouter(config, registry) {
3642
3678
  registerComponents(registry);
3643
3679
  }
3644
3680
 
3645
- var version = "2.0.0";
3681
+ var version = "2.1.1";
3646
3682
  var pkg = {
3647
3683
  version: version};
3648
3684