Skip to content

Commit

Permalink
feat: Add Button component (#16)
Browse files Browse the repository at this point in the history
* Add Button component

Includes Primary, Secondary, Tertiary, and Danger buttons

* Adds support for Icon within a the Button
Adds active state as defined by design for each button
Tertiary button has different sizing, that is now accounted for
Removed 'fill="none"' from Icon svg to allow for passing the Icon's color prop as "inherit"

* Use 'useFocusRing' hook instead of wrapping component

* Updates after rebasing

* Updates after rebasing. 1 - Reorg files. 2 - Use new truss rules for _Px(). 3-Add border radius rules. 4 - Update Button component to work with new Icon component

* Use fill truss prop rather than add('fill',...)
  • Loading branch information
Brandon authored Mar 15, 2021
1 parent 1ff13f6 commit 5c745cd
Show file tree
Hide file tree
Showing 8 changed files with 1,006 additions and 51 deletions.
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@
},
"dependencies": {
"@emotion/react": "^11.1.5",
"@react-types/shared": "^3.4.0"
"@react-types/shared": "^3.4.0",
"react-aria": "^3.4.0",
"react-stately": "^3.3.0"
},
"devDependencies": {
"@babel/core": "^7.13.1",
Expand Down
10 changes: 3 additions & 7 deletions src/Css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,13 +102,9 @@ class CssBuilder<T extends Properties1> {
get bCool1100() { return this.add("borderColor", "rgba(16,27,100,1)"); }

// borderRadiusRules
get br0() { return this.add("borderRadius", "0"); }
get br1() { return this.add("borderRadius", ".125rem"); }
get br2() { return this.add("borderRadius", ".25rem"); }
get br3() { return this.add("borderRadius", ".5rem"); }
get br4() { return this.add("borderRadius", "1rem"); }
get br100() { return this.add("borderRadius", "100%"); }
get brPill() { return this.add("borderRadius", "9999px"); }
get br4() { return this.add("borderRadius", "4px"); }
get br8() { return this.add("borderRadius", "8px"); }
get br16() { return this.add("borderRadius", "16px"); }

// borderRules
get ba() { return this.add("borderStyle", "solid").add("borderWidth", "1px"); }
Expand Down
173 changes: 173 additions & 0 deletions src/components/Button.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import { Meta } from "@storybook/react";
import { Button, Css } from "src/index";

export default {
component: Button,
title: "Components/Buttons",
} as Meta;

export function Buttons() {
return (
<div css={{ ...Css.dg.$, gridTemplateColumns: "repeat(4, auto)" }}>
<div>
<h2>Primary</h2>
<div>
<Button autoFocus>Primary Button</Button>
<Button isDisabled>Disabled</Button>
</div>
<div>
<Button size="md">Primary Button</Button>
<Button size="md" isDisabled>
Disabled
</Button>
</div>
<div>
<Button size="lg">Primary Button</Button>
<Button size="lg" isDisabled>
Disabled
</Button>
</div>
<div>
<Button icon="plus">Primary Button</Button>
<Button isDisabled icon="plus">
Disabled
</Button>
</div>
<div>
<Button size="md" icon="plus">
Primary Button
</Button>
<Button size="md" isDisabled icon="plus">
Disabled
</Button>
</div>
<div>
<Button size="lg" icon="plus">
Primary Button
</Button>
<Button size="lg" isDisabled icon="plus">
Disabled
</Button>
</div>
</div>

<div>
<h2>Secondary</h2>
<div>
<Button variant="secondary">Secondary Button</Button>
<Button variant="secondary" isDisabled>
Disabled
</Button>
</div>
<div>
<Button size="md" variant="secondary">
Secondary Button
</Button>
<Button size="md" variant="secondary" isDisabled>
Disabled
</Button>
</div>
<div>
<Button size="lg" variant="secondary">
Secondary Button
</Button>
<Button size="lg" variant="secondary" isDisabled>
Disabled
</Button>
</div>
<div>
<Button icon="plus" variant="secondary">
Secondary Button
</Button>
<Button icon="plus" variant="secondary" isDisabled>
Disabled
</Button>
</div>
<div>
<Button icon="plus" size="md" variant="secondary">
Secondary Button
</Button>
<Button icon="plus" size="md" variant="secondary" isDisabled>
Disabled
</Button>
</div>
<div>
<Button icon="plus" size="lg" variant="secondary">
Secondary Button
</Button>
<Button icon="plus" size="lg" variant="secondary" isDisabled>
Disabled
</Button>
</div>
</div>

<div>
<h2>Tertiary</h2>
<div>
<Button variant="tertiary">Tertiary Button</Button>
<Button variant="tertiary" isDisabled>
Disabled
</Button>
</div>
<div>
<Button icon="plus" variant="tertiary">
Tertiary Button
</Button>
<Button icon="plus" variant="tertiary" isDisabled>
Disabled
</Button>
</div>
</div>

<div>
<h2>Danger!</h2>
<div>
<Button variant="danger">Danger Button</Button>
<Button variant="danger" isDisabled>
Disabled
</Button>
</div>
<div>
<Button size="md" variant="danger">
Danger Button
</Button>
<Button size="md" variant="danger" isDisabled>
Disabled
</Button>
</div>
<div>
<Button size="lg" variant="danger">
Danger Button
</Button>
<Button size="lg" variant="danger" isDisabled>
Disabled
</Button>
</div>
<div>
<Button icon="trash" variant="danger">
Danger Button
</Button>
<Button icon="trash" variant="danger" isDisabled>
Disabled
</Button>
</div>
<div>
<Button icon="trash" size="md" variant="danger">
Danger Button
</Button>
<Button icon="trash" size="md" variant="danger" isDisabled>
Disabled
</Button>
</div>
<div>
<Button icon="trash" size="lg" variant="danger">
Danger Button
</Button>
<Button icon="trash" size="lg" variant="danger" isDisabled>
Disabled
</Button>
</div>
</div>
</div>
);
}
103 changes: 103 additions & 0 deletions src/components/Button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import type { AriaButtonProps } from "@react-types/button";
import { useMemo, useRef } from "react";
import { useButton, useFocusRing } from "react-aria";
import { Icon, IconProps } from "src/components/Icon";
import { Css, Palette } from "src/Css";

interface ButtonProps extends AriaButtonProps {
variant?: ButtonVariant;
size?: ButtonSize;
icon?: IconProps["icon"];
}

export function Button(props: ButtonProps) {
const { children, icon, variant = "primary", size = "sm" } = props;
const ref = useRef(null);
const { buttonProps } = useButton(props, ref);
const { isFocusVisible, focusProps } = useFocusRing(props);
const buttonStyles = useMemo(() => getButtonStyles(variant, size), [variant, size]);
const focusRingStyles = useMemo(() => (variant === "danger" ? dangerFocusRingStyles : defaultFocusRingStyles), [
variant,
]);

return (
<button
ref={ref}
{...buttonProps}
{...focusProps}
css={{ ...buttonReset, ...buttonStyles, ...(isFocusVisible ? focusRingStyles : {}) }}
>
{icon && <Icon xss={iconStyles[size]} color="inherit" icon={icon} />}
{children}
</button>
);
}

const buttonReset = Css.p0.bsNone.cursorPointer.smEm.br4.dif.itemsCenter
.mPx(4)
.add("font", "inherit")
.add("boxSizing", "border-box")
.add("outline", "inherit").$;
const disabledStyles = Css.add("cursor", "not-allowed").$;
const defaultFocusRingStyles = Css.add("boxShadow", `0px 0px 0px 2px ${Palette.White}, 0 0 0 4px ${Palette.Sky500}`).$;
const dangerFocusRingStyles = Css.add("boxShadow", `0px 0px 0px 2px ${Palette.White}, 0 0 0 4px ${Palette.Coral600}`).$;

const variantStyles: Record<ButtonVariant, {}> = {
primary: {
...Css.bgSky500.white.fill(Palette.White).$,
"&:hover:not(:disabled)": Css.bgSky700.$,
"&:disabled": { ...disabledStyles, ...Css.bgSky200.$ },
"&:active:not(:disabled)": Css.bgSky900.$,
},

secondary: {
...Css.bgWhite.bCoolGray300.bw1.ba.coolGray900.fill(Palette.CoolGray900).$,
"&:hover:not(:disabled):not(:active)": Css.bgCoolGray50.$,
"&:disabled": { ...disabledStyles, ...Css.bgWhite.coolGray300.fill(Palette.CoolGray300).$ },
"&:active:not(:disabled)": Css.bgCoolGray200.$,
},

tertiary: {
...Css.add("background", "none").sky500.fill(Palette.Sky500).$,
"&:hover:not(:disabled)": Css.bgCoolGray100.$,
"&:disabled": { ...disabledStyles, ...Css.coolGray300.fill(Palette.CoolGray300).$ },
"&:active:not(:disabled)": Css.sky900.fill(Palette.Sky900).$,
},

danger: {
...Css.bgCoral600.white.fill(Palette.White).$,
"&:hover:not(:disabled)": Css.bgCoral500.$,
"&:disabled": { ...disabledStyles, ...Css.bgCoral200.$ },
"&:active:not(:disabled)": Css.bgCoral700.$,
},
};

const sizeStyles: Record<ButtonSize, {}> = {
sm: Css.hPx(32).pxPx(12).$,
md: Css.hPx(40).px2.$,
lg: Css.hPx(48).px3.$,
};

const iconStyles: Record<ButtonSize, IconProps["xss"]> = {
sm: Css.mrPx(4).$,
md: Css.mr1.$,
lg: Css.mrPx(10).$,
};

function getButtonStyles(variant: ButtonVariant, size: ButtonSize) {
// Handling tertiary separately as it only supports a single size button. The size it supports does not match styles of other buttons.
if (variant === "tertiary") {
return {
...Css.hPx(40).px1.$,
...variantStyles.tertiary,
};
}

return {
...sizeStyles[size],
...variantStyles[variant],
};
}

type ButtonSize = "sm" | "md" | "lg";
type ButtonVariant = "primary" | "secondary" | "tertiary" | "danger";
3 changes: 2 additions & 1 deletion src/components/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './Icon';
export * from "./Button";
export * from "./Icon";
4 changes: 2 additions & 2 deletions src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export * from './Css';
export * from './components';
export * from "./components";
export * from "./Css";
13 changes: 9 additions & 4 deletions truss/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { generate, GenerateOpts, generateRules, makeRules } from '@homebound/truss';
import { palette } from './palette';
import { generate, GenerateOpts, generateRules, makeRules } from "@homebound/truss";
import { palette } from "./palette";

const increment = 8;
const numberOfIncrements = 8;
Expand Down Expand Up @@ -29,14 +29,19 @@ const fonts: Record<string, { fontWeight: 400 | 500 | 600, fontSize: string; lin
xl5Em: { fontWeight: 600, fontSize: "48px", lineHeight: "48px" },
};

// Pass fonts: {} b/c we create our own font rules
const methods = generateRules({ palette, fonts, numberOfIncrements });

// Custom rules
methods['fontFamilyRules'] = makeRules('fontFamily', {
methods["fontFamilyRules"] = makeRules("fontFamily", {
sansSerif: "'Inter', sans-serif",
});

methods["borderRadiusRules"] = makeRules("borderRadius", {
br4: "4px",
br8: "8px",
br16: "16px",
});

const aliases: Record<string, string[]> = {};

const typeAliases: GenerateOpts["typeAliases"] = {};
Expand Down
Loading

0 comments on commit 5c745cd

Please sign in to comment.