fix(web): take the enlarge button out of the tab order
Every string field in a tool or elicitation form carries an enlarge
button in its right section, so tabbing through a form stopped on one
between each pair of fields. Give it tabIndex={-1}, as #1487 did for the
clear button.
That button was deliberately left in the tab order by #2042, on the
grounds that clearing has a keyboard equivalent and enlarging has none.
So give it one: Enter on the focused single-line field enlarges it and
enters the newline. Nothing was listening for that key there — no
consumer renders SchemaForm inside a <form> — and it is the key a user
presses trying to type the newline the input swallows, which is the
failure #2042 exists to fix.
Enter enters the newline rather than only reshaping the field, since
consuming the keystroke leaves the next word running on from the last. A
field already at its maxLength is enlarged without one. Shift+Enter
enlarges too; the Ctrl/Cmd/Alt chords are left free for a consumer to
bind to running the tool. Clicking the button still only enlarges.
Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
dd67164bea
commit
4c338a37e9
3 changed files with 297 additions and 18 deletions
|
|
@ -11,12 +11,15 @@ const meta: Meta<typeof EnlargeButton> = {
|
|||
export default meta;
|
||||
type Story = StoryObj<typeof EnlargeButton>;
|
||||
|
||||
// Default: labelled "Enlarge", reachable by keyboard, and clickable.
|
||||
// Default: labelled "Enlarge", clickable, and out of the tab order (#2138) —
|
||||
// a form's string fields each carry one, so leaving them in doubled the tab
|
||||
// stops between adjacent fields. SchemaForm binds Enter on the field itself as
|
||||
// the keyboard route in.
|
||||
export const Default: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const button = await canvas.findByRole("button", { name: "Enlarge" });
|
||||
await expect(button).not.toHaveAttribute("tabindex", "-1");
|
||||
await expect(button).toHaveAttribute("tabindex", "-1");
|
||||
await userEvent.click(button);
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -9,14 +9,33 @@ describe("EnlargeButton", () => {
|
|||
expect(screen.getByRole("button", { name: "Enlarge" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("stays in the keyboard tab order", () => {
|
||||
// Out of the tab order like ClearButton (#2138) — a form's string fields each
|
||||
// carry one, so leaving them in doubled the stops between adjacent fields.
|
||||
it("is out of the keyboard tab order", () => {
|
||||
renderWithMantine(<EnlargeButton onClick={vi.fn()} />);
|
||||
expect(screen.getByRole("button", { name: "Enlarge" })).not.toHaveAttribute(
|
||||
expect(screen.getByRole("button", { name: "Enlarge" })).toHaveAttribute(
|
||||
"tabindex",
|
||||
"-1",
|
||||
);
|
||||
});
|
||||
|
||||
// The property that matters is skipped-by-Tab, not the attribute that
|
||||
// implements it — asserted by driving a real Tab past a neighbouring input.
|
||||
it("is skipped when tabbing through the surrounding form", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithMantine(
|
||||
<>
|
||||
<input aria-label="Before" />
|
||||
<EnlargeButton onClick={vi.fn()} />
|
||||
<input aria-label="After" />
|
||||
</>,
|
||||
);
|
||||
|
||||
screen.getByRole("textbox", { name: "Before" }).focus();
|
||||
await user.tab();
|
||||
expect(screen.getByRole("textbox", { name: "After" })).toHaveFocus();
|
||||
});
|
||||
|
||||
it("invokes onClick when clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
const onClick = vi.fn();
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ const EnlargeActionIcon = ActionIcon.withProps({
|
|||
variant: "subtle",
|
||||
color: "gray",
|
||||
size: "sm",
|
||||
tabIndex: -1,
|
||||
});
|
||||
|
||||
/**
|
||||
|
|
@ -35,10 +36,18 @@ const EnlargeActionIcon = ActionIcon.withProps({
|
|||
* the newlines already typed, and either answer (silently discard them, or keep
|
||||
* a value the field can no longer display) is worse than staying enlarged.
|
||||
*
|
||||
* Unlike ClearButton it stays in the keyboard tab order. Clearing a field has a
|
||||
* keyboard equivalent (select-all, delete), so #1487 could take that button out
|
||||
* of the tab order without cost; entering multiline mode has none, so removing
|
||||
* this one would put the feature out of reach of keyboard users entirely.
|
||||
* `tabIndex={-1}` keeps it clickable but out of the keyboard tab order, the same
|
||||
* as ClearButton (#1487) — a form's string fields each carry one, so leaving
|
||||
* them in doubled the tab stops between one field and the next (#2138).
|
||||
*
|
||||
* That is only affordable because the keyboard keeps its own way in: SchemaForm
|
||||
* binds Enter on the single-line field, which enlarges it and enters the
|
||||
* newline in one go. Nothing else is listening for that key there, and it is
|
||||
* the one a user presses trying to type the newline the field cannot hold — so
|
||||
* the gesture that fails is the gesture that fixes it. Do not take this button
|
||||
* out of the tab order anywhere that binding is absent; unlike clearing
|
||||
* (select-all, delete) there is no built-in equivalent, and multiline mode
|
||||
* would simply be unreachable by keyboard.
|
||||
*/
|
||||
export function EnlargeButton({
|
||||
onClick,
|
||||
|
|
|
|||
Loading…
Reference in a new issue