feat: A11y fixes (#2148)

This commit is contained in:
Philip Okugbe
2026-05-04 21:21:37 +01:00
committed by GitHub
parent fe18f22dc6
commit dbe6c2d6ba
62 changed files with 587 additions and 163 deletions

View File

@@ -144,6 +144,7 @@ function CommentDialog({ editor, pageId, readOnly }: CommentDialogProps) {
withCloseButton
withBorder
data-comment-dialog
aria-label={t("Add comment")}
>
<Stack gap={2}>
<Group>

View File

@@ -173,6 +173,15 @@ function CommentListItem({
<Box
className={classes.textSelection}
onClick={() => handleCommentClick(comment)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
handleCommentClick(comment);
}
}}
role="button"
tabIndex={0}
aria-label={t("Jump to comment selection")}
>
<Text size="sm">{comment?.selection}</Text>
</Box>

View File

@@ -46,7 +46,11 @@ function CommentMenu({
return (
<Menu shadow="md" width={200}>
<Menu.Target>
<ActionIcon variant="default" style={{ border: "none" }}>
<ActionIcon
variant="default"
style={{ border: "none" }}
aria-label={t("Comment menu")}
>
<IconDots size={20} stroke={2} />
</ActionIcon>
</Menu.Target>

View File

@@ -36,6 +36,7 @@ export default function AudioView(props: NodeViewProps) {
preload="metadata"
controls
src={safeSrc}
aria-label={placeholder?.name || t("Audio")}
/>
)}
{!safeSrc && previewSrc && (
@@ -45,6 +46,7 @@ export default function AudioView(props: NodeViewProps) {
preload="metadata"
controls
src={previewSrc}
aria-label={placeholder?.name || t("Audio")}
/>
<Loader size={20} pos="absolute" top={6} right={6} />
</Group>
@@ -60,7 +62,7 @@ export default function AudioView(props: NodeViewProps) {
</Group>
)}
{!safeSrc && !previewSrc && !placeholder && (
<audio className={classes.audio} controls />
<audio className={classes.audio} controls aria-label={t("Audio")} />
)}
</div>
</NodeViewWrapper>

View File

@@ -172,6 +172,9 @@ export const ColorSelector: FC<ColorSelectorProps> = ({
fontWeight: 500,
fontSize: rem(16),
}}
aria-label={t("Text color")}
aria-haspopup="dialog"
aria-expanded={isOpen}
>
A
</Button>
@@ -186,20 +189,32 @@ export const ColorSelector: FC<ColorSelectorProps> = ({
{t("Text color")}
</Text>
<SimpleGrid cols={5} spacing="xs">
{TEXT_COLORS.map(({ name, color }, index) => (
{TEXT_COLORS.map(({ name, color }, index) => {
const applyTextColor = () => {
if (name === "Default") {
editor.commands.unsetColor();
} else {
editor
.chain()
.focus()
.setColor(color || "")
.run();
}
setIsOpen(false);
};
return (
<Tooltip key={index} label={t(name)} withArrow>
<Box
onClick={() => {
if (name === "Default") {
editor.commands.unsetColor();
} else {
editor
.chain()
.focus()
.setColor(color || "")
.run();
role="button"
tabIndex={0}
aria-label={t(name)}
aria-pressed={!!editorState[`text_${color}`]}
onClick={applyTextColor}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
applyTextColor();
}
setIsOpen(false);
}}
style={{
width: rem(28),
@@ -221,7 +236,8 @@ export const ColorSelector: FC<ColorSelectorProps> = ({
A
</Box>
</Tooltip>
))}
);
})}
</SimpleGrid>
</Box>
@@ -230,23 +246,35 @@ export const ColorSelector: FC<ColorSelectorProps> = ({
{t("Highlight color")}
</Text>
<SimpleGrid cols={5} spacing="xs">
{HIGHLIGHT_COLORS.map(({ name, color }, index) => (
{HIGHLIGHT_COLORS.map(({ name, color }, index) => {
const applyHighlight = () => {
if (name === "Default") {
editor.commands.unsetHighlight();
} else {
editor
.chain()
.focus()
.toggleMark("highlight", {
color: color || "",
colorName: name.toLowerCase() || "",
})
.run();
}
setIsOpen(false);
};
return (
<Tooltip key={index} label={t(name)} withArrow>
<Box
onClick={() => {
if (name === "Default") {
editor.commands.unsetHighlight();
} else {
editor
.chain()
.focus()
.toggleMark("highlight", {
color: color || "",
colorName: name.toLowerCase() || "",
})
.run();
role="button"
tabIndex={0}
aria-label={t(name)}
aria-pressed={!!editorState[`highlight_${color}`]}
onClick={applyHighlight}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
applyHighlight();
}
setIsOpen(false);
}}
style={{
width: rem(28),
@@ -274,7 +302,8 @@ export const ColorSelector: FC<ColorSelectorProps> = ({
)}
</Box>
</Tooltip>
))}
);
})}
</SimpleGrid>
</Box>

View File

@@ -157,6 +157,9 @@ export const NodeSelector: FC<NodeSelectorProps> = ({
radius="0"
rightSection={<IconChevronDown size={16} />}
onClick={() => setIsOpen(!isOpen)}
aria-label={t("Turn into")}
aria-haspopup="menu"
aria-expanded={isOpen}
>
{t(activeItem?.name)}
</Button>

View File

@@ -92,6 +92,9 @@ export const TextAlignmentSelector: FC<TextAlignmentProps> = ({
radius="0"
rightSection={<IconChevronDown size={16} />}
onClick={() => setIsOpen(!isOpen)}
aria-label={t("Text align")}
aria-haspopup="menu"
aria-expanded={isOpen}
>
<activeItem.icon style={{ width: rem(16) }} stroke={2} />
</Button>

View File

@@ -137,7 +137,13 @@ export default function DrawioView(props: NodeViewProps) {
return (
<NodeViewWrapper data-drag-handle>
<Modal.Root opened={opened} onClose={handleClose} fullScreen closeOnEscape={false}>
<Modal.Root
opened={opened}
onClose={handleClose}
fullScreen
closeOnEscape={false}
aria-label={t("Diagram editor")}
>
<Modal.Overlay />
<Modal.Content style={{ overflow: "hidden" }}>
<Modal.Body pos="relative">

View File

@@ -107,7 +107,17 @@ const EmojiList = ({
}, [selectedIndex]);
return items.length > 0 || isLoading ? (
<Paper id="emoji-command" p="0" shadow="md" withBorder>
<Paper
id="emoji-command"
p="0"
shadow="md"
withBorder
role="listbox"
aria-label="Emoji results"
aria-activedescendant={
items.length > 0 ? `emoji-command-option-${selectedIndex}` : undefined
}
>
{isLoading && <Loader m="xs" color="blue" type="dots" />}
{items.length > 0 && (
<ScrollArea.Autosize
@@ -120,6 +130,10 @@ const EmojiList = ({
{items.map((item, index: number) => (
<ActionIcon
data-item-index={index}
id={`emoji-command-option-${index}`}
role="option"
aria-selected={index === selectedIndex}
aria-label={item.id}
variant="transparent"
key={item.id}
className={clsx(classes.menuBtn, {

View File

@@ -102,6 +102,14 @@ export const LinkEditorPanel = ({
leftSection={<IconLink size={16} stroke={1.5} color="var(--mantine-color-dimmed)" />}
classNames={{ input: classes.linkInput }}
placeholder={t("Paste link or search pages")}
aria-label={t("Paste link or search pages")}
role="combobox"
aria-expanded={showDropdown}
aria-controls="link-editor-results"
aria-autocomplete="list"
aria-activedescendant={
showDropdown ? `link-editor-option-${selectedIndex}` : undefined
}
value={state.url}
onChange={state.onChange}
onKeyDown={handleKeyDown}
@@ -125,10 +133,16 @@ export const LinkEditorPanel = ({
scrollbarSize={6}
mt={state.url.length > 0 ? 8 : 0}
styles={{ content: { minWidth: 0 } }}
id="link-editor-results"
role="listbox"
aria-label={t("Link suggestions")}
>
{showUrlItem && (
<UnstyledButton
data-item-index={0}
id="link-editor-option-0"
role="option"
aria-selected={selectedIndex === 0}
onClick={() => onSetLink(state.url, false)}
className={clsx(classes.searchItem, {
[classes.selectedSearchItem]: selectedIndex === 0,
@@ -156,6 +170,9 @@ export const LinkEditorPanel = ({
return (
<UnstyledButton
data-item-index={itemIndex}
id={`link-editor-option-${itemIndex}`}
role="option"
aria-selected={itemIndex === selectedIndex}
key={page.id || index}
onClick={() => selectPage(page)}
className={clsx(classes.searchItem, {

View File

@@ -287,7 +287,16 @@ const MentionList = forwardRef<any, MentionListProps>((props, ref) => {
);
return (
<Paper id="mention" shadow="md" withBorder radius="md" py={6}>
<Paper
id="mention"
shadow="md"
withBorder
radius="md"
py={6}
role="listbox"
aria-label={t("Mention suggestions")}
aria-activedescendant={`mention-option-${selectedIndex}`}
>
<ScrollArea.Autosize
viewportRef={viewportRef}
mah={350}
@@ -301,7 +310,7 @@ const MentionList = forwardRef<any, MentionListProps>((props, ref) => {
if (item.entityType === "header") {
const isFirst = index === 0;
return (
<div key={`${item.label}-${index}`}>
<div key={`${item.label}-${index}`} role="presentation">
{!isFirst && <Divider my={6} />}
<Text
c="dimmed"
@@ -322,6 +331,9 @@ const MentionList = forwardRef<any, MentionListProps>((props, ref) => {
<UnstyledButton
data-item-index={index}
key={index}
id={`mention-option-${index}`}
role="option"
aria-selected={index === selectedIndex}
onClick={() => selectItem(index)}
className={clsx(classes.menuBtn, {
[classes.selectedItem]: index === selectedIndex,
@@ -348,6 +360,9 @@ const MentionList = forwardRef<any, MentionListProps>((props, ref) => {
<UnstyledButton
data-item-index={index}
key={index}
id={`mention-option-${index}`}
role="option"
aria-selected={index === selectedIndex}
onClick={() => selectItem(index)}
className={clsx(classes.menuBtn, {
[classes.selectedItem]: index === selectedIndex,
@@ -358,7 +373,7 @@ const MentionList = forwardRef<any, MentionListProps>((props, ref) => {
<ActionIcon
variant="subtle"
component="div"
aria-label={item.label}
aria-hidden="true"
color="gray"
size="sm"
>
@@ -390,6 +405,11 @@ const MentionList = forwardRef<any, MentionListProps>((props, ref) => {
{(hasUsers || hasPages) && <Divider my={6} />}
<UnstyledButton
data-item-index={renderItems.indexOf(createPageItemData)}
id={`mention-option-${renderItems.indexOf(createPageItemData)}`}
role="option"
aria-selected={
renderItems.indexOf(createPageItemData) === selectedIndex
}
onClick={() =>
selectItem(renderItems.indexOf(createPageItemData))
}
@@ -405,6 +425,7 @@ const MentionList = forwardRef<any, MentionListProps>((props, ref) => {
component="div"
color="gray"
size="sm"
aria-hidden="true"
>
<IconPlus size={16} stroke={1.5} />
</ActionIcon>

View File

@@ -92,7 +92,20 @@ export default function PdfView(props: NodeViewProps) {
if (hasError) {
return (
<NodeViewWrapper data-drag-handle>
<div data-pdf-error className={clsx(classes.pdfError, { "ProseMirror-selectednode": selected })} onClick={handleSelect}>
<div
data-pdf-error
className={clsx(classes.pdfError, { "ProseMirror-selectednode": selected })}
onClick={handleSelect}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
handleSelect();
}
}}
role="button"
tabIndex={0}
aria-label={t("Failed to load PDF")}
>
<IconFileTypePdf size={32} stroke={1.5} />
<Text size="sm" c="dimmed">
{t("Failed to load PDF")}

View File

@@ -187,12 +187,14 @@ function SearchAndReplaceDialog({ editor, editable = true }: PageFindDialogDialo
position={{ top: 90, right: 50 }}
withBorder
transitionProps={{ transition: "slide-down" }}
aria-label={t("Find and replace")}
>
<Stack gap="xs">
<Flex align="center" gap="xs">
<Input
ref={inputRef}
placeholder={t("Find")}
aria-label={t("Find")}
leftSection={<IconSearch size={16} />}
rightSection={
<Text size="xs" ta="right">
@@ -217,7 +219,12 @@ function SearchAndReplaceDialog({ editor, editable = true }: PageFindDialogDialo
<ActionIcon.Group>
<Tooltip label={t("Previous match (Shift+Enter)")}>
<ActionIcon variant="subtle" color="gray" onClick={previous}>
<ActionIcon
variant="subtle"
color="gray"
onClick={previous}
aria-label={t("Previous match (Shift+Enter)")}
>
<IconArrowNarrowUp
style={{ width: "70%", height: "70%" }}
stroke={1.5}
@@ -225,7 +232,12 @@ function SearchAndReplaceDialog({ editor, editable = true }: PageFindDialogDialo
</ActionIcon>
</Tooltip>
<Tooltip label={t("Next match (Enter)")}>
<ActionIcon variant="subtle" color="gray" onClick={next}>
<ActionIcon
variant="subtle"
color="gray"
onClick={next}
aria-label={t("Next match (Enter)")}
>
<IconArrowNarrowDown
style={{ width: "70%", height: "70%" }}
stroke={1.5}
@@ -237,6 +249,8 @@ function SearchAndReplaceDialog({ editor, editable = true }: PageFindDialogDialo
variant="subtle"
color={caseSensitive.color}
onClick={() => caseSensitiveToggle()}
aria-label={t("Match case (Alt+C)")}
aria-pressed={caseSensitive.isCaseSensitive}
>
<IconLetterCase
style={{ width: "70%", height: "70%" }}
@@ -250,6 +264,8 @@ function SearchAndReplaceDialog({ editor, editable = true }: PageFindDialogDialo
variant="subtle"
color={replaceButton.color}
onClick={() => replaceButtonToggle()}
aria-label={t("Replace")}
aria-pressed={replaceButton.isReplaceShow}
>
<IconReplace
style={{ width: "70%", height: "70%" }}
@@ -259,7 +275,12 @@ function SearchAndReplaceDialog({ editor, editable = true }: PageFindDialogDialo
</Tooltip>
)}
<Tooltip label={t("Close (Escape)")}>
<ActionIcon variant="subtle" color="gray" onClick={closeDialog}>
<ActionIcon
variant="subtle"
color="gray"
onClick={closeDialog}
aria-label={t("Close (Escape)")}
>
<IconX style={{ width: "70%", height: "70%" }} stroke={1.5} />
</ActionIcon>
</Tooltip>
@@ -269,6 +290,7 @@ function SearchAndReplaceDialog({ editor, editable = true }: PageFindDialogDialo
<Flex align="center" gap="xs">
<Input
placeholder={t("Replace")}
aria-label={t("Replace")}
leftSection={<IconReplace size={16} />}
rightSection={<div></div>}
rightSectionPointerEvents="all"

View File

@@ -86,7 +86,15 @@ const CommandList = ({
}, [selectedIndex]);
return flatItems.length > 0 ? (
<Paper id="slash-command" shadow="md" p="xs" withBorder>
<Paper
id="slash-command"
shadow="md"
p="xs"
withBorder
role="listbox"
aria-label={t("Slash commands")}
aria-activedescendant={`slash-command-option-${selectedIndex}`}
>
<ScrollArea
viewportRef={viewportRef}
h={350}
@@ -94,22 +102,30 @@ const CommandList = ({
scrollbarSize={8}
overscrollBehavior="contain"
>
{Object.entries(items).map(([category, categoryItems]) => (
<div key={category}>
{(() => {
let flatIndex = -1;
return Object.entries(items).map(([category, categoryItems]) => (
<div key={category} role="group" aria-label={category}>
<Text c="dimmed" mb={4} fw={500} tt="capitalize">
{category}
</Text>
{categoryItems.map((item: SlashMenuItemType, index: number) => (
{categoryItems.map((item: SlashMenuItemType) => {
flatIndex += 1;
const itemIndex = flatIndex;
return (
<UnstyledButton
data-item-index={index}
key={index}
onClick={() => selectItem(index)}
data-item-index={itemIndex}
key={itemIndex}
id={`slash-command-option-${itemIndex}`}
role="option"
aria-selected={itemIndex === selectedIndex}
onClick={() => selectItem(itemIndex)}
className={clsx(classes.menuBtn, {
[classes.selectedItem]: index === selectedIndex,
[classes.selectedItem]: itemIndex === selectedIndex,
})}
>
<Group>
<ActionIcon variant="default" component="div">
<ActionIcon variant="default" component="div" aria-hidden="true">
<item.icon size={18} />
</ActionIcon>
@@ -124,9 +140,11 @@ const CommandList = ({
</div>
</Group>
</UnstyledButton>
))}
);
})}
</div>
))}
));
})()}
</ScrollArea>
</Paper>
) : null;

View File

@@ -92,8 +92,17 @@ export default function StatusView(props: NodeViewProps) {
colorClassMap[color],
)}
onClick={() => isEditable && setOpened(true)}
onKeyDown={(e) => {
if (isEditable && (e.key === "Enter" || e.key === " ")) {
e.preventDefault();
setOpened(true);
}
}}
role="button"
tabIndex={0}
aria-label={text || "SET STATUS"}
aria-haspopup="dialog"
aria-expanded={opened}
>
{text || "SET STATUS"}
</span>
@@ -127,6 +136,16 @@ export default function StatusView(props: NodeViewProps) {
)}
style={{ backgroundColor: bg }}
onClick={() => handleColorChange(name)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
handleColorChange(name);
}
}}
role="button"
tabIndex={0}
aria-label={name}
aria-pressed={color === name}
>
{color === name && <IconCheck size={14} />}
</Box>

View File

@@ -47,6 +47,7 @@ export default function VideoView(props: NodeViewProps) {
preload="metadata"
controls
src={getFileUrl(src)}
aria-label={placeholder?.name || t("Video")}
/>
)}
{!src && previewSrc && (
@@ -56,6 +57,7 @@ export default function VideoView(props: NodeViewProps) {
preload="metadata"
controls
src={previewSrc}
aria-label={placeholder?.name || t("Video")}
/>
<Loader size={20} pos="absolute" top={6} right={6} />
</Group>
@@ -71,7 +73,7 @@ export default function VideoView(props: NodeViewProps) {
</Group>
)}
{!src && !previewSrc && !placeholder && (
<video className={classes.video} controls />
<video className={classes.video} controls aria-label={t("Video")} />
)}
</div>
</NodeViewWrapper>

View File

@@ -53,15 +53,17 @@ export default function StarButton(props: StarButtonProps) {
}
};
const label = isFavorited
? t("Remove from favorites")
: t("Add to favorites");
return (
<Tooltip
label={isFavorited ? t("Remove from favorites") : t("Add to favorites")}
openDelay={250}
withArrow
>
<Tooltip label={label} openDelay={250} withArrow>
<ActionIcon
variant="subtle"
color={isFavorited ? "yellow" : "gray"}
aria-label={label}
aria-pressed={isFavorited}
onClick={handleToggle}
loading={isPending}
>

View File

@@ -53,7 +53,7 @@ export default function GroupActionMenu() {
arrowPosition="center"
>
<Menu.Target>
<ActionIcon variant="light">
<ActionIcon variant="light" aria-label={t("Group menu")}>
<IconDots size={20} stroke={2} />
</ActionIcon>
</Menu.Target>

View File

@@ -54,7 +54,7 @@ export default function GroupMembersList() {
<Table.Tr>
<Table.Th>{t("User")}</Table.Th>
<Table.Th>{t("Status")}</Table.Th>
<Table.Th></Table.Th>
<Table.Th aria-label={t("Action")} />
</Table.Tr>
</Table.Thead>

View File

@@ -4,7 +4,7 @@ import {
UnstyledButton,
Badge,
Table,
ActionIcon,
ThemeIcon,
Button,
} from "@mantine/core";
import { Link } from "react-router-dom";
@@ -61,13 +61,13 @@ export default function CreatedByMe({ spaceId }: Props) {
>
<Group wrap="nowrap">
{page.icon || (
<ActionIcon
<ThemeIcon
variant="transparent"
color="gray"
size={18}
>
<IconFileDescription size={18} />
</ActionIcon>
</ThemeIcon>
)}
<Text fw={500} size="md" lineClamp={1}>
{page.title || t("Untitled")}

View File

@@ -4,7 +4,7 @@ import {
UnstyledButton,
Badge,
Table,
ActionIcon,
ThemeIcon,
Button,
} from "@mantine/core";
import { Link } from "react-router-dom";
@@ -62,13 +62,13 @@ export default function FavoritesPages({ spaceId }: Props) {
>
<Group wrap="nowrap">
{fav.page.icon || (
<ActionIcon
<ThemeIcon
variant="transparent"
color="gray"
size={18}
>
<IconFileDescription size={18} />
</ActionIcon>
</ThemeIcon>
)}
<Text fw={500} size="md" lineClamp={1}>
{fav.page.title || t("Untitled")}

View File

@@ -58,6 +58,9 @@ export function NotificationPopover() {
variant="subtle"
color="dark"
size="sm"
aria-label={t("Notifications")}
aria-haspopup="dialog"
aria-expanded={opened}
onClick={() => setOpened((o) => !o)}
>
<Indicator

View File

@@ -22,6 +22,7 @@ export default function HistoryModal({ pageId, pageTitle }: Props) {
opened={isModalOpen}
onClose={() => setModalOpen(false)}
fullScreen
aria-label={t("Page history")}
>
<Modal.Overlay />
<Modal.Content style={{ overflow: "hidden" }}>
@@ -49,6 +50,7 @@ export default function HistoryModal({ pageId, pageTitle }: Props) {
size={1400}
opened={isModalOpen}
onClose={() => setModalOpen(false)}
aria-label={t("Page history")}
>
<Modal.Overlay />
<Modal.Content style={{ overflow: "hidden" }}>

View File

@@ -19,6 +19,7 @@ import { buildPageUrl } from "@/features/page/page.utils.ts";
import { usePageQuery } from "@/features/page/queries/page-query.ts";
import { extractPageSlugId } from "@/lib";
import { useMediaQuery } from "@mantine/hooks";
import { useTranslation } from "react-i18next";
function getTitle(name: string, icon: string) {
if (icon) {
@@ -28,6 +29,7 @@ function getTitle(name: string, icon: string) {
}
export default function Breadcrumb() {
const { t } = useTranslation();
const treeData = useAtomValue(treeDataAtom);
const [breadcrumbNodes, setBreadcrumbNodes] = useState<
SpaceTreeNode[] | null
@@ -80,7 +82,7 @@ export default function Breadcrumb() {
));
const renderAnchor = useCallback(
(node: SpaceTreeNode) => (
(node: SpaceTreeNode, isCurrent = false) => (
<Tooltip label={node.name} key={node.id}>
<Anchor
component={Link}
@@ -89,6 +91,7 @@ export default function Breadcrumb() {
fz="sm"
key={node.id}
className={classes.truncatedText}
aria-current={isCurrent ? "page" : undefined}
>
{getTitle(node.name, node.icon)}
</Anchor>
@@ -115,7 +118,11 @@ export default function Breadcrumb() {
key="hidden-nodes"
>
<Popover.Target>
<ActionIcon color="gray" variant="transparent">
<ActionIcon
color="gray"
variant="transparent"
aria-label={t("Show hidden breadcrumbs")}
>
<IconDots size={20} stroke={2} />
</ActionIcon>
</Popover.Target>
@@ -124,11 +131,13 @@ export default function Breadcrumb() {
</Popover.Dropdown>
</Popover>,
//renderAnchor(secondLastNode),
renderAnchor(lastNode),
renderAnchor(lastNode, true),
];
}
return breadcrumbNodes.map(renderAnchor);
return breadcrumbNodes.map((node, i) =>
renderAnchor(node, i === breadcrumbNodes.length - 1),
);
};
const getMobileBreadcrumbItems = () => {
@@ -144,8 +153,12 @@ export default function Breadcrumb() {
key="mobile-hidden-nodes"
>
<Popover.Target>
<Tooltip label="Breadcrumbs">
<ActionIcon color="gray" variant="transparent">
<Tooltip label={t("Breadcrumbs")}>
<ActionIcon
color="gray"
variant="transparent"
aria-label={t("Breadcrumbs")}
>
<IconCornerDownRightDouble size={20} stroke={2} />
</ActionIcon>
</Tooltip>
@@ -157,16 +170,18 @@ export default function Breadcrumb() {
];
}
return breadcrumbNodes.map(renderAnchor);
return breadcrumbNodes.map((node, i) =>
renderAnchor(node, i === breadcrumbNodes.length - 1),
);
};
return (
<div className={classes.breadcrumbDiv}>
<nav aria-label={t("Breadcrumb")} className={classes.breadcrumbDiv}>
{breadcrumbNodes && (
<Breadcrumbs className={classes.breadcrumbs}>
{isMobile ? getMobileBreadcrumbItems() : getBreadcrumbItems()}
</Breadcrumbs>
)}
</div>
</nav>
);
}

View File

@@ -1,4 +1,4 @@
import { ActionIcon, Group, Menu, Text, Tooltip } from "@mantine/core";
import { ActionIcon, Group, Menu, Text, ThemeIcon, Tooltip } from "@mantine/core";
import {
IconArrowRight,
IconArrowsHorizontal,
@@ -99,6 +99,7 @@ export default function PageHeaderMenu({ readOnly }: PageHeaderMenuProps) {
<ActionIcon
variant="subtle"
color="dark"
aria-label={t("Comments")}
onClick={() => toggleAside("comments")}
>
<IconMessage size={20} stroke={2} />
@@ -109,6 +110,7 @@ export default function PageHeaderMenu({ readOnly }: PageHeaderMenuProps) {
<ActionIcon
variant="subtle"
color="dark"
aria-label={t("Table of contents")}
onClick={() => toggleAside("toc")}
>
<IconList size={20} stroke={2} />
@@ -205,7 +207,11 @@ function PageActionMenu({ readOnly }: PageActionMenuProps) {
arrowPosition="center"
>
<Menu.Target>
<ActionIcon variant="subtle" color="dark">
<ActionIcon
variant="subtle"
color="dark"
aria-label={t("Page actions")}
>
<IconDots size={20} />
</ActionIcon>
</Menu.Target>
@@ -416,9 +422,15 @@ function ConnectionWarning() {
openDelay={250}
withArrow
>
<ActionIcon variant="default" c="red" style={{ border: "none" }}>
<ThemeIcon
variant="default"
c="red"
role="status"
aria-label={t("Real-time editor connection lost. Retrying...")}
style={{ border: "none" }}
>
<IconWifiOff size={20} stroke={2} />
</ActionIcon>
</ThemeIcon>
</Tooltip>
);
}

View File

@@ -67,7 +67,7 @@ export default function PageImportModal({
<Modal.Content style={{ overflow: "hidden" }}>
<Modal.Header py={0}>
<Modal.Title fw={500}>{t("Import pages")}</Modal.Title>
<Modal.CloseButton />
<Modal.CloseButton aria-label={t("Close")} />
</Modal.Header>
<Modal.Body>
<ImportFormatSelection spaceId={spaceId} onClose={onClose} />
@@ -332,7 +332,15 @@ function ImportFormatSelection({ spaceId, onClose }: ImportFormatSelection) {
return (
<>
<SimpleGrid cols={2}>
<FileButton onChange={handleFileUpload} accept=".md" multiple resetRef={markdownFileRef}>
<FileButton
onChange={handleFileUpload}
accept=".md"
multiple
resetRef={markdownFileRef}
inputProps={{
"aria-label": t("Choose {{format}} file", { format: "Markdown" }),
}}
>
{(props) => (
<Button
justify="start"
@@ -345,7 +353,15 @@ function ImportFormatSelection({ spaceId, onClose }: ImportFormatSelection) {
)}
</FileButton>
<FileButton onChange={handleFileUpload} accept="text/html" multiple resetRef={htmlFileRef}>
<FileButton
onChange={handleFileUpload}
accept="text/html"
multiple
resetRef={htmlFileRef}
inputProps={{
"aria-label": t("Choose {{format}} file", { format: "HTML" }),
}}
>
{(props) => (
<Button
justify="start"
@@ -363,6 +379,9 @@ function ImportFormatSelection({ spaceId, onClose }: ImportFormatSelection) {
accept=".docx"
multiple
resetRef={docxFileRef}
inputProps={{
"aria-label": t("Choose {{format}} file", { format: "Word (DOCX)" }),
}}
>
{(props) => (
<Tooltip
@@ -387,6 +406,9 @@ function ImportFormatSelection({ spaceId, onClose }: ImportFormatSelection) {
accept=".pdf"
multiple
resetRef={pdfFileRef}
inputProps={{
"aria-label": t("Choose {{format}} file", { format: "PDF" }),
}}
>
{(props) => (
<Tooltip
@@ -410,6 +432,9 @@ function ImportFormatSelection({ spaceId, onClose }: ImportFormatSelection) {
onChange={(file) => handleZipUpload(file, "notion")}
accept="application/zip"
resetRef={notionFileRef}
inputProps={{
"aria-label": t("Choose {{format}} file", { format: "Notion" }),
}}
>
{(props) => (
<Button
@@ -426,6 +451,9 @@ function ImportFormatSelection({ spaceId, onClose }: ImportFormatSelection) {
onChange={(file) => handleZipUpload(file, "confluence")}
accept="application/zip"
resetRef={confluenceFileRef}
inputProps={{
"aria-label": t("Choose {{format}} file", { format: "Confluence" }),
}}
>
{(props) => (
<Tooltip
@@ -463,6 +491,9 @@ function ImportFormatSelection({ spaceId, onClose }: ImportFormatSelection) {
onChange={(file) => handleZipUpload(file, "generic")}
accept="application/zip"
resetRef={zipFileRef}
inputProps={{
"aria-label": t("Choose {{format}} file", { format: "ZIP" }),
}}
>
{(props) => (
<Group justify="center">

View File

@@ -19,7 +19,7 @@ export default function TrashPageContentModal({
const title = pageTitle || t("Untitled");
return (
<Modal.Root size={1200} opened={opened} onClose={onClose}>
<Modal.Root size={1200} opened={opened} onClose={onClose} aria-label={t("Preview")}>
<Modal.Overlay />
<Modal.Content style={{ overflow: "hidden" }}>
<Modal.Header>

View File

@@ -129,7 +129,7 @@ export default function Trash() {
<Table.Th style={{ whiteSpace: "nowrap" }}>
{t("Deleted at")}
</Table.Th>
<Table.Th></Table.Th>
<Table.Th aria-label={t("Action")} />
</Table.Tr>
</Table.Thead>
<Table.Tbody>

View File

@@ -458,6 +458,8 @@ interface CreateNodeProps {
}
function CreateNode({ node, treeApi, onExpandTree }: CreateNodeProps) {
const { t } = useTranslation();
function handleCreate() {
if (node.data.hasChildren && node.children.length === 0) {
node.toggle();
@@ -475,6 +477,7 @@ function CreateNode({ node, treeApi, onExpandTree }: CreateNodeProps) {
<ActionIcon
variant="transparent"
c="gray"
aria-label={t("Create page")}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
@@ -591,6 +594,7 @@ function NodeMenu({ node, treeApi, spaceId }: NodeMenuProps) {
<ActionIcon
variant="transparent"
c="gray"
aria-label={t("Page menu")}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
@@ -725,6 +729,8 @@ interface PageArrowProps {
}
function PageArrow({ node, onExpandTree }: PageArrowProps) {
const { t } = useTranslation();
useEffect(() => {
if (node.isOpen) {
onExpandTree();
@@ -736,6 +742,8 @@ function PageArrow({ node, onExpandTree }: PageArrowProps) {
size={20}
variant="subtle"
c="gray"
aria-label={node.isOpen ? t("Collapse") : t("Expand")}
aria-expanded={node.isInternal ? node.isOpen : undefined}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();

View File

@@ -47,6 +47,7 @@ export function SearchMobileControl({ onSearch }: SearchMobileControlProps) {
<ActionIcon
variant="subtle"
color="dark"
aria-label={t("Search")}
onClick={onSearch}
size="sm"
>

View File

@@ -37,7 +37,7 @@ export default function SessionList() {
<Table.Tr>
<Table.Th>{t("Device Name")}</Table.Th>
<Table.Th>{t("Last Active")}</Table.Th>
<Table.Th />
<Table.Th aria-label={t("Action")} />
</Table.Tr>
</Table.Thead>
<Table.Tbody>
@@ -94,7 +94,7 @@ export default function SessionList() {
<Table.Tr>
<Table.Th>{t("Device Name")}</Table.Th>
<Table.Th>{t("Last Active")}</Table.Th>
{otherSessions.length > 0 && <Table.Th />}
{otherSessions.length > 0 && <Table.Th aria-label={t("Action")} />}
</Table.Tr>
</Table.Thead>
<Table.Tbody>

View File

@@ -75,7 +75,7 @@ export default function ShareActionMenu({ share }: Props) {
arrowPosition="center"
>
<Menu.Target>
<ActionIcon variant="subtle" c="gray">
<ActionIcon variant="subtle" c="gray" aria-label={t("More options")}>
<IconDots size={20} stroke={2} />
</ActionIcon>
</Menu.Target>

View File

@@ -148,6 +148,7 @@ export default function ShareShell({
onClick={toggleTocMobile}
hiddenFrom="sm"
size="sm"
aria-label={t("Table of contents")}
>
<IconList size={20} stroke={2} />
</ActionIcon>
@@ -157,6 +158,7 @@ export default function ShareShell({
<ActionIcon
variant="default"
style={{ border: "none" }}
aria-label={t("Table of contents")}
onClick={toggleToc}
visibleFrom="sm"
size="sm"

View File

@@ -143,7 +143,7 @@ export default function SpaceMembersList({
<Table.Tr>
<Table.Th>{t("Member")}</Table.Th>
<Table.Th>{t("Role")}</Table.Th>
<Table.Th></Table.Th>
<Table.Th aria-label={t("Action")} />
</Table.Tr>
</Table.Thead>

View File

@@ -49,15 +49,15 @@ function WatchButton({ spaceId, watchedIds, size = 16 }: { spaceId: string; watc
}
};
const label = isWatching ? t("Stop watching space") : t("Watch space");
return (
<Tooltip
label={isWatching ? t("Stop watching space") : t("Watch space")}
openDelay={250}
withArrow
>
<Tooltip label={label} openDelay={250} withArrow>
<ActionIcon
variant="subtle"
color={isWatching ? "blue" : "gray"}
aria-label={label}
aria-pressed={isWatching}
onClick={handleToggle}
loading={isPending}
>
@@ -111,7 +111,7 @@ export default function AllSpacesList({
<Table.Tr>
<Table.Th>{t("Space")}</Table.Th>
<Table.Th>{t("Members")}</Table.Th>
<Table.Th w={130}></Table.Th>
<Table.Th w={130} aria-label={t("Action")} />
</Table.Tr>
</Table.Thead>
@@ -168,7 +168,11 @@ export default function AllSpacesList({
<WatchButton spaceId={space.id} watchedIds={watchedIds} size={16} />
<Menu position="bottom-end">
<Menu.Target>
<ActionIcon variant="subtle" color="gray">
<ActionIcon
variant="subtle"
color="gray"
aria-label={t("Space menu")}
>
<IconDots size={16} />
</ActionIcon>
</Menu.Target>

View File

@@ -83,7 +83,11 @@ export default function MemberActionMenu({ userId, deactivatedAt }: Props) {
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>

View File

@@ -34,6 +34,7 @@ export default function WorkspaceInvitesTable() {
<Table.Th>{t("Email")}</Table.Th>
<Table.Th>{t("Role")}</Table.Th>
<Table.Th>{t("Date")}</Table.Th>
<Table.Th aria-label={t("Action")} />
</Table.Tr>
</Table.Thead>

View File

@@ -61,6 +61,7 @@ export default function WorkspaceMembersTable() {
<Table.Th>{t("User")}</Table.Th>
<Table.Th>{t("Status")}</Table.Th>
<Table.Th>{t("Role")}</Table.Th>
<Table.Th aria-label={t("Action")} />
</Table.Tr>
</Table.Thead>