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
4 changes: 4 additions & 0 deletions .jules/palette.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,7 @@
## $(date +%Y-%m-%d) - Prevent accidental data loss in inline editors
**Learning:** Forms that take a long time to fill out (like a WBS editor) are prone to accidental closure by users pressing `Escape` or clicking cancel. This causes immediate data loss without any warning, resulting in frustration.
**Action:** When working on editors that can be dismissed, track whether the user has modified any fields compared to their initial state. If there are changes, intercept the close action and present a confirmation dialog (`window.confirm`) to ensure they really want to discard their edits. Bypass this for intentional saves or explicit data overrides.

## 2026-08-21 - Preserve focus on invalid forms using aria-disabled
**Learning:** When native `disabled` is used on a form submit button, it disappears from the tab order. This is jarring when validation fails interactively, as the user cannot focus the button to read its title or understand why it is disabled.
**Action:** Use `aria-disabled` for form submit buttons linked to interactive validation. Ensure form submit events and button click events prevent default behavior when the button is marked as disabled.
18 changes: 17 additions & 1 deletion app.js
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,12 @@ function bindTableEvents(renderDraftValidation, updateEditorDraftFromEvent) {
return;
}

const saveButton = event.target.closest('button[type="submit"]');
if (saveButton && saveButton.getAttribute('aria-disabled') === 'true') {
event.preventDefault();
return;
}
Comment on lines +404 to +408

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟑 Native browser validation popup still appears when clicking the disabled save button

The guard meant to swallow clicks on the disabled save button (event.target.closest('button[type="submit"]') at app.js:404-408) is placed after an early return that only continues for rows carrying a task id, but the editor form sits in a row without one, so the guard never runs and the browser's native validation bubble pops up on click.
Impact: A user who clicks the greyed-out save button while required fields are invalid sees the native browser validation popup, the exact jarring behavior this change set out to eliminate.

Why the click guard is unreachable

The click listener starts with const row = event.target.closest('tr[data-task-id]'); if (!row) return; (app.js:387-390). The editor form is rendered inside renderEditorRow, whose <tr> gets row.dataset.editorAnchor = anchorId (app.js:797) and is a sibling of task rows, not a descendant. Task rows are the only ones with data-task-id (app.js:687). Therefore, when the save button is clicked, closest('tr[data-task-id]') returns null, the handler returns at line 389, and the new block at app.js:404-408 never executes. Because the click's default action is not prevented, the browser runs constraint validation for required fields (set at app.js:887) and shows the native popup before any submit event is dispatched. The submit-event guard at app.js:439-442 cannot help here because constraint-validation failure suppresses the submit event entirely. Previously the native disabled attribute made the click a no-op, so this is a regression against the PR's goal.

Prompt for agents
The click guard added in bindTableEvents (app.js:404-408) that calls event.preventDefault() when the submit button has aria-disabled="true" is never reached. The tableBody click handler returns early at app.js:387-390 whenever event.target has no ancestor tr[data-task-id]. The editor form's row is created in renderEditorRow with row.dataset.editorAnchor (app.js:797) and has no data-task-id, so clicks on the save button hit the early return before reaching the guard. As a result, clicking the aria-disabled save button is not prevented at the click level, and the browser's native constraint-validation popup (for required fields set at app.js:887) still appears β€” defeating the purpose of the change. Fix by relocating the save-button aria-disabled check to run before the `if (!row) return` early return at the top of the click handler (or otherwise ensure the guard executes for clicks originating inside the editor row), so event.preventDefault() is called on the click before the browser attempts form submission.
Open in Devin Review

Was this helpful? React with πŸ‘ or πŸ‘Ž to provide feedback.


if (!event.target.closest('input, select, button, label, .drag-handle')) {
openEditor({ mode: 'edit', targetId: taskId });
}
Expand Down Expand Up @@ -429,6 +435,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();
});
Expand Down Expand Up @@ -1068,7 +1080,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)';
}

Expand Down
2 changes: 2 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; object-src 'none'; base-uri 'none'; form-action 'self';" />
<title>ScopeWeave Planner</title>
<link rel="preload" href="styles.css" as="style" />
<link rel="modulepreload" href="cloud-sync.js" />
<link rel="modulepreload" href="analytics.js" />
<link rel="modulepreload" href="app.js" />
<link rel="stylesheet" href="styles.css" />
<link rel="stylesheet" href="toast-state.css" />
Expand Down
Loading