diff --git a/CHANGELOG.md b/CHANGELOG.md index e434fa01..7e43f3bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -61,6 +61,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 mode, delegating provider/model/topology policy to the shared service without weakening ScopeWeave's authenticated, fail-closed transport or response boundary controls. +- Kept invalid editor save actions keyboard-discoverable with + `aria-disabled="true"` and an explicit `aria-describedby` relationship while + preserving synchronous submit-time validation as the only persistence gate; + immediately corrected click and Enter submissions now use the latest draft. - Accepted XML whitespace before exact Microsoft Project element delimiters while preserving the linear, regex-free import scanner and rejecting attributes, longer names, non-XML whitespace, nested unmatched blocks, and diff --git a/app.js b/app.js index a04aae71..bacbb51f 100644 --- a/app.js +++ b/app.js @@ -87,7 +87,7 @@ const CSV_HEADERS = [ '스프린트', '스토리포인트' ]; -const CSV_FORMULA_PREFIX_PATTERN = /^\s*[=+\-@|]/; +const CSV_FORMULA_PREFIX_PATTERN = /^\s*[=+\-@|=+-@|]/; const UNSAFE_JSON_KEYS = new Set(['__proto__', 'constructor', 'prototype']); const CSV_FIELD_LABELS = Object.freeze(Object.assign(Object.create(null), { @@ -790,6 +790,9 @@ function renderEditorRow(anchorId) { panel.className = 'editor-panel'; const form = document.createElement('form'); form.dataset.editorForm = 'true'; + // ScopeWeave validation is the sole persistence gate; native constraint + // validation would intercept submission and move focus before saveEditor(). + form.noValidate = true; const editorGrid = document.createElement('div'); editorGrid.className = 'editor-grid'; @@ -1068,7 +1071,13 @@ 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'); + saveButton.setAttribute('aria-describedby', 'editor-errors'); + } else { + saveButton.removeAttribute('aria-disabled'); + saveButton.removeAttribute('aria-describedby'); + } saveButton.title = errors.length > 0 ? '입력값을 올바르게 수정해야 저장할 수 있습니다.' : '저장 (Enter)'; } diff --git a/docs/doctoring/editor-save-validation-accessibility.md b/docs/doctoring/editor-save-validation-accessibility.md new file mode 100644 index 00000000..e65fbb8f --- /dev/null +++ b/docs/doctoring/editor-save-validation-accessibility.md @@ -0,0 +1,75 @@ +# Focusable editor validation and synchronous save authority + +## Decision + +The editor save button remains a native button in the sequential keyboard order. +When the current draft is invalid, ScopeWeave exposes +`aria-disabled="true"`, connects the button to `#editor-errors` with +`aria-describedby`, and keeps the control physically focusable. Activation is +still accepted as an input event, but `saveEditor()` synchronously validates the +latest draft and refuses persistence while errors remain. + +The debounced validation pass is presentation only. It updates field error +states, the error summary, and save-button semantics; it is not an authorization +or persistence boundary. This avoids two inverse races: + +- a user corrects the final error and immediately clicks or presses Enter before + the debounce updates a stale disabled state; and +- a user introduces an error and immediately submits before the presentation + layer catches up. + +Both paths are decided by the same latest-draft validation inside +`saveEditor()`. + +## Accessibility rationale + +WAI-ARIA defines `aria-disabled` as a perceivable disabled state. W3C's +Authoring Practices notes that disabled commands can remain focusable when their +discoverability is useful, provided scripting prevents the unavailable action. +The save action is a primary command whose error relationship benefits from +keyboard discovery, so ScopeWeave keeps it focusable and exposes the current +error summary as its accessible description. + +The native `disabled` attribute is not used for this state because it removes the +button from normal keyboard focus and can preserve a stale block while the +debounced presentation state catches up. The implementation must not treat +`aria-disabled` alone as enforcement; synchronous validation prevents mutation. + +## Executable evidence + +`tests/e2e/editor-validation-sync.spec.js` verifies: + +- an invalid save control remains focusable and described; +- activating it does not create a task; +- a draft corrected immediately before click saves without waiting for debounce; +- a draft corrected immediately before Enter saves without waiting for debounce; +- a newly invalid draft cannot persist before debounce completes; and +- error text remains available through `#editor-errors`. + +The existing full-browser suite is updated to activate invalid save controls and +assert that task count and editor state are unchanged for reversed dates, +invalid calendar dates, and HTML input. It also re-enables the complete +`scopeweave.spec.js` cloud path for the editor acceptance boundary. + +## Compatibility and rollback + +This change does not modify persisted WBS data, API contracts, authentication, +or server storage. It changes only the editor's presentation semantics and keeps +existing synchronous validation behavior as the persistence authority. + +Rollback must revert the button-state implementation, focused browser tests, +full-suite expectations, package script, CHANGELOG entry, and this record +together. Reintroducing native `disabled` requires a new proof that immediate +correction cannot be blocked by stale debounced state. + +## References + +World Wide Web Consortium. (2023). *Accessible Rich Internet Applications +(WAI-ARIA) 1.2*. https://www.w3.org/TR/wai-aria-1.2/ + +World Wide Web Consortium. (2025). *Developing a keyboard interface*. +WAI-ARIA Authoring Practices Guide. +https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/ + +World Wide Web Consortium. (2024). *Web Content Accessibility Guidelines +(WCAG) 2.2*. https://www.w3.org/TR/WCAG22/ diff --git a/index.html b/index.html index d24b2a88..8c1a832f 100644 --- a/index.html +++ b/index.html @@ -6,6 +6,8 @@