-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement basic njwds-button component (#4)
* 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
Showing
10 changed files
with
293 additions
and
59 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
105 changes: 105 additions & 0 deletions
105
packages/stencil-library/src/components/button/button.e2e.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
54
packages/stencil-library/src/components/button/button.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file was deleted.
Oops, something went wrong.
19 changes: 0 additions & 19 deletions
19
packages/stencil-library/src/components/my-component/readme.md
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.