diff --git a/packages/react-components/src/components/Link/Link.css b/packages/react-components/src/components/Link/Link.css index e69de29b..e94324be 100644 --- a/packages/react-components/src/components/Link/Link.css +++ b/packages/react-components/src/components/Link/Link.css @@ -0,0 +1,52 @@ +.bcds-react-aria-Link { + color: var(--typography-color-link); + text-decoration: underline; + text-underline-offset: var(--layout-padding-hair); + cursor: pointer; +} + +/* Sizing */ +.bcds-react-aria-Link.small { + font: var(--typography-regular-small-body); +} + +.bcds-react-aria-Link.medium { + font: var(--typography-regular-body); +} + +.bcds-react-aria-Link.large { + font: var(--typography-regular-large-body); +} + +/* Placeholder large button style */ +.bcds-react-aria-Button.large { + min-height: 40px; +} + +/* Hover */ +.bcds-react-aria-Link[data-hovered] { + color: var(--surface-color-border-active); +} + +.bcds-react-aria-Link[data-hovered].danger { + color: var(--surface-color-primary-danger-button-hover); +} + +/* Focus */ +.bcds-react-aria-Link[data-focus-visible] { + outline: solid var(--layout-border-width-medium) + var(--surface-color-border-active); + outline-offset: var(--layout-margin-hair); + border-radius: var(--layout-border-radius-small); +} + +/* Disabled */ +.bcds-react-aria-Link[data-disabled] { + color: var(--typography-color-disabled); + cursor: not-allowed; +} + +/* Danger */ +.bcds-react-aria-Link.danger { + color: var(--typography-color-danger); +} diff --git a/packages/react-components/src/components/Link/Link.tsx b/packages/react-components/src/components/Link/Link.tsx index 659f324a..342961c4 100644 --- a/packages/react-components/src/components/Link/Link.tsx +++ b/packages/react-components/src/components/Link/Link.tsx @@ -4,23 +4,44 @@ import { } from "react-aria-components"; import "./Link.css"; +import "../Button/Button.css"; export interface LinkProps extends ReactAriaLinkProps { /* Text size */ size?: "small" | "medium" | "large"; + /* Toggles link to use Button styles */ + isButton?: boolean; + /* Sets which Button style is applied */ + buttonVariant?: "primary" | "secondary" | "tertiary"; /* ARIA label */ ariaLabel?: string | undefined; + /* Red colourway for danger/error states */ + danger?: boolean; + /* Override all styling, let link inherit styles from parent */ + isUnstyled?: boolean; } export default function Link({ children, size = "medium", + danger = false, + isButton = false, + buttonVariant = "primary", + isUnstyled = false, ariaLabel, ...props }: LinkProps) { return ( diff --git a/packages/react-components/src/stories/Link.mdx b/packages/react-components/src/stories/Link.mdx index 5f77a64a..90e0a59a 100644 --- a/packages/react-components/src/stories/Link.mdx +++ b/packages/react-components/src/stories/Link.mdx @@ -48,9 +48,16 @@ If a link has an `href`, it renders as an `` element. Otherwise, it will rend Consult the React Aria documentation for [more information about events and routing](https://react-spectrum.adobe.com/react-aria/Link.html#events). -### Styling +## Text links -Link ships with styling based on the [B.C. Design System typescale](https://www2.gov.bc.ca/gov/content?id=72C2CD6E05494C84B9A072DD9C6A5342). Pass a CSS class with the `className` prop to override a link's styles with your own. +### Overriding link styles + +Link ships with styling based on the [B.C. Design System typescale](https://www2.gov.bc.ca/gov/content?id=72C2CD6E05494C84B9A072DD9C6A5342). To override this styling, you can: + +- Pass your own CSS class with the `className` prop +- Pass the `isUnstyled` prop to remove all CSS, allowing a link to inherit its styles from its parent elements + + ### Size @@ -62,22 +69,32 @@ Use the `size` prop to choose between `small`, `medium` and `large` sizes: If no `size` prop is passed, it will default to `medium`. -Links will match the correct heading style from the design system typescale if enclosed in an element like `

`: - - - -### Link icons - -You can pass SVG graphics into the `children` slot to add icons to a link: +### Destructive links - - - +Use the `danger` prop to indicate that a link is destructive: -**Note**: when using an icon-only link, you must also pass an accessible label for assistive technologies using the `ariaLabel` prop. + ### Disabled links Pass `isDisabled` to disable a link. A disabled link cannot be focused or interacted with: + +## Button links + +Use the `isButton` prop to create a link that looks and behaves like the [Button](/docs/components-button-button--docs) component. + +The `buttonVariant` prop to choose between the `primary`, `secondary` and `tertiary` button styles: + + + + + +If no `buttonVariant` prop is passed, it will default to `primary`. If `isButton` isn't set, `buttonVariant` won't do anything. + +You can use the `isDisabled`, `danger` and `size` props as normal: + + + + diff --git a/packages/react-components/src/stories/Link.stories.tsx b/packages/react-components/src/stories/Link.stories.tsx index 164f6baa..e63262ec 100644 --- a/packages/react-components/src/stories/Link.stories.tsx +++ b/packages/react-components/src/stories/Link.stories.tsx @@ -1,7 +1,7 @@ import type { Meta, StoryObj } from "@storybook/react"; import * as tokens from "@bcgov/design-tokens/js"; -import { Link, SvgInfoIcon } from "../components"; +import { Link } from "../components"; import { LinkProps } from "@/components/Link"; const meta = { @@ -13,22 +13,42 @@ const meta = { control: { type: "object" }, description: "Populates link text", }, + href: { + control: { type: "text" }, + description: "Destination URL", + }, + onPress: { + control: { type: "object" }, + description: "Callback function run on press. Use instead of `href`", + }, size: { options: ["small", "medium", "large"], control: { type: "radio" }, description: "Sets text size", }, + isButton: { + control: { type: "boolean" }, + description: "Applies button styling", + }, + buttonVariant: { + options: ["primary", "secondary", "tertiary"], + control: { type: "radio" }, + description: + "Selects which button style is used. Requires `isButton` to be `true`", + }, isDisabled: { control: { type: "boolean" }, description: "Whether a link is enabled or disabled", }, - href: { - control: { type: "text" }, - description: "Destination URL", + isUnstyled: { + control: { type: "boolean" }, + description: + "Overrides all styling, allowing link to inherit styling from its parent", }, - onPress: { - control: { type: "object" }, - description: "Callback function run on press. Use instead of `href`", + ariaLabel: { + control: { type: "text" }, + description: + "Sets aria-label attribute, use if not providing a visible text label", }, }, } satisfies Meta; @@ -73,6 +93,15 @@ export const LargeLink: Story = { }, }; +export const DangerLink: Story = { + ...LinkTemplate, + args: { + danger: true, + children: ["This link is destructive"], + onPress: () => alert("onPress()"), + }, +}; + export const DisabledLink: Story = { ...LinkTemplate, args: { @@ -82,38 +111,72 @@ export const DisabledLink: Story = { }, }; -export const LinkWithLeftIcon: Story = { - ...LinkTemplate, +export const LinkInHeading: Story = { args: { - size: "small", - children: [, "This link has an icon"], + children: ["This link"], + href: "#", + isUnstyled: true, + }, + render: ({ ...args }: LinkProps) => ( +

+ is part of an H2 heading +

+ ), +}; + +export const PrimaryLinkButton: Story = { + args: { + children: ["This is a link button"], + isButton: true, + buttonVariant: "primary", onPress: () => alert("onPress()"), }, }; -export const LinkWithRightIcon: Story = { - ...LinkTemplate, +export const SecondaryLinkButton: Story = { args: { - size: "small", - children: ["This link has an icon", ], + children: ["This is a link button"], + isButton: true, + buttonVariant: "secondary", onPress: () => alert("onPress()"), }, }; -export const IconOnlyLink: Story = { - ...LinkTemplate, +export const TertiaryLinkButton: Story = { args: { - children: [], - ariaLabel: "Information", + children: ["This is a link button"], + isButton: true, + buttonVariant: "tertiary", onPress: () => alert("onPress()"), }, }; -export const LinkInHeading: Story = { - args: { children: ["This link"], onPress: () => alert("onPress()") }, - render: ({ ...args }: LinkProps) => ( -

- is part of an H2 heading -

- ), +export const DisabledLinkButton: Story = { + args: { + children: ["This link button is disabled"], + isButton: true, + buttonVariant: "primary", + isDisabled: true, + onPress: () => alert("onPress()"), + }, +}; + +export const DangerLinkButton: Story = { + args: { + children: ["This is a destructive link button"], + isButton: true, + buttonVariant: "primary", + danger: true, + onPress: () => alert("onPress()"), + }, +}; + +export const SmallLinkButton: Story = { + args: { + children: ["This is a small link button"], + isButton: true, + buttonVariant: "primary", + size: "small", + onPress: () => alert("onPress()"), + }, };