Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions frontend/sync.js
Original file line number Diff line number Diff line change
Expand Up @@ -375,11 +375,13 @@ export function setupSyncFeature() {
eyeOffIcon.style.display = 'block';
togglePasswordBtn.setAttribute('aria-label', '隱藏密碼');
togglePasswordBtn.setAttribute('title', '隱藏密碼');
togglePasswordBtn.setAttribute('aria-pressed', 'true');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Adding aria-pressed to a button that already changes its aria-label to describe an action (e.g., "隱藏密碼") is an accessibility anti-pattern. A toggle button should either have a static label with aria-pressed to indicate state, or a dynamic label that changes to describe the action without aria-pressed. Mixing both can lead to confusing announcements like "Hide password, toggle button, pressed". Consider keeping the label static if you want to use the toggle button pattern.

} else {
eyeIcon.style.display = 'block';
eyeOffIcon.style.display = 'none';
togglePasswordBtn.setAttribute('aria-label', '顯示密碼');
togglePasswordBtn.setAttribute('title', '顯示密碼');
Comment on lines 376 to 383
Comment on lines 376 to 383
togglePasswordBtn.setAttribute('aria-pressed', 'false');
}
});
}
Expand Down
4 changes: 3 additions & 1 deletion frontend/theme.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,11 @@ export function applyTheme(theme) {
}

if (toggleBtn) {
const nextThemeLabel = theme === 'light' ? '深色' : '淺色';
const isLight = theme === 'light';
const nextThemeLabel = isLight ? '深色' : '淺色';
toggleBtn.setAttribute('aria-label', `切換至${nextThemeLabel}模式`);
toggleBtn.setAttribute('title', `切換至${nextThemeLabel}模式`);
Comment on lines +29 to 31
toggleBtn.setAttribute('aria-pressed', isLight ? 'true' : 'false');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Using aria-pressed alongside a dynamic aria-label that describes the next action (e.g., "切換至深色模式") creates a conflict between the button's label and its state. For instance, in light mode, the button would be announced as "Switch to dark mode, toggle button, pressed", which is contradictory. It is recommended to use a static label (e.g., "深色模式") when using aria-pressed, or simply omit aria-pressed if the label already describes the state change.

}

document.dispatchEvent(new CustomEvent('themechange', {
Expand Down
8 changes: 4 additions & 4 deletions public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
</noscript>
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit" async defer></script>
<script src="/theme-init.js"></script>
<link rel="stylesheet" href="/dist/main-bt8E1vXG.css" id="vite-css">
<link rel="stylesheet" href="/dist/main-D2sAJccc.css" id="vite-css">
<link rel="icon" type="image/x-icon" href="/favicon.ico?v=20260402">
<link rel="icon" type="image/png" href="/favicon-96x96.png?v=20260402" sizes="96x96" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg?v=20260402" />
Expand Down Expand Up @@ -91,7 +91,7 @@ <h1>成績分析平台</h1>
</div>
<div class="flex-center-gap-8 header-actions">
<button class="icon-btn theme-toggle-btn" id="themeToggleBtn" type="button" aria-label="切換至淺色模式"
title="切換主題">
title="切換主題" aria-pressed="false">
<span class="dropdown-icon theme-icon theme-icon-moon">
<svg class="inline-svg-icon icon-18" fill="currentColor" xmlns="http://www.w3.org/2000/svg"
height="1em" viewBox="0 -960 960 960" width="1em">
Expand Down Expand Up @@ -185,7 +185,7 @@ <h3 class="flex-center-gap-8">
autocomplete="current-password" placeholder=" " data-tour="login-password">
<label for="passwordInput">密碼</label>
<button type="button" id="togglePasswordBtn" class="toggle-password-btn"
aria-label="顯示密碼" title="顯示密碼">
aria-label="顯示密碼" title="顯示密碼" aria-pressed="false">
<svg class="inline-svg-icon eye-icon" fill="currentColor"
xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 -960 960 960"
width="1em">
Expand Down Expand Up @@ -534,7 +534,7 @@ <h3 class="section-title">
</footer>
</div>

<script type="module" src="/dist/main-D-w4GNFQ.js"></script>
<script type="module" src="/dist/main-0jcAJeLy.js"></script>
</body>

</html>
4 changes: 3 additions & 1 deletion tests/frontend/theme.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ function uninstallDom() {
describe('theme.js', () => {
beforeEach(() => {
installDom(`<!doctype html><html><body>
<button id="themeToggleBtn" aria-label="" title="">
<button id="themeToggleBtn" aria-label="" title="" aria-pressed="false">
<span class="theme-icon theme-icon-moon"></span>
<span class="theme-icon theme-icon-sun hidden"></span>
</button>
Expand Down Expand Up @@ -75,13 +75,15 @@ describe('theme.js', () => {
assert.equal(moon.classList.contains('hidden'), true);
assert.equal(btn.getAttribute('aria-label'), '切換至深色模式');
assert.equal(btn.getAttribute('title'), '切換至深色模式');
assert.equal(btn.getAttribute('aria-pressed'), 'true');

applyTheme('dark');
assert.equal(root.hasAttribute('data-theme'), false);
assert.equal(sun.classList.contains('hidden'), true);
assert.equal(moon.classList.contains('hidden'), false);
assert.equal(btn.getAttribute('aria-label'), '切換至淺色模式');
assert.equal(btn.getAttribute('title'), '切換至淺色模式');
assert.equal(btn.getAttribute('aria-pressed'), 'false');
});

it('setupThemeToggle should toggle theme, persist localStorage and dispatch themechange', async () => {
Expand Down
Loading