Skip to content
Open
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
3 changes: 3 additions & 0 deletions .Jules/palette.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,6 @@
## 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-07-20 - Disabled Button Accessibility with `aria-describedby`
**Learning:** Disabled buttons often rely on `title` attributes for explanations. However, many browsers do not expose tooltips for disabled controls, leaving screen reader users without context. Furthermore, dynamically mapping artifact descriptions to `aria-describedby` requires careful ID generation (e.g., stripping spaces) to ensure proper resolution.
**Action:** Always pair disabled buttons with visible helper text and link them using `aria-describedby` instead of relying on `title`. Ensure test suites (like `ExportModal.test.tsx`) are updated to explicitly verify the `aria-describedby` attribute and its target ID.
22 changes: 15 additions & 7 deletions frontend/src/components/modals/ExportModal.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -155,13 +155,21 @@ describe('ExportModal', () => {
);

expect(screen.getAllByText('먼저 테이블을 추가하세요')).toHaveLength(7);
expect(screen.getByRole('button', { name: 'SQL DDL 복사' })).toBeDisabled();
expect(screen.getByRole('button', { name: 'SVG 이미지 내보내기' })).toBeDisabled();
expect(screen.getByRole('button', { name: 'PlantUML 내보내기' })).toBeDisabled();
expect(screen.getByRole('button', { name: 'Mermaid 내보내기' })).toBeDisabled();
expect(screen.getByRole('button', { name: 'DBML 내보내기' })).toBeDisabled();
expect(screen.getByRole('button', { name: '데이터 사전 CSV 내보내기' })).toBeDisabled();
expect(screen.getByRole('button', { name: '데이터 사전 Markdown 내보내기' })).toBeDisabled();

const checkDisabledExportButton = (name: string, artifactLabel: string) => {
const button = screen.getByRole('button', { name });
expect(button).toBeDisabled();
const descId = `artifact-desc-${artifactLabel.replace(/\s+/g, '-')}`;
expect(button).toHaveAttribute('aria-describedby', descId);
};

checkDisabledExportButton('SQL DDL 복사', 'SQL DDL');
checkDisabledExportButton('SVG 이미지 내보내기', 'SVG 이미지');
checkDisabledExportButton('PlantUML 내보내기', 'PlantUML');
checkDisabledExportButton('Mermaid 내보내기', 'Mermaid');
checkDisabledExportButton('DBML 내보내기', 'DBML');
checkDisabledExportButton('데이터 사전 CSV 내보내기', 'Data Dictionary CSV');
checkDisabledExportButton('데이터 사전 Markdown 내보내기', 'Data Dictionary MD');
Comment on lines +159 to +172
});

it('exposes access-control guidance for disabled button', () => {
Expand Down
36 changes: 20 additions & 16 deletions frontend/src/components/modals/ExportModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -212,23 +212,27 @@ export function ExportModal({
</p>

<div className="exportModal__artifactList">
{artifacts.map((artifact) => (
<div className="exportModal__artifactRow" key={artifact.label}>
<div>
<strong>{artifact.label}</strong>
<span>{artifact.description}</span>
{artifacts.map((artifact) => {
const descId = `artifact-desc-${artifact.label.replace(/\s+/g, '-')}`;
return (
<div className="exportModal__artifactRow" key={artifact.label}>
<div>
<strong>{artifact.label}</strong>
<span id={descId}>{artifact.description}</span>
</div>
<button
type="button"
onClick={artifact.onExport}
disabled={artifact.disabled}
aria-label={artifact.ariaLabel}
aria-live={artifact.label === 'SQL DDL' ? 'polite' : undefined}
aria-describedby={artifact.disabled ? descId : undefined}
>
{artifact.buttonLabel}
</button>
</div>
<button
type="button"
onClick={artifact.onExport}
disabled={artifact.disabled}
aria-label={artifact.ariaLabel}
aria-live={artifact.label === 'SQL DDL' ? 'polite' : undefined}
>
{artifact.buttonLabel}
</button>
</div>
))}
);
})}
</div>
</section>
</div>
Expand Down
Loading