diff --git a/.jules/palette.md b/.jules/palette.md index d892e2f9..ed7f6bc2 100644 --- a/.jules/palette.md +++ b/.jules/palette.md @@ -54,3 +54,6 @@ ## 2024-07-13 - [Table Node Accessibility] **Learning:** Adding `aria-hidden="true"` inside `abbr` elements with `aria-label` prevents screen readers from redundantly announcing short abbreviations like "PK" or "NN" along with their full label. **Action:** When creating short, domain-specific abbreviations with tooltips, use `aria-label` on the wrapper and hide the visual text from screen readers using `aria-hidden="true"` to create a cleaner auditory experience. +## 2026-07-27 - [Descriptive ARIA labels for modal buttons] +**Learning:** Adding dynamic, contextual `aria-label` attributes to generic buttons (like Save, Cancel, Delete) inside modals significantly improves screen reader navigation and clarity, ensuring users understand exactly what action they are taking. +**Action:** Always provide contextual `aria-label`s for generic action buttons in dialogs using dynamic data like node or edge names. diff --git a/frontend/src/components/modals/EditEdgeModal.tsx b/frontend/src/components/modals/EditEdgeModal.tsx index c34b7e55..b3a3ae65 100644 --- a/frontend/src/components/modals/EditEdgeModal.tsx +++ b/frontend/src/components/modals/EditEdgeModal.tsx @@ -83,14 +83,16 @@ export function EditEdgeModal({ type="button" onClick={onRelDelete} style={{ color: "#b91c1c", borderColor: "#fca5a5" }} + aria-label={`관계 삭제 (${editingEdge.source}에서 ${editingEdge.target})`} > 삭제
- + diff --git a/frontend/src/components/modals/EditTableModal.test.tsx b/frontend/src/components/modals/EditTableModal.test.tsx index 1a8c9af5..80244919 100644 --- a/frontend/src/components/modals/EditTableModal.test.tsx +++ b/frontend/src/components/modals/EditTableModal.test.tsx @@ -170,7 +170,7 @@ describe('EditTableModal', () => { render(); const user = userEvent.setup(); - await user.click(screen.getByRole('button', { name: '복제' })); + await user.click(screen.getByRole('button', { name: 'test_table 테이블 복제' })); expect(setNodesMock).toHaveBeenCalled(); expect(onEditTableCancelMock).toHaveBeenCalled(); diff --git a/frontend/src/components/modals/EditTableModal.tsx b/frontend/src/components/modals/EditTableModal.tsx index 998929d4..e3f58758 100644 --- a/frontend/src/components/modals/EditTableModal.tsx +++ b/frontend/src/components/modals/EditTableModal.tsx @@ -191,6 +191,7 @@ export function EditTableModal({ type="button" onClick={onDeleteTable} style={{ color: "#b91c1c", borderColor: "#fca5a5" }} + aria-label={`${editingNode.data.title} 테이블 삭제`} > 테이블 삭제 @@ -218,16 +219,18 @@ export function EditTableModal({ onEditTableCancel(); }} style={{ color: "#034ea2", borderColor: "#93c5fd" }} + aria-label={`${editingNode.data.title} 테이블 복제`} > 복제
- + diff --git a/frontend/src/components/modals/ModalCoverage.test.tsx b/frontend/src/components/modals/ModalCoverage.test.tsx index aa9dac53..9d1229b1 100644 --- a/frontend/src/components/modals/ModalCoverage.test.tsx +++ b/frontend/src/components/modals/ModalCoverage.test.tsx @@ -116,9 +116,9 @@ describe('modal behavior coverage', () => { fireEvent.change(screen.getByLabelText('제약조건 이름 (Label)'), { target: { value: 'fk_changed' }, }) - fireEvent.click(screen.getByRole('button', { name: '삭제' })) - fireEvent.click(screen.getByRole('button', { name: '취소' })) - fireEvent.click(screen.getByRole('button', { name: '저장' })) + fireEvent.click(screen.getByRole('button', { name: '관계 삭제 (a에서 b)' })) + fireEvent.click(screen.getByRole('button', { name: '관계 편집 취소' })) + fireEvent.click(screen.getByRole('button', { name: '관계 저장 (a에서 b)' })) expect(setRelLabel).toHaveBeenCalledWith('fk_changed') expect(onDelete).toHaveBeenCalledOnce() expect(onCancel).toHaveBeenCalledOnce() @@ -181,8 +181,8 @@ describe('modal behavior coverage', () => { expect(deleteEditing(tableNode)?.data.columns).toHaveLength(1) fireEvent.submit(document.getElementById('editTableForm')!) - fireEvent.click(screen.getByRole('button', { name: '테이블 삭제' })) - fireEvent.click(screen.getByRole('button', { name: '복제' })) + fireEvent.click(screen.getByRole('button', { name: 'public.users 테이블 삭제' })) + fireEvent.click(screen.getByRole('button', { name: 'public.users 테이블 복제' })) const duplicate = setNodes.mock.calls[2]?.[0] as (nodes: Node[]) => Node[] const duplicated = duplicate([tableNode])[1]! expect(duplicated).toMatchObject({ @@ -191,7 +191,7 @@ describe('modal behavior coverage', () => { data: { title: 'public.users_copy' }, }) expect(duplicated.data.columns).not.toBe(tableNode.data.columns) - fireEvent.click(screen.getByRole('button', { name: '취소' })) + fireEvent.click(screen.getByRole('button', { name: '테이블 편집 취소' })) fireEvent.click(screen.getByRole('button', { name: '닫기' })) expect(onSubmit).toHaveBeenCalledOnce() expect(onDeleteTable).toHaveBeenCalledOnce()