Skip to content

Commit

Permalink
add link styling and support for link button format
Browse files Browse the repository at this point in the history
  • Loading branch information
mkernohanbc committed Dec 9, 2024
1 parent 6083c0d commit 3712e63
Show file tree
Hide file tree
Showing 4 changed files with 193 additions and 40 deletions.
52 changes: 52 additions & 0 deletions packages/react-components/src/components/Link/Link.css
Original file line number Diff line number Diff line change
@@ -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);
}
23 changes: 22 additions & 1 deletion packages/react-components/src/components/Link/Link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<ReactAriaLink
className={`bcds-react-aria-Link ${size}`}
className={
!isUnstyled
? isButton
? `bcds-react-aria-Button ${size} ${buttonVariant} ${
danger && "danger"
}`
: `bcds-react-aria-Link ${size} ${danger && "danger"}`
: undefined
}
aria-label={ariaLabel}
{...props}
>
Expand Down
43 changes: 30 additions & 13 deletions packages/react-components/src/stories/Link.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,16 @@ If a link has an `href`, it renders as an `<a>` 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

<Canvas of={LinkStories.LinkInHeading} />

### Size

Expand All @@ -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 `<h2>`:

<Canvas of={LinkStories.LinkInHeading} />

### Link icons

You can pass SVG graphics into the `children` slot to add icons to a link:
### Destructive links

<Canvas of={LinkStories.LinkWithLeftIcon} />
<Canvas of={LinkStories.LinkWithRightIcon} />
<Canvas of={LinkStories.IconOnlyLink} />
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.
<Canvas of={LinkStories.DangerLink} />

### Disabled links

Pass `isDisabled` to disable a link. A disabled link cannot be focused or interacted with:

<Canvas of={LinkStories.DisabledLink} />

## 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:

<Canvas of={LinkStories.PrimaryLinkButton} />
<Canvas of={LinkStories.SecondaryLinkButton} />
<Canvas of={LinkStories.TertiaryLinkButton} />

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:

<Canvas of={LinkStories.DisabledLinkButton} />
<Canvas of={LinkStories.DangerLinkButton} />
<Canvas of={LinkStories.SmallLinkButton} />
115 changes: 89 additions & 26 deletions packages/react-components/src/stories/Link.stories.tsx
Original file line number Diff line number Diff line change
@@ -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 = {
Expand All @@ -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<typeof Link>;
Expand Down Expand Up @@ -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: {
Expand All @@ -82,38 +111,72 @@ export const DisabledLink: Story = {
},
};

export const LinkWithLeftIcon: Story = {
...LinkTemplate,
export const LinkInHeading: Story = {
args: {
size: "small",
children: [<SvgInfoIcon />, "This link has an icon"],
children: ["This link"],
href: "#",
isUnstyled: true,
},
render: ({ ...args }: LinkProps) => (
<h2 style={{ font: tokens.typographyBoldH2 }}>
<Link {...args} /> is part of an H2 heading
</h2>
),
};

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", <SvgInfoIcon />],
children: ["This is a link button"],
isButton: true,
buttonVariant: "secondary",
onPress: () => alert("onPress()"),
},
};

export const IconOnlyLink: Story = {
...LinkTemplate,
export const TertiaryLinkButton: Story = {
args: {
children: [<SvgInfoIcon />],
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) => (
<h2 style={{ font: tokens.typographyBoldH2 }}>
<Link {...args} /> is part of an H2 heading
</h2>
),
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()"),
},
};

0 comments on commit 3712e63

Please sign in to comment.