Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Button component #63

Merged
merged 24 commits into from
Apr 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
9d85690
improve Button component and stories
Benaiah Mar 13, 2023
8a92ee3
Button: forward click event
Benaiah Mar 15, 2023
486f099
Button: allow passing a class
Benaiah Mar 15, 2023
86cbfb2
Button: import USWDS styles globally
Benaiah Mar 22, 2023
3f25879
Button: move component files into subfolder
Benaiah Mar 22, 2023
3bdd92d
Button: add tests
Benaiah Mar 22, 2023
42df906
Button: fix import
Benaiah Apr 22, 2023
6bd7ff7
Button: set up new variants based on LDAF design system
Benaiah Apr 22, 2023
fd2c843
Button: add story for inverse variant
Benaiah Apr 22, 2023
dbeaaf9
Button: styling fixes
Benaiah Apr 22, 2023
ec86fae
Button: fix type error in tests
Benaiah Apr 22, 2023
4a44b76
Button: move ButtonTest to a __tests__ directory
Benaiah Apr 22, 2023
696f044
Button: fix import
Benaiah Apr 22, 2023
46180cd
Button: use Storybook's default dark-background setting
Benaiah Apr 22, 2023
c111049
Button: remove bad import in stories
Benaiah Apr 24, 2023
bf176b2
Button: don't re-import styles
Benaiah Apr 25, 2023
30bf9ae
Button: fix appearance of large button
Benaiah Apr 26, 2023
053c48c
Button: make classes reactive to try to improve Storybook behavior
Benaiah Apr 26, 2023
a86fee7
Button: link to Figma page in Storybook
Benaiah Apr 26, 2023
40e3909
Button: add TODO comment in styles
Benaiah Apr 26, 2023
6b4f69f
Button: refactoring, tests, stories
Benaiah Apr 26, 2023
80465d3
Button: make selectors more specific so CSS isn't order-dependent
Benaiah Apr 26, 2023
6eec05a
Button: fix type error in tests
Benaiah Apr 26, 2023
154a4c8
Button: remove unused import
Benaiah Apr 26, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 154 additions & 0 deletions src/lib/components/Button/Button.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
@use "sass:map";

// TODO: replace with global color variables from src/variables.scss
$colors: (
"primary": #0051ad,
"primary-dark": #063c7a,
"primary-darker": #00284d,
"disabled": #c9c9c9,
"disabled-dark": #adadad,
"base-lightest": #f2f1f0,
"base-light": #a9aeb1,
"base-dark": #565c65,
"base-darker": #3d4551,
"gray-05": #f0f0f0,
"primary-lightest": #d1e9ff,
"text-only-hover": rgba(6, 60, 122, 16%),
"text-only-active": rgba(6, 60, 122, 30%),
"outline-inverse": #dcdee0,
);
Comment on lines +4 to +19
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My site title PR defined the current color palette as global variables. That covers everything here except for the last 3 (and gray-05, although $grayscale-05 is extremely close if that's an acceptable substitution, cc @getpunched )

If we like that approach, I can make a smaller PR to just get that in or do follow-up on this once #50 gets in.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A follow-up PR to add that would be great! I've added a TODO comment indicating that we should do that.


.usa-button {
width: auto;
color: #fff;
background: map.get($colors, "primary");
&:hover {
background: map.get($colors, "primary-dark");
}
&:active {
background: map.get($colors, "primary-darker");
}
&:disabled {
background: map.get($colors, "disabled-dark");
}
}

.usa-button.usa-button--base {
background: map.get($colors, "base-dark");
&:hover {
background: map.get($colors, "base-dark");
}
&:active {
background: map.get($colors, "base-darker");
}
}

.usa-button.usa-button--inverse {
color: map.get($colors, "primary");
background: #fff;
&:hover {
color: map.get($colors, "primary");
background: map.get($colors, "gray-05");
}
&:active {
color: map.get($colors, "primary");
background: map.get($colors, "primary-lightest");
}
&:disabled {
color: #fff;
}
}

.usa-button.usa-button--text-only {
color: map.get($colors, "primary");
background: none;
&:hover {
color: map.get($colors, "primary");
background: map.get($colors, "text-only-hover");
}
&:active {
color: map.get($colors, "primary");
background: map.get($colors, "text-only-active");
}
&:disabled {
background: none;
color: map.get($colors, "disabled");
}
}

.usa-button.usa-button--outline {
background: none;
color: map.get($colors, "primary");
border: 2px solid map.get($colors, "primary");
box-shadow: none;
&:hover {
background: none;
color: map.get($colors, "primary-dark");
border: 2px solid map.get($colors, "primary-dark");
box-shadow: none;
}
&:active {
background: none;
color: map.get($colors, "base-darker");
border: 2px solid map.get($colors, "base-darker");
box-shadow: none;
}
&:disabled {
background: none;
color: map.get($colors, "disabled");
border: 2px solid map.get($colors, "disabled");
}
}

.usa-button.usa-button--outline-inverse {
color: map.get($colors, "outline-inverse");
border-color: map.get($colors, "base-light");
&:hover {
color: map.get($colors, "base-lightest");
border-color: map.get($colors, "base-lightest");
}
&:active {
color: #fff;
border-color: #fff;
}
&:disabled {
color: map.get($colors, "disabled-dark");
border-color: map.get($colors, "disabled-dark");
}
}

.usa-button.usa-button--big {
background: map.get($colors, "primary");
color: #fff;
&:hover {
background: map.get($colors, "primary-dark");
}
&:active {
background: map.get($colors, "primary-darker");
}
&:disabled {
background: map.get($colors, "disabled");
}
}

.usa-button.usa-button--big-inverse {
background: #fff;
color: map.get($colors, "primary");
&:hover {
background: map.get($colors, "gray-05");
color: map.get($colors, "primary");
}
&:active {
background: map.get($colors, "primary-lightest");
color: map.get($colors, "primary");
}
&:disabled {
background: map.get($colors, "disabled-dark");
color: #fff;
}
}

.usa-button.usa-button--unstyled {
background: none;
color: #005ea2;
}
39 changes: 39 additions & 0 deletions src/lib/components/Button/Button.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<script lang="ts">
import "./Button.scss";
import classNames from "$lib/util/classNames";
import type { Variant, Type } from "./buttonOptions";

export let disabled = false;

export let unstyled = false;

export let variant: Variant = "primary";

const variantClassesDict: Record<Variant, string[]> = {
primary: [],
base: ["usa-button--base"],
inverse: ["usa-button--inverse"],
"text-only": ["usa-button--text-only"],
outline: ["usa-button--outline"],
"outline-inverse": ["usa-button--outline", "usa-button--outline-inverse"],
big: ["usa-button--big"],
"big-inverse": ["usa-button--big", "usa-button--big-inverse"],
};

$: variantClasses = variantClassesDict[variant];

export let type: Type = "button";

let className = "";
export { className as class };
$: classes = classNames(
"usa-button",
...variantClasses,
unstyled && "usa-button--unstyled",
className
);
</script>

<button {type} {disabled} aria-disabled={disabled} class={classes} on:click>
<slot>Button</slot>
</button>
59 changes: 59 additions & 0 deletions src/lib/components/Button/Button.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import "@testing-library/jest-dom";
import { render, screen } from "@testing-library/svelte";
import userEvent from "@testing-library/user-event";
import { describe, it, expect, vi } from "vitest";
import type { Variant } from "./buttonOptions";

import ButtonTest from "./__tests__/ButtonTest.svelte";

describe("Button", () => {
it("renders", async () => {
render(ButtonTest, { slot: "Test Button" });
const button = screen.getByRole("button");
expect(button).toBeInTheDocument();
expect(button).toHaveTextContent("Test Button");
});

it("clicks", async () => {
const { component } = render(ButtonTest);
const onClick = vi.fn();
component.$on("click", onClick);
await userEvent.click(screen.getByRole("button"));
expect(onClick).toHaveBeenCalledOnce();
});

it("does not click when disabled", async () => {
const { component } = render(ButtonTest, { disabled: true });
const onClick = vi.fn();
component.$on("click", onClick);
await userEvent.click(screen.getByRole("button"));
expect(onClick).not.toHaveBeenCalled();
});

type VariantAndClass = [Variant, string];

(
[
["primary", "usa-button"],
["base", "usa-button usa-button--base"],
["inverse", "usa-button usa-button--inverse"],
["text-only", "usa-button usa-button--text-only"],
["outline", "usa-button usa-button--outline"],
["outline-inverse", "usa-button usa-button--outline usa-button--outline-inverse"],
["big", "usa-button usa-button--big"],
["big-inverse", "usa-button usa-button--big usa-button--big-inverse"],
] satisfies VariantAndClass[]
)
.flatMap(([variant, expectedClass]): [{ variant: Variant; unstyled?: boolean }, string][] => [
[{ variant }, expectedClass],
[{ variant, unstyled: true }, `${expectedClass} usa-button--unstyled`],
])
.forEach(([props, expectedClass]) => {
it(`renders the variant ${props.variant}${
props.unstyled ? " unstyled" : ""
} with the expected CSS classes`, () => {
render(ButtonTest, props);
expect(screen.getByRole("button")).toHaveAttribute("class", expectedClass);
});
});
});
14 changes: 14 additions & 0 deletions src/lib/components/Button/__tests__/ButtonTest.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<script lang="ts">
import type { ComponentProps } from "svelte";
import Button from "../Button.svelte";

type $$Props = ComponentProps<Button> & {
slot?: string;
};

export let slot: string | undefined = "Button";
</script>

<Button {...$$restProps} on:click>
{slot}
</Button>
16 changes: 16 additions & 0 deletions src/lib/components/Button/buttonOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export const variants = [
"primary",
"base",
"inverse",
"text-only",
"outline",
"outline-inverse",
"big",
"big-inverse",
] as const;

export type Variant = (typeof variants)[number];

export const types = ["button", "submit", "reset"] as const;

export type Type = (typeof types)[number];
3 changes: 3 additions & 0 deletions src/lib/components/Button/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { default } from "./Button.svelte";

export * from "./buttonOptions";
101 changes: 101 additions & 0 deletions src/stories/Button.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import type { Meta, StoryObj } from "@storybook/svelte";

import Button, { variants, types } from "$lib/components/Button";

// More on how to set up stories at: https://storybook.js.org/docs/7.0/svelte/writing-stories/introduction
const meta = {
title: "Components/Button",
component: Button,
tags: ["autodocs"],
argTypes: {
variant: {
control: { type: "select" },
options: variants,
},
type: {
control: { type: "select" },
options: types,
},
unstyled: {
control: { type: "boolean" },
},
},
parameters: {
design: {
type: "figma",
url: " https://www.figma.com/file/oGKbyCnCRRdNzLYbiags93/LDAF-Component-Library-USWDS-3.0.2?node-id=2196-3764&t=HICajhP8FIexorTH-4",
},
},
} satisfies Meta<Button>;

export default meta;
type Story = StoryObj<typeof meta>;

// More on writing stories with args: https://storybook.js.org/docs/7.0/svelte/writing-stories/args
export const Primary: Story = {
args: {},
};

export const Base: Story = {
args: {
variant: "base",
},
};

export const Inverse: Story = {
parameters: {
backgrounds: { default: "dark" },
},
args: {
variant: "inverse",
},
};

export const TextOnly: Story = {
args: {
variant: "text-only",
},
};

export const Outline: Story = {
args: {
variant: "outline",
},
};

export const OutlineInverse: Story = {
parameters: {
backgrounds: { default: "dark" },
},
args: {
variant: "outline-inverse",
},
};

export const Big: Story = {
args: {
variant: "big",
},
};

export const BigInverse: Story = {
parameters: {
backgrounds: { default: "dark" },
},
args: {
variant: "big-inverse",
},
};

export const Unstyled: Story = {
args: {
unstyled: true,
},
};

export const BigUnstyled: Story = {
args: {
variant: "big",
unstyled: true,
},
};