-
Notifications
You must be signed in to change notification settings - Fork 0
fix(a11y): keep invalid editor save focusable with aria-disabled #398
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
d803f02
dba7dc4
f6cb064
d9014b3
5b8d40b
0ab7383
4b19d11
ea1bba0
34d2af9
a68c1eb
75577a9
eba0a06
4bc7332
445c7a9
b19ffab
9048a1d
3ae6712
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -408,6 +408,12 @@ function bindTableEvents(renderDraftValidation, updateEditorDraftFromEvent) { | |
| return; | ||
| } | ||
| event.preventDefault(); | ||
|
|
||
| const saveButton = form.querySelector('button[type="submit"]'); | ||
| if (saveButton && saveButton.getAttribute('aria-disabled') === 'true') { | ||
| return; | ||
| } | ||
|
|
||
| renderDraftValidation.flush(); | ||
| saveEditor(); | ||
|
Comment on lines
+412
to
418
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win 유효성 상태를 갱신한 후 제출을 차단하세요. 입력 이벤트의 유효성 렌더링이 대기 중이면 이 코드는 이전
수정 예시 event.preventDefault();
+ renderDraftValidation.flush();
const saveButton = form.querySelector('button[type="submit"]');
if (saveButton && saveButton.getAttribute('aria-disabled') === 'true') {
return;
}
- renderDraftValidation.flush();
saveEditor();🤖 Prompt for AI Agents |
||
| }); | ||
|
|
@@ -1047,7 +1053,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)'; | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -739,25 +739,54 @@ function openReportModal() { | |
| // no DOMParser needed → node-testable); swap for a real XML parser if | ||
| // hand-edited files ever matter. | ||
| export function parseMsProjectXml(xml) { | ||
| // Fully linear extract (indexOf/slice) — no dynamic RegExp and no lazy | ||
| // [\s\S]*? block collectors (those can quadratic-backtrack on truncated input). | ||
| const tag = (block, name) => { | ||
| const m = block.match(new RegExp(`<${name}>([^<]*)</${name}>`)); | ||
| return m ? m[1].trim() : ''; | ||
| const openingTag = `<${name}>`; | ||
| const closingTag = `</${name}>`; | ||
| const valueStart = block.indexOf(openingTag); | ||
| if (valueStart === -1) return ''; | ||
| const contentStart = valueStart + openingTag.length; | ||
| const valueEnd = block.indexOf(closingTag, contentStart); | ||
| return valueEnd === -1 ? '' : block.slice(contentStart, valueEnd).trim(); | ||
| }; | ||
| const collectBlocks = (source, openTag, closeTag) => { | ||
| const out = []; | ||
| let from = 0; | ||
| for (;;) { | ||
| const start = source.indexOf(openTag, from); | ||
| if (start === -1) break; | ||
| const contentStart = start + openTag.length; | ||
| const end = source.indexOf(closeTag, contentStart); | ||
| // Incomplete open tag: stop linearly (do not rescan the remainder). | ||
| if (end === -1) break; | ||
| out.push(source.slice(start, end + closeTag.length)); | ||
| from = end + closeTag.length; | ||
| } | ||
| return out; | ||
|
Comment on lines
744
to
+766
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift 유효한 XML 태그 공백을 처리하세요.
XML 구조를 처리하는 파서로 변경하거나, 시작 및 종료 태그를 XML 구문에 맞게 토큰화하세요. 이 태그 공백 변형의 회귀 테스트도 추가하세요. 🤖 Prompt for AI Agents |
||
| }; | ||
| const predecessorIds = (block) => { | ||
| const ids = []; | ||
| for (const link of collectBlocks(block, '<PredecessorLink>', '</PredecessorLink>')) { | ||
| const uid = tag(link, 'PredecessorUID'); | ||
| if (/^\d+$/.test(uid)) ids.push(`msp-${uid}`); | ||
| } | ||
| return ids; | ||
| }; | ||
| const unescape = (s) => s | ||
| .replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"') | ||
| .replace(/'/g, "'").replace(/&/g, '&'); | ||
| const day = (s) => (/^\d{4}-\d{2}-\d{2}/.test(s) ? s.slice(0, 10) : ''); | ||
| const tasks = []; | ||
| const parents = {}; // depth -> last task id at that depth | ||
| const blocks = xml.match(/<Task>[\s\S]*?<\/Task>/g) || []; | ||
| const blocks = collectBlocks(String(xml || ''), '<Task>', '</Task>'); | ||
| for (const block of blocks) { | ||
| const uid = tag(block, 'UID'); | ||
| const name = unescape(tag(block, 'Name')); | ||
| if (!uid || uid === '0' || !name) continue; // project-summary row / blanks | ||
| const level = Math.max(1, Number(tag(block, 'OutlineLevel')) || 1); | ||
| const depth = Math.min(level, 3); // deeper levels flatten to task level | ||
| const preds = [...block.matchAll(/<PredecessorLink>[\s\S]*?<PredecessorUID>(\d+)<\/PredecessorUID>[\s\S]*?<\/PredecessorLink>/g)] | ||
| .map((m) => `msp-${m[1]}`); | ||
| const preds = predecessorIds(block); | ||
| const pct = Number(tag(block, 'PercentComplete')) || 0; | ||
| const t = { | ||
| id: `msp-${uid}`, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
필수 오류 설명을
title만으로 제공하지 마세요.title은 키보드 및 보조 기술에서 일관되게 노출되지 않습니다. 필수 안내는 명시적 접근성 설명으로 연결해야 합니다. (developer.mozilla.org).jules/palette.md#L118-L120:title이 스크린 리더에서 읽힌다는 설명을 제거하고, 필수 안내에는aria-describedby같은 명시적 연결을 사용하도록 수정하세요.app.js#L1056-L1061: 오류가 있으면 저장 버튼에aria-describedby="editor-errors"를 설정하세요. 오류가 없으면 이 속성을 제거하세요.title은 보조 안내로만 유지하세요.tests/e2e/editor-save-aria-disabled.spec.js#L13-L17:editor-errors에 대한aria-describedby또는 접근성 설명을 검증하세요.📍 Affects 3 files
.jules/palette.md#L118-L120(this comment)app.js#L1056-L1061tests/e2e/editor-save-aria-disabled.spec.js#L13-L17🤖 Prompt for AI Agents