mirror of
https://github.com/outline/outline.git
synced 2026-06-13 03:14:59 +03:00
fix: Various accessibility issues (#10115)
* Round 1 * Round 2 * Shared page
This commit is contained in:
@@ -53,9 +53,10 @@ const ActionButton = React.forwardRef<HTMLButtonElement, Props>(
|
||||
}
|
||||
|
||||
const label =
|
||||
typeof action.name === "function"
|
||||
rest["aria-label"] ??
|
||||
(typeof action.name === "function"
|
||||
? action.name(actionContext)
|
||||
: action.name;
|
||||
: action.name);
|
||||
|
||||
const button = (
|
||||
<button
|
||||
|
||||
@@ -25,6 +25,8 @@ type Props = {
|
||||
onClick?: React.MouseEventHandler<HTMLImageElement>;
|
||||
/** Size of the avatar, defaults to AvatarSize.Large */
|
||||
size?: AvatarSize;
|
||||
/** Optional alt text for the avatar image */
|
||||
alt?: string;
|
||||
/** Optional inline styles to apply to the avatar wrapper */
|
||||
style?: React.CSSProperties;
|
||||
};
|
||||
@@ -53,6 +55,7 @@ function AvatarWithPresence({
|
||||
isCurrentUser,
|
||||
size = AvatarSize.Large,
|
||||
style,
|
||||
alt,
|
||||
}: Props) {
|
||||
const { t } = useTranslation();
|
||||
const status = isPresent
|
||||
@@ -83,7 +86,7 @@ function AvatarWithPresence({
|
||||
$color={user.color}
|
||||
style={style}
|
||||
>
|
||||
<Avatar model={user} onClick={onClick} size={size} />
|
||||
<Avatar model={user} onClick={onClick} size={size} alt={alt} />
|
||||
</AvatarPresence>
|
||||
</Tooltip>
|
||||
</>
|
||||
|
||||
@@ -132,6 +132,7 @@ function Collaborators(props: Props) {
|
||||
isEditing={isEditing}
|
||||
isObserving={isObserving}
|
||||
isCurrentUser={currentUserId === collaborator.id}
|
||||
alt={t("Avatar of {{ name }}", { name: collaborator.name })}
|
||||
onClick={
|
||||
isObservable
|
||||
? handleAvatarClick(
|
||||
|
||||
@@ -143,13 +143,14 @@ const ContentEditable = React.forwardRef(function _ContentEditable(
|
||||
},
|
||||
[]
|
||||
);
|
||||
const contentEditable = !disabled && !readOnly;
|
||||
|
||||
return (
|
||||
<div className={className} dir={dir} onClick={onClick} tabIndex={-1}>
|
||||
{children}
|
||||
<Content
|
||||
ref={contentRef}
|
||||
contentEditable={!disabled && !readOnly}
|
||||
contentEditable={contentEditable}
|
||||
onInput={wrappedEvent(onInput)}
|
||||
onFocus={wrappedEvent(onFocus)}
|
||||
onBlur={wrappedEvent(onBlur)}
|
||||
@@ -157,7 +158,7 @@ const ContentEditable = React.forwardRef(function _ContentEditable(
|
||||
onPaste={handlePaste}
|
||||
data-placeholder={placeholder}
|
||||
suppressContentEditableWarning
|
||||
role="textbox"
|
||||
role={contentEditable ? "textbox" : undefined}
|
||||
{...rest}
|
||||
>
|
||||
{innerValue}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { MoreIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { MenuButton } from "reakit/Menu";
|
||||
import NudeButton from "~/components/NudeButton";
|
||||
|
||||
@@ -8,10 +9,16 @@ type Props = React.ComponentProps<typeof MenuButton> & {
|
||||
};
|
||||
|
||||
export default function OverflowMenuButton({ className, ...rest }: Props) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<MenuButton {...rest}>
|
||||
{(props) => (
|
||||
<NudeButton className={className} {...props}>
|
||||
<NudeButton
|
||||
className={className}
|
||||
aria-label={t("More options")}
|
||||
{...props}
|
||||
>
|
||||
<MoreIcon />
|
||||
</NudeButton>
|
||||
)}
|
||||
|
||||
@@ -114,7 +114,6 @@ function DocumentListItem(
|
||||
<DocumentLink
|
||||
ref={itemRef}
|
||||
dir={document.dir}
|
||||
role="menuitem"
|
||||
$isStarred={document.isStarred}
|
||||
$menuOpen={menuOpen}
|
||||
to={{
|
||||
@@ -279,7 +278,7 @@ const DocumentLink = styled(Link)<{
|
||||
`}
|
||||
`;
|
||||
|
||||
const Heading = styled.h3<{ rtl?: boolean }>`
|
||||
const Heading = styled.span<{ rtl?: boolean }>`
|
||||
display: flex;
|
||||
justify-content: ${(props) => (props.rtl ? "flex-end" : "flex-start")};
|
||||
align-items: center;
|
||||
@@ -289,6 +288,7 @@ const Heading = styled.h3<{ rtl?: boolean }>`
|
||||
color: ${s("text")};
|
||||
font-family: ${s("fontFamily")};
|
||||
font-weight: 500;
|
||||
font-size: 20px;
|
||||
`;
|
||||
|
||||
const StarPositioner = styled(Flex)`
|
||||
|
||||
@@ -168,13 +168,7 @@ const DocumentMeta: React.FC<Props> = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<Container
|
||||
align="center"
|
||||
rtl={document.dir === "rtl"}
|
||||
{...rest}
|
||||
dir="ltr"
|
||||
lang=""
|
||||
>
|
||||
<Container align="center" rtl={document.dir === "rtl"} {...rest} dir="ltr">
|
||||
{to ? (
|
||||
<Link to={to} replace={replace}>
|
||||
{content}
|
||||
|
||||
@@ -67,7 +67,11 @@ function Notifications(
|
||||
<Flex gap={8}>
|
||||
{notifications.approximateUnreadCount > 0 && (
|
||||
<Tooltip content={t("Mark all as read")}>
|
||||
<Button action={markNotificationsAsRead} context={context}>
|
||||
<Button
|
||||
action={markNotificationsAsRead}
|
||||
context={context}
|
||||
aria-label={t("Mark all as read")}
|
||||
>
|
||||
<MarkAsReadIcon />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
@@ -63,6 +63,7 @@ export const OAuthClientForm = observer(function OAuthClientForm_({
|
||||
name="avatarUrl"
|
||||
render={({ field }) => (
|
||||
<ImageInput
|
||||
alt={t("OAuth client icon")}
|
||||
onSuccess={(url) => field.onChange(url)}
|
||||
onError={(err) => setError("avatarUrl", { message: err })}
|
||||
model={{
|
||||
|
||||
@@ -255,6 +255,7 @@ const PaginatedList = <T extends PaginatedItem>({
|
||||
<React.Fragment>
|
||||
{heading}
|
||||
<ArrowKeyNavigation
|
||||
role={rest.role}
|
||||
aria-label={rest["aria-label"]}
|
||||
onEscape={onEscape}
|
||||
className={className}
|
||||
|
||||
@@ -168,6 +168,7 @@ function SearchPopover({ shareId, className }: Props) {
|
||||
<Popover open={open} onOpenChange={setOpen} modal={true}>
|
||||
<PopoverAnchor>
|
||||
<StyledInputSearch
|
||||
role="combobox"
|
||||
aria-controls="search-results"
|
||||
aria-expanded={open}
|
||||
aria-haspopup="listbox"
|
||||
@@ -176,6 +177,8 @@ function SearchPopover({ shareId, className }: Props) {
|
||||
onFocus={handleSearchInputFocus}
|
||||
onKeyDown={handleKeyDown}
|
||||
className={className}
|
||||
label={t("Search")}
|
||||
labelHidden
|
||||
/>
|
||||
</PopoverAnchor>
|
||||
<PopoverContent
|
||||
@@ -194,6 +197,7 @@ function SearchPopover({ shareId, className }: Props) {
|
||||
}}
|
||||
>
|
||||
<PaginatedList<SearchResult>
|
||||
role="listbox"
|
||||
options={{ query, snippetMinWords: 10, snippetMaxWords: 11 }}
|
||||
items={cachedSearchResults}
|
||||
fetch={performSearch}
|
||||
|
||||
@@ -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
|
||||
/>
|
||||
|
||||
@@ -81,6 +81,11 @@ function AppSidebar() {
|
||||
<ToggleButton
|
||||
position="bottom"
|
||||
image={<SidebarIcon />}
|
||||
aria-label={
|
||||
ui.sidebarCollapsed
|
||||
? t("Expand sidebar")
|
||||
: t("Collapse sidebar")
|
||||
}
|
||||
onClick={() => {
|
||||
ui.toggleCollapsedSidebar();
|
||||
(document.activeElement as HTMLElement)?.blur();
|
||||
|
||||
@@ -52,6 +52,9 @@ function SettingsSidebar() {
|
||||
>
|
||||
<Tooltip content={t("Toggle sidebar")} shortcut={`${metaDisplay}+.`}>
|
||||
<ToggleButton
|
||||
aria-label={
|
||||
ui.sidebarCollapsed ? t("Expand sidebar") : t("Collapse sidebar")
|
||||
}
|
||||
position="bottom"
|
||||
image={<SidebarIcon />}
|
||||
onClick={() => {
|
||||
|
||||
@@ -96,6 +96,9 @@ const ToggleSidebar = () => {
|
||||
<ToggleButton
|
||||
position="bottom"
|
||||
image={<SidebarIcon />}
|
||||
aria-label={
|
||||
ui.sidebarCollapsed ? t("Expand sidebar") : t("Collapse sidebar")
|
||||
}
|
||||
onClick={() => {
|
||||
ui.toggleCollapsedSidebar();
|
||||
(document.activeElement as HTMLElement)?.blur();
|
||||
|
||||
@@ -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<HTMLDivElement, Props>(function _Sidebar(
|
||||
ref: React.RefObject<HTMLDivElement>
|
||||
) {
|
||||
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<HTMLDivElement, Props>(function _Sidebar(
|
||||
position="bottom"
|
||||
image={
|
||||
<Avatar
|
||||
alt={user.name}
|
||||
alt={t("Avatar of {{ name} }", { name: user.name })}
|
||||
model={user}
|
||||
size={24}
|
||||
style={{ marginLeft: 4 }}
|
||||
@@ -245,7 +247,11 @@ const Sidebar = React.forwardRef<HTMLDivElement, Props>(function _Sidebar(
|
||||
}
|
||||
>
|
||||
<NotificationsPopover>
|
||||
<SidebarButton position="bottom" image={<NotificationIcon />} />
|
||||
<SidebarButton
|
||||
position="bottom"
|
||||
image={<NotificationIcon />}
|
||||
aria-label={t("Notifications")}
|
||||
/>
|
||||
</NotificationsPopover>
|
||||
</SidebarButton>
|
||||
</AccountMenu>
|
||||
|
||||
@@ -150,6 +150,7 @@ const CollectionLink: React.FC<Props> = ({
|
||||
{can.createDocument && (
|
||||
<NudeButton
|
||||
tooltip={{ content: t("New doc"), delay: 500 }}
|
||||
aria-label={t("New nested document")}
|
||||
onClick={(ev) => {
|
||||
ev.preventDefault();
|
||||
setIsAddingNewChild();
|
||||
|
||||
@@ -364,7 +364,6 @@ function InnerDocumentLink(
|
||||
{can.createChildDocument && (
|
||||
<Tooltip content={t("New doc")}>
|
||||
<NudeButton
|
||||
type={undefined}
|
||||
aria-label={t("New nested document")}
|
||||
onClick={(ev) => {
|
||||
ev.preventDefault();
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
<input {...getInputProps()} />
|
||||
<VisuallyHidden>
|
||||
<label>
|
||||
{t("Import files")}
|
||||
<input {...getInputProps()} />
|
||||
</label>
|
||||
</VisuallyHidden>
|
||||
{isImporting && <LoadingIndicator />}
|
||||
{children}
|
||||
</DropzoneContainer>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -347,6 +347,7 @@ export default function FindAndReplace({
|
||||
<ButtonLarge
|
||||
disabled={disabled}
|
||||
onClick={() => editor.commands.prevSearchMatch()}
|
||||
aria-label={t("Previous match")}
|
||||
>
|
||||
<CaretUpIcon />
|
||||
</ButtonLarge>
|
||||
@@ -355,6 +356,7 @@ export default function FindAndReplace({
|
||||
<ButtonLarge
|
||||
disabled={disabled}
|
||||
onClick={() => editor.commands.nextSearchMatch()}
|
||||
aria-label={t("Next match")}
|
||||
>
|
||||
<CaretDownIcon />
|
||||
</ButtonLarge>
|
||||
@@ -390,7 +392,10 @@ export default function FindAndReplace({
|
||||
shortcut={`${altDisplay}+${metaDisplay}+c`}
|
||||
placement="bottom"
|
||||
>
|
||||
<ButtonSmall onClick={handleCaseSensitive}>
|
||||
<ButtonSmall
|
||||
onClick={handleCaseSensitive}
|
||||
aria-label={t("Match case")}
|
||||
>
|
||||
<CaseSensitiveIcon
|
||||
color={caseSensitive ? theme.accent : theme.textSecondary}
|
||||
/>
|
||||
@@ -401,7 +406,10 @@ export default function FindAndReplace({
|
||||
shortcut={`${altDisplay}+${metaDisplay}+r`}
|
||||
placement="bottom"
|
||||
>
|
||||
<ButtonSmall onClick={handleRegex}>
|
||||
<ButtonSmall
|
||||
onClick={handleRegex}
|
||||
aria-label={t("Enable regex")}
|
||||
>
|
||||
<RegexIcon
|
||||
color={regexEnabled ? theme.accent : theme.textSecondary}
|
||||
/>
|
||||
@@ -416,7 +424,10 @@ export default function FindAndReplace({
|
||||
shortcut={`${altDisplay}+${metaDisplay}+f`}
|
||||
placement="bottom"
|
||||
>
|
||||
<ButtonLarge onClick={handleMore}>
|
||||
<ButtonLarge
|
||||
onClick={handleMore}
|
||||
aria-label={t("Replace options")}
|
||||
>
|
||||
<ReplaceIcon color={theme.textSecondary} />
|
||||
</ButtonLarge>
|
||||
</Tooltip>
|
||||
|
||||
@@ -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 (
|
||||
<StyledFlex ref={ref} align="center">
|
||||
<StyledInput
|
||||
label={t("Image width")}
|
||||
labelHidden
|
||||
value={localDimension.width}
|
||||
onChange={handleChange("width")}
|
||||
onBlur={handleBlur}
|
||||
@@ -212,9 +216,11 @@ export function MediaDimension() {
|
||||
$error={error.width}
|
||||
/>
|
||||
<Text size="xsmall" type="tertiary">
|
||||
x
|
||||
×
|
||||
</Text>
|
||||
<StyledInput
|
||||
label={t("Image height")}
|
||||
labelHidden
|
||||
value={localDimension.height}
|
||||
onChange={handleChange("height")}
|
||||
onBlur={handleBlur}
|
||||
|
||||
@@ -64,7 +64,11 @@ function ToolbarDropdown(props: { active: boolean; item: MenuItem }) {
|
||||
<>
|
||||
<MenuButton {...menu}>
|
||||
{(buttonProps) => (
|
||||
<ToolbarButton {...buttonProps} hovering={menu.visible}>
|
||||
<ToolbarButton
|
||||
{...buttonProps}
|
||||
hovering={menu.visible}
|
||||
aria-label={item.tooltip}
|
||||
>
|
||||
{item.label && <Label>{item.label}</Label>}
|
||||
{item.icon}
|
||||
</ToolbarButton>
|
||||
@@ -118,6 +122,7 @@ function ToolbarMenu(props: Props) {
|
||||
<ToolbarButton
|
||||
onClick={handleClick(item)}
|
||||
active={isActive && !item.label}
|
||||
aria-label={item.label ? undefined : item.tooltip}
|
||||
>
|
||||
{item.label && <Label>{item.label}</Label>}
|
||||
{item.icon}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -20,7 +20,7 @@ const NotificationMenu: React.FC = () => {
|
||||
|
||||
return (
|
||||
<DropdownMenu action={rootAction} ariaLabel={t("Notifications")}>
|
||||
<Button>
|
||||
<Button aria-label={t("Notifications")}>
|
||||
<MoreIcon />
|
||||
</Button>
|
||||
</DropdownMenu>
|
||||
|
||||
@@ -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<RefHandle>
|
||||
) {
|
||||
const { t } = useTranslation();
|
||||
const ref = React.useRef<RefHandle>(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])}
|
||||
>
|
||||
|
||||
@@ -134,6 +134,7 @@ function DocumentHeader({
|
||||
placement="bottom"
|
||||
>
|
||||
<Button
|
||||
aria-label={t("Show contents")}
|
||||
onClick={handleToggle}
|
||||
icon={<TableOfContentsIcon />}
|
||||
borderOnHover
|
||||
|
||||
@@ -23,7 +23,11 @@ function KeyboardShortcutsButton() {
|
||||
|
||||
return (
|
||||
<Tooltip content={t("Keyboard shortcuts")} shortcut="?">
|
||||
<Button onClick={handleOpenKeyboardShortcuts} $hidden={isEditingFocus}>
|
||||
<Button
|
||||
onClick={handleOpenKeyboardShortcuts}
|
||||
$hidden={isEditingFocus}
|
||||
aria-label={t("Keyboard shortcuts")}
|
||||
>
|
||||
<KeyboardIcon />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
@@ -161,6 +161,7 @@ const Application = observer(function Application({ oauthClient }: Props) {
|
||||
name="avatarUrl"
|
||||
render={({ field }) => (
|
||||
<ImageInput
|
||||
alt={t("Application icon")}
|
||||
onSuccess={(url) => field.onChange(url)}
|
||||
onError={(err) => setError("avatarUrl", { message: err })}
|
||||
model={{
|
||||
|
||||
@@ -193,6 +193,7 @@ function Details() {
|
||||
)}
|
||||
>
|
||||
<ImageInput
|
||||
alt={t("Workspace logo")}
|
||||
onSuccess={handleAvatarChange}
|
||||
onError={handleAvatarError}
|
||||
model={team}
|
||||
|
||||
@@ -72,6 +72,7 @@ const Profile = () => {
|
||||
description={t("Choose a photo or image to represent yourself.")}
|
||||
>
|
||||
<ImageInput
|
||||
alt={t("Profile picture")}
|
||||
onSuccess={handleAvatarChange}
|
||||
onError={handleAvatarError}
|
||||
model={user}
|
||||
|
||||
@@ -10,9 +10,10 @@ import ImageUpload, { Props as ImageUploadProps } from "./ImageUpload";
|
||||
|
||||
type Props = ImageUploadProps & {
|
||||
model: IAvatar;
|
||||
alt: string;
|
||||
};
|
||||
|
||||
export default function ImageInput({ model, onSuccess, ...rest }: Props) {
|
||||
export default function ImageInput({ model, onSuccess, alt, ...rest }: Props) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
@@ -27,6 +28,7 @@ export default function ImageInput({ model, onSuccess, ...rest }: Props) {
|
||||
model={model}
|
||||
size={AvatarSize.Upload}
|
||||
variant={AvatarVariant.Square}
|
||||
alt={alt}
|
||||
/>
|
||||
<Flex auto align="center" justify="center" className="upload">
|
||||
<EditIcon />
|
||||
|
||||
@@ -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<HTMLParagraphElement>) => {
|
||||
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
|
||||
|
||||
@@ -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],
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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 <em>{{ title }}</em> to the {{ newCollectionName }} collection will change permission for all workspace members from <em>{{ prevPermission }}</em> to <em>{{ newPermission }}</em>.": "Moving the document <em>{{ title }}</em> to the {{ newCollectionName }} collection will change permission for all workspace members from <em>{{ prevPermission }}</em> to <em>{{ newPermission }}</em>.",
|
||||
"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"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user