fix(a11y): WCAG 2.1 AA fixes (#2219)

This commit is contained in:
Philip Okugbe
2026-05-20 16:47:25 +01:00
committed by GitHub
parent 1c166c4736
commit 92c0e36e46
119 changed files with 1064 additions and 194 deletions
@@ -1,6 +1,6 @@
import { Button, Divider, Group, Modal, Stack } from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import React, { useState } from "react";
import React, { useId, useState } from "react";
import { useAddSpaceMemberMutation } from "@/features/space/queries/space-query.ts";
import { MultiMemberSelect } from "@/features/space/components/multi-member-select.tsx";
import { SpaceMemberRole } from "@/features/space/components/space-member-role.tsx";
@@ -14,6 +14,7 @@ export default function AddSpaceMembersModal({
spaceId,
}: AddSpaceMemberModalProps) {
const { t } = useTranslation();
const titleId = useId();
const [opened, { open, close }] = useDisclosure(false);
const [memberIds, setMemberIds] = useState<string[]>([]);
const [role, setRole] = useState<string>(SpaceRole.WRITER);
@@ -51,24 +52,33 @@ export default function AddSpaceMembersModal({
return (
<>
<Button onClick={open}>{t("Add space members")}</Button>
<Modal opened={opened} onClose={close} title={t("Add space members")}>
<Divider size="xs" mb="xs" />
<Modal.Root opened={opened} onClose={close}>
<Modal.Overlay />
<Modal.Content aria-labelledby={titleId}>
<Modal.Header>
<Modal.Title id={titleId}>{t("Add space members")}</Modal.Title>
<Modal.CloseButton aria-label={t("Close")} />
</Modal.Header>
<Modal.Body>
<Divider size="xs" mb="xs" />
<Stack>
<MultiMemberSelect onChange={handleMultiSelectChange} />
<SpaceMemberRole
onSelect={handleRoleSelection}
defaultRole={role}
label={t("Select role")}
/>
</Stack>
<Stack>
<MultiMemberSelect onChange={handleMultiSelectChange} />
<SpaceMemberRole
onSelect={handleRoleSelection}
defaultRole={role}
label={t("Select role")}
/>
</Stack>
<Group justify="flex-end" mt="md">
<Button onClick={handleSubmit} type="submit">
{t("Add")}
</Button>
</Group>
</Modal>
<Group justify="flex-end" mt="md">
<Button onClick={handleSubmit} type="submit">
{t("Add")}
</Button>
</Group>
</Modal.Body>
</Modal.Content>
</Modal.Root>
</>
);
}
@@ -1,4 +1,4 @@
import { Group, Box, Button, TextInput, Stack, Textarea } from "@mantine/core";
import { Group, Box, Button, TextInput, Stack, Textarea, Text } from "@mantine/core";
import React, { useEffect } from "react";
import { useForm } from "@mantine/form";
import { zod4Resolver } from "mantine-form-zod-resolver";
@@ -69,10 +69,25 @@ export function CreateSpaceForm() {
navigate(getSpaceUrl(createdSpace.slug));
};
function handleValidationFailure(errors: Record<string, unknown>) {
const firstInvalidId = Object.keys(errors)[0];
if (firstInvalidId) {
document.getElementById(firstInvalidId)?.focus();
}
}
return (
<>
<Box maw="500" mx="auto">
<form onSubmit={form.onSubmit((values) => handleSubmit(values))}>
<form
onSubmit={form.onSubmit(
(values) => handleSubmit(values),
handleValidationFailure,
)}
>
<Text size="sm" c="dimmed" mb="sm">
{t("* indicates required fields")}
</Text>
<Stack>
<TextInput
withAsterisk
@@ -80,6 +95,8 @@ export function CreateSpaceForm() {
label={t("Space name")}
placeholder={t("e.g Product Team")}
variant="filled"
data-autofocus
errorProps={{ role: "alert" }}
{...form.getInputProps("name")}
/>
@@ -89,6 +106,7 @@ export function CreateSpaceForm() {
label={t("Space slug")}
placeholder={t("e.g product")}
variant="filled"
errorProps={{ role: "alert" }}
{...form.getInputProps("slug")}
/>
@@ -100,6 +118,7 @@ export function CreateSpaceForm() {
autosize
minRows={2}
maxRows={8}
errorProps={{ role: "alert" }}
{...form.getInputProps("description")}
/>
</Stack>
@@ -11,7 +11,12 @@ export default function CreateSpaceModal() {
<>
<Button onClick={open}>{t("Create space")}</Button>
<Modal opened={opened} onClose={close} title={t("Create space")}>
<Modal
opened={opened}
onClose={close}
title={t("Create space")}
closeButtonProps={{ "aria-label": t("Close") }}
>
<Divider size="xs" mb="xs" />
<CreateSpaceForm />
</Modal>
@@ -48,7 +48,7 @@ export default function SpaceSettingsModal({
{space?.name}
</Text>
</Modal.Title>
<Modal.CloseButton />
<Modal.CloseButton aria-label={t("Close")} />
</Modal.Header>
<Modal.Body>
<div style={{ height: rem(600) }}>
@@ -38,6 +38,8 @@ export function SwitchSpace({
shadow="md"
opened={opened}
onChange={toggle}
trapFocus
returnFocus
>
<Popover.Target>
<Button
@@ -6,6 +6,11 @@
box-shadow: var(--mantine-shadow-xs);
transform: scale(1.02);
}
&:focus-visible {
outline: 2px solid var(--mantine-primary-color-filled);
outline-offset: 2px;
}
}
.cardSection {
@@ -1,4 +1,4 @@
import { Text, Card, rem, Group, Button, Skeleton } from "@mantine/core";
import { Text, Card, rem, Group, Button, Skeleton, Title } from "@mantine/core";
import {
prefetchSpace,
useGetSpacesQuery,
@@ -32,9 +32,9 @@ export default function SpaceCarousel() {
return (
<>
<Group justify="space-between" align="center" mb="md">
<Text fz="sm" fw={500}>
<Title order={2} size="h6" fw={500}>
{t("Spaces you belong to")}
</Text>
</Title>
</Group>
<CardCarousel ariaLabel={t("Spaces you belong to")}>
{Array.from({ length: 4 }, (_, i) => (
@@ -80,9 +80,9 @@ export default function SpaceCarousel() {
return (
<>
<Group justify="space-between" align="center" mb="md">
<Text fz="sm" fw={500}>
<Title order={2} size="h6" fw={500}>
{t("Spaces you belong to")}
</Text>
</Title>
</Group>
<CardCarousel ariaLabel={t("Spaces you belong to")}>{cards}</CardCarousel>
@@ -1,7 +1,7 @@
import React, { useState } from "react";
import { useSpaceQuery } from "@/features/space/queries/space-query.ts";
import { EditSpaceForm } from "@/features/space/components/edit-space-form.tsx";
import { Button, Divider, Text } from "@mantine/core";
import { Button, Divider, Text, Title } from "@mantine/core";
import DeleteSpaceModal from "./delete-space-modal";
import { useDisclosure } from "@mantine/hooks";
import ExportModal from "@/components/common/export-modal.tsx";
@@ -65,9 +65,9 @@ export default function SpaceDetails({ spaceId, readOnly }: SpaceDetailsProps) {
<>
{space && (
<div>
<Text my="md" fw={600}>
<Title order={3} my="md" size="h6" fw={600}>
{t("Details")}
</Text>
</Title>
<div style={{ marginBottom: "20px" }}>
<Text size="sm" fw={500} mb="xs">
@@ -74,7 +74,11 @@ export function SpaceFilterMenu({
/>
<ScrollArea.Autosize mah={280}>
<Menu.Item onClick={() => onChange(null)}>
<Menu.Item
role="menuitemradio"
aria-checked={!value}
onClick={() => onChange(null)}
>
<Group flex="1" gap="xs">
<Avatar
color="initials"
@@ -90,14 +94,19 @@ export function SpaceFilterMenu({
{t("Search in all your spaces")}
</Text>
</div>
{!value && <IconCheck size={20} />}
{!value && <IconCheck size={20} aria-hidden />}
</Group>
</Menu.Item>
<Divider my="xs" />
{orderedSpaces.map((space) => (
<Menu.Item key={space.id} onClick={() => onChange(space.id)}>
<Menu.Item
key={space.id}
role="menuitemradio"
aria-checked={value === space.id}
onClick={() => onChange(space.id)}
>
<Group flex="1" gap="xs">
<Avatar
color="initials"
@@ -108,7 +117,7 @@ export function SpaceFilterMenu({
<Text size="sm" fw={500} style={{ flex: 1 }} truncate>
{space.name}
</Text>
{value === space.id && <IconCheck size={20} />}
{value === space.id && <IconCheck size={20} aria-hidden />}
</Group>
</Menu.Item>
))}
@@ -4,6 +4,11 @@
box-shadow: var(--mantine-shadow-xs);
transform: scale(1.02);
}
&:focus-visible {
outline: 2px solid var(--mantine-primary-color-filled);
outline-offset: 2px;
}
}
.cardSection {
@@ -1,4 +1,4 @@
import { Text, SimpleGrid, Card, rem, Group, Button } from "@mantine/core";
import { Text, SimpleGrid, Card, rem, Group, Button, Title } from "@mantine/core";
import React from "react";
import {
prefetchSpace,
@@ -33,7 +33,7 @@ export default function SpaceGrid() {
>
<Card.Section className={classes.cardSection} h={40}>
<div className={classes.starButton} data-favorited={spaceFavoriteIds.has(space.id)}>
<StarButton type="space" spaceId={space.id} size={16} />
<StarButton type="space" spaceId={space.id} name={space.name} size={16} />
</div>
</Card.Section>
<CustomAvatar
@@ -59,9 +59,9 @@ export default function SpaceGrid() {
return (
<>
<Group justify="space-between" align="center" mb="md">
<Text fz="sm" fw={500}>
<Title order={2} size="h6" fw={500}>
{t("Spaces you belong to")}
</Text>
</Title>
</Group>
<SimpleGrid cols={{ base: 1, xs: 2, sm: 3 }}>{cards}</SimpleGrid>
@@ -12,6 +12,7 @@ import { AutoTooltipText } from "@/components/ui/auto-tooltip-text.tsx";
import { SearchInput } from "@/components/common/search-input.tsx";
import NoTableResults from "@/components/common/no-table-results.tsx";
import { usePaginateAndSearch } from "@/hooks/use-paginate-and-search.tsx";
import rowClasses from "@/components/ui/clickable-table-row.module.css";
export default function SpaceList() {
const { t } = useTranslation();
@@ -42,8 +43,17 @@ export default function SpaceList() {
data?.items.map((space, index) => (
<Table.Tr
key={index}
style={{ cursor: "pointer" }}
className={rowClasses.row}
role="button"
tabIndex={0}
aria-label={t("Open settings for {{name}}", { name: space.name })}
onClick={() => handleClick(space.id)}
onKeyDown={(event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
handleClick(space.id);
}
}}
>
<Table.Td>
<Group gap="sm" wrap="nowrap">
@@ -207,7 +207,11 @@ export default function SpaceMembersList({
arrowPosition="center"
>
<Menu.Target>
<ActionIcon variant="subtle" c="gray">
<ActionIcon
variant="subtle"
c="gray"
aria-label={t("Member actions")}
>
<IconDots size={20} stroke={2} />
</ActionIcon>
</Menu.Target>
@@ -1,4 +1,4 @@
import { Text, Divider } from "@mantine/core";
import { Text, Divider, Title } from "@mantine/core";
import React from "react";
import { useTranslation } from "react-i18next";
import { ISpace } from "@/features/space/types/space.types.ts";
@@ -20,9 +20,9 @@ export default function SpaceSecuritySettings({
return (
<div>
<Text my="md" fw={600}>
<Title order={3} my="md" size="h6" fw={600}>
{t("Security")}
</Text>
</Title>
<SpacePublicSharingToggle space={space} />
@@ -3,8 +3,14 @@
color: inherit;
display: flex;
width: 100%;
&:hover {
text-decoration: none;
}
&:focus-visible {
outline: 2px solid var(--mantine-primary-color-filled);
outline-offset: 2px;
border-radius: var(--mantine-radius-sm);
}
}
@@ -8,6 +8,7 @@ import {
Menu,
Anchor,
Tooltip,
VisuallyHidden,
} from "@mantine/core";
import { IconDots, IconSettings, IconEye, IconEyeOff } from "@tabler/icons-react";
import StarButton from "@/features/favorite/components/star-button";
@@ -28,6 +29,8 @@ import Paginate from "@/components/common/paginate";
import NoTableResults from "@/components/common/no-table-results";
import SpaceSettingsModal from "@/features/space/components/settings-modal";
import classes from "./all-spaces-list.module.css";
import rowClasses from "@/components/ui/clickable-table-row.module.css";
import clsx from "clsx";
import { CustomAvatar } from "@/components/ui/custom-avatar.tsx";
import { AvatarIconType } from "@/features/attachments/types/attachment.types.ts";
import { AutoTooltipText } from "@/components/ui/auto-tooltip-text.tsx";
@@ -107,18 +110,25 @@ export default function AllSpacesList({
<Table.ScrollContainer minWidth={500}>
<Table highlightOnHover verticalSpacing="sm">
<Table.Caption>
<VisuallyHidden>
{t("List of spaces in this workspace")}
</VisuallyHidden>
</Table.Caption>
<Table.Thead>
<Table.Tr>
<Table.Th>{t("Space")}</Table.Th>
<Table.Th>{t("Members")}</Table.Th>
<Table.Th w={130} aria-label={t("Action")} />
<Table.Th w={130}>
<VisuallyHidden>{t("Action")}</VisuallyHidden>
</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{spaces.length > 0 ? (
spaces.map((space) => (
<Table.Tr key={space.id}>
<Table.Tr key={space.id} className={rowClasses.row}>
<Table.Td>
<Anchor
size="sm"
@@ -127,13 +137,13 @@ export default function AllSpacesList({
cursor: "pointer",
color: "var(--mantine-color-text)",
}}
className={clsx(classes.spaceLink, rowClasses.link)}
component={Link}
to={getSpaceUrl(space.slug)}
>
<Group
gap="sm"
wrap="nowrap"
className={classes.spaceLink}
onMouseEnter={() => prefetchSpace(space.slug, space.id)}
>
<CustomAvatar
@@ -164,7 +174,7 @@ export default function AllSpacesList({
</Table.Td>
<Table.Td>
<Group gap="xs" justify="flex-end" wrap="nowrap">
<StarButton type="space" spaceId={space.id} size={16} />
<StarButton type="space" spaceId={space.id} name={space.name} size={16} />
<WatchButton spaceId={space.id} watchedIds={watchedIds} size={16} />
<Menu position="bottom-end">
<Menu.Target>
@@ -1,4 +1,4 @@
import { Text, SimpleGrid, Card, rem, Group, Box, Button } from "@mantine/core";
import { Text, SimpleGrid, Card, rem, Group, Box, Button, Title } from "@mantine/core";
import { useState } from "react";
import { Link } from "react-router-dom";
import { useTranslation } from "react-i18next";
@@ -30,9 +30,9 @@ export default function FavoriteSpacesGrid() {
return (
<Box mb="xl">
<Text size="sm" fw={500} mb="md">
<Title order={2} size="h6" fw={500} mb="md">
{t("Favorite spaces")}
</Text>
</Title>
<SimpleGrid cols={{ base: 1, xs: 2, sm: 4 }}>
{visibleSpaces.map((fav) => (
@@ -53,6 +53,7 @@ export default function FavoriteSpacesGrid() {
<StarButton
type="space"
spaceId={fav.space!.id}
name={fav.space!.name}
size={16}
/>
</div>