From 238e40951e40a4dad239d79d8a805b6e48c9555c Mon Sep 17 00:00:00 2001 From: Victor Trinh Date: Mon, 28 Oct 2024 16:30:15 -0400 Subject: [PATCH] Card --- .changeset/four-rocks-move.md | 5 ++ packages/components/src/Card/index.ts | 1 + .../components/src/Card/src/Card.module.css | 26 ++++++ packages/components/src/Card/src/Card.tsx | 81 +++++++++++++++++++ .../components/src/Card/src/CardContext.ts | 8 ++ packages/components/src/Card/src/index.ts | 3 + .../src/Card/tests/chromatic/Card.stories.tsx | 64 +++++++++++++++ .../src/Card/tests/jest/Card.ssr.test.tsx | 17 ++++ .../src/Card/tests/jest/Card.test.tsx | 56 +++++++++++++ packages/components/src/index.ts | 1 + 10 files changed, 262 insertions(+) create mode 100644 .changeset/four-rocks-move.md create mode 100644 packages/components/src/Card/index.ts create mode 100644 packages/components/src/Card/src/Card.module.css create mode 100644 packages/components/src/Card/src/Card.tsx create mode 100644 packages/components/src/Card/src/CardContext.ts create mode 100644 packages/components/src/Card/src/index.ts create mode 100644 packages/components/src/Card/tests/chromatic/Card.stories.tsx create mode 100644 packages/components/src/Card/tests/jest/Card.ssr.test.tsx create mode 100644 packages/components/src/Card/tests/jest/Card.test.tsx diff --git a/.changeset/four-rocks-move.md b/.changeset/four-rocks-move.md new file mode 100644 index 000000000..c8e428576 --- /dev/null +++ b/.changeset/four-rocks-move.md @@ -0,0 +1,5 @@ +--- +"@hopper-ui/components": patch +--- + +Added Card component diff --git a/packages/components/src/Card/index.ts b/packages/components/src/Card/index.ts new file mode 100644 index 000000000..401c73ac2 --- /dev/null +++ b/packages/components/src/Card/index.ts @@ -0,0 +1 @@ +export * from "./src/index.ts"; diff --git a/packages/components/src/Card/src/Card.module.css b/packages/components/src/Card/src/Card.module.css new file mode 100644 index 000000000..b9f2b7363 --- /dev/null +++ b/packages/components/src/Card/src/Card.module.css @@ -0,0 +1,26 @@ +.hop-Card { + /* Internal Variables */ + --width: 100%; + --border-radius: var(--hop-shape-rounded-md); + + /* Main */ + --hop-Card-main-border: var(--hop-space-10) solid var(--hop-neutral-border-weak); + --hop-Card-main-background-color: var(--hop-neutral-surface); + + /* Second */ + --hop-Card-second-border: none; + --hop-Card-second-background-color: var(--hop-neutral-surface-weakest); + + inline-size: var(--width); + border-radius: var(--border-radius); +} + +.hop-Card--main { + background-color: var(--hop-Card-main-background-color); + border: var(--hop-Card-main-border); +} + +.hop-Card--second-level { + background-color: var(--hop-Card-second-background-color); + border: var(--hop-Card-second-border); +} diff --git a/packages/components/src/Card/src/Card.tsx b/packages/components/src/Card/src/Card.tsx new file mode 100644 index 000000000..4d90757b6 --- /dev/null +++ b/packages/components/src/Card/src/Card.tsx @@ -0,0 +1,81 @@ +import { useStyledSystem, type StyledComponentProps } from "@hopper-ui/styled-system"; +import clsx from "clsx"; +import { forwardRef, type CSSProperties, type ForwardedRef } from "react"; +import { HeaderContext, useContextProps } from "react-aria-components"; + +import { ContentContext, FooterContext } from "../../layout/index.ts"; +import { cssModule, SlotProvider, type BaseComponentDOMProps } from "../../utils/index.ts"; + +import { CardContext } from "./CardContext.ts"; + +import styles from "./Card.module.css"; + +export const GlobalCardCssSelector = "hop-Card"; + +export interface CardProps extends StyledComponentProps { + variant?: "main" | "second-level"; +} + +const Card = (props: CardProps, ref: ForwardedRef) => { + [props, ref] = useContextProps(props, ref, CardContext); + const { stylingProps, ...ownProps } = useStyledSystem(props); + const { + className, + children, + style, + slot, + variant = "main", + ...otherProps + } = ownProps; + + const classNames = clsx( + GlobalCardCssSelector, + cssModule( + styles, + "hop-Card", + variant + ), + stylingProps.className, + className + ); + + const mergedStyles: CSSProperties = { + ...style, + ...stylingProps.style + }; + + return ( +
+ + {children} + +
+ ); +}; + +/** + * The Card component represents a Card within a Hopper container such as a Modal or Card. + * + * [View Documentation](TODO) + */ +const _Card = forwardRef(Card); +_Card.displayName = "Card"; + +export { _Card as Card }; diff --git a/packages/components/src/Card/src/CardContext.ts b/packages/components/src/Card/src/CardContext.ts new file mode 100644 index 000000000..3d157a8c6 --- /dev/null +++ b/packages/components/src/Card/src/CardContext.ts @@ -0,0 +1,8 @@ +import { createContext } from "react"; +import type { ContextValue } from "react-aria-components"; + +import type { CardProps } from "./Card.tsx"; + +export const CardContext = createContext>({}); + +CardContext.displayName = "CardContext"; diff --git a/packages/components/src/Card/src/index.ts b/packages/components/src/Card/src/index.ts new file mode 100644 index 000000000..f2aef513e --- /dev/null +++ b/packages/components/src/Card/src/index.ts @@ -0,0 +1,3 @@ +export * from "./Card.tsx"; +export * from "./CardContext.ts"; + diff --git a/packages/components/src/Card/tests/chromatic/Card.stories.tsx b/packages/components/src/Card/tests/chromatic/Card.stories.tsx new file mode 100644 index 000000000..3979558c0 --- /dev/null +++ b/packages/components/src/Card/tests/chromatic/Card.stories.tsx @@ -0,0 +1,64 @@ +import type { Meta, StoryObj } from "@storybook/react"; + +import { Header } from "../../../Header/index.ts"; +import { Content, Footer } from "../../../layout/index.ts"; +import { Card } from "../../src/Card.tsx"; + +const meta = { + title: "Components/Card", + component: Card +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Main = { + render: props => ( + +
+ Header +
+ + Content + +
+ Footer +
+
+ ) +} satisfies Story; + +export const SecondLevel = { + render: props => ( + +
+ Header +
+ + Content + +
+ Footer +
+
+ ) +} satisfies Story; + +export const EmbeddedCard = { + render: props => ( + +
+ Header +
+ + + Embedded + + +
+ Footer +
+
+ ) +} satisfies Story; diff --git a/packages/components/src/Card/tests/jest/Card.ssr.test.tsx b/packages/components/src/Card/tests/jest/Card.ssr.test.tsx new file mode 100644 index 000000000..4a8b4a7c0 --- /dev/null +++ b/packages/components/src/Card/tests/jest/Card.ssr.test.tsx @@ -0,0 +1,17 @@ +/** + * @jest-environment node + */ +import { renderToString } from "react-dom/server"; + +import { Card } from "../../src/Card.tsx"; + +describe("Card", () => { + it("should render on the server", () => { + const renderOnServer = () => + renderToString( + Text + ); + + expect(renderOnServer).not.toThrow(); + }); +}); diff --git a/packages/components/src/Card/tests/jest/Card.test.tsx b/packages/components/src/Card/tests/jest/Card.test.tsx new file mode 100644 index 000000000..c3e420eed --- /dev/null +++ b/packages/components/src/Card/tests/jest/Card.test.tsx @@ -0,0 +1,56 @@ +import { render, screen } from "@hopper-ui/test-utils"; +import { createRef } from "react"; + +import { Card } from "../../src/Card.tsx"; +import { CardContext } from "../../src/CardContext.ts"; + +describe("Card", () => { + it("should render with default class", () => { + render(12); + + const element = screen.getByTestId("Card"); + expect(element).toHaveClass("hop-Card"); + }); + + it("should support custom class", () => { + render(12); + + const element = screen.getByTestId("Card"); + expect(element).toHaveClass("hop-Card"); + expect(element).toHaveClass("test"); + }); + + it("should support custom style", () => { + render(12); + + const element = screen.getByTestId("Card"); + expect(element).toHaveStyle({ marginTop: "var(--hop-space-stack-sm)", marginBottom: "13px" }); + }); + + it("should support DOM props", () => { + render(12); + + const element = screen.getByTestId("Card"); + expect(element).toHaveAttribute("data-foo", "bar"); + }); + + it("should support slots", () => { + render( + + 12 + + ); + + const element = screen.getByTestId("Card"); + expect(element).toHaveAttribute("slot", "test"); + expect(element).toHaveAttribute("aria-label", "test"); + }); + + it("should support refs", () => { + const ref = createRef(); + render(12); + + expect(ref.current).not.toBeNull(); + expect(ref.current instanceof HTMLElement).toBeTruthy(); + }); +}); diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index 299ced646..7f59d4856 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -1,6 +1,7 @@ export * from "./Avatar/index.ts"; export * from "./Badge/index.ts"; export * from "./buttons/index.ts"; +export * from "./Card/index.ts"; export * from "./checkbox/index.ts"; export * from "./ComboBox/index.ts"; export * from "./Divider/index.ts";