mirror of
https://github.com/outline/outline.git
synced 2026-06-13 11:25:03 +03:00
fix: Address various a11y findings (#11977)
* A11y improvements * fix: Accessibility improvements for sidebar, layout, and emoji icons - Add role="main" to content area and role="contentinfo" to right sidebar - Add aria-expanded to sidebar Disclosure toggle button - Add nav landmark with aria-label to shared sidebar navigation - Render SidebarLink as button instead of div when no link target - Hide decorative emoji icons from screen readers (aria-hidden) - Add aria-hidden to EmojiIcon SVG element Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: Restore PopoverTrigger in FindAndReplace, add role to span PopoverAnchor broke the find/replace popover. Revert to PopoverTrigger and instead add role="button" and aria-label to the span so ARIA attributes from Radix are valid on the element. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: Sidebar button styling * fix: Use semantic list elements for References document list Change the References list container from div to ul and wrap each ReferenceListItem in an li element for proper screen reader semantics. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: Address PR review feedback for accessibility changes - Heading buttons: switch from mousedown to click for keyboard access - Heading fold: add aria-expanded attribute - FindAndReplace: use real button element instead of span with role - SidebarLink: branch render to avoid passing NavLink props to button - Right sidebar: use role=complementary instead of contentinfo Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: Use translation hook for FindAndReplace, revert anchor click handler - Use t() for aria-label in FindAndReplace button - Revert heading anchor from click back to mousedown to avoid side effects Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: Add ts-expect-error for styled NavLink overload mismatch The spread props on the NavLink branch cause a TypeScript overload mismatch that was previously suppressed. Re-add the suppression. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -48,6 +48,7 @@ const Layout = React.forwardRef(function Layout_(
|
||||
<Content
|
||||
auto
|
||||
justify="center"
|
||||
role="main"
|
||||
$isResizing={ui.sidebarIsResizing}
|
||||
$sidebarCollapsed={sidebarCollapsed}
|
||||
$hasSidebar={!!sidebar}
|
||||
|
||||
@@ -103,7 +103,13 @@ function Right({ children, border, className, skipInitialAnimation }: Props) {
|
||||
};
|
||||
|
||||
return (
|
||||
<Sidebar {...animationProps} $border={border} className={className}>
|
||||
<Sidebar
|
||||
{...animationProps}
|
||||
$border={border}
|
||||
className={className}
|
||||
role="complementary"
|
||||
aria-label="Right sidebar"
|
||||
>
|
||||
<Position style={style} column>
|
||||
<ErrorBoundary>{children}</ErrorBoundary>
|
||||
<ResizeBorder
|
||||
|
||||
@@ -78,7 +78,7 @@ function SharedSidebar({ share }: Props) {
|
||||
<Shortcut>{metaDisplay}K</Shortcut>
|
||||
</SearchButton>
|
||||
</TopSection>
|
||||
<Section>
|
||||
<Section as="nav" aria-label={t("Documents")}>
|
||||
{share.collectionId ? (
|
||||
<SharedCollectionLink
|
||||
node={rootNode}
|
||||
|
||||
@@ -18,6 +18,7 @@ function Disclosure({ onClick, expanded, ...rest }: Props) {
|
||||
size={20}
|
||||
onClick={onClick}
|
||||
aria-label={expanded ? t("Collapse") : t("Expand")}
|
||||
aria-expanded={expanded}
|
||||
{...rest}
|
||||
>
|
||||
<StyledCollapsedIcon $expanded={expanded} size={20} />
|
||||
|
||||
@@ -147,6 +147,49 @@ function SidebarLink(
|
||||
|
||||
const DisclosureComponent = icon ? HiddenDisclosure : Disclosure;
|
||||
|
||||
const innerContent = (
|
||||
<>
|
||||
<ContextMenu action={contextAction} ariaLabel={t("Link options")}>
|
||||
<Content>
|
||||
{hasDisclosure && (
|
||||
<DisclosureComponent
|
||||
expanded={expanded}
|
||||
onClick={preventDefault}
|
||||
onPointerDown={handleDisclosureClick}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
)}
|
||||
{icon && <IconWrapper aria-hidden>{icon}</IconWrapper>}
|
||||
<Label $ellipsis={ellipsis}>{label}</Label>
|
||||
{unreadBadge && <UnreadBadge style={unreadStyle} />}
|
||||
</Content>
|
||||
</ContextMenu>
|
||||
{menu && <Actions $showActions={$showActions}>{menu}</Actions>}
|
||||
</>
|
||||
);
|
||||
|
||||
if (!to) {
|
||||
return (
|
||||
<Link
|
||||
as={href ? "a" : "button"}
|
||||
$isActiveDrop={isActiveDrop}
|
||||
$isDraft={isDraft}
|
||||
$disabled={disabled}
|
||||
style={active ? activeStyle : style}
|
||||
onClick={handleClick}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
onDragEnter={handleMouseEnter}
|
||||
href={href}
|
||||
className={className}
|
||||
ref={ref}
|
||||
{...rest}
|
||||
>
|
||||
{innerContent}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Link
|
||||
$isActiveDrop={isActiveDrop}
|
||||
@@ -159,31 +202,15 @@ function SidebarLink(
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
onDragEnter={handleMouseEnter}
|
||||
// @ts-expect-error exact does not exist on div
|
||||
exact={exact !== false}
|
||||
to={to}
|
||||
as={to ? undefined : href ? "a" : "div"}
|
||||
to={to!}
|
||||
href={href}
|
||||
className={className}
|
||||
// @ts-expect-error spread props cause overload mismatch with styled NavLink
|
||||
ref={ref}
|
||||
{...rest}
|
||||
>
|
||||
<ContextMenu action={contextAction} ariaLabel={t("Link options")}>
|
||||
<Content>
|
||||
{hasDisclosure && (
|
||||
<DisclosureComponent
|
||||
expanded={expanded}
|
||||
onClick={preventDefault}
|
||||
onPointerDown={handleDisclosureClick}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
)}
|
||||
{icon && <IconWrapper>{icon}</IconWrapper>}
|
||||
<Label $ellipsis={ellipsis}>{label}</Label>
|
||||
{unreadBadge && <UnreadBadge style={unreadStyle} />}
|
||||
</Content>
|
||||
</ContextMenu>
|
||||
{menu && <Actions $showActions={$showActions}>{menu}</Actions>}
|
||||
{innerContent}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
@@ -273,6 +300,8 @@ const Link = styled(NavLink)<{
|
||||
font-size: 16px;
|
||||
cursor: var(--pointer);
|
||||
overflow: hidden;
|
||||
border: 0;
|
||||
width: 100%;
|
||||
${undraggableOnDesktop()}
|
||||
|
||||
${(props) =>
|
||||
@@ -352,6 +381,8 @@ const Label = styled.div<{ $ellipsis: boolean }>`
|
||||
line-height: 24px;
|
||||
margin-left: 2px;
|
||||
min-width: 0;
|
||||
text-align: left;
|
||||
|
||||
${(props) => props.$ellipsis && ellipsis()}
|
||||
|
||||
* {
|
||||
|
||||
@@ -367,7 +367,11 @@ export default function FindAndReplace({
|
||||
return (
|
||||
<Popover open={localOpen} onOpenChange={setLocalOpen}>
|
||||
<PopoverTrigger>
|
||||
<span style={style} />
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t("Find and replace")}
|
||||
style={{ ...style, background: "none", border: 0, padding: 0 }}
|
||||
/>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
aria-label={t("Find and replace")}
|
||||
|
||||
@@ -72,7 +72,12 @@ function TableOfContentsMenu() {
|
||||
|
||||
return (
|
||||
<DropdownMenu action={rootAction} ariaLabel={t("Table of contents")}>
|
||||
<Button icon={<TableOfContentsIcon />} borderOnHover neutral />
|
||||
<Button
|
||||
icon={<TableOfContentsIcon />}
|
||||
aria-label={t("Table of contents")}
|
||||
borderOnHover
|
||||
neutral
|
||||
/>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -275,7 +275,9 @@ const DocumentTitle = React.forwardRef(function DocumentTitle_(
|
||||
</React.Suspense>
|
||||
</IconTitleWrapper>
|
||||
) : icon ? (
|
||||
<IconTitleWrapper dir={dir}>{fallbackIcon}</IconTitleWrapper>
|
||||
<IconTitleWrapper dir={dir} aria-hidden>
|
||||
{fallbackIcon}
|
||||
</IconTitleWrapper>
|
||||
) : null}
|
||||
</Title>
|
||||
);
|
||||
|
||||
@@ -77,30 +77,32 @@ function ReferenceListItem({
|
||||
const initial = title.charAt(0).toUpperCase();
|
||||
|
||||
return (
|
||||
<DocumentLink
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
to={{
|
||||
pathname: shareId
|
||||
? sharedModelPath(shareId, document.url)
|
||||
: document.url,
|
||||
hash: anchor ? `d-${anchor}` : undefined,
|
||||
state: {
|
||||
title: document.title,
|
||||
sidebarContext,
|
||||
},
|
||||
}}
|
||||
{...rest}
|
||||
>
|
||||
<Content gap={4} dir="auto">
|
||||
{icon ? (
|
||||
<Icon value={icon} color={color ?? undefined} initial={initial} />
|
||||
) : (
|
||||
<DocumentIcon />
|
||||
)}
|
||||
<Title>{isEmoji ? title.replace(icon!, "") : title}</Title>
|
||||
</Content>
|
||||
</DocumentLink>
|
||||
<li>
|
||||
<DocumentLink
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
to={{
|
||||
pathname: shareId
|
||||
? sharedModelPath(shareId, document.url)
|
||||
: document.url,
|
||||
hash: anchor ? `d-${anchor}` : undefined,
|
||||
state: {
|
||||
title: document.title,
|
||||
sidebarContext,
|
||||
},
|
||||
}}
|
||||
{...rest}
|
||||
>
|
||||
<Content gap={4} dir="auto">
|
||||
{icon ? (
|
||||
<Icon value={icon} color={color ?? undefined} initial={initial} />
|
||||
) : (
|
||||
<DocumentIcon />
|
||||
)}
|
||||
<Title>{isEmoji ? title.replace(icon!, "") : title}</Title>
|
||||
</Content>
|
||||
</DocumentLink>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -171,12 +171,15 @@ const Content = styled.div`
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const List = styled.div<{ $active: boolean }>`
|
||||
const List = styled.ul<{ $active: boolean }>`
|
||||
visibility: ${({ $active }) => ($active ? "visible" : "hidden")};
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
`;
|
||||
|
||||
export default observer(References);
|
||||
|
||||
@@ -42,6 +42,7 @@ function SharedCollection({ collection }: Props) {
|
||||
to={{
|
||||
pathname: collectionPath(collection, "overview"),
|
||||
}}
|
||||
aria-label={t("Edit collection")}
|
||||
neutral
|
||||
>
|
||||
{isMobile ? null : t("Edit")}
|
||||
|
||||
+2
-2
@@ -130,7 +130,7 @@ async function start(_id: number, disconnect: () => void) {
|
||||
}
|
||||
|
||||
this.body = `
|
||||
<html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Redirecting…</title>
|
||||
</head>
|
||||
@@ -146,7 +146,7 @@ async function start(_id: number, disconnect: () => void) {
|
||||
} else {
|
||||
// Default GET method using meta refresh
|
||||
this.body = `
|
||||
<html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta http-equiv="refresh" content="0;URL='${escape(url)}'" />
|
||||
</head>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Error - //inject-status//</title>
|
||||
<meta
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Error - //inject-status//</title>
|
||||
<meta
|
||||
|
||||
@@ -30,7 +30,12 @@ const Span = styled.span<{ $size: number }>`
|
||||
`;
|
||||
|
||||
const SVG = ({ size, emoji }: { size: number; emoji: string }) => (
|
||||
<svg width={size} height={size} xmlns="http://www.w3.org/2000/svg">
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<text
|
||||
x="50%"
|
||||
y="55%"
|
||||
|
||||
@@ -200,6 +200,7 @@ export default class Heading extends Node {
|
||||
anchor.innerText = "#";
|
||||
anchor.type = "button";
|
||||
anchor.className = "heading-anchor";
|
||||
anchor.setAttribute("aria-label", "Copy link to heading");
|
||||
anchor.addEventListener("mousedown", (event) =>
|
||||
this.handleCopyLink(event)
|
||||
);
|
||||
@@ -213,7 +214,15 @@ export default class Heading extends Node {
|
||||
fold.className = `heading-fold ${
|
||||
node.attrs.collapsed ? "collapsed" : ""
|
||||
}`;
|
||||
fold.addEventListener("mousedown", (event) =>
|
||||
fold.setAttribute(
|
||||
"aria-label",
|
||||
node.attrs.collapsed ? "Expand section" : "Collapse section"
|
||||
);
|
||||
fold.setAttribute(
|
||||
"aria-expanded",
|
||||
(!node.attrs.collapsed).toString()
|
||||
);
|
||||
fold.addEventListener("click", (event) =>
|
||||
this.handleFoldContent(event)
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user