diff --git a/app/client/packages/design-system/headless/src/components/Button/Button.stories.mdx b/app/client/packages/design-system/headless/src/components/Button/Button.stories.mdx
new file mode 100644
index 0000000000..90d9263ad7
--- /dev/null
+++ b/app/client/packages/design-system/headless/src/components/Button/Button.stories.mdx
@@ -0,0 +1,23 @@
+import { Canvas, Meta, Story, ArgsTable } from "@storybook/addon-docs";
+
+import { Button } from "./";
+
+
+
+export const Template = (args) => ;
+
+# Button
+
+A button is a clickable element that is used to trigger an action.
+
+
+
+
diff --git a/app/client/packages/design-system/headless/src/components/Button/Button.tsx b/app/client/packages/design-system/headless/src/components/Button/Button.tsx
index f86105f786..aefdad25b2 100644
--- a/app/client/packages/design-system/headless/src/components/Button/Button.tsx
+++ b/app/client/packages/design-system/headless/src/components/Button/Button.tsx
@@ -6,9 +6,14 @@ import { useFocusRing } from "@react-aria/focus";
import { useHover } from "@react-aria/interactions";
import { useFocusableRef } from "@react-spectrum/utils";
import type { FocusableRef } from "@react-types/shared";
-import type { ButtonProps as SpectrumButtonProps } from "@react-types/button";
+import type {
+ AriaButtonProps as SpectrumAriaBaseButtonProps,
+ ButtonProps as SpectrumButtonProps,
+} from "@react-types/button";
-export interface ButtonProps extends SpectrumButtonProps {
+export interface ButtonProps
+ extends SpectrumButtonProps,
+ SpectrumAriaBaseButtonProps {
className?: string;
}
diff --git a/app/client/packages/design-system/headless/src/components/Icon/Icon.tsx b/app/client/packages/design-system/headless/src/components/Icon/Icon.tsx
new file mode 100644
index 0000000000..dd78944ebd
--- /dev/null
+++ b/app/client/packages/design-system/headless/src/components/Icon/Icon.tsx
@@ -0,0 +1,32 @@
+import React from "react";
+import type { AriaLabelingProps, DOMProps } from "@react-types/shared";
+import type { ReactElement } from "react";
+import { filterDOMProps } from "@react-aria/utils";
+
+export interface IconProps extends DOMProps, AriaLabelingProps {
+ "aria-label"?: string;
+ children: ReactElement;
+ "aria-hidden"?: boolean | "false" | "true";
+ role?: string;
+}
+
+export function Icon(props: IconProps) {
+ const {
+ "aria-hidden": ariaHiddenProp,
+ "aria-label": ariaLabel,
+ children,
+ role = "img",
+ ...otherProps
+ } = props;
+
+ const ariaHidden = !ariaHiddenProp ? undefined : ariaHiddenProp;
+
+ return React.cloneElement(children, {
+ ...filterDOMProps(otherProps),
+ focusable: "false",
+ "aria-label": ariaLabel,
+ "aria-hidden": ariaLabel ? ariaHidden || undefined : true,
+ role,
+ "data-icon": "",
+ });
+}
diff --git a/app/client/packages/design-system/headless/src/components/Icon/index.tsx b/app/client/packages/design-system/headless/src/components/Icon/index.tsx
new file mode 100644
index 0000000000..65d727be32
--- /dev/null
+++ b/app/client/packages/design-system/headless/src/components/Icon/index.tsx
@@ -0,0 +1 @@
+export { Icon } from "./Icon";
diff --git a/app/client/packages/design-system/headless/src/index.ts b/app/client/packages/design-system/headless/src/index.ts
index dcb257a9bc..9213dac78b 100644
--- a/app/client/packages/design-system/headless/src/index.ts
+++ b/app/client/packages/design-system/headless/src/index.ts
@@ -2,3 +2,4 @@
export * from "./components/Button";
export * from "./components/Checkbox";
export * from "./components/Field";
+export * from "./components/Icon";
diff --git a/app/client/packages/design-system/widgets/jest.config.js b/app/client/packages/design-system/widgets/jest.config.js
new file mode 100644
index 0000000000..c24244a098
--- /dev/null
+++ b/app/client/packages/design-system/widgets/jest.config.js
@@ -0,0 +1,5 @@
+module.exports = {
+ preset: "ts-jest",
+ roots: ["/src"],
+ testEnvironment: "jsdom",
+};
diff --git a/app/client/packages/design-system/widgets/package.json b/app/client/packages/design-system/widgets/package.json
index a6b28d4f96..b476864730 100644
--- a/app/client/packages/design-system/widgets/package.json
+++ b/app/client/packages/design-system/widgets/package.json
@@ -6,7 +6,8 @@
"license": "MIT",
"scripts": {
"lint:ci": "eslint --cache .",
- "prettier:ci": "prettier --check ."
+ "prettier:ci": "prettier --check .",
+ "test:unit": "jest"
},
"dependencies": {
"@capsizecss/core": "^3.1.0",
diff --git a/app/client/packages/design-system/widgets/src/components/Button/Button.stories.mdx b/app/client/packages/design-system/widgets/src/components/Button/Button.stories.mdx
index 9b324a42cd..16d3442a0e 100644
--- a/app/client/packages/design-system/widgets/src/components/Button/Button.stories.mdx
+++ b/app/client/packages/design-system/widgets/src/components/Button/Button.stories.mdx
@@ -1,5 +1,8 @@
import { Canvas, Meta, Story, ArgsTable } from "@storybook/addon-docs";
+
import { Button } from "./";
+import { Icon } from "@design-system/headless";
+import EmotionHappyLineIcon from "remixicon-react/EmotionHappyLineIcon";
+
+# With Icon
+
+
+
+# Icon Position
+
+
diff --git a/app/client/packages/design-system/widgets/src/components/Button/Button.test.tsx b/app/client/packages/design-system/widgets/src/components/Button/Button.test.tsx
new file mode 100644
index 0000000000..558aa676aa
--- /dev/null
+++ b/app/client/packages/design-system/widgets/src/components/Button/Button.test.tsx
@@ -0,0 +1,66 @@
+import React from "react";
+import "@testing-library/jest-dom";
+import { Icon } from "@design-system/headless";
+import { render, screen } from "@testing-library/react";
+import EmotionHappyLineIcon from "remixicon-react/EmotionHappyLineIcon";
+
+import { Button } from "./";
+
+describe("@design-system/widgets/Button", () => {
+ it("renders children when passed", () => {
+ render();
+ expect(screen.getByRole("button")).toHaveTextContent("Click me");
+ });
+
+ it("passes type to button component", () => {
+ render();
+ expect(screen.getByRole("button")).toHaveAttribute("type", "submit");
+ });
+
+ it("sets variant based on prop", () => {
+ render();
+ expect(screen.getByRole("button")).toHaveAttribute(
+ "data-variant",
+ "primary",
+ );
+ });
+
+ it("sets disabled attribute based on prop", () => {
+ render();
+ expect(screen.getByRole("button")).toBeDisabled();
+ expect(screen.getByRole("button")).toHaveAttribute("data-disabled");
+ });
+
+ it("sets data-loading attribute and icon based on loading prop", () => {
+ render();
+ expect(screen.getByRole("button")).toHaveAttribute("data-loading");
+
+ const icon = screen.getByRole("button").querySelector("[data-icon]");
+ expect(icon).toBeInTheDocument();
+ });
+
+ it("renders icon when passed", () => {
+ const { container } = render(
+