From bb32d1db5f678dd7e2db4e179b1c65ad947d036f Mon Sep 17 00:00:00 2001 From: Cliff Hall Date: Sun, 14 Jun 2026 22:27:46 -0400 Subject: [PATCH] fix(web): section-header hover outline + clear buttons out of tab order (#1487) (#1488) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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) * 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) * 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) --------- Co-authored-by: Claude Opus 4.8 (1M context) --- clients/web/src/App.css | 29 ++++++++++------ .../ClearButton/ClearButton.stories.tsx | 21 ++++++++++++ .../elements/ClearButton/ClearButton.test.tsx | 34 +++++++++++++++++++ .../elements/ClearButton/ClearButton.tsx | 14 ++++++++ .../groups/AppControls/AppControls.tsx | 9 ++--- .../ExperimentalFeaturesPanel.tsx | 13 +++---- .../HistoryControls/HistoryControls.test.tsx | 8 +++++ .../HistoryControls/HistoryControls.tsx | 10 ++---- .../HistoryListPanel/HistoryListPanel.tsx | 5 +-- .../ImportServerJsonPanel.tsx | 12 +++---- .../groups/LogControls/LogControls.tsx | 18 ++-------- .../NetworkControls/NetworkControls.tsx | 17 ++-------- .../PromptArgumentsForm.tsx | 7 ++-- .../groups/PromptControls/PromptControls.tsx | 17 ++-------- .../ResourceControls/ResourceControls.tsx | 15 ++------ .../ResourceTemplatePanel.tsx | 5 ++- .../groups/RootsTable/RootsTable.tsx | 8 ++--- .../SamplingRequestPanel.tsx | 8 ++--- .../groups/SchemaForm/SchemaForm.tsx | 7 ++-- .../ServerConfigModal/ServerConfigModal.tsx | 20 ++++------- .../ServerSettingsForm/ServerSettingsForm.tsx | 29 +++++----------- .../groups/TaskControls/TaskControls.tsx | 18 ++-------- .../groups/ToolControls/ToolControls.tsx | 17 ++-------- clients/web/src/theme/Accordion.ts | 12 +++++-- clients/web/src/theme/Autocomplete.ts | 4 +++ clients/web/src/theme/Group.ts | 19 +++++------ 26 files changed, 180 insertions(+), 196 deletions(-) create mode 100644 clients/web/src/components/elements/ClearButton/ClearButton.stories.tsx create mode 100644 clients/web/src/components/elements/ClearButton/ClearButton.test.tsx create mode 100644 clients/web/src/components/elements/ClearButton/ClearButton.tsx diff --git a/clients/web/src/App.css b/clients/web/src/App.css index 1e4a915f..9807a3bd 100644 --- a/clients/web/src/App.css +++ b/clients/web/src/App.css @@ -283,14 +283,18 @@ /* ── Filter toggle buttons (Logging / History / Network) ───────── * - * Shared by the FilterToggleButton element. Unlike `.list-item`, hover is a - * thin border rather than a background fill, and the active (on) state is the - * filled background. Keeping the two states distinct means toggling a button - * off while the cursor is still over it is immediately visible, instead of the - * hover background masking the removal of the active background (issue #1460). - * The 1px transparent border is reserved here (not as an inline style in the - * theme variant — inline styles outrank stylesheet `:hover` rules) so the hover - * border doesn't shift layout. */ + * Shared by the FilterToggleButton element, the History list's collapsible + * section headers (the `sectionHeader` Group variant), and the Resources + * disclosure accordion controls (the `disclosure` Accordion variant). The + * active (on / open) state is keyed off `aria-pressed` for the toggle buttons + * and `aria-expanded` for the accordion controls. Unlike `.list-item`, + * hover is a thin border rather than a background fill, and the active (on) + * state is the filled background. Keeping the two states distinct means + * toggling a button off while the cursor is still over it is immediately + * visible, instead of the hover background masking the removal of the active + * background (issue #1460). The 1px transparent border is reserved here (not as + * an inline style in the theme variant — inline styles outrank stylesheet + * `:hover` rules) so the hover border doesn't shift layout. */ .filter-toggle { border: 1px solid transparent; @@ -301,7 +305,8 @@ border-color: var(--inspector-brand-primary); } -.filter-toggle[aria-pressed="true"] { +.filter-toggle[aria-pressed="true"], +.filter-toggle[aria-expanded="true"] { background-color: var(--mantine-primary-color-light); } @@ -342,7 +347,11 @@ /* ── Accordion button hover ─────────────────────────────── */ -.mantine-Accordion-control:hover { +/* Default accordions (e.g. Server Settings) keep the background-fill hover. + * The Resources `disclosure` accordion opts out via `.filter-toggle` (added to + * its control slot in theme/Accordion.ts) so it gets the outline-on-hover + * treatment instead — see the `.filter-toggle` rules above. */ +.mantine-Accordion-control:not(.filter-toggle):hover { background-color: var(--mantine-primary-color-light-hover); } diff --git a/clients/web/src/components/elements/ClearButton/ClearButton.stories.tsx b/clients/web/src/components/elements/ClearButton/ClearButton.stories.tsx new file mode 100644 index 00000000..e76d1bbe --- /dev/null +++ b/clients/web/src/components/elements/ClearButton/ClearButton.stories.tsx @@ -0,0 +1,21 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { expect, userEvent, within } from "storybook/test"; +import { ClearButton } from "./ClearButton"; + +const meta: Meta = { + title: "Elements/ClearButton", + component: ClearButton, +}; + +export default meta; +type Story = StoryObj; + +// Default: labelled "Clear", out of the tab order, but still mouse-clickable. +export const Default: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const button = await canvas.findByRole("button", { name: "Clear" }); + await expect(button).toHaveAttribute("tabindex", "-1"); + await userEvent.click(button); + }, +}; diff --git a/clients/web/src/components/elements/ClearButton/ClearButton.test.tsx b/clients/web/src/components/elements/ClearButton/ClearButton.test.tsx new file mode 100644 index 00000000..98750e57 --- /dev/null +++ b/clients/web/src/components/elements/ClearButton/ClearButton.test.tsx @@ -0,0 +1,34 @@ +import { describe, it, expect, vi } from "vitest"; +import userEvent from "@testing-library/user-event"; +import { renderWithMantine, screen } from "../../../test/renderWithMantine"; +import { ClearButton } from "./ClearButton"; + +describe("ClearButton", () => { + it('renders with the "Clear" accessible name', () => { + renderWithMantine(); + expect(screen.getByRole("button", { name: "Clear" })).toBeInTheDocument(); + }); + + it("is removed from the keyboard tab order", () => { + renderWithMantine(); + expect(screen.getByRole("button", { name: "Clear" })).toHaveAttribute( + "tabindex", + "-1", + ); + }); + + it("invokes onClick when clicked", async () => { + const user = userEvent.setup(); + const onClick = vi.fn(); + renderWithMantine(); + await user.click(screen.getByRole("button", { name: "Clear" })); + expect(onClick).toHaveBeenCalledTimes(1); + }); + + it("allows the accessible name to be overridden", () => { + renderWithMantine(); + expect( + screen.getByRole("button", { name: "Clear search" }), + ).toBeInTheDocument(); + }); +}); diff --git a/clients/web/src/components/elements/ClearButton/ClearButton.tsx b/clients/web/src/components/elements/ClearButton/ClearButton.tsx new file mode 100644 index 00000000..f6435097 --- /dev/null +++ b/clients/web/src/components/elements/ClearButton/ClearButton.tsx @@ -0,0 +1,14 @@ +import { CloseButton } from "@mantine/core"; + +/** + * The clear (×) affordance shown in a populated text input's right section + * (`rightSection`). Wraps Mantine's `CloseButton` with a fixed + * `aria-label="Clear"` and `tabIndex={-1}`, so the button stays mouse-clickable + * but is skipped during keyboard tab navigation — tabbing through a form lands + * on the next field, not on the clear button (see #1487). Pass `onClick` to + * reset the field's value to "". Both presets can still be overridden per-site. + */ +export const ClearButton = CloseButton.withProps({ + "aria-label": "Clear", + tabIndex: -1, +}); diff --git a/clients/web/src/components/groups/AppControls/AppControls.tsx b/clients/web/src/components/groups/AppControls/AppControls.tsx index 50f77d32..14d02908 100644 --- a/clients/web/src/components/groups/AppControls/AppControls.tsx +++ b/clients/web/src/components/groups/AppControls/AppControls.tsx @@ -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 ? ( - onSearchChange("")} - /> - ) : null + searchText ? onSearchChange("")} /> : null } /> diff --git a/clients/web/src/components/groups/ExperimentalFeaturesPanel/ExperimentalFeaturesPanel.tsx b/clients/web/src/components/groups/ExperimentalFeaturesPanel/ExperimentalFeaturesPanel.tsx index 7b878f67..6150a0b9 100644 --- a/clients/web/src/components/groups/ExperimentalFeaturesPanel/ExperimentalFeaturesPanel.tsx +++ b/clients/web/src/components/groups/ExperimentalFeaturesPanel/ExperimentalFeaturesPanel.tsx @@ -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 ? ( - onHeaderChange(index, "", header.value)} /> ) : null @@ -264,8 +263,7 @@ export function ExperimentalFeaturesPanel({ rightSectionPointerEvents="auto" rightSection={ header.value ? ( - onHeaderChange(index, header.key, "")} /> ) : null @@ -291,10 +289,7 @@ export function ExperimentalFeaturesPanel({ rightSectionPointerEvents="auto" rightSection={ requestDraft ? ( - onRequestChange("")} - /> + onRequestChange("")} /> ) : null } /> diff --git a/clients/web/src/components/groups/HistoryControls/HistoryControls.test.tsx b/clients/web/src/components/groups/HistoryControls/HistoryControls.test.tsx index f5bb09d2..d727b32c 100644 --- a/clients/web/src/components/groups/HistoryControls/HistoryControls.test.tsx +++ b/clients/web/src/components/groups/HistoryControls/HistoryControls.test.tsx @@ -45,6 +45,14 @@ describe("HistoryControls", () => { expect(onSearchChange).toHaveBeenCalledWith(""); }); + it("keeps the search Clear button out of the keyboard tab order", () => { + renderWithMantine(); + expect(screen.getByRole("button", { name: "Clear" })).toHaveAttribute( + "tabindex", + "-1", + ); + }); + it("renders the method filter placeholder", () => { renderWithMantine(); expect(screen.getByPlaceholderText("All methods")).toBeInTheDocument(); diff --git a/clients/web/src/components/groups/HistoryControls/HistoryControls.tsx b/clients/web/src/components/groups/HistoryControls/HistoryControls.tsx index 12427a95..7e262976 100644 --- a/clients/web/src/components/groups/HistoryControls/HistoryControls.tsx +++ b/clients/web/src/components/groups/HistoryControls/HistoryControls.tsx @@ -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 ? ( - onSearchChange("")} - /> - ) : null + searchText ? onSearchChange("")} /> : null } /> diff --git a/clients/web/src/components/groups/HistoryListPanel/HistoryListPanel.tsx b/clients/web/src/components/groups/HistoryListPanel/HistoryListPanel.tsx index 1d05d8cc..cd9125fe 100644 --- a/clients/web/src/components/groups/HistoryListPanel/HistoryListPanel.tsx +++ b/clients/web/src/components/groups/HistoryListPanel/HistoryListPanel.tsx @@ -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 diff --git a/clients/web/src/components/groups/ImportServerJsonPanel/ImportServerJsonPanel.tsx b/clients/web/src/components/groups/ImportServerJsonPanel/ImportServerJsonPanel.tsx index 6ce84053..49a32794 100644 --- a/clients/web/src/components/groups/ImportServerJsonPanel/ImportServerJsonPanel.tsx +++ b/clients/web/src/components/groups/ImportServerJsonPanel/ImportServerJsonPanel.tsx @@ -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 ? ( - onJsonChange("")} /> + onJsonChange("")} /> ) : null } /> @@ -149,8 +149,7 @@ export function ImportServerJsonPanel({ rightSectionPointerEvents="auto" rightSection={ envVar.value ? ( - onEnvVarChange(envVar.name, "")} /> ) : null @@ -169,10 +168,7 @@ export function ImportServerJsonPanel({ rightSectionPointerEvents="auto" rightSection={ draft.nameOverride ? ( - onServerNameChange("")} - /> + onServerNameChange("")} /> ) : null } /> diff --git a/clients/web/src/components/groups/LogControls/LogControls.tsx b/clients/web/src/components/groups/LogControls/LogControls.tsx index caeedaf1..a6db532a 100644 --- a/clients/web/src/components/groups/LogControls/LogControls.tsx +++ b/clients/web/src/components/groups/LogControls/LogControls.tsx @@ -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 ? ( - onFilterChange("")} - /> - ) : null + filterText ? onFilterChange("")} /> : null } /> diff --git a/clients/web/src/components/groups/NetworkControls/NetworkControls.tsx b/clients/web/src/components/groups/NetworkControls/NetworkControls.tsx index 04b041f0..7bd2be2a 100644 --- a/clients/web/src/components/groups/NetworkControls/NetworkControls.tsx +++ b/clients/web/src/components/groups/NetworkControls/NetworkControls.tsx @@ -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 ? ( - onFilterChange("")} - /> - ) : null + filterText ? onFilterChange("")} /> : null } /> diff --git a/clients/web/src/components/groups/PromptArgumentsForm/PromptArgumentsForm.tsx b/clients/web/src/components/groups/PromptArgumentsForm/PromptArgumentsForm.tsx index cee1d9ab..33ef02ef 100644 --- a/clients/web/src/components/groups/PromptArgumentsForm/PromptArgumentsForm.tsx +++ b/clients/web/src/components/groups/PromptArgumentsForm/PromptArgumentsForm.tsx @@ -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] ? ( - handleChange(arg.name, "")} - /> + handleChange(arg.name, "")} /> ) : null } /> diff --git a/clients/web/src/components/groups/PromptControls/PromptControls.tsx b/clients/web/src/components/groups/PromptControls/PromptControls.tsx index 75c4c08f..c2e8a34b 100644 --- a/clients/web/src/components/groups/PromptControls/PromptControls.tsx +++ b/clients/web/src/components/groups/PromptControls/PromptControls.tsx @@ -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 ? ( - onSearchChange("")} - /> - ) : null + searchText ? onSearchChange("")} /> : null } /> diff --git a/clients/web/src/components/groups/ResourceControls/ResourceControls.tsx b/clients/web/src/components/groups/ResourceControls/ResourceControls.tsx index 7c71a0db..15f94247 100644 --- a/clients/web/src/components/groups/ResourceControls/ResourceControls.tsx +++ b/clients/web/src/components/groups/ResourceControls/ResourceControls.tsx @@ -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 ? ( - onSearchChange("")} - /> + onSearchChange("")} /> ) : null } /> diff --git a/clients/web/src/components/groups/ResourceTemplatePanel/ResourceTemplatePanel.tsx b/clients/web/src/components/groups/ResourceTemplatePanel/ResourceTemplatePanel.tsx index bca5093f..68b2905e 100644 --- a/clients/web/src/components/groups/ResourceTemplatePanel/ResourceTemplatePanel.tsx +++ b/clients/web/src/components/groups/ResourceTemplatePanel/ResourceTemplatePanel.tsx @@ -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] ? ( - handleVariableChange(varName, "")} /> ) : null diff --git a/clients/web/src/components/groups/RootsTable/RootsTable.tsx b/clients/web/src/components/groups/RootsTable/RootsTable.tsx index 64164c5a..949942b2 100644 --- a/clients/web/src/components/groups/RootsTable/RootsTable.tsx +++ b/clients/web/src/components/groups/RootsTable/RootsTable.tsx @@ -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 ? ( - onNewRootDraftChange({ ...newRootDraft, name: "" }) } @@ -115,8 +114,7 @@ export function RootsTable({ rightSectionPointerEvents="auto" rightSection={ newRootDraft.uri ? ( - onNewRootDraftChange({ ...newRootDraft, uri: "" })} /> ) : null diff --git a/clients/web/src/components/groups/SamplingRequestPanel/SamplingRequestPanel.tsx b/clients/web/src/components/groups/SamplingRequestPanel/SamplingRequestPanel.tsx index 50e05fab..c6f950f4 100644 --- a/clients/web/src/components/groups/SamplingRequestPanel/SamplingRequestPanel.tsx +++ b/clients/web/src/components/groups/SamplingRequestPanel/SamplingRequestPanel.tsx @@ -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 ? ( - onResultChange({ ...draftResult, @@ -181,8 +180,7 @@ export function SamplingRequestPanel({ rightSectionPointerEvents="auto" rightSection={ draftResult.model ? ( - onResultChange({ ...draftResult, model: "" })} /> ) : null diff --git a/clients/web/src/components/groups/SchemaForm/SchemaForm.tsx b/clients/web/src/components/groups/SchemaForm/SchemaForm.tsx index 9418fba1..ad6e9a0b 100644 --- a/clients/web/src/components/groups/SchemaForm/SchemaForm.tsx +++ b/clients/web/src/components/groups/SchemaForm/SchemaForm.tsx @@ -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 ? ( - handleFieldChange(fieldName, "")} - /> + handleFieldChange(fieldName, "")} /> ) : null } /> diff --git a/clients/web/src/components/groups/ServerConfigModal/ServerConfigModal.tsx b/clients/web/src/components/groups/ServerConfigModal/ServerConfigModal.tsx index a2976108..4a6cf570 100644 --- a/clients/web/src/components/groups/ServerConfigModal/ServerConfigModal.tsx +++ b/clients/web/src/components/groups/ServerConfigModal/ServerConfigModal.tsx @@ -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 ? ( - setForm((f) => ({ ...f, id: "" }))} /> @@ -321,8 +320,7 @@ export function ServerConfigModal({ rightSectionPointerEvents="auto" rightSection={ form.command ? ( - setForm((f) => ({ ...f, command: "" }))} /> @@ -344,8 +342,7 @@ export function ServerConfigModal({ rightSectionPointerEvents="auto" rightSection={ form.argsText ? ( - setForm((f) => ({ ...f, argsText: "" }))} /> @@ -367,8 +364,7 @@ export function ServerConfigModal({ rightSectionPointerEvents="auto" rightSection={ form.envText ? ( - setForm((f) => ({ ...f, envText: "" }))} /> @@ -387,8 +383,7 @@ export function ServerConfigModal({ rightSectionPointerEvents="auto" rightSection={ form.cwd ? ( - setForm((f) => ({ ...f, cwd: "" }))} /> @@ -410,8 +405,7 @@ export function ServerConfigModal({ rightSectionPointerEvents="auto" rightSection={ form.url ? ( - setForm((f) => ({ ...f, url: "" }))} /> diff --git a/clients/web/src/components/groups/ServerSettingsForm/ServerSettingsForm.tsx b/clients/web/src/components/groups/ServerSettingsForm/ServerSettingsForm.tsx index ed4452ed..cbf3e615 100644 --- a/clients/web/src/components/groups/ServerSettingsForm/ServerSettingsForm.tsx +++ b/clients/web/src/components/groups/ServerSettingsForm/ServerSettingsForm.tsx @@ -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 ? ( - onChange(index, "", item.value)} - /> + onChange(index, "", item.value)} /> ) : null } /> @@ -105,10 +102,7 @@ function KeyValueRows({ rightSectionPointerEvents="auto" rightSection={ item.value ? ( - onChange(index, item.key, "")} - /> + onChange(index, item.key, "")} /> ) : null } /> @@ -145,8 +139,7 @@ function RootRows({ rightSectionPointerEvents="auto" rightSection={ root.uri ? ( - onChange(index, "", root.name ?? "")} /> ) : null @@ -159,10 +152,7 @@ function RootRows({ rightSectionPointerEvents="auto" rightSection={ root.name ? ( - onChange(index, root.uri, "")} - /> + onChange(index, root.uri, "")} /> ) : null } /> @@ -372,8 +362,7 @@ export function ServerSettingsForm({ rightSectionPointerEvents="auto" rightSection={ settings.oauthClientId ? ( - onOAuthChange({ ...currentOAuth(), clientId: "" }) } @@ -394,8 +383,7 @@ export function ServerSettingsForm({ rightSectionPointerEvents="auto" rightSection={ settings.oauthClientSecret ? ( - onOAuthChange({ ...currentOAuth(), clientSecret: "" }) } @@ -415,8 +403,7 @@ export function ServerSettingsForm({ rightSectionPointerEvents="auto" rightSection={ settings.oauthScopes ? ( - onOAuthChange({ ...currentOAuth(), scopes: "" }) } diff --git a/clients/web/src/components/groups/TaskControls/TaskControls.tsx b/clients/web/src/components/groups/TaskControls/TaskControls.tsx index df7b8df2..aa727484 100644 --- a/clients/web/src/components/groups/TaskControls/TaskControls.tsx +++ b/clients/web/src/components/groups/TaskControls/TaskControls.tsx @@ -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 ? ( - onSearchChange("")} - /> - ) : null + searchText ? onSearchChange("")} /> : null } /> diff --git a/clients/web/src/components/groups/ToolControls/ToolControls.tsx b/clients/web/src/components/groups/ToolControls/ToolControls.tsx index 57a25a4c..6eb6620a 100644 --- a/clients/web/src/components/groups/ToolControls/ToolControls.tsx +++ b/clients/web/src/components/groups/ToolControls/ToolControls.tsx @@ -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 ? ( - onSearchChange("")} - /> - ) : null + searchText ? onSearchChange("")} /> : null } /> diff --git a/clients/web/src/theme/Accordion.ts b/clients/web/src/theme/Accordion.ts index 39b247aa..44e579f6 100644 --- a/clients/web/src/theme/Accordion.ts +++ b/clients/web/src/theme/Accordion.ts @@ -1,17 +1,25 @@ import { Accordion } from "@mantine/core"; export const ThemeAccordion = Accordion.extend({ - // The `disclosure` variant drives two behaviours via App.css (see #1462): + // The `disclosure` variant drives three behaviours via App.css (see #1462): // - `disclosure-chevron` on the chevron slot rotates a right-pointing arrow // 90° (right → down) on open, instead of Mantine's default 180° flip. // - `disclosure-sections` on the root makes the accordion a full-height // flex column: section headers stay pinned and each open section's panel // scrolls within its own (item-count-weighted) share of the space, so // nothing scrolls until the panel is full. + // - `filter-toggle` on the control gives the section headers the same + // outline-on-hover treatment as the FilterToggleButton and the History + // section headers: a thin border on hover (rather than a background fill) + // and a filled background when the section is open (`aria-expanded`). // Pair it with `chevron={}` and per-item `flex` weights. classNames: (_theme, props) => { if (props.variant === "disclosure") - return { root: "disclosure-sections", chevron: "disclosure-chevron" }; + return { + root: "disclosure-sections", + chevron: "disclosure-chevron", + control: "filter-toggle", + }; return {}; }, }); diff --git a/clients/web/src/theme/Autocomplete.ts b/clients/web/src/theme/Autocomplete.ts index 439c37f3..1fdaef40 100644 --- a/clients/web/src/theme/Autocomplete.ts +++ b/clients/web/src/theme/Autocomplete.ts @@ -8,5 +8,9 @@ export const ThemeAutocomplete = Autocomplete.extend({ // collapses any open dropdown automatically. Per-site `clearable={false}` // can opt out where a clear button doesn't make sense. clearable: true, + // Keep the clear button out of the keyboard tab order so tabbing through a + // form moves to the next field, not onto the clear button (it stays + // mouse-clickable). + clearButtonProps: { tabIndex: -1 }, }, }); diff --git a/clients/web/src/theme/Group.ts b/clients/web/src/theme/Group.ts index 5c63caa3..de6623cf 100644 --- a/clients/web/src/theme/Group.ts +++ b/clients/web/src/theme/Group.ts @@ -1,17 +1,16 @@ import { Group } from "@mantine/core"; export const ThemeGroup = Group.extend({ - // `sectionHeader` styles a Group as a collapsible-section header "pleat": - // rounded, with the same hover highlight as the listItem toggles. The active - // (open) background is passed per-instance via the `bg` prop. + // `sectionHeader` styles a Group as a collapsible-section header "pleat". It + // shares the `.filter-toggle` treatment used by the FilterToggleButton: a thin + // outline on hover (rather than a background fill) so hover stays visually + // distinct from the active state. The active (open) background is passed + // per-instance via the `bg` prop. The border-radius and the reserved + // transparent border both come from `.filter-toggle` in App.css — the border + // must NOT be set here as an inline style, since inline styles outrank the + // stylesheet `:hover` rule (see #1460). classNames: (_theme, props) => { - if (props.variant === "sectionHeader") return { root: "list-item" }; + if (props.variant === "sectionHeader") return { root: "filter-toggle" }; return {}; }, - styles: (_theme, props) => { - if (props.variant === "sectionHeader") { - return { root: { borderRadius: "var(--mantine-radius-md)" } }; - } - return { root: {} }; - }, });