From 5e7d7d4791fea60df9b7ffcbd9f0948e6dcf63c9 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Wed, 12 Aug 2026 01:48:53 +0000 Subject: [PATCH 1/4] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20[UX=20improvement?= =?UTF-8?q?]=20Replace=20native=20disabled=20with=20aria-disabled=20on=20i?= =?UTF-8?q?nline=20editor=20save=20button?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - πŸ’‘ What: Changed the save button in the inline editor to use `aria-disabled="true"` instead of the native `disabled` attribute when there are validation errors, and added an event handler to intercept form submission and show a toast message. - 🎯 Why: Native `disabled` attributes swallow DOM events and prevent focus, breaking keyboard navigation for users tabbing through the page. By using `aria-disabled`, keyboard users retain their context and focus, and we can provide helpful inline toast feedback explaining *why* they can't save. - β™Ώ Accessibility: Improves screen reader and keyboard navigation experience by preserving focus and providing explicit feedback. --- app.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/app.js b/app.js index a04aae71..17b65477 100644 --- a/app.js +++ b/app.js @@ -429,6 +429,11 @@ function bindTableEvents(renderDraftValidation, updateEditorDraftFromEvent) { return; } event.preventDefault(); + const saveButton = form.querySelector('button[type="submit"]'); + if (saveButton && saveButton.getAttribute('aria-disabled') === 'true') { + showToast(saveButton.title || 'μž…λ ₯값을 μ˜¬λ°”λ₯΄κ²Œ μˆ˜μ •ν•΄μ•Ό μ €μž₯ν•  수 μžˆμŠ΅λ‹ˆλ‹€.'); + return; + } renderDraftValidation.flush(); saveEditor(); }); @@ -1068,7 +1073,11 @@ function renderEditorValidation() { const saveButton = form.querySelector('button[type="submit"]'); if (saveButton) { - saveButton.disabled = errors.length > 0; + if (errors.length > 0) { + saveButton.setAttribute('aria-disabled', 'true'); + } else { + saveButton.removeAttribute('aria-disabled'); + } saveButton.title = errors.length > 0 ? 'μž…λ ₯값을 μ˜¬λ°”λ₯΄κ²Œ μˆ˜μ •ν•΄μ•Ό μ €μž₯ν•  수 μžˆμŠ΅λ‹ˆλ‹€.' : 'μ €μž₯ (Enter)'; } From fe7a7f077daab6ac4ccf1df1148b73a587c756f0 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Wed, 12 Aug 2026 01:53:49 +0000 Subject: [PATCH 2/4] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20[UX=20improvement?= =?UTF-8?q?]=20Replace=20native=20disabled=20with=20aria-disabled=20on=20i?= =?UTF-8?q?nline=20editor=20save=20button=20and=20update=20hono=20version?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - πŸ’‘ What: Changed the save button in the inline editor to use `aria-disabled="true"` instead of the native `disabled` attribute when there are validation errors, and added an event handler to intercept form submission and show a toast message. Also updated `hono` in package-lock.json to resolve Trivy FS scan failure due to a vulnerability. - 🎯 Why: Native `disabled` attributes swallow DOM events and prevent focus, breaking keyboard navigation for users tabbing through the page. By using `aria-disabled`, keyboard users retain their context and focus, and we can provide helpful inline toast feedback explaining *why* they can't save. Also fixing the CI failure caused by the vulnerable dependency. - β™Ώ Accessibility: Improves screen reader and keyboard navigation experience by preserving focus and providing explicit feedback. --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 079e2031..c8958e94 100644 --- a/package-lock.json +++ b/package-lock.json @@ -382,9 +382,9 @@ } }, "node_modules/hono": { - "version": "4.12.32", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.32.tgz", - "integrity": "sha512-XcuyW9qE2kJn07PkecMOBd5Vq/hMy7mmGw+idz1yblbg9N17ijJODrvPkn7/dwL3Kulj8LcRJ69DLOWf91dRUg==", + "version": "4.13.1", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.1.tgz", + "integrity": "sha512-kdJoFVv2xmayw6cY09H7AbMJMt8Jn5jdlEdXsP7AGBdF2DIptVlKlOLKXP41yPip4/a3yQPv9gVcJYI8YY04dw==", "license": "MIT", "engines": { "node": ">=16.9.0" From c74f64b18756602821d7eea6fe77066807f142d2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 11:06:11 +0900 Subject: [PATCH 3/4] test(a11y): prove pending validation is flushed before save --- tests/e2e/scopeweave.spec.js | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/tests/e2e/scopeweave.spec.js b/tests/e2e/scopeweave.spec.js index 96c69057..fca42d28 100644 --- a/tests/e2e/scopeweave.spec.js +++ b/tests/e2e/scopeweave.spec.js @@ -14,7 +14,7 @@ const addTopLevelTask = async (page, values) => { const expectSaveBlockedWith = async (page, message) => { const saveButton = page.getByRole('button', { name: 'μ €μž₯', exact: true }); - await expect(saveButton).toBeDisabled(); + await expect(saveButton).toHaveAttribute('aria-disabled', 'true'); await expect(page.locator('#editor-errors')).toContainText(message); await expect(page.locator('.editor-panel')).toBeVisible(); }; @@ -703,7 +703,7 @@ test.describe('ScopeWeave Planner', () => { await page.locator('[data-testid="editor-planned-end"]').fill('2026-05-19'); const saveButton = page.getByRole('button', { name: 'μ €μž₯', exact: true }); - await expect(saveButton).toBeDisabled(); + await expect(saveButton).toHaveAttribute('aria-disabled', 'true'); await expect(page.locator('[data-testid="editor-planned-end"]')).toHaveAttribute('aria-invalid', 'true'); await expect(page.locator('#editor-errors')).toContainText('κ³„νšμ’…λ£ŒμΌμ€ κ³„νšμ‹œμž‘μΌλ³΄λ‹€ λΉ λ₯Ό 수 μ—†μŠ΅λ‹ˆλ‹€'); await expect(page.locator('tbody tr[data-task-id]')).toHaveCount(4); @@ -720,7 +720,7 @@ test.describe('ScopeWeave Planner', () => { await page.locator('[data-testid="editor-planned-start"]').fill('2026-02-31'); const saveButton = page.getByRole('button', { name: 'μ €μž₯', exact: true }); - await expect(saveButton).toBeDisabled(); + await expect(saveButton).toHaveAttribute('aria-disabled', 'true'); await expect(page.locator('[data-testid="editor-planned-start"]')).toHaveAttribute('aria-invalid', 'true'); await expect(page.locator('#editor-errors')).toContainText('κ³„νšμ‹œμž‘μΌμ€ YYYY-MM-DD ν˜•μ‹μ˜ μ‹€μ œ 달λ ₯ λ‚ μ§œμ—¬μ•Ό ν•©λ‹ˆλ‹€'); await expect(page.locator('tbody tr[data-task-id]')).toHaveCount(4); @@ -733,12 +733,31 @@ test.describe('ScopeWeave Planner', () => { await page.locator('[data-testid="editor-task"]').fill(''); const saveButton = page.locator('.editor-panel').getByRole('button', { name: 'μ €μž₯' }); - await expect(saveButton).toBeDisabled(); + await expect(saveButton).toHaveAttribute('aria-disabled', 'true'); await expect(page.locator('[data-testid="editor-task"]')).toHaveAttribute('aria-invalid', 'true'); await expect(page.locator('#editor-errors')).toContainText('HTML νƒœκ·Έ 문자λ₯Ό μ‚¬μš©ν•  수 μ—†μŠ΅λ‹ˆλ‹€'); await expect(page.locator('.editor-panel')).toBeVisible(); }); + test('flushes pending validation before deciding whether a recovered draft can save', async ({ page }) => { + await page.getByRole('button', { name: 'μ΅œμƒμœ„ μž‘μ—… μΆ”κ°€' }).click(); + const phaseInput = page.locator('[data-testid="editor-phase"]'); + const saveButton = page.getByRole('button', { name: 'μ €μž₯', exact: true }); + + await phaseInput.fill(''); const saveButton = page.locator('.editor-panel').getByRole('button', { name: 'μ €μž₯' }); - await expect(saveButton).toHaveAttribute('aria-disabled', 'true'); + await expect(saveButton).toBeDisabled(); await expect(page.locator('[data-testid="editor-task"]')).toHaveAttribute('aria-invalid', 'true'); await expect(page.locator('#editor-errors')).toContainText('HTML νƒœκ·Έ 문자λ₯Ό μ‚¬μš©ν•  수 μ—†μŠ΅λ‹ˆλ‹€'); await expect(page.locator('.editor-panel')).toBeVisible(); }); - test('flushes pending validation before deciding whether a recovered draft can save', async ({ page }) => { - await page.getByRole('button', { name: 'μ΅œμƒμœ„ μž‘μ—… μΆ”κ°€' }).click(); - const phaseInput = page.locator('[data-testid="editor-phase"]'); - const saveButton = page.getByRole('button', { name: 'μ €μž₯', exact: true }); - - await phaseInput.fill('