diff options
| author | Nolan Darilek <[email protected]> | 2026-01-22 05:10:53 -0600 |
|---|---|---|
| committer | GitHub <[email protected]> | 2026-01-22 05:10:53 -0600 |
| commit | 3435327bc074a7ba8c3fe8939c97de54bbdefd65 (patch) | |
| tree | d906b07c16e8e90808e5f78b72f72e8737f53eeb /packages/app/src | |
| parent | 8a043edfd5a504421301cc183eedf4ecdb4c57b0 (diff) | |
| download | opencode-3435327bc074a7ba8c3fe8939c97de54bbdefd65.tar.gz opencode-3435327bc074a7ba8c3fe8939c97de54bbdefd65.zip | |
fix(app): session screen accessibility improvements (#9907)
Diffstat (limited to 'packages/app/src')
11 files changed, 96 insertions, 59 deletions
diff --git a/packages/app/src/components/dialog-connect-provider.tsx b/packages/app/src/components/dialog-connect-provider.tsx index 4ec4c8daa..d8d4ad9c2 100644 --- a/packages/app/src/components/dialog-connect-provider.tsx +++ b/packages/app/src/components/dialog-connect-provider.tsx @@ -143,7 +143,7 @@ export function DialogConnectProvider(props: { provider: string }) { } return ( - <Dialog title={<IconButton tabIndex={-1} icon="arrow-left" variant="ghost" onClick={goBack} />}> + <Dialog title={<IconButton tabIndex={-1} icon="arrow-left" variant="ghost" onClick={goBack} aria-label="Go back" />}> <div class="flex flex-col gap-6 px-2.5 pb-3"> <div class="px-2.5 flex gap-4 items-center"> <ProviderIcon id={props.provider as IconName} class="size-5 shrink-0 icon-strong-base" /> diff --git a/packages/app/src/components/dialog-edit-project.tsx b/packages/app/src/components/dialog-edit-project.tsx index acf146ef5..cf5535a67 100644 --- a/packages/app/src/components/dialog-edit-project.tsx +++ b/packages/app/src/components/dialog-edit-project.tsx @@ -193,6 +193,8 @@ export function DialogEditProject(props: { project: LocalProject }) { {(color) => ( <button type="button" + aria-label={`Select ${color} color`} + aria-pressed={store.color === color} classList={{ "flex items-center justify-center size-10 p-0.5 rounded-lg overflow-hidden transition-colors cursor-default": true, "bg-transparent border-2 border-icon-strong-base hover:bg-surface-base-hover": diff --git a/packages/app/src/components/dialog-select-model.tsx b/packages/app/src/components/dialog-select-model.tsx index 4fd8af1b3..5569d7780 100644 --- a/packages/app/src/components/dialog-select-model.tsx +++ b/packages/app/src/components/dialog-select-model.tsx @@ -1,5 +1,5 @@ import { Popover as Kobalte } from "@kobalte/core/popover" -import { Component, createMemo, createSignal, JSX, Show } from "solid-js" +import { Component, ComponentProps, createMemo, createSignal, JSX, Show, ValidComponent } from "solid-js" import { useLocal } from "@/context/local" import { useDialog } from "@opencode-ai/ui/context/dialog" import { popularProviders } from "@/hooks/use-providers" @@ -86,10 +86,12 @@ const ModelList: Component<{ ) } -export const ModelSelectorPopover: Component<{ +export function ModelSelectorPopover<T extends ValidComponent = "div">(props: { provider?: string - children: JSX.Element -}> = (props) => { + children?: JSX.Element + triggerAs?: T + triggerProps?: ComponentProps<T> +}) { const [open, setOpen] = createSignal(false) const dialog = useDialog() @@ -101,7 +103,9 @@ export const ModelSelectorPopover: Component<{ return ( <Kobalte open={open()} onOpenChange={setOpen} placement="top-start" gutter={8}> - <Kobalte.Trigger as="div">{props.children}</Kobalte.Trigger> + <Kobalte.Trigger as={props.triggerAs ?? "div"} {...(props.triggerProps as any)}> + {props.children} + </Kobalte.Trigger> <Kobalte.Portal> <Kobalte.Content class="w-72 h-80 flex flex-col rounded-md border border-border-base bg-surface-raised-stronger-non-alpha shadow-md z-50 outline-none overflow-hidden"> <Kobalte.Title class="sr-only">{language.t("dialog.model.select.title")}</Kobalte.Title> diff --git a/packages/app/src/components/dialog-select-server.tsx b/packages/app/src/components/dialog-select-server.tsx index bb0ad5b43..a8e7ca4f7 100644 --- a/packages/app/src/components/dialog-select-server.tsx +++ b/packages/app/src/components/dialog-select-server.tsx @@ -158,6 +158,7 @@ export function DialogSelectServer() { icon="circle-x" variant="ghost" class="bg-transparent transition-opacity shrink-0 hover:scale-110" + aria-label="Remove server" onClick={(e) => { e.stopPropagation() handleRemove(i) diff --git a/packages/app/src/components/prompt-input.tsx b/packages/app/src/components/prompt-input.tsx index 3bd7856ee..825f2b116 100644 --- a/packages/app/src/components/prompt-input.tsx +++ b/packages/app/src/components/prompt-input.tsx @@ -1487,6 +1487,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => { variant="ghost" class="h-6 w-6" onClick={() => prompt.context.removeActive()} + aria-label="Remove active file from context" /> </div> )} @@ -1524,6 +1525,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => { variant="ghost" class="h-6 w-6" onClick={() => prompt.context.remove(item.key)} + aria-label="Remove file from context" /> </div> )} @@ -1556,6 +1558,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => { type="button" onClick={() => removeImageAttachment(attachment.id)} class="absolute -top-1.5 -right-1.5 size-5 rounded-full bg-surface-raised-stronger-non-alpha border border-border-base flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity hover:bg-surface-raised-base-hover" + aria-label="Remove attachment" > <Icon name="close" class="size-3 text-text-weak" /> </button> @@ -1574,6 +1577,13 @@ export const PromptInput: Component<PromptInputProps> = (props) => { editorRef = el props.ref?.(el) }} + role="textbox" + aria-multiline="true" + aria-label={ + store.mode === "shell" + ? language.t("prompt.placeholder.shell") + : language.t("prompt.placeholder.normal", { example: language.t(EXAMPLES[store.placeholder]) }) + } contenteditable="true" onInput={handleInput} onPaste={handlePaste} @@ -1638,21 +1648,22 @@ export const PromptInput: Component<PromptInputProps> = (props) => { </TooltipKeybind> } > - <ModelSelectorPopover> - <TooltipKeybind - placement="top" - title={language.t("command.model.choose")} - keybind={command.keybind("model.choose")} + <TooltipKeybind + placement="top" + title={language.t("command.model.choose")} + keybind={command.keybind("model.choose")} + > + <ModelSelectorPopover + triggerAs={Button} + triggerProps={{ variant: "ghost" }} > - <Button as="div" variant="ghost"> - <Show when={local.model.current()?.provider?.id}> - <ProviderIcon id={local.model.current()!.provider.id as IconName} class="size-4 shrink-0" /> - </Show> - {local.model.current()?.name ?? language.t("dialog.model.select.title")} - <Icon name="chevron-down" size="small" /> - </Button> - </TooltipKeybind> - </ModelSelectorPopover> + <Show when={local.model.current()?.provider?.id}> + <ProviderIcon id={local.model.current()!.provider.id as IconName} class="size-4 shrink-0" /> + </Show> + {local.model.current()?.name ?? language.t("dialog.model.select.title")} + <Icon name="chevron-down" size="small" /> + </ModelSelectorPopover> + </TooltipKeybind> </Show> <Show when={local.model.variant.list().length > 0}> <TooltipKeybind @@ -1683,6 +1694,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => { "text-text-base": !permission.isAutoAccepting(params.id!, sdk.directory), "hover:bg-surface-success-base": permission.isAutoAccepting(params.id!, sdk.directory), }} + aria-label="Toggle auto-accept permissions" + aria-pressed={permission.isAutoAccepting(params.id!, sdk.directory)} > <Icon name="chevron-double-right" @@ -1711,7 +1724,13 @@ export const PromptInput: Component<PromptInputProps> = (props) => { <SessionContextUsage /> <Show when={store.mode === "normal"}> <Tooltip placement="top" value={language.t("prompt.action.attachFile")}> - <Button type="button" variant="ghost" class="size-6" onClick={() => fileInputRef.click()}> + <Button + type="button" + variant="ghost" + class="size-6" + onClick={() => fileInputRef.click()} + aria-label="Attach file" + > <Icon name="photo" class="size-4.5" /> </Button> </Tooltip> @@ -1743,6 +1762,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => { icon={working() ? "stop" : "arrow-up"} variant="primary" class="h-6 w-4.5" + aria-label={working() ? "Stop" : "Send message"} /> </Tooltip> </div> diff --git a/packages/app/src/components/session-context-usage.tsx b/packages/app/src/components/session-context-usage.tsx index 53148d416..677e29dbb 100644 --- a/packages/app/src/components/session-context-usage.tsx +++ b/packages/app/src/components/session-context-usage.tsx @@ -96,7 +96,7 @@ export function SessionContextUsage(props: SessionContextUsageProps) { <Switch> <Match when={variant() === "indicator"}>{circle()}</Match> <Match when={true}> - <Button type="button" variant="ghost" class="size-6" onClick={openContext}> + <Button type="button" variant="ghost" class="size-6" onClick={openContext} aria-label="View context usage"> {circle()} </Button> </Match> diff --git a/packages/app/src/components/session/session-header.tsx b/packages/app/src/components/session/session-header.tsx index 98b9635c5..f2ffa3ec5 100644 --- a/packages/app/src/components/session/session-header.tsx +++ b/packages/app/src/components/session/session-header.tsx @@ -135,6 +135,7 @@ export function SessionHeader() { type="button" class="hidden md:flex w-[320px] p-1 pl-1.5 items-center gap-2 justify-between rounded-md border border-border-weak-base bg-surface-raised-base transition-colors cursor-default hover:bg-surface-raised-base-hover focus:bg-surface-raised-base-hover active:bg-surface-raised-base-active" onClick={() => command.trigger("file.open")} + aria-label="Search files" > <div class="flex min-w-0 flex-1 items-center gap-2 overflow-visible"> <Icon name="magnifying-glass" size="normal" class="icon-base shrink-0" /> @@ -184,6 +185,10 @@ export function SessionHeader() { variant="ghost" class="group/review-toggle size-6 p-0" onClick={() => view().reviewPanel.toggle()} + aria-label="Toggle review panel" + aria-expanded={view().reviewPanel.opened()} + aria-controls="review-panel" + tabIndex={showReview() ? 0 : -1} > <div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0"> <Icon @@ -214,6 +219,9 @@ export function SessionHeader() { variant="ghost" class="group/terminal-toggle size-6 p-0" onClick={() => view().terminal.toggle()} + aria-label="Toggle terminal" + aria-expanded={view().terminal.opened()} + aria-controls="terminal-panel" > <div class="relative flex items-center justify-center size-4 [&>*]:absolute [&>*]:inset-0"> <Icon @@ -235,32 +243,23 @@ export function SessionHeader() { </Button> </TooltipKeybind> </div> - <div - class="flex items-center" - classList={{ - "opacity-0 pointer-events-none": !showShare(), - }} - aria-hidden={!showShare()} - > - <Popover - title={language.t("session.share.popover.title")} - description={ - shareUrl() - ? language.t("session.share.popover.description.shared") - : language.t("session.share.popover.description.unshared") - } - trigger={ - <Tooltip class="shrink-0" value={language.t("command.session.share")}> - <Button - variant="secondary" - classList={{ "rounded-r-none": shareUrl() !== undefined }} - style={{ scale: 1 }} - > - {language.t("session.share.action.share")} - </Button> - </Tooltip> - } - > + <Show when={showShare()}> + <div class="flex items-center"> + <Popover + title={language.t("session.share.popover.title")} + description={ + shareUrl() + ? language.t("session.share.popover.description.shared") + : language.t("session.share.popover.description.unshared") + } + triggerAs={Button} + triggerProps={{ + variant: "secondary", + classList: { "rounded-r-none": shareUrl() !== undefined }, + style: { scale: 1 }, + }} + trigger={language.t("session.share.action.share")} + > <div class="flex flex-col gap-2"> <Show when={shareUrl()} @@ -322,10 +321,12 @@ export function SessionHeader() { class="rounded-l-none" onClick={copyLink} disabled={state.unshare} + aria-label="Copy share link" /> </Tooltip> </Show> - </div> + </div> + </Show> </div> </Portal> )} diff --git a/packages/app/src/components/session/session-sortable-tab.tsx b/packages/app/src/components/session/session-sortable-tab.tsx index a4a434b05..6f2c044fc 100644 --- a/packages/app/src/components/session/session-sortable-tab.tsx +++ b/packages/app/src/components/session/session-sortable-tab.tsx @@ -37,7 +37,7 @@ export function SortableTab(props: { tab: string; onTabClose: (tab: string) => v value={props.tab} closeButton={ <Tooltip value={language.t("common.closeTab")} placement="bottom"> - <IconButton icon="close" variant="ghost" onClick={() => props.onTabClose(props.tab)} /> + <IconButton icon="close" variant="ghost" onClick={() => props.onTabClose(props.tab)} aria-label="Close tab" /> </Tooltip> } hideCloseButton diff --git a/packages/app/src/components/session/session-sortable-terminal-tab.tsx b/packages/app/src/components/session/session-sortable-terminal-tab.tsx index a3aaf6061..3ef395c9a 100644 --- a/packages/app/src/components/session/session-sortable-terminal-tab.tsx +++ b/packages/app/src/components/session/session-sortable-terminal-tab.tsx @@ -139,6 +139,7 @@ export function SortableTerminalTab(props: { terminal: LocalPTY; onClose?: () => e.stopPropagation() close() }} + aria-label="Close terminal" /> } > diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index befdf721d..2a050543c 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -1916,6 +1916,7 @@ export default function Layout(props: ParentProps) { "bg-surface-base-hover border border-border-weak-base": !selected() && open(), }} onClick={() => navigateToProject(props.project.worktree)} + onBlur={() => setOpen(false)} > <ProjectIcon project={props.project} notify /> </button> @@ -2343,7 +2344,8 @@ export default function Layout(props: ParentProps) { <div class="relative bg-background-base flex-1 min-h-0 flex flex-col select-none [&_input]:select-text [&_textarea]:select-text [&_[contenteditable]]:select-text"> <Titlebar /> <div class="flex-1 min-h-0 flex"> - <div + <nav + aria-label="Projects and sessions" classList={{ "hidden xl:block": true, "relative shrink-0": true, @@ -2364,7 +2366,7 @@ export default function Layout(props: ParentProps) { onCollapse={layout.sidebar.close} /> </Show> - </div> + </nav> <div class="xl:hidden"> <div classList={{ @@ -2376,7 +2378,8 @@ export default function Layout(props: ParentProps) { if (e.target === e.currentTarget) layout.mobileSidebar.hide() }} /> - <div + <nav + aria-label="Projects and sessions" classList={{ "@container fixed top-10 bottom-0 left-0 z-50 w-72 bg-background-base transition-transform duration-200 ease-out": true, "translate-x-0": layout.mobileSidebar.opened(), @@ -2385,7 +2388,7 @@ export default function Layout(props: ParentProps) { onClick={(e) => e.stopPropagation()} > <SidebarContent mobile /> - </div> + </nav> </div> <main diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 5e4eeafd5..19ee5d304 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -782,7 +782,7 @@ export default function Page() { const activeElement = document.activeElement as HTMLElement | undefined if (activeElement) { const isProtected = activeElement.closest("[data-prevent-autofocus]") - const isInput = /^(INPUT|TEXTAREA|SELECT)$/.test(activeElement.tagName) || activeElement.isContentEditable + const isInput = /^(INPUT|TEXTAREA|SELECT|BUTTON)$/.test(activeElement.tagName) || activeElement.isContentEditable if (isProtected || isInput) return } if (dialog.active) return @@ -1404,6 +1404,7 @@ export default function Page() { <div ref={autoScroll.contentRef} + role="log" class="flex flex-col gap-32 items-start justify-start pb-[calc(var(--prompt-height,8rem)+64px)] md:pb-[calc(var(--prompt-height,10rem)+64px)] transition-[margin]" classList={{ "w-full": true, @@ -1552,7 +1553,7 @@ export default function Page() { {/* Desktop tabs panel (Review + Context + Files) - hidden on mobile */} <Show when={isDesktop() && showTabs()}> - <div class="relative flex-1 min-w-0 h-full border-l border-border-weak-base"> + <aside id="review-panel" aria-label="Review and files" class="relative flex-1 min-w-0 h-full border-l border-border-weak-base"> <DragDropProvider onDragStart={handleDragStart} onDragEnd={handleDragEnd} @@ -1586,7 +1587,7 @@ export default function Page() { value="context" closeButton={ <Tooltip value={language.t("common.closeTab")} placement="bottom"> - <IconButton icon="close" variant="ghost" onClick={() => tabs().close("context")} /> + <IconButton icon="close" variant="ghost" onClick={() => tabs().close("context")} aria-label="Close context tab" /> </Tooltip> } hideCloseButton @@ -1612,6 +1613,7 @@ export default function Page() { variant="ghost" iconSize="large" onClick={() => dialog.show(() => <DialogSelectFile />)} + aria-label="Open file" /> </TooltipKeybind> </div> @@ -1913,12 +1915,15 @@ export default function Page() { </Show> </DragOverlay> </DragDropProvider> - </div> + </aside> </Show> </div> <Show when={isDesktop() && view().terminal.opened()}> <div + id="terminal-panel" + role="region" + aria-label="Terminal" class="relative w-full flex flex-col shrink-0 border-t border-border-weak-base" style={{ height: `${layout.terminal.height()}px` }} > @@ -1990,7 +1995,7 @@ export default function Page() { keybind={command.keybind("terminal.new")} class="flex items-center" > - <IconButton icon="plus-small" variant="ghost" iconSize="large" onClick={terminal.new} /> + <IconButton icon="plus-small" variant="ghost" iconSize="large" onClick={terminal.new} aria-label="New terminal" /> </TooltipKeybind> </div> </Tabs.List> |
