Skip to content

Commit

Permalink
feat: add Button component (#42)
Browse files Browse the repository at this point in the history
* 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

* chore: tweak Highlight color scheme

* fix: use `button-` prefix on component tokens
  • Loading branch information
jaredcwhite authored Jul 17, 2023
1 parent 6fe423b commit 2b09432
Show file tree
Hide file tree
Showing 9 changed files with 638 additions and 0 deletions.
1 change: 1 addition & 0 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ export { default as Toast } from "./src/blocks/Toast"
export { default as Card } from "./src/blocks/Card"
export { default as Grid } from "./src/layout/Grid"
export { default as Tabs } from "./src/navigation/Tabs"
export { default as Button } from "./src/actions/Button"
185 changes: 185 additions & 0 deletions src/actions/Button.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
.seeds-button {
--button-border-width: var(--seeds-border-2);
--button-font-family: var(--seeds-font-alt-sans);
--button-font-weight: bold;
--button-interior-gap: var(--seeds-s3);
--button-icon-size-percentage: 75%;
--button-icon-side-padding: var(--seeds-s4);

--button-padding-sm: calc(var(--seeds-s3_5) - var(--button-border-width))
calc(var(--seeds-s5) - var(--button-border-width));
--button-font-size-sm: var(--seeds-font-size-sm);
--button-border-radius-sm: var(--seeds-rounded);
--button-padding-md: calc(var(--seeds-s4) - var(--button-border-width))
calc(var(--seeds-s6) - var(--button-border-width));
--button-font-size-md: var(--seeds-font-size-base);
--button-border-radius-md: var(--seeds-rounded);
--button-padding-lg: calc(var(--seeds-s5) - var(--button-border-width))
calc(var(--seeds-s7) - var(--button-border-width));
--button-font-size-lg: var(--seeds-font-size-lg);
--button-border-radius-lg: var(--seeds-rounded);

all: initial;
display: inline-flex;
gap: var(--button-interior-gap);
padding: var(--button-padding-md);
border-style: solid;
border-width: var(--button-border-width);
border-radius: var(--button-border-radius-md);
font-family: var(--button-font-family);
font-weight: var(--button-font-weight);
font-size: var(--button-font-size-md);
line-height: var(--seeds-line-height-tight);
cursor: pointer;

&[data-size="sm"] {
padding: var(--button-padding-sm);
font-size: var(--button-font-size-sm);
border-radius: var(--button-border-radius-sm);
}

&[data-size="lg"] {
padding: var(--button-padding-lg);
font-size: var(--button-font-size-lg);
border-radius: var(--button-border-radius-lg);
}

&[data-variant="primary"] {
background-color: var(--seeds-color-primary);
border-color: var(--seeds-color-primary);
color: var(--seeds-color-on-inverse);

&:hover {
background-color: var(--seeds-color-primary-dark);
border-color: var(--seeds-color-primary-dark);
}
}

&[data-variant="primary-outlined"] {
background-color: var(--seeds-bg-color-surface);
border-color: var(--seeds-color-primary);
color: var(--seeds-color-primary);

&:hover {
background-color: var(--seeds-color-primary-dark);
border-color: var(--seeds-color-primary-dark);
color: var(--seeds-color-on-inverse);
}
}

&[data-variant="secondary"] {
background-color: var(--seeds-color-secondary);
border-color: var(--seeds-color-secondary);
color: var(--seeds-color-on-inverse);

&:hover {
background-color: var(--seeds-color-secondary-dark);
border-color: var(--seeds-color-secondary-dark);
}
}

&[data-variant="secondary-outlined"] {
background-color: var(--seeds-bg-color-surface);
border-color: var(--seeds-color-secondary);
color: var(--seeds-color-secondary);

&:hover {
background-color: var(--seeds-color-secondary-dark);
border-color: var(--seeds-color-secondary-dark);
color: var(--seeds-color-on-inverse);
}
}

&[data-variant="alert"] {
background-color: var(--seeds-color-alert);
border-color: var(--seeds-color-alert);
color: var(--seeds-color-on-inverse);

&:hover {
background-color: var(--seeds-color-alert-dark);
border-color: var(--seeds-color-alert-dark);
}
}

&[data-variant="alert-outlined"] {
background-color: var(--seeds-bg-color-surface);
border-color: var(--seeds-color-alert);
color: var(--seeds-color-alert);

&:hover {
background-color: var(--seeds-color-alert-dark);
border-color: var(--seeds-color-alert-dark);
color: var(--seeds-color-on-inverse);
}
}

&[data-variant="highlight"] {
background-color: var(--seeds-color-highlight);
border-color: var(--seeds-color-highlight);
color: var(--seeds-color-secondary-darker);

&:hover {
background-color: var(--seeds-color-highlight-dark);
border-color: var(--seeds-color-highlight-dark);
}
}

&[data-variant="highlight-outlined"] {
background-color: var(--seeds-bg-color-surface);
border-color: var(--seeds-color-highlight);
color: var(--seeds-color-secondary-darker);

&:hover {
background-color: var(--seeds-color-highlight-dark);
border-color: var(--seeds-color-highlight-dark);
}
}

&[data-variant="borderless-text"] {
background-color: transparent;
border-color: transparent;
color: var(--seeds-color-primary);
font-weight: normal;
text-decoration: underline;
gap: var(--seeds-s1);
}

&:not([data-variant$="-outlined"]) {
&[disabled],
&[disabled]:hover {
background-color: var(--seeds-color-gray-450);
border-color: var(--seeds-color-gray-450);
color: white;
}
}

&[data-variant$="-outlined"] {
&[disabled],
&[disabled]:hover {
background-color: var(--seeds-color-bg-color-surface);
border-color: var(--seeds-color-gray-450);
color: var(--seeds-color-gray-450);
}
}

&.has-lead-icon {
padding-inline-start: var(--button-icon-side-padding);
}

&.has-tail-icon {
padding-inline-end: var(--button-icon-side-padding);
}

& > * {
align-self: center;
}

& > .seeds-icon {
font-size: var(--button-icon-size-percentage);
}

&:focus-visible {
outline: var(--seeds-focus-ring-outline);
box-shadow: var(--seeds-focus-ring-box-shadow);
}
}
128 changes: 128 additions & 0 deletions src/actions/Button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import React, { useContext } from "react"
import {
NavigationContext,
isExternalLink,
shouldShowExternalLinkIcon,
} from "../global/NavigationContext"
import Icon from "../icons/Icon"

import { faArrowUpRightFromSquare } from "@fortawesome/free-solid-svg-icons"

import "./Button.scss"

export interface ButtonProps {
/** Button content */
children: React.ReactNode
/** Appearance of the component */
variant?:
| "primary"
| "primary-outlined"
| "secondary"
| "secondary-outlined"
| "alert"
| "alert-outlined"
| "highlight"
| "highlight-outlined"
| "borderless-text"
/** Button size */
size?: "sm" | "md" | "lg"
/** Icon to show before the label text */
leadIcon?: React.ReactNode
/** Icon to show after the label text */
tailIcon?: React.ReactNode
/** Set to true if you don't want external links to show a related icon */
hideExternalLinkIcon?: boolean
/** Event handler for the button click */
onClick?: (e: React.MouseEvent) => void
/** Use an `<a href>` tag instead of `<button>` for a hyperlink */
href?: string
/** HTML button type */
type?: "button" | "submit" | "reset"
/** Set to true to disable the button */
disabled?: boolean
/** Set to true to hide the button from the accessibility tree */
ariaHidden?: boolean
/** Accessible label if button doesn't contain text content */
ariaLabel?: string
/** Element ID */
id?: string
/** Additional CSS classes */
className?: string
}

const setupButtonProps = (props: ButtonProps) => {
const classNames = ["seeds-button"]

const tailIcon = shouldShowExternalLinkIcon(props) ? (
<Icon icon={faArrowUpRightFromSquare} />
) : (
props.tailIcon
)

if (props.className) classNames.push(props.className)
if (props.leadIcon) classNames.push("has-lead-icon")
if (tailIcon) classNames.push("has-tail-icon")

return {
updatedProps: {
"data-variant": props.variant || "primary",
"data-size": props.size,
id: props.id,
className: classNames.join(" "),
"aria-label": props.ariaLabel,
"aria-hidden": props.ariaHidden,
tabIndex: props.ariaHidden ? -1 : null,
},
inner: {
leadIcon: props.leadIcon,
tailIcon,
children: props.children,
},
}
}

const ButtonElement = (props: ButtonProps) => {
return <button {...props} />
}

const LinkButton = (props: ButtonProps) => {
if (props.href && isExternalLink(props.href)) {
return <a target="_blank" {...props} />
} else {
const { LinkComponent } = useContext(NavigationContext)
return <LinkComponent {...props} />
}
}

const Button = (props: ButtonProps) => {
const { updatedProps, inner } = setupButtonProps(props)

const buttonInner = (
<>
{inner.leadIcon}
{inner.children}
{inner.tailIcon}
</>
)

if (props.href) {
return (
<LinkButton href={props.href} {...updatedProps}>
{buttonInner}
</LinkButton>
)
} else {
return (
<ButtonElement
type={props.type || "button"}
disabled={props.disabled}
onClick={props.onClick}
{...updatedProps}
>
{buttonInner}
</ButtonElement>
)
}
}

export default Button
28 changes: 28 additions & 0 deletions src/actions/__stories__/Button.docs.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { ArgsTable } from "@storybook/addon-docs"
import Button from "../Button"

# &lt;Button /&gt;

## Properties

<ArgsTable of={Button} />

## Theme Variables

| Name | Description | Default |
| ------------------------------- | ----------------------------------------- | ----------------------- |
| `--button-border-width` | Border width | `--seeds-border-2` |
| `--button-font-family` | Font family | `--seeds-font-alt-sans` |
| `--button-font-weight` | Font weight | `none` |
| `--button-interior-gap` | Space between icons/text | `--seeds-s3` |
| `--button-icon-size-percentage` | Relative size to base font | `75%` |
| `--button-icon-side-padding` | Space between an icon and the button edge | `--seeds-s4` |
| `--button-padding-sm` | Small button padding | |
| `--button-font-size-sm` | Small button font size | `--seeds-font-size-sm` |
| `--button-border-radius-sm` | Small button border radius | `--seeds-rounded` |
| `--button-padding-md` | Medium button padding | |
| `--button-font-size-md` | Medium button font size | `--seeds-font-size-md` |
| `--button-border-radius-md` | Medium button border radius | `--seeds-rounded` |
| `--button-padding-lg` | Large button padding | |
| `--button-font-size-lg` | Large button font size | `--seeds-font-size-lg` |
| `--button-border-radius-lg` | Large button border radius | `--seeds-rounded` |
Loading

0 comments on commit 2b09432

Please sign in to comment.