fix(web): section-header hover outline + clear buttons out of tab order (#1487) (#1488)

* fix(web): section-header hover outline + clear buttons out of tab order (#1487)

Two button-behavior adjustments:

1. History section headers (Pinned / History) now use the filter-toggle
   style — a thin outline on hover instead of a background fill — by pointing
   the `sectionHeader` Group variant at the shared `.filter-toggle` class. The
   open section keeps its filled background (passed per-instance via `bg`).

2. Every input clear button added in #1448 is removed from the keyboard tab
   order via `tabIndex={-1}` (CloseButton clear buttons app-wide, plus the
   Autocomplete clear button through `theme/Autocomplete.ts` clearButtonProps),
   so tabbing through a form moves to the next field, not onto the clear button.
   The buttons stay mouse-clickable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(web): apply filter-toggle hover outline to Resources accordion headers (#1487)

The Resources screen `disclosure` accordion controls now share the same
outline-on-hover treatment as the filter toggle buttons and History section
headers: a thin border on hover instead of a background fill, plus a filled
background when the section is open (keyed off `aria-expanded`). The `.filter-
toggle` class is added to the disclosure control slot; the default-variant
accordion (Server Settings) keeps its background-fill hover via a `:not(.filter-
toggle)` guard.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(web): extract ClearButton element, replace 34 inline clear buttons

Addresses the code-review suggestion on PR #1488: the same five-line
`CloseButton` clear-affordance block (aria-label="Clear" + tabIndex={-1} +
onClick) was hand-copied across 17 files. Extract a single
`elements/ClearButton` (Mantine `CloseButton.withProps` with the aria-label and
tabIndex baked in) and replace all 34 call sites, so the clear-button contract
lives in one place. A global CloseButton theme default would be wrong here — it
would also catch tabbable modal-close X buttons and the Select clear buttons —
so a dedicated element is the right altitude.

Net -151 lines. Adds element unit tests + a Storybook play test covering the
aria-label / tabIndex / clickable contract for all sites at once.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Cliff Hall 2026-06-14 22:27:46 -04:00 • committed by GitHub
commit bb32d1db5f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 180 additions and 196 deletions

View file

@ -1,5 +1,4 @@
import {
CloseButton,
Group,
ScrollArea,
Stack,
@ -7,6 +6,7 @@ import {
TextInput,
Title,
} from "@mantine/core";
import { ClearButton } from "../../elements/ClearButton/ClearButton";
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
import { ListChangedIndicator } from "../../elements/ListChangedIndicator/ListChangedIndicator";
import { AppListItem } from "../AppListItem/AppListItem";
@ -64,12 +64,7 @@ export function AppControls({
onChange={(e) => onSearchChange(e.currentTarget.value)}
rightSectionPointerEvents="auto"
rightSection={
searchText ? (
<CloseButton
aria-label="Clear"
onClick={() => onSearchChange("")}
/>
) : null
searchText ? <ClearButton onClick={() => onSearchChange("")} /> : null
}
/>
<ScrollArea.Autosize viewportRef={viewportRef} mah={LIST_MAX_HEIGHT}>

View file

@ -5,7 +5,6 @@ import {
Button,
Card,
Checkbox,
CloseButton,
Divider,
Group,
Stack,
@ -15,6 +14,7 @@ import {
Textarea,
Title,
} from "@mantine/core";
import { ClearButton } from "../../elements/ClearButton/ClearButton";
import type {
ClientCapabilities,
JSONRPCErrorResponse,
@ -248,8 +248,7 @@ export function ExperimentalFeaturesPanel({
rightSectionPointerEvents="auto"
rightSection={
header.key ? (
<CloseButton
aria-label="Clear"
<ClearButton
onClick={() => onHeaderChange(index, "", header.value)}
/>
) : null
@ -264,8 +263,7 @@ export function ExperimentalFeaturesPanel({
rightSectionPointerEvents="auto"
rightSection={
header.value ? (
<CloseButton
aria-label="Clear"
<ClearButton
onClick={() => onHeaderChange(index, header.key, "")}
/>
) : null
@ -291,10 +289,7 @@ export function ExperimentalFeaturesPanel({
rightSectionPointerEvents="auto"
rightSection={
requestDraft ? (
<CloseButton
aria-label="Clear"
onClick={() => onRequestChange("")}
/>
<ClearButton onClick={() => onRequestChange("")} />
) : null
}
/>

View file

@ -45,6 +45,14 @@ describe("HistoryControls", () => {
expect(onSearchChange).toHaveBeenCalledWith("");
});
it("keeps the search Clear button out of the keyboard tab order", () => {
renderWithMantine(<HistoryControls {...baseProps} searchText="abc" />);
expect(screen.getByRole("button", { name: "Clear" })).toHaveAttribute(
"tabindex",
"-1",
);
});
it("renders the method filter placeholder", () => {
renderWithMantine(<HistoryControls {...baseProps} />);
expect(screen.getByPlaceholderText("All methods")).toBeInTheDocument();

View file

@ -1,4 +1,5 @@
import { CloseButton, Select, Stack, TextInput, Title } from "@mantine/core";
import { Select, Stack, TextInput, Title } from "@mantine/core";
import { ClearButton } from "../../elements/ClearButton/ClearButton";
import type {
MessageMethod,
MessageOrigin,
@ -35,12 +36,7 @@ export function HistoryControls({
onChange={(event) => onSearchChange(event.currentTarget.value)}
rightSectionPointerEvents="auto"
rightSection={
searchText ? (
<CloseButton
aria-label="Clear"
onClick={() => onSearchChange("")}
/>
) : null
searchText ? <ClearButton onClick={() => onSearchChange("")} /> : null
}
/>

View file

@ -58,8 +58,9 @@ const EmptyState = Text.withProps({
py: "xl",
});
// The section header is a single "pleat" bar (rounded, hover-highlighted, with
// the active background passed per instance via `bg`). Inside it sit the
// The section header is a single "pleat" bar (rounded, with the filter-button
// outline-on-hover treatment and the active background passed per instance via
// `bg`). Inside it sit the
// clickable toggle area (the title, filling the left) and the optional
// Clear/Export actions on the right — so the actions live on the pleat itself,
// not beside it. The toggle is its own button (the actions can't nest inside a

View file

@ -1,6 +1,5 @@
import {
Button,
CloseButton,
Divider,
Group,
Radio,
@ -10,6 +9,7 @@ import {
Textarea,
Title,
} from "@mantine/core";
import { ClearButton } from "../../elements/ClearButton/ClearButton";
import type { InspectorServerJsonDraft } from "@inspector/core/mcp/types.js";
export interface ValidationResult {
@ -92,7 +92,7 @@ export function ImportServerJsonPanel({
rightSectionPointerEvents="auto"
rightSection={
draft.rawText ? (
<CloseButton aria-label="Clear" onClick={() => onJsonChange("")} />
<ClearButton onClick={() => onJsonChange("")} />
) : null
}
/>
@ -149,8 +149,7 @@ export function ImportServerJsonPanel({
rightSectionPointerEvents="auto"
rightSection={
envVar.value ? (
<CloseButton
aria-label="Clear"
<ClearButton
onClick={() => onEnvVarChange(envVar.name, "")}
/>
) : null
@ -169,10 +168,7 @@ export function ImportServerJsonPanel({
rightSectionPointerEvents="auto"
rightSection={
draft.nameOverride ? (
<CloseButton
aria-label="Clear"
onClick={() => onServerNameChange("")}
/>
<ClearButton onClick={() => onServerNameChange("")} />
) : null
}
/>

View file

@ -1,12 +1,5 @@
import {
Button,
CloseButton,
Group,
Select,
Stack,
TextInput,
Title,
} from "@mantine/core";
import { Button, Group, Select, Stack, TextInput, Title } from "@mantine/core";
import { ClearButton } from "../../elements/ClearButton/ClearButton";
import type { LoggingLevel } from "@modelcontextprotocol/sdk/types.js";
import { FilterToggleButton } from "../../elements/FilterToggleButton/FilterToggleButton";
@ -66,12 +59,7 @@ export function LogControls({
onChange={(e) => onFilterChange(e.currentTarget.value)}
rightSectionPointerEvents="auto"
rightSection={
filterText ? (
<CloseButton
aria-label="Clear"
onClick={() => onFilterChange("")}
/>
) : null
filterText ? <ClearButton onClick={() => onFilterChange("")} /> : null
}
/>

View file

@ -1,11 +1,5 @@
import {
Button,
CloseButton,
Group,
Stack,
TextInput,
Title,
} from "@mantine/core";
import { Button, Group, Stack, TextInput, Title } from "@mantine/core";
import { ClearButton } from "../../elements/ClearButton/ClearButton";
import type { FetchRequestCategory } from "@inspector/core/mcp/types.js";
import { FilterToggleButton } from "../../elements/FilterToggleButton/FilterToggleButton";
@ -47,12 +41,7 @@ export function NetworkControls({
onChange={(e) => onFilterChange(e.currentTarget.value)}
rightSectionPointerEvents="auto"
rightSection={
filterText ? (
<CloseButton
aria-label="Clear"
onClick={() => onFilterChange("")}
/>
) : null
filterText ? <ClearButton onClick={() => onFilterChange("")} /> : null
}
/>

View file

@ -2,13 +2,13 @@ import { useCallback, useEffect, useRef, useState } from "react";
import {
Autocomplete,
Button,
CloseButton,
Group,
Stack,
Text,
TextInput,
Title,
} from "@mantine/core";
import { ClearButton } from "../../elements/ClearButton/ClearButton";
import type { Prompt } from "@modelcontextprotocol/sdk/types.js";
export interface PromptArgumentsFormProps {
@ -225,10 +225,7 @@ export function PromptArgumentsForm({
rightSectionPointerEvents="auto"
rightSection={
argumentValues[arg.name] ? (
<CloseButton
aria-label="Clear"
onClick={() => handleChange(arg.name, "")}
/>
<ClearButton onClick={() => handleChange(arg.name, "")} />
) : null
}
/>

View file

@ -1,11 +1,5 @@
import {
CloseButton,
Group,
ScrollArea,
Stack,
TextInput,
Title,
} from "@mantine/core";
import { Group, ScrollArea, Stack, TextInput, Title } from "@mantine/core";
import { ClearButton } from "../../elements/ClearButton/ClearButton";
import type { Prompt } from "@modelcontextprotocol/sdk/types.js";
import { ListChangedIndicator } from "../../elements/ListChangedIndicator/ListChangedIndicator";
import { PromptListItem } from "../PromptListItem/PromptListItem";
@ -56,12 +50,7 @@ export function PromptControls({
onChange={(e) => onSearchChange(e.currentTarget.value)}
rightSectionPointerEvents="auto"
rightSection={
searchText ? (
<CloseButton
aria-label="Clear"
onClick={() => onSearchChange("")}
/>
) : null
searchText ? <ClearButton onClick={() => onSearchChange("")} /> : null
}
/>
<ScrollArea.Autosize viewportRef={viewportRef} mah={LIST_MAX_HEIGHT}>

View file

@ -1,11 +1,5 @@
import {
Accordion,
CloseButton,
Group,
Stack,
TextInput,
Title,
} from "@mantine/core";
import { Accordion, Group, Stack, TextInput, Title } from "@mantine/core";
import { ClearButton } from "../../elements/ClearButton/ClearButton";
import { RiArrowRightSLine } from "react-icons/ri";
import type {
Resource,
@ -186,10 +180,7 @@ export function ResourceControls({
rightSectionPointerEvents="auto"
rightSection={
searchText ? (
<CloseButton
aria-label="Clear"
onClick={() => onSearchChange("")}
/>
<ClearButton onClick={() => onSearchChange("")} />
) : null
}
/>

View file

@ -2,13 +2,13 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
Autocomplete,
Button,
CloseButton,
Group,
Stack,
Text,
TextInput,
Title,
} from "@mantine/core";
import { ClearButton } from "../../elements/ClearButton/ClearButton";
import type { ResourceTemplate } from "@modelcontextprotocol/sdk/types.js";
import { AnnotationBadge } from "../../elements/AnnotationBadge/AnnotationBadge";
import { CopyButton } from "../../elements/CopyButton/CopyButton";
@ -265,8 +265,7 @@ export function ResourceTemplatePanel({
rightSectionPointerEvents="auto"
rightSection={
variables[varName] ? (
<CloseButton
aria-label="Clear"
<ClearButton
onClick={() => handleVariableChange(varName, "")}
/>
) : null

View file

@ -2,7 +2,6 @@ import {
ActionIcon,
Alert,
Button,
CloseButton,
Divider,
Group,
Stack,
@ -11,6 +10,7 @@ import {
TextInput,
Title,
} from "@mantine/core";
import { ClearButton } from "../../elements/ClearButton/ClearButton";
import type { Root } from "@modelcontextprotocol/sdk/types.js";
export type RootDraft = { name: string; uri: string };
@ -94,8 +94,7 @@ export function RootsTable({
rightSectionPointerEvents="auto"
rightSection={
newRootDraft.name ? (
<CloseButton
aria-label="Clear"
<ClearButton
onClick={() =>
onNewRootDraftChange({ ...newRootDraft, name: "" })
}
@ -115,8 +114,7 @@ export function RootsTable({
rightSectionPointerEvents="auto"
rightSection={
newRootDraft.uri ? (
<CloseButton
aria-label="Clear"
<ClearButton
onClick={() => onNewRootDraftChange({ ...newRootDraft, uri: "" })}
/>
) : null

View file

@ -1,7 +1,6 @@
import {
Badge,
Button,
CloseButton,
Divider,
Group,
Paper,
@ -12,6 +11,7 @@ import {
Textarea,
Title,
} from "@mantine/core";
import { ClearButton } from "../../elements/ClearButton/ClearButton";
import type {
CreateMessageRequestParams,
CreateMessageResult,
@ -159,8 +159,7 @@ export function SamplingRequestPanel({
rightSectionPointerEvents="auto"
rightSection={
draftResult.content.type === "text" && draftResult.content.text ? (
<CloseButton
aria-label="Clear"
<ClearButton
onClick={() =>
onResultChange({
...draftResult,
@ -181,8 +180,7 @@ export function SamplingRequestPanel({
rightSectionPointerEvents="auto"
rightSection={
draftResult.model ? (
<CloseButton
aria-label="Clear"
<ClearButton
onClick={() => onResultChange({ ...draftResult, model: "" })}
/>
) : null

View file

@ -1,6 +1,5 @@
import {
Checkbox,
CloseButton,
JsonInput,
MultiSelect,
NumberInput,
@ -9,6 +8,7 @@ import {
Text,
TextInput,
} from "@mantine/core";
import { ClearButton } from "../../elements/ClearButton/ClearButton";
import type { JsonSchemaType } from "../../../utils/jsonUtils";
const FieldLabel = Text.withProps({
@ -119,10 +119,7 @@ export function SchemaForm({
rightSectionPointerEvents="auto"
rightSection={
rawValue ? (
<CloseButton
aria-label="Clear"
onClick={() => handleFieldChange(fieldName, "")}
/>
<ClearButton onClick={() => handleFieldChange(fieldName, "")} />
) : null
}
/>

View file

@ -1,7 +1,6 @@
import { useEffect, useMemo, useState } from "react";
import {
Button,
CloseButton,
Group,
Modal,
Select,
@ -10,6 +9,7 @@ import {
TextInput,
Textarea,
} from "@mantine/core";
import { ClearButton } from "../../elements/ClearButton/ClearButton";
import type {
MCPServerConfig,
StdioServerConfig,
@ -279,8 +279,7 @@ export function ServerConfigModal({
rightSectionPointerEvents="auto"
rightSection={
form.id ? (
<CloseButton
aria-label="Clear"
<ClearButton
disabled={submitting}
onClick={() => setForm((f) => ({ ...f, id: "" }))}
/>
@ -321,8 +320,7 @@ export function ServerConfigModal({
rightSectionPointerEvents="auto"
rightSection={
form.command ? (
<CloseButton
aria-label="Clear"
<ClearButton
disabled={submitting}
onClick={() => setForm((f) => ({ ...f, command: "" }))}
/>
@ -344,8 +342,7 @@ export function ServerConfigModal({
rightSectionPointerEvents="auto"
rightSection={
form.argsText ? (
<CloseButton
aria-label="Clear"
<ClearButton
disabled={submitting}
onClick={() => setForm((f) => ({ ...f, argsText: "" }))}
/>
@ -367,8 +364,7 @@ export function ServerConfigModal({
rightSectionPointerEvents="auto"
rightSection={
form.envText ? (
<CloseButton
aria-label="Clear"
<ClearButton
disabled={submitting}
onClick={() => setForm((f) => ({ ...f, envText: "" }))}
/>
@ -387,8 +383,7 @@ export function ServerConfigModal({
rightSectionPointerEvents="auto"
rightSection={
form.cwd ? (
<CloseButton
aria-label="Clear"
<ClearButton
disabled={submitting}
onClick={() => setForm((f) => ({ ...f, cwd: "" }))}
/>
@ -410,8 +405,7 @@ export function ServerConfigModal({
rightSectionPointerEvents="auto"
rightSection={
form.url ? (
<CloseButton
aria-label="Clear"
<ClearButton
disabled={submitting}
onClick={() => setForm((f) => ({ ...f, url: "" }))}
/>

View file

@ -3,13 +3,13 @@ import {
ActionIcon,
Button,
Checkbox,
CloseButton,
Group,
NumberInput,
Stack,
Text,
TextInput,
} from "@mantine/core";
import { ClearButton } from "../../elements/ClearButton/ClearButton";
import type {
InspectorServerSettings,
OAuthSettings,
@ -91,10 +91,7 @@ function KeyValueRows({
rightSectionPointerEvents="auto"
rightSection={
item.key ? (
<CloseButton
aria-label="Clear"
onClick={() => onChange(index, "", item.value)}
/>
<ClearButton onClick={() => onChange(index, "", item.value)} />
) : null
}
/>
@ -105,10 +102,7 @@ function KeyValueRows({
rightSectionPointerEvents="auto"
rightSection={
item.value ? (
<CloseButton
aria-label="Clear"
onClick={() => onChange(index, item.key, "")}
/>
<ClearButton onClick={() => onChange(index, item.key, "")} />
) : null
}
/>
@ -145,8 +139,7 @@ function RootRows({
rightSectionPointerEvents="auto"
rightSection={
root.uri ? (
<CloseButton
aria-label="Clear"
<ClearButton
onClick={() => onChange(index, "", root.name ?? "")}
/>
) : null
@ -159,10 +152,7 @@ function RootRows({
rightSectionPointerEvents="auto"
rightSection={
root.name ? (
<CloseButton
aria-label="Clear"
onClick={() => onChange(index, root.uri, "")}
/>
<ClearButton onClick={() => onChange(index, root.uri, "")} />
) : null
}
/>
@ -372,8 +362,7 @@ export function ServerSettingsForm({
rightSectionPointerEvents="auto"
rightSection={
settings.oauthClientId ? (
<CloseButton
aria-label="Clear"
<ClearButton
onClick={() =>
onOAuthChange({ ...currentOAuth(), clientId: "" })
}
@ -394,8 +383,7 @@ export function ServerSettingsForm({
rightSectionPointerEvents="auto"
rightSection={
settings.oauthClientSecret ? (
<CloseButton
aria-label="Clear"
<ClearButton
onClick={() =>
onOAuthChange({ ...currentOAuth(), clientSecret: "" })
}
@ -415,8 +403,7 @@ export function ServerSettingsForm({
rightSectionPointerEvents="auto"
rightSection={
settings.oauthScopes ? (
<CloseButton
aria-label="Clear"
<ClearButton
onClick={() =>
onOAuthChange({ ...currentOAuth(), scopes: "" })
}

View file

@ -1,12 +1,5 @@
import {
Button,
CloseButton,
Group,
Select,
Stack,
TextInput,
Title,
} from "@mantine/core";
import { Button, Group, Select, Stack, TextInput, Title } from "@mantine/core";
import { ClearButton } from "../../elements/ClearButton/ClearButton";
import type { TaskStatus } from "@modelcontextprotocol/sdk/types.js";
const STATUS_OPTIONS: TaskStatus[] = [
@ -50,12 +43,7 @@ export function TaskControls({
onChange={(event) => onSearchChange(event.currentTarget.value)}
rightSectionPointerEvents="auto"
rightSection={
searchText ? (
<CloseButton
aria-label="Clear"
onClick={() => onSearchChange("")}
/>
) : null
searchText ? <ClearButton onClick={() => onSearchChange("")} /> : null
}
/>

View file

@ -1,11 +1,5 @@
import {
CloseButton,
Group,
ScrollArea,
Stack,
TextInput,
Title,
} from "@mantine/core";
import { Group, ScrollArea, Stack, TextInput, Title } from "@mantine/core";
import { ClearButton } from "../../elements/ClearButton/ClearButton";
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
import { ListChangedIndicator } from "../../elements/ListChangedIndicator/ListChangedIndicator";
import { ToolListItem } from "../ToolListItem/ToolListItem";
@ -57,12 +51,7 @@ export function ToolControls({
onChange={(e) => onSearchChange(e.currentTarget.value)}
rightSectionPointerEvents="auto"
rightSection={
searchText ? (
<CloseButton
aria-label="Clear"
onClick={() => onSearchChange("")}
/>
) : null
searchText ? <ClearButton onClick={() => onSearchChange("")} /> : null
}
/>
<ScrollArea.Autosize viewportRef={viewportRef} mah={LIST_MAX_HEIGHT}>