Skip to content

Commit

Permalink
Redesign text toolbar (#567)
Browse files Browse the repository at this point in the history
Ref #555
#107

Implemented icon button according new design,
added hover and disabled states for non-default variant.

Migrated text toolbar from toggle group to icon button.
Toggle between superscript and subscript.

<img width="285" alt="image"
src="https://user-images.githubusercontent.com/5635476/206522786-28cc6b7d-6336-4c13-8eb7-b83428598dcb.png">

<img width="315" alt="image"
src="https://user-images.githubusercontent.com/5635476/206523165-1b4a080d-0c8d-41a1-b5a9-47c197b78998.png">

<img width="197" alt="image"
src="https://user-images.githubusercontent.com/5635476/206521819-14c1fbb3-6c37-4264-8f7b-cef256b4ab23.png">
  • Loading branch information
TrySound committed Dec 9, 2022
1 parent 6cc2847 commit 7637d48
Show file tree
Hide file tree
Showing 9 changed files with 241 additions and 94 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,16 @@ export const ToolbarConnectorPlugin = () => {

// dispatch commands sent from toolbar
useSubscribe("formatTextToolbar", (type) => {
let isSuperscript = false;
let isSubscript = false;
editor.getEditorState().read(() => {
const selection = $getSelection();
if ($isRangeSelection(selection)) {
isSuperscript = selection.hasFormat("superscript");
isSubscript = selection.hasFormat("subscript");
}
});

if (type === "bold") {
editor.dispatchCommand(FORMAT_TEXT_COMMAND, "bold");
}
Expand All @@ -175,9 +185,17 @@ export const ToolbarConnectorPlugin = () => {
}
if (type === "superscript") {
editor.dispatchCommand(FORMAT_TEXT_COMMAND, "superscript");
// remove subscript if superscript is added
if (isSubscript) {
editor.dispatchCommand(FORMAT_TEXT_COMMAND, "subscript");
}
}
if (type === "subscript") {
editor.dispatchCommand(FORMAT_TEXT_COMMAND, "subscript");
// remove superscript if subscript is added
if (isSuperscript) {
editor.dispatchCommand(FORMAT_TEXT_COMMAND, "superscript");
}
}
if (type === "link") {
const editorState = editor.getEditorState();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import { useRef, useEffect, type MouseEventHandler } from "react";
import { useRef, useEffect } from "react";
import { computePosition, flip, offset, shift } from "@floating-ui/dom";
import { type Publish } from "~/shared/pubsub";
import {
useSelectedInstanceData,
type TextToolbarState,
useTextToolbarState,
} from "~/designer/shared/nano-states";
import { ToggleGroupRoot, ToggleGroupItem } from "@webstudio-is/design-system";
import { Flex, IconButton, Tooltip } from "@webstudio-is/design-system";
import {
FontBoldIcon,
FontItalicIcon,
SuperscriptIcon,
SubscriptIcon,
Link2Icon,
BrushIcon,
FormatClearIcon,
CrossSmallIcon,
} from "@webstudio-is/icons";
import { useSubscribe } from "~/shared/pubsub";

Expand All @@ -41,11 +41,6 @@ export const useSubscribeTextToolbar = () => {
useSubscribe("hideTextToolbar", () => setTextToolbar(undefined));
};

const onClickPreventDefault: MouseEventHandler<HTMLDivElement> = (event) => {
event.preventDefault();
event.stopPropagation();
};

const getRectForRelativeRect = (parent: DOMRect, rel: DOMRect) => {
return {
x: parent.x + rel.x,
Expand Down Expand Up @@ -87,86 +82,104 @@ const Toolbar = ({ state, onToggle }: ToolbarProps) => {
}
}, [state.selectionRect]);

const value: Format[] = [];
if (state.isBold) {
value.push("bold");
}
if (state.isItalic) {
value.push("italic");
}
if (state.isSuperscript) {
value.push("superscript");
}
if (state.isSubscript) {
value.push("subscript");
}
if (state.isLink) {
value.push("link");
}
if (state.isSpan) {
value.push("span");
}
const isCleared =
state.isBold === false &&
state.isItalic === false &&
state.isSuperscript === false &&
state.isSubscript === false &&
state.isLink === false &&
state.isSpan === false;

return (
<ToggleGroupRoot
<Flex
ref={rootRef}
type="multiple"
value={value}
onValueChange={(newValues: Format[]) => {
// @todo refactor with per button callback
if (state.isBold !== newValues.includes("bold")) {
onToggle("bold");
}
if (state.isItalic !== newValues.includes("italic")) {
onToggle("italic");
}
if (state.isSuperscript !== newValues.includes("superscript")) {
onToggle("superscript");
}
if (state.isSubscript !== newValues.includes("subscript")) {
onToggle("subscript");
}
if (state.isLink !== newValues.includes("link")) {
onToggle("link");
}
if (state.isSpan !== newValues.includes("span")) {
onToggle("span");
}
if (newValues.includes("clear")) {
onToggle("clear");
}
}}
onClick={onClickPreventDefault}
gap={2}
css={{
position: "absolute",
top: 0,
left: 0,
pointerEvents: "auto",
background: "$loContrast",
padding: "$spacing$3",
borderRadius: "$borderRadius$6",
border: "1px solid $slate8",
filter:
"drop-shadow(0px 2px 7px rgba(0, 0, 0, 0.1)) drop-shadow(0px 5px 17px rgba(0, 0, 0, 0.15))",
}}
onClick={(event) => {
event.stopPropagation();
}}
>
<ToggleGroupItem value="bold">
<FontBoldIcon />
</ToggleGroupItem>
<ToggleGroupItem value="italic">
<FontItalicIcon />
</ToggleGroupItem>
<ToggleGroupItem value="superscript">
<SuperscriptIcon />
</ToggleGroupItem>
<ToggleGroupItem value="subscript">
<SubscriptIcon />
</ToggleGroupItem>
<ToggleGroupItem value="link">
<Link2Icon />
</ToggleGroupItem>
<ToggleGroupItem value="span">
<BrushIcon />
</ToggleGroupItem>
<ToggleGroupItem value="clear">
<FormatClearIcon />
</ToggleGroupItem>
</ToggleGroupRoot>
<Tooltip content="Clear styles">
<IconButton
aria-label="Clear styles"
disabled={isCleared}
onClick={() => onToggle("clear")}
>
<CrossSmallIcon />
</IconButton>
</Tooltip>

<Tooltip content="Bold">
<IconButton
aria-label="Bold"
variant={state.isBold ? "set" : "default"}
onClick={() => onToggle("bold")}
>
<FontBoldIcon />
</IconButton>
</Tooltip>

<Tooltip content="Italic">
<IconButton
aria-label="Italic"
variant={state.isItalic ? "set" : "default"}
onClick={() => onToggle("italic")}
>
<FontItalicIcon />
</IconButton>
</Tooltip>

<Tooltip content="Superscript">
<IconButton
aria-label="Superscript"
variant={state.isSuperscript ? "set" : "default"}
onClick={() => onToggle("superscript")}
>
<SuperscriptIcon />
</IconButton>
</Tooltip>

<Tooltip content="Subscript">
<IconButton
aria-label="Subscript"
variant={state.isSubscript ? "set" : "default"}
onClick={() => onToggle("subscript")}
>
<SubscriptIcon />
</IconButton>
</Tooltip>

<Tooltip content="Inline link">
<IconButton
aria-label="Inline link"
variant={state.isLink ? "set" : "default"}
onClick={() => onToggle("link")}
>
<Link2Icon />
</IconButton>
</Tooltip>

<Tooltip content="Wrap with span">
<IconButton
aria-label="Wrap with span"
variant={state.isSpan ? "set" : "default"}
onClick={() => onToggle("span")}
>
<BrushIcon />
</IconButton>
</Tooltip>
</Flex>
);
};

Expand Down
46 changes: 46 additions & 0 deletions packages/design-system/src/components/icon-button.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type { ComponentStory } from "@storybook/react";
import { CrossIcon } from "@webstudio-is/icons";
import { Flex } from "./flex";
import { IconButton } from "./icon-button";

export default {
component: IconButton,
};

export const Default: ComponentStory<typeof IconButton> = () => {
return (
<Flex direction="column" gap={2}>
<Flex gap={2}>
<IconButton>
<CrossIcon fill="currentColor" />
</IconButton>
<IconButton variant="preset">
<CrossIcon fill="currentColor" />
</IconButton>
<IconButton variant="set">
<CrossIcon fill="currentColor" />
</IconButton>
<IconButton variant="inherited">
<CrossIcon fill="currentColor" />
</IconButton>
<IconButton variant="active">
<CrossIcon fill="currentColor" />
</IconButton>
</Flex>
<Flex gap={2}>
<IconButton disabled={true}>
<CrossIcon fill="currentColor" />
</IconButton>
<IconButton variant="preset" disabled={true}>
<CrossIcon fill="currentColor" />
</IconButton>
<IconButton variant="set" disabled={true}>
<CrossIcon fill="currentColor" />
</IconButton>
<IconButton variant="inherited" disabled={true}>
<CrossIcon fill="currentColor" />
</IconButton>
</Flex>
</Flex>
);
};
86 changes: 86 additions & 0 deletions packages/design-system/src/components/icon-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { styled } from "../stitches.config";

export const IconButton = styled("button", {
// reset styles
boxSizing: "border-box",
padding: 0,
appearance: "none",
backgroundColor: "transparent",
border: "1px solid transparent",
// center icon
display: "flex",
justifyContent: "center",
alignItems: "center",
// prevent shrinking inside flex box
flexShrink: 0,
// set size and shape
width: 28,
height: 28,
borderRadius: "$borderRadius$3",

"&:focus-visible": {
outline: "2px solid $blue10",
},
"&:disabled": {
borderColor: "transparent",
pointerEvents: "none",
},

variants: {
variant: {
default: {
color: "$slate12",
"&:hover": {
backgroundColor: "$slate6",
},
"&:disabled": {
color: "$slate8",
},
},
preset: {
backgroundColor: "$slate6",
borderColor: "$slate8",
color: "$slate12",
"&:hover": {
backgroundColor: "$slate8",
},
"&:disabled": {
color: "$slate8",
},
},
set: {
backgroundColor: "$blue4",
borderColor: "$blue6",
color: "$blue11",
"&:hover": {
backgroundColor: "$blue6",
},
"&:disabled": {
color: "$blue6",
},
},
inherited: {
backgroundColor: "$orange4",
borderColor: "$orange6",
color: "$orange11",
"&:hover": {
backgroundColor: "$orange6",
},
"&:disabled": {
color: "$orange6",
},
},
active: {
backgroundColor: "$blue10",
color: "White",
// non-interactive state because usually covered with overlay
},
},
},

defaultVariants: {
variant: "default",
},
});

IconButton.displayName = "IconButton";
1 change: 1 addition & 0 deletions packages/design-system/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export { ScrollArea } from "./components/scrollbar";
export { Tooltip, InputErrorsTooltip } from "./components/tooltip";
export { Button } from "./components/button";
export { IconButtonDeprecated } from "./components/icon-button-deprecated";
export { IconButton } from "./components/icon-button";
export { Box } from "./components/box";
export { ProgressBar } from "./components/progress-bar";
export * from "./components/popover";
Expand Down
1 change: 0 additions & 1 deletion packages/icons/icons/format-clear.svg

This file was deleted.

Loading

0 comments on commit 7637d48

Please sign in to comment.