Skip to content

fix(a11y): keep invalid editor save focusable with aria-disabled - #398

Closed
seonghobae wants to merge 17 commits into
developfrom
palette-a11y-tooltip-focus-5177313803984877086
Closed

fix(a11y): keep invalid editor save focusable with aria-disabled#398
seonghobae wants to merge 17 commits into
developfrom
palette-a11y-tooltip-focus-5177313803984877086

Conversation

@seonghobae

@seonghobae seonghobae commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Replaces native disabled on the editor save button with aria-disabled="true" when validation fails, keeping the explanatory tooltip reachable by keyboard and assistive technology.
  • Guards the delegated form submit handler so pointer, keyboard, and scripted activation cannot save an invalid draft.
  • Removes aria-disabled once the draft is valid and restores the normal save tooltip.
  • Adds Playwright regression coverage for focusability, blocked activation, validation messaging, and successful save after correction.

Verification

  • tests/e2e/editor-save-aria-disabled.spec.js
  • Full repository checks required before merge.

Originally created by Jules for accessibility hardening.

Summary by CodeRabbit

  • 접근성 개선

    • 유효하지 않은 편집기 저장 상태에서도 저장 버튼의 포커스와 클릭을 허용하면서 저장을 차단합니다.
    • 입력이 유효해지면 저장 버튼이 정상적으로 활성화됩니다.
  • 버그 수정

    • MS Project XML 파서가 특수문자, 빈 입력, 불완전한 태그를 안정적으로 처리합니다.
  • 테스트

    • 편집기 저장 접근성 및 MS Project XML 처리에 대한 검증을 강화했습니다.
  • 개발 환경

    • Node.js 지원 버전을 22.13.0 기준으로 고정했습니다.

@google-labs-jules

Copy link
Copy Markdown

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@seonghobae, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 52 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 12bf765a-30f0-4ffd-bd33-f9999adb1774

📥 Commits

Reviewing files that changed from the base of the PR and between b19ffab and 3ae6712.

📒 Files selected for processing (2)
  • package.json
  • tests/unit/editor-submit-accessibility.test.mjs
📝 Walkthrough

Walkthrough

편집기 저장 버튼이 disabled 대신 aria-disabled를 사용하고 제출을 차단합니다. MSP XML 파서는 선형 탐색 방식으로 변경되었습니다. Node.js 버전과 패키지 요구사항이 고정되었으며 관련 테스트가 추가되었습니다.

Changes

편집기 저장 접근성 처리

Layer / File(s) Summary
저장 상태와 제출 처리
.jules/palette.md, app.js, tests/unit/editor-submit-accessibility.test.mjs, tests/e2e/editor-save-aria-disabled.spec.js
유효하지 않은 상태에서 저장 버튼이 aria-disabled="true"를 사용합니다. 제출 이벤트는 해당 상태에서 저장을 중단합니다. 단위 테스트와 E2E 테스트는 포커스, 제목, 클릭, 저장 완료 동작을 검증합니다.

MSP XML 파싱

Layer / File(s) Summary
선형 XML 수집과 입력 검증
cloud-sync.js, tests/unit/msproject.test.mjs
parseMsProjectXmlindexOfslice로 태그와 <Task> 블록을 수집합니다. 닫히지 않은 태그와 작업 블록을 중단 처리하며, 특수문자 작업명과 null 입력을 테스트합니다.

Node.js 및 패키지 버전 고정

Layer / File(s) Summary
실행 환경과 의존성 설정
package.json, .github/workflows/fuzz.yml, .github/workflows/server-tests.yml
워크플로의 Node.js 버전을 22.13.0으로 고정합니다. packageManager, Node.js engines, @hono/node-server, hono, c8 버전을 갱신합니다.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목은 유효하지 않은 편집기 저장 버튼을 aria-disabled로 유지해 포커스 가능하게 변경하는 주요 내용을 정확히 설명합니다.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch palette-a11y-tooltip-focus-5177313803984877086

Comment @coderabbitai help to get the list of available commands.

@seonghobae
seonghobae enabled auto-merge (squash) August 3, 2026 00:35
@seonghobae
seonghobae changed the base branch from develop to fix/security-hono-csv-hardening August 3, 2026 01:13
@seonghobae
seonghobae changed the base branch from fix/security-hono-csv-hardening to develop August 3, 2026 01:19
@seonghobae seonghobae changed the title 🎨 Palette: 툴팁을 가진 요소를 키보드로 접근할 수 있도록 개선 fix(a11y): keep invalid editor save focusable with aria-disabled Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

@jules Please make the submit guard flush the debounced draft validation before reading aria-disabled, then decide whether to return or call saveEditor(). The current order can reject an immediately submitted valid edit because the button still has stale aria-disabled="true". Extend tests/e2e/editor-save-aria-disabled.spec.js with an immediate valid input + Enter/click path that proves the latest input state is used. Preserve focusability and blocked activation for genuinely invalid drafts.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.jules/palette.md:
- Around line 118-120: Update .jules/palette.md lines 118-120 to remove the
claim that title is reliably announced by screen readers and require explicit
accessible descriptions such as aria-describedby; in app.js lines 1056-1061, set
aria-describedby="editor-errors" on the save button when validation errors exist
and remove it when they do not, while retaining title only as supplemental
guidance; in tests/e2e/editor-save-aria-disabled.spec.js lines 13-17, add an
assertion that the save button references editor-errors or exposes the
corresponding accessible description.

In `@app.js`:
- Around line 412-418: Update the submit handler around renderDraftValidation
and saveEditor so renderDraftValidation.flush() runs before checking the submit
button’s aria-disabled state or state.editor.errors; return without calling
saveEditor when the refreshed validation reports the draft is invalid. Add a
regression test covering deletion of a required value while validation rendering
is pending.

In `@cloud-sync.js`:
- Around line 744-766: Update the tag and collectBlocks helpers to recognize
XML-valid whitespace before the closing angle bracket in both opening and
closing tags, such as Task, UID, and their corresponding end tags, while
preserving exact element-name matching and incomplete-input handling. Add
regression coverage for these whitespace variants in task blocks and fields.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 18e6db79-f5a0-4484-ba9f-a6dea23c6132

📥 Commits

Reviewing files that changed from the base of the PR and between a756b7e and b19ffab.

⛔ Files ignored due to path filters (2)
  • package-lock.json is excluded by !**/package-lock.json
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (9)
  • .github/workflows/fuzz.yml
  • .github/workflows/server-tests.yml
  • .jules/palette.md
  • app.js
  • cloud-sync.js
  • package.json
  • tests/e2e/editor-save-aria-disabled.spec.js
  • tests/unit/editor-submit-accessibility.test.mjs
  • tests/unit/msproject.test.mjs

Comment thread .jules/palette.md
Comment on lines +118 to +120
## 2026-08-03 - Replace native disabled with aria-disabled for form submit buttons
**Learning:** Using the native `disabled` attribute on form submission buttons prevents them from receiving focus. This completely hides the button's `title` tooltip (which often explains *why* the form cannot be submitted) from keyboard-only and screen-reader users, leading to a confusing UX when validation fails.
**Action:** Use `aria-disabled="true"` instead of `disabled` for submit buttons when form validation fails. This ensures the button remains in the tab order so the `title` tooltip can be read. Since the browser no longer blocks the form submission natively, ensure the form's `submit` event listener explicitly checks `getAttribute('aria-disabled') === 'true'` and aborts if necessary.

Copy link
Copy Markdown

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-L1061
  • tests/e2e/editor-save-aria-disabled.spec.js#L13-L17
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.jules/palette.md around lines 118 - 120, Update .jules/palette.md lines
118-120 to remove the claim that title is reliably announced by screen readers
and require explicit accessible descriptions such as aria-describedby; in app.js
lines 1056-1061, set aria-describedby="editor-errors" on the save button when
validation errors exist and remove it when they do not, while retaining title
only as supplemental guidance; in tests/e2e/editor-save-aria-disabled.spec.js
lines 13-17, add an assertion that the save button references editor-errors or
exposes the corresponding accessible description.

Comment thread app.js
Comment on lines +412 to 418
const saveButton = form.querySelector('button[type="submit"]');
if (saveButton && saveButton.getAttribute('aria-disabled') === 'true') {
return;
}

renderDraftValidation.flush();
saveEditor();

Copy link
Copy Markdown

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

유효성 상태를 갱신한 후 제출을 차단하세요.

입력 이벤트의 유효성 렌더링이 대기 중이면 이 코드는 이전 aria-disabled 값을 검사합니다. 유효한 초안에서 필수 값을 지운 직후 제출하면 saveEditor()가 호출될 수 있습니다.

renderDraftValidation.flush()를 먼저 호출하세요. 그 후 갱신된 aria-disabled 또는 state.editor.errors를 검사하세요. 이 순서를 검증하는 회귀 테스트도 추가하세요.

수정 예시
     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
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app.js` around lines 412 - 418, Update the submit handler around
renderDraftValidation and saveEditor so renderDraftValidation.flush() runs
before checking the submit button’s aria-disabled state or state.editor.errors;
return without calling saveEditor when the refreshed validation reports the
draft is invalid. Add a regression test covering deletion of a required value
while validation rendering is pending.

Comment thread cloud-sync.js
Comment on lines 744 to +766
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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

유효한 XML 태그 공백을 처리하세요.

tagcollectBlocks<Task></Task>처럼 정확히 일치하는 문자열만 찾습니다. XML은 <Task >, <UID >, </Task >와 같은 공백 표현을 허용합니다. 이 형식의 MSP XML은 작업 블록 또는 필드를 찾지 못해서 가져오기에 실패합니다.

XML 구조를 처리하는 파서로 변경하거나, 시작 및 종료 태그를 XML 구문에 맞게 토큰화하세요. 이 태그 공백 변형의 회귀 테스트도 추가하세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cloud-sync.js` around lines 744 - 766, Update the tag and collectBlocks
helpers to recognize XML-valid whitespace before the closing angle bracket in
both opening and closing tags, such as Task, UID, and their corresponding end
tags, while preserving exact element-name matching and incomplete-input
handling. Add regression coverage for these whitespace variants in task blocks
and fields.

Copy link
Copy Markdown
Contributor Author

@jules Please address all current actionable review findings on this branch as one focused accessibility fix:

  1. In the form submit handler, cancel/flush pending debounced validation before deciding whether submission is allowed, then synchronously validate the latest draft. Do not rely on a stale aria-disabled value. Invalid latest state must stay open and show feedback; a just-corrected valid value submitted immediately by Enter/click must save.
  2. While invalid, keep the save button focusable with aria-disabled="true" and set aria-describedby="editor-errors"; remove both attributes when valid. Treat title as supplemental only, and correct .jules/palette.md accordingly.
  3. Extend unit/E2E regression coverage for both race directions: (a) fill the required value and immediately submit; (b) delete a required value and immediately submit before the debounce delay.
  4. Do not duplicate the XML parser implementation here. Once fix(security): hono 2.0.12 + CSV formula guard + JWT fail-closed + XML ReDoS #386 lands, this PR will be stacked/rebased onto that security baseline; keep this branch's product diff focused on editor accessibility.

Run targeted unit/E2E tests and the full repository checks.

Copy link
Copy Markdown
Contributor Author

Closing in favor of #411. The direction—keep invalid Save focusable with aria-disabled—is correct, but the current submit order reads stale DOM validity before debounced validation is synchronized, and the control lacks an explicit aria-describedby relationship to the error region. This branch also carries copied security/runtime/XML changes that belong to #386. #411 preserves both race-direction regressions and the accessibility contract for a clean post-security-baseline implementation.

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