diff --git a/.Jules/palette.md b/.Jules/palette.md index 32fb11be..3bc5e311 100644 --- a/.Jules/palette.md +++ b/.Jules/palette.md @@ -43,3 +43,6 @@ ## 2024-06-26 - [Abbreviation Comprehension in ERD Nodes] **Learning:** Users without deep database administration backgrounds may not immediately recognize domain-specific abbreviations like "PK" or "FK" rendered as minimalist badges inside dense ERD nodes. **Action:** Always provide `title` attributes on technical acronym badges (like Primary Key / Foreign Key) to ensure clarity and improve accessibility without cluttering the space-constrained node UI. +## 2026-07-04 - Focus Trap Anti-Pattern in Non-Interactive Badges +**Learning:** Adding `tabIndex={0}` to static, non-interactive text badges (like truncated text spans or abbreviation `abbr` tags) just to expose their native `title` or `aria-label` to keyboard users creates an accessibility anti-pattern. This significantly bloats the page's tab sequence, degrading keyboard navigation and frustrating screen reader users who expect interactive elements upon focus. +**Action:** Do not use `tabIndex={0}` as a shortcut for making non-interactive tooltips accessible. Truncated static text or badges should be paired with proper `aria-label` attributes without adding them to the focus order unless they are actionable. diff --git a/frontend/src/erd/TableNode.tsx b/frontend/src/erd/TableNode.tsx index f80d8488..b0eacdf3 100644 --- a/frontend/src/erd/TableNode.tsx +++ b/frontend/src/erd/TableNode.tsx @@ -54,7 +54,6 @@ function AccessibleTruncatedText({ className={className} title={title ?? accessibleText} aria-label={accessibleText} - tabIndex={0} > {children ?? text} @@ -116,10 +115,22 @@ function TableNode(props: NodeProps) { /> ) : null} {data.badges?.pk ? ( - PK + + PK + ) : null} {data.badges?.fk ? ( - FK + + FK + ) : null} @@ -153,12 +164,20 @@ function TableNode(props: NodeProps) { {c.data_type} {c.is_pk ? ( - + PK ) : null} {c.is_not_null ? ( - + NOT NULL ) : null} @@ -176,13 +195,16 @@ function TableNode(props: NodeProps) { className="tableNode__more" title="생략된 컬럼이 더 있습니다" aria-label="생략된 컬럼이 더 있습니다" - tabIndex={0} > … {data.columns.length - MAX_RENDERED_COLUMNS} more ) : null} {data.indexes?.length ? ( -
+
Indexes
{data.indexes.slice(0, 4).map((index) => { const columnsText = `(${index.columns.join(", ")})`; @@ -207,7 +229,6 @@ function TableNode(props: NodeProps) { className="tableNode__more" title="생략된 인덱스가 더 있습니다" aria-label="생략된 인덱스가 더 있습니다" - tabIndex={0} > … {data.indexes.length - 4} more indexes
diff --git a/frontend/src/erd/__tests__/TableNode.test.tsx b/frontend/src/erd/__tests__/TableNode.test.tsx index 8e8f65a9..2c217b8e 100644 --- a/frontend/src/erd/__tests__/TableNode.test.tsx +++ b/frontend/src/erd/__tests__/TableNode.test.tsx @@ -33,7 +33,7 @@ describe('TableNode', () => { expect(screen.getByText('Core')).toBeInTheDocument(); }); - it('exposes truncated metadata to keyboard and assistive technology users', () => { + it('exposes truncated metadata to assistive technology users', () => { const data = { title: 'public.users', comment: 'Stores application users with long operational notes', @@ -73,12 +73,12 @@ describe('TableNode', () => { ]) { const item = screen.getByLabelText(name); expect(item).toHaveAttribute('title', name); - expect(item).toHaveAttribute('tabindex', '0'); + expect(item).not.toHaveAttribute('tabindex', '0'); } const indexName = screen.getByLabelText('idx_users_email_unique_long_name'); expect(indexName).toHaveAttribute('title', 'Access method: btree'); - expect(indexName).toHaveAttribute('tabindex', '0'); + expect(indexName).not.toHaveAttribute('tabindex', '0'); const [notNullBadge] = screen.getAllByLabelText('필수 입력 (Not Null)'); expect(notNullBadge).toHaveAttribute('title', 'Not Null');