Refactor control components file structure

This commit is contained in:
Mitchell McCaffrey
2021-07-20 20:41:26 +10:00
parent 24dddad66f
commit b703a08d2c
13 changed files with 47 additions and 47 deletions

View File

@@ -0,0 +1,171 @@
import { useEffect } from "react";
import { Flex, IconButton } from "theme-ui";
import { useMedia } from "react-media";
import RadioIconButton from "../RadioIconButton";
import ColorControl from "./shared/ColorControl";
import AlphaBlendToggle from "./shared/AlphaBlendToggle";
import ToolSection from "./shared/ToolSection";
import BrushIcon from "../../icons/BrushToolIcon";
import BrushPaintIcon from "../../icons/BrushPaintIcon";
import BrushLineIcon from "../../icons/BrushLineIcon";
import BrushRectangleIcon from "../../icons/BrushRectangleIcon";
import BrushCircleIcon from "../../icons/BrushCircleIcon";
import BrushTriangleIcon from "../../icons/BrushTriangleIcon";
import EraseAllIcon from "../../icons/EraseAllIcon";
import EraseIcon from "../../icons/EraseToolIcon";
import UndoButton from "./shared/UndoButton";
import RedoButton from "./shared/RedoButton";
import Divider from "../Divider";
import { useKeyboard } from "../../contexts/KeyboardContext";
import shortcuts from "../../shortcuts";
import {
DrawingToolSettings as DrawingToolSettingsType,
DrawingToolType,
} from "../../types/Drawing";
type DrawingToolSettingsProps = {
settings: DrawingToolSettingsType;
onSettingChange: (change: Partial<DrawingToolSettingsType>) => void;
onToolAction: (action: string) => void;
disabledActions: string[];
};
function DrawingToolSettings({
settings,
onSettingChange,
onToolAction,
disabledActions,
}: DrawingToolSettingsProps) {
// Keyboard shotcuts
function handleKeyDown(event: KeyboardEvent) {
if (shortcuts.drawBrush(event)) {
onSettingChange({ type: "brush" });
} else if (shortcuts.drawPaint(event)) {
onSettingChange({ type: "paint" });
} else if (shortcuts.drawLine(event)) {
onSettingChange({ type: "line" });
} else if (shortcuts.drawRect(event)) {
onSettingChange({ type: "rectangle" });
} else if (shortcuts.drawCircle(event)) {
onSettingChange({ type: "circle" });
} else if (shortcuts.drawTriangle(event)) {
onSettingChange({ type: "triangle" });
} else if (shortcuts.drawErase(event)) {
onSettingChange({ type: "erase" });
} else if (shortcuts.drawBlend(event)) {
onSettingChange({ useBlending: !settings.useBlending });
} else if (shortcuts.redo(event) && !disabledActions.includes("redo")) {
onToolAction("mapRedo");
} else if (shortcuts.undo(event) && !disabledActions.includes("undo")) {
onToolAction("mapUndo");
}
}
useKeyboard(handleKeyDown);
// Change to brush if on erase and it gets disabled
useEffect(() => {
if (settings.type === "erase" && disabledActions.includes("erase")) {
onSettingChange({ type: "brush" });
}
}, [disabledActions, settings, onSettingChange]);
const isSmallScreen = useMedia({ query: "(max-width: 799px)" });
const tools = [
{
id: "brush",
title: "Brush (B)",
isSelected: settings.type === "brush",
icon: <BrushIcon />,
},
{
id: "paint",
title: "Paint (P)",
isSelected: settings.type === "paint",
icon: <BrushPaintIcon />,
},
{
id: "line",
title: "Line (L)",
isSelected: settings.type === "line",
icon: <BrushLineIcon />,
},
{
id: "rectangle",
title: "Rectangle (R)",
isSelected: settings.type === "rectangle",
icon: <BrushRectangleIcon />,
},
{
id: "circle",
title: "Circle (C)",
isSelected: settings.type === "circle",
icon: <BrushCircleIcon />,
},
{
id: "triangle",
title: "Triangle (T)",
isSelected: settings.type === "triangle",
icon: <BrushTriangleIcon />,
},
];
return (
<Flex sx={{ alignItems: "center" }}>
<ColorControl
color={settings.color}
onColorChange={(color) => onSettingChange({ color })}
exclude={["primary"]}
/>
<Divider vertical />
<ToolSection
tools={tools}
onToolClick={(tool) =>
onSettingChange({ type: tool.id as DrawingToolType })
}
collapse={isSmallScreen}
/>
<Divider vertical />
<RadioIconButton
title="Erase (E)"
onClick={() => onSettingChange({ type: "erase" })}
isSelected={settings.type === "erase"}
disabled={disabledActions.includes("erase")}
>
<EraseIcon />
</RadioIconButton>
<IconButton
aria-label="Erase All"
title="Erase All"
onClick={() => onToolAction("eraseAll")}
disabled={disabledActions.includes("erase")}
>
<EraseAllIcon />
</IconButton>
<Divider vertical />
<AlphaBlendToggle
useBlending={settings.useBlending}
onBlendingChange={(useBlending) => onSettingChange({ useBlending })}
/>
<Divider vertical />
<UndoButton
onClick={() => onToolAction("mapUndo")}
disabled={disabledActions.includes("undo")}
/>
<RedoButton
onClick={() => onToolAction("mapRedo")}
disabled={disabledActions.includes("redo")}
/>
</Flex>
);
}
export default DrawingToolSettings;

View File

@@ -0,0 +1,150 @@
import { Flex } from "theme-ui";
import { useMedia } from "react-media";
import RadioIconButton from "../RadioIconButton";
import MultilayerToggle from "./shared/MultilayerToggle";
import FogPreviewToggle from "./shared/FogPreviewToggle";
import FogCutToggle from "./shared/FogCutToggle";
import FogBrushIcon from "../../icons/FogBrushIcon";
import FogPolygonIcon from "../../icons/FogPolygonIcon";
import FogRemoveIcon from "../../icons/FogRemoveIcon";
import FogToggleIcon from "../../icons/FogToggleIcon";
import FogRectangleIcon from "../../icons/FogRectangleIcon";
import UndoButton from "./shared/UndoButton";
import RedoButton from "./shared/RedoButton";
import ToolSection from "./shared/ToolSection";
import Divider from "../Divider";
import { useKeyboard } from "../../contexts/KeyboardContext";
import shortcuts from "../../shortcuts";
import {
FogToolSettings as FogToolSettingsType,
FogToolType,
} from "../../types/Fog";
type FogToolSettingsProps = {
settings: FogToolSettingsType;
onSettingChange: (change: Partial<FogToolSettingsType>) => void;
onToolAction: (action: string) => void;
disabledActions: string[];
};
function FogToolSettings({
settings,
onSettingChange,
onToolAction,
disabledActions,
}: FogToolSettingsProps) {
// Keyboard shortcuts
function handleKeyDown(event: KeyboardEvent) {
if (shortcuts.fogPolygon(event)) {
onSettingChange({ type: "polygon" });
} else if (shortcuts.fogBrush(event)) {
onSettingChange({ type: "brush" });
} else if (shortcuts.fogToggle(event)) {
onSettingChange({ type: "toggle" });
} else if (shortcuts.fogErase(event)) {
onSettingChange({ type: "remove" });
} else if (shortcuts.fogLayer(event)) {
onSettingChange({ multilayer: !settings.multilayer });
} else if (shortcuts.fogPreview(event)) {
onSettingChange({ preview: !settings.preview });
} else if (shortcuts.fogCut(event)) {
onSettingChange({ useFogCut: !settings.useFogCut });
} else if (shortcuts.fogRectangle(event)) {
onSettingChange({ type: "rectangle" });
} else if (shortcuts.redo(event) && !disabledActions.includes("redo")) {
onToolAction("fogRedo");
} else if (shortcuts.undo(event) && !disabledActions.includes("undo")) {
onToolAction("fogUndo");
}
}
useKeyboard(handleKeyDown);
const isSmallScreen = useMedia({ query: "(max-width: 799px)" });
const drawTools = [
{
id: "polygon",
title: "Fog Polygon (P)",
isSelected: settings.type === "polygon",
icon: <FogPolygonIcon />,
disabled: settings.preview,
},
{
id: "rectangle",
title: "Fog Rectangle (R)",
isSelected: settings.type === "rectangle",
icon: <FogRectangleIcon />,
disabled: settings.preview,
},
{
id: "brush",
title: "Fog Brush (B)",
isSelected: settings.type === "brush",
icon: <FogBrushIcon />,
disabled: settings.preview,
},
];
return (
<Flex sx={{ alignItems: "center" }}>
<ToolSection
tools={drawTools}
onToolClick={(tool) =>
onSettingChange({ type: tool.id as FogToolType })
}
collapse={isSmallScreen}
/>
<Divider vertical />
<RadioIconButton
title="Toggle Fog (T)"
onClick={() => onSettingChange({ type: "toggle" })}
isSelected={settings.type === "toggle"}
disabled={settings.preview}
>
<FogToggleIcon />
</RadioIconButton>
<RadioIconButton
title="Erase Fog (E)"
onClick={() => onSettingChange({ type: "remove" })}
isSelected={settings.type === "remove"}
disabled={settings.preview}
>
<FogRemoveIcon />
</RadioIconButton>
<Divider vertical />
<FogCutToggle
useFogCut={settings.useFogCut}
onFogCutChange={(useFogCut) => onSettingChange({ useFogCut })}
disabled={settings.preview}
/>
<MultilayerToggle
multilayer={settings.multilayer}
onMultilayerChange={(multilayer) => onSettingChange({ multilayer })}
disabled={settings.preview}
/>
<FogPreviewToggle
useFogPreview={settings.preview}
onFogPreviewChange={(preview) => onSettingChange({ preview })}
/>
<Divider vertical />
<UndoButton
onClick={() => onToolAction("fogUndo")}
disabled={disabledActions.includes("undo")}
/>
<RedoButton
onClick={() => onToolAction("fogRedo")}
disabled={disabledActions.includes("redo")}
/>
</Flex>
);
}
export default FogToolSettings;

View File

@@ -0,0 +1,27 @@
import { Flex } from "theme-ui";
import ColorControl from "./shared/ColorControl";
import { PointerToolSettings as PointerToolSettingsType } from "../../types/Pointer";
type PointerToolSettingsProps = {
settings: PointerToolSettingsType;
onSettingChange: (change: Partial<PointerToolSettingsType>) => void;
};
function PointerToolSettings({
settings,
onSettingChange,
}: PointerToolSettingsProps) {
return (
<Flex sx={{ alignItems: "center" }}>
<ColorControl
color={settings.color}
onColorChange={(color) => onSettingChange({ color })}
exclude={["black", "darkGray", "lightGray", "white", "primary"]}
/>
</Flex>
);
}
export default PointerToolSettings;

View File

@@ -0,0 +1,63 @@
import { Flex } from "theme-ui";
import {
SelectToolSettings as SelectToolSettingsType,
SelectToolType,
} from "../../types/Select";
import { useKeyboard } from "../../contexts/KeyboardContext";
import ToolSection from "./shared/ToolSection";
import shortcuts from "../../shortcuts";
import RectIcon from "../../icons/SelectRectangleIcon";
import PathIcon from "../../icons/SelectPathIcon";
type SelectToolSettingsProps = {
settings: SelectToolSettingsType;
onSettingChange: (change: Partial<SelectToolSettingsType>) => void;
};
function SelectToolSettings({
settings,
onSettingChange,
}: SelectToolSettingsProps) {
// Keyboard shotcuts
function handleKeyDown(event: KeyboardEvent) {
if (shortcuts.selectPath(event)) {
onSettingChange({ type: "path" });
} else if (shortcuts.selectRect(event)) {
onSettingChange({ type: "rectangle" });
}
}
useKeyboard(handleKeyDown);
const tools = [
{
id: "path",
title: "Lasso Selection (L)",
isSelected: settings.type === "path",
icon: <PathIcon />,
},
{
id: "rectangle",
title: "Rectangle Selection (R)",
isSelected: settings.type === "rectangle",
icon: <RectIcon />,
},
];
return (
<Flex sx={{ alignItems: "center" }}>
<ToolSection
tools={tools}
onToolClick={(tool) =>
onSettingChange({ type: tool.id as SelectToolType })
}
/>
</Flex>
);
}
export default SelectToolSettings;

View File

@@ -0,0 +1,26 @@
import { IconButton } from "theme-ui";
import BlendOnIcon from "../../../icons/BlendOnIcon";
import BlendOffIcon from "../../../icons/BlendOffIcon";
type AlphaBlendToggleProps = {
useBlending: boolean;
onBlendingChange: (useBlending: boolean) => void;
};
function AlphaBlendToggle({
useBlending,
onBlendingChange,
}: AlphaBlendToggleProps) {
return (
<IconButton
aria-label={useBlending ? "Disable Blending (O)" : "Enable Blending (O)"}
title={useBlending ? "Disable Blending (O)" : "Enable Blending (O)"}
onClick={() => onBlendingChange(!useBlending)}
>
{useBlending ? <BlendOnIcon /> : <BlendOffIcon />}
</IconButton>
);
}
export default AlphaBlendToggle;

View File

@@ -0,0 +1,125 @@
import React, { useState } from "react";
import { Box, SxProp } from "theme-ui";
import colors, { colorOptions, Color } from "../../../helpers/colors";
import MapMenu from "../../map/MapMenu";
type ColorCircleProps = {
color: Color;
selected: boolean;
onClick?: React.MouseEventHandler<HTMLDivElement>;
} & SxProp;
function ColorCircle({ color, selected, onClick, sx }: ColorCircleProps) {
return (
<Box
key={color}
sx={{
borderRadius: "50%",
transform: "scale(0.75)",
backgroundColor: colors[color],
cursor: "pointer",
...sx,
}}
onClick={onClick}
aria-label={`Brush Color ${color}`}
>
{selected && (
<Box
sx={{
width: "100%",
height: "100%",
border: "2px solid white",
position: "absolute",
top: 0,
borderRadius: "50%",
}}
/>
)}
</Box>
);
}
type ColorControlProps = {
color: Color;
onColorChange: (newColor: Color) => void;
exclude: Color[];
};
function ColorControl({ color, onColorChange, exclude }: ColorControlProps) {
const [showColorMenu, setShowColorMenu] = useState(false);
const [colorMenuOptions, setColorMenuOptions] = useState({});
function handleControlClick(event: React.MouseEvent<HTMLDivElement>) {
if (showColorMenu) {
setShowColorMenu(false);
setColorMenuOptions({});
} else {
setShowColorMenu(true);
const rect = event.currentTarget.getBoundingClientRect();
setColorMenuOptions({
// Align the right of the submenu to the left of the tool and center vertically
left: `${rect.left + rect.width / 2}px`,
top: `${rect.bottom + 16}px`,
style: { transform: "translateX(-50%)" },
// Exclude this node from the sub menus auto close
excludeNode: event.currentTarget,
});
}
}
const colorMenu = (
<MapMenu
isOpen={showColorMenu}
onRequestClose={() => {
setShowColorMenu(false);
setColorMenuOptions({});
}}
{...colorMenuOptions}
>
<Box
sx={{
width: "104px",
display: "flex",
flexWrap: "wrap",
justifyContent: "space-between",
}}
p={1}
>
{colorOptions
.filter((color) => !exclude.includes(color))
.map((c) => (
<ColorCircle
key={c}
color={c}
selected={c === color}
onClick={() => {
onColorChange(c);
setShowColorMenu(false);
setColorMenuOptions({});
}}
sx={{ width: "25%", paddingTop: "25%" }}
/>
))}
</Box>
</MapMenu>
);
return (
<>
<ColorCircle
color={color}
selected
onClick={handleControlClick}
sx={{ width: "24px", height: "24px" }}
/>
{colorMenu}
</>
);
}
ColorControl.defaultProps = {
exclude: [],
};
export default ColorControl;

View File

@@ -0,0 +1,31 @@
import { IconButton } from "theme-ui";
import CutOnIcon from "../../../icons/FogCutOnIcon";
import CutOffIcon from "../../../icons/FogCutOffIcon";
type FogCutToggleProps = {
useFogCut: boolean;
onFogCutChange: (useFogCut: boolean) => void;
disabled?: boolean;
};
function FogCutToggle({
useFogCut,
onFogCutChange,
disabled,
}: FogCutToggleProps) {
return (
<IconButton
aria-label={
useFogCut ? "Disable Fog Cutting (C)" : "Enable Fog Cutting (C)"
}
title={useFogCut ? "Disable Fog Cutting (C)" : "Enable Fog Cutting (C)"}
onClick={() => onFogCutChange(!useFogCut)}
disabled={disabled}
>
{useFogCut ? <CutOnIcon /> : <CutOffIcon />}
</IconButton>
);
}
export default FogCutToggle;

View File

@@ -0,0 +1,30 @@
import { IconButton } from "theme-ui";
import PreviewOnIcon from "../../../icons/FogPreviewOnIcon";
import PreviewOffIcon from "../../../icons/FogPreviewOffIcon";
type FogPreviewToggleProps = {
useFogPreview: boolean;
onFogPreviewChange: (useFogCut: boolean) => void;
};
function FogPreviewToggle({
useFogPreview,
onFogPreviewChange,
}: FogPreviewToggleProps) {
return (
<IconButton
aria-label={
useFogPreview ? "Disable Fog Preview (F)" : "Enable Fog Preview (F)"
}
title={
useFogPreview ? "Disable Fog Preview (F)" : "Enable Fog Preview (F)"
}
onClick={() => onFogPreviewChange(!useFogPreview)}
>
{useFogPreview ? <PreviewOnIcon /> : <PreviewOffIcon />}
</IconButton>
);
}
export default FogPreviewToggle;

View File

@@ -0,0 +1,31 @@
import { IconButton } from "theme-ui";
import MultilayerOnIcon from "../../../icons/FogMultilayerOnIcon";
import MultilayerOffIcon from "../../../icons/FogMultilayerOffIcon";
type MultilayerToggleProps = {
multilayer: boolean;
onMultilayerChange: (multilayer: boolean) => void;
disabled?: boolean;
};
function MultilayerToggle({
multilayer,
onMultilayerChange,
disabled,
}: MultilayerToggleProps) {
return (
<IconButton
aria-label={
multilayer ? "Disable Multilayer (L)" : "Enable Multilayer (L)"
}
title={multilayer ? "Disable Multilayer (L)" : "Enable Multilayer (L)"}
onClick={() => onMultilayerChange(!multilayer)}
disabled={disabled}
>
{multilayer ? <MultilayerOnIcon /> : <MultilayerOffIcon />}
</IconButton>
);
}
export default MultilayerToggle;

View File

@@ -0,0 +1,26 @@
import React from "react";
import { IconButton } from "theme-ui";
import RedoIcon from "../../../icons/RedoIcon";
import { isMacLike } from "../../../helpers/shared";
type RedoButtonProps = {
onClick?: React.MouseEventHandler<HTMLButtonElement>;
disabled?: boolean;
};
function RedoButton({ onClick, disabled }: RedoButtonProps) {
return (
<IconButton
title={`Redo (${isMacLike ? "Cmd" : "Ctrl"} + Shift + Z)`}
aria-label={`Redo (${isMacLike ? "Cmd" : "Ctrl"} + Shift + Z)`}
onClick={onClick}
disabled={disabled}
>
<RedoIcon />
</IconButton>
);
}
export default RedoButton;

View File

@@ -0,0 +1,124 @@
import React, { useState, useEffect } from "react";
import { Box, Flex } from "theme-ui";
import RadioIconButton from "../../RadioIconButton";
export type Tool = {
id: string;
title: string;
isSelected: boolean;
icon: React.ReactNode;
disabled?: boolean;
};
type ToolSectionProps = {
collapse: boolean;
tools: Tool[];
onToolClick: (tool: Tool) => void;
};
// Section of map tools with the option to collapse into a vertical list
function ToolSection({ collapse, tools, onToolClick }: ToolSectionProps) {
const [showMore, setShowMore] = useState(false);
const [collapsedTool, setCollapsedTool] = useState<Tool>();
useEffect(() => {
const selectedTool = tools.find((tool) => tool.isSelected);
if (selectedTool) {
setCollapsedTool(selectedTool);
} else {
// No selected tool, deselect if we have a tool or get the first tool if not
setCollapsedTool((prevTool) =>
prevTool ? { ...prevTool, isSelected: false } : tools[0]
);
}
}, [tools]);
function handleToolClick(tool: Tool) {
if (collapse && tool.isSelected) {
setShowMore(!showMore);
} else if (collapse && !tool.isSelected) {
setShowMore(false);
}
onToolClick(tool);
}
function renderTool(tool: Tool) {
return (
<RadioIconButton
title={tool.title}
onClick={() => handleToolClick(tool)}
key={tool.id}
isSelected={tool.isSelected}
disabled={tool.disabled}
>
{tool.icon}
</RadioIconButton>
);
}
if (collapse) {
if (!collapsedTool) {
return null;
}
return (
<Box sx={{ position: "relative" }}>
{renderTool(collapsedTool)}
{/* Render chevron when more tools is available */}
<Box
sx={{
position: "absolute",
width: 0,
height: 0,
borderTop: "4px solid",
borderTopColor: "text",
borderLeft: "4px solid transparent",
borderRight: "4px solid transparent",
transform: "translate(0, -4px) rotate(-45deg)",
bottom: 0,
right: 0,
pointerEvents: "none",
}}
/>
{showMore && (
<Flex
sx={{
position: "absolute",
top: "40px",
left: "50%",
transform: "translateX(-50%)",
flexDirection: "column",
borderRadius: "4px",
}}
bg="overlay"
p={2}
>
{tools.filter((tool) => !tool.isSelected).map(renderTool)}
</Flex>
)}
</Box>
);
} else {
return (
<>
{tools.map((tool) => (
<RadioIconButton
title={tool.title}
onClick={() => handleToolClick(tool)}
key={tool.id}
isSelected={tool.isSelected}
disabled={tool.disabled}
>
{tool.icon}
</RadioIconButton>
))}
</>
);
}
}
ToolSection.defaultProps = {
collapse: false,
};
export default ToolSection;

View File

@@ -0,0 +1,26 @@
import React from "react";
import { IconButton } from "theme-ui";
import UndoIcon from "../../../icons/UndoIcon";
import { isMacLike } from "../../../helpers/shared";
type UndoButtonProps = {
onClick?: React.MouseEventHandler<HTMLButtonElement>;
disabled?: boolean;
};
function UndoButton({ onClick, disabled }: UndoButtonProps) {
return (
<IconButton
title={`Undo (${isMacLike ? "Cmd" : "Ctrl"} + Z)`}
aria-label={`Undo (${isMacLike ? "Cmd" : "Ctrl"} + Z)`}
onClick={onClick}
disabled={disabled}
>
<UndoIcon />
</IconButton>
);
}
export default UndoButton;