From 750b63184dfc2569fe4db4676bdd30fe5ac3ed63 Mon Sep 17 00:00:00 2001 From: Jamie Henson Date: Thu, 12 Dec 2024 12:31:43 +0000 Subject: [PATCH] feat: add LinkButton component, update FeaturedLink usage in PricingCards --- src/core/Button.tsx | 46 +++++++++++----- src/core/LinkButton.tsx | 54 +++++++++++++++++++ src/core/LinkButton/LinkButton.stories.tsx | 40 ++++++++++++++ .../__snapshots__/LinkButton.stories.tsx.snap | 31 +++++++++++ src/core/Pricing/PricingCards.tsx | 16 +++--- src/core/Pricing/data.tsx | 1 - src/core/styles/buttons.css | 14 +++-- 7 files changed, 178 insertions(+), 24 deletions(-) create mode 100644 src/core/LinkButton.tsx create mode 100644 src/core/LinkButton/LinkButton.stories.tsx create mode 100644 src/core/LinkButton/__snapshots__/LinkButton.stories.tsx.snap diff --git a/src/core/Button.tsx b/src/core/Button.tsx index 8a1073431..89dbbc278 100644 --- a/src/core/Button.tsx +++ b/src/core/Button.tsx @@ -7,7 +7,7 @@ export type ButtonType = "priority" | "primary" | "secondary"; type ButtonSize = "lg" | "md" | "sm" | "xs"; -type ButtonProps = { +export type ButtonPropsBase = { /** * The type of button: priority, primary, or secondary. */ @@ -28,7 +28,10 @@ type ButtonProps = { * Optional classes to add to the button element. */ className?: string; -} & React.ButtonHTMLAttributes; +}; + +type ButtonProps = ButtonPropsBase & + React.ButtonHTMLAttributes; // got to go the long way round because of ol' mate Taily Waily const buttonClasses: Record> = { @@ -62,6 +65,32 @@ export const iconModifierClasses: Record< xs: { left: "", right: "" }, }; +export const commonButtonProps = (props: ButtonPropsBase) => { + const { variant = "primary", size, leftIcon, rightIcon, className } = props; + + return { + className: cn( + buttonClasses[variant][size ?? "md"], + { [iconModifierClasses[size ?? "md"].left]: leftIcon }, + { [iconModifierClasses[size ?? "md"].right]: rightIcon }, + className, + ), + }; +}; + +export const commonButtonInterior = ( + props: PropsWithChildren, +) => { + const { leftIcon, rightIcon, children } = props; + return ( + <> + {leftIcon ? : null} + {children} + {rightIcon ? : null} + + ); +}; + const Button: React.FC> = ({ variant = "primary", size, @@ -73,17 +102,10 @@ const Button: React.FC> = ({ }) => { return ( ); }; diff --git a/src/core/LinkButton.tsx b/src/core/LinkButton.tsx new file mode 100644 index 000000000..bb54a90e0 --- /dev/null +++ b/src/core/LinkButton.tsx @@ -0,0 +1,54 @@ +import React from "react"; +import { + ButtonPropsBase, + commonButtonInterior, + commonButtonProps, +} from "./Button"; +import cn from "./utils/cn"; + +export type LinkButtonProps = ButtonPropsBase & { + disabled?: boolean; + onClick?: (event: React.MouseEvent) => void; +} & React.AnchorHTMLAttributes; + +const LinkButton: React.FC = ({ + variant = "primary", + size, + leftIcon, + rightIcon, + children, + className, + disabled, + onClick, + ...rest +}) => { + const handleClick = (e: React.MouseEvent) => { + if (disabled) { + e.preventDefault(); + return; + } + onClick?.(e); + }; + + return ( + )} + > + {commonButtonInterior({ leftIcon, rightIcon, children })} + + ); +}; + +export default LinkButton; diff --git a/src/core/LinkButton/LinkButton.stories.tsx b/src/core/LinkButton/LinkButton.stories.tsx new file mode 100644 index 000000000..9f5fd956f --- /dev/null +++ b/src/core/LinkButton/LinkButton.stories.tsx @@ -0,0 +1,40 @@ +import React from "react"; +import { Meta, StoryFn } from "@storybook/react"; +import LinkButton, { LinkButtonProps } from "../LinkButton"; + +export default { + title: "Components/Link Button", + component: LinkButton, + parameters: { + docs: { + description: { + component: + "A variant of the `Button` component that renders an `anchor` element instead of a `button` element, with all the typing constraints and props one would expect from an anchor.", + }, + }, + }, +} as Meta; + +const Template: StoryFn = (args) => ; + +export const Default = Template.bind({}); +Default.args = { + href: "#", + variant: "primary", + children: "Default Link Button", +}; + +export const External = Template.bind({}); +External.args = { + href: "https://www.ably.com", + children: "External Link Button", + target: "_blank", + rel: "noopener noreferrer", +}; + +export const Disabled = Template.bind({}); +Disabled.args = { + href: "#", + children: "Disabled Link Button", + disabled: true, +}; diff --git a/src/core/LinkButton/__snapshots__/LinkButton.stories.tsx.snap b/src/core/LinkButton/__snapshots__/LinkButton.stories.tsx.snap new file mode 100644 index 000000000..2c324e252 --- /dev/null +++ b/src/core/LinkButton/__snapshots__/LinkButton.stories.tsx.snap @@ -0,0 +1,31 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Components/Link Button Default smoke-test 1`] = ` + + Default Link Button + +`; + +exports[`Components/Link Button Disabled smoke-test 1`] = ` + + Disabled Link Button + +`; + +exports[`Components/Link Button External smoke-test 1`] = ` + + External Link Button + +`; diff --git a/src/core/Pricing/PricingCards.tsx b/src/core/Pricing/PricingCards.tsx index 128c63918..2b05d162b 100644 --- a/src/core/Pricing/PricingCards.tsx +++ b/src/core/Pricing/PricingCards.tsx @@ -3,6 +3,7 @@ import throttle from "lodash.throttle"; import type { PricingDataFeature } from "./types"; import Icon from "../Icon"; import FeaturedLink from "../FeaturedLink"; +import LinkButton from "../LinkButton"; import { IconName } from "../Icon/types"; import Tooltip from "../Tooltip"; import cn from "../utils/cn"; @@ -183,18 +184,17 @@ const PricingCards = ({ data, delimiter }: PricingCardsProps) => { {cta ? (
- {cta.text} - +
) : delimiter ? null : (
diff --git a/src/core/Pricing/data.tsx b/src/core/Pricing/data.tsx index d40152b7e..52d4940e4 100644 --- a/src/core/Pricing/data.tsx +++ b/src/core/Pricing/data.tsx @@ -160,7 +160,6 @@ export const planData: PricingDataFeature[] = [ cta: { text: "Contact us", url: "/contact", - className: "ui-btn-alt text-neutral-000", }, sections: [ { diff --git a/src/core/styles/buttons.css b/src/core/styles/buttons.css index 4a7f50ccc..44ea53918 100644 --- a/src/core/styles/buttons.css +++ b/src/core/styles/buttons.css @@ -1,6 +1,6 @@ @layer components { .ui-button-base { - @apply inline-flex rounded justify-center items-center gap-12 text-neutral-000 transition-colors focus-base disabled:cursor-not-allowed; + @apply inline-flex rounded justify-center items-center gap-12 text-neutral-000 transition-colors focus-base; } .ui-button-lg-base { @@ -20,11 +20,11 @@ } .ui-button-disabled-base { - @apply disabled:bg-gui-unavailable disabled:text-gui-unavailable-dark disabled:hover:bg-gui-unavailable; + @apply disabled:cursor-not-allowed disabled:bg-gui-unavailable disabled:text-gui-unavailable-dark disabled:hover:bg-gui-unavailable; } .ui-button-disabled-base-dark { - @apply disabled:bg-gui-unavailable-dark disabled:text-gui-unavailable disabled:hover:bg-gui-unavailable-dark; + @apply disabled:cursor-not-allowed disabled:bg-gui-unavailable-dark disabled:text-gui-unavailable disabled:hover:bg-gui-unavailable-dark; } .ui-button-priority-base { @@ -122,4 +122,12 @@ .ui-button-sm-right-icon { @apply pr-[18px]; } + + .ui-button-disabled { + @apply select-none pointer-events-none bg-gui-unavailable text-gui-unavailable-dark hover:bg-gui-unavailable; + } + + .ui-theme-dark .ui-button-disabled { + @apply select-none pointer-events-none bg-gui-unavailable-dark text-gui-unavailable hover:bg-gui-unavailable-dark; + } }