diff --git a/.jules/palette.md b/.jules/palette.md
index bd0f73248..231c92c67 100644
--- a/.jules/palette.md
+++ b/.jules/palette.md
@@ -57,3 +57,7 @@
## 2026-07-30 - Add window.confirm for destructive actions
**Learning:** Destructive actions like deleting groups and edge relationships previously occurred immediately without user confirmation.
**Action:** Always wrap delete operations with window.confirm() dialogs and ensure corresponding tests successfully mock window.confirm.
+
+## 2026-08-01 - Focusable aria-disabled buttons for screen readers
+**Learning:** Native `disabled` attributes on buttons remove them from the tab order, preventing screen reader users from discovering them and hearing their `aria-describedby` helper text explaining *why* they are disabled.
+**Action:** When a disabled button has important explanatory context (like requiring project permissions), use `aria-disabled="true"` with custom styling (opacity and cursor) and manually prevent action (e.g., `e.preventDefault()`) instead of the native `disabled` attribute to keep it discoverable via keyboard navigation.
diff --git a/frontend/src/components/modals/ExportModal.test.tsx b/frontend/src/components/modals/ExportModal.test.tsx
index bd89c5384..4ab429039 100644
--- a/frontend/src/components/modals/ExportModal.test.tsx
+++ b/frontend/src/components/modals/ExportModal.test.tsx
@@ -1,5 +1,6 @@
import '@testing-library/jest-dom/vitest';
import { cleanup, fireEvent, render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { ExportModal } from './ExportModal';
@@ -171,13 +172,23 @@ describe('ExportModal', () => {
expect(screen.getByRole('button', { name: '데이터 사전 Markdown 내보내기' })).toBeDisabled();
});
- it('exposes access-control guidance for disabled button', () => {
+ it('exposes access-control guidance for disabled button', async () => {
+ const user = userEvent.setup();
render();
expect(screen.getByText('접근 권한 관리는 프로젝트 권한 설정에서 처리합니다.')).toBeInTheDocument();
const accessManagementButton = screen.getByRole('button', { name: '접근 관리' });
- expect(accessManagementButton).toBeDisabled();
+ expect(accessManagementButton).toHaveAttribute('aria-disabled', 'true');
expect(accessManagementButton).toHaveAttribute('aria-describedby', 'share-export-access-hint');
expect(accessManagementButton).not.toHaveAttribute('title');
+
+ accessManagementButton.focus();
+ expect(accessManagementButton).toHaveFocus();
+
+ // Attempting to "click" or submit via keyboard should not trigger any action
+ // (though there's no specific prop to mock here, verifying it doesn't throw or navigate is sufficient,
+ // and we simulate the interaction to ensure the preventDefault runs)
+ await user.keyboard('{Enter}');
+ expect(accessManagementButton).toHaveFocus();
});
});
diff --git a/frontend/src/components/modals/ExportModal.tsx b/frontend/src/components/modals/ExportModal.tsx
index 995393ceb..9ccda8ad9 100644
--- a/frontend/src/components/modals/ExportModal.tsx
+++ b/frontend/src/components/modals/ExportModal.tsx
@@ -202,7 +202,8 @@ export function ExportModal({
)}