diff --git a/packages/documentation/src/Components/Button.stories.tsx b/packages/documentation/src/Components/Button.stories.tsx index 3a5058ba..2b1c5b37 100644 --- a/packages/documentation/src/Components/Button.stories.tsx +++ b/packages/documentation/src/Components/Button.stories.tsx @@ -10,15 +10,28 @@ export default { export const Basic: Story = (args) => { return ( -
- - - - - - - -
+ <> +
+ + + +
+ +

+ The following row is having the variant prop hard-coded: +

+
+ + + +
+ ); }; @@ -30,6 +43,7 @@ Basic.args = { size: "medium", raw: false, noBorder: false, + variant: "primary", }; Basic.argTypes = { mode: { @@ -44,4 +58,8 @@ Basic.argTypes = { options: ["small", "medium", "large"], control: { type: "radio" }, }, + variant: { + options: ["primary", "secondary", "danger"], + control: { type: "radio" }, + }, }; diff --git a/packages/ui-components/src/components/Button/Button.tsx b/packages/ui-components/src/components/Button/Button.tsx index 581cbb2f..891ec981 100644 --- a/packages/ui-components/src/components/Button/Button.tsx +++ b/packages/ui-components/src/components/Button/Button.tsx @@ -19,6 +19,7 @@ export const Button = React.forwardRef( noBorder = false, "aria-label": ariaLabel, spacing, + variant = "primary", ...otherProps }, @@ -35,6 +36,7 @@ export const Button = React.forwardRef( size, noBorder, spacing, + variant, }); return ( diff --git a/packages/ui-components/src/components/Button/ButtonTypes.d.ts b/packages/ui-components/src/components/Button/ButtonTypes.d.ts index 3bcec97b..fcbe67ef 100644 --- a/packages/ui-components/src/components/Button/ButtonTypes.d.ts +++ b/packages/ui-components/src/components/Button/ButtonTypes.d.ts @@ -49,6 +49,10 @@ export type ButtonProps = { * Handler to call when the Button is clicked. */ onClick?: React.MouseEventHandler; + /** + * The variant style of the Button. + */ + variant?: "primary" | "secondary" | "danger"; } & CommonButtonProps & React.ButtonHTMLAttributes; diff --git a/packages/ui-components/src/components/Button/__tests__/Button.test.tsx b/packages/ui-components/src/components/Button/__tests__/Button.test.tsx index 88a511e5..4ce025f2 100644 --- a/packages/ui-components/src/components/Button/__tests__/Button.test.tsx +++ b/packages/ui-components/src/components/Button/__tests__/Button.test.tsx @@ -174,10 +174,132 @@ describe("Button modifiers", () => { ]); }); - it("should render a light button", async () => { - render(); + it("should render a dark button with variant 'secondary'", async () => { + render( + , + ); const button = await screen.findByRole("button"); - expectToHaveClasses(button, ["bg-action-light", "text-copy-lighter"]); + expectToHaveClasses(button, [ + BUTTON_CLASSNAME, + "not-prose", + "rounded-full", + "bg-action-light", + "text-copy-lighter", + "px-4", + "text-base", + "font-medium", + "max-h-9", + "py-1", + "border", + "border-border-light", + "focus:outline", + "focus:outline-2", + "focus:outline-offset-2", + "focus:outline-focus-dark", + "dark:focus:outline-focus-light", + "hover:text-copy-light-hover", + "hover:bg-action-light-hover", + "active:text-copy-light-active", + "active:bg-action-light-active", + ]); + }); + + it("should render a light button with variant 'secondary'", async () => { + render( + , + ); + const button = await screen.findByRole("button"); + expectToHaveClasses(button, [ + BUTTON_CLASSNAME, + "not-prose", + "rounded-full", + "bg-action-dark", + "text-copy-light", + "px-4", + "text-base", + "font-medium", + "max-h-9", + "py-1", + "border", + "border-border-dark", + "focus:outline", + "focus:outline-2", + "focus:outline-offset-2", + "focus:outline-focus-dark", + "dark:focus:outline-focus-light", + "hover:text-copy-light-hover", + "hover:bg-action-dark-hover", + "active:text-copy-light-active", + "active:bg-action-dark-active", + ]); + }); + + it("should render a dark button with variant 'danger'", async () => { + render( + , + ); + const button = await screen.findByRole("button"); + expectToHaveClasses(button, [ + BUTTON_CLASSNAME, + "not-prose", + "rounded-full", + "bg-action-danger-dark", + "text-copy-light", + "px-4", + "text-base", + "font-medium", + "max-h-9", + "py-1", + "border", + "border-border-danger-dark", + "focus:outline", + "focus:outline-2", + "focus:outline-offset-2", + "focus:outline-focus-dark", + "dark:focus:outline-focus-light", + "hover:text-copy-light-hover", + "hover:bg-action-danger-dark-hover", + "active:text-copy-lighter-active", + "active:bg-action-danger-dark-active", + ]); + }); + + it("should render a light button with variant 'danger'", async () => { + render( + , + ); + const button = await screen.findByRole("button"); + expectToHaveClasses(button, [ + BUTTON_CLASSNAME, + "not-prose", + "rounded-full", + "bg-action-danger-light", + "text-copy-lighter", + "px-4", + "text-base", + "font-medium", + "max-h-9", + "py-1", + "border", + "border-border-danger-medium", + "focus:outline", + "focus:outline-2", + "focus:outline-offset-2", + "focus:outline-focus-dark", + "dark:focus:outline-focus-light", + "hover:text-copy-light-hover", + "hover:bg-action-danger-light-hover", + "active:text-copy-lighter-active", + "active:bg-action-danger-light-active", + ]); }); it("should render a disabled dark button", async () => { diff --git a/packages/ui-components/src/components/Button/utilities.ts b/packages/ui-components/src/components/Button/utilities.ts index 5b605098..59bee169 100644 --- a/packages/ui-components/src/components/Button/utilities.ts +++ b/packages/ui-components/src/components/Button/utilities.ts @@ -23,6 +23,7 @@ type getButtonClassesProps = { labelLeft?: string; labelRight?: string; noBackground?: boolean; + variant?: "primary" | "secondary" | "danger"; } & SpacingProps; const getButtonSizesClasses = ({ @@ -73,74 +74,174 @@ const getButtonSizesClasses = ({ const getButtonBaseClasses = ({ mode, noBackground, + variant, }: { - mode: string; + mode: "dark" | "light" | "system" | "alt-system"; + variant: "primary" | "secondary" | "danger"; noBackground?: boolean; }) => { - return clsx("not-prose rounded-full", { - "bg-action-dark text-copy-light": mode === "dark" && !noBackground, - "bg-action-light text-copy-lighter": mode === "light" && !noBackground, - "bg-action-dark text-copy-light dark:bg-action-light dark:text-copy-lighter": - mode === "system" && !noBackground, - "bg-action-light text-copy-lighter dark:bg-action-dark dark:text-copy-light": - mode === "alt-system" && !noBackground, - }); + if (noBackground) { + return "not-prose rounded-full"; + } + + if (variant === "primary") { + return clsx("not-prose rounded-full", { + "bg-action-dark text-copy-light": mode === "dark", + "bg-action-light text-copy-lighter": mode === "light", + "bg-action-dark text-copy-light dark:bg-action-light dark:text-copy-lighter": + mode === "system", + "bg-action-light text-copy-lighter dark:bg-action-dark dark:text-copy-light": + mode === "alt-system", + }); + } + if (variant === "secondary") { + return clsx("not-prose rounded-full", { + "bg-action-dark text-copy-light": mode === "light", + "bg-action-light text-copy-lighter": mode === "dark", + "bg-action-dark text-copy-light dark:bg-action-light dark:text-copy-lighter": + mode === "alt-system", + "bg-action-light text-copy-lighter dark:bg-action-dark dark:text-copy-light": + mode === "system", + }); + } + if (variant === "danger") { + return clsx("not-prose rounded-full", { + "bg-action-danger-dark text-copy-light": mode === "dark", + "bg-action-danger-light text-copy-lighter": mode === "light", + "bg-action-danger-dark text-copy-light dark:bg-action-danger-light dark:text-copy-lighter": + mode === "system", + "bg-action-danger-light text-copy-lighter dark:bg-action-danger-dark dark:text-copy-light": + mode === "alt-system", + }); + } }; const getButtonHoverClasses = ({ mode, disabled, + variant, }: { disabled: boolean; - mode: string; + mode: "dark" | "light" | "system" | "alt-system"; + variant: "primary" | "secondary" | "danger"; }) => { - return disabled - ? "" - : clsx("hover:text-copy-light-hover", { - "hover:bg-action-dark-hover": mode === "dark", - "hover:bg-action-light-hover": mode === "light", - "hover:bg-action-dark-hover dark:hover:bg-action-light-hover": - mode === "system", - "hover:bg-action-light-hover dark:hover:bg-action-dark-hover": - mode === "alt-system", - }); + if (disabled) { + return ""; + } + if (variant === "primary") { + return clsx("hover:text-copy-light-hover", { + "hover:bg-action-dark-hover": mode === "dark", + "hover:bg-action-light-hover": mode === "light", + "hover:bg-action-dark-hover dark:hover:bg-action-light-hover": + mode === "system", + "hover:bg-action-light-hover dark:hover:bg-action-dark-hover": + mode === "alt-system", + }); + } + if (variant === "secondary") { + return clsx("hover:text-copy-light-hover", { + "hover:bg-action-dark-hover": mode === "light", + "hover:bg-action-light-hover": mode === "dark", + "hover:bg-action-dark-hover dark:hover:bg-action-light-hover": + mode === "alt-system", + "hover:bg-action-light-hover dark:hover:bg-action-dark-hover": + mode === "system", + }); + } + if (variant === "danger") { + return clsx("hover:text-copy-light-hover", { + "hover:bg-action-danger-dark-hover": mode === "dark", + "hover:bg-action-danger-light-hover": mode === "light", + "hover:bg-action-danger-dark-hover dark:hover:bg-action-danger-light-hover": + mode === "system", + "hover:bg-action-danger-light-hover dark:hover:bg-action-danger-dark-hover": + mode === "alt-system", + }); + } }; const getButtonActiveClasses = ({ mode, disabled, + variant, }: { disabled: boolean; - mode: string; + mode: "dark" | "light" | "system" | "alt-system"; + variant: "primary" | "secondary" | "danger"; }) => { - return disabled - ? "" - : clsx("active:text-copy-light-active", { - "active:bg-action-dark-active": mode === "dark", - "active:bg-action-light-active": mode === "light", - "active:bg-action-dark-active dark:active:bg-action-light-active": - mode === "system", - "active:bg-action-light-active dark:active:bg-action-dark-active": - mode === "alt-system", - }); + if (disabled) { + return ""; + } + if (variant === "primary") { + return clsx("active:text-copy-light-active", { + "active:bg-action-dark-active": mode === "dark", + "active:bg-action-light-active": mode === "light", + "active:bg-action-dark-active dark:active:bg-action-light-active": + mode === "system", + "active:bg-action-light-active dark:active:bg-action-dark-active": + mode === "alt-system", + }); + } + if (variant === "secondary") { + return clsx("active:text-copy-light-active", { + "active:bg-action-dark-active": mode === "light", + "active:bg-action-light-active": mode === "dark", + "active:bg-action-dark-active dark:active:bg-action-light-active": + mode === "alt-system", + "active:bg-action-light-active dark:active:bg-action-dark-active": + mode === "system", + }); + } + if (variant === "danger") { + return clsx("active:text-copy-lighter-active", { + "active:bg-action-danger-dark-active": mode === "dark", + "active:bg-action-danger-light-active": mode === "light", + "active:bg-action-danger-dark-active dark:active:bg-action-danger-light-active": + mode === "system", + "active:bg-action-danger-light-active dark:active:bg-action-danger-dark-active": + mode === "alt-system", + }); + } }; const getButtonBorderClasses = ({ mode, noBorder, + variant, }: { - mode: string; + mode: "dark" | "light" | "system" | "alt-system"; noBorder: boolean; + variant: "primary" | "secondary" | "danger"; }) => { - return clsx("border", { - "border-border-dark": !noBorder && mode === "dark", - "border-border-light": !noBorder && mode === "light", - "border-border-dark dark:border-border-light": - !noBorder && mode === "system", - "border-border-light dark:border-border-dark": - !noBorder && mode === "alt-system", - "border-transparent": noBorder, - }); + if (noBorder) { + return "border border-transparent"; + } + if (variant === "primary") { + return clsx("border", { + "border-border-dark": mode === "dark", + "border-border-light": mode === "light", + "border-border-dark dark:border-border-light": mode === "system", + "border-border-light dark:border-border-dark": mode === "alt-system", + }); + } + if (variant === "secondary") { + return clsx("border", { + "border-border-dark": mode === "light", + "border-border-light": mode === "dark", + "border-border-dark dark:border-border-light": mode === "alt-system", + "border-border-light dark:border-border-dark": mode === "system", + }); + } + if (variant === "danger") { + return clsx("border", { + "border-border-danger-dark": mode === "dark", + "border-border-danger-medium": mode === "light", + "border-border-danger-dark dark:border-border-danger-medium": + mode === "system", + "border-border-danger-medium dark:border-border-danger-dark": + mode === "alt-system", + }); + } }; const getButtonFocusClasses = ({ focusMode }: { focusMode: string }) => { @@ -170,19 +271,23 @@ export const getButtonClasses = ({ labelLeft, spacing, noBackground, + variant, }: getButtonClassesProps) => { + if (!variant) { + variant = "primary"; + } return raw ? clsx(BUTTON_CLASSNAME, className) : clsx( BUTTON_CLASSNAME, className, getSpacing(spacing), - getButtonBaseClasses({ mode, noBackground }), + getButtonBaseClasses({ mode, variant, noBackground }), getButtonSizesClasses({ type, size, labelRight, labelLeft }), - getButtonBorderClasses({ mode, noBorder }), + getButtonBorderClasses({ mode, variant, noBorder }), getButtonFocusClasses({ focusMode }), - getButtonHoverClasses({ mode, disabled }), - getButtonActiveClasses({ mode, disabled }), + getButtonHoverClasses({ mode, variant, disabled }), + getButtonActiveClasses({ mode, variant, disabled }), { "w-full": fullWidth, "disabled:cursor-not-allowed disabled:opacity-50": disabled, diff --git a/packages/ui-styles/src/plugins/tailwindcss/tokens.ts b/packages/ui-styles/src/plugins/tailwindcss/tokens.ts index 4f6343c0..4eaf8b04 100644 --- a/packages/ui-styles/src/plugins/tailwindcss/tokens.ts +++ b/packages/ui-styles/src/plugins/tailwindcss/tokens.ts @@ -15,6 +15,14 @@ export default { "action-light-hover": colors.slate[600], "action-light-active": colors.slate[700], + "action-danger-dark": colors.red[900], + "action-danger-dark-hover": colors.red[700], + "action-danger-dark-active": colors.red[600], + + "action-danger-light": colors.red[600], + "action-danger-light-hover": colors.red[700], + "action-danger-light-active": colors.red[800], + /** * Surface tokens. */ @@ -63,6 +71,10 @@ export default { "border-medium": colors.slate[400], "border-light": colors.slate[300], + "border-danger-dark": colors.red[900], + "border-danger-medium": colors.red[400], + "border-danger-light": colors.red[300], + "border-white": "#ffffff", "border-error-dark": colors.red[700], "border-error-light": errorColorLight,