* 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:
parent
2488ca5e38
commit
bb32d1db5f
3 changed files with 180 additions and 196 deletions
|
|
@ -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<typeof ClearButton> = {
|
||||
title: "Elements/ClearButton",
|
||||
component: ClearButton,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof ClearButton>;
|
||||
|
||||
// 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);
|
||||
},
|
||||
};
|
||||
|
|
@ -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(<ClearButton />);
|
||||
expect(screen.getByRole("button", { name: "Clear" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("is removed from the keyboard tab order", () => {
|
||||
renderWithMantine(<ClearButton />);
|
||||
expect(screen.getByRole("button", { name: "Clear" })).toHaveAttribute(
|
||||
"tabindex",
|
||||
"-1",
|
||||
);
|
||||
});
|
||||
|
||||
it("invokes onClick when clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onClick = vi.fn();
|
||||
renderWithMantine(<ClearButton onClick={onClick} />);
|
||||
await user.click(screen.getByRole("button", { name: "Clear" }));
|
||||
expect(onClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("allows the accessible name to be overridden", () => {
|
||||
renderWithMantine(<ClearButton aria-label="Clear search" />);
|
||||
expect(
|
||||
screen.getByRole("button", { name: "Clear search" }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
@ -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,
|
||||
});
|
||||
Loading…
Reference in a new issue