fix(site): WCAG 2.1 AA accessibility remediation for core frontend flows (#22673)

This PR is several accessibility improvements researched by Mux.
Manually tested and these changes should be mostly harmless.


## Summary

Targeted WCAG 2.1 AA accessibility remediation across core frontend user
flows: login, dashboard navigation, audit interactions, settings, and
workspace parameter inputs.

### Changes

#### Navigation, keyboard & focus visibility (WCAG 2.4.1, 2.1.1, 2.4.7,
1.4.11)

- **DashboardLayout**: Added "Skip to main content" link (visually
hidden, visible on focus) with `#main-content` target on the main outlet
container.
- **AuditLogRow**: Expanded keyboard handler so both Enter and Space
toggle expandable audit details (with `preventDefault` on Space to
prevent scroll).
- **model-selector**: Removed `focus:ring-0 focus-visible:ring-0` from
`SelectTrigger` to restore the default visible focus indicator.

#### Forms & input assistance (WCAG 3.3.1, 3.3.2, 1.3.5)

- **PasswordSignInForm**: Wired `aria-invalid` and `aria-describedby` on
email/password inputs, pointing to stable-ID error elements
(`signin-email-error`, `signin-password-error`).
- **AccountForm**: Added `autoComplete="name"` to the Name field.

#### Name/role/value & status messages (WCAG 1.1.1, 4.1.2, 4.1.3, 1.3.1)

- **PortForwardButton**: Added `aria-label="Delete shared port"` to the
icon-only delete button.
- **DynamicParameter**: Replaced mouse-only peek-and-hold reveal with a
persistent keyboard-accessible toggle. Added dynamic `aria-label` ("Show
value"/"Hide value") and `aria-pressed`.
- **Tabs**: Removed incorrect `role="tablist"` (these are route
navigation links, not ARIA tabs). Added `aria-current="page"` on the
active `TabLink`.
- **Loader**: Wrapped spinner in `role="status" aria-live="polite"`
container with an `aria-label` for screen reader announcements.
- **Alert**: Changed `AlertTitle` from `<h1>` to `<h2>` to avoid
multiple page-level headings.

### Testing

- **9 Vitest test files** (22 tests) — all new or extended to cover the
a11y changes.
- **1 Jest test file** (38 tests) — DynamicParameter tests updated for
toggle semantics + keyboard activation.
- `pnpm lint:types` 
- `pnpm check` (Biome lint + format) 

### Files changed

| File | Change |
|------|--------|
| `site/src/modules/dashboard/DashboardLayout.tsx` | Skip link +
`#main-content` id |
| `site/src/modules/dashboard/DashboardLayout.test.tsx` | Skip link
assertions |
| `site/src/pages/AuditPage/AuditLogRow/AuditLogRow.tsx` | Space + Enter
keyboard handling |
| `site/src/pages/AuditPage/AuditPage.test.tsx` | Keyboard toggle tests
|
| `site/src/components/ai-elements/model-selector.tsx` | Remove focus
ring suppression |
| `site/src/components/ai-elements/model-selector.test.tsx` | **New** —
focus ring assertion |
| `site/src/pages/LoginPage/PasswordSignInForm.tsx` | aria-invalid +
aria-describedby |
| `site/src/pages/LoginPage/LoginPage.test.tsx` | Error association
tests |
| `site/src/pages/UserSettingsPage/AccountPage/AccountForm.tsx` |
autoComplete="name" |
| `site/src/pages/UserSettingsPage/AccountPage/AccountForm.test.tsx` |
Autocomplete assertion |
| `site/src/modules/resources/PortForwardButton.tsx` | aria-label on
delete button |
| `site/src/modules/resources/PortForwardButton.test.tsx` | **New** —
accessible name test |
| `site/src/modules/workspaces/DynamicParameter/DynamicParameter.tsx` |
Keyboard toggle + ARIA |
|
`site/src/modules/workspaces/DynamicParameter/DynamicParameter.jest.tsx`
| Toggle semantics tests |
| `site/src/components/Tabs/Tabs.tsx` | Remove tablist role, add
aria-current |
| `site/src/components/Tabs/Tabs.test.tsx` | **New** —
tablist/aria-current tests |
| `site/src/components/Loader/Loader.tsx` | role="status" + aria-live |
| `site/src/components/Loader/Loader.test.tsx` | **New** — status
semantics tests |
| `site/src/components/Alert/Alert.tsx` | h1 → h2 |
| `site/src/components/Alert/Alert.test.tsx` | **New** — heading level
test |
This commit is contained in:
Jaayden Halko
2026-03-06 22:19:13 +07:00
committed by GitHub
parent 752e6ecc16
commit ec48636ba8
21 changed files with 401 additions and 35 deletions
+20
View File
@@ -0,0 +1,20 @@
import { render, screen } from "@testing-library/react";
import { Alert, AlertDescription, AlertTitle } from "./Alert";
describe("AlertTitle", () => {
it("renders as an h2 heading", () => {
render(
<Alert>
<AlertTitle>Deployment warning</AlertTitle>
<AlertDescription>Something needs your attention.</AlertDescription>
</Alert>,
);
expect(
screen.getByRole("heading", { level: 2, name: "Deployment warning" }),
).toBeInTheDocument();
expect(
screen.queryByRole("heading", { level: 1, name: "Deployment warning" }),
).not.toBeInTheDocument();
});
});
+2 -2
View File
@@ -135,9 +135,9 @@ export const AlertDescription: React.FC<React.PropsWithChildren> = ({
);
};
export const AlertTitle: React.FC<React.ComponentPropsWithRef<"h1">> = ({
export const AlertTitle: React.FC<React.ComponentPropsWithRef<"h2">> = ({
className,
...props
}) => {
return <h1 className={cn("m-0 text-sm font-medium", className)} {...props} />;
return <h2 className={cn("m-0 text-sm font-medium", className)} {...props} />;
};
@@ -0,0 +1,19 @@
import { render, screen } from "@testing-library/react";
import { Loader } from "./Loader";
describe("Loader", () => {
it("announces loading status politely", () => {
render(<Loader />);
expect(screen.getByRole("status")).toHaveAttribute("aria-live", "polite");
expect(screen.getByLabelText("Loading")).toBeInTheDocument();
});
it("applies custom spinner labels when provided", () => {
render(<Loader label="Loading workspace resources" />);
expect(
screen.getByLabelText("Loading workspace resources"),
).toBeInTheDocument();
});
});
+7 -3
View File
@@ -14,21 +14,25 @@ interface LoaderProps extends HTMLAttributes<HTMLDivElement> {
export const Loader: FC<LoaderProps> = ({
fullscreen,
size = "lg",
label = "Loading...",
label,
className,
...attrs
}) => {
const resolvedLabel = label ?? "Loading";
return (
<div
data-testid="loader"
{...attrs}
role="status"
aria-live="polite"
data-testid="loader"
className={cn(
"flex items-center justify-center",
fullscreen ? "absolute inset-0 bg-surface-primary" : "w-full p-8",
className,
)}
>
<Spinner aria-label={label} size={size} loading={true} />
<Spinner aria-label={resolvedLabel} size={size} loading={true} />
</div>
);
};
+40
View File
@@ -0,0 +1,40 @@
import { render, screen } from "@testing-library/react";
import { MemoryRouter } from "react-router";
import { TabLink, Tabs, TabsList } from "./Tabs";
const renderTabs = (active = "overview") => {
render(
<MemoryRouter>
<Tabs active={active}>
<TabsList>
<TabLink to="/overview" value="overview">
Overview
</TabLink>
<TabLink to="/settings" value="settings">
Settings
</TabLink>
</TabsList>
</Tabs>
</MemoryRouter>,
);
};
describe("Tabs", () => {
it("does not expose tablist semantics for link navigation", () => {
renderTabs();
expect(screen.queryByRole("tablist")).not.toBeInTheDocument();
});
it("marks only the active tab link as the current page", () => {
renderTabs("overview");
expect(screen.getByRole("link", { name: "Overview" })).toHaveAttribute(
"aria-current",
"page",
);
expect(screen.getByRole("link", { name: "Settings" })).not.toHaveAttribute(
"aria-current",
);
});
});
+2 -5
View File
@@ -96,11 +96,7 @@ export const TabsList: FC<TabsListProps> = ({ className, ...props }) => {
return (
<div ref={listRef} className="relative">
<div
role="tablist"
className={cn("flex items-baseline gap-6", className)}
{...props}
/>
<div className={cn("flex items-baseline gap-6", className)} {...props} />
<div
ref={indicatorRef}
className="absolute bottom-0 h-px bg-surface-invert-primary opacity-0 transition-all duration-300 ease-in-out"
@@ -128,6 +124,7 @@ export const TabLink: FC<TabLinkProps> = ({
return (
<Link
data-active={isActive}
aria-current={isActive ? "page" : undefined}
{...linkProps}
className={cn(
"text-sm text-content-secondary no-underline font-medium py-3 px-1 hover:text-content-primary rounded-md",
@@ -0,0 +1,26 @@
import { render, screen } from "@testing-library/react";
import { ModelSelector, type ModelSelectorOption } from "./model-selector";
const mockModelOptions: readonly ModelSelectorOption[] = [
{
id: "gpt-4o-mini",
provider: "openai",
model: "gpt-4o-mini",
displayName: "GPT-4o mini",
},
];
test("does not suppress focus ring styles on the model selector trigger", () => {
render(
<ModelSelector
options={mockModelOptions}
value="gpt-4o-mini"
onValueChange={vi.fn()}
/>,
);
const trigger = screen.getByRole("combobox");
expect(trigger.className).not.toContain("focus:ring-0");
expect(trigger.className).not.toContain("focus-visible:ring-0");
});
@@ -93,7 +93,7 @@ export const ModelSelector: FC<ModelSelectorProps> = ({
<Select value={value} onValueChange={onValueChange} disabled={isDisabled}>
<SelectTrigger
className={cn(
"h-8 w-auto gap-1.5 border-none bg-transparent px-1 text-xs shadow-none transition-colors hover:bg-transparent hover:text-content-primary [&>svg]:transition-colors [&>svg]:hover:text-content-primary focus:ring-0 focus-visible:ring-0",
"h-8 w-auto gap-1.5 border-none bg-transparent px-1 text-xs shadow-none transition-colors hover:bg-transparent hover:text-content-primary [&>svg]:transition-colors [&>svg]:hover:text-content-primary",
className,
)}
>
@@ -1,6 +1,10 @@
import { renderWithAuth } from "testHelpers/renderHelpers";
import {
renderWithAuth,
waitForLoaderToBeRemoved,
} from "testHelpers/renderHelpers";
import { server } from "testHelpers/server";
import { screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { HttpResponse, http } from "msw";
import { DashboardLayout } from "./DashboardLayout";
@@ -19,3 +23,40 @@ test("Show the new Coder version notification", async () => {
});
await screen.findByTestId("update-check-snackbar");
});
test("renders a skip link before navigation content", async () => {
renderWithAuth(<DashboardLayout />, {
children: [{ element: <h1>Test page</h1> }],
});
await waitForLoaderToBeRemoved();
const skipToContentLink = screen.getByRole("link", {
name: "Skip to main content",
});
const navigation = screen.getAllByRole("navigation")[0];
const mainContent = document.getElementById("main-content");
expect(skipToContentLink).toHaveAttribute("href", "#main-content");
expect(mainContent).toHaveAttribute("tabindex", "-1");
expect(
skipToContentLink.compareDocumentPosition(navigation) &
Node.DOCUMENT_POSITION_FOLLOWING,
).toBeTruthy();
});
test("moves focus to main content when skip link is clicked", async () => {
renderWithAuth(<DashboardLayout />, {
children: [{ element: <h1>Test page</h1> }],
});
await waitForLoaderToBeRemoved();
const user = userEvent.setup();
const skipToContentLink = screen.getByRole("link", {
name: "Skip to main content",
});
const mainContent = document.getElementById("main-content");
expect(mainContent).not.toBeNull();
await user.click(skipToContentLink);
expect(mainContent).toHaveFocus();
});
+21 -1
View File
@@ -8,6 +8,7 @@ import { AnnouncementBanners } from "modules/dashboard/AnnouncementBanners/Annou
import { LicenseBanner } from "modules/dashboard/LicenseBanner/LicenseBanner";
import { type FC, type HTMLAttributes, Suspense } from "react";
import { Outlet } from "react-router";
import { cn } from "utils/cn";
import { docs } from "utils/docs";
import { DeploymentBanner } from "./DeploymentBanner/DeploymentBanner";
import { Navbar } from "./Navbar/Navbar";
@@ -24,9 +25,28 @@ export const DashboardLayout: FC = () => {
<AnnouncementBanners />
<div className="flex flex-col min-h-screen justify-between">
{/* biome-ignore lint/a11y/useValidAnchor: Skip links use fragment anchors by design. */}
<a
href="#main-content"
onClick={(e) => {
e.preventDefault();
const main = document.getElementById("main-content");
main?.focus();
}}
className="sr-only focus:not-sr-only focus:absolute focus:z-50 focus:p-4 focus:bg-surface-primary focus:text-content-primary"
>
Skip to main content
</a>
<Navbar />
<div className="relative flex flex-col flex-1 min-h-0 overflow-y-auto">
<div
id="main-content"
tabIndex={-1}
className={cn(
"relative flex flex-col flex-1 min-h-0 overflow-y-auto",
"focus:outline-none",
)}
>
<Suspense fallback={<Loader />}>
<Outlet />
</Suspense>
@@ -0,0 +1,31 @@
import {
MockListeningPortsResponse,
MockSharedPortsResponse,
MockTemplate,
MockWorkspace,
MockWorkspaceAgent,
} from "testHelpers/entities";
import { render } from "testHelpers/renderHelpers";
import { screen } from "@testing-library/react";
import { PortForwardPopoverView } from "./PortForwardButton";
describe("PortForwardPopoverView", () => {
it("adds an accessible name to each shared-port delete button", () => {
render(
<PortForwardPopoverView
host="coder.test"
workspace={MockWorkspace}
agent={MockWorkspaceAgent}
template={MockTemplate}
sharedPorts={MockSharedPortsResponse.shares}
listeningPorts={MockListeningPortsResponse.ports}
portSharingControlsEnabled
refetchSharedPorts={vi.fn()}
/>,
);
expect(
screen.getAllByRole("button", { name: "Delete shared port" }),
).toHaveLength(MockSharedPortsResponse.shares.length);
});
});
@@ -527,6 +527,7 @@ export const PortForwardPopoverView: FC<PortForwardPopoverViewProps> = ({
<Button
size="icon"
variant="subtle"
aria-label="Delete shared port"
onClick={async () => {
await deleteSharedPortMutation.mutateAsync({
agent_name: agent.name,
@@ -835,7 +835,7 @@ describe("DynamicParameter", () => {
},
});
it("renders a password field by default and toggles visibility on mouse events", async () => {
it("toggles visibility with persistent pressed-state semantics", async () => {
render(
<DynamicParameter
parameter={mockMaskedInputParameter}
@@ -847,12 +847,49 @@ describe("DynamicParameter", () => {
const input = screen.getByLabelText("Masked Input Parameter");
expect(input).toHaveAttribute("type", "password");
const toggleButton = screen.getByRole("button");
fireEvent.mouseDown(toggleButton);
const showButton = screen.getByRole("button", { name: "Show value" });
expect(showButton).toHaveAttribute("aria-pressed", "false");
await userEvent.click(showButton);
expect(input).toHaveAttribute("type", "text");
fireEvent.mouseUp(toggleButton);
const hideButton = screen.getByRole("button", { name: "Hide value" });
expect(hideButton).toHaveAttribute("aria-pressed", "true");
await userEvent.click(hideButton);
expect(input).toHaveAttribute("type", "password");
expect(
screen.getByRole("button", { name: "Show value" }),
).toHaveAttribute("aria-pressed", "false");
});
it("supports keyboard activation for toggling masked values", async () => {
render(
<DynamicParameter
parameter={mockMaskedInputParameter}
value="secret123"
onChange={mockOnChange}
/>,
);
const input = screen.getByLabelText("Masked Input Parameter");
const showButton = screen.getByRole("button", { name: "Show value" });
showButton.focus();
await userEvent.keyboard("[Enter]");
expect(input).toHaveAttribute("type", "text");
expect(
screen.getByRole("button", { name: "Hide value" }),
).toHaveAttribute("aria-pressed", "true");
const hideButton = screen.getByRole("button", { name: "Hide value" });
hideButton.focus();
await userEvent.keyboard("[Space]");
expect(input).toHaveAttribute("type", "password");
expect(
screen.getByRole("button", { name: "Show value" }),
).toHaveAttribute("aria-pressed", "false");
});
});
@@ -540,9 +540,9 @@ const MaskableInput: FC<MaskableInputProps> = ({
type="button"
variant="subtle"
size="icon"
onMouseDown={() => setShowMaskedInput(true)}
onMouseOut={() => setShowMaskedInput(false)}
onMouseUp={() => setShowMaskedInput(false)}
aria-label={showMaskedInput ? "Hide value" : "Show value"}
aria-pressed={showMaskedInput}
onClick={() => setShowMaskedInput((value) => !value)}
disabled={disabled}
>
{showMaskedInput ? (
@@ -594,9 +594,9 @@ const MaskableTextArea: FC<MaskableInputProps> = ({
type="button"
variant="subtle"
size="icon"
onMouseDown={() => setShowMaskedInput(true)}
onMouseOut={() => setShowMaskedInput(false)}
onMouseUp={() => setShowMaskedInput(false)}
aria-label={showMaskedInput ? "Hide value" : "Show value"}
aria-pressed={showMaskedInput}
onClick={() => setShowMaskedInput((value) => !value)}
disabled={disabled}
>
{showMaskedInput ? (
@@ -80,7 +80,8 @@ export const AuditLogRow: FC<AuditLogRowProps> = ({
role="button"
onClick={toggle}
onKeyDown={(event) => {
if (event.key === "Enter") {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
toggle();
}
}}
+59 -1
View File
@@ -8,7 +8,13 @@ import {
waitForLoaderToBeRemoved,
} from "testHelpers/renderHelpers";
import { server } from "testHelpers/server";
import { screen, waitFor } from "@testing-library/react";
import {
createEvent,
fireEvent,
screen,
waitFor,
within,
} from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { API } from "api/api";
import type { AuditLogsRequest } from "api/typesGenerated";
@@ -80,6 +86,58 @@ describe("AuditPage", () => {
screen.getByTestId(`audit-log-row-${MockAuditLog2.id}`);
});
it("toggles an expandable audit row with Enter", async () => {
vi.spyOn(API, "getAuditLogs").mockResolvedValue({
audit_logs: [MockAuditLog],
count: 1,
});
await renderPage();
const row = screen.getByTestId(`audit-log-row-${MockAuditLog.id}`);
const expandableRowButton = within(row).getByRole("button");
expect(screen.queryByText(/ttl:/i)).not.toBeInTheDocument();
fireEvent.keyDown(expandableRowButton, { key: "Enter" });
expect(screen.getAllByText(/ttl:/i)).toHaveLength(2);
fireEvent.keyDown(expandableRowButton, { key: "Enter" });
await waitFor(() => {
expect(screen.queryByText(/ttl:/i)).not.toBeInTheDocument();
});
});
it("toggles an expandable audit row with Space and prevents default", async () => {
vi.spyOn(API, "getAuditLogs").mockResolvedValue({
audit_logs: [MockAuditLog],
count: 1,
});
await renderPage();
const row = screen.getByTestId(`audit-log-row-${MockAuditLog.id}`);
const expandableRowButton = within(row).getByRole("button");
const spaceEvent = createEvent.keyDown(expandableRowButton, {
key: " ",
code: "Space",
});
const preventDefaultSpy = vi.spyOn(spaceEvent, "preventDefault");
fireEvent(expandableRowButton, spaceEvent);
expect(preventDefaultSpy).toHaveBeenCalled();
expect(screen.getAllByText(/ttl:/i)).toHaveLength(2);
fireEvent.keyDown(expandableRowButton, { key: " " });
await waitFor(() => {
expect(screen.queryByText(/ttl:/i)).not.toBeInTheDocument();
});
});
describe("Filtering", () => {
it("filters by URL", async () => {
const getAuditLogsSpy = vi
@@ -48,6 +48,48 @@ describe("LoginPage", () => {
expect(errorMessage).toBeDefined();
});
it("associates email validation errors with the email input", async () => {
// When
render(<LoginPage />);
await waitForLoaderToBeRemoved();
const emailInput = screen.getByLabelText(new RegExp(Language.emailLabel));
const passwordInput = screen.getByLabelText(
new RegExp(Language.passwordLabel),
);
expect(emailInput).not.toHaveAttribute("aria-invalid", "true");
expect(emailInput).not.toHaveAttribute(
"aria-describedby",
"signin-email-error",
);
expect(passwordInput).not.toHaveAttribute("aria-invalid", "true");
expect(passwordInput).not.toHaveAttribute(
"aria-describedby",
"signin-password-error",
);
const signInButton = await screen.findByText(Language.passwordSignIn);
fireEvent.click(signInButton);
// Then
const emailError = await screen.findByText(Language.emailRequired);
expect(emailInput).toHaveAttribute("aria-invalid", "true");
expect(emailInput).toHaveAttribute(
"aria-describedby",
"signin-email-error",
);
const emailErrorElement = document.getElementById("signin-email-error");
expect(emailErrorElement).toBe(emailError);
expect(emailErrorElement).toHaveTextContent(Language.emailRequired);
expect(passwordInput).not.toHaveAttribute("aria-invalid", "true");
expect(passwordInput).not.toHaveAttribute(
"aria-describedby",
"signin-password-error",
);
expect(document.getElementById("signin-password-error")).toBeNull();
});
it("redirects to the setup page if there is no first user", async () => {
// Given
server.use(
@@ -41,6 +41,8 @@ export const PasswordSignInForm: FC<PasswordSignInFormProps> = ({
const getFieldHelpers = getFormHelpers(form);
const emailField = getFieldHelpers("email");
const passwordField = getFieldHelpers("password");
const emailErrorId = "signin-email-error";
const passwordErrorId = "signin-password-error";
return (
<form onSubmit={form.handleSubmit} className="flex flex-col gap-5">
@@ -59,9 +61,13 @@ export const PasswordSignInForm: FC<PasswordSignInFormProps> = ({
autoComplete="email"
type="email"
aria-invalid={Boolean(emailField.error)}
aria-describedby={emailField.error ? emailErrorId : undefined}
/>
{emailField.error && (
<span className="text-xs text-content-destructive text-left">
<span
id={emailErrorId}
className="text-xs text-content-destructive text-left"
>
{emailField.helperText}
</span>
)}
@@ -80,10 +86,14 @@ export const PasswordSignInForm: FC<PasswordSignInFormProps> = ({
onBlur={passwordField.onBlur}
autoComplete="current-password"
type="password"
aria-invalid={passwordField.error}
aria-invalid={Boolean(passwordField.error)}
aria-describedby={passwordField.error ? passwordErrorId : undefined}
/>
{passwordField.error && (
<span className="text-xs text-content-destructive text-left">
<span
id={passwordErrorId}
className="text-xs text-content-destructive text-left"
>
{passwordField.helperText}
</span>
)}
@@ -414,7 +414,7 @@ test("display pending badge and update it to running when status changes", async
renderEditorPage(createTestQueryClient());
const status = await screen.findByRole("status");
const status = await screen.findByRole("status", { name: /pending/i });
expect(status).toHaveTextContent("Pending");
// Manually update the endpoint, as to not rely on the editor page
@@ -23,9 +23,7 @@ describe("AccountForm", () => {
email={MockUserMember.email}
initialValues={mockInitialValues}
isLoading={false}
onSubmit={() => {
return;
}}
onSubmit={vi.fn()}
/>,
);
@@ -39,6 +37,28 @@ describe("AccountForm", () => {
});
});
it("sets name autocomplete to name", async () => {
// Given
const mockInitialValues: UpdateUserProfileRequest = {
username: MockUserMember.username,
name: MockUserMember.name ?? MockUserMember.username,
};
// When
render(
<AccountForm
editable
email={MockUserMember.email}
initialValues={mockInitialValues}
isLoading={false}
onSubmit={vi.fn()}
/>,
);
// Then
const nameInput = await screen.findByLabelText("Name");
expect(nameInput).toHaveAttribute("autocomplete", "name");
});
describe("when editable is set to false", () => {
it("does not allow updating username", async () => {
// Given
@@ -54,9 +74,7 @@ describe("AccountForm", () => {
email={MockUserMember.email}
initialValues={mockInitialValues}
isLoading={false}
onSubmit={() => {
return;
}}
onSubmit={vi.fn()}
/>,
);
@@ -77,6 +77,7 @@ export const AccountForm: FC<AccountFormProps> = ({
/>
<TextField
{...getFieldHelpers("name")}
autoComplete="name"
fullWidth
onBlur={(e) => {
e.target.value = e.target.value.trim();