mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
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:
parent
5fd6c4d853
commit
69061e20ac
11 changed files with 177 additions and 107 deletions
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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)}
|
||||
|
|
62
src/components/ToolButton.tsx
Normal file
62
src/components/ToolButton.tsx
Normal 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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue