diff --git a/packages/components/src/common/warnings.ts b/packages/components/src/common/warnings.ts index 9d18c003f..26b9957f6 100644 --- a/packages/components/src/common/warnings.ts +++ b/packages/components/src/common/warnings.ts @@ -2,6 +2,7 @@ export enum SDSWarningTypes { ButtonMinimalIsAllCaps = "buttonMinimalIsAllCaps", ButtonMissingSDSProps = "buttonMissingProps", ButtonIconMissingIconProp = "buttonIconMissingIconProp", + ButtonToggleMissingIconProp = "buttonToggleMissingIconProp", ChipDeprecated = "chipDeprecated", MenuSelectDeprecated = "menuSelectDeprecated", TooltipSubtitle = "tooltipSubtitle", @@ -9,7 +10,7 @@ export enum SDSWarningTypes { TooltipInverted = "tooltipInverted", } -const SDS_WARNINGS = { +export const SDS_WARNINGS = { [SDSWarningTypes.ButtonMinimalIsAllCaps]: { hasWarned: false, message: @@ -25,6 +26,10 @@ const SDS_WARNINGS = { message: "Warning: Buttons with an SDS type of icon require an icon prop to be provided.", }, + [SDSWarningTypes.ButtonToggleMissingIconProp]: { + hasWarned: false, + message: "Warning: Button Toggles require an icon prop to be provided.", + }, [SDSWarningTypes.ChipDeprecated]: { hasWarned: false, message: "Warning: will be deprecated and replaced with ", diff --git a/packages/components/src/core/Autocomplete/__tests__/__snapshots__/index.test.tsx.snap b/packages/components/src/core/Autocomplete/__tests__/__snapshots__/index.test.tsx.snap index f00aeed5f..f4133fb52 100644 --- a/packages/components/src/core/Autocomplete/__tests__/__snapshots__/index.test.tsx.snap +++ b/packages/components/src/core/Autocomplete/__tests__/__snapshots__/index.test.tsx.snap @@ -28,7 +28,7 @@ exports[` ControlledOpen story renders snapshot 1`] = ` ControlledOpen story renders snapshot 1`] = ` ControlledOpen story renders snapshot 1`] = ` Default story renders snapshot 1`] = ` Default story renders snapshot 1`] = ` Default story renders snapshot 1`] = ` { const spaces = getSpaces(props); return css` + ${sdsType !== "tertiary" + ? `margin: 0 ${spaces?.xxs}px ${spaces?.xxs}px 0;` + : ""} + .MuiSvgIcon-root { height: ${iconSizes?.s.height}px; width: ${iconSizes?.s.width}px; @@ -137,6 +141,10 @@ const medium = (props: ButtonIconExtraProps): SerializedStyles => { const spaces = getSpaces(props); return css` + ${sdsType !== "tertiary" + ? `margin: 0 ${spaces?.xxs}px ${spaces?.xxs}px 0;` + : ""} + .MuiSvgIcon-root { height: ${iconSizes?.l.height}px; width: ${iconSizes?.l.width}px; @@ -151,6 +159,10 @@ const large = (props: ButtonIconExtraProps): SerializedStyles => { const spaces = getSpaces(props); return css` + ${sdsType !== "tertiary" + ? `margin: 0 ${spaces?.s}px ${spaces?.s}px 0;` + : ""} + .MuiSvgIcon-root { height: ${iconSizes?.xl.height}px; width: ${iconSizes?.xl.height}px; diff --git a/packages/components/src/core/ButtonToggle/__storybook__/constants.tsx b/packages/components/src/core/ButtonToggle/__storybook__/constants.tsx new file mode 100644 index 000000000..a92b92ff8 --- /dev/null +++ b/packages/components/src/core/ButtonToggle/__storybook__/constants.tsx @@ -0,0 +1,29 @@ +import CustomSdsIcon from "src/common/storybook/customSdsIcon"; +import CustomSvgIcon from "src/common/storybook/customSvgIcon"; + +export const BUTTON_TOGGLE_EXCLUDED_CONTROLS = [ + "sdsSize", + "icon", + "sdsStage", + "sdsType", +]; + +export const BUTTON_TOGGLE_ICON_OPTIONS = [ + "InfoCircle", + "SlidersHorizontal", + "Filter", + "DotsHorizontal", + "LinesHorizontal3", + , + , +]; + +export const BUTTON_TOGGLE_ICON_LABELS = [ + "SDS Icon: InfoCircle (s/m/l)", + "SDS Icon: SlidersHorizontal (m/l)", + "SDS Icon: Filter (s)", + "SDS Icon: DotsHorizontal (s/m/l)", + "SDS Icon: LinesHorizontal3 (s/m/l)", + "Custom SDS Icon (s/m/l)", + "Custom SVG Icon (s/m/l)", +]; diff --git a/packages/components/src/core/ButtonToggle/__storybook__/index.stories.tsx b/packages/components/src/core/ButtonToggle/__storybook__/index.stories.tsx new file mode 100644 index 000000000..2dd187d22 --- /dev/null +++ b/packages/components/src/core/ButtonToggle/__storybook__/index.stories.tsx @@ -0,0 +1,76 @@ +import { Args, Meta } from "@storybook/react"; +import { BADGE } from "@geometricpanda/storybook-addon-badges"; +import { ButtonToggle } from "./stories/default"; +import { + BUTTON_TOGGLE_EXCLUDED_CONTROLS, + BUTTON_TOGGLE_ICON_LABELS, + BUTTON_TOGGLE_ICON_OPTIONS, +} from "./constants"; +import { TestDemo } from "./stories/test"; + +export default { + argTypes: { + disabled: { + control: { + type: "boolean", + }, + }, + icon: { + control: { + labels: BUTTON_TOGGLE_ICON_LABELS, + type: "select", + }, + mapping: BUTTON_TOGGLE_ICON_OPTIONS, + options: Object.keys(BUTTON_TOGGLE_ICON_OPTIONS), + }, + sdsSize: { + control: { + type: "select", + }, + options: ["small", "medium", "large"], + }, + sdsStage: { + control: { + type: "radio", + }, + options: ["on", "off"], + }, + sdsType: { + control: { + type: "radio", + }, + options: ["primary", "secondary"], + }, + }, + component: ButtonToggle, + parameters: { + badges: [BADGE.BETA], + }, + title: "Components/Buttons/ButtonToggle", +} as Meta; + +// Default + +export const Default = { + args: { + disabled: false, + icon: "InfoCircle", + sdsSize: "medium", + sdsStage: "off", + sdsType: "primary", + }, +}; + +// Test + +export const Test = { + parameters: { + controls: { + exclude: BUTTON_TOGGLE_EXCLUDED_CONTROLS, + }, + snapshot: { + skip: true, + }, + }, + render: (args: Args) => , +}; diff --git a/packages/components/src/core/ButtonToggle/__storybook__/stories/default.tsx b/packages/components/src/core/ButtonToggle/__storybook__/stories/default.tsx new file mode 100644 index 000000000..37eb33932 --- /dev/null +++ b/packages/components/src/core/ButtonToggle/__storybook__/stories/default.tsx @@ -0,0 +1,8 @@ +import { Args } from "@storybook/react"; +import RawButtonToggle from "src/core/ButtonToggle"; + +export const ButtonToggle = (props: Args): JSX.Element => { + const { icon } = props; + + return ; +}; diff --git a/packages/components/src/core/ButtonToggle/__storybook__/stories/test.tsx b/packages/components/src/core/ButtonToggle/__storybook__/stories/test.tsx new file mode 100644 index 000000000..744ca119e --- /dev/null +++ b/packages/components/src/core/ButtonToggle/__storybook__/stories/test.tsx @@ -0,0 +1,15 @@ +import { Args } from "@storybook/react"; +import ButtonToggle from "src/core/ButtonToggle"; + +export const TestDemo = (props: Args): JSX.Element => { + const { icon } = props; + + return ( + + ); +}; diff --git a/packages/components/src/core/ButtonToggle/__tests__/ButtonToggle.namespace-test.tsx b/packages/components/src/core/ButtonToggle/__tests__/ButtonToggle.namespace-test.tsx new file mode 100644 index 000000000..1078681f4 --- /dev/null +++ b/packages/components/src/core/ButtonToggle/__tests__/ButtonToggle.namespace-test.tsx @@ -0,0 +1,13 @@ +import { ButtonToggle, ButtonToggleProps } from "@czi-sds/components"; + +const ButtonToggleNameSpaceTest = (props: ButtonToggleProps) => { + return ( + + ); +}; diff --git a/packages/components/src/core/ButtonToggle/__tests__/__snapshots__/index.test.tsx.snap b/packages/components/src/core/ButtonToggle/__tests__/__snapshots__/index.test.tsx.snap new file mode 100644 index 000000000..f5bb55c8b --- /dev/null +++ b/packages/components/src/core/ButtonToggle/__tests__/__snapshots__/index.test.tsx.snap @@ -0,0 +1,30 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` Default story renders snapshot 1`] = ` + + + + + + +`; diff --git a/packages/components/src/core/ButtonToggle/__tests__/index.test.tsx b/packages/components/src/core/ButtonToggle/__tests__/index.test.tsx new file mode 100644 index 000000000..26799bf00 --- /dev/null +++ b/packages/components/src/core/ButtonToggle/__tests__/index.test.tsx @@ -0,0 +1,90 @@ +import { generateSnapshots } from "@chanzuckerberg/story-utils"; +import { composeStories } from "@storybook/react"; +import { cleanup, render, screen } from "@testing-library/react"; +import * as stories from "../__storybook__/index.stories"; +import { SDS_WARNINGS, SDSWarningTypes } from "src/common/warnings"; + +const { Test } = composeStories(stories); + +const BUTTON_TOGGLE_TEST_ID = "button-toggle"; + +describe("", () => { + generateSnapshots(stories); + + it("renders ButtonToggle component", () => { + render(); + const panelElement = screen.getByTestId(BUTTON_TOGGLE_TEST_ID); + expect(panelElement).not.toBeNull(); + }); + + it("renders with different sdsSize values", () => { + render(); + const smallButton = screen.getByTestId(BUTTON_TOGGLE_TEST_ID); + expect(smallButton).toBeInTheDocument(); + + // (masoudmanson): cleanup is necessary to avoid having + // multiple elements with the same test id in the DOM + cleanup(); + + render(); + const largeButton = screen.getByTestId(BUTTON_TOGGLE_TEST_ID); + expect(largeButton).toBeInTheDocument(); + }); + + it("renders with different sdsStage values", () => { + render(); + const onStageButton = screen.getByTestId(BUTTON_TOGGLE_TEST_ID); + expect(onStageButton).toBeInTheDocument(); + + cleanup(); + + render(); + const offStageButton = screen.getByTestId(BUTTON_TOGGLE_TEST_ID); + expect(offStageButton).toBeInTheDocument(); + }); + + it("renders with different sdsType values", () => { + render(); + const primaryButton = screen.getByTestId(BUTTON_TOGGLE_TEST_ID); + expect(primaryButton).toBeInTheDocument(); + + cleanup(); + + render(); + const secondaryButton = screen.getByTestId(BUTTON_TOGGLE_TEST_ID); + expect(secondaryButton).toBeInTheDocument(); + }); + + it("displays warning when icon is missing", () => { + const warnSpy = jest.spyOn(console, "warn").mockImplementation(() => {}); + render(); + expect(warnSpy).toHaveBeenCalledWith( + expect.stringContaining( + SDS_WARNINGS[SDSWarningTypes.ButtonToggleMissingIconProp].message + ) + ); + warnSpy.mockRestore(); + }); + + it("displays an error when an icon doesn't support the ButtonToggle size", () => { + const errorSpy = jest.spyOn(console, "error").mockImplementation(() => {}); + // (masoudmanson): SlidersHorizontal icon doesn't support the small size + // make sure to change this to another icon if the SlidersHorizontal icon is updated + const SdsIconWithoutSmallSize = "SlidersHorizontal"; + render( + + ); + expect(errorSpy).toHaveBeenCalledWith( + expect.stringContaining( + `Error: Icon ${SdsIconWithoutSmallSize} not found for size s. This is a @czi-sds/components problem.` + ) + ); + errorSpy.mockRestore(); + }); + + it("renders with disabled state", () => { + render(); + const disabledButton = screen.getByTestId(BUTTON_TOGGLE_TEST_ID); + expect(disabledButton).toBeDisabled(); + }); +}); diff --git a/packages/components/src/core/ButtonToggle/index.tsx b/packages/components/src/core/ButtonToggle/index.tsx new file mode 100644 index 000000000..80e06641f --- /dev/null +++ b/packages/components/src/core/ButtonToggle/index.tsx @@ -0,0 +1,50 @@ +import React from "react"; +import { ButtonProps } from "@mui/material"; +import { IconNameToSizes } from "../Icon"; +import { + SDSWarningTypes, + showWarningIfFirstOccurence, +} from "src/common/warnings"; +import { StyledButtonToggle } from "./style"; + +export interface ButtonToggleProps extends ButtonProps { + disabled?: boolean; + icon: keyof IconNameToSizes | React.ReactElement; + sdsSize?: "small" | "medium" | "large"; + sdsStage?: "on" | "off"; + sdsType?: "primary" | "secondary"; +} + +/** + * @see https://mui.com/material-ui/react-button/ + */ + +const ButtonToggle = React.forwardRef( + (props, ref) => { + const { + icon, + sdsSize = "medium", + sdsStage = "off", + sdsType = "primary", + ...rest + } = props; + + if (icon !== undefined) { + return ( + + ); + } else { + showWarningIfFirstOccurence(SDSWarningTypes.ButtonToggleMissingIconProp); + return null; + } + } +); + +export default ButtonToggle; diff --git a/packages/components/src/core/ButtonToggle/style.ts b/packages/components/src/core/ButtonToggle/style.ts new file mode 100644 index 000000000..1effb6933 --- /dev/null +++ b/packages/components/src/core/ButtonToggle/style.ts @@ -0,0 +1,30 @@ +import styled from "@emotion/styled"; +import ButtonIcon from "../ButtonIcon"; +import { CommonThemeProps, getSemanticColors } from "../styles"; +import { ButtonToggleProps } from "."; + +/** + * (masoudmanson): Since StyledButtonToggle is built on top of ButtonIcon, + * we only need to exclude the `sdsStage` prop from being forwarded, + * as it is not a valid prop for ButtonIcon. + * All other props should be passed down to ButtonIcon; otherwise, + * the component won’t function as expected. + */ +const doNotForwardProps = ["sdsStage"]; + +interface ButtonToggleExtraProps extends CommonThemeProps { + sdsStage?: ButtonToggleProps["sdsStage"]; +} + +export const StyledButtonToggle = styled(ButtonIcon, { + shouldForwardProp: (prop: string) => !doNotForwardProps.includes(prop), +})` + ${(props: ButtonToggleExtraProps) => { + const { sdsStage } = props; + const semanticColors = getSemanticColors(props); + + return ` + background-color: ${sdsStage === "on" ? semanticColors?.base?.fillHover : "transparent"}; + `; + }} +`; diff --git a/packages/components/src/core/CellBasic/__storybook__/style.ts b/packages/components/src/core/CellBasic/__storybook__/style.ts index ad7abc929..546666d1f 100644 --- a/packages/components/src/core/CellBasic/__storybook__/style.ts +++ b/packages/components/src/core/CellBasic/__storybook__/style.ts @@ -16,7 +16,6 @@ export const ButtonIconsGroupRight = styled("div")` return ` align-items: center; display: inline-flex; - gap: ${spaces?.xxxs}px; height: 100%; border-left: solid 1px ${semanticColors?.base?.divider}; padding-left: ${spaces?.xs}px; @@ -25,14 +24,7 @@ export const ButtonIconsGroupRight = styled("div")` `; export const ButtonIconsGroupBottom = styled("div")` - ${(props: CommonThemeProps) => { - const spaces = getSpaces(props); - - return ` - display: inline-flex; - gap: ${spaces?.xs}px; - `; - }} + display: inline-flex; `; export const StyledButton = styled(Button)` @@ -95,8 +87,8 @@ export const StyledCellBasic = styled(CellBasic)` return ` border: dashed 1px ${semanticColors?.base?.divider}; height: 70px; - maxWidth: 250px; - width: 250px; + max-width: 300px; + width: 300px; `; }} `; diff --git a/packages/components/src/core/Dialog/__tests__/__snapshots__/index.test.tsx.snap b/packages/components/src/core/Dialog/__tests__/__snapshots__/index.test.tsx.snap index a97786750..8339b6ff5 100644 --- a/packages/components/src/core/Dialog/__tests__/__snapshots__/index.test.tsx.snap +++ b/packages/components/src/core/Dialog/__tests__/__snapshots__/index.test.tsx.snap @@ -22,7 +22,7 @@ exports[` Dialog all sizes match the snapshots 1`] = ` > Dialog all sizes match the snapshots 4`] = ` > Dialog all sizes match the snapshots 7`] = ` > Dialog all sizes match the snapshots 10`] = ` > Default story renders snapshot 1`] = ` > { - setOpen((prev) => !prev)} - aria-label="Panel Toggle" - > - Toggle Panel - + sdsStage={open ? "on" : "off"} + /> {LONG_LOREM_IPSUM} {LONG_LOREM_IPSUM} diff --git a/packages/components/src/core/Panel/__tests__/__snapshots__/index.test.tsx.snap b/packages/components/src/core/Panel/__tests__/__snapshots__/index.test.tsx.snap index 07ef7858d..2e49b56ce 100644 --- a/packages/components/src/core/Panel/__tests__/__snapshots__/index.test.tsx.snap +++ b/packages/components/src/core/Panel/__tests__/__snapshots__/index.test.tsx.snap @@ -3,7 +3,6 @@ exports[` Default story renders snapshot 1`] = ` Default story renders snapshot 1`] = ` Default story renders snapshot 1`] = ` >
{LONG_LOREM_IPSUM}