Skip to content

Make overlays, status messages and the wheel reachable by screen readers - #87

Merged
min0625 merged 1 commit into
mainfrom
a11y-dialogs-and-live-region
Jul 28, 2026
Merged

min0625 merged 1 commit into
mainfrom
a11y-dialogs-and-live-region

Conversation

@min0625

@min0625 min0625 commented Jul 28, 2026

Copy link
Copy Markdown
Owner

Screen-reader and reduced-motion pass over the existing UI, plus a few layout fixes the pass turned up. No gameplay, economy or level-data changes.

Dialogs

  • #overlay-clear and #overlay-settings cards get role="dialog" aria-modal="true" aria-labelledby tabindex="-1" and receive focus on open — focus lands on the card, not the first button, so VoiceOver reads "過關! +10 金幣" / "設定" instead of "下一關,按鈕" / "關閉,按鈕".
  • Settings gains a visible ✕ (the tap-outside-to-close path is unreachable with VoiceOver swipe navigation) and Escape. All three close paths share closeSettings(), which hands focus back to the gear button.
  • No JS focus trap: aria-modal already is one for touch AT, and the only way out is desktop Tab, which in this project only serves dev iteration (UI doc §4-E).
  • The dictionary card is reparented into whichever dialog is currently modal when it opens. Left as a body child it is outside the modal subtree, so the level-clear card's word chips — the card's main entry point — would open something a screen-reader user can never reach. position: fixed ignores DOM position, so nothing moves visually.

Status announcements

  • #preview loses aria-live: showPreview rewrites it on every letter, which announced C→CA→CAT for the whole drag.
  • Result messages now also go to a new .sr-only #status (role="status"), parked outside every screen so it never leaves the a11y tree. Both are cleared after 900ms so a repeated message announces again.
  • Invalid words announce notAWord ("{字} 不是單字") rather than the bare word — the "invalid" part lives entirely in the shake animation, and a correct hit speaks the same word through TTS.

Wheel

  • Letters become <div>s. They are pointer-events: none and hit-testing is coordinate-based, so they never received their own click: button semantics promised "double-tap to activate" for something that does nothing, and put every letter in the tab order. A div still reads out the letter.
  • shuffle / pronounce {word} aria-labels move into strings.js — they were the only English strings a Chinese-language screen reader would hit.
  • #redeem-input gets a real aria-label (a placeholder stops being the accessible name once you start typing).

Reduced motion

Two hand-maintained selector lists become one global rule at 0.01ms. animation: none suppresses animationend, and both grid.js and main.js's new playOnce() remove their animation class on that event — the list version left those classes stuck forever. It also removes the "remember to add your new component to the list" failure mode, which fails silently on the users who need it most.

Layout fixes found along the way

  • .card scrolls when taller than the viewport. Expanding About (six lines of license text) on a small screen used to be clipped by align-items: center, which pushed the scrim off-screen too and effectively killed tap-outside-to-close.
  • The hint button moves from the wheel's horizontal midline (the widest, tightest line on the page — 4px/2.5px of clearance at 390px, overlapping the circle at 320px) to its bottom-left corner.
  • The rotate mask now also requires pointer: coarse: a short desktop window matched landscape + max-height: 500px and covered the whole game with "please rotate", which a desktop user cannot do.

bun run check passes.

Screen-reader and reduced-motion pass over the existing UI, plus a few
layout fixes the pass turned up.

- Both overlay cards become real dialogs (role/aria-modal/aria-labelledby
  /tabindex="-1") and take focus on open, so VoiceOver announces what was
  opened instead of leaving the user swiping through the board behind the
  scrim. Settings gains a visible close button and an Escape path, both
  routed through one closeSettings() that hands focus back to the gear.
- The dictionary card is moved into whichever dialog is currently modal
  when it pops up; left outside, aria-modal hides it from assistive tech
  and the level-clear card's word chips would open something unreachable.
- #preview stops being an aria-live region (it rewrites once per letter
  during a drag) and result messages go to a new sr-only #status instead.
  Invalid words announce "not a word" rather than the bare letters, which
  would otherwise read the same as a correct hit.
- Wheel letters become divs: they are pointer-events: none and never got
  their own click, so button semantics promised an interaction that does
  not exist and put every letter in the tab order.
- Reduced-motion switches from two hand-maintained selector lists to a
  global 0.01ms rule, so animationend still fires and the classes that
  playOnce()/grid.js remove on it do not get stuck.
- Cards scroll when taller than the viewport (expanded About used to be
  clipped, taking the tap-to-dismiss scrim with it); the hint button moves
  to the wheel's bottom-left corner where there is actual room; the rotate
  mask now requires pointer: coarse so a short desktop window is not told
  to rotate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Improve screen-reader and reduced-motion accessibility for overlays and wheel

✨ Enhancement 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Make level-clear/settings overlays real modal dialogs with focus management
• Add non-visual status live region and fix noisy/incorrect announcements
• Simplify reduced-motion handling and fix layout/scroll edge cases
Diagram

graph TD
  A["index.html"] --> B["src/main.js"] --> C["Modal overlays"] --> D["src/style.css"]
  B --> E["#status live region"] --> D
  B --> F["src/dictionary-card.js"] --> C
  B --> G["src/wheel.js"] --> H["src/strings.js"]
  F --> H
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Use native + showModal()
  • ➕ Built-in modal semantics and focus management in supporting browsers
  • ➕ Less custom code for focus/keyboard behavior
  • ➖ Styling/stacking behavior can be tricky vs existing overlay DOM/CSS
  • ➖ Inconsistent behavior/quirks across browsers; may require polyfills anyway
2. Add `inert` (or a focus-trap) for full keyboard containment
  • ➕ Hard-prevents Tab focus from escaping the dialog subtree
  • ➕ Improves robustness for desktop keyboard navigation
  • ➖ More code/complexity for a project that intentionally de-emphasizes keyboard play
  • ➖ Potential compatibility/polyfill concerns and more interaction edge cases
3. Make wheel accessible via an alternate non-gesture control
  • ➕ Would enable actual non-visual gameplay (not just truthful semantics)
  • ➕ Could be implemented as an ordered letter list + action buttons
  • ➖ Significant product/design scope increase beyond this PR
  • ➖ Requires new interaction model and likely additional instructions/tutorial changes

Recommendation: Keep the PR’s approach (ARIA modal dialogs + explicit focus placement + separate status live region + truthful wheel semantics). It materially improves screen-reader usability without expanding scope into full keyboard/AT gameplay. If desktop keyboard support becomes a goal later, add inert/focus-trap then; for now aria-modal plus focus management is a reasonable tradeoff.

Files changed (10) +203 / -88

Enhancement (5) +167 / -69
index.htmlMake overlays modal dialogs and add screen-reader-only status region +25/-3

Make overlays modal dialogs and add screen-reader-only status region

• Removes aria-live from #preview, adds role=dialog/aria-modal/aria-labelledby/tabindex to overlay cards, introduces a visible settings close button, and adds a persistent #status (role=status) live region outside screen containers.

index.html

dictionary-card.jsLocalize speak button label and reparent card into active modal subtree +7/-1

Localize speak button label and reparent card into active modal subtree

• Moves the speak button’s aria-label to strings.js and appends the dictionary card under the closest aria-modal dialog (or body) so assistive tech can reach it when a modal is open.

src/dictionary-card.js

main.jsAdd dialog focus management, unified settings close, and #status announcements +54/-20

Add dialog focus management, unified settings close, and #status announcements

• Introduces playOnce() to reliably replay animations and clean up classes (including under reduced motion), adds aria-label to redeem input, routes flashPreview messages into the new #status live region (with specialized invalid-word phrasing), focuses dialog cards on open, and implements closeSettings() with focus restoration plus Escape handling.

src/main.js

strings.jsAdd localized accessibility strings for announcements and icon buttons +7/-0

Add localized accessibility strings for announcements and icon buttons

• Adds notAWord() for screen-reader-only invalid announcements and provides shuffleLabel/speakLabel so icon-only controls don’t introduce English strings into a Chinese screen-reader experience.

src/strings.js

style.cssAdd dialog scrolling, sr-only utility, close icon, and global reduced-motion +74/-45

Add dialog scrolling, sr-only utility, close icon, and global reduced-motion

• Adds close icon styling and card close button layout, constrains .card height with internal scrolling (and overscroll containment), introduces .sr-only for live regions, refines rotate-mask media query with pointer: coarse, and replaces selector lists with a global reduced-motion duration clamp that preserves animationend behavior.

src/style.css

Bug fix (1) +7 / -2
wheel.jsMake wheel letters non-interactive elements and localize shuffle label +7/-2

Make wheel letters non-interactive elements and localize shuffle label

• Changes letter nodes from buttons to divs to avoid misleading activation semantics and tab stops, and moves the shuffle button aria-label to strings.shuffleLabel.

src/wheel.js

Documentation (4) +29 / -17
CLAUDE.mdClarify wheel letter semantics and grid aria-label contract +1/-1

Clarify wheel letter semantics and grid aria-label contract

• Updates internal architecture notes to reflect wheel letters being non-buttons and explains why grid cells must keep role+aria-label for meaningful screen-reader output.

CLAUDE.md

word-puzzle-game-design.mdDocument pointer-coarse rotate mask and global reduced-motion rule +8/-4

Document pointer-coarse rotate mask and global reduced-motion rule

• Expands design notes for iOS embedded WebView scroll prevention (adds .card as scrollable allowlist), refines landscape rotate-mask condition to avoid desktop false positives, and documents the global reduced-motion duration clamp rationale.

docs/word-puzzle-game-design.md

word-puzzle-ui-screens.mdUpdate UI spec for dialogs, status announcements, and wheel semantics +17/-10

Update UI spec for dialogs, status announcements, and wheel semantics

• Documents dialog ARIA/focus behavior for clear/settings overlays, adds guidance for #status live region and preview non-live behavior, explains dict-card reparenting into modal subtree, and records wheel letters as non-interactive divs with localized aria-labels.

docs/word-puzzle-ui-screens.md

grid.jsRefine accessibility commentary around role/aria-label usage +3/-2

Refine accessibility commentary around role/aria-label usage

• Clarifies why grid cell role=button must be paired with aria-label and contrasts the behavior with wheel letters now being non-button elements.

src/grid.js

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (2) 📜 Skill insights (0)

Context used
✅ Compliance rules (platform): 79 rules
✅ Skills: frontend-design

Grey Divider


Remediation recommended

1. strings.js imported outside main.js 📘 Rule violation ⌂ Architecture
Description
src/dictionary-card.js and src/wheel.js now import ./strings.js, creating inter-module imports
between src/ game modules outside the allowed src/main.js coordinator. This violates the
module-boundary rule and makes cross-module coupling harder to control.
Code

src/dictionary-card.js[2]

+import { strings } from './strings.js';
Evidence
The checklist forbids inter-module imports between src/ modules except from src/main.js. The PR
adds direct imports of ./strings.js in src/dictionary-card.js and src/wheel.js.

Rule 2064841: Disallow inter-module imports between game modules in src/ (except main.js as central coordinator)
src/dictionary-card.js[2-2]
src/wheel.js[3-3]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Non-`main.js` modules under `src/` are importing another `src/` module (`./strings.js`), which violates the rule that only `src/main.js` may wire modules together.

## Issue Context
New imports were added to support localized `aria-label`s, but they introduce prohibited inter-module dependencies.

## Fix Focus Areas
- src/dictionary-card.js[1-5]
- src/wheel.js[1-5]
- src/main.js[1-120]
- src/strings.js[1-80]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Non-English comments added 📘 Rule violation ⚙ Maintainability
Description
Multiple added/modified comments contain non-English natural language (e.g., Chinese) in source
files. This violates the requirement that source code comments be written in English, reducing
maintainability for English-speaking reviewers/auditors.
Code

index.html[R91-93]

+    <!-- 拼字串顯示區刻意**不是** aria-live 區塊:showPreview 每滑過一個字母就重寫一次,
+         掛上去等於在整段拖曳期間逐字播報 C→CA→CAT。真正該播的只有 flashPreview 那幾則結果
+         訊息,那些走下面的 #status(UI 文件 §4-F 第三段)-->
Evidence
The checklist requires that all added/modified source code comments be written in English. The PR
introduces/changes comment blocks in Chinese in index.html, src/main.js, and src/strings.js.

Rule 2366288: Source code comments must be written in English
index.html[91-93]
src/main.js[15-23]
src/strings.js[13-15]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
New/modified comments in source files are written in a non-English natural language, which violates the English-only comment requirement.

## Issue Context
This PR adds extensive explanatory comments for accessibility behaviors; they should be translated to English (or reduced/removed if redundant).

## Fix Focus Areas
- index.html[91-93]
- src/main.js[15-23]
- src/strings.js[13-15]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. playOnce listener buildup 🐞 Bug ☼ Reliability
Description
playOnce() adds a fresh anonymous "animationend" listener every call; when the animation is
restarted before it ends, the previous run is canceled (no animationend), leaving stale listeners
that can accumulate during rapid repeated interactions. This can cause unbounded temporary listener
growth and multiple redundant callbacks firing on the next completed animation.
Code

src/main.js[R18-23]

+function playOnce(el, cls) {
+  el.classList.remove(cls);
+  void el.offsetWidth;
+  el.classList.add(cls);
+  el.addEventListener('animationend', () => el.classList.remove(cls), { once: true });
+}
Evidence
playOnce() always restarts the animation by removing and re-adding the class and attaches an
animationend listener each time; it’s used on the hint button click path. The shake animation is
0.35s, making it easy for repeated clicks to restart/cancel an in-flight animation before its end
event, which prevents the { once: true } listener from self-removing.

src/main.js[15-23]
src/main.js[438-446]
src/style.css[313-319]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`playOnce(el, cls)` attaches a new anonymous `animationend` listener on every invocation. If the animation is restarted before it finishes (class removed/re-added), the previous animation run can be canceled without emitting `animationend`, so the `{ once: true }` listener never self-removes. Rapid repeated calls can therefore accumulate stale listeners until a later animation finally completes.

### Issue Context
This helper is used on user-click paths (e.g. hint/claim shake). The `shake` animation duration is 0.35s, so rapid clicking can easily restart the animation before the prior run ends.

### Fix Focus Areas
- src/main.js[15-23]
- src/main.js[439-446]
- src/style.css[313-319]

### Suggested fix
- Use a stable handler (not an inline anonymous closure) and register it for both `animationend` *and* `animationcancel` with `{ once: true }`, so canceled runs don’t leave listeners behind.
- Optionally guard against bubbled events by checking `e.target === el` and, if relevant, `e.animationName`.
- Alternative robust approach: keep a per-element `AbortController`/listener cleanup in a `WeakMap` so each new `playOnce()` aborts prior listeners before starting a new run.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (1)
4. Focus returns to hidden 🐞 Bug ≡ Correctness
Description
closeSettings() always focuses #btn-settings, but #btn-settings exists only inside #screen-game,
which may be hidden if the user keyboard-navigates to and activates #btn-level behind the untrapped
modal. Closing settings after switching screens can leave focus on a hidden element or fail to
restore focus to any visible control.
Code

src/main.js[R592-598]

+// 三條關閉路徑(點卡片外、右上角 ✕、Escape)共用這支,才不會有一條忘了把焦點還回去。
+function closeSettings() {
+  $('overlay-settings').hidden = true;
+  // 焦點交還給開卡的那顆齒輪:不還的話焦點落回 body,讀螢幕的人得從畫面最上面重新滑一次
+  // 才找得回自己剛才在哪裡。
+  $('btn-settings').focus();
+}
Evidence
closeSettings() forces focus back to #btn-settings, which is nested under #screen-game. The code
allows screen switching via #btn-level and showScreen() hides #screen-game. The HTML comment
states there is no focus trap, so keyboard focus can escape the dialog to reach underlying controls
and change screens before closing the overlay.

src/main.js[593-598]
index.html[81-88]
src/main.js[565-572]
src/main.js[196-209]
index.html[140-148]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`closeSettings()` unconditionally focuses `#btn-settings`, but that button lives inside `#screen-game`. Because there is intentionally no JS focus trap for the settings dialog, keyboard users can Tab to underlying controls (like `#btn-level`) and switch to another screen while the settings overlay remains open. When the overlay is closed, focusing a control in a hidden section can strand focus.

### Issue Context
- Settings dialog explicitly does not implement a focus trap.
- `showLevels()` calls `showScreen('levels')`, which hides `#screen-game`.

### Fix Focus Areas
- src/main.js[592-607]
- src/main.js[565-572]
- src/main.js[196-209]
- index.html[81-88]
- index.html[140-148]

### Suggested fix
Pick one (or combine):
1) **Safer focus restore:** store the opener element when opening settings (e.g., `lastSettingsOpener = document.activeElement` or explicitly the gear button), and in `closeSettings()` only focus it if it’s still connected and visible (not in a hidden screen). Otherwise, focus a visible fallback on the current screen (e.g., first focusable in the visible section).
2) **Prevent background activation:** add a minimal keyboard focus trap or make background inert while the modal is open (even if you keep no trap for touch AT).
3) **Close-on-screen-change:** when `showScreen(name !== 'game')`, also close `overlay-settings` (and pick an appropriate focus target for that screen).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

Comment thread src/dictionary-card.js
@@ -1,4 +1,5 @@
// 查詞卡片(設計文件 §6):釋義 + 喇叭發音。只有已找到的字能查(由呼叫端把關)。
import { strings } from './strings.js';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

1. strings.js imported outside main.js 📘 Rule violation ⌂ Architecture

src/dictionary-card.js and src/wheel.js now import ./strings.js, creating inter-module imports
between src/ game modules outside the allowed src/main.js coordinator. This violates the
module-boundary rule and makes cross-module coupling harder to control.
Agent Prompt
## Issue description
Non-`main.js` modules under `src/` are importing another `src/` module (`./strings.js`), which violates the rule that only `src/main.js` may wire modules together.

## Issue Context
New imports were added to support localized `aria-label`s, but they introduce prohibited inter-module dependencies.

## Fix Focus Areas
- src/dictionary-card.js[1-5]
- src/wheel.js[1-5]
- src/main.js[1-120]
- src/strings.js[1-80]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread index.html
Comment on lines +91 to +93
<!-- 拼字串顯示區刻意**不是** aria-live 區塊:showPreview 每滑過一個字母就重寫一次,
掛上去等於在整段拖曳期間逐字播報 C→CA→CAT。真正該播的只有 flashPreview 那幾則結果
訊息,那些走下面的 #status(UI 文件 §4-F 第三段)-->

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

2. Non-english comments added 📘 Rule violation ⚙ Maintainability

Multiple added/modified comments contain non-English natural language (e.g., Chinese) in source
files. This violates the requirement that source code comments be written in English, reducing
maintainability for English-speaking reviewers/auditors.
Agent Prompt
## Issue description
New/modified comments in source files are written in a non-English natural language, which violates the English-only comment requirement.

## Issue Context
This PR adds extensive explanatory comments for accessibility behaviors; they should be translated to English (or reduced/removed if redundant).

## Fix Focus Areas
- index.html[91-93]
- src/main.js[15-23]
- src/strings.js[13-15]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread src/main.js
Comment on lines +18 to +23
function playOnce(el, cls) {
el.classList.remove(cls);
void el.offsetWidth;
el.classList.add(cls);
el.addEventListener('animationend', () => el.classList.remove(cls), { once: true });
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

3. Playonce listener buildup 🐞 Bug ☼ Reliability

playOnce() adds a fresh anonymous "animationend" listener every call; when the animation is
restarted before it ends, the previous run is canceled (no animationend), leaving stale listeners
that can accumulate during rapid repeated interactions. This can cause unbounded temporary listener
growth and multiple redundant callbacks firing on the next completed animation.
Agent Prompt
### Issue description
`playOnce(el, cls)` attaches a new anonymous `animationend` listener on every invocation. If the animation is restarted before it finishes (class removed/re-added), the previous animation run can be canceled without emitting `animationend`, so the `{ once: true }` listener never self-removes. Rapid repeated calls can therefore accumulate stale listeners until a later animation finally completes.

### Issue Context
This helper is used on user-click paths (e.g. hint/claim shake). The `shake` animation duration is 0.35s, so rapid clicking can easily restart the animation before the prior run ends.

### Fix Focus Areas
- src/main.js[15-23]
- src/main.js[439-446]
- src/style.css[313-319]

### Suggested fix
- Use a stable handler (not an inline anonymous closure) and register it for both `animationend` *and* `animationcancel` with `{ once: true }`, so canceled runs don’t leave listeners behind.
- Optionally guard against bubbled events by checking `e.target === el` and, if relevant, `e.animationName`.
- Alternative robust approach: keep a per-element `AbortController`/listener cleanup in a `WeakMap` so each new `playOnce()` aborts prior listeners before starting a new run.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread src/main.js
Comment on lines +592 to +598
// 三條關閉路徑(點卡片外、右上角 ✕、Escape)共用這支,才不會有一條忘了把焦點還回去。
function closeSettings() {
$('overlay-settings').hidden = true;
// 焦點交還給開卡的那顆齒輪:不還的話焦點落回 body,讀螢幕的人得從畫面最上面重新滑一次
// 才找得回自己剛才在哪裡。
$('btn-settings').focus();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

4. Focus returns to hidden 🐞 Bug ≡ Correctness

closeSettings() always focuses #btn-settings, but #btn-settings exists only inside #screen-game,
which may be hidden if the user keyboard-navigates to and activates #btn-level behind the untrapped
modal. Closing settings after switching screens can leave focus on a hidden element or fail to
restore focus to any visible control.
Agent Prompt
### Issue description
`closeSettings()` unconditionally focuses `#btn-settings`, but that button lives inside `#screen-game`. Because there is intentionally no JS focus trap for the settings dialog, keyboard users can Tab to underlying controls (like `#btn-level`) and switch to another screen while the settings overlay remains open. When the overlay is closed, focusing a control in a hidden section can strand focus.

### Issue Context
- Settings dialog explicitly does not implement a focus trap.
- `showLevels()` calls `showScreen('levels')`, which hides `#screen-game`.

### Fix Focus Areas
- src/main.js[592-607]
- src/main.js[565-572]
- src/main.js[196-209]
- index.html[81-88]
- index.html[140-148]

### Suggested fix
Pick one (or combine):
1) **Safer focus restore:** store the opener element when opening settings (e.g., `lastSettingsOpener = document.activeElement` or explicitly the gear button), and in `closeSettings()` only focus it if it’s still connected and visible (not in a hidden screen). Otherwise, focus a visible fallback on the current screen (e.g., first focusable in the visible section).
2) **Prevent background activation:** add a minimal keyboard focus trap or make background inert while the modal is open (even if you keep no trap for touch AT).
3) **Close-on-screen-change:** when `showScreen(name !== 'game')`, also close `overlay-settings` (and pick an appropriate focus target for that screen).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@min0625
min0625 merged commit 59cdcb4 into main Jul 28, 2026
1 check passed
@min0625
min0625 deleted the a11y-dialogs-and-live-region branch July 28, 2026 05:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant