From 13aafda02e6c3dbce471293b79ff33dc49a1b6e2 Mon Sep 17 00:00:00 2001
From: Lizzy Zhang <77908367+ezhangy@users.noreply.github.com>
Date: Wed, 14 Aug 2024 15:23:38 -0400
Subject: [PATCH] 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
---
 packages/stencil-library/src/components.d.ts  |   6 +
 .../src/components/banner/readme.md           |  10 --
 .../src/components/button/button.e2e.ts       | 105 ++++++++++++++++++
 .../src/components/button/button.spec.ts      |  54 ++++++++-
 .../src/components/button/button.tsx          |  78 +++++++++++--
 .../src/components/button/index.html          |  58 ++++++++++
 .../src/components/button/readme.md           |  17 ---
 .../src/components/my-component/readme.md     |  19 ----
 packages/stencil-library/src/index.html       |   4 +-
 packages/stencil-library/src/interface.d.ts   |   1 +
 10 files changed, 293 insertions(+), 59 deletions(-)
 delete mode 100644 packages/stencil-library/src/components/banner/readme.md
 create mode 100644 packages/stencil-library/src/components/button/button.e2e.ts
 create mode 100644 packages/stencil-library/src/components/button/index.html
 delete mode 100644 packages/stencil-library/src/components/button/readme.md
 delete mode 100644 packages/stencil-library/src/components/my-component/readme.md
 create mode 100644 packages/stencil-library/src/interface.d.ts

diff --git a/packages/stencil-library/src/components.d.ts b/packages/stencil-library/src/components.d.ts
index 0540ae4..2f223fd 100644
--- a/packages/stencil-library/src/components.d.ts
+++ b/packages/stencil-library/src/components.d.ts
@@ -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 {
         /**
@@ -30,6 +32,8 @@ export namespace Components {
     interface NjwdsBanner {
     }
     interface NjwdsButton {
+        "asChild": boolean;
+        "mode": Mode;
         "variant": ButtonVariant;
     }
 }
@@ -88,6 +92,8 @@ declare namespace LocalJSX {
     interface NjwdsBanner {
     }
     interface NjwdsButton {
+        "asChild"?: boolean;
+        "mode"?: Mode;
         "variant"?: ButtonVariant;
     }
     interface IntrinsicElements {
diff --git a/packages/stencil-library/src/components/banner/readme.md b/packages/stencil-library/src/components/banner/readme.md
deleted file mode 100644
index 33a1c29..0000000
--- a/packages/stencil-library/src/components/banner/readme.md
+++ /dev/null
@@ -1,10 +0,0 @@
-# njwds-banner
-
-
-
-<!-- Auto Generated Below -->
-
-
-----------------------------------------------
-
-*Built with [StencilJS](https://stenciljs.com/)*
diff --git a/packages/stencil-library/src/components/button/button.e2e.ts b/packages/stencil-library/src/components/button/button.e2e.ts
new file mode 100644
index 0000000..45cfbbf
--- /dev/null
+++ b/packages/stencil-library/src/components/button/button.e2e.ts
@@ -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');
+    });
+  });
+});
diff --git a/packages/stencil-library/src/components/button/button.spec.ts b/packages/stencil-library/src/components/button/button.spec.ts
index 322d05f..5af3a21 100644
--- a/packages/stencil-library/src/components/button/button.spec.ts
+++ b/packages/stencil-library/src/components/button/button.spec.ts
@@ -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');
+    });
   });
 });
diff --git a/packages/stencil-library/src/components/button/button.tsx b/packages/stencil-library/src/components/button/button.tsx
index b3fe328..19d7eea 100644
--- a/packages/stencil-library/src/components/button/button.tsx
+++ b/packages/stencil-library/src/components/button/button.tsx
@@ -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>
+            )
     }
 }
\ No newline at end of file
diff --git a/packages/stencil-library/src/components/button/index.html b/packages/stencil-library/src/components/button/index.html
new file mode 100644
index 0000000..b960c92
--- /dev/null
+++ b/packages/stencil-library/src/components/button/index.html
@@ -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>
diff --git a/packages/stencil-library/src/components/button/readme.md b/packages/stencil-library/src/components/button/readme.md
deleted file mode 100644
index db30254..0000000
--- a/packages/stencil-library/src/components/button/readme.md
+++ /dev/null
@@ -1,17 +0,0 @@
-# njwds-button
-
-
-
-<!-- Auto Generated Below -->
-
-
-## Properties
-
-| Property  | Attribute | Description | Type                                                                                | Default     |
-| --------- | --------- | ----------- | ----------------------------------------------------------------------------------- | ----------- |
-| `variant` | `variant` |             | `"accent-cool" \| "accent-warm" \| "base" \| "default" \| "outline" \| "secondary"` | `"default"` |
-
-
-----------------------------------------------
-
-*Built with [StencilJS](https://stenciljs.com/)*
diff --git a/packages/stencil-library/src/components/my-component/readme.md b/packages/stencil-library/src/components/my-component/readme.md
deleted file mode 100644
index 06b5864..0000000
--- a/packages/stencil-library/src/components/my-component/readme.md
+++ /dev/null
@@ -1,19 +0,0 @@
-# my-component
-
-
-
-<!-- Auto Generated Below -->
-
-
-## Properties
-
-| Property | Attribute | Description     | Type     | Default     |
-| -------- | --------- | --------------- | -------- | ----------- |
-| `first`  | `first`   | The first name  | `string` | `undefined` |
-| `last`   | `last`    | The last name   | `string` | `undefined` |
-| `middle` | `middle`  | The middle name | `string` | `undefined` |
-
-
-----------------------------------------------
-
-*Built with [StencilJS](https://stenciljs.com/)*
diff --git a/packages/stencil-library/src/index.html b/packages/stencil-library/src/index.html
index c2b3413..51bfa77 100644
--- a/packages/stencil-library/src/index.html
+++ b/packages/stencil-library/src/index.html
@@ -21,8 +21,8 @@ <h2>Button</h2>
     <njwds-button>
       Default
     </njwds-button>
-    <njwds-button variant="secondary">
-      Secondary
+    <njwds-button variant="secondary" as-child>
+      <button autofocus="true">test</button>
     </njwds-button>
   </body>
 </html>
diff --git a/packages/stencil-library/src/interface.d.ts b/packages/stencil-library/src/interface.d.ts
new file mode 100644
index 0000000..ad01be2
--- /dev/null
+++ b/packages/stencil-library/src/interface.d.ts
@@ -0,0 +1 @@
+export type Mode = 'light' | 'dark';