style(FN-1847): unify dialog overlay styling and add accessibility attributes

- Add role=dialog and aria-modal attributes to all modal dialogs for accessibility
- Standardize close button aria-labels across all modals
- Unify overlay backdrop-filter, z-index, and background styles in styles.css
- Add light theme overrides for non-standard overlay backgrounds
- Update AgentListModal tests to reflect new aria attributes
- Add changeset for @gsxdsm/fusion package
This commit is contained in:
Fusion
2026-04-15 20:44:40 -07:00
committed by gsxdsm
parent f2c5ac7b82
commit be8add9ef4
27 changed files with 97 additions and 50 deletions

View File

@@ -180,6 +180,8 @@ export function ActivityLogModal({
onClick={(e) => {
if (e.target === e.currentTarget) onClose();
}}
role="dialog"
aria-modal="true"
data-testid="activity-log-modal-overlay"
>
<div className="modal modal-lg activity-log-modal" data-testid="activity-log-modal">

View File

@@ -286,7 +286,7 @@ export function AgentDetailView({ agentId, projectId, onClose, addToast, onChild
if (isLoading) {
return (
<div className="agent-detail-overlay" onClick={(e) => e.target === e.currentTarget && onClose()}>
<div className="agent-detail-overlay" onClick={(e) => e.target === e.currentTarget && onClose()} role="dialog" aria-modal="true">
<div className="agent-detail-modal">
<div className="agent-detail-loading">
<Loader2 className="animate-spin" size={24} />
@@ -305,7 +305,7 @@ export function AgentDetailView({ agentId, projectId, onClose, addToast, onChild
const health = getHealthStatus();
return (
<div className="agent-detail-overlay" onClick={(e) => e.target === e.currentTarget && onClose()}>
<div className="agent-detail-overlay" onClick={(e) => e.target === e.currentTarget && onClose()} role="dialog" aria-modal="true">
<div className="agent-detail-modal">
{/* Header */}
<div className="agent-detail-header">
@@ -413,7 +413,7 @@ export function AgentDetailView({ agentId, projectId, onClose, addToast, onChild
<button className="btn-icon" onClick={() => void loadAgent()} title="Refresh">
<RefreshCw size={16} />
</button>
<button className="btn-icon" onClick={onClose} title="Close">
<button className="btn-icon" onClick={onClose} aria-label="Close" title="Close">
<X size={20} />
</button>
</div>

View File

@@ -173,7 +173,7 @@ export function AgentGenerationModal({
Generate Agent
</span>
<button
className="btn-icon"
className="modal-close"
onClick={handleCancel}
aria-label="Close"
style={{
@@ -185,7 +185,7 @@ export function AgentGenerationModal({
lineHeight: 1,
}}
>
×
&times;
</button>
</div>

View File

@@ -346,8 +346,8 @@ export function AgentImportModal({ isOpen, onClose, onImported, projectId }: Age
{/* Header */}
<div className="agent-dialog-header">
<span className="agent-dialog-header-title">Import Agents</span>
<button className="btn-icon" onClick={handleClose} aria-label="Close">
×
<button className="modal-close" onClick={handleClose} aria-label="Close">
&times;
</button>
</div>

View File

@@ -176,7 +176,7 @@ export function AgentListModal({ isOpen, onClose, addToast, projectId }: AgentLi
if (!isOpen) return null;
return (
<div className="modal-overlay open" onClick={(e) => e.target === e.currentTarget && onClose()}>
<div className="modal-overlay open" onClick={(e) => e.target === e.currentTarget && onClose()} role="dialog" aria-modal="true">
<div className="modal modal--wide">
<div className="modal-header">
<h2 className="modal-title">
@@ -212,8 +212,8 @@ export function AgentListModal({ isOpen, onClose, addToast, projectId }: AgentLi
>
<RefreshCw size={16} className={isLoading ? "spin" : ""} />
</button>
<button className="btn-icon" onClick={onClose} title="Close">
<X size={20} />
<button className="modal-close" onClick={onClose} aria-label="Close">
&times;
</button>
</div>
</div>

View File

@@ -112,7 +112,7 @@ export function ChangesDiffModal({
const isDone = column === "done";
return (
<div className="modal-overlay open" onClick={onClose}>
<div className="modal-overlay open" onClick={onClose} role="dialog" aria-modal="true">
<div
className="modal changes-diff-modal"
onClick={(e) => e.stopPropagation()}
@@ -171,7 +171,7 @@ export function ChangesDiffModal({
Refresh
</button>
)}
<button className="modal-close" onClick={onClose}>
<button className="modal-close" onClick={onClose} aria-label="Close">
<X size={20} />
</button>
</div>

View File

@@ -75,7 +75,7 @@ function NewChatDialog({ onClose, onCreate }: NewChatDialogProps) {
};
return (
<div className="chat-new-dialog-backdrop" onClick={onClose}>
<div className="chat-new-dialog-backdrop" onClick={onClose} role="dialog" aria-modal="true">
<div className="chat-new-dialog" onClick={(e) => e.stopPropagation()}>
<h3>New Chat</h3>
<form onSubmit={handleSubmit}>

View File

@@ -167,7 +167,7 @@ export function FileBrowserModal({
};
return (
<div className="modal-overlay open" onClick={onClose}>
<div className="modal-overlay open" onClick={onClose} role="dialog" aria-modal="true">
<div className="modal file-browser-modal" onClick={(e) => e.stopPropagation()}>
<div className="modal-header file-browser-modal-header">
<div className="file-browser-header-title">
@@ -186,7 +186,7 @@ export function FileBrowserModal({
workspaces={workspaces}
onSelect={handleWorkspaceSelect}
/>
<button className="modal-close" onClick={onClose}>
<button className="modal-close" onClick={onClose} aria-label="Close">
<X size={20} />
</button>
</div>

View File

@@ -346,7 +346,7 @@ export function GitHubImportModal({ isOpen, onClose, onImport, tasks, projectId
const showInlineErrorBanner = activeTab === "issues" ? showIssuesError : showPullsError;
return (
<div className="modal-overlay open" onClick={(e) => e.target === e.currentTarget && onClose()}>
<div className="modal-overlay open" onClick={(e) => e.target === e.currentTarget && onClose()} role="dialog" aria-modal="true">
<div className="modal modal-lg github-import-modal">
<div className="modal-header github-import-modal__header">
<div>

View File

@@ -675,7 +675,7 @@ export function GitManagerModal({ isOpen, onClose, tasks, addToast, projectId }:
if (!isOpen) return null;
return (
<div className="modal-overlay open" onClick={(e) => e.target === e.currentTarget && onClose()}>
<div className="modal-overlay open" onClick={(e) => e.target === e.currentTarget && onClose()} role="dialog" aria-modal="true">
<div className="modal gm-modal" ref={modalRef}>
<div className="modal-header">
<h3>
@@ -691,7 +691,7 @@ export function GitManagerModal({ isOpen, onClose, tasks, addToast, projectId }:
>
<RefreshCw size={14} className={loading ? "spin" : ""} />
</button>
<button className="modal-close" onClick={onClose}>
<button className="modal-close" onClick={onClose} aria-label="Close">
<X size={18} />
</button>
</div>

View File

@@ -271,6 +271,8 @@ export function MailboxModal({
onClick={(e) => {
if (e.target === e.currentTarget) onClose();
}}
role="dialog"
aria-modal="true"
data-testid="mailbox-modal-overlay"
>
<div className="modal modal-lg mailbox-modal" data-testid="mailbox-modal">

View File

@@ -448,6 +448,8 @@ export function MilestoneSliceInterviewModal({
<div
className="modal-overlay open"
onClick={(e) => e.target === e.currentTarget && handleCancel()}
role="dialog"
aria-modal="true"
data-testid="milestone-slice-interview-modal"
>
<div className="modal modal-lg planning-modal">

View File

@@ -684,7 +684,7 @@ export function MissionInterviewModal({
if (!isOpen) return null;
return (
<div className="modal-overlay open" onClick={(e) => e.target === e.currentTarget && handleCancel()}>
<div className="modal-overlay open" onClick={(e) => e.target === e.currentTarget && handleCancel()} role="dialog" aria-modal="true">
<div className="modal modal-lg planning-modal">
<div className="modal-header">
<div className="detail-title-row">

View File

@@ -3481,6 +3481,8 @@ export function MissionManager({ isOpen, isInline = false, onClose, addToast, pr
className="mission-manager-overlay open"
onClick={(e) => e.target === e.currentTarget && onClose()}
data-testid="mission-manager-overlay"
role="dialog"
aria-modal="true"
>
{manager}
</div>

View File

@@ -137,7 +137,7 @@ export function ModelSelectionModal({
const hasValidatorOverride = Boolean(validatorValue);
return (
<div className="modal-overlay open" onClick={handleOverlayClick} data-testid="model-selection-modal">
<div className="modal-overlay open" onClick={handleOverlayClick} role="dialog" aria-modal="true" data-testid="model-selection-modal">
<div className="modal modal-lg">
<div className="modal-header">
<div className="detail-title-row">

View File

@@ -208,14 +208,14 @@ export function NewTaskModal({ isOpen, onClose, projectId, tasks, onCreateTask,
if (!isOpen) return null;
return (
<div className="modal-overlay open" onClick={handleClose} onKeyDown={handleKeyDown}>
<div
className="modal modal-lg new-task-modal"
<div className="modal-overlay open" onClick={handleClose} onKeyDown={handleKeyDown} role="dialog" aria-modal="true">
<div
className="modal modal-lg new-task-modal"
onClick={(e) => e.stopPropagation()}
>
<div className="modal-header">
<h3>New Task</h3>
<button className="modal-close" onClick={handleClose} disabled={isSubmitting}>
<button className="modal-close" onClick={handleClose} disabled={isSubmitting} aria-label="Close">
&times;
</button>
</div>

View File

@@ -717,7 +717,7 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
if (!isOpen) return null;
return (
<div className="modal-overlay open" onClick={(e) => e.target === e.currentTarget && handleCancel()}>
<div className="modal-overlay open" onClick={(e) => e.target === e.currentTarget && handleCancel()} role="dialog" aria-modal="true">
<div className="modal modal-lg planning-modal">
<div className="modal-header">
<div className="detail-title-row">

View File

@@ -465,7 +465,7 @@ export function ScheduledTasksModal({ onClose, addToast, projectId }: ScheduledT
return (
<div className="modal-overlay open" onClick={handleOverlayClick}>
<div className="modal modal-lg" role="dialog" aria-labelledby="schedules-modal-title">
<div className="modal modal-lg" role="dialog" aria-modal="true" aria-labelledby="schedules-modal-title">
<div className="modal-header">
<h3 id="schedules-modal-title">Scheduled Tasks</h3>
<div className="modal-header-actions">

View File

@@ -172,6 +172,7 @@ export function ScriptsModal({ isOpen, onClose, addToast, projectId, onRunScript
className="modal scripts-modal"
onClick={(e) => e.stopPropagation()}
role="dialog"
aria-modal="true"
aria-label="Scripts"
>
{/* Header */}
@@ -180,8 +181,8 @@ export function ScriptsModal({ isOpen, onClose, addToast, projectId, onRunScript
<Terminal size={18} style={{ marginRight: "8px", verticalAlign: "middle" }} />
Scripts
</h2>
<button className="btn-icon" onClick={onClose} aria-label="Close">
<X size={16} />
<button className="modal-close" onClick={onClose} aria-label="Close">
&times;
</button>
</div>

View File

@@ -2636,11 +2636,11 @@ export function SettingsModal({
};
return (
<div className="modal-overlay open" onClick={handleOverlayClick}>
<div className="modal-overlay open" onClick={handleOverlayClick} role="dialog" aria-modal="true">
<div className="modal modal-lg">
<div className="modal-header">
<h3>Settings</h3>
<button className="modal-close" onClick={onClose}>
<button className="modal-close" onClick={onClose} aria-label="Close">
&times;
</button>
</div>
@@ -2726,11 +2726,11 @@ export function SettingsModal({
{/* Import Confirmation Dialog */}
{importDialogOpen && importPreview && (
<div className="modal-overlay open" onClick={(e) => e.target === e.currentTarget && setImportDialogOpen(false)}>
<div className="modal-overlay open" onClick={(e) => e.target === e.currentTarget && setImportDialogOpen(false)} role="dialog" aria-modal="true">
<div className="modal modal-md">
<div className="modal-header">
<h3>Import Settings</h3>
<button className="modal-close" onClick={() => setImportDialogOpen(false)}>
<button className="modal-close" onClick={() => setImportDialogOpen(false)} aria-label="Close">
&times;
</button>
</div>

View File

@@ -515,7 +515,7 @@ export function SubtaskBreakdownModal({ isOpen, onClose, initialDescription, onT
if (!isOpen) return null;
return (
<div className="modal-overlay open" onClick={(event) => event.target === event.currentTarget && void handleClose()}>
<div className="modal-overlay open" onClick={(event) => event.target === event.currentTarget && void handleClose()} role="dialog" aria-modal="true">
<div className="modal modal-lg planning-modal">
<div className="modal-header">
<div className="detail-title-row">

View File

@@ -1037,7 +1037,7 @@ export function TaskDetailModal({
const prAutomationLabel = task.status ? prAutomationStatusLabels[task.status] : undefined;
return (
<div className="modal-overlay open" onClick={handleOverlayClick}>
<div className="modal-overlay open" onClick={handleOverlayClick} role="dialog" aria-modal="true">
<div className="modal modal-lg" onDragOver={handleDragOver} onDrop={handleDrop}>
<div className="modal-header">
<div className="detail-title-row">
@@ -1057,7 +1057,7 @@ export function TaskDetailModal({
<Pencil size={14} />
</button>
)}
<button className="modal-close" onClick={onClose}>
<button className="modal-close" onClick={onClose} aria-label="Close">
&times;
</button>
</div>
@@ -1798,6 +1798,8 @@ export function TaskDetailModal({
<div
className="modal-overlay open detail-refine-overlay"
onClick={handleCloseRefineModal}
role="dialog"
aria-modal="true"
>
<div
className="modal detail-refine-modal"
@@ -1805,7 +1807,7 @@ export function TaskDetailModal({
>
<div className="modal-header">
<h3 className="detail-refine-title">Refine</h3>
<button className="modal-close" onClick={handleCloseRefineModal}>
<button className="modal-close" onClick={handleCloseRefineModal} aria-label="Close">
&times;
</button>
</div>

View File

@@ -779,6 +779,8 @@ export function TerminalModal({ isOpen, onClose, initialCommand, projectId }: Te
<div
className="modal-overlay open"
onClick={handleOverlayClick}
role="dialog"
aria-modal="true"
data-testid="terminal-modal-overlay"
style={
keyboardOverlap > 0

View File

@@ -373,13 +373,14 @@ export function WorkflowStepManager({ isOpen, onClose, addToast, projectId }: Wo
className="modal workflow-step-manager-modal"
onClick={(e) => e.stopPropagation()}
role="dialog"
aria-modal="true"
aria-label="Workflow Steps"
>
{/* Header */}
<div className="modal-header">
<h2>Workflow Steps</h2>
<button className="btn-icon" onClick={onClose} aria-label="Close">
<X size={16} />
<button className="modal-close" onClick={onClose} aria-label="Close">
&times;
</button>
</div>

View File

@@ -130,11 +130,11 @@ describe("AgentListModal", () => {
);
await waitFor(() => {
const closeButton = screen.getByTitle("Close");
const closeButton = screen.getByRole("button", { name: "Close" });
expect(closeButton).toBeTruthy();
});
const closeButton = screen.getByTitle("Close");
const closeButton = screen.getByRole("button", { name: "Close" });
fireEvent.click(closeButton);
expect(mockOnClose).toHaveBeenCalled();
});
@@ -1412,7 +1412,7 @@ describe("AgentListModal", () => {
});
// Close via close button
fireEvent.click(screen.getByTitle("Close"));
fireEvent.click(screen.getByRole("button", { name: "Close" }));
expect(mockOnClose).toHaveBeenCalledTimes(1);
// Unmount and verify closed state works