Skip to content

Commit

Permalink
Implement basic njwds-button component (#4)
Browse files Browse the repository at this point in the history
* wip: add button tests for variant prop

* refactor: rename boolean button props to look more like HTML attributes

* refactor: button component to only have variant prop

* tests: add button tests to check as-child attribute

* chore: delete unnecessary buttonAttributes component

* chore: remove buttonAttributes from Njwdsbutton interface

* refactor: button component uses NJWDS naming for variant property

* delete autogen docs

* add components to www output

* delete autogen readmes

* add button doc page

* fix: inverse needs outline class to work

* rename variants to secondary-dark and link-dark

* refactor variant prop to type

* wip: button design review

* wip: update button HTML docs

* add mode prop
  • Loading branch information
ezhangy authored Aug 14, 2024
1 parent 51e0624 commit 13aafda
Show file tree
Hide file tree
Showing 10 changed files with 293 additions and 59 deletions.
6 changes: 6 additions & 0 deletions packages/stencil-library/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
*/
import { HTMLStencilElement, JSXBase } from "@stencil/core/internal";
import { ButtonVariant } from "./components/button/button";
import { Mode } from "./interface";
export { ButtonVariant } from "./components/button/button";
export { Mode } from "./interface";
export namespace Components {
interface MyComponent {
/**
Expand All @@ -30,6 +32,8 @@ export namespace Components {
interface NjwdsBanner {
}
interface NjwdsButton {
"asChild": boolean;
"mode": Mode;
"variant": ButtonVariant;
}
}
Expand Down Expand Up @@ -88,6 +92,8 @@ declare namespace LocalJSX {
interface NjwdsBanner {
}
interface NjwdsButton {
"asChild"?: boolean;
"mode"?: Mode;
"variant"?: ButtonVariant;
}
interface IntrinsicElements {
Expand Down
10 changes: 0 additions & 10 deletions packages/stencil-library/src/components/banner/readme.md

This file was deleted.

105 changes: 105 additions & 0 deletions packages/stencil-library/src/components/button/button.e2e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { E2EElement, newE2EPage } from '@stencil/core/testing';

const renderAndGetButtonElement = async (content: string): Promise<E2EElement> => {
const page = await newE2EPage();
await page.setContent(content);
const button = await page.find('njwds-button > button');
return button;
};

describe('<njwds-button>', () => {
describe('variant', () => {
it('renders with only the "usa-button" class by default', async () => {
const button = await renderAndGetButtonElement('<njwds-button></njwds-button>');
expect(button.className).toBe('usa-button');
});

it('renders the primary variant with only the "usa-button" class', async () => {
const button = await renderAndGetButtonElement(`
<njwds-button variant="primary"></njwds-button>
`);
const buttonClasses = button.className.split(' ').sort();
expect(buttonClasses).toEqual(['usa-button']);
});

it('renders the secondary variant with "usa-button--outline" class', async () => {
const button = await renderAndGetButtonElement(`
<njwds-button variant="secondary"></njwds-button>
`);
const buttonClasses = button.className.split(' ').sort();
expect(buttonClasses).toEqual(['usa-button', 'usa-button--outline'].sort());
});

it('renders the link variant with USWDS unstyled styling', async () => {
const button = await renderAndGetButtonElement(`
<njwds-button variant="link"></njwds-button>
`);
const buttonClasses = button.className.split(' ').sort();
expect(buttonClasses).toEqual(['usa-button', 'usa-button--unstyled'].sort());
});

it('renders danger variant with USWDS secondary styling', async () => {
const button = await renderAndGetButtonElement(`
<njwds-button variant="danger"></njwds-button>
`);
const buttonClasses = button.className.split(' ').sort();
expect(buttonClasses).toEqual(['usa-button', 'usa-button--secondary'].sort());
});
});

describe('mode', () => {
// TODO: How to write a test checking if mode="light" by default

it.each(['primary', 'danger'])("mode doesn't affect %s variant", async variant => {
const lightButtonClasses = (
await renderAndGetButtonElement(`
<njwds-button variant="${variant}" mode="light"></njwds-button>
`)
).className.split(' ');
const darkButtonClasses = (
await renderAndGetButtonElement(`
<njwds-button variant="${variant}" mode="dark"></njwds-button>
`)
).className.split(' ');
expect(lightButtonClasses.sort()).toEqual(darkButtonClasses.sort());
});

it('renders dark mode link variant as USWDS outline inverse, unstyled variant', async () => {
const button = await renderAndGetButtonElement(`
<njwds-button variant="link" mode="dark"></njwds-button>
`);
const buttonClasses = button.className.split(' ').sort();
expect(buttonClasses).toEqual(['usa-button', 'usa-button--unstyled', 'usa-button--outline', 'usa-button--inverse'].sort());
});

it('renders dark mode link variant as USWDS outline inverse, unstyled variant', async () => {
const button = await renderAndGetButtonElement(`
<njwds-button variant="link" mode="dark"></njwds-button>
`);
const buttonClasses = button.className.split(' ').sort();
expect(buttonClasses).toEqual(['usa-button', 'usa-button--unstyled', 'usa-button--outline', 'usa-button--inverse'].sort());
});
});

describe('asChild', () => {
it('renders a button element when asChild is false (default)', async () => {
const button = await renderAndGetButtonElement('<njwds-button></njwds-button>');
expect(button.tagName).toBe('BUTTON');
});

it('renders with only the "usa-button" class by default', async () => {
const button = await renderAndGetButtonElement('<njwds-button as-child><button></button></njwds-button>');
expect(button.className.trim()).toBe('usa-button');
});

it('renders the button slot element with custom attributes when asChild is true', async () => {
const button = await renderAndGetButtonElement(`
<njwds-button as-child>
<button data-test-1="1" data-test-2="2"></button>
</njwds-button>
`);
expect(button).toEqualAttribute('data-test-1', '1');
expect(button).toEqualAttribute('data-test-2', '2');
});
});
});
54 changes: 51 additions & 3 deletions packages/stencil-library/src/components/button/button.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,54 @@
import { newSpecPage } from '@stencil/core/testing';
import { Button } from './button';

describe('<njwds-button>', () => {
describe('variant', () => {
it('by default, renders without special styling', () => {});
it.each(['secondary', 'accent-cool', 'accent-warm', 'base', 'outline']);
describe('asChild', () => {
it('throws an error when asChild is true there are no slots', async () => {
const renderButtonWithNoSlots = () =>
newSpecPage({
components: [Button],
html: `<njwds-button as-child></njwds-button>`,
});
await expect(renderButtonWithNoSlots).rejects.toThrow();
});

it('throws an error when asChild is true and there is more than one slot', async () => {
const renderButtonWithTwoSlots = () =>
newSpecPage({
components: [Button],
html: `
<njwds-button as-child>
<button>slot 1</button>
<button>slot 2</button>
</njwds-button>`,
});
await expect(renderButtonWithTwoSlots).rejects.toThrow();
});

it('throws an error when asChild is true and the slot element is not a button', async () => {
const renderButtonWithParagraphSlot = async () =>
await newSpecPage({
components: [Button],
html: `
<njwds-button as-child>
<p>slot</p>
</njwds-button>`,
});
await expect(renderButtonWithParagraphSlot).rejects.toThrow();
});

it('does not throw error when asChild is true and there is a single slot element that is a <button>', async () => {
const page = await newSpecPage({
components: [Button],
html: `
<njwds-button as-child="true">
<button>slot</button>
</njwds-button>`,
});
const slotElements = page.root.children;
expect(slotElements.length).toBe(1);
const slotElement = slotElements[0];
expect(slotElement.tagName).toBe('BUTTON');
});
});
});
78 changes: 70 additions & 8 deletions packages/stencil-library/src/components/button/button.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,81 @@
import { Component, Prop, h } from "@stencil/core";
import { Host, HTMLStencilElement } from "@stencil/core/internal";
import { Element } from '@stencil/core';
import { Mode } from "../../interface";

export type ButtonVariant = "default" | "secondary" | "accent-cool" | "accent-warm" | "base" | "outline"

export type ButtonVariant = "primary" | "secondary" | "link" | "danger"

@Component({
tag: "njwds-button",
})
export class Button {
@Prop() variant: ButtonVariant = "default";
@Prop() variant: ButtonVariant = "primary";
@Prop() mode: Mode = "light"

@Prop() asChild: boolean = false
@Element() private hostElement: HTMLStencilElement;

private getButtonClassName(): string {
const getVariantClassName = (variant: ButtonVariant): string => {
switch (variant) {
case 'primary':
return ""
case "secondary":
return "usa-button--outline"
case 'link':
return "usa-button--unstyled"
case 'danger':
return "usa-button--secondary"
}
}

const getDarkModeVariantClassName = (variant: ButtonVariant) => {
switch (variant) {
case "secondary":
return "usa-button--outline usa-button--inverse"
case 'link':
return "usa-button--unstyled usa-button--outline usa-button--inverse"
default:
return getVariantClassName(variant)
}
}

return this.mode === "light"
? `usa-button ${getVariantClassName(this.variant)}`
: `usa-button ${getDarkModeVariantClassName(this.variant)}`
}

componentWillLoad() {
if (this.asChild) {
const slotElements = this.hostElement.children
if (slotElements.length !== 1) {
throw new Error(`If the asChild property is set to true on the njwds-button component, the component must have exactly one slot element. Instead got ${slotElements.length} elements.`)
}
if (slotElements[0].tagName !== "BUTTON") {
throw new Error(`If the asChild property is set to true on the njwds-button component, the slot element must be a <button>. Instead got ${slotElements[0].outerHTML}`)
}
const buttonElement = slotElements[0] as HTMLButtonElement
const buttonClassName = this.getButtonClassName()
buttonElement.className = `${buttonElement.className} ${buttonClassName}`
}

}


render() {
const variantClass = `usa-button--${this.variant}`
return (
<button class={`usa-button ${variantClass}`}>
<slot />
</button>
)
const buttonClassName = this.getButtonClassName()

return this.asChild
? (
<Host>
<slot />
</Host>
)
: (
<button class={buttonClassName}>
<slot />
</button>
)
}
}
58 changes: 58 additions & 0 deletions packages/stencil-library/src/components/button/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<!DOCTYPE html>
<html dir="ltr" lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=5.0" />
<title>Button Component</title>
<script type="module" src="/build/stencil-library.esm.js"></script>
<script nomodule src="/build/stencil-library.js"></script>
<link rel="stylesheet" href="/build/css/styles.css" />
</head>
<body>
<h1>Button</h1>
<h2>Primary</h2>
<njwds-button variant="primary">
Button
</njwds-button>
<h2>Secondary</h2>
<njwds-button variant="secondary">
Button
</njwds-button>
<h2>Secondary Dark</h2>
<njwds-button variant="secondary" mode="dark" style="background-color: black; padding: 2rem;">
Button
</njwds-button>
<h2>Link</h2>
<njwds-button variant="link" >
Button
</njwds-button>
<h2>Link Dark</h2>
<njwds-button variant="link" mode="dark" style="background-color: black;">
Button
</njwds-button>
<h2>Danger</h2>
<njwds-button variant="danger">
Button
</njwds-button>

<h2>As Child</h2>
<njwds-button as-child>
<button "data-test"="test" >
Button
</button>
</njwds-button>

<h2>Planned Styling Changes</h2>
<h3>Danger Button Active State</h3>
<njwds-button type="danger">
Button
</njwds-button>
<h3>Focus Ring</h3>
<njwds-button style="background-color: black; padding: 2rem;">
Button
</njwds-button>

<h2>Questions</h2>
<p>Should we have a single "type" property with "secondary-dark" and "link-dark"? Or a "mode" property that can be "light"/"dark", and only change the styling for the "secondary" and "link" button types?</p>
</body>
</html>
17 changes: 0 additions & 17 deletions packages/stencil-library/src/components/button/readme.md

This file was deleted.

19 changes: 0 additions & 19 deletions packages/stencil-library/src/components/my-component/readme.md

This file was deleted.

Loading

0 comments on commit 13aafda

Please sign in to comment.