Add toggle group component (#1809)

Ref https://github.com/webstudio-is/webstudio-builder/issues/1779
https://github.com/webstudio-is/webstudio-builder/issues/1779

- Here added new toggle group component aligned with figma.
- Replaced all legacy toggle group usages.
- Removed local/remote/overwritten colors from controls with labels.
- Prevented unselect of toggle group with single toggle
- Dropped unused toggle component from design system
This commit is contained in:
Bogdan Chadkin authored and GitHub committed 2023-06-20 21:23:23 +03:00
1 parent 056170e1a8
commit 45c74fc7ec
12 files changed
+287 -185

No files matched your search

@@ -7,7 +7,7 @@ import {
InputField,
Flex,
ToggleGroup,
ToggleGroupItem,
ToggleGroupButton,
Select,
Tooltip,
Label,
@@ -385,9 +385,9 @@ export const UrlControl = ({
}}
>
{Object.entries(modes).map(([key, { icon, label }]) => (
<ToggleGroupItem value={key} key={key}>
<Tooltip content={label}>{icon}</Tooltip>
</ToggleGroupItem>
<Tooltip key={key} content={label}>
<ToggleGroupButton value={key}>{icon}</ToggleGroupButton>
</Tooltip>
))}
</ToggleGroup>
</Flex>
@@ -1,15 +1,13 @@
import {
styled,
Flex,
ToggleGroup,
ToggleGroupItem,
ToggleGroupButton,
EnhancedTooltip,
} from "@webstudio-is/design-system";
import type { StyleSource } from "../../shared/style-info";
import { theme } from "@webstudio-is/design-system";
export type ToggleGroupControlProps = {
styleSource: StyleSource;
styleSource?: StyleSource;
value: string;
items: { child: JSX.Element; label: string; value: string }[];
onValueChange?: (value: string) => void;
@@ -24,6 +22,7 @@ export const ToggleGroupControl = ({
}: ToggleGroupControlProps) => {
return (
<ToggleGroup
color={styleSource}
type="single"
value={value}
onValueChange={onValueChange}
@@ -31,44 +30,13 @@ export const ToggleGroupControl = ({
>
{items.map(({ child, label, value }, index) => {
return (
<ToggleGroupControlItem
key={index}
value={value}
source={styleSource}
>
<EnhancedTooltip content={label}>
<EnhancedTooltip key={index} content={label}>
<ToggleGroupButton value={value}>
<Flex>{child}</Flex>
</EnhancedTooltip>
</ToggleGroupControlItem>
</ToggleGroupButton>
</EnhancedTooltip>
);
})}
</ToggleGroup>
);
};
const ToggleGroupControlItem = styled(ToggleGroupItem, {
variants: {
source: {
default: {},
preset: {},
local: {
"&[data-state=on]": {
color: theme.colors.foregroundLocalMain,
backgroundColor: theme.colors.backgroundLocalMain,
},
},
overwritten: {
"&[data-state=on]": {
color: theme.colors.foregroundOverwrittenMain,
backgroundColor: theme.colors.backgroundOverwrittenMain,
},
},
remote: {
"&[data-state=on]": {
color: theme.colors.foregroundRemoteMain,
backgroundColor: theme.colors.backgroundRemoteMain,
},
},
},
},
});
@@ -9,7 +9,7 @@ import {
Flex,
Grid,
ToggleGroup,
ToggleGroupItem,
ToggleGroupButton,
Separator,
styled,
} from "@webstudio-is/design-system";
@@ -131,12 +131,12 @@ export const BackgroundContent = (props: BackgroundContentProps) => {
}
}}
>
<ToggleGroupItem value={"image"}>
<ToggleGroupButton value={"image"}>
<Flex css={{ px: theme.spacing[2] }}>Image</Flex>
</ToggleGroupItem>
<ToggleGroupItem value={"gradient"}>
</ToggleGroupButton>
<ToggleGroupButton value={"gradient"}>
<Flex css={{ px: theme.spacing[2] }}>Gradient</Flex>
</ToggleGroupItem>
</ToggleGroupButton>
</ToggleGroup>
</Flex>
</BackgroundSection>
@@ -311,12 +311,12 @@ export const BackgroundContent = (props: BackgroundContentProps) => {
});
}}
>
<ToggleGroupItem value={"scroll"}>
<ToggleGroupButton value={"scroll"}>
<Flex css={{ px: theme.spacing[3] }}>Scroll</Flex>
</ToggleGroupItem>
<ToggleGroupItem value={"fixed"}>
</ToggleGroupButton>
<ToggleGroupButton value={"fixed"}>
<Flex css={{ px: theme.spacing[3] }}>Fixed</Flex>
</ToggleGroupItem>
</ToggleGroupButton>
</ToggleGroup>
</Flex>
@@ -4,7 +4,7 @@ import {
Grid,
theme,
ToggleGroup,
ToggleGroupItem,
ToggleGroupButton,
Tooltip,
} from "@webstudio-is/design-system";
import {
@@ -92,11 +92,11 @@ export const BorderStyle = (
}}
>
{borderStyleValues.map(({ value, Icon }) => (
<ToggleGroupItem key={value} value={value}>
<Tooltip content={toPascalCase(value)}>
<Tooltip key={value} content={toPascalCase(value)}>
<ToggleGroupButton value={value}>
<Icon />
</Tooltip>
</ToggleGroupItem>
</ToggleGroupButton>
</Tooltip>
))}
</ToggleGroup>
</Grid>
@@ -19,7 +19,6 @@ import {
MenuEllipsesIcon,
} from "@webstudio-is/icons";
import { FloatingPanel } from "~/builder/shared/floating-panel";
import { getStyleSource } from "../../shared/style-info";
import { CollapsibleSection } from "../../shared/collapsible-section";
const properties: StyleProperty[] = [
@@ -59,7 +58,6 @@ const FlexChildSectionAlign = (props: RenderCategoryProps) => {
onReset={() => deleteProperty("alignSelf")}
/>
<ToggleGroupControl
styleSource={getStyleSource(currentStyle.alignSelf)}
onValueChange={(value) => setAlignSelf({ type: "keyword", value })}
value={toValue(currentStyle.alignSelf?.value)}
items={[
@@ -117,10 +115,6 @@ const FlexChildSectionSizing = (props: RenderCategoryProps) => {
}}
/>
<ToggleGroupControl
styleSource={getStyleSource(
currentStyle.flexGrow,
currentStyle.flexShrink
)}
onValueChange={(value) => {
switch (value) {
case "none": {
@@ -279,7 +273,6 @@ const FlexChildSectionOrder = (props: RenderCategoryProps) => {
onReset={() => deleteProperty("order")}
/>
<ToggleGroupControl
styleSource={getStyleSource(currentStyle.order)}
onValueChange={(value) => {
switch (value) {
case "0":
@@ -3,7 +3,7 @@ import {
Grid,
theme,
ToggleGroup,
ToggleGroupItem,
ToggleGroupButton,
Tooltip,
} from "@webstudio-is/design-system";
import {
@@ -65,11 +65,11 @@ export const OutlineStyle = (
}
>
{outlineStyleValues.map(({ value, Icon }) => (
<ToggleGroupItem key={value} value={value}>
<Tooltip content={toPascalCase(value)}>
<Tooltip key={value} content={toPascalCase(value)}>
<ToggleGroupButton value={value}>
<Icon />
</Tooltip>
</ToggleGroupItem>
</ToggleGroupButton>
</Tooltip>
))}
</ToggleGroup>
</Grid>
@@ -15,7 +15,6 @@ import {
ScrollIcon,
AutoScrollIcon,
} from "@webstudio-is/icons";
import { getStyleSource } from "../../shared/style-info";
import { CollapsibleSection } from "../../shared/collapsible-section";
import { theme } from "@webstudio-is/design-system";
import { ToggleGroupControl } from "../../controls/toggle/toggle-control";
@@ -133,7 +132,6 @@ export const SizeSection = ({
onReset={() => deleteProperty("overflow")}
/>
<ToggleGroupControl
styleSource={getStyleSource(style.overflow)}
items={[
{
child: <EyeconOpenIcon />,
@@ -347,7 +347,6 @@ export const TypographySectionAdvancedPopover = (
onReset={() => deleteProperty("direction")}
/>
<ToggleGroupControl
styleSource={getStyleSource(currentStyle.direction)}
onValueChange={(value) =>
setDirection({ type: "keyword", value })
}
@@ -374,7 +373,6 @@ export const TypographySectionAdvancedPopover = (
onReset={() => deleteProperty("hyphens")}
/>
<ToggleGroupControl
styleSource={getStyleSource(currentStyle.hyphens)}
onValueChange={(value) => setHyphens({ type: "keyword", value })}
value={toValue(currentStyle.hyphens?.value)}
items={[
@@ -399,7 +397,6 @@ export const TypographySectionAdvancedPopover = (
onReset={() => deleteProperty("textOverflow")}
/>
<ToggleGroupControl
styleSource={getStyleSource(currentStyle.textOverflow)}
onValueChange={(value) =>
setTextOverflow({ type: "keyword", value })
}
@@ -0,0 +1,152 @@
import type { ComponentProps } from "react";
import { BorderRadiusIndividualIcon } from "@webstudio-is/icons";
import { ToggleGroup, ToggleGroupButton } from "./toggle-group";
import { StorySection, StoryGrid } from "./storybook";
import { EnhancedTooltip, EnhancedTooltipProvider } from "./enhanced-tooltip";
const toggleGroupColors = [
"default",
"preset",
"local",
"overwritten",
"remote",
] as const;
const ToggleGroupButtons = () => {
return (
<>
<ToggleGroupButton value="one">
<BorderRadiusIndividualIcon fill="currentColor" />
</ToggleGroupButton>
<ToggleGroupButton value="two">
<BorderRadiusIndividualIcon fill="currentColor" />
</ToggleGroupButton>
<ToggleGroupButton value="three">
<BorderRadiusIndividualIcon fill="currentColor" />
</ToggleGroupButton>
<ToggleGroupButton value="four">
<BorderRadiusIndividualIcon fill="currentColor" />
</ToggleGroupButton>
<ToggleGroupButton value="five">
<BorderRadiusIndividualIcon fill="currentColor" />
</ToggleGroupButton>
<ToggleGroupButton value="six">
<BorderRadiusIndividualIcon fill="currentColor" />
</ToggleGroupButton>
<ToggleGroupButton value="seven">
<BorderRadiusIndividualIcon fill="currentColor" />
</ToggleGroupButton>
</>
);
};
export const Demo = ({
type = "single",
color = "default",
disabled = false,
}: ComponentProps<typeof ToggleGroup>) => (
<>
<StorySection title="Configurable">
<StoryGrid css={{ alignItems: "flex-start" }}>
<ToggleGroup key={color} type={type} color={color} disabled={disabled}>
<ToggleGroupButtons />
</ToggleGroup>
</StoryGrid>
</StorySection>
<StorySection title="Colors">
<StoryGrid css={{ alignItems: "flex-start" }}>
{toggleGroupColors.map((color) => (
<ToggleGroup
key={color}
color={color}
type="single"
defaultValue="one"
>
<ToggleGroupButtons />
</ToggleGroup>
))}
</StoryGrid>
</StorySection>
<StorySection title="Variants disabled">
<StoryGrid css={{ alignItems: "flex-start" }}>
{toggleGroupColors.map((color) => (
<ToggleGroup
key={color}
color={color}
type="single"
defaultValue="one"
disabled={true}
>
<ToggleGroupButtons />
</ToggleGroup>
))}
</StoryGrid>
</StorySection>
<StorySection title="With text">
<StoryGrid css={{ alignItems: "flex-start" }}>
<EnhancedTooltipProvider>
<ToggleGroup color={color} type="single" defaultValue="one">
<EnhancedTooltip content="One">
<ToggleGroupButton value="one">One</ToggleGroupButton>
</EnhancedTooltip>
<EnhancedTooltip content="Two">
<ToggleGroupButton value="two">Two</ToggleGroupButton>
</EnhancedTooltip>
<EnhancedTooltip content="Three">
<ToggleGroupButton value="three">Three</ToggleGroupButton>
</EnhancedTooltip>
</ToggleGroup>
</EnhancedTooltipProvider>
</StoryGrid>
</StorySection>
<StorySection title="With tooltips">
<StoryGrid css={{ alignItems: "flex-start" }}>
<EnhancedTooltipProvider>
<ToggleGroup color={color} type="single" defaultValue="one">
<EnhancedTooltip content="One">
<ToggleGroupButton value="one">
<BorderRadiusIndividualIcon fill="currentColor" />
</ToggleGroupButton>
</EnhancedTooltip>
<EnhancedTooltip content="Two">
<ToggleGroupButton value="two">
<BorderRadiusIndividualIcon fill="currentColor" />
</ToggleGroupButton>
</EnhancedTooltip>
<EnhancedTooltip content="Three">
<ToggleGroupButton value="three">
<BorderRadiusIndividualIcon fill="currentColor" />
</ToggleGroupButton>
</EnhancedTooltip>
</ToggleGroup>
</EnhancedTooltipProvider>
</StoryGrid>
</StorySection>
</>
);
Demo.argTypes = {
type: {
control: { type: "inline-radio", options: ["single", "multiple"] },
},
color: {
control: { type: "inline-radio", options: toggleGroupColors },
},
disabled: {
control: { type: "boolean" },
},
};
Demo.storyName = "Toggle Group";
export default {
title: "Library/Toggle Group",
parameters: {
// to make the variant=contrast visible
backgrounds: { default: "Panel" },
},
};
@@ -1,48 +1,111 @@
import { styled } from "../stitches.config";
/**
* Implementation of the "Toggle Group" component from:
* https://www.figma.com/file/sfCE7iLS0k25qCxiifQNLE/%F0%9F%93%9A-Webstudio-Library?type=design&node-id=4-2831&t=9qVuJbUcZqhAI06U-0
*/
import {
type ComponentProps,
type ElementRef,
createContext,
useContext,
forwardRef,
} from "react";
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group";
import { theme } from "../stitches.config";
import { styled, theme } from "../stitches.config";
import { IconButton } from "./icon-button";
import { textVariants } from "./text";
export const ToggleGroup = styled(ToggleGroupPrimitive.Root, {
display: "inline-flex",
borderRadius: theme.spacing[3],
boxShadow: `0 0 0 ${theme.spacing[1]} ${theme.colors.slate7}`,
padding: 2,
backgroundColor: theme.colors.backgroundControls,
});
type Color = "default" | "preset" | "local" | "remote" | "overwritten";
export const ToggleGroupItem = styled(ToggleGroupPrimitive.Item, {
// all: "unset", // @note weird bug, this somehow gets into weird specifity issues with how styles are inserted
border: "none",
backgroundColor: theme.colors.backgroundControls,
color: theme.colors.hiContrast,
const ToggleGroupContext = createContext<{
color?: Color;
}>({});
type ToggleGroupProps = ComponentProps<
typeof ToggleGroupPrimitive.ToggleGroup
> & {
color?: Color;
};
const BaseToggleGroup = forwardRef<ElementRef<"div">, ToggleGroupProps>(
({ color = "default", children, onValueChange, ...props }, ref) => {
return (
<ToggleGroupContext.Provider value={{ color }}>
<ToggleGroupPrimitive.ToggleGroup
ref={ref}
{...props}
onValueChange={(newValue: string | string[]) => {
// prevent unselecting buttons when only single can be selected
if (newValue !== "") {
onValueChange?.(newValue as string & string[]);
}
}}
>
{children}
</ToggleGroupPrimitive.ToggleGroup>
</ToggleGroupContext.Provider>
);
}
);
BaseToggleGroup.displayName = "BaseToggleGroup";
export const ToggleGroup = styled(BaseToggleGroup, {
boxSizing: "border-box",
display: "flex",
whiteSpace: "nowrap",
fontSize: theme.deprecatedFontSize[4],
lineHeight: 1,
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
height: theme.spacing[11],
outline: "none",
"&": {
px: theme.spacing[3],
},
"&:first-child": {
marginLeft: 0,
borderTopLeftRadius: theme.spacing[3],
borderBottomLeftRadius: theme.spacing[3],
},
"&:last-child": {
borderTopRightRadius: theme.spacing[3],
borderBottomRightRadius: theme.spacing[3],
},
"&:hover": { backgroundColor: theme.colors.slateA3 },
// @note because the outline is outside of the element others can end up covering it
"&:focus-visible": {
outline: `2px solid ${theme.colors.borderFocus}`,
outlineOffset: "-2px",
borderRadius: theme.spacing[3],
zIndex: 1,
},
"&[data-state=on]": { backgroundColor: theme.colors.slate5 },
padding: 1,
background: theme.colors.backgroundControls,
border: `1px solid ${theme.colors.borderMain}`,
borderRadius: theme.borderRadius[4],
});
const BaseToggleGroupButton = forwardRef<
ElementRef<"button">,
ComponentProps<typeof IconButton>
>((props, ref) => {
const { color } = useContext(ToggleGroupContext);
return (
<IconButton
ref={ref}
{...props}
variant={
// default is unselected state
// when button is selected fallback to preset
props["aria-checked"] === true
? color === "default"
? "preset"
: color
: "default"
}
css={{
width: "auto",
height: theme.spacing[11],
minWidth: theme.spacing[11],
// accept only 16px icons or text
paddingLeft: theme.spacing[3],
paddingRight: theme.spacing[3],
borderRadius: theme.borderRadius[2],
...textVariants.labelsTitleCase,
}}
/>
);
});
BaseToggleGroupButton.displayName = "BaseToggleGroupButton";
type ToggleGroupButtonProps = ComponentProps<typeof ToggleGroupPrimitive.Item>;
export const ToggleGroupButton = forwardRef<
ElementRef<"button">,
ToggleGroupButtonProps
>(({ children, ...props }, ref) => {
return (
<ToggleGroupPrimitive.Item ref={ref} {...props} asChild>
<BaseToggleGroupButton>{children}</BaseToggleGroupButton>
</ToggleGroupPrimitive.Item>
);
});
ToggleGroupButton.displayName = "ToggleGroupButton";
@@ -1,68 +0,0 @@
import { styled } from "../stitches.config";
import * as TogglePrimitive from "@radix-ui/react-toggle";
import { theme } from "../stitches.config";
export const Toggle = styled(TogglePrimitive.Root, {
// Reset
alignItems: "center",
appearance: "none",
borderWidth: "0",
boxSizing: "border-box",
display: "inline-flex",
flexShrink: 0,
fontFamily: "inherit",
fontSize: theme.deprecatedFontSize[4],
WebkitFontSmoothing: "antialiased",
justifyContent: "center",
lineHeight: "1",
outline: "none",
padding: "0",
textDecoration: "none",
userSelect: "none",
WebkitTapHighlightColor: "transparent",
color: theme.colors.hiContrast,
"&::before": {
boxSizing: "border-box",
},
"&::after": {
boxSizing: "border-box",
},
height: theme.spacing[11],
width: theme.spacing[11],
backgroundColor: "transparent",
"@hover": {
"&:hover": {
backgroundColor: theme.colors.slateA3,
},
},
"&:active": {
backgroundColor: theme.colors.slateA4,
},
"&:focus": {
boxShadow: `inset 0 0 0 1px ${theme.colors.slateA8}, 0 0 0 1px ${theme.colors.slateA8}`,
zIndex: 1,
},
'&[data-state="on"]': {
backgroundColor: theme.colors.slateA5,
"@hover": {
"&:hover": {
backgroundColor: theme.colors.slateA5,
},
},
"&:active": {
backgroundColor: theme.colors.slateA7,
},
},
variants: {
shape: {
circle: {
borderRadius: theme.borderRadius.round,
},
square: {
borderRadius: theme.borderRadius[4],
},
},
},
});
-1
View File
@@ -55,7 +55,6 @@ export {
SidebarTabsTrigger,
} from "./components/sidebar-tabs";
export { Card } from "./components/card";
export { Toggle } from "./components/toggle";
export { Tooltip, InputErrorsTooltip } from "./components/tooltip";
export {
EnhancedTooltip,