Skip to content
Closed
7 changes: 7 additions & 0 deletions .Jules/palette.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,10 @@
## 2024-07-14 - Native Keyboard Submission with Forms for Modals
**Learning:** Modals designed with plain `<div>` elements as wrappers instead of `<form>` lack native keyboard submission support, forcing users to switch from keyboard to mouse to confirm actions like "Save".
**Action:** When designing modals or popups containing inputs, always use a `<form>` element to wrap the content, handle the `onSubmit` event (calling `e.preventDefault()`), and set the primary confirmation button to `type="submit"` to enable seamless Enter-key submission for keyboard users.
## 2026-06-30 - Dynamically Disabled Buttons with Explanatory Hints
**Learning:** Using the native `disabled` attribute on buttons removes them from the tab sequence. This makes it impossible for keyboard or screen reader users to reach the button and discover any attached explanatory hints (like `aria-describedby` explaining why it's disabled).
**Action:** When a dynamically disabled button provides important contextual hints about its disabled state, use `aria-disabled="true"` instead of the native `disabled` attribute. Ensure visual disabled styling (opacity, cursor) is applied and an `onClick={e => e.preventDefault()}` handler is added to prevent submission while maintaining discoverability in the tab sequence.

## 2026-06-30 - Prevent Default Event Bubbling on Disabled Buttons
**Learning:** Using `onClick={e => e.preventDefault()}` on an `aria-disabled` button does not prevent the click event from bubbling up the DOM tree in React. Tests mimicking clicks on the disabled button might still trigger parent handlers, causing assertions like `expect(parentHandler).not.toHaveBeenCalled()` to fail.
**Action:** When creating an `aria-disabled` button and attempting to block activation, use `onClick={e => { e.preventDefault(); e.stopPropagation(); }}` to ensure the event does not bubble up to parent containers.
20 changes: 17 additions & 3 deletions frontend/src/components/modals/ExportModal.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -171,13 +171,27 @@ describe('ExportModal', () => {
expect(screen.getByRole('button', { name: '데이터 사전 Markdown 내보내기' })).toBeDisabled();
});

it('exposes access-control guidance for disabled button', () => {
render(<ExportModal {...baseProps} canCreateShareLink={false} />);
it('keeps access-control guidance focusable while fully blocking activation', async () => {
const parentClick = vi.fn();
render(
<div onClick={parentClick}>
<ExportModal {...baseProps} canCreateShareLink={false} />
</div>
);

expect(screen.getByText('μ ‘κ·Ό κΆŒν•œ κ΄€λ¦¬λŠ” ν”„λ‘œμ νŠΈ κΆŒν•œ μ„€μ •μ—μ„œ μ²˜λ¦¬ν•©λ‹ˆλ‹€.')).toBeInTheDocument();
const accessManagementButton = screen.getByRole('button', { name: 'μ ‘κ·Ό 관리' });
expect(accessManagementButton).toBeDisabled();

// Verify it is functionally disabled but discoverable
expect(accessManagementButton).toHaveAttribute('aria-disabled', 'true');
accessManagementButton.focus();
expect(accessManagementButton).toHaveFocus();

expect(accessManagementButton).toHaveAttribute('aria-describedby', 'share-export-access-hint');
expect(accessManagementButton).not.toHaveAttribute('title');

// Ensure it does not trigger click events or bubble up
fireEvent.click(accessManagementButton);
expect(parentClick).not.toHaveBeenCalled();
});
});
3 changes: 2 additions & 1 deletion frontend/src/components/modals/ExportModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,8 @@ export function ExportModal({
)}
<button
type="button"
disabled
aria-disabled="true"
Comment thread
seonghobae marked this conversation as resolved.
onClick={(e) => { e.preventDefault(); e.stopPropagation(); }}
aria-describedby="share-export-access-hint"
className="exportModal__disabledHintButton"
Comment thread
seonghobae marked this conversation as resolved.
>
Expand Down
Loading