fix(a11y): WCAG 2.1 AA fixes (#2219)
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user