diff --git a/app/components/ActionButton.tsx b/app/components/ActionButton.tsx index d04a33439e..7b9371f091 100644 --- a/app/components/ActionButton.tsx +++ b/app/components/ActionButton.tsx @@ -53,9 +53,10 @@ const ActionButton = React.forwardRef( } const label = - typeof action.name === "function" + rest["aria-label"] ?? + (typeof action.name === "function" ? action.name(actionContext) - : action.name; + : action.name); const button = ( diff --git a/app/components/OAuthClient/OAuthClientForm.tsx b/app/components/OAuthClient/OAuthClientForm.tsx index 2108e42ed6..4e1f26e2a6 100644 --- a/app/components/OAuthClient/OAuthClientForm.tsx +++ b/app/components/OAuthClient/OAuthClientForm.tsx @@ -63,6 +63,7 @@ export const OAuthClientForm = observer(function OAuthClientForm_({ name="avatarUrl" render={({ field }) => ( field.onChange(url)} onError={(err) => setError("avatarUrl", { message: err })} model={{ diff --git a/app/components/PaginatedList.tsx b/app/components/PaginatedList.tsx index 21906bb4de..775bf794d8 100644 --- a/app/components/PaginatedList.tsx +++ b/app/components/PaginatedList.tsx @@ -255,6 +255,7 @@ const PaginatedList = ({ {heading} + role="listbox" options={{ query, snippetMinWords: 10, snippetMaxWords: 11 }} items={cachedSearchResults} fetch={performSearch} diff --git a/app/components/Sharing/components/Actions.tsx b/app/components/Sharing/components/Actions.tsx index ea69af6989..a6d0e6fa17 100644 --- a/app/components/Sharing/components/Actions.tsx +++ b/app/components/Sharing/components/Actions.tsx @@ -25,6 +25,11 @@ export const AppearanceAction = observer(() => { onClick={() => ui.setTheme(resolvedTheme === "light" ? Theme.Dark : Theme.Light) } + aria-label={ + resolvedTheme === "light" + ? t("Switch to dark") + : t("Switch to light") + } neutral borderOnHover /> diff --git a/app/components/Sidebar/App.tsx b/app/components/Sidebar/App.tsx index 2cdc18f56f..c107114d1e 100644 --- a/app/components/Sidebar/App.tsx +++ b/app/components/Sidebar/App.tsx @@ -81,6 +81,11 @@ function AppSidebar() { } + aria-label={ + ui.sidebarCollapsed + ? t("Expand sidebar") + : t("Collapse sidebar") + } onClick={() => { ui.toggleCollapsedSidebar(); (document.activeElement as HTMLElement)?.blur(); diff --git a/app/components/Sidebar/Settings.tsx b/app/components/Sidebar/Settings.tsx index c014343665..3a27401c9e 100644 --- a/app/components/Sidebar/Settings.tsx +++ b/app/components/Sidebar/Settings.tsx @@ -52,6 +52,9 @@ function SettingsSidebar() { > } onClick={() => { diff --git a/app/components/Sidebar/Shared.tsx b/app/components/Sidebar/Shared.tsx index ea2e925b9b..12c481f713 100644 --- a/app/components/Sidebar/Shared.tsx +++ b/app/components/Sidebar/Shared.tsx @@ -96,6 +96,9 @@ const ToggleSidebar = () => { } + aria-label={ + ui.sidebarCollapsed ? t("Expand sidebar") : t("Collapse sidebar") + } onClick={() => { ui.toggleCollapsedSidebar(); (document.activeElement as HTMLElement)?.blur(); diff --git a/app/components/Sidebar/Sidebar.tsx b/app/components/Sidebar/Sidebar.tsx index 830757867f..107a9f7a18 100644 --- a/app/components/Sidebar/Sidebar.tsx +++ b/app/components/Sidebar/Sidebar.tsx @@ -21,6 +21,7 @@ import { TooltipProvider } from "../TooltipContext"; import ResizeBorder from "./components/ResizeBorder"; import SidebarButton from "./components/SidebarButton"; import ToggleButton from "./components/ToggleButton"; +import { useTranslation } from "react-i18next"; const ANIMATION_MS = 250; @@ -35,6 +36,7 @@ const Sidebar = React.forwardRef(function _Sidebar( ref: React.RefObject ) { const [isCollapsing, setCollapsing] = React.useState(false); + const { t } = useTranslation(); const theme = useTheme(); const { ui } = useStores(); const location = useLocation(); @@ -237,7 +239,7 @@ const Sidebar = React.forwardRef(function _Sidebar( position="bottom" image={ (function _Sidebar( } > - } /> + } + aria-label={t("Notifications")} + /> diff --git a/app/components/Sidebar/components/CollectionLink.tsx b/app/components/Sidebar/components/CollectionLink.tsx index 2a527746f5..9256d69c65 100644 --- a/app/components/Sidebar/components/CollectionLink.tsx +++ b/app/components/Sidebar/components/CollectionLink.tsx @@ -150,6 +150,7 @@ const CollectionLink: React.FC = ({ {can.createDocument && ( { ev.preventDefault(); setIsAddingNewChild(); diff --git a/app/components/Sidebar/components/DocumentLink.tsx b/app/components/Sidebar/components/DocumentLink.tsx index 72a8f44ee8..12a5e06387 100644 --- a/app/components/Sidebar/components/DocumentLink.tsx +++ b/app/components/Sidebar/components/DocumentLink.tsx @@ -364,7 +364,6 @@ function InnerDocumentLink( {can.createChildDocument && ( { ev.preventDefault(); diff --git a/app/components/Sidebar/components/DropToImport.tsx b/app/components/Sidebar/components/DropToImport.tsx index 5e2e9893b4..e3c190d01a 100644 --- a/app/components/Sidebar/components/DropToImport.tsx +++ b/app/components/Sidebar/components/DropToImport.tsx @@ -1,3 +1,4 @@ +import { VisuallyHidden } from "@radix-ui/react-visually-hidden"; import invariant from "invariant"; import { observer } from "mobx-react"; import { useCallback } from "react"; @@ -61,7 +62,12 @@ function DropToImport({ disabled, children, collectionId, documentId }: Props) { $isDragActive={isDragActive} tabIndex={-1} > - + + + {isImporting && } {children} diff --git a/app/components/Sidebar/components/Header.tsx b/app/components/Sidebar/components/Header.tsx index ded2c0354c..56dbacdff4 100644 --- a/app/components/Sidebar/components/Header.tsx +++ b/app/components/Sidebar/components/Header.tsx @@ -1,7 +1,7 @@ import { CollapsedIcon } from "outline-icons"; import * as React from "react"; import styled, { keyframes } from "styled-components"; -import { s } from "@shared/styles"; +import { extraArea, s } from "@shared/styles"; import usePersistedState from "~/hooks/usePersistedState"; import { undraggableOnDesktop } from "~/styles"; @@ -71,17 +71,18 @@ const Button = styled.button` font-size: 13px; font-weight: 600; user-select: none; - color: ${s("textTertiary")}; + color: ${s("sidebarText")}; + position: relative; letter-spacing: 0.03em; margin: 0; padding: 4px 2px 4px 12px; - height: 22px; border: 0; background: none; border-radius: 4px; -webkit-appearance: none; transition: all 100ms ease; ${undraggableOnDesktop()} + ${extraArea(4)} &:not(:disabled):hover, &:not(:disabled):active { @@ -102,7 +103,8 @@ const Disclosure = styled(CollapsedIcon)<{ expanded?: boolean }>` const H3 = styled.h3` margin: 0; - &:hover { + &:hover, + &:focus-within { ${Disclosure} { opacity: 1; } diff --git a/app/editor/components/FindAndReplace.tsx b/app/editor/components/FindAndReplace.tsx index 8f34419ee0..6b6e6e9593 100644 --- a/app/editor/components/FindAndReplace.tsx +++ b/app/editor/components/FindAndReplace.tsx @@ -347,6 +347,7 @@ export default function FindAndReplace({ editor.commands.prevSearchMatch()} + aria-label={t("Previous match")} > @@ -355,6 +356,7 @@ export default function FindAndReplace({ editor.commands.nextSearchMatch()} + aria-label={t("Next match")} > @@ -390,7 +392,10 @@ export default function FindAndReplace({ shortcut={`${altDisplay}+${metaDisplay}+c`} placement="bottom" > - + @@ -401,7 +406,10 @@ export default function FindAndReplace({ shortcut={`${altDisplay}+${metaDisplay}+r`} placement="bottom" > - + @@ -416,7 +424,10 @@ export default function FindAndReplace({ shortcut={`${altDisplay}+${metaDisplay}+f`} placement="bottom" > - + diff --git a/app/editor/components/MediaDimension.tsx b/app/editor/components/MediaDimension.tsx index d103580ba2..f74725f861 100644 --- a/app/editor/components/MediaDimension.tsx +++ b/app/editor/components/MediaDimension.tsx @@ -7,6 +7,7 @@ import { EditorStyleHelper } from "@shared/editor/styles/EditorStyleHelper"; import { extraArea } from "@shared/styles"; import Input, { NativeInput, Outline } from "~/components/Input"; import { useEditor } from "./EditorContext"; +import { useTranslation } from "react-i18next"; type Dimension = { width: string; @@ -20,6 +21,7 @@ export function MediaDimension() { width: { min: number; max: number }; height: { min: number; max: number }; }>(); + const { t } = useTranslation(); const { view, commands } = useEditor(); const { state } = view; const { selection } = state; @@ -205,6 +207,8 @@ export function MediaDimension() { return ( - x + × {(buttonProps) => ( - + {item.label && } {item.icon} @@ -118,6 +122,7 @@ function ToolbarMenu(props: Props) { {item.label && } {item.icon} diff --git a/app/editor/index.tsx b/app/editor/index.tsx index 5169a676c1..8edf2b01b5 100644 --- a/app/editor/index.tsx +++ b/app/editor/index.tsx @@ -498,6 +498,7 @@ export class Editor extends React.PureComponent< // Tell third-party libraries and screen-readers that this is an input view.dom.setAttribute("role", "textbox"); + view.dom.setAttribute("aria-label", "Editor content"); return view; } diff --git a/app/hooks/useDictionary.ts b/app/hooks/useDictionary.ts index cd76fc4cc1..5e2729fcc0 100644 --- a/app/hooks/useDictionary.ts +++ b/app/hooks/useDictionary.ts @@ -35,7 +35,7 @@ export default function useDictionary() { deleteRow: t("Delete"), deleteTable: t("Delete table"), deleteAttachment: t("Delete file"), - dimensions: t("Width x Height"), + dimensions: `${t("Width")} × ${t("Height")}`, download: t("Download"), downloadAttachment: t("Download file"), replaceAttachment: t("Replace file"), diff --git a/app/menus/NotificationMenu.tsx b/app/menus/NotificationMenu.tsx index 08b6c4495a..ad2d24dce7 100644 --- a/app/menus/NotificationMenu.tsx +++ b/app/menus/NotificationMenu.tsx @@ -20,7 +20,7 @@ const NotificationMenu: React.FC = () => { return ( - diff --git a/app/scenes/Document/components/DocumentTitle.tsx b/app/scenes/Document/components/DocumentTitle.tsx index 511dc17b5d..dd94bc7541 100644 --- a/app/scenes/Document/components/DocumentTitle.tsx +++ b/app/scenes/Document/components/DocumentTitle.tsx @@ -23,6 +23,7 @@ import { useDocumentContext } from "~/components/DocumentContext"; import { PopoverButton } from "~/components/IconPicker/components/PopoverButton"; import useBoolean from "~/hooks/useBoolean"; import usePolicy from "~/hooks/usePolicy"; +import { useTranslation } from "react-i18next"; const IconPicker = React.lazy(() => import("~/components/IconPicker")); @@ -70,6 +71,7 @@ const DocumentTitle = React.forwardRef(function _DocumentTitle( }: Props, externalRef: React.RefObject ) { + const { t } = useTranslation(); const ref = React.useRef(null); const [iconPickerIsOpen, handleOpen, setIconPickerClosed] = useBoolean(); const { editor } = useDocumentContext(); @@ -249,6 +251,7 @@ const DocumentTitle = React.forwardRef(function _DocumentTitle( autoFocus={!title} maxLength={DocumentValidation.maxTitleLength} readOnly={readOnly} + aria-label={t("Document title")} dir="auto" ref={mergeRefs([ref, externalRef])} > diff --git a/app/scenes/Document/components/Header.tsx b/app/scenes/Document/components/Header.tsx index 7e6c103b6d..2a544e508d 100644 --- a/app/scenes/Document/components/Header.tsx +++ b/app/scenes/Document/components/Header.tsx @@ -134,6 +134,7 @@ function DocumentHeader({ placement="bottom" > diff --git a/app/scenes/Settings/Application.tsx b/app/scenes/Settings/Application.tsx index e838a8541c..308ab8e7ea 100644 --- a/app/scenes/Settings/Application.tsx +++ b/app/scenes/Settings/Application.tsx @@ -161,6 +161,7 @@ const Application = observer(function Application({ oauthClient }: Props) { name="avatarUrl" render={({ field }) => ( field.onChange(url)} onError={(err) => setError("avatarUrl", { message: err })} model={{ diff --git a/app/scenes/Settings/Details.tsx b/app/scenes/Settings/Details.tsx index f39cc158e4..dce104b57e 100644 --- a/app/scenes/Settings/Details.tsx +++ b/app/scenes/Settings/Details.tsx @@ -193,6 +193,7 @@ function Details() { )} > { description={t("Choose a photo or image to represent yourself.")} > diff --git a/shared/editor/components/Caption.tsx b/shared/editor/components/Caption.tsx index 9b68931b4c..56c9c4a29c 100644 --- a/shared/editor/components/Caption.tsx +++ b/shared/editor/components/Caption.tsx @@ -2,6 +2,7 @@ import * as React from "react"; import styled from "styled-components"; import { s } from "../../styles"; import { EditorStyleHelper } from "../styles/EditorStyleHelper"; +import { useTranslation } from "react-i18next"; type Props = { /** Callback triggered when the caption is blurred */ @@ -23,6 +24,7 @@ type Props = { * A component that renders a caption for an image or video. */ function Caption({ placeholder, children, isSelected, width, ...rest }: Props) { + const { t } = useTranslation(); const handlePaste = (event: React.ClipboardEvent) => { event.preventDefault(); const text = event.clipboardData.getData("text/plain"); @@ -42,6 +44,7 @@ function Caption({ placeholder, children, isSelected, width, ...rest }: Props) { onPaste={handlePaste} className={EditorStyleHelper.imageCaption} tabIndex={-1} + aria-label={t("Caption")} role="textbox" contentEditable suppressContentEditableWarning diff --git a/shared/editor/nodes/CheckboxItem.ts b/shared/editor/nodes/CheckboxItem.ts index 24e77451ab..b600daa466 100644 --- a/shared/editor/nodes/CheckboxItem.ts +++ b/shared/editor/nodes/CheckboxItem.ts @@ -9,6 +9,7 @@ import toggleCheckboxItem from "../commands/toggleCheckboxItem"; import { MarkdownSerializerState } from "../lib/markdown/serializer"; import checkboxRule from "../rules/checkboxes"; import Node from "./Node"; +import { v4 } from "uuid"; export default class CheckboxItem extends Node { get name() { @@ -34,6 +35,7 @@ export default class CheckboxItem extends Node { }, ], toDOM: (node) => { + const id = `checkbox-${v4()}`; const checked = node.attrs.checked.toString(); let input; if (typeof document !== "undefined") { @@ -41,6 +43,7 @@ export default class CheckboxItem extends Node { input.tabIndex = -1; input.className = "checkbox"; input.setAttribute("aria-checked", checked); + input.setAttribute("aria-labelledby", id); input.setAttribute("role", "checkbox"); input.addEventListener("click", this.handleClick); } @@ -60,7 +63,7 @@ export default class CheckboxItem extends Node { ? [input] : [["span", { class: "checkbox", "aria-checked": checked }]]), ], - ["div", 0], + ["div", { id }, 0], ]; }, }; diff --git a/shared/i18n/locales/en_US/translation.json b/shared/i18n/locales/en_US/translation.json index aed8ac233c..abe62ce115 100644 --- a/shared/i18n/locales/en_US/translation.json +++ b/shared/i18n/locales/en_US/translation.json @@ -175,6 +175,7 @@ "currently viewing": "currently viewing", "previously edited": "previously edited", "You": "You", + "Avatar of {{ name }}": "Avatar of {{ name }}", "Viewers": "Viewers", "Collections are used to group documents and choose permissions": "Collections are used to group documents and choose permissions", "Name": "Name", @@ -205,6 +206,7 @@ "Move document": "Move document", "Moving": "Moving", "Moving the document {{ title }} to the {{ newCollectionName }} collection will change permission for all workspace members from {{ prevPermission }} to {{ newPermission }}.": "Moving the document {{ title }} to the {{ newCollectionName }} collection will change permission for all workspace members from {{ prevPermission }} to {{ newPermission }}.", + "More options": "More options", "Submenu": "Submenu", "Collections could not be loaded, please reload the app": "Collections could not be loaded, please reload the app", "Start view": "Start view", @@ -329,6 +331,7 @@ "Mark all as read": "Mark all as read", "You're all caught up": "You're all caught up", "Icon": "Icon", + "OAuth client icon": "OAuth client icon", "My App": "My App", "Tagline": "Tagline", "A short description": "A short description", @@ -405,12 +408,15 @@ "{{ count }} groups added to the document": "{{ count }} groups added to the document", "{{ count }} groups added to the document_plural": "{{ count }} groups added to the document", "Logo": "Logo", + "Expand sidebar": "Expand sidebar", + "Collapse sidebar": "Collapse sidebar", "Archived collections": "Archived collections", "New doc": "New doc", "Empty": "Empty", "Collapse": "Collapse", "Expand": "Expand", "Document not supported – try Markdown, Plain text, HTML, or Word": "Document not supported – try Markdown, Plain text, HTML, or Word", + "Import files": "Import files", "Go back": "Go back", "Go forward": "Go forward", "Could not load shared documents": "Could not load shared documents", @@ -427,6 +433,7 @@ "The {{ documentName }} cannot be moved here": "The {{ documentName }} cannot be moved here", "Return to App": "Back to App", "Installation": "Installation", + "Avatar of {{ name} }": "Avatar of {{ name} }", "Unstar document": "Unstar document", "Star document": "Star document", "Template created, go ahead and customize it": "Template created, go ahead and customize it", @@ -460,6 +467,8 @@ "Replacement": "Replacement", "Replace": "Replace", "Replace all": "Replace all", + "Image width": "Image width", + "Image height": "Image height", "Profile picture": "Profile picture", "Create a new doc": "Create a new doc", "{{ userName }} won't be notified, as they do not have access to this document": "{{ userName }} won't be notified, as they do not have access to this document", @@ -486,7 +495,8 @@ "Create a new child doc": "Create a new child doc", "Delete table": "Delete table", "Delete file": "Delete file", - "Width x Height": "Width x Height", + "Width": "Width", + "Height": "Height", "Download file": "Download file", "Replace file": "Replace file", "Delete image": "Delete image", @@ -680,6 +690,7 @@ "only you": "only you", "person": "person", "people": "people", + "Document title": "Document title", "Last updated": "Last updated", "Type '/' to insert, or start writing…": "Type '/' to insert, or start writing…", "Hide contents": "Hide contents", @@ -924,6 +935,7 @@ "Rotate secret": "Rotate secret", "Rotating the client secret will invalidate the current secret. Make sure to update any applications using these credentials.": "Rotating the client secret will invalidate the current secret. Make sure to update any applications using these credentials.", "Displayed to users when authorizing": "Displayed to users when authorizing", + "Application icon": "Application icon", "Developer information shown to users when authorizing": "Developer information shown to users when authorizing", "Developer name": "Developer name", "Developer URL": "Developer URL", @@ -1030,6 +1042,7 @@ "These settings affect the way that your workspace appears to everyone on the team.": "These settings affect the way that your workspace appears to everyone on the team.", "Display": "Display", "The logo is displayed at the top left of the application.": "The logo is displayed at the top left of the application.", + "Workspace logo": "Workspace logo", "The workspace name, usually the same as your company name.": "The workspace name, usually the same as your company name.", "Description": "Description", "A short description of your workspace.": "A short description of your workspace.", @@ -1260,6 +1273,7 @@ "{{ user }} updated {{ timeAgo }}": "{{ user }} updated {{ timeAgo }}", "You created {{ timeAgo }}": "You created {{ timeAgo }}", "{{ user }} created {{ timeAgo }}": "{{ user }} created {{ timeAgo }}", + "Caption": "Caption", "Open": "Open", "Error loading data": "Error loading data" }