Skip to content

Commit

Permalink
feat(link): add Link component; change Button to always use button tag
Browse files Browse the repository at this point in the history
  • Loading branch information
Diedra authored and dierat committed Sep 8, 2021
1 parent 8c1d0d4 commit 32b587a
Show file tree
Hide file tree
Showing 10 changed files with 482 additions and 3 deletions.
2 changes: 2 additions & 0 deletions packages/components/src/Button/button.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ type Args = React.ComponentProps<typeof Button>;

const Template: Story<Args> = (args) => <Button {...args} />;

// TODO: Consolidate with Link stories.

export const Primary = Template.bind(null);
Primary.args = {
children: "Button",
Expand Down
19 changes: 19 additions & 0 deletions packages/components/src/Link/Link.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { generateSnapshots, prepareStory } from "@chanzuckerberg/story-utils";
import { render, screen } from "@testing-library/react";
import React from "react";
import * as LinkStoryFile from "./Link.stories";

describe("<Link />", () => {
generateSnapshots(LinkStoryFile);

it("forwards refs", () => {
const ref = React.createRef<HTMLAnchorElement>();
render(prepareStory(LinkStoryFile.Primary, { ref }));

// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
ref.current!.focus();

const Link = screen.getByRole("link");
expect(Link).toHaveFocus();
});
});
7 changes: 7 additions & 0 deletions packages/components/src/Link/Link.stories.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.color {
@apply p-2;
}

.variant {
@apply mb-5;
}
242 changes: 242 additions & 0 deletions packages/components/src/Link/Link.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
import { Story } from "@storybook/react/types-6-0";
import React from "react";
import Clickable from "../Clickable";
import Heading from "../Heading";
import CheckCircleRoundedIcon from "../Icons/CheckCircleRounded";
import Text from "../Text";
import Link, { LinkProps } from "./Link";
import styles from "./Link.stories.module.css";

const sizes = ["small", "medium", "large"] as const;
const allColors = ["alert", "brand", "neutral", "success", "warning"] as const;
const variants = ["flat", "outline", "link"] as const;
const states = ["inactive", "hover", "focus", "active", "disabled"] as const;

// For now, the UI kit only includes alert & brand "flat" buttons.
const flatColors = ["alert", "brand"] as const;

export default {
title: "Link",
component: Link,
argTypes: {
children: {
control: {
type: "text",
},
},
color: {
control: {
type: "radio",
options: allColors,
},
},
},
};

type Args = React.ComponentProps<typeof Link>;

const Template: Story<Args> = (args) => <Link {...args} />;

// TODO: Consolidate with button stories.

const defaultArgs = {
children: "Link",
variant: "flat" as const,
size: "medium" as const,
color: "brand" as const,
href: "",
};

export const Primary = Template.bind(null);
Primary.args = {
...defaultArgs,
};

export const Secondary = Template.bind(null);
Secondary.args = {
...defaultArgs,
variant: "outline",
};

export const Tertiary = Template.bind(null);
Tertiary.args = {
...defaultArgs,
color: "neutral",
variant: "outline",
};

export const Destructive = Template.bind(null);
Destructive.args = {
...defaultArgs,
color: "alert",
};

export const PrimarySmall = Template.bind(null);
PrimarySmall.args = {
...defaultArgs,
size: "small",
};

export const SecondarySmall = Template.bind(null);
SecondarySmall.args = {
...defaultArgs,
size: "small",
variant: "outline",
};

export const TertiarySmall = Template.bind(null);
TertiarySmall.args = {
...defaultArgs,
color: "neutral",
size: "small",
variant: "outline",
};

export const LinkButton = Template.bind(null);
LinkButton.args = {
...defaultArgs,
children: "Link Button",
variant: "link",
};

export const linkInBody: Story<Args> = (args) => (
<Text size="body">
This text surrounds the <Link {...args} /> and shows that the link should
adhere to its appearance
</Text>
);
linkInBody.args = {
...defaultArgs,
children: "Link Button",
variant: "link",
};

export const linkInHeading: Story<Args> = (args) => (
<Text size="h1">
This text surrounds the <Link {...args} /> and shows that the link should
adhere to its appearance
</Text>
);
linkInHeading.args = {
...defaultArgs,
children: "Link Button",
variant: "link",
};

export const withDataTestId = Template.bind(null);
withDataTestId.args = {
...defaultArgs,
children: "Link with data-testid",
color: "alert",
"data-testid": "fake-test-id",
};

export const linkWithIcon = Template.bind(null);
linkWithIcon.args = {
...defaultArgs,
children: (
<>
Link with icon <CheckCircleRoundedIcon purpose="decorative" />
</>
),
color: "success",
variant: "link",
};

export const outlineWithIcon = Template.bind(null);
outlineWithIcon.args = {
...defaultArgs,
children: (
<>
Outline with icon <CheckCircleRoundedIcon purpose="decorative" />
</>
),
color: "warning",
variant: "outline",
};

export const withFakeClassName = Template.bind(null);
withFakeClassName.args = {
...defaultArgs,
children: "With fake className",
color: "warning",
variant: "outline",
className: "fake-className",
};

// Show grids with all variants

const gridParameters = {
axe: {
skip: true,
},
snapshot: {
skip: true,
},
};

const renderSize = (
size: LinkProps["size"],
textColor: "white" | "neutral",
children: React.ReactNode,
) =>
variants.map((variant) => {
const colors = variant === "flat" ? flatColors : allColors;

return (
<React.Fragment key={variant}>
<Heading size="h2" color={textColor}>
{variant} - {size}
</Heading>
<table className={styles.variant}>
<tbody>
{states.map((state) => (
<tr key={state}>
<th scope="row">
<Text size="body" color={textColor}>
{state}
</Text>
</th>
{colors.map((color) => (
<td key={color} className={styles.color}>
{/* To pass the "state" prop (only used for demonstration in storybook),
we must use Clickable instead of Link */}
<Clickable
as={"a"}
size={size}
color={color}
variant={variant}
state={state}
href=""
>
{children}
</Clickable>
</td>
))}
</tr>
))}
</tbody>
</table>
</React.Fragment>
);
});

export const allVariants = () => (
<ul>
{sizes.map((size) => (
<li key={size}>{renderSize(size, "neutral", "Link")}</li>
))}
</ul>
);

allVariants.parameters = gridParameters;

export const mediumVariantsOnDarkBackground = () =>
renderSize("medium", "white", "Link");

mediumVariantsOnDarkBackground.parameters = {
...gridParameters,
backgrounds: {
default: "dark",
},
};
31 changes: 31 additions & 0 deletions packages/components/src/Link/Link.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React, { ReactNode, forwardRef } from "react";
import Clickable, { ClickableProps } from "../Clickable";

type LinkHTMLElementProps = Omit<
React.AnchorHTMLAttributes<HTMLAnchorElement>,
"disabled"
>;

export type LinkProps = LinkHTMLElementProps & {
as?: "a" | React.ComponentType<any>;
/**
* The link contents or label.
*/
children: ReactNode;
color?: ClickableProps<"button">["color"];
"data-testid"?: string;
size?: ClickableProps<"button">["size"];
variant?: ClickableProps<"button">["variant"];
};

const Link = forwardRef<HTMLAnchorElement, LinkProps>(
({ as = "a", variant = "link", color = "brand", ...rest }, ref) => {
return (
<Clickable {...rest} as={as} variant={variant} color={color} ref={ref} />
);
},
);

Link.displayName = "Link";

export default Link;
Loading

0 comments on commit 32b587a

Please sign in to comment.