Skip to content

Commit 45823ec

Browse files
authored
Merge pull request #2139 from modelcontextprotocol/v2/feat/2138-enlarge-button-tab-order
fix(web): take the enlarge button out of the tab order
2 parents dd67164 + d49798c commit 45823ec

5 files changed

Lines changed: 608 additions & 29 deletions

File tree

‎clients/web/src/components/elements/EnlargeButton/EnlargeButton.stories.tsx‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,15 @@ const meta: Meta<typeof EnlargeButton> = {
1111
export default meta;
1212
type Story = StoryObj<typeof EnlargeButton>;
1313

14-
// Default: labelled "Enlarge", reachable by keyboard, and clickable.
14+
// Default: labelled "Enlarge", clickable, and out of the tab order (#2138) —
15+
// a form's string fields each carry one, so leaving them in doubled the tab
16+
// stops between adjacent fields. SchemaForm binds Enter on the field itself as
17+
// the keyboard route in.
1518
export const Default: Story = {
1619
play: async ({ canvasElement }) => {
1720
const canvas = within(canvasElement);
1821
const button = await canvas.findByRole("button", { name: "Enlarge" });
19-
await expect(button).not.toHaveAttribute("tabindex", "-1");
22+
await expect(button).toHaveAttribute("tabindex", "-1");
2023
await userEvent.click(button);
2124
},
2225
};

‎clients/web/src/components/elements/EnlargeButton/EnlargeButton.test.tsx‎

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,33 @@ describe("EnlargeButton", () => {
99
expect(screen.getByRole("button", { name: "Enlarge" })).toBeInTheDocument();
1010
});
1111

12-
it("stays in the keyboard tab order", () => {
12+
// Out of the tab order like ClearButton (#2138) — a form's string fields each
13+
// carry one, so leaving them in doubled the stops between adjacent fields.
14+
it("is out of the keyboard tab order", () => {
1315
renderWithMantine(<EnlargeButton onClick={vi.fn()} />);
14-
expect(screen.getByRole("button", { name: "Enlarge" })).not.toHaveAttribute(
16+
expect(screen.getByRole("button", { name: "Enlarge" })).toHaveAttribute(
1517
"tabindex",
1618
"-1",
1719
);
1820
});
1921

22+
// The property that matters is skipped-by-Tab, not the attribute that
23+
// implements it — asserted by driving a real Tab past a neighbouring input.
24+
it("is skipped when tabbing through the surrounding form", async () => {
25+
const user = userEvent.setup();
26+
renderWithMantine(
27+
<>
28+
<input aria-label="Before" />
29+
<EnlargeButton onClick={vi.fn()} />
30+
<input aria-label="After" />
31+
</>,
32+
);
33+
34+
screen.getByRole("textbox", { name: "Before" }).focus();
35+
await user.tab();
36+
expect(screen.getByRole("textbox", { name: "After" })).toHaveFocus();
37+
});
38+
2039
it("invokes onClick when clicked", async () => {
2140
const user = userEvent.setup();
2241
const onClick = vi.fn();

‎clients/web/src/components/elements/EnlargeButton/EnlargeButton.tsx‎

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ const EnlargeActionIcon = ActionIcon.withProps({
2323
variant: "subtle",
2424
color: "gray",
2525
size: "sm",
26+
tabIndex: -1,
2627
});
2728

2829
/**
@@ -35,10 +36,18 @@ const EnlargeActionIcon = ActionIcon.withProps({
3536
* the newlines already typed, and either answer (silently discard them, or keep
3637
* a value the field can no longer display) is worse than staying enlarged.
3738
*
38-
* Unlike ClearButton it stays in the keyboard tab order. Clearing a field has a
39-
* keyboard equivalent (select-all, delete), so #1487 could take that button out
40-
* of the tab order without cost; entering multiline mode has none, so removing
41-
* this one would put the feature out of reach of keyboard users entirely.
39+
* `tabIndex={-1}` keeps it clickable but out of the keyboard tab order, the same
40+
* as ClearButton (#1487) — a form's string fields each carry one, so leaving
41+
* them in doubled the tab stops between one field and the next (#2138).
42+
*
43+
* That is only affordable because the keyboard keeps its own way in: SchemaForm
44+
* binds Enter on the single-line field, which enlarges it and enters the
45+
* newline in one go. Nothing else is listening for that key there, and it is
46+
* the one a user presses trying to type the newline the field cannot hold — so
47+
* the gesture that fails is the gesture that fixes it. Do not take this button
48+
* out of the tab order anywhere that binding is absent; unlike clearing
49+
* (select-all, delete) there is no built-in equivalent, and multiline mode
50+
* would simply be unreachable by keyboard.
4251
*/
4352
export function EnlargeButton({
4453
onClick,

0 commit comments

Comments
 (0)