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.

## 2024-05-31 - Replace native disabled with aria-disabled for form submit buttons
**Learning:** When replacing a native `disabled` attribute with `aria-disabled='true'` on a `<button type='submit'>`, the button becomes active in the DOM and will trigger native HTML5 form validation popups and `submit` events when clicked. This can cause confusing validation popups and unintended execution.
**Action:** Explicitly check the `aria-disabled` attribute and call `event.preventDefault()` on both the form's `submit` event and the button's `click` event to prevent unintended execution and native validation popups, while preserving focusability.
17 changes: 16 additions & 1 deletion app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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('입력값을 올바르게 수정해야 저장할 수 있습니다.');
return;
}
renderDraftValidation.flush();
saveEditor();
});
Expand Down Expand Up @@ -822,6 +827,12 @@ function renderEditorRow(anchorId) {
saveButton.textContent = '저장';
saveButton.title = '저장 (Enter)';
saveButton.setAttribute('aria-keyshortcuts', 'Enter');
saveButton.addEventListener('click', (event) => {
if (saveButton.getAttribute('aria-disabled') === 'true') {
event.preventDefault();
event.target.closest('form').dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
}
Comment on lines +830 to +834

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Immediate corrections remain unsavable

After users correct an invalid field, the click listener reads the old state for 150 ms. An immediate save is silently discarded.

Prompt for agents
The submit button's click guard uses aria-disabled, which is updated by a 150 ms debounced renderEditorValidation call. A user can correct the final error and click Save before that update, causing preventDefault to cancel the valid submission. Avoid using stale rendered ARIA state as the source of truth. Ensure native validation popups remain suppressed while every save attempt validates the current draft and provides feedback; this may require coordinating renderEditorRow, the delegated submit handler in bindTableEvents, and the validation debounce.
Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

});
const cancelButton = document.createElement('button');
cancelButton.type = 'button';
cancelButton.className = 'secondary-button';
Expand Down Expand Up @@ -1068,7 +1079,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');
Comment on lines +1082 to +1085

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📝 Info: Existing disabled assertions still apply

Playwright’s toBeDisabled() recognizes aria-disabled="true". Existing editor validation assertions still exercise the new state.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

}
saveButton.title = errors.length > 0 ? '입력값을 올바르게 수정해야 저장할 수 있습니다.' : '저장 (Enter)';
}

Expand Down
18 changes: 18 additions & 0 deletions tests/e2e/aria-disabled-submit.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { expect, test } from '@playwright/test';

test('aria-disabled save remains focusable and explains why activation is blocked', async ({ page }) => {
await page.goto('./');
await page.getByRole('button', { name: '최상위 작업 추가' }).click();

const saveButton = page.getByRole('button', { name: '저장', exact: true });
await expect(saveButton).toHaveAttribute('aria-disabled', 'true');
await saveButton.focus();
await expect(saveButton).toBeFocused();

await saveButton.click({ force: true });

await expect(page.locator('.editor-panel')).toBeVisible();
await expect(page.locator('#toast')).toHaveText('입력값을 올바르게 수정해야 저장할 수 있습니다.');
await expect(page.locator('#toast')).toHaveAttribute('role', 'status');
await expect(page.locator('#toast')).toHaveClass(/\bshow\b/);
});
24 changes: 24 additions & 0 deletions tests/e2e/mobile-validation.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { test, expect } from '@playwright/test';

test.use({
viewport: { width: 375, height: 812 },
isMobile: true,
hasTouch: true,
});

test('mobile layout renders without breaking and aria-disabled save remains focusable and explains why activation is blocked', async ({ page }) => {
await page.goto('./');
await page.getByRole('button', { name: '최상위 작업 추가' }).click();

const saveButton = page.getByRole('button', { name: '저장', exact: true });
await expect(saveButton).toHaveAttribute('aria-disabled', 'true');
await saveButton.focus();
await expect(saveButton).toBeFocused();

await saveButton.click({ force: true });

await expect(page.locator('.editor-panel')).toBeVisible();
await expect(page.locator('#toast')).toHaveText('입력값을 올바르게 수정해야 저장할 수 있습니다.');
await expect(page.locator('#toast')).toHaveAttribute('role', 'status');
await expect(page.locator('#toast')).toHaveClass(/\bshow\b/);
});
Loading