Some a11y fixes (#534)

* Rename ToolIcon to ToolButton

It makes more semantic sense

* Label and keyboard shortcuts announcement

* Refactor common props for ToolButton

* Better doc outline and form controls

* Adjust color picker

* Styling fixes

Co-authored-by: Christopher Chedeau <vjeuxx@gmail.com>
This commit is contained in:
Guillermo Peralta Scura 2020-01-25 14:52:03 -03:00 committed by Christopher Chedeau
parent 5fd6c4d853
commit 69061e20ac
11 changed files with 177 additions and 107 deletions

View file

@ -4,21 +4,28 @@ export function ButtonSelect<T>({
options,
value,
onChange,
group,
}: {
options: { value: T; text: string }[];
value: T | null;
onChange: (value: T) => void;
group: string;
}) {
return (
<div className="buttonList">
{options.map(option => (
<button
<label
key={option.text}
onClick={() => onChange(option.value)}
className={value === option.value ? "active" : ""}
>
<input
type="radio"
name={group}
onChange={() => onChange(option.value)}
checked={value === option.value ? true : false}
/>
{option.text}
</button>
</label>
))}
</div>
);

View file

@ -10,10 +10,12 @@ const Picker = function({
colors,
color,
onChange,
label,
}: {
colors: string[];
color: string | null;
onChange: (color: string) => void;
label: string;
}) {
return (
<div className="color-picker">
@ -42,6 +44,7 @@ const Picker = function({
</div>
<ColorInput
color={color}
label={label}
onChange={color => {
onChange(color);
}}
@ -54,9 +57,11 @@ const Picker = function({
function ColorInput({
color,
onChange,
label,
}: {
color: string | null;
onChange: (color: string) => void;
label: string;
}) {
const colorRegex = /^([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8}|transparent)$/;
const [innerValue, setInnerValue] = React.useState(color);
@ -71,7 +76,7 @@ function ColorInput({
<input
spellCheck={false}
className="color-picker-input"
aria-label="Hex color code"
aria-label={label}
onChange={e => {
const value = e.target.value;
if (value.match(colorRegex)) {
@ -91,10 +96,12 @@ export function ColorPicker({
type,
color,
onChange,
label,
}: {
type: "canvasBackground" | "elementBackground" | "elementStroke";
color: string | null;
onChange: (color: string) => void;
label: string;
}) {
const [isActive, setActive] = React.useState(false);
@ -103,12 +110,13 @@ export function ColorPicker({
<div className="color-picker-control-container">
<button
className="color-picker-label-swatch"
aria-label="Change color"
aria-label={label}
style={color ? { backgroundColor: color } : undefined}
onClick={() => setActive(!isActive)}
/>
<ColorInput
color={color}
label={label}
onChange={color => {
onChange(color);
}}
@ -123,6 +131,7 @@ export function ColorPicker({
onChange={changedColor => {
onChange(changedColor);
}}
label={label}
/>
</Popover>
) : null}

View file

@ -3,7 +3,7 @@ import "./ExportDialog.css";
import React, { useState, useEffect, useRef } from "react";
import { Modal } from "./Modal";
import { ToolIcon } from "./ToolIcon";
import { ToolButton } from "./ToolButton";
import { clipboard, exportFile, downloadFile, link } from "./icons";
import { Island } from "./Island";
import { ExcalidrawElement } from "../element/types";
@ -91,7 +91,7 @@ export function ExportDialog({
return (
<>
<ToolIcon
<ToolButton
onClick={() => setModalIsShown(true)}
icon={exportFile}
type="button"
@ -109,7 +109,7 @@ export function ExportDialog({
<div className="ExportDialog__preview" ref={previewRef}></div>
<div className="ExportDialog__actions">
<Stack.Row gap={2}>
<ToolIcon
<ToolButton
type="button"
icon={downloadFile}
title={t("buttons.exportToPng")}
@ -117,7 +117,7 @@ export function ExportDialog({
onClick={() => onExportToPng(exportedElements, scale)}
/>
{probablySupportsClipboard && (
<ToolIcon
<ToolButton
type="button"
icon={clipboard}
title={t("buttons.copyToClipboard")}
@ -127,7 +127,7 @@ export function ExportDialog({
}
/>
)}
<ToolIcon
<ToolButton
type="button"
icon={link}
title={t("buttons.getShareableLink")}
@ -147,12 +147,13 @@ export function ExportDialog({
<div className="ExportDialog__scales">
<Stack.Row gap={1} align="baseline">
{scales.map(s => (
<ToolIcon
<ToolButton
key={s}
size="s"
type="radio"
icon={"x" + s}
name="export-canvas-scale"
aria-label="Export"
id="export-canvas-scale"
checked={scale === s}
onChange={() => setScale(s)}

View file

@ -0,0 +1,62 @@
import "./ToolIcon.scss";
import React from "react";
type ToolIconSize = "s" | "m";
type ToolButtonBaseProps = {
icon: React.ReactNode;
"aria-label": string;
"aria-keyshortcuts"?: string;
title?: string;
name?: string;
id?: string;
size?: ToolIconSize;
};
type ToolButtonProps =
| (ToolButtonBaseProps & { type: "button"; onClick?(): void })
| (ToolButtonBaseProps & {
type: "radio";
checked: boolean;
onChange?(): void;
});
const DEFAULT_SIZE: ToolIconSize = "m";
export function ToolButton(props: ToolButtonProps) {
const sizeCn = `ToolIcon_size_${props.size || DEFAULT_SIZE}`;
if (props.type === "button")
return (
<button
className={`ToolIcon_type_button ToolIcon ${sizeCn}`}
title={props.title}
aria-label={props["aria-label"]}
type="button"
onClick={props.onClick}
>
<div className="ToolIcon__icon" aria-hidden="true">
{props.icon}
</div>
</button>
);
return (
<label className="ToolIcon">
<input
className={`ToolIcon_type_radio ${sizeCn}`}
type="radio"
name={props.name}
title={props.title}
aria-label={props["aria-label"]}
aria-keyshortcuts={props["aria-keyshortcuts"]}
id={props.id}
onChange={props.onChange}
checked={props.checked}
/>
<div className="ToolIcon__icon">{props.icon}</div>
</label>
);
}

View file

@ -1,61 +0,0 @@
import "./ToolIcon.scss";
import React from "react";
type ToolIconSize = "s" | "m";
type ToolIconProps =
| {
type: "button";
icon: React.ReactNode;
"aria-label": string;
title?: string;
name?: string;
id?: string;
onClick?(): void;
size?: ToolIconSize;
}
| {
type: "radio";
icon: React.ReactNode;
title?: string;
name?: string;
id?: string;
checked: boolean;
onChange?(): void;
size?: ToolIconSize;
};
const DEFAULT_SIZE: ToolIconSize = "m";
export function ToolIcon(props: ToolIconProps) {
const sizeCn = `ToolIcon_size_${props.size || DEFAULT_SIZE}`;
if (props.type === "button")
return (
<label className={`ToolIcon ${sizeCn}`} title={props.title}>
<button
className="ToolIcon_type_button"
aria-label={props["aria-label"]}
type="button"
onClick={props.onClick}
>
<div className="ToolIcon__icon">{props.icon}</div>
</button>
</label>
);
return (
<label className={`ToolIcon ${sizeCn}`} title={props.title}>
<input
className="ToolIcon_type_radio"
type="radio"
name={props.name}
id={props.id}
onChange={props.onChange}
checked={props.checked}
/>
<div className="ToolIcon__icon">{props.icon}</div>
</label>
);
}