use icons for toggle labels (#2315)
This commit is contained in:
parent
856ab50090
commit
7491fcc3f3
@ -12,6 +12,7 @@ import {
|
||||
canChangeSharpness,
|
||||
} from "../scene";
|
||||
import { ButtonSelect } from "../components/ButtonSelect";
|
||||
import { ButtonIconSelect } from "../components/ButtonIconSelect";
|
||||
import {
|
||||
isTextElement,
|
||||
redrawTextBoundingBox,
|
||||
@ -25,6 +26,20 @@ import { register } from "./register";
|
||||
import { newElementWith } from "../element/mutateElement";
|
||||
import { DEFAULT_FONT_SIZE, DEFAULT_FONT_FAMILY } from "../constants";
|
||||
import { randomInteger } from "../random";
|
||||
import {
|
||||
FillHachureIcon,
|
||||
FillCrossHatchIcon,
|
||||
FillSolidIcon,
|
||||
StrokeWidthIcon,
|
||||
StrokeStyleSolidIcon,
|
||||
StrokeStyleDashedIcon,
|
||||
StrokeStyleDottedIcon,
|
||||
EdgeSharpIcon,
|
||||
EdgeRoundIcon,
|
||||
SloppinessArchitectIcon,
|
||||
SloppinessArtistIcon,
|
||||
SloppinessCartoonistIcon,
|
||||
} from "../components/icons";
|
||||
|
||||
const changeProperty = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
@ -141,11 +156,23 @@ export const actionChangeFillStyle = register({
|
||||
PanelComponent: ({ elements, appState, updateData }) => (
|
||||
<fieldset>
|
||||
<legend>{t("labels.fill")}</legend>
|
||||
<ButtonSelect
|
||||
<ButtonIconSelect
|
||||
options={[
|
||||
{ value: "hachure", text: t("labels.hachure") },
|
||||
{ value: "cross-hatch", text: t("labels.crossHatch") },
|
||||
{ value: "solid", text: t("labels.solid") },
|
||||
{
|
||||
value: "hachure",
|
||||
text: t("labels.hachure"),
|
||||
icon: <FillHachureIcon appearance={appState.appearance} />,
|
||||
},
|
||||
{
|
||||
value: "cross-hatch",
|
||||
text: t("labels.crossHatch"),
|
||||
icon: <FillCrossHatchIcon appearance={appState.appearance} />,
|
||||
},
|
||||
{
|
||||
value: "solid",
|
||||
text: t("labels.solid"),
|
||||
icon: <FillSolidIcon appearance={appState.appearance} />,
|
||||
},
|
||||
]}
|
||||
group="fill"
|
||||
value={getFormValue(
|
||||
@ -178,12 +205,39 @@ export const actionChangeStrokeWidth = register({
|
||||
PanelComponent: ({ elements, appState, updateData }) => (
|
||||
<fieldset>
|
||||
<legend>{t("labels.strokeWidth")}</legend>
|
||||
<ButtonSelect
|
||||
<ButtonIconSelect
|
||||
group="stroke-width"
|
||||
options={[
|
||||
{ value: 1, text: t("labels.thin") },
|
||||
{ value: 2, text: t("labels.bold") },
|
||||
{ value: 4, text: t("labels.extraBold") },
|
||||
{
|
||||
value: 1,
|
||||
text: t("labels.thin"),
|
||||
icon: (
|
||||
<StrokeWidthIcon
|
||||
appearance={appState.appearance}
|
||||
strokeWidth={2}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
value: 2,
|
||||
text: t("labels.bold"),
|
||||
icon: (
|
||||
<StrokeWidthIcon
|
||||
appearance={appState.appearance}
|
||||
strokeWidth={6}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
value: 4,
|
||||
text: t("labels.extraBold"),
|
||||
icon: (
|
||||
<StrokeWidthIcon
|
||||
appearance={appState.appearance}
|
||||
strokeWidth={10}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]}
|
||||
value={getFormValue(
|
||||
elements,
|
||||
@ -214,12 +268,24 @@ export const actionChangeSloppiness = register({
|
||||
PanelComponent: ({ elements, appState, updateData }) => (
|
||||
<fieldset>
|
||||
<legend>{t("labels.sloppiness")}</legend>
|
||||
<ButtonSelect
|
||||
<ButtonIconSelect
|
||||
group="sloppiness"
|
||||
options={[
|
||||
{ value: 0, text: t("labels.architect") },
|
||||
{ value: 1, text: t("labels.artist") },
|
||||
{ value: 2, text: t("labels.cartoonist") },
|
||||
{
|
||||
value: 0,
|
||||
text: t("labels.architect"),
|
||||
icon: <SloppinessArchitectIcon appearance={appState.appearance} />,
|
||||
},
|
||||
{
|
||||
value: 1,
|
||||
text: t("labels.artist"),
|
||||
icon: <SloppinessArtistIcon appearance={appState.appearance} />,
|
||||
},
|
||||
{
|
||||
value: 2,
|
||||
text: t("labels.cartoonist"),
|
||||
icon: <SloppinessCartoonistIcon appearance={appState.appearance} />,
|
||||
},
|
||||
]}
|
||||
value={getFormValue(
|
||||
elements,
|
||||
@ -249,12 +315,24 @@ export const actionChangeStrokeStyle = register({
|
||||
PanelComponent: ({ elements, appState, updateData }) => (
|
||||
<fieldset>
|
||||
<legend>{t("labels.strokeStyle")}</legend>
|
||||
<ButtonSelect
|
||||
<ButtonIconSelect
|
||||
group="strokeStyle"
|
||||
options={[
|
||||
{ value: "solid", text: t("labels.strokeStyle_solid") },
|
||||
{ value: "dashed", text: t("labels.strokeStyle_dashed") },
|
||||
{ value: "dotted", text: t("labels.strokeStyle_dotted") },
|
||||
{
|
||||
value: "solid",
|
||||
text: t("labels.strokeStyle_solid"),
|
||||
icon: <StrokeStyleSolidIcon appearance={appState.appearance} />,
|
||||
},
|
||||
{
|
||||
value: "dashed",
|
||||
text: t("labels.strokeStyle_dashed"),
|
||||
icon: <StrokeStyleDashedIcon appearance={appState.appearance} />,
|
||||
},
|
||||
{
|
||||
value: "dotted",
|
||||
text: t("labels.strokeStyle_dotted"),
|
||||
icon: <StrokeStyleDottedIcon appearance={appState.appearance} />,
|
||||
},
|
||||
]}
|
||||
value={getFormValue(
|
||||
elements,
|
||||
@ -488,11 +566,19 @@ export const actionChangeSharpness = register({
|
||||
PanelComponent: ({ elements, appState, updateData }) => (
|
||||
<fieldset>
|
||||
<legend>{t("labels.edges")}</legend>
|
||||
<ButtonSelect
|
||||
<ButtonIconSelect
|
||||
group="edges"
|
||||
options={[
|
||||
{ value: "sharp", text: t("labels.sharp") },
|
||||
{ value: "round", text: t("labels.round") },
|
||||
{
|
||||
value: "sharp",
|
||||
text: t("labels.sharp"),
|
||||
icon: <EdgeSharpIcon appearance={appState.appearance} />,
|
||||
},
|
||||
{
|
||||
value: "round",
|
||||
text: t("labels.round"),
|
||||
icon: <EdgeRoundIcon appearance={appState.appearance} />,
|
||||
},
|
||||
]}
|
||||
value={getFormValue(
|
||||
elements,
|
||||
|
33
src/components/ButtonIconSelect.tsx
Normal file
33
src/components/ButtonIconSelect.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import React from "react";
|
||||
import clsx from "clsx";
|
||||
|
||||
// TODO: It might be "clever" to add option.icon to the existing component <ButtonSelect />
|
||||
export const ButtonIconSelect = <T extends Object>({
|
||||
options,
|
||||
value,
|
||||
onChange,
|
||||
group,
|
||||
}: {
|
||||
options: { value: T; text: string; icon: JSX.Element }[];
|
||||
value: T | null;
|
||||
onChange: (value: T) => void;
|
||||
group: string;
|
||||
}) => (
|
||||
<div className="buttonList buttonListIcon">
|
||||
{options.map((option) => (
|
||||
<label
|
||||
key={option.text}
|
||||
className={clsx({ active: value === option.value })}
|
||||
title={option.text}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name={group}
|
||||
onChange={() => onChange(option.value)}
|
||||
checked={value === option.value ? true : false}
|
||||
/>
|
||||
{option.icon}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
);
|
@ -504,3 +504,180 @@ export const UngroupIcon = React.memo(
|
||||
{ width: 182, height: 182 },
|
||||
),
|
||||
);
|
||||
|
||||
export const FillHachureIcon = React.memo(
|
||||
({ appearance }: { appearance: "light" | "dark" }) =>
|
||||
createIcon(
|
||||
<g stroke={iconFillColor(appearance)} fill="none">
|
||||
<path d="M0 0s0 0 0 0m0 0s0 0 0 0m.133 12.04L10.63-.033M.133 12.04L10.63-.034M2.234 21.818L21.26-.07M2.234 21.818L21.26-.07m-8.395 21.852L31.89-.103M12.865 21.783L31.89-.103m-8.395 21.852L41.208 1.37M23.495 21.75L41.208 1.37m-7.083 20.343l7.216-8.302m-7.216 8.302l7.216-8.302" />
|
||||
<path
|
||||
d="M0 0h40M0 0h40m0 0v20m0-20v20m0 0H0m40 0H0m0 0V0m0 20V0"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
</g>,
|
||||
{ width: 40, height: 20 },
|
||||
),
|
||||
);
|
||||
|
||||
export const FillCrossHatchIcon = React.memo(
|
||||
({ appearance }: { appearance: "light" | "dark" }) =>
|
||||
createIcon(
|
||||
<g stroke={iconFillColor(appearance)} fill="none">
|
||||
<path d="M0 0s0 0 0 0m0 0s0 0 0 0m.133 12.04L10.63-.033M.133 12.04L10.63-.034M2.234 21.818L21.26-.07M2.234 21.818L21.26-.07m-8.395 21.852L31.89-.103M12.865 21.783C17.87 16.025 22.875 10.266 31.89-.103m-8.395 21.852L41.208 1.37M23.495 21.75L41.208 1.37m-7.083 20.343l7.216-8.302m-7.216 8.302l7.216-8.302M-.09 19.92s0 0 0 0m0 0s0 0 0 0m12.04-.133L-.126 9.29m12.075 10.497L-.126 9.29m24.871 11.02C19.872 16.075 15 11.84.595-.684m24.15 20.994L.595-.684m36.19 20.861L12.636-.817m24.15 20.994L12.636-.817m30.909 16.269L24.676-.95m18.868 16.402L24.676-.95m18.833 5.771L37.472-.427m6.037 5.248L37.472-.427" />
|
||||
<path
|
||||
d="M0 0h40M0 0h40m0 0v20m0-20v20m0 0H0m40 0H0m0 0V0m0 20V0"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
</g>,
|
||||
{ width: 40, height: 20 },
|
||||
),
|
||||
);
|
||||
|
||||
export const FillSolidIcon = React.memo(
|
||||
({ appearance }: { appearance: "light" | "dark" }) =>
|
||||
createIcon(
|
||||
<>
|
||||
<path d="M0 0h120v60H0" strokeWidth={0} />
|
||||
<path
|
||||
d="M0 0h40M0 0h40m0 0v20m0-20v20m0 0H0m40 0H0m0 0V0m0 20V0"
|
||||
stroke={iconFillColor(appearance)}
|
||||
strokeWidth={2}
|
||||
fill="none"
|
||||
/>
|
||||
</>,
|
||||
{ width: 40, height: 20 },
|
||||
),
|
||||
);
|
||||
|
||||
export const StrokeWidthIcon = React.memo(
|
||||
({
|
||||
appearance,
|
||||
strokeWidth,
|
||||
}: {
|
||||
appearance: "light" | "dark";
|
||||
strokeWidth: number;
|
||||
}) =>
|
||||
createIcon(
|
||||
<path
|
||||
d="M0 10h40M0 10h40"
|
||||
stroke={iconFillColor(appearance)}
|
||||
strokeWidth={strokeWidth}
|
||||
fill="none"
|
||||
/>,
|
||||
{ width: 40, height: 20 },
|
||||
),
|
||||
);
|
||||
|
||||
export const StrokeStyleSolidIcon = React.memo(
|
||||
({ appearance }: { appearance: "light" | "dark" }) =>
|
||||
createIcon(
|
||||
<path
|
||||
d="M0 10h40M0 10h40"
|
||||
stroke={iconFillColor(appearance)}
|
||||
strokeWidth={2}
|
||||
fill="none"
|
||||
/>,
|
||||
{ width: 40, height: 20 },
|
||||
),
|
||||
);
|
||||
|
||||
export const StrokeStyleDashedIcon = React.memo(
|
||||
({ appearance }: { appearance: "light" | "dark" }) =>
|
||||
createIcon(
|
||||
<path
|
||||
d="M3.286 9.998h32.759"
|
||||
stroke={iconFillColor(appearance)}
|
||||
strokeWidth={2.5}
|
||||
fill="none"
|
||||
strokeDasharray="12 8"
|
||||
/>,
|
||||
{ width: 40, height: 20 },
|
||||
),
|
||||
);
|
||||
|
||||
export const StrokeStyleDottedIcon = React.memo(
|
||||
({ appearance }: { appearance: "light" | "dark" }) =>
|
||||
createIcon(
|
||||
<path
|
||||
d="M0 10h40M0 10h40"
|
||||
stroke={iconFillColor(appearance)}
|
||||
strokeWidth={2}
|
||||
fill="none"
|
||||
strokeDasharray="3 6"
|
||||
/>,
|
||||
{ width: 40, height: 20 },
|
||||
),
|
||||
);
|
||||
|
||||
export const SloppinessArchitectIcon = React.memo(
|
||||
({ appearance }: { appearance: "light" | "dark" }) =>
|
||||
createIcon(
|
||||
<path
|
||||
d="M.268 17.938C4.05 15.093 19.414.725 22.96.868c3.547.143-4.149 16.266-1.41 17.928 2.738 1.662 14.866-6.632 17.84-7.958m-39.123 7.1C4.05 15.093 19.414.725 22.96.868c3.547.143-4.149 16.266-1.41 17.928 2.738 1.662 14.866-6.632 17.84-7.958"
|
||||
stroke={iconFillColor(appearance)}
|
||||
strokeWidth={2}
|
||||
fill="none"
|
||||
/>,
|
||||
{ width: 40, height: 20 },
|
||||
),
|
||||
);
|
||||
|
||||
export const SloppinessArtistIcon = React.memo(
|
||||
({ appearance }: { appearance: "light" | "dark" }) =>
|
||||
createIcon(
|
||||
<path
|
||||
d="M2.663 18.134c3.963-2.578 18.855-12.098 22.675-12.68 3.82-.58-1.966 8.367.242 9.196 2.209.828 10.649-3.14 13.01-4.224M7.037 15.474c4.013-2.198 14.19-14.648 17.18-14.32 2.99.329-1.749 14.286.759 16.292 2.507 2.006 12.284-2.68 14.286-4.256"
|
||||
stroke={iconFillColor(appearance)}
|
||||
strokeWidth={2}
|
||||
fill="none"
|
||||
/>,
|
||||
{ width: 40, height: 20 },
|
||||
),
|
||||
);
|
||||
|
||||
export const SloppinessCartoonistIcon = React.memo(
|
||||
({ appearance }: { appearance: "light" | "dark" }) =>
|
||||
createIcon(
|
||||
<>
|
||||
<path
|
||||
d="M1.944 17.15C6.056 14.637 22.368 1.86 26.615 2.083c4.248.223-.992 14.695.815 16.406 1.807 1.71 8.355-5.117 10.026-6.14m-35.512 4.8C6.056 14.637 22.368 1.86 26.615 2.083c4.248.223-.992 14.695.815 16.406 1.807 1.71 8.355-5.117 10.026-6.14"
|
||||
stroke={iconFillColor(appearance)}
|
||||
strokeWidth={2}
|
||||
fill="none"
|
||||
/>
|
||||
<path
|
||||
d="M3.114 10.534c2.737-1.395 12.854-8.814 16.42-8.368 3.568.445 2.35 10.282 4.984 11.04 2.635.756 9.019-5.416 10.822-6.5M3.114 10.535c2.737-1.395 12.854-8.814 16.42-8.368 3.568.445 2.35 10.282 4.984 11.04 2.635.756 9.019-5.416 10.822-6.5"
|
||||
stroke={iconFillColor(appearance)}
|
||||
strokeWidth={2}
|
||||
fill="none"
|
||||
/>
|
||||
</>,
|
||||
{ width: 40, height: 20 },
|
||||
),
|
||||
);
|
||||
|
||||
export const EdgeSharpIcon = React.memo(
|
||||
({ appearance }: { appearance: "light" | "dark" }) =>
|
||||
createIcon(
|
||||
<path
|
||||
d="M9.18 19.68V6.346m0 13.336V6.345m0 0h29.599m-29.6 0h29.6"
|
||||
stroke={iconFillColor(appearance)}
|
||||
strokeWidth={2}
|
||||
fill="none"
|
||||
/>,
|
||||
{ width: 40, height: 20 },
|
||||
),
|
||||
);
|
||||
|
||||
export const EdgeRoundIcon = React.memo(
|
||||
({ appearance }: { appearance: "light" | "dark" }) =>
|
||||
createIcon(
|
||||
<path
|
||||
d="M9.444 19.537c.484-2.119-2.1-10.449 2.904-12.71 5.004-2.263 22.601-.72 27.121-.863M9.444 19.537c.484-2.119-2.1-10.449 2.904-12.71 5.004-2.263 22.601-.72 27.121-.863"
|
||||
stroke={iconFillColor(appearance)}
|
||||
strokeWidth={2}
|
||||
fill="none"
|
||||
/>,
|
||||
{ width: 40, height: 20 },
|
||||
),
|
||||
);
|
||||
|
@ -172,6 +172,22 @@
|
||||
}
|
||||
}
|
||||
|
||||
.buttonList.buttonListIcon {
|
||||
label {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
svg {
|
||||
width: 36px;
|
||||
height: 18px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
&.active svg {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.App-bottom-bar {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
@ -513,18 +513,18 @@ describe("regression tests", () => {
|
||||
// select rectangle tool to show properties menu
|
||||
UI.clickTool("rectangle");
|
||||
// english lang should display `hachure` label
|
||||
expect(screen.queryByText(/hachure/i)).not.toBeNull();
|
||||
expect(screen.queryByTitle(/hachure/i)).not.toBeNull();
|
||||
fireEvent.change(document.querySelector(".dropdown-select__language")!, {
|
||||
target: { value: "de-DE" },
|
||||
});
|
||||
// switching to german, `hachure` label should no longer exist
|
||||
await waitFor(() => expect(screen.queryByText(/hachure/i)).toBeNull());
|
||||
await waitFor(() => expect(screen.queryByTitle(/hachure/i)).toBeNull());
|
||||
// reset language
|
||||
fireEvent.change(document.querySelector(".dropdown-select__language")!, {
|
||||
target: { value: "en" },
|
||||
});
|
||||
// switching back to English
|
||||
await waitFor(() => expect(screen.queryByText(/hachure/i)).not.toBeNull());
|
||||
await waitFor(() => expect(screen.queryByTitle(/hachure/i)).not.toBeNull());
|
||||
});
|
||||
|
||||
it("make a group and duplicate it", () => {
|
||||
@ -877,13 +877,13 @@ describe("regression tests", () => {
|
||||
clickLabeledElement("Background");
|
||||
clickLabeledElement("#e64980");
|
||||
// Fill style
|
||||
fireEvent.click(screen.getByLabelText("Cross-hatch"));
|
||||
fireEvent.click(screen.getByTitle("Cross-hatch"));
|
||||
// Stroke width
|
||||
fireEvent.click(screen.getByLabelText("Bold"));
|
||||
fireEvent.click(screen.getByTitle("Bold"));
|
||||
// Stroke style
|
||||
fireEvent.click(screen.getByLabelText("Dotted"));
|
||||
fireEvent.click(screen.getByTitle("Dotted"));
|
||||
// Roughness
|
||||
fireEvent.click(screen.getByLabelText("Cartoonist"));
|
||||
fireEvent.click(screen.getByTitle("Cartoonist"));
|
||||
// Opacity
|
||||
fireEvent.change(screen.getByLabelText("Opacity"), {
|
||||
target: { value: "60" },
|
||||
|
Loading…
x
Reference in New Issue
Block a user