From b08276b232687ecf34d8b9b44e04c4fd8f59bef7 Mon Sep 17 00:00:00 2001 From: Jared White Date: Fri, 8 Sep 2023 14:32:44 -0700 Subject: [PATCH] feat: add Link component (#48) * feat: add skeleton code for Button component * feat: add more options to Button * feat: continue building out Button * feat: add lots of Button styles * feat: add additional variants of Button * feat: additional styling fixes and code comments * docs: add docs for Button * chore: add Aria functionality to the Button * test: add tests for Button * chore: add Button to index exports * chore: tweak docs and remove router interface * fix: button prop typo * chore: update Button component to use `--seeds` nomenclature * test: use the right icon class in Button test * chore: update Button with more prefix fixes * chore: fix icon sizing * feat: provide more options for external link icons * chore: refactor Button internals, restyle hovers * feat: add Link component * chore: tweak Highlight color scheme * fix: use `button-` prefix on component tokens * fix: use `link-` prefix on component tokens * chore: remove unneeded `disabled` prop * test: add unit test for Link * chore: add Link export * chore: make `href` attribute of Link required * chore: add screen reader text for external links * test: resolve link role testing * chore: make blank target options for Links and Buttons --- index.ts | 1 + src/actions/Button.tsx | 18 ++++- src/actions/Link.scss | 19 +++++ src/actions/Link.tsx | 82 ++++++++++++++++++++++ src/actions/__stories__/Button.stories.tsx | 23 +++--- src/actions/__stories__/Link.docs.mdx | 15 ++++ src/actions/__stories__/Link.stories.tsx | 47 +++++++++++++ src/actions/__tests__/Button.test.tsx | 35 ++++----- src/actions/__tests__/Link.test.tsx | 61 ++++++++++++++++ src/global/NavigationContext.tsx | 4 ++ src/global/app-css.scss | 11 +++ 11 files changed, 289 insertions(+), 27 deletions(-) create mode 100644 src/actions/Link.scss create mode 100644 src/actions/Link.tsx create mode 100644 src/actions/__stories__/Link.docs.mdx create mode 100644 src/actions/__stories__/Link.stories.tsx create mode 100644 src/actions/__tests__/Link.test.tsx diff --git a/index.ts b/index.ts index 6bcc8c2..efdd0bb 100644 --- a/index.ts +++ b/index.ts @@ -1,6 +1,7 @@ export * from "./src/expandableText/ExpandableText" export { default as Button } from "./src/actions/Button" +export { default as Link } from "./src/actions/Link" export { default as Alert } from "./src/blocks/Alert" export { default as Message } from "./src/blocks/Message" export { default as Toast } from "./src/blocks/Toast" diff --git a/src/actions/Button.tsx b/src/actions/Button.tsx index 0eaa352..d3ef3ea 100644 --- a/src/actions/Button.tsx +++ b/src/actions/Button.tsx @@ -1,6 +1,7 @@ import React, { useContext } from "react" import { NavigationContext, + ExternalLinkScreenReaderText, isExternalLink, shouldShowExternalLinkIcon, } from "../global/NavigationContext" @@ -36,6 +37,8 @@ export interface ButtonProps { onClick?: (e: React.MouseEvent) => void /** Use an `` tag instead of ` @@ -40,7 +40,7 @@ export const buttonVariants = () => ( Disabled Button -
+
@@ -57,8 +57,8 @@ export const buttonVariants = () => ( Disabled Button
- -
+ +
@@ -75,7 +75,7 @@ export const buttonVariants = () => ( Disabled Button
-
+
@@ -92,8 +92,8 @@ export const buttonVariants = () => ( Disabled Button
- -
+ +
@@ -134,7 +134,12 @@ export const buttonVariants = () => ( export const linkButtons = () => (
- + +
) diff --git a/src/actions/__stories__/Link.docs.mdx b/src/actions/__stories__/Link.docs.mdx new file mode 100644 index 0000000..50f3329 --- /dev/null +++ b/src/actions/__stories__/Link.docs.mdx @@ -0,0 +1,15 @@ +import { ArgsTable } from "@storybook/addon-docs" +import Link from "../Link" + +# <Link /> + +## Properties + + + +## Theme Variables + +| Name | Description | Default | +| ----------------------------- | -------------------------- | ------------ | +| `--link-interior-gap` | Space between icons/text | `--seeds-s1` | +| `--link-icon-size-percentage` | Relative size to base font | `75%` | diff --git a/src/actions/__stories__/Link.stories.tsx b/src/actions/__stories__/Link.stories.tsx new file mode 100644 index 0000000..8623508 --- /dev/null +++ b/src/actions/__stories__/Link.stories.tsx @@ -0,0 +1,47 @@ +import React from "react" + +import Link from "../Link" + +import Icon from "../../icons/Icon" +import { faHeart } from "@fortawesome/free-solid-svg-icons" + +import MDXDocs from "./Link.docs.mdx" + +export default { + title: "Actions/Link", + component: Link, + parameters: { + docs: { + page: MDXDocs, + }, + }, +} + +export const basicLink = () => ( +

+ Hello world. This is a link. +

+) + +export const externalLink = () => ( +
+ Internal Link + + External Link (Same Window) + + + External Link (New Window) + +
+) + +export const linksWithIcons = () => ( +
+ }> + Lead Icon + + }> + Tail Icon + +
+) diff --git a/src/actions/__tests__/Button.test.tsx b/src/actions/__tests__/Button.test.tsx index d9150ee..38202c0 100644 --- a/src/actions/__tests__/Button.test.tsx +++ b/src/actions/__tests__/Button.test.tsx @@ -29,18 +29,19 @@ describe(") - expect(getByRole("link", { name: content })).toHaveAttribute( - "href", - "/my-page" - ) + expect(getByRole("link", { name: content })).toHaveAttribute("href", "/my-page") expect(container.querySelector("svg")).toBeNull() }) it("displays external links with an icon", () => { const content = "Button Label" - const { getByRole, container } = render() + const { getByRole, container } = render( + + ) - expect(getByRole("link", { name: content })).toHaveAttribute( + expect(getByRole("link", { name: `${content} (opens in a new tab)` })).toHaveAttribute( "href", "https://example.com" ) @@ -49,23 +50,25 @@ describe(") - - expect(getByRole("link", { name: content })).toHaveAttribute( - "href", - "https://example.com" + const { getByRole, container } = render( + ) + + expect(getByRole("link", { name: content })).toHaveAttribute("href", "https://example.com") expect(container.querySelector("svg[data-icon='heart']")).toBeVisible() }) it("displays external links with no icon", () => { const content = "Button Label" - const { getByRole, container } = render() - - expect(getByRole("link", { name: content })).toHaveAttribute( - "href", - "https://example.com" + const { getByRole, container } = render( + ) + + expect(getByRole("link", { name: content })).toHaveAttribute("href", "https://example.com") expect(container.querySelector("svg")).toBeNull() }) }) diff --git a/src/actions/__tests__/Link.test.tsx b/src/actions/__tests__/Link.test.tsx new file mode 100644 index 0000000..e593e91 --- /dev/null +++ b/src/actions/__tests__/Link.test.tsx @@ -0,0 +1,61 @@ +import { render, cleanup } from "@testing-library/react" +import Link from "../Link" + +import Icon from "../../icons/Icon" +import { faHeart } from "@fortawesome/free-solid-svg-icons" + +afterEach(cleanup) + +describe("", () => { + it("displays a link with a URL", () => { + const content = "Link Label" + const { getByRole, getByText, container } = render( + + {content} + + ) + expect(getByText(content)).toBeInTheDocument() + expect(container.querySelector("#test-link.test-class")).not.toBeNull() + expect(getByRole("link", { name: content })).toHaveAttribute("href", "/my-page") + expect(container.querySelector("svg")).toBeNull() + }) + + it("displays external links with an icon", () => { + const content = "Link Label" + const { getByRole, container } = render( + + {content} + + ) + + expect(getByRole("link", { name: `${content} (opens in a new tab)` })).toHaveAttribute( + "href", + "https://example.com" + ) + expect(container.querySelector("svg[data-icon='arrow-up-right-from-square']")).toBeVisible() + }) + + it("displays external links with a custom icon", () => { + const content = "Button Label" + const { getByRole, container } = render( + }> + {content} + + ) + + expect(getByRole("link", { name: content })).toHaveAttribute("href", "https://example.com") + expect(container.querySelector("svg[data-icon='heart']")).toBeVisible() + }) + + it("displays external links with no icon", () => { + const content = "Button Label" + const { getByRole, container } = render( + + {content} + + ) + + expect(getByRole("link", { name: content })).toHaveAttribute("href", "https://example.com") + expect(container.querySelector("svg")).toBeNull() + }) +}) diff --git a/src/global/NavigationContext.tsx b/src/global/NavigationContext.tsx index 528b6b4..6924415 100644 --- a/src/global/NavigationContext.tsx +++ b/src/global/NavigationContext.tsx @@ -25,6 +25,10 @@ export const shouldShowExternalLinkIcon = ({ return href && !tailIcon && isExternalLink(href) && !hideExternalLinkIcon } +export const ExternalLinkScreenReaderText = () => ( + (opens in a new tab) +) + type DefaultLinkProps = DetailedHTMLProps< AnchorHTMLAttributes, HTMLAnchorElement diff --git a/src/global/app-css.scss b/src/global/app-css.scss index e2768ff..1d67f3f 100644 --- a/src/global/app-css.scss +++ b/src/global/app-css.scss @@ -77,4 +77,15 @@ body { line-height: inherit; } +.seeds-screen-reader-only { + position: absolute; + overflow: hidden; + clip: rect(1px, 1px, 1px, 1px); + clip-path: inset(50%); + height: 1px; + width: 1px; + padding: 0; + margin: -1px; +} + @import "text.scss";