Skip to content

Commit

Permalink
Card
Browse files Browse the repository at this point in the history
  • Loading branch information
victortrinh2 committed Oct 30, 2024
1 parent 091d308 commit 238e409
Show file tree
Hide file tree
Showing 10 changed files with 262 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .changeset/four-rocks-move.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@hopper-ui/components": patch
---

Added Card component
1 change: 1 addition & 0 deletions packages/components/src/Card/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./src/index.ts";
26 changes: 26 additions & 0 deletions packages/components/src/Card/src/Card.module.css
Original file line number Diff line number Diff line change
@@ -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);
}
81 changes: 81 additions & 0 deletions packages/components/src/Card/src/Card.tsx
Original file line number Diff line number Diff line change
@@ -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<BaseComponentDOMProps> {
variant?: "main" | "second-level";
}

const Card = (props: CardProps, ref: ForwardedRef<HTMLElement>) => {
[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 (
<section
ref={ref}
className={classNames}
style={mergedStyles}
slot={slot ?? undefined}
{...otherProps}
>
<SlotProvider values={[
[HeaderContext, {
className: styles["hop-Card__header"]
}],
[ContentContext, {
className: styles["hop-Card__content"]
}],
[FooterContext, {
className: styles["hop-Card__footer"]
}]
]}
>
{children}
</SlotProvider>
</section>
);
};

/**
* The Card component represents a Card within a Hopper container such as a Modal or Card.
*
* [View Documentation](TODO)
*/
const _Card = forwardRef<HTMLElement, CardProps>(Card);
_Card.displayName = "Card";

export { _Card as Card };
8 changes: 8 additions & 0 deletions packages/components/src/Card/src/CardContext.ts
Original file line number Diff line number Diff line change
@@ -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<ContextValue<CardProps, HTMLElement>>({});

CardContext.displayName = "CardContext";
3 changes: 3 additions & 0 deletions packages/components/src/Card/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from "./Card.tsx";
export * from "./CardContext.ts";

64 changes: 64 additions & 0 deletions packages/components/src/Card/tests/chromatic/Card.stories.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof Card>;

export default meta;

type Story = StoryObj<typeof meta>;

export const Main = {
render: props => (
<Card {...props}>
<Header>
Header
</Header>
<Content>
Content
</Content>
<Footer>
Footer
</Footer>
</Card>
)
} satisfies Story;

export const SecondLevel = {
render: props => (
<Card variant="second-level" {...props}>
<Header>
Header
</Header>
<Content>
Content
</Content>
<Footer>
Footer
</Footer>
</Card>
)
} satisfies Story;

export const EmbeddedCard = {
render: props => (
<Card variant="second-level" {...props}>
<Header>
Header
</Header>
<Content>
<Card>
Embedded
</Card>
</Content>
<Footer>
Footer
</Footer>
</Card>
)
} satisfies Story;
17 changes: 17 additions & 0 deletions packages/components/src/Card/tests/jest/Card.ssr.test.tsx
Original file line number Diff line number Diff line change
@@ -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(
<Card>Text</Card>
);

expect(renderOnServer).not.toThrow();
});
});
56 changes: 56 additions & 0 deletions packages/components/src/Card/tests/jest/Card.test.tsx
Original file line number Diff line number Diff line change
@@ -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(<Card data-testid="Card">12</Card>);

const element = screen.getByTestId("Card");
expect(element).toHaveClass("hop-Card");
});

it("should support custom class", () => {
render(<Card data-testid="Card" className="test">12</Card>);

const element = screen.getByTestId("Card");
expect(element).toHaveClass("hop-Card");
expect(element).toHaveClass("test");
});

it("should support custom style", () => {
render(<Card data-testid="Card" marginTop="stack-sm" style={{ marginBottom: "13px" }} >12</Card>);

const element = screen.getByTestId("Card");
expect(element).toHaveStyle({ marginTop: "var(--hop-space-stack-sm)", marginBottom: "13px" });
});

it("should support DOM props", () => {
render(<Card data-testid="Card" data-foo="bar">12</Card>);

const element = screen.getByTestId("Card");
expect(element).toHaveAttribute("data-foo", "bar");
});

it("should support slots", () => {
render(
<CardContext.Provider value={{ slots: { test: { "aria-label": "test" } } }}>
<Card data-testid="Card" slot="test">12</Card>
</CardContext.Provider>
);

const element = screen.getByTestId("Card");
expect(element).toHaveAttribute("slot", "test");
expect(element).toHaveAttribute("aria-label", "test");
});

it("should support refs", () => {
const ref = createRef<HTMLElement>();
render(<Card data-testid="Card" ref={ref}>12</Card>);

expect(ref.current).not.toBeNull();
expect(ref.current instanceof HTMLElement).toBeTruthy();
});
});
1 change: 1 addition & 0 deletions packages/components/src/index.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down

0 comments on commit 238e409

Please sign in to comment.