- {shapes.map((shape) => (
-
-
- {types.map((type) => (
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-
- {children}
-
- ))}
-
- ))}
+ args: {
+ onClick: undefined,
+ },
+ argTypes: {
+ type: hiddenArgControl,
+ shape: hiddenArgControl,
+ },
+ render: ({ children, ...args }) => (
+
+ {shapes.map((shape) => (
+
+
+ {types.map((type) => (
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+
+ {children}
+
+ ))}
- ),
-};
+ ))}
+
+ ),
+}
export const CustomClassName: Story = {
- render: ({ children }) =>
{children} ,
-};
+ render: ({ children }) => (
+
{children}
+ ),
+}
diff --git a/src/components/badge/badge.test.tsx b/src/components/badge/badge.test.tsx
index 802908a9..5d9d8a4b 100644
--- a/src/components/badge/badge.test.tsx
+++ b/src/components/badge/badge.test.tsx
@@ -1,30 +1,29 @@
-import { render, screen } from "@testing-library/react";
-import React from "react";
-import { describe, expect, it } from "vitest";
-import { Badge } from "./badge";
+import { render, screen } from '@testing-library/react'
+import { describe, expect, it } from 'vitest'
+import { Badge } from './badge'
-describe("Badge", () => {
- it("renders a badge with default appearance and passed string", () => {
- const text = "Default Badge";
- // ARRANGE
- render(
{text} );
+describe('Badge', () => {
+ it('renders a badge with default appearance and passed string', () => {
+ const text = 'Default Badge'
+ // ARRANGE
+ render(
{text} )
- // ASSERT
- const badge = screen.getByText(text);
- expect(badge).toBeInTheDocument();
- expect(badge).toHaveAttribute("role", "button");
- expect(badge).toHaveClass("rounded-sm");
- });
+ // ASSERT
+ const badge = screen.getByText(text)
+ expect(badge).toBeInTheDocument()
+ expect(badge).toHaveAttribute('role', 'button')
+ expect(badge).toHaveClass('rounded-sm')
+ })
- it("renders a badge with rounded-sm appearance and passed string", () => {
- const text = "Rounded Badge";
- // ARRANGE
- render(
{text} );
+ it('renders a badge with rounded-sm appearance and passed string', () => {
+ const text = 'Rounded Badge'
+ // ARRANGE
+ render(
{text} )
- // ASSERT
- const badge = screen.getByText(text);
- expect(badge).toBeInTheDocument();
- expect(badge).toHaveAttribute("role", "button");
- expect(badge).toHaveClass("rounded-full");
- });
-});
+ // ASSERT
+ const badge = screen.getByText(text)
+ expect(badge).toBeInTheDocument()
+ expect(badge).toHaveAttribute('role', 'button')
+ expect(badge).toHaveClass('rounded-full')
+ })
+})
diff --git a/src/components/badge/badge.tsx b/src/components/badge/badge.tsx
index 57387cdb..8169070d 100644
--- a/src/components/badge/badge.tsx
+++ b/src/components/badge/badge.tsx
@@ -1,59 +1,59 @@
-import React, { ReactNode } from "react";
-import { classNames } from "../../util/class-names";
+import { ReactNode } from 'react'
+import { classNames } from '../../util/class-names'
const buttonVariants = {
- primary: "bg-primary-100 text-primary-500",
- violet: "bg-violet-100 text-violet-800",
- green: "bg-success-100 text-success-600",
- neutral: "bg-neutral-200 text-neutral-800",
- yellow: "bg-warning-100 text-warning-600",
- teal: "bg-teal-100 text-teal-800",
- orange: "bg-orange-100 text-orange-800",
- pink: "bg-pink-300 text-pink-800",
- red: "bg-danger-100 text-danger-600",
- purple: "bg-purple-100 text-purple-800",
-};
+ primary: 'bg-primary-100 text-primary-500',
+ violet: 'bg-violet-100 text-violet-800',
+ green: 'bg-success-100 text-success-600',
+ neutral: 'bg-neutral-200 text-neutral-800',
+ yellow: 'bg-warning-100 text-warning-600',
+ teal: 'bg-teal-100 text-teal-800',
+ orange: 'bg-orange-100 text-orange-800',
+ pink: 'bg-pink-300 text-pink-800',
+ red: 'bg-danger-100 text-danger-600',
+ purple: 'bg-purple-100 text-purple-800',
+}
const shapeVariants = {
- rounded: "rounded-full",
- default: "rounded-sm",
-};
+ rounded: 'rounded-full',
+ default: 'rounded-sm',
+}
-export type BadgeType = keyof typeof buttonVariants;
+export type BadgeType = keyof typeof buttonVariants
export interface BadgeProps {
- type?: BadgeType;
- shape?: keyof typeof shapeVariants;
- children: ReactNode;
- onClick?: () => void;
- className?: string;
+ type?: BadgeType
+ shape?: keyof typeof shapeVariants
+ children: ReactNode
+ onClick?: () => void
+ className?: string
}
export const Badge = ({
- type = "primary",
- shape = "default",
- children,
- onClick,
- className,
+ type = 'primary',
+ shape = 'default',
+ children,
+ onClick,
+ className,
}: BadgeProps) => {
- const interactiveVariant = onClick ? "cursor-pointer" : "pointer-events-none";
+ const interactiveVariant = onClick ? 'cursor-pointer' : 'pointer-events-none'
- return (
-
- {children}
-
- );
-};
+ return (
+
+ {children}
+
+ )
+}
diff --git a/src/components/badge/index.ts b/src/components/badge/index.ts
index 33475df4..4365d9bd 100644
--- a/src/components/badge/index.ts
+++ b/src/components/badge/index.ts
@@ -1 +1 @@
-export { Badge, BadgeType } from "./badge";
+export { Badge, BadgeType } from './badge'
diff --git a/src/components/breadcrumb-navigation/breadcrumb-navigation-arrow.tsx b/src/components/breadcrumb-navigation/breadcrumb-navigation-arrow.tsx
index 8ce02107..26263e54 100644
--- a/src/components/breadcrumb-navigation/breadcrumb-navigation-arrow.tsx
+++ b/src/components/breadcrumb-navigation/breadcrumb-navigation-arrow.tsx
@@ -1,6 +1,5 @@
-import React from "react";
-import { ChevronRightIcon } from "../../icons";
+import { ChevronRightIcon } from '../../icons'
export const BreadcrumbNavigationArrow = () => {
- return
;
-};
+ return
+}
diff --git a/src/components/breadcrumb-navigation/breadcrumb-navigation-item.tsx b/src/components/breadcrumb-navigation/breadcrumb-navigation-item.tsx
index 3bb91698..926039ce 100644
--- a/src/components/breadcrumb-navigation/breadcrumb-navigation-item.tsx
+++ b/src/components/breadcrumb-navigation/breadcrumb-navigation-item.tsx
@@ -1,26 +1,28 @@
-import React from "react";
-import { classNames } from "../../util/class-names";
-import { AsChildProps, Slot } from "../slot/slot";
+import type { AnchorHTMLAttributes, FC } from 'react'
+import { classNames } from '../../util/class-names'
+import { AsChildProps, Slot } from '../slot/slot'
-type BreadcrumbNavigationItemProps = AsChildProps
> & {
- isActive?: boolean;
-};
+type BreadcrumbNavigationItemProps = AsChildProps<
+ AnchorHTMLAttributes
+> & {
+ isActive?: boolean
+}
-export const BreadcrumbNavigationItem: React.FC = ({
- isActive,
- asChild,
- ...props
+export const BreadcrumbNavigationItem: FC = ({
+ isActive,
+ asChild,
+ ...props
}) => {
- const Comp = asChild ? Slot : "a";
+ const Comp = asChild ? Slot : 'a'
- return (
-
- );
-};
+ return (
+
+ )
+}
diff --git a/src/components/breadcrumb-navigation/breadcrumb-navigation.stories.tsx b/src/components/breadcrumb-navigation/breadcrumb-navigation.stories.tsx
index 6f5bd975..e83d9d41 100644
--- a/src/components/breadcrumb-navigation/breadcrumb-navigation.stories.tsx
+++ b/src/components/breadcrumb-navigation/breadcrumb-navigation.stories.tsx
@@ -1,33 +1,32 @@
-import type { Meta, StoryObj } from "@storybook/react";
-import React from "react";
-import { BreadcrumbNavigation } from "./breadcrumb-navigation";
+import type { Meta, StoryObj } from '@storybook/react-vite'
+import { BreadcrumbNavigation } from './breadcrumb-navigation'
const meta: Meta = {
- title: "Breadcrumb Navigation",
- component: BreadcrumbNavigation,
- args: {},
-};
+ title: 'Breadcrumb Navigation',
+ component: BreadcrumbNavigation,
+ args: {},
+}
-export default meta;
+export default meta
-type Story = StoryObj;
+type Story = StoryObj
export const Base: Story = {
- render: () => (
-
-
- Home
+ render: () => (
+
+
+ Home
-
+
- Library
+ Library
-
+
-
- Book
-
-
-
- ),
-};
+
+ Book
+
+
+
+ ),
+}
diff --git a/src/components/breadcrumb-navigation/breadcrumb-navigation.test.tsx b/src/components/breadcrumb-navigation/breadcrumb-navigation.test.tsx
new file mode 100644
index 00000000..fdb7f53f
--- /dev/null
+++ b/src/components/breadcrumb-navigation/breadcrumb-navigation.test.tsx
@@ -0,0 +1,19 @@
+import { describe, expect, it } from 'vitest'
+import { render, screen } from '@testing-library/react'
+import { BreadcrumbNavigation } from './breadcrumb-navigation'
+
+describe('BreadcrumbNavigation', () => {
+ it('renders items and separators', () => {
+ const { container } = render(
+
+ Home
+
+ Settings
+ ,
+ )
+
+ expect(screen.getByText('Home')).toBeInTheDocument()
+ expect(screen.getByText('Settings')).toBeInTheDocument()
+ expect(container.querySelector('svg')).toBeInTheDocument()
+ })
+})
diff --git a/src/components/breadcrumb-navigation/breadcrumb-navigation.tsx b/src/components/breadcrumb-navigation/breadcrumb-navigation.tsx
index bc25258d..203b346a 100644
--- a/src/components/breadcrumb-navigation/breadcrumb-navigation.tsx
+++ b/src/components/breadcrumb-navigation/breadcrumb-navigation.tsx
@@ -1,16 +1,16 @@
-import React from "react";
-import { BreadcrumbNavigationItem } from "./breadcrumb-navigation-item";
-import { BreadcrumbNavigationArrow } from "./breadcrumb-navigation-arrow";
+import type { ReactNode } from 'react'
+import { BreadcrumbNavigationItem } from './breadcrumb-navigation-item'
+import { BreadcrumbNavigationArrow } from './breadcrumb-navigation-arrow'
export interface BreadcrumbNavigationProps {
- children: React.ReactNode;
+ children: ReactNode
}
const BreadcrumbNavigation = ({ children }: BreadcrumbNavigationProps) => {
- return {children} ;
-};
+ return {children}
+}
-BreadcrumbNavigation.Item = BreadcrumbNavigationItem;
-BreadcrumbNavigation.Arrow = BreadcrumbNavigationArrow;
+BreadcrumbNavigation.Item = BreadcrumbNavigationItem
+BreadcrumbNavigation.Arrow = BreadcrumbNavigationArrow
-export { BreadcrumbNavigation };
+export { BreadcrumbNavigation }
diff --git a/src/components/breadcrumb-navigation/index.tsx b/src/components/breadcrumb-navigation/index.tsx
index 40689950..5d780345 100644
--- a/src/components/breadcrumb-navigation/index.tsx
+++ b/src/components/breadcrumb-navigation/index.tsx
@@ -1 +1 @@
-export { BreadcrumbNavigation } from "./breadcrumb-navigation";
+export { BreadcrumbNavigation } from './breadcrumb-navigation'
diff --git a/src/components/button-group/button-group.stories.tsx b/src/components/button-group/button-group.stories.tsx
index 86d965d5..f1c23bed 100644
--- a/src/components/button-group/button-group.stories.tsx
+++ b/src/components/button-group/button-group.stories.tsx
@@ -1,34 +1,53 @@
-import type { Meta, StoryObj } from "@storybook/react";
-import React from "react";
-import { ButtonGroup } from "./button-group";
+import type { Meta, StoryObj } from '@storybook/react-vite'
+import { ButtonGroup } from './button-group'
const meta: Meta = {
- title: "ButtonGroup",
- component: ButtonGroup,
- parameters: {
- options: {
- showPanel: false,
- },
+ title: 'ButtonGroup',
+ component: ButtonGroup,
+ args: {
+ normalLabel: 'Normal',
+ disabledLabel: 'Disabled',
+ activeLabel: 'Active',
+ activeDisabledLabel: 'Active & Disabled',
+ extraLabel: 'Button 1',
+ },
+ argTypes: {
+ normalLabel: { control: 'text' },
+ disabledLabel: { control: 'text' },
+ activeLabel: { control: 'text' },
+ activeDisabledLabel: { control: 'text' },
+ extraLabel: { control: 'text' },
+ },
+ parameters: {
+ options: {
+ showPanel: false,
},
-};
+ },
+}
-export default meta;
-type Story = StoryObj;
+export default meta
+type Story = StoryObj
export const Default: Story = {
- render: () => (
-
- Normal
-
- Disabled
-
-
- Active
-
-
- Active & Disabled
-
- Button 1
-
- ),
-};
+ render: ({
+ normalLabel,
+ disabledLabel,
+ activeLabel,
+ activeDisabledLabel,
+ extraLabel,
+ }) => (
+
+ {normalLabel}
+
+ {disabledLabel}
+
+
+ {activeLabel}
+
+
+ {activeDisabledLabel}
+
+ {extraLabel}
+
+ ),
+}
diff --git a/src/components/button-group/button-group.test.tsx b/src/components/button-group/button-group.test.tsx
new file mode 100644
index 00000000..257db1f6
--- /dev/null
+++ b/src/components/button-group/button-group.test.tsx
@@ -0,0 +1,23 @@
+import { describe, expect, it } from 'vitest'
+import { render, screen } from '@testing-library/react'
+import { ButtonGroup } from './button-group'
+
+describe('ButtonGroup', () => {
+ it('renders grouped buttons and highlights active state', () => {
+ const { container } = render(
+
+ Alpha
+
+ Beta
+
+ ,
+ )
+
+ const buttons = screen.getAllByRole('button')
+ expect(buttons).toHaveLength(2)
+ expect(buttons[1]).toHaveClass('bg-primary-50')
+
+ const group = container.firstElementChild
+ expect(group).toHaveClass('isolate')
+ })
+})
diff --git a/src/components/button-group/button-group.tsx b/src/components/button-group/button-group.tsx
index b9b2cbbd..e61947dd 100644
--- a/src/components/button-group/button-group.tsx
+++ b/src/components/button-group/button-group.tsx
@@ -1,42 +1,43 @@
-import React from "react";
-import { classNames } from "../../util/class-names";
+import type { ComponentPropsWithoutRef } from 'react'
+import { ReactNode } from 'react'
+import { classNames } from '../../util/class-names'
-interface ButtonProps extends React.ComponentPropsWithoutRef<"button"> {
- children?: React.ReactNode;
- type: "button" | "submit" | "reset";
- isActive?: boolean;
+interface ButtonProps extends ComponentPropsWithoutRef<'button'> {
+ children?: ReactNode
+ type: 'button' | 'submit' | 'reset'
+ isActive?: boolean
}
const Button = ({ children, type, isActive, ...props }: ButtonProps) => {
- return (
-
- {children}
-
- );
-};
+ return (
+
+ {children}
+
+ )
+}
interface ButtonGroupProps {
- children?: React.ReactNode;
+ children?: ReactNode
}
const ButtonGroup = ({ children }: ButtonGroupProps) => {
- return (
-
- {children}
-
- );
-};
+ return (
+
+ {children}
+
+ )
+}
-ButtonGroup.Button = Button;
+ButtonGroup.Button = Button
-export { ButtonGroup };
+export { ButtonGroup }
diff --git a/src/components/button-group/index.tsx b/src/components/button-group/index.tsx
index 4f0b2848..cc99d7e5 100644
--- a/src/components/button-group/index.tsx
+++ b/src/components/button-group/index.tsx
@@ -1 +1 @@
-export { ButtonGroup } from "./button-group";
+export { ButtonGroup } from './button-group'
diff --git a/src/components/button/button.stories.tsx b/src/components/button/button.stories.tsx
index 2f633cce..6c5acb32 100644
--- a/src/components/button/button.stories.tsx
+++ b/src/components/button/button.stories.tsx
@@ -1,65 +1,64 @@
-import type { Meta, StoryObj } from "@storybook/react";
-import React from "react";
-import { Button, ButtonProps } from "./button";
-import { ChatIcon, DiagramTreeIcon, LockIcon } from "../../icons";
-import { hiddenArgControl } from "../../util/storybook-utils";
+import type { Meta, StoryObj } from '@storybook/react-vite'
+import { Button, ButtonProps } from './button'
+import { ChatIcon, DiagramTreeIcon, LockIcon } from '../../icons'
+import { hiddenArgControl } from '../../util/storybook-utils'
-const variants: ButtonProps["variant"][] = [
- "primary",
- "secondary",
- "minimal",
- "danger",
- "danger-secondary",
-];
-const icons = { undefined, ChatIcon, DiagramTreeIcon, LockIcon };
+const variants: ButtonProps['variant'][] = [
+ 'primary',
+ 'secondary',
+ 'minimal',
+ 'danger',
+ 'danger-secondary',
+]
+const icons = { undefined, ChatIcon, DiagramTreeIcon, LockIcon }
const iconArg = {
- description: "Icon component",
- options: Object.keys(icons),
- mapping: icons,
-};
+ description: 'Icon component',
+ options: Object.keys(icons),
+ mapping: icons,
+}
const meta: Meta = {
- title: "Button",
- component: Button,
- args: {
- children: "Badge Label",
- LeftIcon: undefined,
- RightIcon: undefined,
- loading: false,
- },
- argTypes: {
- onClick: hiddenArgControl,
- LeftIcon: iconArg,
- RightIcon: iconArg,
- },
-};
+ title: 'Button',
+ component: Button,
+ args: {
+ children: 'Badge Label',
+ LeftIcon: undefined,
+ RightIcon: undefined,
+ loading: false,
+ },
+ argTypes: {
+ onClick: hiddenArgControl,
+ LeftIcon: iconArg,
+ RightIcon: iconArg,
+ },
+}
-export default meta;
-type Story = StoryObj;
+export default meta
+type Story = StoryObj
-export const Base: Story = {};
+export const Base: Story = {}
export const WithIcons: Story = {
- args: {
- LeftIcon: icons.ChatIcon,
- RightIcon: icons.LockIcon,
- },
-};
+ args: {
+ LeftIcon: icons.ChatIcon,
+ RightIcon: icons.LockIcon,
+ },
+}
export const Loading: Story = {
- args: { loading: true },
-};
+ args: { loading: true },
+}
export const Disabled: Story = {
- args: { disabled: true },
-};
+ args: { disabled: true },
+}
export const Types: Story = {
- argTypes: { type: hiddenArgControl },
- render: ({ children, ...args }) => (
-
- {variants.map((variant) => (
-
- {children}
-
- ))}
-
- ),
-};
+ argTypes: { type: hiddenArgControl },
+ render: ({ children, ...args }) => (
+
+ {variants.map((variant) => (
+
+ {children}
+
+ ))}
+
+ ),
+}
diff --git a/src/components/button/button.test.tsx b/src/components/button/button.test.tsx
index ba3d4c39..8f7de906 100644
--- a/src/components/button/button.test.tsx
+++ b/src/components/button/button.test.tsx
@@ -1,124 +1,123 @@
-import { describe, expect, it, vi } from "vitest";
-import { fireEvent, render, screen } from "@testing-library/react";
-import React from "react";
-import { Button } from "./button";
-import { AddIcon } from "../../icons";
+import { describe, expect, it, vi } from 'vitest'
+import { fireEvent, render, screen } from '@testing-library/react'
+import { Button } from './button'
+import { AddIcon } from '../../icons'
-describe("Button", () => {
- it("renders a button with text and button type", () => {
- const text = "Button Type";
- // ARRANGE
- render(
- null}>
- {text}
-
- );
+describe('Button', () => {
+ it('renders a button with text and button type', () => {
+ const text = 'Button Type'
+ // ARRANGE
+ render(
+ null}>
+ {text}
+ ,
+ )
- // ASSERT
- const button = screen.getByText(text);
- expect(button).toBeInTheDocument();
- expect(button).toHaveAttribute("type", "button");
- });
+ // ASSERT
+ const button = screen.getByText(text)
+ expect(button).toBeInTheDocument()
+ expect(button).toHaveAttribute('type', 'button')
+ })
- it("renders a button with text and submit type", () => {
- const text = "Submit Type";
- // ARRANGE
- render(
- null}>
- {text}
-
- );
+ it('renders a button with text and submit type', () => {
+ const text = 'Submit Type'
+ // ARRANGE
+ render(
+ null}>
+ {text}
+ ,
+ )
- // ASSERT
- const button = screen.getByText(text);
- expect(button).toBeInTheDocument();
- expect(button).toHaveAttribute("type", "submit");
- });
+ // ASSERT
+ const button = screen.getByText(text)
+ expect(button).toBeInTheDocument()
+ expect(button).toHaveAttribute('type', 'submit')
+ })
- it("renders a button with text and left icon", () => {
- const text = "Left icon";
- // ARRANGE
- render(
- null}>
- {text}
-
- );
+ it('renders a button with text and left icon', () => {
+ const text = 'Left icon'
+ // ARRANGE
+ render(
+ null}>
+ {text}
+ ,
+ )
- // ASSERT
- const button = screen.getByText(text);
- expect(button).toBeInTheDocument();
- expect(button).toHaveAttribute("type", "button");
- expect(button.firstChild?.nodeName.toLowerCase()).toBe("svg");
- });
+ // ASSERT
+ const button = screen.getByText(text)
+ expect(button).toBeInTheDocument()
+ expect(button).toHaveAttribute('type', 'button')
+ expect(button.firstChild?.nodeName.toLowerCase()).toBe('svg')
+ })
- it("renders a button with text and right icon", () => {
- const text = "Right icon";
- // ARRANGE
- render(
- null}>
- {text}
-
- );
+ it('renders a button with text and right icon', () => {
+ const text = 'Right icon'
+ // ARRANGE
+ render(
+ null}>
+ {text}
+ ,
+ )
- // ASSERT
- const button = screen.getByText(text);
- expect(button).toBeInTheDocument();
- expect(button).toHaveAttribute("type", "button");
- expect(button.lastChild?.nodeName.toLowerCase()).toBe("svg");
- });
+ // ASSERT
+ const button = screen.getByText(text)
+ expect(button).toBeInTheDocument()
+ expect(button).toHaveAttribute('type', 'button')
+ expect(button.lastChild?.nodeName.toLowerCase()).toBe('svg')
+ })
- it("renders a button with text and onClick", () => {
- const text = "Onclick button";
- const mock = vi.fn();
- // ARRANGE
- render(
-
- {text}
-
- );
+ it('renders a button with text and onClick', () => {
+ const text = 'Onclick button'
+ const mock = vi.fn()
+ // ARRANGE
+ render(
+
+ {text}
+ ,
+ )
- // ASSERT
- const button = screen.getByText(text);
- expect(button).toBeInTheDocument();
- expect(button).toHaveAttribute("type", "button");
- fireEvent.click(button);
- expect(mock).toHaveBeenCalledTimes(1);
- });
+ // ASSERT
+ const button = screen.getByText(text)
+ expect(button).toBeInTheDocument()
+ expect(button).toHaveAttribute('type', 'button')
+ fireEvent.click(button)
+ expect(mock).toHaveBeenCalledTimes(1)
+ })
- it("renders a button with text and disabled state", () => {
- const text = "Disabled button";
- const mock = vi.fn();
- // ARRANGE
- render(
-
- {text}
-
- );
+ it('renders a button with text and disabled state', () => {
+ const text = 'Disabled button'
+ const mock = vi.fn()
+ // ARRANGE
+ render(
+
+ {text}
+ ,
+ )
- // ASSERT
- const button = screen.getByText(text);
- expect(button).toBeInTheDocument();
- expect(button).toHaveAttribute("type", "button");
- expect(button).toBeDisabled();
- fireEvent.click(button);
- expect(mock).toHaveBeenCalledTimes(0);
- });
+ // ASSERT
+ const button = screen.getByText(text)
+ expect(button).toBeInTheDocument()
+ expect(button).toHaveAttribute('type', 'button')
+ expect(button).toBeDisabled()
+ fireEvent.click(button)
+ expect(mock).toHaveBeenCalledTimes(0)
+ })
- it("renders a button with loading state", () => {
- const text = "Loading button";
- const mock = vi.fn();
- // ARRANGE
- render(
-
- {text}
-
- );
+ it('renders a button with loading state', () => {
+ const text = 'Loading button'
+ const mock = vi.fn()
+ // ARRANGE
+ render(
+
+ {text}
+ ,
+ )
- // ASSERT
- const button = screen.getByText(text);
- expect(button).toBeInTheDocument();
- expect(button).toHaveAttribute("type", "button");
- expect(button.firstChild?.nodeName.toLowerCase()).toBe("svg");
- expect(button.firstChild).toHaveClass("animate-spin");
- });
-});
+ // ASSERT
+ const button = screen.getByText(text)
+ expect(button).toBeInTheDocument()
+ expect(button).toHaveAttribute('type', 'button')
+ expect(button.firstChild?.nodeName.toLowerCase()).toBe('svg')
+ expect(button.firstChild).toHaveClass('animate-spin')
+ })
+})
diff --git a/src/components/button/button.tsx b/src/components/button/button.tsx
index b584f6ae..c30c4d2e 100644
--- a/src/components/button/button.tsx
+++ b/src/components/button/button.tsx
@@ -1,62 +1,74 @@
+import type { ButtonHTMLAttributes, ElementType } from 'react'
/* eslint-disable react/button-has-type */
-import React from "react";
-import { classNames } from "../../util/class-names";
-import { Spinner } from "../spinner";
+import { classNames } from '../../util/class-names'
+import { Spinner } from '../spinner'
+
+const baseClasses =
+ 'group flex h-8 items-center gap-2 whitespace-nowrap rounded-sm px-4 text-xs font-semibold focus:outline-hidden disabled:cursor-not-allowed'
const buttonVariants = {
- primary:
- "bg-primary-500 text-neutral-0 hover:bg-primary-600 active:bg-primary-600 focus:ring-2 focus:ring-primary-200 focus:bg-primary-600 disabled:bg-primary-200 fill-neutral-0",
- secondary:
- "text-neutral-700 bg-neutral-0 border border-neutral-400 hover:border-neutral-600 hover:text-neutral-800 active:bg-neutral-100 focus:ring-2 focus:ring-primary-200 focus:text-neutral-800 disabled:text-neutral-500 disabled:border-neutral-300 disabled:bg-neutral-0 fill-neutral-0",
- minimal:
- "text-neutral-700 hover:bg-neutral-100 hover:text-neutral-800 active:bg-neutral-200 focus:ring-2 focus:ring-primary-200 focus:text-neutral-800 disabled:text-neutral-500 disabled:bg-neutral-0 fill-neutral-0 underline",
- danger: "text-neutral-0 bg-danger-500 hover:bg-danger-500 active:bg-danger-700 focus:ring-2 focus:ring-danger-100 focus:bg-danger-600 disabled:bg-danger-100 fill-neutral-0",
- "danger-secondary":
- "bg-neutral-0 text-danger-500 border border-danger-400 hover:bg-danger-50 hover:text-danger-600 active:border-danger-700 active:text-danger-700 active:bg-danger-100 focus:ring-2 focus:ring-danger-100 focus:text-danger-600 disabled:border-danger-100 disabled:text-danger-100 disabled:bg-neutral-0 fill-danger-600 disabled:fill-danger-100",
-};
+ primary:
+ 'bg-primary-500 text-neutral-0 hover:bg-primary-600 active:bg-primary-600 focus:ring-2 focus:ring-primary-200 focus:bg-primary-600 disabled:bg-primary-200 fill-neutral-0',
+ secondary:
+ 'text-neutral-700 bg-neutral-0 border border-neutral-400 hover:border-neutral-600 hover:text-neutral-800 active:bg-neutral-100 focus:ring-2 focus:ring-primary-200 focus:text-neutral-800 disabled:text-neutral-500 disabled:border-neutral-300 disabled:bg-neutral-0 fill-neutral-0',
+ minimal:
+ 'text-neutral-700 hover:bg-neutral-100 hover:text-neutral-800 active:bg-neutral-200 focus:ring-2 focus:ring-primary-200 focus:text-neutral-800 disabled:text-neutral-500 disabled:bg-neutral-0 fill-neutral-0 underline',
+ danger:
+ 'text-neutral-0 bg-danger-500 hover:bg-danger-500 active:bg-danger-700 focus:ring-2 focus:ring-danger-100 focus:bg-danger-600 disabled:bg-danger-100 fill-neutral-0',
+ 'danger-secondary':
+ 'bg-neutral-0 text-danger-500 border border-danger-400 hover:bg-danger-50 hover:text-danger-600 active:border-danger-700 active:text-danger-700 active:bg-danger-100 focus:ring-2 focus:ring-danger-100 focus:text-danger-600 disabled:border-danger-100 disabled:text-danger-100 disabled:bg-neutral-0 fill-danger-600 disabled:fill-danger-100',
+}
const iconVariants = {
- primary: "text-neutral-0",
- secondary:
- "fill-neutral-600 group-hover:text-neutral-700 group-focus:text-neutral-700 group-disabled:text-neutral-400",
- minimal:
- "fill-neutral-600 group-hover:text-neutral-700 group-focus:text-neutral-700 group-disabled:text-neutral-400",
- danger: "",
- "danger-secondary": "",
-};
-
-export interface ButtonProps extends React.ButtonHTMLAttributes {
- variant?: keyof typeof buttonVariants;
- loading?: boolean;
- LeftIcon?: React.ElementType;
- RightIcon?: React.ElementType;
+ primary: 'text-neutral-0',
+ secondary:
+ 'fill-neutral-600 group-hover:text-neutral-700 group-focus:text-neutral-700 group-disabled:text-neutral-400',
+ minimal:
+ 'fill-neutral-600 group-hover:text-neutral-700 group-focus:text-neutral-700 group-disabled:text-neutral-400',
+ danger: '',
+ 'danger-secondary': '',
}
+export interface ButtonProps extends ButtonHTMLAttributes {
+ variant?: keyof typeof buttonVariants
+ loading?: boolean
+ LeftIcon?: ElementType
+ RightIcon?: ElementType
+}
+
+const getButtonClassName = (
+ variant: keyof typeof buttonVariants,
+ className?: string,
+) => classNames(baseClasses, buttonVariants[variant], className)
+
+const getButtonIconClassName = (variant: keyof typeof iconVariants) =>
+ `${iconVariants[variant]} h-3 w-3`
+
const Button = ({
- variant = "primary",
- className,
- children,
- loading,
- LeftIcon,
- RightIcon,
- ...props
+ variant = 'primary',
+ className,
+ children,
+ loading,
+ LeftIcon,
+ RightIcon,
+ ...props
}: ButtonProps) => {
- const commonClasses = classNames(
- `group flex h-8 items-center gap-2 whitespace-nowrap rounded-sm px-4 text-xs font-semibold focus:outline-hidden disabled:cursor-not-allowed`,
- buttonVariants[variant],
- className
- );
-
- return (
-
- {loading ? : null}
- {LeftIcon && !loading ? (
-
- ) : null}
- {children}
- {RightIcon ? : null}
-
- );
-};
-
-export { Button };
+ const commonClasses = getButtonClassName(variant, className)
+
+ return (
+
+ {loading ?
+
+ : null}
+ {LeftIcon && !loading ?
+
+ : null}
+ {children}
+ {RightIcon ?
+
+ : null}
+
+ )
+}
+
+export { Button, buttonVariants, getButtonClassName, getButtonIconClassName }
diff --git a/src/components/button/index.ts b/src/components/button/index.ts
index 398a3c50..db783b10 100644
--- a/src/components/button/index.ts
+++ b/src/components/button/index.ts
@@ -1 +1 @@
-export { Button } from "./button";
+export { Button } from './button'
diff --git a/src/components/checkbox/checkbox.stories.tsx b/src/components/checkbox/checkbox.stories.tsx
index 494ca2d7..d1dbd42f 100644
--- a/src/components/checkbox/checkbox.stories.tsx
+++ b/src/components/checkbox/checkbox.stories.tsx
@@ -1,31 +1,31 @@
-import type { Meta, StoryObj } from "@storybook/react";
-import React, { useState } from "react";
+import type { Meta, StoryObj } from '@storybook/react-vite'
+import { useState } from 'react'
-import { Checkbox } from "./checkbox";
-import { hiddenArgControl } from "../../util/storybook-utils";
+import { Checkbox } from './checkbox'
+import { hiddenArgControl } from '../../util/storybook-utils'
const meta: Meta = {
- title: "Checkbox",
- component: Checkbox,
- args: {
- id: "checkbox-id",
- label: "Checkbox label",
- disabled: false,
- },
- argTypes: {
- checked: hiddenArgControl,
- onChange: hiddenArgControl,
- },
- render: (args) => {
- // eslint-disable-next-line react-hooks/rules-of-hooks
- const [checked, setChecked] = useState(false);
- const toggleCheck = () => setChecked((val) => !val);
+ title: 'Checkbox',
+ component: Checkbox,
+ args: {
+ id: 'checkbox-id',
+ label: 'Checkbox label',
+ disabled: false,
+ },
+ argTypes: {
+ checked: hiddenArgControl,
+ onChange: hiddenArgControl,
+ },
+ render: (args) => {
+ // eslint-disable-next-line react-hooks/rules-of-hooks
+ const [checked, setChecked] = useState(false)
+ const toggleCheck = () => setChecked((val) => !val)
- return ;
- },
-};
+ return
+ },
+}
-export default meta;
-type Story = StoryObj;
+export default meta
+type Story = StoryObj
-export const Default: Story = {};
+export const Default: Story = {}
diff --git a/src/components/checkbox/checkbox.test.tsx b/src/components/checkbox/checkbox.test.tsx
new file mode 100644
index 00000000..f0ed7f4d
--- /dev/null
+++ b/src/components/checkbox/checkbox.test.tsx
@@ -0,0 +1,41 @@
+import { fireEvent, render, screen } from '@testing-library/react'
+import { describe, expect, it, vi } from 'vitest'
+import { Checkbox } from './checkbox'
+
+describe('Checkbox', () => {
+ it('renders a labeled checkbox and triggers onChange', () => {
+ const onChange = vi.fn()
+ // ARRANGE
+ render(
+ ,
+ )
+
+ // ASSERT
+ const checkbox = screen.getByRole('checkbox', { name: 'Accept terms' })
+ expect(checkbox).not.toBeChecked()
+ fireEvent.click(checkbox)
+ expect(onChange).toHaveBeenCalledTimes(1)
+ })
+
+ it('renders a disabled checkbox with muted label', () => {
+ // ARRANGE
+ render(
+ ,
+ )
+
+ // ASSERT
+ const checkbox = screen.getByRole('checkbox', { name: 'Disabled' })
+ expect(checkbox).toBeDisabled()
+ expect(screen.getByText('Disabled')).toHaveClass('text-neutral-600')
+ })
+})
diff --git a/src/components/checkbox/checkbox.tsx b/src/components/checkbox/checkbox.tsx
index c2af40ca..b3f6a32a 100644
--- a/src/components/checkbox/checkbox.tsx
+++ b/src/components/checkbox/checkbox.tsx
@@ -1,28 +1,37 @@
-import React from "react";
-import { classNames } from "../../util/class-names";
+import type { ChangeEvent } from 'react'
+import { classNames } from '../../util/class-names'
interface CheckboxProps {
- id: string;
- label: string;
- checked: boolean;
- onChange?: (event: React.ChangeEvent) => void;
- disabled?: boolean;
+ id: string
+ label: string
+ checked: boolean
+ onChange?: (event: ChangeEvent) => void
+ disabled?: boolean
}
-export const Checkbox = ({ id, label, checked, onChange, disabled = false }: CheckboxProps) => {
- return (
-
-
-
- {label}
-
-
- );
-};
+export const Checkbox = ({
+ id,
+ label,
+ checked,
+ onChange,
+ disabled = false,
+}: CheckboxProps) => {
+ return (
+
+
+
+ {label}
+
+
+ )
+}
diff --git a/src/components/checkbox/index.ts b/src/components/checkbox/index.ts
index 2d4f967d..11c849ff 100644
--- a/src/components/checkbox/index.ts
+++ b/src/components/checkbox/index.ts
@@ -1 +1 @@
-export { Checkbox } from "./checkbox";
+export { Checkbox } from './checkbox'
diff --git a/src/components/dialog/dialog.stories.tsx b/src/components/dialog/dialog.stories.tsx
index e4514eb0..2434b65a 100644
--- a/src/components/dialog/dialog.stories.tsx
+++ b/src/components/dialog/dialog.stories.tsx
@@ -1,139 +1,153 @@
-import type { Meta, StoryObj } from "@storybook/react";
+import type { ComponentProps } from 'react'
+import { useState } from 'react'
+import type { Meta, StoryObj } from '@storybook/react-vite'
-import React, { useState } from "react";
-import { getStoryDescription, hiddenArgControl } from "../../util/storybook-utils";
-import { Dialog } from "./dialog";
-import { Button } from "../button/button";
-import { TickIcon } from "../../icons";
-import { FormField } from "../form-field/form-field";
-import { Toggle } from "../toggle";
-import { Alert } from "../alert/alert";
+import {
+ getStoryDescription,
+ hiddenArgControl,
+} from '../../util/storybook-utils'
+import { Dialog } from './dialog'
+import { Button } from '../button/button'
+import { TickIcon } from '../../icons'
+import { FormField } from '../form-field/form-field'
+import { Toggle } from '../toggle'
+import { Alert } from '../alert/alert'
-const noop = () => undefined;
+const noop = () => undefined
-const SpanFooter = () => test footer 🍭 ;
-const IconFooters = ({ onClose }: Pick, "onClose">) => (
- <>
- onClose?.(false)}>
- Cancel
-
+const SpanFooter = () => test footer 🍭
+const IconFooters = ({
+ onClose,
+}: Pick, 'onClose'>) => (
+ <>
+ onClose?.(false)}>
+ Cancel
+
- onClose?.(true)}>
- Confirm
-
- >
-);
-const footerOptions = { undefined, SpanFooter: , buttons: };
+ onClose?.(true)}
+ >
+ Confirm
+
+ >
+)
+const footerOptions = {
+ undefined,
+ SpanFooter: ,
+ buttons: ,
+}
const footerArgs = {
- options: Object.keys(footerOptions),
- mapping: footerOptions,
-};
+ options: Object.keys(footerOptions),
+ mapping: footerOptions,
+}
const meta: Meta = {
- title: "Dialog",
- component: Dialog,
- parameters: {
- ...getStoryDescription("Modal showing on top of the screen"),
- inlineStories: false, // keep controls interactive
- },
- args: {
- title: "Dialog Title",
- children: "Dialog Description",
- isShown: false,
- footer: undefined,
- },
- argTypes: {
- isShown: hiddenArgControl,
- onClose: hiddenArgControl,
- footer: footerArgs,
- },
- render: ({ children, ...args }) => {
- // eslint-disable-next-line react-hooks/rules-of-hooks
- const [isShown, setIsShown] = useState(false);
- const toggleBtn = () => setIsShown((val) => !val);
+ title: 'Dialog',
+ component: Dialog,
+ parameters: {
+ ...getStoryDescription('Modal showing on top of the screen'),
+ inlineStories: false, // keep controls interactive
+ },
+ args: {
+ title: 'Dialog Title',
+ children: 'Dialog Description',
+ isShown: false,
+ footer: undefined,
+ },
+ argTypes: {
+ isShown: hiddenArgControl,
+ onClose: hiddenArgControl,
+ footer: footerArgs,
+ },
+ render: ({ children, ...args }) => {
+ // eslint-disable-next-line react-hooks/rules-of-hooks
+ const [isShown, setIsShown] = useState(false)
+ const toggleBtn = () => setIsShown((val) => !val)
- return (
-
-
- show Modal
-
+ return (
+
+
+ show Modal
+
-
- {children}
-
-
- );
- },
-};
+
+ {children}
+
+
+ )
+ },
+}
-export default meta;
-type Story = StoryObj;
+export default meta
+type Story = StoryObj
-export const Default: Story = {};
+export const Default: Story = {}
export const WithFooterButtons: Story = {
- argTypes: {
- footer: hiddenArgControl,
- },
- render: ({ children, ...args }) => {
- // eslint-disable-next-line react-hooks/rules-of-hooks
- const [isShown, setIsShown] = useState(false);
- const toggleBtn = () => setIsShown((val) => !val);
+ argTypes: {
+ footer: hiddenArgControl,
+ },
+ render: ({ children, ...args }) => {
+ // eslint-disable-next-line react-hooks/rules-of-hooks
+ const [isShown, setIsShown] = useState(false)
+ const toggleBtn = () => setIsShown((val) => !val)
- return (
-
-
- show Modal
-
+ return (
+
+
+ show Modal
+
- setIsShown(false)} />}
- isShown={isShown}
- onClose={toggleBtn}
- >
- {children}
-
-
- );
- },
-};
+
setIsShown(false)} />}
+ isShown={isShown}
+ onClose={toggleBtn}
+ >
+ {children}
+
+
+ )
+ },
+}
export const WithLongContent: Story = {
- args: {
- children: (
- <>
-
- Paragraph Content
-
-
- Label
-
- Description
-
-
-
-
- Value 1
-
-
- Value 2
-
-
- Value 3
-
-
-
-
- {`
+ args: {
+ children: (
+ <>
+
+
Paragraph Content
+
+
+ Label
+
+ Description
+
+
+
+
+ Value 1
+
+
+ Value 2
+
+
+ Value 3
+
+
+
+
+ {`
"Oh, hush, hush, my child!" said Van Helsing. "God does not purchase souls in
this wise; and the Devil, though he may purchase, does not keep faith. But God
is merciful and just, and knows your pain and your devotion to that dear Madam
@@ -145,10 +159,10 @@ export const WithLongContent: Story = {
before he can hither come, be he never so quick. What we must hope for is that
my Lord Arthur and Quincey arrive first."
`}
-
+
-
- {`
+
+ {`
"He will be here before long now," said Van Helsing, who had been consulting his
pocket-book. "Nota bene, in Madam's telegram he went south from Carfax, that
means he went to cross the river, and he could only do so at slack of tide,
@@ -163,14 +177,14 @@ export const WithLongContent: Story = {
hand as he spoke, for we all could hear a key softly inserted in the lock of the
hall door.
`}
-
- litipsum.com
-
-
- >
- ),
- },
- argTypes: {
- children: hiddenArgControl,
- },
-};
+
+ litipsum.com
+
+
+ >
+ ),
+ },
+ argTypes: {
+ children: hiddenArgControl,
+ },
+}
diff --git a/src/components/dialog/dialog.test.tsx b/src/components/dialog/dialog.test.tsx
new file mode 100644
index 00000000..58223772
--- /dev/null
+++ b/src/components/dialog/dialog.test.tsx
@@ -0,0 +1,22 @@
+import { describe, expect, it, vi } from 'vitest'
+import { fireEvent, render, screen } from '@testing-library/react'
+import { Dialog } from './dialog'
+
+describe('Dialog', () => {
+ it('renders content and triggers onClose from the close button', async () => {
+ const onClose = vi.fn()
+ const { container } = render(
+
+ Dialog content
+ ,
+ )
+
+ expect(await screen.findByText('Confirm action')).toBeInTheDocument()
+ expect(screen.getByText('Dialog content')).toBeInTheDocument()
+
+ const closeButton = await screen.findByRole('button')
+ fireEvent.click(closeButton)
+
+ expect(onClose).toHaveBeenCalledWith(false)
+ })
+})
diff --git a/src/components/dialog/dialog.tsx b/src/components/dialog/dialog.tsx
index 80ebe9e8..4ca64ef6 100644
--- a/src/components/dialog/dialog.tsx
+++ b/src/components/dialog/dialog.tsx
@@ -1,121 +1,122 @@
import {
- Description as HeadlessDescription,
- Dialog as HeadlessDialog,
- DialogPanel as HeadlessDialogPanel,
- DialogTitle as HeadlessDialogTitle,
- Transition,
- TransitionChild,
-} from "@headlessui/react";
-import React, { Fragment } from "react";
-import { CrossIcon } from "../../icons";
-import { classNames } from "../../util/class-names";
-import { IconButton } from "../icon-button/icon-button";
+ Description as HeadlessDescription,
+ Dialog as HeadlessDialog,
+ DialogPanel as HeadlessDialogPanel,
+ DialogTitle as HeadlessDialogTitle,
+ Transition,
+ TransitionChild,
+} from '@headlessui/react'
+import { Fragment } from 'react'
+import type { ReactNode, ReactElement } from 'react'
+import { CrossIcon } from '../../icons'
+import { classNames } from '../../util/class-names'
+import { IconButton } from '../icon-button/icon-button'
export interface DialogProps {
- isShown?: boolean;
- title?: string;
- onClose?: (submitted: boolean) => void;
- isCloseable?: boolean;
- className?: string;
- children: React.ReactNode;
- footer?: React.ReactNode | null;
- footerPosition?: "end" | "start";
- hasBackground?: boolean;
- position?: "center" | "bottom-right";
+ isShown?: boolean
+ title?: string
+ onClose?: (submitted: boolean) => void
+ isCloseable?: boolean
+ className?: string
+ children: ReactNode
+ footer?: ReactNode | null
+ footerPosition?: 'end' | 'start'
+ hasBackground?: boolean
+ position?: 'center' | 'bottom-right'
}
export const Dialog = ({
- isShown,
- children,
- className,
- isCloseable = true,
- footer,
- footerPosition = "end",
- onClose,
- title,
- hasBackground = true,
- position = "center",
-}: DialogProps): JSX.Element | null => {
- const handleClose = (submitted = false) => {
- if (isCloseable && onClose) {
- onClose(submitted);
- }
- };
+ isShown,
+ children,
+ className,
+ isCloseable = true,
+ footer,
+ footerPosition = 'end',
+ onClose,
+ title,
+ hasBackground = true,
+ position = 'center',
+}: DialogProps): ReactElement | null => {
+ const handleClose = (submitted = false) => {
+ if (isCloseable && onClose) {
+ onClose(submitted)
+ }
+ }
- return (
-
-
- {hasBackground ? (
-
- handleClose(true)}
- />
-
- ) : null}
+ return (
+
+
+ {hasBackground ?
+
+ handleClose(true)}
+ />
+
+ : null}
+
+
+
+
+
+ {title}
+
+
+ {isCloseable && (
+ handleClose(false)}
+ />
+ )}
+ {children}
+
+ {!!footer && (
-
-
- );
-};
+ )}
+
+
+
+
+
+ )
+}
diff --git a/src/components/dialog/index.ts b/src/components/dialog/index.ts
index bca0c2f7..afbb355f 100644
--- a/src/components/dialog/index.ts
+++ b/src/components/dialog/index.ts
@@ -1 +1 @@
-export { Dialog } from "./dialog";
+export { Dialog } from './dialog'
diff --git a/src/components/disclosure/disclosure.stories.tsx b/src/components/disclosure/disclosure.stories.tsx
index f2cf392e..22b74b50 100644
--- a/src/components/disclosure/disclosure.stories.tsx
+++ b/src/components/disclosure/disclosure.stories.tsx
@@ -1,65 +1,72 @@
-import type { Meta, StoryObj } from "@storybook/react";
-import React from "react";
-import { Disclosure } from "./disclosure";
+import type { Meta, StoryObj } from '@storybook/react-vite'
+import { Disclosure } from './disclosure'
const meta: Meta
= {
- title: "Disclosure",
- component: Disclosure,
- parameters: {
- options: {
- showPanel: false,
- },
+ title: 'Disclosure',
+ component: Disclosure,
+ args: {
+ buttonLabel: 'Disclosure Button',
+ panelContent: 'Disclosure Content',
+ },
+ argTypes: {
+ buttonLabel: { control: 'text' },
+ panelContent: { control: 'text' },
+ },
+ parameters: {
+ options: {
+ showPanel: false,
},
-};
+ },
+}
-export default meta;
-type Story = StoryObj;
+export default meta
+type Story = StoryObj
export const Default: Story = {
- render: () => (
-
-
- Disclosure Button
- Disclosure Content
-
-
- ),
-};
+ render: ({ buttonLabel, panelContent }) => (
+
+
+ {buttonLabel}
+ {panelContent}
+
+
+ ),
+}
export const Stacked: Story = {
- render: () => (
-
-
- Disclosure Button
- Disclosure Content
-
-
- Disclosure Button
- Disclosure Content
-
-
- Disclosure Button
- Disclosure Content
-
-
- ),
-};
+ render: ({ buttonLabel, panelContent }) => (
+
+
+ {buttonLabel}
+ {panelContent}
+
+
+ {buttonLabel}
+ {panelContent}
+
+
+ {buttonLabel}
+ {panelContent}
+
+
+ ),
+}
export const DefaultOpen: Story = {
- render: () => (
-
-
- Disclosure Button
- Disclosure Content
-
-
- Disclosure Button
- Disclosure Content
-
-
- Disclosure Button
- Disclosure Content
-
-
- ),
-};
+ render: ({ buttonLabel, panelContent }) => (
+
+
+ {buttonLabel}
+ {panelContent}
+
+
+ {buttonLabel}
+ {panelContent}
+
+
+ {buttonLabel}
+ {panelContent}
+
+
+ ),
+}
diff --git a/src/components/disclosure/disclosure.test.tsx b/src/components/disclosure/disclosure.test.tsx
new file mode 100644
index 00000000..f76cfb59
--- /dev/null
+++ b/src/components/disclosure/disclosure.test.tsx
@@ -0,0 +1,17 @@
+import { describe, expect, it } from 'vitest'
+import { render, screen } from '@testing-library/react'
+import { Disclosure } from './disclosure'
+
+describe('Disclosure', () => {
+ it('renders panel content when open', () => {
+ render(
+
+ Details
+ Panel content
+ ,
+ )
+
+ expect(screen.getByText('Details')).toBeInTheDocument()
+ expect(screen.getByText('Panel content')).toBeInTheDocument()
+ })
+})
diff --git a/src/components/disclosure/disclosure.tsx b/src/components/disclosure/disclosure.tsx
index 73375e76..c757b293 100644
--- a/src/components/disclosure/disclosure.tsx
+++ b/src/components/disclosure/disclosure.tsx
@@ -1,50 +1,60 @@
+import type { ComponentPropsWithoutRef } from 'react'
+import { ReactNode } from 'react'
import {
- DisclosurePanelProps,
- Disclosure as HeadlessUiDisclosure,
- DisclosureButton as HeadlessUiDisclosureButton,
- DisclosurePanel as HeadlessUiDisclosurePanel,
-} from "@headlessui/react";
-import React from "react";
-import { ChevronDownIcon } from "../../icons";
-import { classNames } from "../../util/class-names";
+ DisclosurePanelProps,
+ Disclosure as HeadlessUiDisclosure,
+ DisclosureButton as HeadlessUiDisclosureButton,
+ DisclosurePanel as HeadlessUiDisclosurePanel,
+} from '@headlessui/react'
+import { ChevronDownIcon } from '../../icons'
+import { classNames } from '../../util/class-names'
-const DisclosurePanel = ({ children, ...props }: Omit) => {
- return {children} ;
-};
+const DisclosurePanel = ({
+ children,
+ ...props
+}: Omit) => {
+ return (
+ {children}
+ )
+}
-interface DisclosureButtonProps extends React.ComponentPropsWithoutRef<"button"> {
- children: React.ReactNode;
+interface DisclosureButtonProps extends ComponentPropsWithoutRef<'button'> {
+ children: ReactNode
}
const DisclosureButton = ({ children, ...props }: DisclosureButtonProps) => {
- return (
-
- {({ open }) => (
- <>
- {children}
-
- >
- )}
-
- );
-};
+ return (
+
+ {({ open }) => (
+ <>
+ {children}
+
+ >
+ )}
+
+ )
+}
const Disclosure = ({
- children,
- defaultOpen = false,
+ children,
+ defaultOpen = false,
}: {
- children: React.ReactNode;
- defaultOpen?: boolean;
+ children: ReactNode
+ defaultOpen?: boolean
}) => {
- return {children} ;
-};
+ return (
+
+ {children}
+
+ )
+}
-Disclosure.Button = DisclosureButton;
-Disclosure.Panel = DisclosurePanel;
+Disclosure.Button = DisclosureButton
+Disclosure.Panel = DisclosurePanel
-export { Disclosure };
+export { Disclosure }
diff --git a/src/components/disclosure/index.tsx b/src/components/disclosure/index.tsx
index b1969328..d1469b00 100644
--- a/src/components/disclosure/index.tsx
+++ b/src/components/disclosure/index.tsx
@@ -1 +1 @@
-export { Disclosure } from "./disclosure";
+export { Disclosure } from './disclosure'
diff --git a/src/components/divider-line/divider-line.stories.tsx b/src/components/divider-line/divider-line.stories.tsx
index ca62ed3f..d3754213 100644
--- a/src/components/divider-line/divider-line.stories.tsx
+++ b/src/components/divider-line/divider-line.stories.tsx
@@ -1,22 +1,33 @@
-import type { Meta, StoryObj } from "@storybook/react";
+import type { Meta, StoryObj } from '@storybook/react-vite'
-import React from "react";
-import { DividerLine } from "./divider-line";
+import { DividerLine } from './divider-line'
const meta: Meta = {
- title: "DividerLine",
- component: DividerLine,
- parameters: { layout: "fullscreen" },
- render: () => (
-
- ),
-};
+ title: 'DividerLine',
+ component: DividerLine,
+ args: {
+ padding: 8,
+ showIcons: true,
+ },
+ argTypes: {
+ padding: { control: { type: 'range', min: 0, max: 32, step: 2 } },
+ showIcons: { control: 'boolean' },
+ },
+ parameters: { layout: 'fullscreen' },
+ render: ({ padding, showIcons }) => (
+
+ {showIcons ?
+
🌞
+ : null}
+
+ {showIcons ?
+
🌙
+ : null}
+
+ ),
+}
-export default meta;
-type Story = StoryObj;
+export default meta
+type Story = StoryObj
-export const Default: Story = {};
+export const Default: Story = {}
diff --git a/src/components/divider-line/divider-line.test.tsx b/src/components/divider-line/divider-line.test.tsx
new file mode 100644
index 00000000..1cb089dc
--- /dev/null
+++ b/src/components/divider-line/divider-line.test.tsx
@@ -0,0 +1,13 @@
+import { describe, expect, it } from 'vitest'
+import { render } from '@testing-library/react'
+import { DividerLine } from './divider-line'
+
+describe('DividerLine', () => {
+ it('renders a horizontal rule', () => {
+ const { container } = render( )
+ const divider = container.querySelector('hr')
+
+ expect(divider).toBeInTheDocument()
+ expect(divider).toHaveClass('bg-neutral-300')
+ })
+})
diff --git a/src/components/divider-line/divider-line.tsx b/src/components/divider-line/divider-line.tsx
index f5a1bb03..54d6ceca 100644
--- a/src/components/divider-line/divider-line.tsx
+++ b/src/components/divider-line/divider-line.tsx
@@ -1,5 +1,5 @@
-import React from "react";
-
export const DividerLine = () => {
- return ;
-};
+ return (
+
+ )
+}
diff --git a/src/components/divider-line/index.ts b/src/components/divider-line/index.ts
index 8e429289..8710dafe 100644
--- a/src/components/divider-line/index.ts
+++ b/src/components/divider-line/index.ts
@@ -1 +1 @@
-export { DividerLine } from "./divider-line";
+export { DividerLine } from './divider-line'
diff --git a/src/components/featured-tag/featured-tag.stories.tsx b/src/components/featured-tag/featured-tag.stories.tsx
index 5700c838..e77b0b0b 100644
--- a/src/components/featured-tag/featured-tag.stories.tsx
+++ b/src/components/featured-tag/featured-tag.stories.tsx
@@ -1,57 +1,69 @@
-import type { Meta, StoryObj } from "@storybook/react";
-import React, { useState } from "react";
-import { FeaturedTag } from "./featured-tag";
-import { Panel } from "../panel";
-import { FormField } from "../form-field";
+import type { Meta, StoryObj } from '@storybook/react-vite'
+import { useState } from 'react'
+import { FeaturedTag } from './featured-tag'
+import { Panel } from '../panel'
+import { FormField } from '../form-field'
const meta: Meta = {
- title: "Input/FeaturedTag",
- component: FeaturedTag,
-};
-
-export default meta;
-
-type Story = StoryObj;
-
-const RadioBoxWithRecommendationTag = () => {
- const [value, setValue] = useState("value_1");
-
- return (
-
-
- {
- setValue(newValue);
- }}
- id="value"
- >
-
- Option 1Recommended!
-
-
- Option 2
-
- Option 3
-
-
-
- );
-};
+ title: 'Input/FeaturedTag',
+ component: FeaturedTag,
+ args: {
+ tagLabel: 'Recommended!',
+ panelText: 'This example uses a Panel component',
+ },
+ argTypes: {
+ tagLabel: { control: 'text' },
+ panelText: { control: 'text' },
+ },
+}
+
+export default meta
+
+type Story = StoryObj
+
+const RadioBoxWithRecommendationTag = ({ tagLabel }: { tagLabel: string }) => {
+ const [value, setValue] = useState('value_1')
+
+ return (
+
+
+ {
+ setValue(newValue)
+ }}
+ id='value'
+ >
+
+ Option 1{tagLabel}
+
+
+
+ Option 2
+
+
+
+ Option 3
+
+
+
+
+ )
+}
export const Default: Story = {
- render: () => {
- return ;
- },
-};
+ render: ({ tagLabel }) => (
+
+ ),
+}
export const PanelExample: Story = {
- render: () => (
-
-
- This example uses a Panel component
- Recommended!
-
-
- ),
-};
+ render: ({ tagLabel, panelText }) => (
+
+
+ {panelText}
+ {tagLabel}
+
+
+ ),
+}
diff --git a/src/components/featured-tag/featured-tag.test.tsx b/src/components/featured-tag/featured-tag.test.tsx
new file mode 100644
index 00000000..fccc1929
--- /dev/null
+++ b/src/components/featured-tag/featured-tag.test.tsx
@@ -0,0 +1,13 @@
+import { describe, expect, it } from 'vitest'
+import { render, screen } from '@testing-library/react'
+import { FeaturedTag } from './featured-tag'
+
+describe('FeaturedTag', () => {
+ it('renders featured tag content', () => {
+ render(Featured )
+
+ const tag = screen.getByText('Featured')
+ expect(tag).toBeInTheDocument()
+ expect(tag).toHaveClass('border-primary-600')
+ })
+})
diff --git a/src/components/featured-tag/featured-tag.tsx b/src/components/featured-tag/featured-tag.tsx
index 71e65f5d..042ea8dc 100644
--- a/src/components/featured-tag/featured-tag.tsx
+++ b/src/components/featured-tag/featured-tag.tsx
@@ -1,20 +1,20 @@
-import React, { ReactNode } from "react";
-import { classNames } from "../../util/class-names";
+import { ReactNode } from 'react'
+import { classNames } from '../../util/class-names'
interface FeaturedTagProps {
- children: ReactNode;
- className?: string;
+ children: ReactNode
+ className?: string
}
export const FeaturedTag = ({ children, className }: FeaturedTagProps) => {
- return (
-
- {children}
-
- );
-};
+ return (
+
+ {children}
+
+ )
+}
diff --git a/src/components/featured-tag/index.ts b/src/components/featured-tag/index.ts
index f054081b..758b9d81 100644
--- a/src/components/featured-tag/index.ts
+++ b/src/components/featured-tag/index.ts
@@ -1 +1 @@
-export { FeaturedTag } from "./featured-tag";
+export { FeaturedTag } from './featured-tag'
diff --git a/src/components/form-field/form-field-description.tsx b/src/components/form-field/form-field-description.tsx
index 9680be2c..de6804c4 100644
--- a/src/components/form-field/form-field-description.tsx
+++ b/src/components/form-field/form-field-description.tsx
@@ -1,14 +1,17 @@
-import React from "react";
+import type { ReactNode } from 'react'
export interface FormFieldDescriptionProps {
- id: string;
- children: React.ReactNode;
+ id: string
+ children: ReactNode
}
-export const FormFieldDescription = ({ id, children }: FormFieldDescriptionProps) => {
- return (
-
- {children}
-
- );
-};
+export const FormFieldDescription = ({
+ id,
+ children,
+}: FormFieldDescriptionProps) => {
+ return (
+
+ {children}
+
+ )
+}
diff --git a/src/components/form-field/form-field-error-message.tsx b/src/components/form-field/form-field-error-message.tsx
index 28f5aacf..baf4314b 100644
--- a/src/components/form-field/form-field-error-message.tsx
+++ b/src/components/form-field/form-field-error-message.tsx
@@ -1,15 +1,17 @@
-import React from "react";
-import { WarningSignIcon } from "../../icons";
+import type { ReactNode } from 'react'
+import { WarningSignIcon } from '../../icons'
export interface FormFieldErrorMessageProps {
- children: React.ReactNode;
+ children: ReactNode
}
-export const FormFieldErrorMessage = ({ children }: FormFieldErrorMessageProps) => {
- return (
-
- );
-};
+export const FormFieldErrorMessage = ({
+ children,
+}: FormFieldErrorMessageProps) => {
+ return (
+
+ )
+}
diff --git a/src/components/form-field/form-field-group.stories.tsx b/src/components/form-field/form-field-group.stories.tsx
index 5f2e3768..6c212a23 100644
--- a/src/components/form-field/form-field-group.stories.tsx
+++ b/src/components/form-field/form-field-group.stories.tsx
@@ -1,154 +1,171 @@
/* eslint-disable react/jsx-props-no-spreading */
-import type { Meta, StoryObj } from "@storybook/react";
-import React from "react";
-import { FormFieldGroup } from "./form-field-group";
-import { FormField } from "./form-field";
-
-const meta: Meta = {
- title: "Input / Combined Fields",
- component: FormFieldGroup,
-};
-
-export default meta;
-
-type Story = StoryObj;
+import type { Meta, StoryObj } from '@storybook/react-vite'
+import { FormFieldGroup } from './form-field-group'
+import { FormField } from './form-field'
+
+type FormFieldGroupStoryArgs = {
+ error: boolean
+ disabled: boolean
+ groupWidth: number
+}
+
+const meta: Meta = {
+ title: 'Input / Combined Fields',
+ args: {
+ error: false,
+ disabled: false,
+ groupWidth: 480,
+ },
+ argTypes: {
+ error: { control: 'boolean' },
+ disabled: { control: 'boolean' },
+ groupWidth: { control: { type: 'range', min: 320, max: 720, step: 20 } },
+ },
+}
+
+export default meta
+
+type Story = StoryObj
const TextInputFields = ({
- error = false,
- disabled = false,
+ error = false,
+ disabled = false,
}: {
- error?: boolean;
- disabled?: boolean;
+ error?: boolean
+ disabled?: boolean
}) => {
- return (
-
-
-
- Textfields Only
-
- A group with only textfields
-
-
-
-
- {}}
- ariaDescribedBy="value-description"
- error={error}
- disabled={disabled}
- />
-
- {}}
- ariaDescribedBy="value-description"
- error={error}
- disabled={disabled}
- />
-
- {}}
- ariaDescribedBy="value-description"
- error={error}
- disabled={disabled}
- />
-
-
-
-
-
- Mixed Fields
-
- A group with mixed fields (textfields and listboxes)
-
-
-
-
- {}}
- ariaDescribedBy="value-description"
- error={error}
- disabled={disabled}
- />
-
- {}}
- ariaDescribedBy="value-description"
- error={error}
- disabled={disabled}
- />
-
- {}}
- ariaDescribedBy="value-description"
- error={error}
- disabled={disabled}
- />
-
- {}}>
-
-
-
-
-
-
-
-
- Option 1
-
-
-
-
-
- Option 2
-
-
-
-
-
-
-
-
- );
-};
+ return (
+
+
+
+ Textfields Only
+
+ A group with only textfields
+
+
+
+
+ {}}
+ ariaDescribedBy='value-description'
+ error={error}
+ disabled={disabled}
+ />
+
+ {}}
+ ariaDescribedBy='value-description'
+ error={error}
+ disabled={disabled}
+ />
+
+ {}}
+ ariaDescribedBy='value-description'
+ error={error}
+ disabled={disabled}
+ />
+
+
+
+
+
+ Mixed Fields
+
+ A group with mixed fields (textfields and listboxes)
+
+
+
+
+ {}}
+ ariaDescribedBy='value-description'
+ error={error}
+ disabled={disabled}
+ />
+
+ {}}
+ ariaDescribedBy='value-description'
+ error={error}
+ disabled={disabled}
+ />
+
+ {}}
+ ariaDescribedBy='value-description'
+ error={error}
+ disabled={disabled}
+ />
+
+ {}}>
+
+
+
+
+
+
+
+
+ Option 1
+
+
+
+
+
+ Option 2
+
+
+
+
+
+
+
+
+ )
+}
export const Default: Story = {
- render: () => (
-
-
-
- ),
-};
+ render: ({ error, disabled, groupWidth }) => (
+
+
+
+ ),
+}
export const WithError: Story = {
- render: () => (
-
-
-
- ),
-};
+ render: () => (
+
+
+
+ ),
+}
export const Disabled: Story = {
- render: () => (
-
-
-
- ),
-};
+ render: () => (
+
+
+
+ ),
+}
diff --git a/src/components/form-field/form-field-group.test.tsx b/src/components/form-field/form-field-group.test.tsx
new file mode 100644
index 00000000..7263e161
--- /dev/null
+++ b/src/components/form-field/form-field-group.test.tsx
@@ -0,0 +1,20 @@
+import { describe, expect, it } from 'vitest'
+import { render, screen } from '@testing-library/react'
+import { FormFieldGroup } from './form-field-group'
+
+describe('FormFieldGroup', () => {
+ it('renders children inside the group container', () => {
+ render(
+
+ First
+ Second
+ ,
+ )
+
+ const first = screen.getByText('First')
+ const group = first.closest('div')
+
+ expect(first).toBeInTheDocument()
+ expect(group).toHaveClass('form-field-group')
+ })
+})
diff --git a/src/components/form-field/form-field-group.tsx b/src/components/form-field/form-field-group.tsx
index 7d0c264d..a75f9120 100644
--- a/src/components/form-field/form-field-group.tsx
+++ b/src/components/form-field/form-field-group.tsx
@@ -1,9 +1,13 @@
-import React from "react";
+import type { ReactNode } from 'react'
export interface FormFieldGroupProps {
- children: React.ReactNode;
+ children: ReactNode
}
export const FormFieldGroup = ({ children }: FormFieldGroupProps) => {
- return {children}
;
-};
+ return (
+
+ {children}
+
+ )
+}
diff --git a/src/components/form-field/form-field-label-group.tsx b/src/components/form-field/form-field-label-group.tsx
index 3b3f6e40..ae47405b 100644
--- a/src/components/form-field/form-field-label-group.tsx
+++ b/src/components/form-field/form-field-label-group.tsx
@@ -1,9 +1,9 @@
-import React from "react";
+import type { ReactNode } from 'react'
export interface FormFieldLabelGroupProps {
- children: React.ReactNode;
+ children: ReactNode
}
export const FormFieldLabelGroup = ({ children }: FormFieldLabelGroupProps) => {
- return {children}
;
-};
+ return {children}
+}
diff --git a/src/components/form-field/form-field-label.tsx b/src/components/form-field/form-field-label.tsx
index d26def3c..96680fcb 100644
--- a/src/components/form-field/form-field-label.tsx
+++ b/src/components/form-field/form-field-label.tsx
@@ -1,19 +1,25 @@
-import React from "react";
+import type { ReactNode } from 'react'
export interface FormFieldLabelProps {
- htmlFor: string;
- children: React.ReactNode;
- optional?: boolean;
+ htmlFor: string
+ children: ReactNode
+ optional?: boolean
}
-export const FormFieldLabel = ({ htmlFor, children, optional }: FormFieldLabelProps) => {
- return (
-
-
- {children}
-
+export const FormFieldLabel = ({
+ htmlFor,
+ children,
+ optional,
+}: FormFieldLabelProps) => {
+ return (
+
+
+ {children}
+
- {optional ?
(Optional)
: null}
-
- );
-};
+ {optional ?
+
(Optional)
+ : null}
+
+ )
+}
diff --git a/src/components/form-field/form-field.tsx b/src/components/form-field/form-field.tsx
index 27d9dfe9..858e63f9 100644
--- a/src/components/form-field/form-field.tsx
+++ b/src/components/form-field/form-field.tsx
@@ -1,40 +1,40 @@
-import React from "react";
-import { FormFieldDescription } from "./form-field-description";
-import { FormFieldErrorMessage } from "./form-field-error-message";
-import { FormFieldLabel } from "./form-field-label";
-import { FormFieldLabelGroup } from "./form-field-label-group";
-import { RadioInput } from "./radio-input/radio-input";
-import { TextInput } from "./text-input/text-input";
-import { Textarea } from "./textarea/textarea";
-import { NumberInput } from "./number-input/number-input";
-import { Listbox } from "./listbox/listbox";
-import { MultiCombobox } from "./multi-combobox/multi-combobox";
-import { SingleCombobox } from "./single-combobox/single-combobox";
-import { FormFieldGroup } from "./form-field-group";
-import { SearchInput } from "./search-input/search-input";
-import { RadioBox } from "./radio-box/radio-box";
+import type { ReactNode } from 'react'
+import { FormFieldDescription } from './form-field-description'
+import { FormFieldErrorMessage } from './form-field-error-message'
+import { FormFieldLabel } from './form-field-label'
+import { FormFieldLabelGroup } from './form-field-label-group'
+import { RadioInput } from './radio-input/radio-input'
+import { TextInput } from './text-input/text-input'
+import { Textarea } from './textarea/textarea'
+import { NumberInput } from './number-input/number-input'
+import { Listbox } from './listbox/listbox'
+import { MultiCombobox } from './multi-combobox/multi-combobox'
+import { SingleCombobox } from './single-combobox/single-combobox'
+import { FormFieldGroup } from './form-field-group'
+import { SearchInput } from './search-input/search-input'
+import { RadioBox } from './radio-box/radio-box'
interface FormFieldProps {
- children: React.ReactNode;
+ children: ReactNode
}
const FormField = ({ children }: FormFieldProps) => {
- return {children}
;
-};
+ return {children}
+}
-FormField.LabelGroup = FormFieldLabelGroup;
-FormField.Label = FormFieldLabel;
-FormField.Description = FormFieldDescription;
-FormField.ErrorMessage = FormFieldErrorMessage;
-FormField.TextInput = TextInput;
-FormField.Textarea = Textarea;
-FormField.RadioInput = RadioInput;
-FormField.NumberInput = NumberInput;
-FormField.Listbox = Listbox;
-FormField.MultiCombobox = MultiCombobox;
-FormField.SingleCombobox = SingleCombobox;
-FormField.Group = FormFieldGroup;
-FormField.SearchInput = SearchInput;
-FormField.RadioBox = RadioBox;
+FormField.LabelGroup = FormFieldLabelGroup
+FormField.Label = FormFieldLabel
+FormField.Description = FormFieldDescription
+FormField.ErrorMessage = FormFieldErrorMessage
+FormField.TextInput = TextInput
+FormField.Textarea = Textarea
+FormField.RadioInput = RadioInput
+FormField.NumberInput = NumberInput
+FormField.Listbox = Listbox
+FormField.MultiCombobox = MultiCombobox
+FormField.SingleCombobox = SingleCombobox
+FormField.Group = FormFieldGroup
+FormField.SearchInput = SearchInput
+FormField.RadioBox = RadioBox
-export { FormField };
+export { FormField }
diff --git a/src/components/form-field/index.ts b/src/components/form-field/index.ts
index d1834b2b..b5a73ed2 100644
--- a/src/components/form-field/index.ts
+++ b/src/components/form-field/index.ts
@@ -1 +1 @@
-export { FormField } from "./form-field";
+export { FormField } from './form-field'
diff --git a/src/components/form-field/listbox/listbox-badge-option.tsx b/src/components/form-field/listbox/listbox-badge-option.tsx
index 929c666c..bcdee05e 100644
--- a/src/components/form-field/listbox/listbox-badge-option.tsx
+++ b/src/components/form-field/listbox/listbox-badge-option.tsx
@@ -1,18 +1,18 @@
-import React from "react";
-import { Badge, BadgeType } from "../../badge/badge";
+import type { ReactNode } from 'react'
+import { Badge, BadgeType } from '../../badge/badge'
export interface ListboxBadgeOptionProps {
- children: React.ReactNode;
- badgeType?: BadgeType;
+ children: ReactNode
+ badgeType?: BadgeType
}
export const ListboxBadgeOption = ({
- children,
- badgeType = "neutral",
+ children,
+ badgeType = 'neutral',
}: ListboxBadgeOptionProps) => {
- return (
-
- {children}
-
- );
-};
+ return (
+
+ {children}
+
+ )
+}
diff --git a/src/components/form-field/listbox/listbox-button-badge-value.tsx b/src/components/form-field/listbox/listbox-button-badge-value.tsx
index 3a555780..9b5fd598 100644
--- a/src/components/form-field/listbox/listbox-button-badge-value.tsx
+++ b/src/components/form-field/listbox/listbox-button-badge-value.tsx
@@ -1,19 +1,18 @@
-import React from "react";
-import { Badge, BadgeType } from "../../badge/badge";
+import { Badge, BadgeType } from '../../badge/badge'
export interface ListboxButtonBadgeValueProps {
- placeholder: string;
- value?: string | number | null;
- badgeType?: BadgeType;
+ placeholder: string
+ value?: string | number | null
+ badgeType?: BadgeType
}
export const ListboxButtonBadgeValue = ({
- placeholder,
- value,
- badgeType = "neutral",
+ placeholder,
+ value,
+ badgeType = 'neutral',
}: ListboxButtonBadgeValueProps) => {
- if (!value) {
- return {placeholder}
;
- }
- return {value} ;
-};
+ if (!value) {
+ return {placeholder}
+ }
+ return {value}
+}
diff --git a/src/components/form-field/listbox/listbox-button-text-value.tsx b/src/components/form-field/listbox/listbox-button-text-value.tsx
index 7bab662f..22cdec31 100644
--- a/src/components/form-field/listbox/listbox-button-text-value.tsx
+++ b/src/components/form-field/listbox/listbox-button-text-value.tsx
@@ -1,17 +1,18 @@
-import React from "react";
-
export interface ListboxButtonTextValueProps {
- placeholder: string;
- value?: string | number | null;
+ placeholder: string
+ value?: string | number | null
}
-export const ListboxButtonTextValue = ({ placeholder, value }: ListboxButtonTextValueProps) => {
- if (!value) {
- return (
-
- {placeholder}
-
- );
- }
- return {value}
;
-};
+export const ListboxButtonTextValue = ({
+ placeholder,
+ value,
+}: ListboxButtonTextValueProps) => {
+ if (!value) {
+ return (
+
+ {placeholder}
+
+ )
+ }
+ return {value}
+}
diff --git a/src/components/form-field/listbox/listbox-button.tsx b/src/components/form-field/listbox/listbox-button.tsx
index 047477fc..64a74948 100644
--- a/src/components/form-field/listbox/listbox-button.tsx
+++ b/src/components/form-field/listbox/listbox-button.tsx
@@ -1,37 +1,40 @@
-import { ListboxButton as HeadlessUiListboxButton } from "@headlessui/react";
-import React from "react";
-import { CaretDownIcon } from "../../../icons";
-import { classNames } from "../../../util/class-names";
-import { ListboxButtonBadgeValue } from "./listbox-button-badge-value";
-import { ListboxButtonTextValue } from "./listbox-button-text-value";
+import type { ReactNode } from 'react'
+import { ListboxButton as HeadlessUiListboxButton } from '@headlessui/react'
+import { CaretDownIcon } from '../../../icons'
+import { classNames } from '../../../util/class-names'
+import { ListboxButtonBadgeValue } from './listbox-button-badge-value'
+import { ListboxButtonTextValue } from './listbox-button-text-value'
export interface ListboxButtonProps {
- children: React.ReactNode;
- disabled?: boolean;
+ children: ReactNode
+ disabled?: boolean
}
const ListboxButton = ({ children, disabled }: ListboxButtonProps) => {
- return (
-
- {children}
+ return (
+
+ {children}
-
-
- );
-};
+
+
+ )
+}
-ListboxButton.BadgeValue = ListboxButtonBadgeValue;
-ListboxButton.TextValue = ListboxButtonTextValue;
+ListboxButton.BadgeValue = ListboxButtonBadgeValue
+ListboxButton.TextValue = ListboxButtonTextValue
-export { ListboxButton };
+export { ListboxButton }
diff --git a/src/components/form-field/listbox/listbox-option.tsx b/src/components/form-field/listbox/listbox-option.tsx
index becc19cf..a72a6064 100644
--- a/src/components/form-field/listbox/listbox-option.tsx
+++ b/src/components/form-field/listbox/listbox-option.tsx
@@ -1,43 +1,48 @@
-import { ListboxOption as HeadlessUiListboxOption } from "@headlessui/react";
-import React, { Fragment } from "react";
-import { classNames } from "../../../util/class-names";
-import { ListboxBadgeOption } from "./listbox-badge-option";
-import { ListboxTextOption } from "./listbox-text-option";
+import { ListboxOption as HeadlessUiListboxOption } from '@headlessui/react'
+import { Fragment } from 'react'
+import type { ReactNode } from 'react'
+import { classNames } from '../../../util/class-names'
+import { ListboxBadgeOption } from './listbox-badge-option'
+import { ListboxTextOption } from './listbox-text-option'
const listboxOptionStyles = {
- base: "relative cursor-pointer px-3 py-2 ",
- selected:
- "bg-primary-100 text-primary-500 before:absolute before:bottom-0 before:left-0 before:top-0 before:block before:w-[2px] before:rounded-r-md before:bg-primary-400",
- active: "bg-neutral-50 bg-primary-100",
- disabled: "cursor-not-allowed bg-neutral-50 text-neutral-400",
-};
+ base: 'relative cursor-pointer px-3 py-2 ',
+ selected:
+ 'bg-primary-100 text-primary-500 before:absolute before:bottom-0 before:left-0 before:top-0 before:block before:w-[2px] before:rounded-r-md before:bg-primary-400',
+ active: 'bg-neutral-50 bg-primary-100',
+ disabled: 'cursor-not-allowed bg-neutral-50 text-neutral-400',
+}
export interface ListboxOptionProps {
- value: TValue;
- children: React.ReactNode;
- disabled?: boolean;
+ value: TValue
+ children: ReactNode
+ disabled?: boolean
}
-const ListboxOption = ({ value, disabled, children }: ListboxOptionProps) => {
- return (
-
- {({ active, selected }) => (
-
- {children}
-
- )}
-
- );
-};
+const ListboxOption = ({
+ value,
+ disabled,
+ children,
+}: ListboxOptionProps) => {
+ return (
+
+ {({ active, selected }) => (
+
+ {children}
+
+ )}
+
+ )
+}
-ListboxOption.BadgeOption = ListboxBadgeOption;
-ListboxOption.TextOption = ListboxTextOption;
+ListboxOption.BadgeOption = ListboxBadgeOption
+ListboxOption.TextOption = ListboxTextOption
-export { ListboxOption };
+export { ListboxOption }
diff --git a/src/components/form-field/listbox/listbox-options.tsx b/src/components/form-field/listbox/listbox-options.tsx
index 4019fd9e..0df3cdac 100644
--- a/src/components/form-field/listbox/listbox-options.tsx
+++ b/src/components/form-field/listbox/listbox-options.tsx
@@ -1,14 +1,14 @@
-import { ListboxOptions as HeadlessUiListboxOptions } from "@headlessui/react";
-import React from "react";
+import type { ReactNode } from 'react'
+import { ListboxOptions as HeadlessUiListboxOptions } from '@headlessui/react'
export interface ListboxOptionsProps {
- children: React.ReactNode;
+ children: ReactNode
}
export const ListboxOptions = ({ children }: ListboxOptionsProps) => {
- return (
-
- {children}
-
- );
-};
+ return (
+
+ {children}
+
+ )
+}
diff --git a/src/components/form-field/listbox/listbox-text-option.tsx b/src/components/form-field/listbox/listbox-text-option.tsx
index f1ca2482..eeeb7c4a 100644
--- a/src/components/form-field/listbox/listbox-text-option.tsx
+++ b/src/components/form-field/listbox/listbox-text-option.tsx
@@ -1,18 +1,22 @@
-import React from "react";
+import type { ElementType } from 'react'
+import { ReactNode } from 'react'
export interface ListboxTextOptionProps {
- children: React.ReactNode;
- LeftIcon?: React.ElementType;
+ children: ReactNode
+ LeftIcon?: ElementType
}
-export const ListboxTextOption = ({ children, LeftIcon }: ListboxTextOptionProps) => {
- return (
-
- {LeftIcon ? (
-
- ) : null}
+export const ListboxTextOption = ({
+ children,
+ LeftIcon,
+}: ListboxTextOptionProps) => {
+ return (
+
+ {LeftIcon ?
+
+ : null}
-
{children}
-
- );
-};
+
{children}
+
+ )
+}
diff --git a/src/components/form-field/listbox/listbox.stories.tsx b/src/components/form-field/listbox/listbox.stories.tsx
index 37240a80..379e395e 100644
--- a/src/components/form-field/listbox/listbox.stories.tsx
+++ b/src/components/form-field/listbox/listbox.stories.tsx
@@ -1,117 +1,230 @@
/* eslint-disable react/jsx-props-no-spreading */
-import type { Meta, StoryObj } from "@storybook/react";
-import React, { FC } from "react";
-import { FormField } from "../form-field";
+import type { Meta, StoryObj } from '@storybook/react-vite'
+import { FC, useState } from 'react'
+import { FormField } from '../form-field'
-const meta: Meta = {
- title: "Input/Listbox",
- component: FormField.Listbox,
-};
+type ListboxStoryArgs = {
+ label: string
+ description: string
+ placeholder: string
+ width: number
+ disabled: boolean
+}
+
+const meta: Meta = {
+ title: 'Input/Listbox',
+ args: {
+ label: 'Label',
+ description: 'Description',
+ placeholder: 'Select...',
+ width: 288,
+ disabled: false,
+ },
+ argTypes: {
+ label: { control: 'text' },
+ description: { control: 'text' },
+ placeholder: { control: 'text' },
+ width: { control: { type: 'range', min: 200, max: 360, step: 16 } },
+ disabled: { control: 'boolean' },
+ },
+}
-export default meta;
+export default meta
-type Story = StoryObj;
+type Story = StoryObj
interface Person {
- id: number;
- name: string;
- isDead?: boolean;
+ id: number
+ name: string
+ isDead?: boolean
}
const people: Person[] = [
- { id: 1, name: "John Lennon", isDead: true },
- { id: 2, name: "Kenton Towne" },
- { id: 3, name: "Therese Wunsch" },
- { id: 4, name: "Benedict Kessler" },
- { id: 5, name: "Katelyn Rohan" },
-];
-
-const ListboxTextWithHooks = () => {
- const [selectedPerson, setSelectedPerson] = React.useState(null);
-
- return (
-
-
- Label
- Description
-
-
-
-
-
-
- {people.map((person) => (
-
-
- {person.name}
-
-
- ))}
-
-
-
- );
-};
-
-const ListboxBadgeWithHooks: FC<{ disabled?: boolean }> = ({ disabled }) => {
- const [selectedPerson, setSelectedPerson] = React.useState(null);
-
- return (
-
-
- Label
- Description
-
-
-
-
-
-
-
-
- {people.map((person) => (
-
-
- {person.name}
-
-
- ))}
-
-
-
- );
-};
+ { id: 1, name: 'John Lennon', isDead: true },
+ { id: 2, name: 'Kenton Towne' },
+ { id: 3, name: 'Therese Wunsch' },
+ { id: 4, name: 'Benedict Kessler' },
+ { id: 5, name: 'Katelyn Rohan' },
+]
+
+const ListboxTextWithHooks = ({
+ label,
+ description,
+ placeholder,
+}: {
+ label: string
+ description: string
+ placeholder: string
+}) => {
+ const [selectedPerson, setSelectedPerson] = useState(null)
+
+ return (
+
+
+ {label}
+
+ {description}
+
+
+
+
+
+
+
+ {people.map((person) => (
+
+
+ {person.name}
+
+
+ ))}
+
+
+
+ )
+}
+
+const ListboxBadgeWithHooks: FC<{
+ disabled?: boolean
+ label: string
+ description: string
+ placeholder: string
+}> = ({ disabled, label, description, placeholder }) => {
+ const [selectedPerson, setSelectedPerson] = useState(null)
+
+ return (
+
+
+ {label}
+
+ {description}
+
+
+
+
+
+
+
+
+
+ {people.map((person) => (
+
+
+ {person.name}
+
+
+ ))}
+
+
+
+ )
+}
+
+const ListboxMultiWithHooks = ({
+ label,
+ description,
+ placeholder,
+}: {
+ label: string
+ description: string
+ placeholder: string
+}) => {
+ const [selectedPeople, setSelectedPeople] = useState([])
+ const selectedLabels = selectedPeople.map((person) => person.name).join(', ')
+
+ return (
+
+
+ {label}
+
+ {description}
+
+
+
+
+
+
+
+ {people.map((person) => (
+
+
+ {person.name}
+
+
+ ))}
+
+
+
+ )
+}
export const Default: Story = {
- render: () => (
-
-
-
- ),
-};
+ render: ({ label, description, placeholder, width }) => (
+
+
+
+ ),
+}
export const Badge: Story = {
- render: () => (
-
-
-
- ),
-};
+ render: ({ label, description, placeholder, width, disabled }) => (
+
+
+
+ ),
+}
export const Disabled: Story = {
- render: () => (
-
-
-
- ),
-};
+ render: ({ label, description, placeholder, width }) => (
+
+
+
+ ),
+}
+
+export const Multiple: Story = {
+ render: ({ label, description, placeholder, width }) => (
+
+
+
+ ),
+}
diff --git a/src/components/form-field/listbox/listbox.test.tsx b/src/components/form-field/listbox/listbox.test.tsx
new file mode 100644
index 00000000..a1386fe0
--- /dev/null
+++ b/src/components/form-field/listbox/listbox.test.tsx
@@ -0,0 +1,20 @@
+import { describe, expect, it, vi } from 'vitest'
+import { render, screen } from '@testing-library/react'
+import { FormField } from '../form-field'
+
+describe('Listbox', () => {
+ it('renders the placeholder text', () => {
+ render(
+
+
+
+
+ ,
+ )
+
+ expect(screen.getByText('Select...')).toBeInTheDocument()
+ })
+})
diff --git a/src/components/form-field/listbox/listbox.tsx b/src/components/form-field/listbox/listbox.tsx
index 0ae4675f..510f7866 100644
--- a/src/components/form-field/listbox/listbox.tsx
+++ b/src/components/form-field/listbox/listbox.tsx
@@ -1,38 +1,51 @@
-import { Listbox as HeadlessListbox } from "@headlessui/react";
-import React from "react";
-import { ListboxOptions } from "./listbox-options";
-import { ListboxOption } from "./listbox-option";
-import { ListboxButton } from "./listbox-button";
-import { classNames } from "../../../util/class-names";
+import type { ReactNode } from 'react'
+import { Listbox as HeadlessListbox } from '@headlessui/react'
+import { ListboxOptions } from './listbox-options'
+import { ListboxOption } from './listbox-option'
+import { ListboxButton } from './listbox-button'
+import { classNames } from '../../../util/class-names'
const formFieldGroupStyles = classNames(
- // first element
- "[.group.form-field-group_&:first-child_button]:rounded-r-none [.group.form-field-group_&:first-child_button]:border-r-0",
- // elements in between
- "[.group.form-field-group_&:not(:first-child):not(:last-child)_button]:rounded-none [.group.form-field-group_&:not(:first-child):not(:last-child)_button]:border-r-0",
- // last element
- "[.group.form-field-group_&:last-child_button]:rounded-l-none"
-);
+ // first element
+ '[.group.form-field-group_&:first-child_button]:rounded-r-none [.group.form-field-group_&:first-child_button]:border-r-0',
+ // elements in between
+ '[.group.form-field-group_&:not(:first-child):not(:last-child)_button]:rounded-none [.group.form-field-group_&:not(:first-child):not(:last-child)_button]:border-r-0',
+ // last element
+ '[.group.form-field-group_&:last-child_button]:rounded-l-none',
+)
export interface ListboxProps {
- children: React.ReactNode;
- value: TValue;
- onChange: (value: TValue) => void;
- className?: string;
+ children: ReactNode
+ value: TValue
+ onChange: (value: TValue) => void
+ multiple?: boolean
+ className?: string
}
-const Listbox = ({ children, value, onChange, className }: ListboxProps) => {
- return (
-
-
- {children}
-
-
- );
-};
+const Listbox = ({
+ children,
+ value,
+ onChange,
+ multiple,
+ className,
+}: ListboxProps) => {
+ return (
+
+
+ {children}
+
+
+ )
+}
-Listbox.Button = ListboxButton;
-Listbox.Options = ListboxOptions;
-Listbox.Option = ListboxOption;
+Listbox.Button = ListboxButton
+Listbox.Options = ListboxOptions
+Listbox.Option = ListboxOption
-export { Listbox };
+export { Listbox }
diff --git a/src/components/form-field/multi-combobox/multi-combobox-badges.stories.tsx b/src/components/form-field/multi-combobox/multi-combobox-badges.stories.tsx
new file mode 100644
index 00000000..11240afc
--- /dev/null
+++ b/src/components/form-field/multi-combobox/multi-combobox-badges.stories.tsx
@@ -0,0 +1,41 @@
+import type { Meta, StoryObj } from '@storybook/react-vite'
+import { FormField } from '../form-field'
+import { hiddenArgControl } from '../../../util/storybook-utils'
+import {
+ MultiComboboxBadgeStory,
+ multiComboboxArgTypes,
+ multiComboboxArgs,
+} from './multi-combobox.story-helpers'
+
+const meta: Meta = {
+ title: 'Input/MultiCombobox/Badges',
+ component: FormField.MultiCombobox,
+ args: multiComboboxArgs,
+ argTypes: {
+ ...multiComboboxArgTypes,
+ value: hiddenArgControl,
+ onChange: hiddenArgControl,
+ className: hiddenArgControl,
+ },
+ parameters: {
+ controls: {
+ exclude: ['value', 'onChange', 'className'],
+ },
+ },
+}
+
+export default meta
+
+type Story = StoryObj
+
+export const Badges: Story = {
+ render: ({ label, description, placeholder, width }) => (
+
+
+
+ ),
+}
diff --git a/src/components/form-field/multi-combobox/multi-combobox-custom-option.tsx b/src/components/form-field/multi-combobox/multi-combobox-custom-option.tsx
index 60df5c90..bc2c1ba4 100644
--- a/src/components/form-field/multi-combobox/multi-combobox-custom-option.tsx
+++ b/src/components/form-field/multi-combobox/multi-combobox-custom-option.tsx
@@ -1,21 +1,21 @@
-import { ComboboxOption as HeadlessUiComboboxOption } from "@headlessui/react";
-import React from "react";
+import type { ReactNode } from 'react'
+import { ComboboxOption as HeadlessUiComboboxOption } from '@headlessui/react'
export interface MultiComboboxCustomOptionProps {
- value: TValue;
- children: React.ReactNode;
+ value: TValue
+ children: ReactNode
}
export const MultiComboboxCustomOption = ({
- value,
- children,
+ value,
+ children,
}: MultiComboboxCustomOptionProps) => {
- return (
-
- {children}
-
- );
-};
+ return (
+
+ {children}
+
+ )
+}
diff --git a/src/components/form-field/multi-combobox/multi-combobox-custom-value.stories.tsx b/src/components/form-field/multi-combobox/multi-combobox-custom-value.stories.tsx
new file mode 100644
index 00000000..6698f6e8
--- /dev/null
+++ b/src/components/form-field/multi-combobox/multi-combobox-custom-value.stories.tsx
@@ -0,0 +1,41 @@
+import type { Meta, StoryObj } from '@storybook/react-vite'
+import { FormField } from '../form-field'
+import { hiddenArgControl } from '../../../util/storybook-utils'
+import {
+ MultiComboboxCustomValueStory,
+ multiComboboxArgTypes,
+ multiComboboxArgs,
+} from './multi-combobox.story-helpers'
+
+const meta: Meta = {
+ title: 'Input/MultiCombobox/CustomValue',
+ component: FormField.MultiCombobox,
+ args: multiComboboxArgs,
+ argTypes: {
+ ...multiComboboxArgTypes,
+ value: hiddenArgControl,
+ onChange: hiddenArgControl,
+ className: hiddenArgControl,
+ },
+ parameters: {
+ controls: {
+ exclude: ['value', 'onChange', 'className'],
+ },
+ },
+}
+
+export default meta
+
+type Story = StoryObj
+
+export const CustomValue: Story = {
+ render: ({ label, description, placeholder, width }) => (
+
+
+
+ ),
+}
diff --git a/src/components/form-field/multi-combobox/multi-combobox-empty-option.tsx b/src/components/form-field/multi-combobox/multi-combobox-empty-option.tsx
index 723e7814..29bad4cc 100644
--- a/src/components/form-field/multi-combobox/multi-combobox-empty-option.tsx
+++ b/src/components/form-field/multi-combobox/multi-combobox-empty-option.tsx
@@ -1,9 +1,11 @@
-import React from "react";
+import type { ReactNode } from 'react'
export interface MultiComboboxEmptyOptionProps {
- children: React.ReactNode;
+ children: ReactNode
}
-export const MultiComboboxEmptyOption = ({ children }: MultiComboboxEmptyOptionProps) => {
- return {children}
;
-};
+export const MultiComboboxEmptyOption = ({
+ children,
+}: MultiComboboxEmptyOptionProps) => {
+ return {children}
+}
diff --git a/src/components/form-field/multi-combobox/multi-combobox-input.tsx b/src/components/form-field/multi-combobox/multi-combobox-input.tsx
index c9695072..57a298da 100644
--- a/src/components/form-field/multi-combobox/multi-combobox-input.tsx
+++ b/src/components/form-field/multi-combobox/multi-combobox-input.tsx
@@ -1,55 +1,58 @@
+import type { ChangeEvent } from 'react'
import {
- ComboboxInputProps,
- ComboboxButton as HeadlessUiComboboxButton,
- ComboboxInput as HeadlessUiComboboxInput,
-} from "@headlessui/react";
-import React from "react";
-import { CaretDownIcon } from "../../../icons";
-import { classNames } from "../../../util/class-names";
+ ComboboxInputProps,
+ ComboboxButton as HeadlessUiComboboxButton,
+ ComboboxInput as HeadlessUiComboboxInput,
+} from '@headlessui/react'
+import { CaretDownIcon } from '../../../icons'
+import { classNames } from '../../../util/class-names'
-export interface MultiComboboxInputProps extends Omit {
- id: string;
- displayValue: string;
- placeholder: string;
- onChange: (event: React.ChangeEvent) => void;
- showButton?: boolean;
+export interface MultiComboboxInputProps extends Omit<
+ ComboboxInputProps,
+ 'displayValue'
+> {
+ id: string
+ displayValue: string
+ placeholder: string
+ onChange: (event: ChangeEvent) => void
+ showButton?: boolean
}
export const MultiComboboxInput = ({
- id,
- displayValue,
- placeholder,
- onChange,
- showButton = true,
- disabled = false,
- ...props
+ id,
+ displayValue,
+ placeholder,
+ onChange,
+ showButton = true,
+ disabled = false,
+ ...props
}: MultiComboboxInputProps) => {
- return (
-
-
displayValue}
- onChange={onChange}
- className={classNames(
- "paragraph-100 focus-visible:border-primary-400 focus-visible:ring-primary-200 flex h-8 w-full items-center rounded-sm border border-neutral-400 py-2 pr-8 pl-3 focus-visible:ring-2",
- disabled &&
- "cursor-not-allowed border-neutral-300 bg-neutral-100 text-neutral-600 hover:border-neutral-300"
- )}
- {...props}
+ return (
+
+
displayValue}
+ onChange={onChange}
+ className={classNames(
+ 'paragraph-100 focus-visible:border-primary-400 focus-visible:ring-primary-200 flex h-8 w-full items-center rounded-sm border border-neutral-400 py-2 pr-8 pl-3 focus-visible:ring-2',
+ disabled &&
+ 'cursor-not-allowed border-neutral-300 bg-neutral-100 text-neutral-600 hover:border-neutral-300',
+ )}
+ {...props}
+ />
+ {showButton && !disabled ?
+
+
+
- {showButton && !disabled ? (
-
-
-
-
-
- ) : null}
-
- );
-};
+
+
+ : null}
+
+ )
+}
diff --git a/src/components/form-field/multi-combobox/multi-combobox-option.tsx b/src/components/form-field/multi-combobox/multi-combobox-option.tsx
index 16bf089a..5afdf028 100644
--- a/src/components/form-field/multi-combobox/multi-combobox-option.tsx
+++ b/src/components/form-field/multi-combobox/multi-combobox-option.tsx
@@ -1,25 +1,25 @@
-import { ComboboxOption as HeadlessUiComboboxOption } from "@headlessui/react";
-import React from "react";
-import { SmallTickIcon } from "../../../icons";
+import type { ReactNode } from 'react'
+import { ComboboxOption as HeadlessUiComboboxOption } from '@headlessui/react'
+import { SmallTickIcon } from '../../../icons'
export interface MultiComboboxOptionProps {
- value: TValue;
- children: React.ReactNode;
+ value: TValue
+ children: ReactNode
}
export const MultiComboboxOption = ({
- value,
- children,
+ value,
+ children,
}: MultiComboboxOptionProps) => {
- return (
-
- {children}
-
-
-
-
- );
-};
+ return (
+
+ {children}
+
+
+
+
+ )
+}
diff --git a/src/components/form-field/multi-combobox/multi-combobox-options.tsx b/src/components/form-field/multi-combobox/multi-combobox-options.tsx
index 6f61e417..363137e0 100644
--- a/src/components/form-field/multi-combobox/multi-combobox-options.tsx
+++ b/src/components/form-field/multi-combobox/multi-combobox-options.tsx
@@ -1,24 +1,27 @@
-import { ComboboxOptions as HeadlessUiComboboxOptions } from "@headlessui/react";
-import React from "react";
-import { classNames } from "../../../util/class-names";
+import type { ReactNode } from 'react'
+import { ComboboxOptions as HeadlessUiComboboxOptions } from '@headlessui/react'
+import { classNames } from '../../../util/class-names'
export interface MultiComboboxOptionsProps {
- children: React.ReactNode;
- className?: string;
+ children: ReactNode
+ className?: string
}
-const MultiComboboxOptions = ({ children, className }: MultiComboboxOptionsProps) => {
- return (
-
- {children}
-
- );
-};
+const MultiComboboxOptions = ({
+ children,
+ className,
+}: MultiComboboxOptionsProps) => {
+ return (
+
+ {children}
+
+ )
+}
-export { MultiComboboxOptions };
+export { MultiComboboxOptions }
diff --git a/src/components/form-field/multi-combobox/multi-combobox-results-badges.tsx b/src/components/form-field/multi-combobox/multi-combobox-results-badges.tsx
index 54ddbce5..cdcac9f6 100644
--- a/src/components/form-field/multi-combobox/multi-combobox-results-badges.tsx
+++ b/src/components/form-field/multi-combobox/multi-combobox-results-badges.tsx
@@ -1,9 +1,11 @@
-import React from "react";
+import type { ReactNode } from 'react'
export interface MultiComboboxResultsBadgesProps {
- children: React.ReactNode;
+ children: ReactNode
}
-export const MultiComboboxResultsBadges = ({ children }: MultiComboboxResultsBadgesProps) => {
- return {children}
;
-};
+export const MultiComboboxResultsBadges = ({
+ children,
+}: MultiComboboxResultsBadgesProps) => {
+ return {children}
+}
diff --git a/src/components/form-field/multi-combobox/multi-combobox-results-label.tsx b/src/components/form-field/multi-combobox/multi-combobox-results-label.tsx
index d053b732..fe561b93 100644
--- a/src/components/form-field/multi-combobox/multi-combobox-results-label.tsx
+++ b/src/components/form-field/multi-combobox/multi-combobox-results-label.tsx
@@ -1,9 +1,11 @@
-import React from "react";
+import type { ReactNode } from 'react'
export interface MultiComboboxResultsLabelProps {
- children: React.ReactNode;
+ children: ReactNode
}
-export const MultiComboboxResultsLabel = ({ children }: MultiComboboxResultsLabelProps) => {
- return {children}
;
-};
+export const MultiComboboxResultsLabel = ({
+ children,
+}: MultiComboboxResultsLabelProps) => {
+ return {children}
+}
diff --git a/src/components/form-field/multi-combobox/multi-combobox-results-tags.tsx b/src/components/form-field/multi-combobox/multi-combobox-results-tags.tsx
index 45ca66bf..18fc7622 100644
--- a/src/components/form-field/multi-combobox/multi-combobox-results-tags.tsx
+++ b/src/components/form-field/multi-combobox/multi-combobox-results-tags.tsx
@@ -1,9 +1,11 @@
-import React from "react";
+import type { ReactNode } from 'react'
export interface MultiComboboxResultsTagsProps {
- children: React.ReactNode;
+ children: ReactNode
}
-export const MultiComboboxResultsTags = ({ children }: MultiComboboxResultsTagsProps) => {
- return {children}
;
-};
+export const MultiComboboxResultsTags = ({
+ children,
+}: MultiComboboxResultsTagsProps) => {
+ return {children}
+}
diff --git a/src/components/form-field/multi-combobox/multi-combobox-results-text.tsx b/src/components/form-field/multi-combobox/multi-combobox-results-text.tsx
index a2314515..f5dcbb82 100644
--- a/src/components/form-field/multi-combobox/multi-combobox-results-text.tsx
+++ b/src/components/form-field/multi-combobox/multi-combobox-results-text.tsx
@@ -1,9 +1,11 @@
-import React from "react";
+import type { ReactNode } from 'react'
export interface MultiComboboxResultsTextProps {
- children: React.ReactNode;
+ children: ReactNode
}
-export const MultiComboboxResultsText = ({ children }: MultiComboboxResultsTextProps) => {
- return {children}
;
-};
+export const MultiComboboxResultsText = ({
+ children,
+}: MultiComboboxResultsTextProps) => {
+ return {children}
+}
diff --git a/src/components/form-field/multi-combobox/multi-combobox-results.tsx b/src/components/form-field/multi-combobox/multi-combobox-results.tsx
index 65fc42ac..4515fc7a 100644
--- a/src/components/form-field/multi-combobox/multi-combobox-results.tsx
+++ b/src/components/form-field/multi-combobox/multi-combobox-results.tsx
@@ -1,20 +1,20 @@
-import React from "react";
-import { MultiComboboxResultsBadges } from "./multi-combobox-results-badges";
-import { MultiComboboxResultsLabel } from "./multi-combobox-results-label";
-import { MultiComboboxResultsTags } from "./multi-combobox-results-tags";
-import { MultiComboboxResultsText } from "./multi-combobox-results-text";
+import type { ReactNode } from 'react'
+import { MultiComboboxResultsBadges } from './multi-combobox-results-badges'
+import { MultiComboboxResultsLabel } from './multi-combobox-results-label'
+import { MultiComboboxResultsTags } from './multi-combobox-results-tags'
+import { MultiComboboxResultsText } from './multi-combobox-results-text'
export interface MultiComboboxResultsProps {
- children: React.ReactNode;
+ children: ReactNode
}
const MultiComboboxResults = ({ children }: MultiComboboxResultsProps) => {
- return {children}
;
-};
+ return {children}
+}
-MultiComboboxResults.Label = MultiComboboxResultsLabel;
-MultiComboboxResults.Text = MultiComboboxResultsText;
-MultiComboboxResults.Badges = MultiComboboxResultsBadges;
-MultiComboboxResults.Tags = MultiComboboxResultsTags;
+MultiComboboxResults.Label = MultiComboboxResultsLabel
+MultiComboboxResults.Text = MultiComboboxResultsText
+MultiComboboxResults.Badges = MultiComboboxResultsBadges
+MultiComboboxResults.Tags = MultiComboboxResultsTags
-export { MultiComboboxResults };
+export { MultiComboboxResults }
diff --git a/src/components/form-field/multi-combobox/multi-combobox-tags.stories.tsx b/src/components/form-field/multi-combobox/multi-combobox-tags.stories.tsx
new file mode 100644
index 00000000..b0d617f4
--- /dev/null
+++ b/src/components/form-field/multi-combobox/multi-combobox-tags.stories.tsx
@@ -0,0 +1,41 @@
+import type { Meta, StoryObj } from '@storybook/react-vite'
+import { FormField } from '../form-field'
+import { hiddenArgControl } from '../../../util/storybook-utils'
+import {
+ MultiComboboxTagStory,
+ multiComboboxArgTypes,
+ multiComboboxArgs,
+} from './multi-combobox.story-helpers'
+
+const meta: Meta = {
+ title: 'Input/MultiCombobox/Tags',
+ component: FormField.MultiCombobox,
+ args: multiComboboxArgs,
+ argTypes: {
+ ...multiComboboxArgTypes,
+ value: hiddenArgControl,
+ onChange: hiddenArgControl,
+ className: hiddenArgControl,
+ },
+ parameters: {
+ controls: {
+ exclude: ['value', 'onChange', 'className'],
+ },
+ },
+}
+
+export default meta
+
+type Story = StoryObj
+
+export const Tags: Story = {
+ render: ({ label, description, placeholder, width }) => (
+
+
+
+ ),
+}
diff --git a/src/components/form-field/multi-combobox/multi-combobox.stories.tsx b/src/components/form-field/multi-combobox/multi-combobox.stories.tsx
index b65e250b..41470a15 100644
--- a/src/components/form-field/multi-combobox/multi-combobox.stories.tsx
+++ b/src/components/form-field/multi-combobox/multi-combobox.stories.tsx
@@ -1,312 +1,42 @@
/* eslint-disable react/jsx-props-no-spreading */
-import type { Meta, StoryObj } from "@storybook/react";
-import React from "react";
-import { FormField } from "../form-field";
-import { Badge } from "../../badge/badge";
-import { Tag } from "../../tag/tag";
-
-const meta: Meta = {
- title: "Input/MultiCombobox",
- component: FormField.MultiCombobox,
-};
-
-export default meta;
-
-type Story = StoryObj;
-
-const people = [
- "Durward Reynolds",
- "Kenton Towne",
- "Therese Wunsch",
- "Benedict Kessler",
- "Katelyn Rohan",
-];
-
-const MultiComboboxWithHooks = () => {
- const [selectedPersons, setSelectedPersons] = React.useState([]);
- const [query, setQuery] = React.useState("");
-
- const filteredPeople =
- query === ""
- ? people
- : people.filter((person) => {
- return person.toLowerCase().includes(query.toLowerCase());
- });
-
- return (
-
-
- Label
- Description
-
- setSelectedPersons(value)}
- >
- setQuery(event.target.value)}
- />
-
- {filteredPeople.length === 0 ? (
-
-
- No persons found for {query}
-
-
- ) : null}
- {filteredPeople.map((person) => (
-
- {person}
-
- ))}
-
- {selectedPersons.length > 0 ? (
-
-
- Selected values:
-
-
- {selectedPersons.map((person) => person).join(", ")}
-
-
- ) : null}
-
-
- );
-};
-
-const MultiComboboxBadgeWithHooks = () => {
- const [selectedPersons, setSelectedPersons] = React.useState([]);
- const [query, setQuery] = React.useState("");
-
- const filteredPeople =
- query === ""
- ? people
- : people.filter((person) => {
- return person.toLowerCase().includes(query.toLowerCase());
- });
-
- return (
-
-
- Label
- Description
-
- setSelectedPersons(value)}
- >
- setQuery(event.target.value)}
- />
-
- {filteredPeople.length === 0 ? (
-
-
- No persons found for {query}
-
-
- ) : null}
- {filteredPeople.map((person) => (
-
- {person}
-
- ))}
-
- {selectedPersons.length > 0 ? (
-
-
- Selected values:
-
-
- {selectedPersons.map((person) => {
- return {person} ;
- })}
-
-
- ) : null}
-
-
- );
-};
-
-export const Badges: Story = {
- render: () => (
-
-
-
- ),
-};
-
-const MultiComboboxTagWithHooks = () => {
- const [selectedPersons, setSelectedPersons] = React.useState([]);
- const [query, setQuery] = React.useState("");
-
- const filteredPeople =
- query === ""
- ? people
- : people.filter((person) => {
- return person.toLowerCase().includes(query.toLowerCase());
- });
-
- return (
-
-
- Label
- Description
-
- setSelectedPersons(value)}
- >
- setQuery(event.target.value)}
- />
-
- {filteredPeople.length === 0 ? (
-
-
- No persons found for {query}
-
-
- ) : null}
- {filteredPeople.map((person) => (
-
- {person}
-
- ))}
-
- {selectedPersons.length > 0 ? (
-
-
- Selected values:
-
-
- {selectedPersons.map((person) => {
- return (
- {
- setSelectedPersons(
- selectedPersons.filter((p) => p !== person)
- );
- }}
- >
- {person}
-
- );
- })}
-
-
- ) : null}
-
-
- );
-};
-
-const MultiComboboxCustomValueWithHooks = () => {
- const [selectedPersons, setSelectedPersons] = React.useState([]);
- const [query, setQuery] = React.useState("");
- const [peopleCopy, setPeopleCopy] = React.useState(people);
-
- const handleSelectedPeopleChange = (value: string[]) => {
- const uniqueCustomValue = value.filter((valueItem) => {
- return peopleCopy.indexOf(valueItem) === -1;
- });
- setQuery("");
- setPeopleCopy([...uniqueCustomValue, ...peopleCopy]);
- setSelectedPersons(value);
- };
-
- const filteredPeople =
- query === ""
- ? peopleCopy
- : peopleCopy.filter((person) => {
- return person.toLowerCase().includes(query.toLowerCase());
- });
-
- return (
-
-
- Label
- Description
-
-
- setQuery(event.target.value)}
- />
-
- {query.length > 0 && peopleCopy.indexOf(query) === -1 && (
-
- Create tag: {query}
-
- )}
- {filteredPeople.map((person) => (
-
- {person}
-
- ))}
-
- {selectedPersons.length > 0 ? (
-
-
- Selected values:
-
-
- {selectedPersons.map((person) => {
- return (
- {
- setSelectedPersons(
- selectedPersons.filter((p) => p !== person)
- );
- }}
- >
- {person}
-
- );
- })}
-
-
- ) : null}
-
-
- );
-};
+import type { Meta, StoryObj } from '@storybook/react-vite'
+import { FormField } from '../form-field'
+import { hiddenArgControl } from '../../../util/storybook-utils'
+import {
+ MultiComboboxTextStory,
+ multiComboboxArgTypes,
+ multiComboboxArgs,
+} from './multi-combobox.story-helpers'
+
+const meta: Meta = {
+ title: 'Input/MultiCombobox',
+ component: FormField.MultiCombobox,
+ args: multiComboboxArgs,
+ argTypes: {
+ ...multiComboboxArgTypes,
+ value: hiddenArgControl,
+ onChange: hiddenArgControl,
+ className: hiddenArgControl,
+ },
+ parameters: {
+ controls: {
+ exclude: ['value', 'onChange', 'className'],
+ },
+ },
+}
+
+export default meta
+
+type Story = StoryObj
export const Default: Story = {
- render: () => (
-
-
-
- ),
-};
-
-export const Tags: Story = {
- render: () => (
-
-
-
- ),
-};
-
-export const CustomValue: Story = {
- render: () => (
-
-
-
- ),
-};
+ render: ({ label, description, placeholder, width }) => (
+
+
+
+ ),
+}
diff --git a/src/components/form-field/multi-combobox/multi-combobox.story-helpers.tsx b/src/components/form-field/multi-combobox/multi-combobox.story-helpers.tsx
new file mode 100644
index 00000000..8ce47013
--- /dev/null
+++ b/src/components/form-field/multi-combobox/multi-combobox.story-helpers.tsx
@@ -0,0 +1,317 @@
+import { useState } from 'react'
+import { FormField } from '../form-field'
+import { Badge } from '../../badge/badge'
+import { Tag } from '../../tag/tag'
+
+export type MultiComboboxStoryArgs = {
+ label: string
+ description: string
+ placeholder: string
+ width: number
+}
+
+export const multiComboboxArgs: MultiComboboxStoryArgs = {
+ label: 'Label',
+ description: 'Description',
+ placeholder: 'Select person...',
+ width: 288,
+}
+
+export const multiComboboxArgTypes = {
+ label: { control: 'text' },
+ description: { control: 'text' },
+ placeholder: { control: 'text' },
+ width: { control: { type: 'range', min: 200, max: 360, step: 16 } },
+}
+
+const people = [
+ 'Durward Reynolds',
+ 'Kenton Towne',
+ 'Therese Wunsch',
+ 'Benedict Kessler',
+ 'Katelyn Rohan',
+]
+
+export const MultiComboboxTextStory = ({
+ label,
+ description,
+ placeholder,
+}: Pick) => {
+ const [selectedPersons, setSelectedPersons] = useState([])
+ const [query, setQuery] = useState('')
+
+ const filteredPeople =
+ query === '' ? people : (
+ people.filter((person) => {
+ return person.toLowerCase().includes(query.toLowerCase())
+ })
+ )
+
+ return (
+
+
+ {label}
+
+ {description}
+
+
+ setSelectedPersons(value)}
+ >
+ setQuery(event.target.value)}
+ />
+
+ {filteredPeople.length === 0 ?
+
+
+ No persons found for {query}
+
+
+ : null}
+ {filteredPeople.map((person) => (
+
+ {person}
+
+ ))}
+
+ {selectedPersons.length > 0 ?
+
+
+ Selected values:
+
+
+ {selectedPersons.map((person) => person).join(', ')}
+
+
+ : null}
+
+
+ )
+}
+
+export const MultiComboboxBadgeStory = ({
+ label,
+ description,
+ placeholder,
+}: Pick) => {
+ const [selectedPersons, setSelectedPersons] = useState([])
+ const [query, setQuery] = useState('')
+
+ const filteredPeople =
+ query === '' ? people : (
+ people.filter((person) => {
+ return person.toLowerCase().includes(query.toLowerCase())
+ })
+ )
+
+ return (
+
+
+ {label}
+
+ {description}
+
+
+ setSelectedPersons(value)}
+ >
+ setQuery(event.target.value)}
+ />
+
+ {filteredPeople.length === 0 ?
+
+
+ No persons found for {query}
+
+
+ : null}
+ {filteredPeople.map((person) => (
+
+ {person}
+
+ ))}
+
+ {selectedPersons.length > 0 ?
+
+
+ Selected values:
+
+
+ {selectedPersons.map((person) => {
+ return {person}
+ })}
+
+
+ : null}
+
+
+ )
+}
+
+export const MultiComboboxTagStory = ({
+ label,
+ description,
+ placeholder,
+}: Pick) => {
+ const [selectedPersons, setSelectedPersons] = useState([])
+ const [query, setQuery] = useState('')
+
+ const filteredPeople =
+ query === '' ? people : (
+ people.filter((person) => {
+ return person.toLowerCase().includes(query.toLowerCase())
+ })
+ )
+
+ return (
+
+
+ {label}
+
+ {description}
+
+
+ setSelectedPersons(value)}
+ >
+ setQuery(event.target.value)}
+ />
+
+ {filteredPeople.length === 0 ?
+
+
+ No persons found for {query}
+
+
+ : null}
+ {filteredPeople.map((person) => (
+
+ {person}
+
+ ))}
+
+ {selectedPersons.length > 0 ?
+
+
+ Selected values:
+
+
+ {selectedPersons.map((person) => {
+ return (
+ {
+ setSelectedPersons(
+ selectedPersons.filter((p) => p !== person),
+ )
+ }}
+ >
+ {person}
+
+ )
+ })}
+
+
+ : null}
+
+
+ )
+}
+
+export const MultiComboboxCustomValueStory = ({
+ label,
+ description,
+ placeholder,
+}: Pick) => {
+ const [selectedPersons, setSelectedPersons] = useState([])
+ const [query, setQuery] = useState('')
+ const [peopleCopy, setPeopleCopy] = useState(people)
+
+ const handleSelectedPeopleChange = (value: string[]) => {
+ const uniqueCustomValue = value.filter((valueItem) => {
+ return peopleCopy.indexOf(valueItem) === -1
+ })
+ setQuery('')
+ setPeopleCopy([...uniqueCustomValue, ...peopleCopy])
+ setSelectedPersons(value)
+ }
+
+ const filteredPeople =
+ query === '' ? peopleCopy : (
+ peopleCopy.filter((person) => {
+ return person.toLowerCase().includes(query.toLowerCase())
+ })
+ )
+
+ return (
+
+
+ {label}
+
+ {description}
+
+
+
+ setQuery(event.target.value)}
+ />
+
+ {query.length > 0 && peopleCopy.indexOf(query) === -1 && (
+
+ Create tag: {query}
+
+ )}
+ {filteredPeople.map((person) => (
+
+ {person}
+
+ ))}
+
+ {selectedPersons.length > 0 ?
+
+
+ Selected values:
+
+
+ {selectedPersons.map((person) => {
+ return (
+ {
+ setSelectedPersons(
+ selectedPersons.filter((p) => p !== person),
+ )
+ }}
+ >
+ {person}
+
+ )
+ })}
+
+
+ : null}
+
+
+ )
+}
diff --git a/src/components/form-field/multi-combobox/multi-combobox.tsx b/src/components/form-field/multi-combobox/multi-combobox.tsx
index 9f404bc4..f78ee944 100644
--- a/src/components/form-field/multi-combobox/multi-combobox.tsx
+++ b/src/components/form-field/multi-combobox/multi-combobox.tsx
@@ -1,31 +1,35 @@
-import { Combobox as HeadlessUiCombobox } from "@headlessui/react";
-import React from "react";
-import { MultiComboboxCustomOption } from "./multi-combobox-custom-option";
-import { MultiComboboxEmptyOption } from "./multi-combobox-empty-option";
-import { MultiComboboxInput } from "./multi-combobox-input";
-import { MultiComboboxOption } from "./multi-combobox-option";
-import { MultiComboboxOptions } from "./multi-combobox-options";
-import { MultiComboboxResults } from "./multi-combobox-results";
+import type { ReactNode } from 'react'
+import { Combobox as HeadlessUiCombobox } from '@headlessui/react'
+import { MultiComboboxCustomOption } from './multi-combobox-custom-option'
+import { MultiComboboxEmptyOption } from './multi-combobox-empty-option'
+import { MultiComboboxInput } from './multi-combobox-input'
+import { MultiComboboxOption } from './multi-combobox-option'
+import { MultiComboboxOptions } from './multi-combobox-options'
+import { MultiComboboxResults } from './multi-combobox-results'
export interface MultiComboboxProps {
- value: TValue[];
- onChange: (value: TValue[]) => void;
- children: React.ReactNode;
+ value: TValue[]
+ onChange: (value: TValue[]) => void
+ children: ReactNode
}
-const MultiCombobox = ({ value, onChange, children }: MultiComboboxProps) => {
- return (
-
- {children}
-
- );
-};
+const MultiCombobox = ({
+ value,
+ onChange,
+ children,
+}: MultiComboboxProps) => {
+ return (
+
+ {children}
+
+ )
+}
-MultiCombobox.Input = MultiComboboxInput;
-MultiCombobox.Options = MultiComboboxOptions;
-MultiCombobox.Option = MultiComboboxOption;
-MultiCombobox.CustomOption = MultiComboboxCustomOption;
-MultiCombobox.EmptyOption = MultiComboboxEmptyOption;
-MultiCombobox.Results = MultiComboboxResults;
+MultiCombobox.Input = MultiComboboxInput
+MultiCombobox.Options = MultiComboboxOptions
+MultiCombobox.Option = MultiComboboxOption
+MultiCombobox.CustomOption = MultiComboboxCustomOption
+MultiCombobox.EmptyOption = MultiComboboxEmptyOption
+MultiCombobox.Results = MultiComboboxResults
-export { MultiCombobox };
+export { MultiCombobox }
diff --git a/src/components/form-field/number-input/number-input.tsx b/src/components/form-field/number-input/number-input.tsx
index 65a272a4..8dfc70e8 100644
--- a/src/components/form-field/number-input/number-input.tsx
+++ b/src/components/form-field/number-input/number-input.tsx
@@ -1,76 +1,76 @@
-import React from "react";
-import { classNames } from "../../../util/class-names";
+import type { ChangeEvent, ElementType } from 'react'
+import { classNames } from '../../../util/class-names'
const textAlignVariants = {
- left: "text-left",
- center: "text-center",
- right: "text-right",
-};
+ left: 'text-left',
+ center: 'text-center',
+ right: 'text-right',
+}
export interface NumberInputProps {
- id: string;
- placeholder: string;
- value: number;
- textAlign?: keyof typeof textAlignVariants;
- onChange?: (event: React.ChangeEvent) => void;
- readOnly?: boolean;
- ariaDescribedBy: string;
- LeftIcon?: React.ElementType;
- error?: boolean;
- disabled?: boolean;
- min?: number;
- max?: number;
+ id: string
+ placeholder: string
+ value: number
+ textAlign?: keyof typeof textAlignVariants
+ onChange?: (event: ChangeEvent) => void
+ readOnly?: boolean
+ ariaDescribedBy: string
+ LeftIcon?: ElementType
+ error?: boolean
+ disabled?: boolean
+ min?: number
+ max?: number
}
export const NumberInput = ({
- id,
- placeholder,
- value,
- min,
- max,
- onChange,
- ariaDescribedBy,
- LeftIcon,
- textAlign = "left",
- readOnly,
- error,
- disabled,
+ id,
+ placeholder,
+ value,
+ min,
+ max,
+ onChange,
+ ariaDescribedBy,
+ LeftIcon,
+ textAlign = 'left',
+ readOnly,
+ error,
+ disabled,
}: NumberInputProps) => {
- return (
-
- {LeftIcon ? (
-
-
-
- ) : null}
-
-
+ return (
+
+ {LeftIcon ?
+
+
- );
-};
+ : null}
+
+
+
+ )
+}
diff --git a/src/components/form-field/radio-box/radio-box-option.tsx b/src/components/form-field/radio-box/radio-box-option.tsx
index b04cb61b..71e7f29c 100644
--- a/src/components/form-field/radio-box/radio-box-option.tsx
+++ b/src/components/form-field/radio-box/radio-box-option.tsx
@@ -1,77 +1,87 @@
-import { Radio as HeadlessUiRadio } from "@headlessui/react";
-import React, { Fragment } from "react";
-import { classNames } from "../../../util/class-names";
+import { Radio as HeadlessUiRadio } from '@headlessui/react'
+import { Fragment } from 'react'
+import type { ReactNode } from 'react'
+import { classNames } from '../../../util/class-names'
export interface RadioBoxOptionProps {
- children: React.ReactNode;
- value: string;
- disabled?: boolean;
- className?: string;
+ children: ReactNode
+ value: string
+ disabled?: boolean
+ className?: string
}
const radioBoxContainerStyles = {
- base: "group relative flex items-center gap-3 rounded-lg bg-neutral-0 border p-4 border-neutral-300 hover:border-primary-600 hover:bg-primary-50 cursor-pointer focus:outline-hidden data-focus:outline-2 data-focus:outline-primary-200",
- checked: "border-primary-600 bg-primary-600 hover:bg-primary-600 hover:text-neutral-0",
- disabled:
- "bg-neutral-100 group-hover:border-neutral-300 group-hover:bg-neutral-100 hover:border-neutral-300 hover:bg-neutral-100 cursor-not-allowed",
-};
+ base: 'group relative flex items-center gap-3 rounded-lg bg-neutral-0 border p-4 border-neutral-300 hover:border-primary-600 hover:bg-primary-50 cursor-pointer focus:outline-hidden data-focus:outline-2 data-focus:outline-primary-200',
+ checked:
+ 'border-primary-600 bg-primary-600 hover:bg-primary-600 hover:text-neutral-0',
+ disabled:
+ 'bg-neutral-100 group-hover:border-neutral-300 group-hover:bg-neutral-100 hover:border-neutral-300 hover:bg-neutral-100 cursor-not-allowed',
+}
const radioBoxCircleStyles = {
- base: "relative inline-block h-4 w-4 shrink-0 rounded-full bg-neutral-0 border border-neutral-300",
- unchecked: "group-hover:border-primary-600 group-hover:bg-neutral-0",
- checked: "border-transparent",
- disabled:
- "bg-neutral-200 border-neutral-200 group-hover:border-neutral-200 group-hover:bg-neutral-200",
-};
-
-const Title = ({ children }: { children: React.ReactNode }) => (
-
- {children}
-
-);
+ base: 'relative inline-block h-4 w-4 shrink-0 rounded-full bg-neutral-0 border border-neutral-300',
+ unchecked: 'group-hover:border-primary-600 group-hover:bg-neutral-0',
+ checked: 'border-transparent',
+ disabled:
+ 'bg-neutral-200 border-neutral-200 group-hover:border-neutral-200 group-hover:bg-neutral-200',
+}
-const Description = ({ children }: { children: React.ReactNode }) => (
-
- {children}
-
-);
+const Title = ({ children }: { children: ReactNode }) => (
+
+ {children}
+
+)
-export const RadioBoxOption = ({ children, value, disabled, className }: RadioBoxOptionProps) => {
- return (
-
- {({ checked, disabled: optionDisabled }) => (
-
-
+const Description = ({ children }: { children: ReactNode }) => (
+
+ {children}
+
+)
-
{children}
-
+export const RadioBoxOption = ({
+ children,
+ value,
+ disabled,
+ className,
+}: RadioBoxOptionProps) => {
+ return (
+
+ {({ checked, disabled: optionDisabled }) => (
+
+
- );
-};
+ >
+ {checked && (
+
+ )}
+
+
+
+ {children}
+
+
+ )}
+
+ )
+}
-RadioBoxOption.Title = Title;
-RadioBoxOption.Description = Description;
+RadioBoxOption.Title = Title
+RadioBoxOption.Description = Description
diff --git a/src/components/form-field/radio-box/radio-box.stories.tsx b/src/components/form-field/radio-box/radio-box.stories.tsx
index d631dca8..165dca01 100644
--- a/src/components/form-field/radio-box/radio-box.stories.tsx
+++ b/src/components/form-field/radio-box/radio-box.stories.tsx
@@ -1,66 +1,116 @@
-import type { Meta, StoryObj } from "@storybook/react";
-import React, { useState } from "react";
-import { FormField } from "../form-field";
-import { RadioBox } from "./radio-box";
-import { FeaturedTag } from "../../featured-tag/featured-tag";
+import type { Meta, StoryObj } from '@storybook/react-vite'
+import { useEffect, useState } from 'react'
+import { FormField } from '../form-field'
+import { RadioBox } from './radio-box'
+import { FeaturedTag } from '../../featured-tag/featured-tag'
const meta: Meta = {
- title: "Input/RadioBox",
- component: RadioBox,
-};
+ title: 'Input/RadioBox',
+ component: RadioBox,
+ args: {
+ recommendedLabel: 'Recommended',
+ optionOneTitle: 'Option 1',
+ optionTwoTitle: 'Option 2',
+ optionThreeTitle: 'Option 3',
+ optionDescription:
+ 'To be, or not to be, that is the question: Whether ’tis nobler in the mind to suffer The slings and arrows of outrageous fortune, …',
+ defaultValue: 'value_1',
+ disableThirdOption: true,
+ width: 384,
+ },
+ argTypes: {
+ recommendedLabel: { control: 'text' },
+ optionOneTitle: { control: 'text' },
+ optionTwoTitle: { control: 'text' },
+ optionThreeTitle: { control: 'text' },
+ optionDescription: { control: 'text' },
+ defaultValue: {
+ control: 'select',
+ options: ['value_1', 'value_2', 'value_3'],
+ },
+ disableThirdOption: { control: 'boolean' },
+ width: { control: { type: 'range', min: 320, max: 520, step: 16 } },
+ },
+}
-export default meta;
+export default meta
-type Story = StoryObj;
+type Story = StoryObj
-const RadioBoxWithHooks = () => {
- const [value, setValue] = useState("value_1");
+const RadioBoxWithHooks = ({
+ recommendedLabel,
+ optionOneTitle,
+ optionTwoTitle,
+ optionThreeTitle,
+ optionDescription,
+ defaultValue,
+ disableThirdOption,
+}: {
+ recommendedLabel: string
+ optionOneTitle: string
+ optionTwoTitle: string
+ optionThreeTitle: string
+ optionDescription: string
+ defaultValue: string
+ disableThirdOption: boolean
+}) => {
+ const [value, setValue] = useState(defaultValue)
- return (
-
-
-
- Recommended
+ useEffect(() => {
+ setValue(defaultValue)
+ }, [defaultValue])
- Option 1
+ return (
+
+
+
+ {recommendedLabel}
-
- To be, or not to be, that is the question: Whether ’tis nobler in the mind
- to suffer The slings and arrows of outrageous fortune, …
-
-
+
+ {optionOneTitle}
+
-
- Option 2
+
+ {optionDescription}
+
+
-
- To be, or not to be, that is the question: Whether ’tis nobler in the mind
- to suffer The slings and arrows of outrageous fortune, …
-
-
+
+
+ {optionTwoTitle}
+
-
- Option 3
+
+ {optionDescription}
+
+
-
- To be, or not to be, that is the question: Whether ’tis nobler in the mind
- to suffer The slings and arrows of outrageous fortune, …
-
-
-
-
- );
-};
+
+
+ {optionThreeTitle}
+
+
+
+ {optionDescription}
+
+
+
+
+ )
+}
export const Default: Story = {
- render: () => (
-
-
-
- ),
-};
+ render: ({ width, ...args }) => (
+
+
+
+ ),
+}
diff --git a/src/components/form-field/radio-box/radio-box.tsx b/src/components/form-field/radio-box/radio-box.tsx
index 97dac92e..dcef0503 100644
--- a/src/components/form-field/radio-box/radio-box.tsx
+++ b/src/components/form-field/radio-box/radio-box.tsx
@@ -1,21 +1,32 @@
-import { RadioGroup as HeadlessUiRadioGroup } from "@headlessui/react";
-import React from "react";
-import { RadioBoxOption } from "./radio-box-option";
+import type { ReactNode } from 'react'
+import { RadioGroup as HeadlessUiRadioGroup } from '@headlessui/react'
+import { RadioBoxOption } from './radio-box-option'
export interface RadioBoxProps {
- id: string;
- children: React.ReactNode;
- value: string;
- onChange: (value: string) => void;
- className?: string;
+ id: string
+ children: ReactNode
+ value: string
+ onChange: (value: string) => void
+ className?: string
}
-export const RadioBox = ({ id, value, children, onChange, className }: RadioBoxProps) => {
- return (
-
- {children}
-
- );
-};
+export const RadioBox = ({
+ id,
+ value,
+ children,
+ onChange,
+ className,
+}: RadioBoxProps) => {
+ return (
+
+ {children}
+
+ )
+}
-RadioBox.Option = RadioBoxOption;
+RadioBox.Option = RadioBoxOption
diff --git a/src/components/form-field/radio-input/radio-input-option.tsx b/src/components/form-field/radio-input/radio-input-option.tsx
index a24f4eb5..20d9fe3a 100644
--- a/src/components/form-field/radio-input/radio-input-option.tsx
+++ b/src/components/form-field/radio-input/radio-input-option.tsx
@@ -1,55 +1,58 @@
-import { Radio as HeadlessUiRadio } from "@headlessui/react";
-import React from "react";
-import { classNames } from "../../../util/class-names";
+import type { ReactNode } from 'react'
+import { Radio as HeadlessUiRadio } from '@headlessui/react'
+import { classNames } from '../../../util/class-names'
export interface RadioInputOptionProps {
- children: React.ReactNode;
- value: string;
- disabled?: boolean;
+ children: ReactNode
+ value: string
+ disabled?: boolean
}
-export const RadioInputOption = ({ children, value, disabled }: RadioInputOptionProps) => {
- return (
-
- {({ checked, disabled: optionDisabled }) => (
-
- {checked ? (
-
-
-
- ) : (
-
- )}
-
- {children}
-
-
+export const RadioInputOption = ({
+ children,
+ value,
+ disabled,
+}: RadioInputOptionProps) => {
+ return (
+
+ {({ checked, disabled: optionDisabled }) => (
+
+ {checked ?
+
+
+
+ :
+ }
+
- );
-};
+ >
+ {children}
+
+
+ )}
+
+ )
+}
diff --git a/src/components/form-field/radio-input/radio-input.stories.tsx b/src/components/form-field/radio-input/radio-input.stories.tsx
index 8b096b54..8936bf60 100644
--- a/src/components/form-field/radio-input/radio-input.stories.tsx
+++ b/src/components/form-field/radio-input/radio-input.stories.tsx
@@ -1,40 +1,90 @@
-import type { Meta, StoryObj } from "@storybook/react";
-import React, { useState } from "react";
-import { FormField } from "../form-field";
+import type { Meta, StoryObj } from '@storybook/react-vite'
+import { useEffect, useState } from 'react'
+import { FormField } from '../form-field'
const meta: Meta = {
- title: "Input/RadioInput",
- component: FormField.RadioInput,
-};
+ title: 'Input/RadioInput',
+ component: FormField.RadioInput,
+ args: {
+ label: 'Label',
+ description: 'Description',
+ optionOneLabel: 'Value 1',
+ optionTwoLabel: 'Value 2',
+ optionThreeLabel: 'Value 3',
+ defaultValue: 'value_1',
+ disableThirdOption: true,
+ },
+ argTypes: {
+ label: { control: 'text' },
+ description: { control: 'text' },
+ optionOneLabel: { control: 'text' },
+ optionTwoLabel: { control: 'text' },
+ optionThreeLabel: { control: 'text' },
+ defaultValue: {
+ control: 'select',
+ options: ['value_1', 'value_2', 'value_3'],
+ },
+ disableThirdOption: { control: 'boolean' },
+ },
+}
-export default meta;
+export default meta
-type Story = StoryObj;
+type Story = StoryObj
-const RadioInputWithHooks = () => {
- const [value, setValue] = useState("value_1");
+const RadioInputWithHooks = ({
+ label,
+ description,
+ optionOneLabel,
+ optionTwoLabel,
+ optionThreeLabel,
+ defaultValue,
+ disableThirdOption,
+}: {
+ label: string
+ description: string
+ optionOneLabel: string
+ optionTwoLabel: string
+ optionThreeLabel: string
+ defaultValue: string
+ disableThirdOption: boolean
+}) => {
+ const [value, setValue] = useState(defaultValue)
- return (
-
-
- Label
- Description
-
-
- Value 1
- Value 2
-
- Value 3
-
-
-
- );
-};
+ useEffect(() => {
+ setValue(defaultValue)
+ }, [defaultValue])
+
+ return (
+
+
+ {label}
+
+ {description}
+
+
+
+
+ {optionOneLabel}
+
+
+ {optionTwoLabel}
+
+
+ {optionThreeLabel}
+
+
+
+ )
+}
export const Default: Story = {
- render: () => (
-
-
-
- ),
-};
+ render: (args) => (
+
+
+
+ ),
+}
diff --git a/src/components/form-field/radio-input/radio-input.tsx b/src/components/form-field/radio-input/radio-input.tsx
index e4ce0ef7..2ab55c7e 100644
--- a/src/components/form-field/radio-input/radio-input.tsx
+++ b/src/components/form-field/radio-input/radio-input.tsx
@@ -1,22 +1,22 @@
-import { RadioGroup as HeadlessUiRadioGroup } from "@headlessui/react";
-import React from "react";
-import { RadioInputOption } from "./radio-input-option";
+import type { ReactNode } from 'react'
+import { RadioGroup as HeadlessUiRadioGroup } from '@headlessui/react'
+import { RadioInputOption } from './radio-input-option'
export interface RadioInputProps {
- id: string;
- children: React.ReactNode;
- value: string;
- onChange: (value: string) => void;
+ id: string
+ children: ReactNode
+ value: string
+ onChange: (value: string) => void
}
const RadioInput = ({ id, children, value, onChange }: RadioInputProps) => {
- return (
-
- {children}
-
- );
-};
+ return (
+
+ {children}
+
+ )
+}
-RadioInput.Option = RadioInputOption;
+RadioInput.Option = RadioInputOption
-export { RadioInput };
+export { RadioInput }
diff --git a/src/components/form-field/search-input/search-input.stories.tsx b/src/components/form-field/search-input/search-input.stories.tsx
index 40be859d..dcefb954 100644
--- a/src/components/form-field/search-input/search-input.stories.tsx
+++ b/src/components/form-field/search-input/search-input.stories.tsx
@@ -1,81 +1,109 @@
/* eslint-disable react/jsx-props-no-spreading */
-import type { Meta, StoryObj } from "@storybook/react";
-import React, { useState } from "react";
-import { FormField } from "../form-field";
+import type { Meta, StoryObj } from '@storybook/react-vite'
+import { useEffect, useState } from 'react'
+import { FormField } from '../form-field'
const meta: Meta = {
- title: "Input/SearchInput",
- component: FormField.SearchInput,
-};
+ title: 'Input/SearchInput',
+ component: FormField.SearchInput,
+ args: {
+ error: false,
+ disabled: false,
+ readOnly: false,
+ value: '',
+ label: 'Label',
+ description: 'Description',
+ },
+ argTypes: {
+ error: { control: 'boolean' },
+ disabled: { control: 'boolean' },
+ readOnly: { control: 'boolean' },
+ value: { control: 'text' },
+ label: { control: 'text' },
+ description: { control: 'text' },
+ },
+}
-export default meta;
+export default meta
-type Story = StoryObj;
+type Story = StoryObj
const SearchInputWithHooks = ({
- error = false,
- disabled = false,
- readOnly = false,
- value,
+ error = false,
+ disabled = false,
+ readOnly = false,
+ value,
+ label = 'Label',
+ description = 'Description',
}: {
- error?: boolean;
- disabled?: boolean;
- readOnly?: boolean;
- value?: string;
+ error?: boolean
+ disabled?: boolean
+ readOnly?: boolean
+ value?: string
+ label?: string
+ description?: string
}) => {
- const [inputValue, setInputValue] = useState(value);
+ const [inputValue, setInputValue] = useState(value)
- return (
-
-
- Label
- Description
-
- setInputValue(e.target.value)}
- ariaDescribedBy="value-description"
- error={error}
- disabled={disabled}
- readOnly={readOnly}
- onClear={() => {
- setInputValue("");
- }}
- />
- {error ? Error message. : null}
-
- );
-};
+ useEffect(() => {
+ setInputValue(value)
+ }, [value])
+
+ return (
+
+
+ {label}
+
+ {description}
+
+
+ setInputValue(e.target.value)}
+ ariaDescribedBy='value-description'
+ error={error}
+ disabled={disabled}
+ readOnly={readOnly}
+ onClear={() => {
+ setInputValue('')
+ }}
+ />
+ {error ?
+ Error message.
+ : null}
+
+ )
+}
export const Default: Story = {
- render: () => (
-
-
-
- ),
-};
+ render: (args) => (
+
+
+
+ ),
+}
export const WithError: Story = {
- render: () => (
-
-
-
- ),
-};
+ render: () => (
+
+
+
+ ),
+}
export const ReadOnly: Story = {
- render: () => (
-
-
-
- ),
-};
+ render: () => (
+
+
+
+ ),
+}
export const Disabled: Story = {
- render: () => (
-
-
-
- ),
-};
+ render: () => (
+
+
+
+ ),
+}
diff --git a/src/components/form-field/search-input/search-input.test.tsx b/src/components/form-field/search-input/search-input.test.tsx
new file mode 100644
index 00000000..6b243dfc
--- /dev/null
+++ b/src/components/form-field/search-input/search-input.test.tsx
@@ -0,0 +1,19 @@
+import { describe, expect, it, vi } from 'vitest'
+import { fireEvent, render, screen } from '@testing-library/react'
+import { SearchInput } from './search-input'
+
+describe('SearchInput', () => {
+ it('renders a clear button when value is set', () => {
+ const onClear = vi.fn()
+
+ render( {}} />)
+
+ const clearButton = screen.getByRole('button', {
+ name: 'Clear search input',
+ })
+ expect(clearButton).toBeInTheDocument()
+
+ fireEvent.click(clearButton)
+ expect(onClear).toHaveBeenCalledTimes(1)
+ })
+})
diff --git a/src/components/form-field/search-input/search-input.tsx b/src/components/form-field/search-input/search-input.tsx
index 305e2c0a..b93a3a49 100644
--- a/src/components/form-field/search-input/search-input.tsx
+++ b/src/components/form-field/search-input/search-input.tsx
@@ -1,93 +1,95 @@
-import React, { useCallback, useRef } from "react";
-import { classNames } from "../../../util/class-names";
-import CrossIcon from "../../../icons/cross-icon";
-import SearchIcon from "../../../icons/search-icon";
+import type { ComponentPropsWithoutRef, KeyboardEvent } from 'react'
+import { useCallback, useRef } from 'react'
+import { classNames } from '../../../util/class-names'
+import CrossIcon from '../../../icons/cross-icon'
+import SearchIcon from '../../../icons/search-icon'
-export interface SearchInputProps extends React.ComponentPropsWithoutRef<"input"> {
- autoSelect?: boolean;
- ariaDescribedBy?: string;
- error?: boolean;
- onClear: () => void;
+export interface SearchInputProps extends ComponentPropsWithoutRef<'input'> {
+ autoSelect?: boolean
+ ariaDescribedBy?: string
+ error?: boolean
+ onClear: () => void
}
export const SearchInput = ({
- ariaDescribedBy,
- readOnly,
- autoSelect,
- onClear,
- error,
- value,
- disabled,
- className,
- ...props
+ ariaDescribedBy,
+ readOnly,
+ autoSelect,
+ onClear,
+ error,
+ value,
+ disabled,
+ className,
+ ...props
}: SearchInputProps) => {
- const inputRef = useRef(null);
- const isClearIconShown = !readOnly && !disabled && value !== undefined && value !== "";
+ const inputRef = useRef(null)
+ const isClearIconShown =
+ !readOnly && !disabled && value !== undefined && value !== ''
- const handleAutoSelection = useCallback(() => {
- if (autoSelect && inputRef.current) {
- inputRef.current.select();
- }
- }, [autoSelect]);
+ const handleAutoSelection = useCallback(() => {
+ if (autoSelect && inputRef.current) {
+ inputRef.current.select()
+ }
+ }, [autoSelect])
- const handleKeyDown = useCallback(
- (event: React.KeyboardEvent) => {
- if (event.key === "Enter" || event.key === " ") {
- event.preventDefault();
- event.stopPropagation();
+ const handleKeyDown = useCallback(
+ (event: KeyboardEvent) => {
+ if (event.key === 'Enter' || event.key === ' ') {
+ event.preventDefault()
+ event.stopPropagation()
- onClear();
- }
- },
- [onClear]
- );
+ onClear()
+ }
+ },
+ [onClear],
+ )
- return (
-
-
-
-
+ return (
+
+
+
+
-
+
- {isClearIconShown ? (
-
-
-
-
-
- ) : null}
+ {isClearIconShown ?
+
+
+
+
- );
-};
+ : null}
+
+ )
+}
diff --git a/src/components/form-field/single-combobox/single-combobox-custom-option.tsx b/src/components/form-field/single-combobox/single-combobox-custom-option.tsx
index acdb27ad..61d95092 100644
--- a/src/components/form-field/single-combobox/single-combobox-custom-option.tsx
+++ b/src/components/form-field/single-combobox/single-combobox-custom-option.tsx
@@ -1,21 +1,21 @@
-import { ComboboxOption as HeadlessUiComboboxOption } from "@headlessui/react";
-import React from "react";
+import type { ReactNode } from 'react'
+import { ComboboxOption as HeadlessUiComboboxOption } from '@headlessui/react'
export interface SingleComboboxCustomOptionProps
{
- value: TValue;
- children: React.ReactNode;
+ value: TValue
+ children: ReactNode
}
export const SingleComboboxCustomOption = ({
- value,
- children,
+ value,
+ children,
}: SingleComboboxCustomOptionProps) => {
- return (
-
- {children}
-
- );
-};
+ return (
+
+ {children}
+
+ )
+}
diff --git a/src/components/form-field/single-combobox/single-combobox-empty-option.tsx b/src/components/form-field/single-combobox/single-combobox-empty-option.tsx
index 334fd3f0..dac91bc0 100644
--- a/src/components/form-field/single-combobox/single-combobox-empty-option.tsx
+++ b/src/components/form-field/single-combobox/single-combobox-empty-option.tsx
@@ -1,9 +1,11 @@
-import React from "react";
+import type { ReactNode } from 'react'
export interface SingleComboboxEmptyOptionProps {
- children: React.ReactNode;
+ children: ReactNode
}
-export const SingleComboboxEmptyOption = ({ children }: SingleComboboxEmptyOptionProps) => {
- return {children}
;
-};
+export const SingleComboboxEmptyOption = ({
+ children,
+}: SingleComboboxEmptyOptionProps) => {
+ return {children}
+}
diff --git a/src/components/form-field/single-combobox/single-combobox-input.tsx b/src/components/form-field/single-combobox/single-combobox-input.tsx
index 7d6849e4..406e626d 100644
--- a/src/components/form-field/single-combobox/single-combobox-input.tsx
+++ b/src/components/form-field/single-combobox/single-combobox-input.tsx
@@ -1,46 +1,46 @@
+import type { ChangeEvent } from 'react'
import {
- ComboboxButton as HeadlessUiComboboxButton,
- ComboboxInput as HeadlessUiComboboxInput,
-} from "@headlessui/react";
-import React from "react";
-import { CaretDownIcon } from "../../../icons";
+ ComboboxButton as HeadlessUiComboboxButton,
+ ComboboxInput as HeadlessUiComboboxInput,
+} from '@headlessui/react'
+import { CaretDownIcon } from '../../../icons'
export interface SingleComboboxInputProps {
- id: string;
- displayValue?(item: TValue): string;
- placeholder: string;
- onChange: (event: React.ChangeEvent) => void;
- showButton?: boolean;
+ id: string
+ displayValue?(item: TValue): string
+ placeholder: string
+ onChange: (event: ChangeEvent) => void
+ showButton?: boolean
}
export const SingleComboboxInput = ({
- id,
- displayValue,
- placeholder,
- onChange,
- showButton = true,
+ id,
+ displayValue,
+ placeholder,
+ onChange,
+ showButton = true,
}: SingleComboboxInputProps) => {
- return (
-
-
+
+ {showButton ?
+
+
+
- {showButton ? (
-
-
-
-
-
- ) : null}
-
- );
-};
+
+
+ : null}
+
+ )
+}
diff --git a/src/components/form-field/single-combobox/single-combobox-options.tsx b/src/components/form-field/single-combobox/single-combobox-options.tsx
index 24413e9c..79c4b084 100644
--- a/src/components/form-field/single-combobox/single-combobox-options.tsx
+++ b/src/components/form-field/single-combobox/single-combobox-options.tsx
@@ -1,17 +1,19 @@
-import { ComboboxOptions as HeadlessUiComboboxOptions } from "@headlessui/react";
-import React from "react";
+import type { ReactNode } from 'react'
+import { ComboboxOptions as HeadlessUiComboboxOptions } from '@headlessui/react'
export interface SingleComboboxOptionsProps {
- children: React.ReactNode;
+ children: ReactNode
}
-export const SingleComboboxOptions = ({ children }: SingleComboboxOptionsProps) => {
- return (
-
- {children}
-
- );
-};
+export const SingleComboboxOptions = ({
+ children,
+}: SingleComboboxOptionsProps) => {
+ return (
+
+ {children}
+
+ )
+}
diff --git a/src/components/form-field/single-combobox/single-combobox-result-input.tsx b/src/components/form-field/single-combobox/single-combobox-result-input.tsx
index a57a1d28..59f681d1 100644
--- a/src/components/form-field/single-combobox/single-combobox-result-input.tsx
+++ b/src/components/form-field/single-combobox/single-combobox-result-input.tsx
@@ -1,27 +1,30 @@
-import { ComboboxButton as HeadlessUiComboboxButton } from "@headlessui/react";
-import React from "react";
-import { CaretDownIcon } from "../../../icons";
-import { Tag } from "../../tag/tag";
+import type { ReactNode } from 'react'
+import { ComboboxButton as HeadlessUiComboboxButton } from '@headlessui/react'
+import { CaretDownIcon } from '../../../icons'
+import { Tag } from '../../tag/tag'
export interface SingleComboboxResultInputProps {
- onUnselect: () => void;
- children: React.ReactNode;
+ onUnselect: () => void
+ children: ReactNode
}
export const SingleComboboxResultInput = ({
- onUnselect,
- children,
+ onUnselect,
+ children,
}: SingleComboboxResultInputProps) => {
- return (
-
- );
-};
+ return (
+
+ )
+}
diff --git a/src/components/form-field/single-combobox/single-combobox.option.tsx b/src/components/form-field/single-combobox/single-combobox.option.tsx
index 1dd63af2..664d873d 100644
--- a/src/components/form-field/single-combobox/single-combobox.option.tsx
+++ b/src/components/form-field/single-combobox/single-combobox.option.tsx
@@ -1,19 +1,19 @@
-import { ComboboxOption as HeadlessUiComboboxOption } from "@headlessui/react";
-import React from "react";
+import type { ReactNode } from 'react'
+import { ComboboxOption as HeadlessUiComboboxOption } from '@headlessui/react'
export interface SingleComboboxOptionProps {
- value: TValue;
- children: React.ReactNode;
+ value: TValue
+ children: ReactNode
}
export const SingleComboboxOption = ({
- value,
- children,
+ value,
+ children,
}: SingleComboboxOptionProps) => (
-
- {children}
-
-);
+
+ {children}
+
+)
diff --git a/src/components/form-field/single-combobox/single-combobox.stories.tsx b/src/components/form-field/single-combobox/single-combobox.stories.tsx
index 8b765130..14779f38 100644
--- a/src/components/form-field/single-combobox/single-combobox.stories.tsx
+++ b/src/components/form-field/single-combobox/single-combobox.stories.tsx
@@ -1,179 +1,199 @@
-import type { Meta, StoryObj } from "@storybook/react";
+import { useState } from 'react'
+import type { Meta, StoryObj } from '@storybook/react-vite'
-import React from "react";
-import { Tag } from "../../tag/tag";
-import { FormField } from "../form-field";
-import { SingleCombobox } from "./single-combobox";
+import { Tag } from '../../tag/tag'
+import { FormField } from '../form-field'
+import { SingleCombobox } from './single-combobox'
-const meta: Meta = {
- title: "Input/SingleCombobox",
- component: SingleCombobox,
-};
+type SingleComboboxStoryArgs = {
+ placeholder: string
+ width: number
+}
+
+const meta: Meta = {
+ title: 'Input/SingleCombobox',
+ args: {
+ placeholder: 'Select person...',
+ width: 288,
+ },
+ argTypes: {
+ placeholder: { control: 'text' },
+ width: { control: { type: 'range', min: 200, max: 360, step: 16 } },
+ },
+}
-export default meta;
-type Story = StoryObj;
+export default meta
+type Story = StoryObj
const people = [
- "Durward Reynolds",
- "Kenton Towne",
- "Therese Wunsch",
- "Benedict Kessler",
- "Katelyn Rohan",
-];
-
-const SingleComboboxWithHooks = () => {
- const [selectedPerson, setSelectedPerson] = React.useState("");
- const [query, setQuery] = React.useState("");
-
- const filteredPeople =
- query === ""
- ? people
- : people.filter((person) => {
- return person.toLowerCase().includes(query.toLowerCase());
- });
-
- return (
- {
- setQuery("");
- setSelectedPerson(value);
- }}
- >
- setQuery(event.target.value)}
- />
-
- {filteredPeople.length === 0 ? (
-
-
- No persons found for {query}
-
-
- ) : null}
- {filteredPeople.map((person) => (
-
- {person}
-
- ))}
-
-
- );
-};
+ 'Durward Reynolds',
+ 'Kenton Towne',
+ 'Therese Wunsch',
+ 'Benedict Kessler',
+ 'Katelyn Rohan',
+]
+
+const SingleComboboxWithHooks = ({ placeholder }: { placeholder: string }) => {
+ const [selectedPerson, setSelectedPerson] = useState(null)
+ const [query, setQuery] = useState('')
+
+ const filteredPeople =
+ query === '' ? people : (
+ people.filter((person) => {
+ return person.toLowerCase().includes(query.toLowerCase())
+ })
+ )
+
+ return (
+ {
+ setQuery('')
+ setSelectedPerson(value)
+ }}
+ >
+ setQuery(event.target.value)}
+ />
+
+ {filteredPeople.length === 0 ?
+
+
+ No persons found for {query}
+
+
+ : null}
+ {filteredPeople.map((person) => (
+
+ {person}
+
+ ))}
+
+
+ )
+}
interface Person {
- id: number;
- name: string;
- short: string;
+ id: number
+ name: string
+ short: string
}
const peopleObjects: Person[] = [
- { id: 1, name: "Durward Reynolds", short: "Durward" },
- { id: 2, name: "Kenton Towne", short: "Kenton" },
- { id: 3, name: "Therese Wunsch", short: "Therese" },
- { id: 4, name: "Benedict Kessler", short: "Benedict" },
- { id: 5, name: "Katelyn Rohan", short: "Katelyn" },
-];
-
-const SingleComboboxObjectValueWithHooks = () => {
- const [selectedPerson, setSelectedPerson] = React.useState();
- const [query, setQuery] = React.useState("");
-
- const filteredPeople =
- query === ""
- ? peopleObjects
- : peopleObjects.filter((person) => {
- return person.name.toLowerCase().includes(query.toLowerCase());
- });
-
- return (
- {
- setQuery("");
- setSelectedPerson(value);
- }}
- >
- person.name}
- onChange={(event) => setQuery(event.target.value)}
- />
-
- {filteredPeople.map((person) => (
-
- {person.short}
-
- ))}
-
-
- );
-};
-
-const SingleComboboxCustomValueWithHooks = () => {
- const [selectedPerson, setSelectedPerson] = React.useState("");
- const [query, setQuery] = React.useState("");
-
- const filteredPeople =
- query === ""
- ? people
- : people.filter((person) => {
- return person.toLowerCase().includes(query.toLowerCase());
- });
-
- return (
- {
- setQuery("");
- setSelectedPerson(value);
- }}
- >
- setQuery(event.target.value)}
- />
-
- {query.length > 0 && people.indexOf(query) === -1 && (
-
- Create tag: {query}
-
- )}
-
- {filteredPeople.map((person) => (
-
- {person}
-
- ))}
-
-
- );
-};
+ { id: 1, name: 'Durward Reynolds', short: 'Durward' },
+ { id: 2, name: 'Kenton Towne', short: 'Kenton' },
+ { id: 3, name: 'Therese Wunsch', short: 'Therese' },
+ { id: 4, name: 'Benedict Kessler', short: 'Benedict' },
+ { id: 5, name: 'Katelyn Rohan', short: 'Katelyn' },
+]
+
+const SingleComboboxObjectValueWithHooks = ({
+ placeholder,
+}: {
+ placeholder: string
+}) => {
+ const [selectedPerson, setSelectedPerson] = useState(null)
+ const [query, setQuery] = useState('')
+
+ const filteredPeople =
+ query === '' ? peopleObjects : (
+ peopleObjects.filter((person) => {
+ return person.name.toLowerCase().includes(query.toLowerCase())
+ })
+ )
+
+ return (
+ {
+ setQuery('')
+ setSelectedPerson(value)
+ }}
+ >
+ person.name}
+ onChange={(event) => setQuery(event.target.value)}
+ />
+
+ {filteredPeople.map((person) => (
+
+ {person.short}
+
+ ))}
+
+
+ )
+}
+
+const SingleComboboxCustomValueWithHooks = ({
+ placeholder,
+}: {
+ placeholder: string
+}) => {
+ const [selectedPerson, setSelectedPerson] = useState(null)
+ const [query, setQuery] = useState('')
+
+ const filteredPeople =
+ query === '' ? people : (
+ people.filter((person) => {
+ return person.toLowerCase().includes(query.toLowerCase())
+ })
+ )
+
+ return (
+ {
+ setQuery('')
+ setSelectedPerson(value)
+ }}
+ >
+ setQuery(event.target.value)}
+ />
+
+ {query.length > 0 && people.indexOf(query) === -1 && (
+
+ Create tag: {query}
+
+ )}
+
+ {filteredPeople.map((person) => (
+
+ {person}
+
+ ))}
+
+
+ )
+}
export const Default: Story = {
- render: () => (
-
-
-
- ),
-};
+ render: ({ placeholder, width }) => (
+
+
+
+ ),
+}
export const ObjectValue: Story = {
- render: () => (
-
-
-
- ),
-};
+ render: ({ placeholder, width }) => (
+
+
+
+ ),
+}
export const CustomValue: Story = {
- render: () => (
-
-
-
- ),
-};
+ render: ({ placeholder, width }) => (
+
+
+
+ ),
+}
diff --git a/src/components/form-field/single-combobox/single-combobox.test.tsx b/src/components/form-field/single-combobox/single-combobox.test.tsx
new file mode 100644
index 00000000..f55258da
--- /dev/null
+++ b/src/components/form-field/single-combobox/single-combobox.test.tsx
@@ -0,0 +1,20 @@
+import { describe, expect, it, vi } from 'vitest'
+import { render, screen } from '@testing-library/react'
+import { FormField } from '../form-field'
+import { SingleCombobox } from './single-combobox'
+
+describe('SingleCombobox', () => {
+ it('renders the input placeholder', () => {
+ render(
+
+ {}}
+ />
+ ,
+ )
+
+ expect(screen.getByPlaceholderText('Select person...')).toBeInTheDocument()
+ })
+})
diff --git a/src/components/form-field/single-combobox/single-combobox.tsx b/src/components/form-field/single-combobox/single-combobox.tsx
index e2e99820..f9db2c8b 100644
--- a/src/components/form-field/single-combobox/single-combobox.tsx
+++ b/src/components/form-field/single-combobox/single-combobox.tsx
@@ -1,37 +1,37 @@
-import { Combobox as HeadlessUiCombobox } from "@headlessui/react";
-import React from "react";
-import { SingleComboboxCustomOption } from "./single-combobox-custom-option";
-import { SingleComboboxEmptyOption } from "./single-combobox-empty-option";
-import { SingleComboboxInput } from "./single-combobox-input";
-import { SingleComboboxOptions } from "./single-combobox-options";
-import { SingleComboboxResultInput } from "./single-combobox-result-input";
-import { SingleComboboxOption } from "./single-combobox.option";
+import type { ReactNode } from 'react'
+import { Combobox as HeadlessUiCombobox } from '@headlessui/react'
+import { SingleComboboxCustomOption } from './single-combobox-custom-option'
+import { SingleComboboxEmptyOption } from './single-combobox-empty-option'
+import { SingleComboboxInput } from './single-combobox-input'
+import { SingleComboboxOptions } from './single-combobox-options'
+import { SingleComboboxResultInput } from './single-combobox-result-input'
+import { SingleComboboxOption } from './single-combobox.option'
export interface SingleComboboxProps {
- value?: TValue;
- onChange: (value: TValue) => void;
- children: React.ReactNode;
- disabled?: boolean;
+ value?: TValue | null
+ onChange: (value: TValue | null) => void
+ children: ReactNode
+ disabled?: boolean
}
const SingleCombobox = ({
- value,
- onChange,
- children,
- disabled,
+ value,
+ onChange,
+ children,
+ disabled,
}: SingleComboboxProps) => {
- return (
-
- {children}
-
- );
-};
+ return (
+
+ {children}
+
+ )
+}
-SingleCombobox.Input = SingleComboboxInput;
-SingleCombobox.ResultInput = SingleComboboxResultInput;
-SingleCombobox.Options = SingleComboboxOptions;
-SingleCombobox.Option = SingleComboboxOption;
-SingleCombobox.EmptyOption = SingleComboboxEmptyOption;
-SingleCombobox.CustomOption = SingleComboboxCustomOption;
+SingleCombobox.Input = SingleComboboxInput
+SingleCombobox.ResultInput = SingleComboboxResultInput
+SingleCombobox.Options = SingleComboboxOptions
+SingleCombobox.Option = SingleComboboxOption
+SingleCombobox.EmptyOption = SingleComboboxEmptyOption
+SingleCombobox.CustomOption = SingleComboboxCustomOption
-export { SingleCombobox };
+export { SingleCombobox }
diff --git a/src/components/form-field/text-input/text-input.stories.tsx b/src/components/form-field/text-input/text-input.stories.tsx
index 4e48e45b..1ab22abf 100644
--- a/src/components/form-field/text-input/text-input.stories.tsx
+++ b/src/components/form-field/text-input/text-input.stories.tsx
@@ -1,105 +1,125 @@
/* eslint-disable react/jsx-props-no-spreading */
-import type { Meta, StoryObj } from "@storybook/react";
-import React, { useState } from "react";
-import { FormField } from "../form-field";
-import { SearchIcon } from "../../../icons";
+import type { Meta, StoryObj } from '@storybook/react-vite'
+import { useState } from 'react'
+import { FormField } from '../form-field'
+import { SearchIcon } from '../../../icons'
const meta: Meta = {
- title: "Input/TextInput",
- component: FormField.TextInput,
-};
+ title: 'Input/TextInput',
+ component: FormField.TextInput,
+ args: {
+ error: false,
+ disabled: false,
+ hasLeftIcon: false,
+ readOnly: false,
+ value: '',
+ optional: false,
+ },
+ argTypes: {
+ error: { control: 'boolean' },
+ disabled: { control: 'boolean' },
+ hasLeftIcon: { control: 'boolean' },
+ readOnly: { control: 'boolean' },
+ value: { control: 'text' },
+ optional: { control: 'boolean' },
+ },
+}
-export default meta;
+export default meta
-type Story = StoryObj;
+type Story = StoryObj
const TextInputWithHooks = ({
- error = false,
- disabled = false,
- hasLeftIcon = false,
- readOnly = false,
- value,
- optional,
+ error = false,
+ disabled = false,
+ hasLeftIcon = false,
+ readOnly = false,
+ value,
+ optional,
}: {
- error?: boolean;
- disabled?: boolean;
- hasLeftIcon?: boolean;
- readOnly?: boolean;
- value?: string;
- optional?: boolean;
+ error?: boolean
+ disabled?: boolean
+ hasLeftIcon?: boolean
+ readOnly?: boolean
+ value?: string
+ optional?: boolean
}) => {
- const [inputValue, setInputValue] = useState(value);
+ const [inputValue, setInputValue] = useState(value)
- return (
-
-
-
- Label
-
+ return (
+
+
+
+ Label
+
- Description
-
+
+ Description
+
+
- setInputValue(e.target.value)}
- ariaDescribedBy="value-description"
- error={error}
- disabled={disabled}
- LeftIcon={hasLeftIcon ? SearchIcon : undefined}
- readOnly={readOnly}
- />
+ setInputValue(e.target.value)}
+ ariaDescribedBy='value-description'
+ error={error}
+ disabled={disabled}
+ LeftIcon={hasLeftIcon ? SearchIcon : undefined}
+ readOnly={readOnly}
+ />
- {error ? Error message. : null}
-
- );
-};
+ {error ?
+ Error message.
+ : null}
+
+ )
+}
export const Default: Story = {
- render: () => (
-
-
-
- ),
-};
+ render: (args) => (
+
+
+
+ ),
+}
export const WithError: Story = {
- render: () => (
-
-
-
- ),
-};
+ render: () => (
+
+
+
+ ),
+}
export const WithLeftIcon: Story = {
- render: () => (
-
-
-
- ),
-};
+ render: () => (
+
+
+
+ ),
+}
export const ReadOnly: Story = {
- render: () => (
-
-
-
- ),
-};
+ render: () => (
+
+
+
+ ),
+}
export const Disabled: Story = {
- render: () => (
-
-
-
- ),
-};
+ render: () => (
+
+
+
+ ),
+}
export const Optional: Story = {
- render: () => (
-
-
-
- ),
-};
+ render: () => (
+
+
+
+ ),
+}
diff --git a/src/components/form-field/text-input/text-input.test.tsx b/src/components/form-field/text-input/text-input.test.tsx
new file mode 100644
index 00000000..353b295e
--- /dev/null
+++ b/src/components/form-field/text-input/text-input.test.tsx
@@ -0,0 +1,42 @@
+import { describe, expect, it, vi } from 'vitest'
+import { fireEvent, render, screen } from '@testing-library/react'
+import { TextInput } from './text-input'
+
+describe('TextInput', () => {
+ it('renders a text input and responds to changes', () => {
+ const handleChange = vi.fn()
+
+ render(
+ ,
+ )
+
+ const input = screen.getByPlaceholderText('Enter name')
+ expect(input).toBeInTheDocument()
+
+ fireEvent.change(input, { target: { value: 'Alice' } })
+ expect(handleChange).toHaveBeenCalled()
+ })
+
+ it('honors disabled and error states', () => {
+ render(
+ {}} disabled />,
+ )
+
+ const input = screen.getByDisplayValue('Disabled')
+ expect(input).toBeDisabled()
+ expect(input).toHaveClass('cursor-not-allowed')
+ })
+
+ it('shows error styling when enabled', () => {
+ render( {}} error />)
+
+ const input = screen.getByDisplayValue('Invalid')
+ expect(input).toHaveClass('border-danger-500')
+ })
+})
diff --git a/src/components/form-field/text-input/text-input.tsx b/src/components/form-field/text-input/text-input.tsx
index 05f1e0b2..b0a38f6d 100644
--- a/src/components/form-field/text-input/text-input.tsx
+++ b/src/components/form-field/text-input/text-input.tsx
@@ -1,81 +1,82 @@
-import React, { useRef } from "react";
-import { classNames } from "../../../util/class-names";
+import type { ComponentPropsWithoutRef, ElementType } from 'react'
+import { useRef } from 'react'
+import { classNames } from '../../../util/class-names'
-const targetAttachmentIdentifier = "target-field";
+const targetAttachmentIdentifier = 'target-field'
// note: these strings need to be static,
// template literals aren’t recognized by tailwind,
// so we can’t use ${targetAttachmentIdentifier} here
const formFieldGroupStyles = classNames(
- // first element
- `[.group.form-field-group_&:first-child_.target-field]:rounded-r-none [.group.form-field-group_&:first-child_.target-field]:border-r-0`,
- // elements in between
- `[.group.form-field-group_&:not(:first-child):not(:last-child)_.target-field]:rounded-none [.group.form-field-group_&:not(:first-child):not(:last-child)_.target-field]:border-r-0`,
- // last element
- `[.group.form-field-group_&:last-child_.target-field]:border-l [.group.form-field-group_&:last-child_.target-field]:rounded-l-none`
-);
+ // first element
+ `[.group.form-field-group_&:first-child_.target-field]:rounded-r-none [.group.form-field-group_&:first-child_.target-field]:border-r-0`,
+ // elements in between
+ `[.group.form-field-group_&:not(:first-child):not(:last-child)_.target-field]:rounded-none [.group.form-field-group_&:not(:first-child):not(:last-child)_.target-field]:border-r-0`,
+ // last element
+ `[.group.form-field-group_&:last-child_.target-field]:border-l [.group.form-field-group_&:last-child_.target-field]:rounded-l-none`,
+)
-export interface TextInputProps extends React.ComponentPropsWithoutRef<"input"> {
- type?: "text" | "password" | "email" | "date" | "datetime-local";
- autoSelect?: boolean;
- ariaDescribedBy?: string;
- LeftIcon?: React.ElementType;
- error?: boolean;
+export interface TextInputProps extends ComponentPropsWithoutRef<'input'> {
+ type?: 'text' | 'password' | 'email' | 'date' | 'datetime-local'
+ autoSelect?: boolean
+ ariaDescribedBy?: string
+ LeftIcon?: ElementType
+ error?: boolean
}
export const TextInput = ({
- ariaDescribedBy,
- type = "text",
- LeftIcon,
- readOnly,
- autoSelect,
- error,
- disabled,
- className,
- ...props
+ ariaDescribedBy,
+ type = 'text',
+ LeftIcon,
+ readOnly,
+ autoSelect,
+ error,
+ disabled,
+ className,
+ ...props
}: TextInputProps) => {
- const inputRef = useRef(null);
+ const inputRef = useRef(null)
- const handleAutoSelection = () => {
- if (autoSelect && inputRef.current) {
- inputRef.current.select();
- }
- };
+ const handleAutoSelection = () => {
+ if (autoSelect && inputRef.current) {
+ inputRef.current.select()
+ }
+ }
- return (
-
- {LeftIcon ? (
-
-
-
- ) : null}
-
-
+ return (
+
+ {LeftIcon ?
+
+
- );
-};
+ : null}
+
+
+
+ )
+}
diff --git a/src/components/form-field/textarea/textarea.stories.tsx b/src/components/form-field/textarea/textarea.stories.tsx
index b8a17070..600deac3 100644
--- a/src/components/form-field/textarea/textarea.stories.tsx
+++ b/src/components/form-field/textarea/textarea.stories.tsx
@@ -1,66 +1,98 @@
/* eslint-disable react/jsx-props-no-spreading */
-import type { Meta, StoryObj } from "@storybook/react";
-import React, { useState } from "react";
-import { FormField } from "../form-field";
+import type { Meta, StoryObj } from '@storybook/react-vite'
+import { useEffect, useState } from 'react'
+import { FormField } from '../form-field'
const meta: Meta
= {
- title: "Input/Textarea",
- component: FormField.Textarea,
-};
+ title: 'Input/Textarea',
+ component: FormField.Textarea,
+ args: {
+ error: false,
+ disabled: false,
+ label: 'Label',
+ description: 'Description',
+ placeholder: 'Placeholder',
+ value: '',
+ },
+ argTypes: {
+ error: { control: 'boolean' },
+ disabled: { control: 'boolean' },
+ label: { control: 'text' },
+ description: { control: 'text' },
+ placeholder: { control: 'text' },
+ value: { control: 'text' },
+ },
+}
-export default meta;
+export default meta
-type Story = StoryObj;
+type Story = StoryObj
const TextareaWithHooks = ({
- error = false,
- disabled = false,
+ error = false,
+ disabled = false,
+ label = 'Label',
+ description = 'Description',
+ placeholder = 'Placeholder',
+ value = '',
}: {
- error?: boolean;
- disabled?: boolean;
+ error?: boolean
+ disabled?: boolean
+ label?: string
+ description?: string
+ placeholder?: string
+ value?: string
}) => {
- const [value, setValue] = useState("");
+ const [inputValue, setInputValue] = useState(value)
- return (
-
-
- Label
- Description
-
- setValue(e.target.value)}
- ariaDescribedBy="value-description"
- error={error}
- disabled={disabled}
- />
- {error ? Error message. : null}
-
- );
-};
+ useEffect(() => {
+ setInputValue(value)
+ }, [value])
+
+ return (
+
+
+ {label}
+
+ {description}
+
+
+ setInputValue(e.target.value)}
+ ariaDescribedBy='value-description'
+ error={error}
+ disabled={disabled}
+ />
+ {error ?
+ Error message.
+ : null}
+
+ )
+}
export const Default: Story = {
- render: () => (
-
-
-
- ),
-};
+ render: (args) => (
+
+
+
+ ),
+}
export const WithError: Story = {
- render: () => (
-
-
-
- ),
-};
+ render: () => (
+
+
+
+ ),
+}
export const Disabled: Story = {
- render: () => (
-
-
-
- ),
-};
+ render: () => (
+
+
+
+ ),
+}
diff --git a/src/components/form-field/textarea/textarea.tsx b/src/components/form-field/textarea/textarea.tsx
index 55253cb0..02e37963 100644
--- a/src/components/form-field/textarea/textarea.tsx
+++ b/src/components/form-field/textarea/textarea.tsx
@@ -1,51 +1,51 @@
-import React from "react";
-import { classNames } from "../../../util/class-names";
+import type { ChangeEvent } from 'react'
+import { classNames } from '../../../util/class-names'
export interface TextareaProps {
- id: string;
- placeholder: string;
- value: string;
- onChange: (event: React.ChangeEvent) => void;
- ariaDescribedBy?: string;
- error?: boolean;
- disabled?: boolean;
- rows?: number;
- cols?: number;
- className?: string;
+ id: string
+ placeholder: string
+ value: string
+ onChange: (event: ChangeEvent) => void
+ ariaDescribedBy?: string
+ error?: boolean
+ disabled?: boolean
+ rows?: number
+ cols?: number
+ className?: string
}
export const Textarea = ({
- id,
- value,
- onChange,
- placeholder,
- ariaDescribedBy,
- error,
- disabled,
- rows,
- cols,
- className,
+ id,
+ value,
+ onChange,
+ placeholder,
+ ariaDescribedBy,
+ error,
+ disabled,
+ rows,
+ cols,
+ className,
}: TextareaProps) => {
- return (
-
- );
-};
+ return (
+
+ )
+}
diff --git a/src/components/icon-button/icon-button.stories.tsx b/src/components/icon-button/icon-button.stories.tsx
index 4b01e180..3b8f0245 100644
--- a/src/components/icon-button/icon-button.stories.tsx
+++ b/src/components/icon-button/icon-button.stories.tsx
@@ -1,30 +1,30 @@
-import type { Meta, StoryObj } from "@storybook/react";
+import type { Meta, StoryObj } from '@storybook/react-vite'
-import { IconButton, IconButtonProps } from "./icon-button";
-import { WalkIcon } from "../../icons";
+import { IconButton, IconButtonProps } from './icon-button'
+import { WalkIcon } from '../../icons'
-const VariantTypes: IconButtonProps["variant"][] = [
- "primary",
- "secondary",
- "minimal",
- "danger",
- "danger-secondary",
-];
+const VariantTypes: IconButtonProps['variant'][] = [
+ 'primary',
+ 'secondary',
+ 'minimal',
+ 'danger',
+ 'danger-secondary',
+]
const meta: Meta = {
- title: "IconButton",
- component: IconButton,
- args: {
- variant: "primary",
- disabled: false,
- Icon: WalkIcon,
- },
- argTypes: {
- variant: { options: VariantTypes },
- },
-};
+ title: 'IconButton',
+ component: IconButton,
+ args: {
+ variant: 'primary',
+ disabled: false,
+ Icon: WalkIcon,
+ },
+ argTypes: {
+ variant: { options: VariantTypes },
+ },
+}
-export default meta;
-type Story = StoryObj;
+export default meta
+type Story = StoryObj
-export const Default: Story = {};
+export const Default: Story = {}
diff --git a/src/components/icon-button/icon-button.test.tsx b/src/components/icon-button/icon-button.test.tsx
new file mode 100644
index 00000000..4e8ee58b
--- /dev/null
+++ b/src/components/icon-button/icon-button.test.tsx
@@ -0,0 +1,14 @@
+import { describe, expect, it } from 'vitest'
+import { render, screen } from '@testing-library/react'
+import { AddIcon } from '../../icons'
+import { IconButton } from './icon-button'
+
+describe('IconButton', () => {
+ it('renders a button with an icon', () => {
+ render( )
+
+ const button = screen.getByRole('button', { name: 'Add item' })
+ expect(button).toBeInTheDocument()
+ expect(button.querySelector('svg')).toBeInTheDocument()
+ })
+})
diff --git a/src/components/icon-button/icon-button.tsx b/src/components/icon-button/icon-button.tsx
index 29e74151..079d98ff 100644
--- a/src/components/icon-button/icon-button.tsx
+++ b/src/components/icon-button/icon-button.tsx
@@ -1,38 +1,44 @@
-import React from "react";
-import { classNames } from "../../util/class-names";
+import type { ButtonHTMLAttributes, ElementType } from 'react'
+import { classNames } from '../../util/class-names'
const iconButtonVariants = {
- primary:
- "bg-primary-500 text-neutral-0 hover:bg-primary-600 active:bg-primary-600 focus:ring-2 focus:ring-primary-200 focus:bg-primary-600 disabled:bg-primary-200 fill-neutral-0",
- secondary:
- "bg-neutral-0 text-neutral-600 border border-neutral-400 hover:border-neutral-600 hover:text-neutral-700 active:bg-neutral-100 active:text-neutral-700 focus:ring-2 focus:ring-primary-200 disabled:border-neutral-300 disabled:text-neutral-400 disabled:bg-neutral-0 fill-neutral-600",
+ primary:
+ 'bg-primary-500 text-neutral-0 hover:bg-primary-600 active:bg-primary-600 focus:ring-2 focus:ring-primary-200 focus:bg-primary-600 disabled:bg-primary-200 fill-neutral-0',
+ secondary:
+ 'bg-neutral-0 text-neutral-600 border border-neutral-400 hover:border-neutral-600 hover:text-neutral-700 active:bg-neutral-100 active:text-neutral-700 focus:ring-2 focus:ring-primary-200 disabled:border-neutral-300 disabled:text-neutral-400 disabled:bg-neutral-0 fill-neutral-600',
- minimal:
- "text-neutral-600 hover:text-neutral-700 hover:bg-neutral-100 active:bg-neutral-200 active:text-neutral-700 focus:ring-2 focus:ring-primary-200 focus:bg-neutral-50 disabled:text-neutral-400 disabled:bg-transparent fill-neutral-600",
+ minimal:
+ 'text-neutral-600 hover:text-neutral-700 hover:bg-neutral-100 active:bg-neutral-200 active:text-neutral-700 focus:ring-2 focus:ring-primary-200 focus:bg-neutral-50 disabled:text-neutral-400 disabled:bg-transparent fill-neutral-600',
- danger: "bg-danger-500 text-neutral-0 hover:bg-danger-600 active:bg-danger-700 focus:ring-2 focus:ring-danger-100 focus:bg-danger-600 disabled:bg-danger-100 fill-neutral-0",
+ danger:
+ 'bg-danger-500 text-neutral-0 hover:bg-danger-600 active:bg-danger-700 focus:ring-2 focus:ring-danger-100 focus:bg-danger-600 disabled:bg-danger-100 fill-neutral-0',
- "danger-secondary":
- "bg-neutral-0 text-danger-500 border border-danger-500 hover:bg-danger-50 hover:text-danger-600 active:border-danger-700 active:text-danger-700 active:bg-danger-100 focus:ring-2 focus:ring-danger-100 focus:text-danger-600 disabled:border-danger-100 disabled:text-danger-100 disabled:bg-neutral-0 fill-danger-600",
-};
+ 'danger-secondary':
+ 'bg-neutral-0 text-danger-500 border border-danger-500 hover:bg-danger-50 hover:text-danger-600 active:border-danger-700 active:text-danger-700 active:bg-danger-100 focus:ring-2 focus:ring-danger-100 focus:text-danger-600 disabled:border-danger-100 disabled:text-danger-100 disabled:bg-neutral-0 fill-danger-600',
+}
-export interface IconButtonProps extends React.ButtonHTMLAttributes {
- Icon: React.ElementType;
- variant?: keyof typeof iconButtonVariants;
+export interface IconButtonProps extends ButtonHTMLAttributes {
+ Icon: ElementType
+ variant?: keyof typeof iconButtonVariants
}
-export const IconButton = ({ Icon, variant = "primary", className, ...props }: IconButtonProps) => {
- return (
-
-
-
- );
-};
+export const IconButton = ({
+ Icon,
+ variant = 'primary',
+ className,
+ ...props
+}: IconButtonProps) => {
+ return (
+
+
+
+ )
+}
diff --git a/src/components/icon-button/index.ts b/src/components/icon-button/index.ts
index b373d17b..3690957b 100644
--- a/src/components/icon-button/index.ts
+++ b/src/components/icon-button/index.ts
@@ -1 +1 @@
-export { IconButton } from "./icon-button";
+export { IconButton } from './icon-button'
diff --git a/src/components/index.ts b/src/components/index.ts
index ab1e1fad..e4bf1408 100644
--- a/src/components/index.ts
+++ b/src/components/index.ts
@@ -1,37 +1,37 @@
-export { Alert, AlertIntent } from "./alert";
-export { Avatar } from "./avatar";
-export { Badge, BadgeType } from "./badge";
-export { BreadcrumbNavigation } from "./breadcrumb-navigation";
-export { Button } from "./button";
-export { Checkbox } from "./checkbox";
-export { Dialog } from "./dialog";
-export { DividerLine } from "./divider-line";
-export { FormField } from "./form-field";
-export { IconButton } from "./icon-button";
-export { InlineAlert } from "./inline-alert";
-export { LastChangedInfo } from "./last-changed-info";
-export { Menu } from "./menu";
-export { Navigation } from "./navigation";
-export { PopoverMenu } from "./popover-menu";
-export { Page } from "./page";
-export { Panel } from "./panel";
-export { Section } from "./section";
-export { Sidebar } from "./sidebar";
-export { SidebarContainer } from "./sidebar-container";
-export { Sidesheet } from "./sidesheet";
-export { Skeleton } from "./skeleton";
-export { Spinner } from "./spinner";
-export { SpinnerOverlay } from "./spinner-overlay";
-export { Tab } from "./tab";
-export { TableUnvirtualized } from "./table-unvirtualized";
-export { TableVirtualized, TableVirtualizedProps } from "./table-virtualized";
-export { TableKeyValuePair } from "./table-key-value-pair";
-export { Tag } from "./tag";
-export { Toast } from "./toast";
-export { Toggle } from "./toggle";
-export { Tooltip } from "./tooltip";
-export { TopBar } from "./top-bar";
-export { Disclosure } from "./disclosure";
-export { ButtonGroup } from "./button-group";
-export { FeaturedTag } from "./featured-tag";
-export { Link } from "./link";
+export { Alert, AlertIntent } from './alert'
+export { Avatar } from './avatar'
+export { Badge, BadgeType } from './badge'
+export { BreadcrumbNavigation } from './breadcrumb-navigation'
+export { Button } from './button'
+export { Checkbox } from './checkbox'
+export { Dialog } from './dialog'
+export { DividerLine } from './divider-line'
+export { FormField } from './form-field'
+export { IconButton } from './icon-button'
+export { InlineAlert } from './inline-alert'
+export { LastChangedInfo } from './last-changed-info'
+export { Menu } from './menu'
+export { Navigation } from './navigation'
+export { PopoverMenu } from './popover-menu'
+export { Page } from './page'
+export { Panel } from './panel'
+export { Section } from './section'
+export { Sidebar } from './sidebar'
+export { SidebarContainer } from './sidebar-container'
+export { Sidesheet } from './sidesheet'
+export { Skeleton } from './skeleton'
+export { Spinner } from './spinner'
+export { SpinnerOverlay } from './spinner-overlay'
+export { Tab } from './tab'
+export { TableUnvirtualized } from './table-unvirtualized'
+export { TableVirtualized, TableVirtualizedProps } from './table-virtualized'
+export { TableKeyValuePair } from './table-key-value-pair'
+export { Tag } from './tag'
+export { Toast } from './toast'
+export { Toggle } from './toggle'
+export { Tooltip } from './tooltip'
+export { TopBar } from './top-bar'
+export { Disclosure } from './disclosure'
+export { ButtonGroup } from './button-group'
+export { FeaturedTag } from './featured-tag'
+export { Link } from './link'
diff --git a/src/components/inline-alert/index.ts b/src/components/inline-alert/index.ts
index 5292cac6..132579db 100644
--- a/src/components/inline-alert/index.ts
+++ b/src/components/inline-alert/index.ts
@@ -1 +1 @@
-export { InlineAlert } from "./inline-alert";
+export { InlineAlert } from './inline-alert'
diff --git a/src/components/inline-alert/inline-alert.stories.tsx b/src/components/inline-alert/inline-alert.stories.tsx
index 4cbcf69e..aae5ae3a 100644
--- a/src/components/inline-alert/inline-alert.stories.tsx
+++ b/src/components/inline-alert/inline-alert.stories.tsx
@@ -1,43 +1,49 @@
-import type { Meta, StoryObj } from "@storybook/react";
+import type { Meta, StoryObj } from '@storybook/react-vite'
-import React from "react";
-import { InlineAlert, InlineAlertProps } from "./inline-alert";
-import { getStoryDescription } from "../../util/storybook-utils";
+import { InlineAlert, InlineAlertProps } from './inline-alert'
+import { getStoryDescription } from '../../util/storybook-utils'
-const intents: InlineAlertProps["intent"][] = ["info", "success", "warning", "danger"];
+const intents: InlineAlertProps['intent'][] = [
+ 'info',
+ 'success',
+ 'warning',
+ 'danger',
+]
const meta: Meta = {
- component: InlineAlert,
- title: "Inline Alert",
- parameters: getStoryDescription("Inline alert text to inform user about contextual things"),
- args: {
- title: "Alert title",
- children: "Alert text",
- },
-};
+ component: InlineAlert,
+ title: 'Inline Alert',
+ parameters: getStoryDescription(
+ 'Inline alert text to inform user about contextual things',
+ ),
+ args: {
+ title: 'Alert title',
+ children: 'Alert text',
+ },
+}
-export default meta;
+export default meta
-type Story = StoryObj;
+type Story = StoryObj
-export const Basic: Story = {};
+export const Basic: Story = {}
export const Intents: Story = {
- argTypes: {
- intent: { table: { disable: true } },
- },
- render: ({ children, ...args }) => (
-
- {intents.map((intent) => (
-
- {children}
-
- ))}
-
- ),
-};
+ argTypes: {
+ intent: { table: { disable: true } },
+ },
+ render: ({ children, ...args }) => (
+
+ {intents.map((intent) => (
+
+ {children}
+
+ ))}
+
+ ),
+}
export const OnlyTitles: Story = {
- ...Intents,
- args: { children: undefined },
-};
+ ...Intents,
+ args: { children: undefined },
+}
diff --git a/src/components/inline-alert/inline-alert.test.tsx b/src/components/inline-alert/inline-alert.test.tsx
new file mode 100644
index 00000000..f75ca165
--- /dev/null
+++ b/src/components/inline-alert/inline-alert.test.tsx
@@ -0,0 +1,16 @@
+import { describe, expect, it } from 'vitest'
+import { render, screen } from '@testing-library/react'
+import { InlineAlert } from './inline-alert'
+
+describe('InlineAlert', () => {
+ it('renders the title and content', () => {
+ render(
+
+ Follow instructions.
+ ,
+ )
+
+ expect(screen.getByText('Attention')).toBeInTheDocument()
+ expect(screen.getByText('Follow instructions.')).toBeInTheDocument()
+ })
+})
diff --git a/src/components/inline-alert/inline-alert.tsx b/src/components/inline-alert/inline-alert.tsx
index 56ac9955..bacc0346 100644
--- a/src/components/inline-alert/inline-alert.tsx
+++ b/src/components/inline-alert/inline-alert.tsx
@@ -1,51 +1,64 @@
-import React from "react";
-import { classNames } from "../../util/class-names";
-import { ErrorIcon, InfoSignIcon, TickCircleIcon, WarningSignIcon } from "../../icons";
+import type { ElementType } from 'react'
+import { ReactNode } from 'react'
+import { classNames } from '../../util/class-names'
+import {
+ ErrorIcon,
+ InfoSignIcon,
+ TickCircleIcon,
+ WarningSignIcon,
+} from '../../icons'
-export type InlineAlertIntent = "success" | "info" | "warning" | "danger";
+export type InlineAlertIntent = 'success' | 'info' | 'warning' | 'danger'
const titleVariants: Record = {
- info: "text-primary-600",
- danger: "text-danger-500",
- success: "text-success-500",
- warning: "text-warning-600",
-};
+ info: 'text-primary-600',
+ danger: 'text-danger-500',
+ success: 'text-success-500',
+ warning: 'text-warning-600',
+}
const iconVariants: Record = {
- info: "fill-primary-400",
- danger: "fill-danger-400",
- success: "fill-success-400",
- warning: "fill-warning-500",
-};
+ info: 'fill-primary-400',
+ danger: 'fill-danger-400',
+ success: 'fill-success-400',
+ warning: 'fill-warning-500',
+}
-const iconNames: Record = {
- info: InfoSignIcon,
- success: TickCircleIcon,
- warning: WarningSignIcon,
- danger: ErrorIcon,
-};
+const iconNames: Record = {
+ info: InfoSignIcon,
+ success: TickCircleIcon,
+ warning: WarningSignIcon,
+ danger: ErrorIcon,
+}
export interface InlineAlertProps {
- title: string;
- className?: string;
- intent: InlineAlertIntent;
- children?: React.ReactNode;
+ title: string
+ className?: string
+ intent: InlineAlertIntent
+ children?: ReactNode
}
-export const InlineAlert = ({ title, className, children, intent = "info" }: InlineAlertProps) => {
- const Icon = iconNames[intent];
- const titleColor = titleVariants[intent];
- const iconColor = iconVariants[intent];
+export const InlineAlert = ({
+ title,
+ className,
+ children,
+ intent = 'info',
+}: InlineAlertProps) => {
+ const Icon = iconNames[intent]
+ const titleColor = titleVariants[intent]
+ const iconColor = iconVariants[intent]
- return (
-
-
-
-
-
-
{title}
- {children ?
{children}
: null}
-
-
- );
-};
+ return (
+
+
+
+
+
+
{title}
+ {children ?
+
{children}
+ : null}
+
+
+ )
+}
diff --git a/src/components/last-changed-info/index.ts b/src/components/last-changed-info/index.ts
index c34272ad..f5d5d780 100644
--- a/src/components/last-changed-info/index.ts
+++ b/src/components/last-changed-info/index.ts
@@ -1 +1 @@
-export { LastChangedInfo } from "./last-changed-info";
+export { LastChangedInfo } from './last-changed-info'
diff --git a/src/components/last-changed-info/last-changed-info.stories.tsx b/src/components/last-changed-info/last-changed-info.stories.tsx
index dbe70796..0d55b0c1 100644
--- a/src/components/last-changed-info/last-changed-info.stories.tsx
+++ b/src/components/last-changed-info/last-changed-info.stories.tsx
@@ -1,21 +1,20 @@
-import type { Meta, StoryObj } from "@storybook/react";
+import type { Meta, StoryObj } from '@storybook/react-vite'
-import React from "react";
-import { LastChangedInfo } from "./last-changed-info";
+import { LastChangedInfo } from './last-changed-info'
const meta: Meta = {
- title: "Last Changed Info",
- component: LastChangedInfo,
-};
+ title: 'Last Changed Info',
+ component: LastChangedInfo,
+}
-export default meta;
-type Story = StoryObj;
+export default meta
+type Story = StoryObj
export const Default: Story = {
- render: (args) => ,
- args: {
- changedDate: new Date(),
- changedBy: "By_You",
- className: "",
- },
-};
+ render: (args) => ,
+ args: {
+ changedDate: new Date(),
+ changedBy: 'By_You',
+ className: '',
+ },
+}
diff --git a/src/components/last-changed-info/last-changed-info.test.tsx b/src/components/last-changed-info/last-changed-info.test.tsx
new file mode 100644
index 00000000..a0d7511a
--- /dev/null
+++ b/src/components/last-changed-info/last-changed-info.test.tsx
@@ -0,0 +1,17 @@
+import { describe, expect, it } from 'vitest'
+import { render, screen } from '@testing-library/react'
+import { LastChangedInfo } from './last-changed-info'
+
+describe('LastChangedInfo', () => {
+ it('renders the formatted change message', () => {
+ render(
+ ,
+ )
+
+ const message = screen.getByText(/Last changed on/i)
+ expect(message).toHaveTextContent('by Jane Doe')
+ })
+})
diff --git a/src/components/last-changed-info/last-changed-info.tsx b/src/components/last-changed-info/last-changed-info.tsx
index 7d399da7..7a240da3 100644
--- a/src/components/last-changed-info/last-changed-info.tsx
+++ b/src/components/last-changed-info/last-changed-info.tsx
@@ -1,21 +1,24 @@
-import React from "react";
-import { classNames } from "../../util/class-names";
-import { getAbusixDateString } from "../../util/date";
+import { classNames } from '../../util/class-names'
+import { getAbusixDateString } from '../../util/date'
interface LastChangedInfoProps {
- changedDate: Date;
- changedBy: string | null;
- className?: string;
+ changedDate: Date
+ changedBy: string | null
+ className?: string
}
-export const LastChangedInfo = ({ changedDate, changedBy, className }: LastChangedInfoProps) => {
- const date = getAbusixDateString(changedDate);
- const changedByAppendix = changedBy ? `by ${changedBy}` : "";
- const lastChangeText = `Last changed on ${date} ${changedByAppendix}`;
+export const LastChangedInfo = ({
+ changedDate,
+ changedBy,
+ className,
+}: LastChangedInfoProps) => {
+ const date = getAbusixDateString(changedDate)
+ const changedByAppendix = changedBy ? `by ${changedBy}` : ''
+ const lastChangeText = `Last changed on ${date} ${changedByAppendix}`
- return (
-
- {lastChangeText}
-
- );
-};
+ return (
+
+ {lastChangeText}
+
+ )
+}
diff --git a/src/components/link/index.tsx b/src/components/link/index.tsx
index f7f96c3f..49fd79be 100644
--- a/src/components/link/index.tsx
+++ b/src/components/link/index.tsx
@@ -1 +1 @@
-export { Link } from "./link";
+export { Link } from './link'
diff --git a/src/components/link/link.stories.tsx b/src/components/link/link.stories.tsx
index e471f656..e1dd3dfe 100644
--- a/src/components/link/link.stories.tsx
+++ b/src/components/link/link.stories.tsx
@@ -1,61 +1,60 @@
-import React from "react";
-import type { Meta, StoryObj } from "@storybook/react";
-import { Link } from "./link";
-import { ChatIcon, DiagramTreeIcon, LockIcon } from "../../icons";
-import { hiddenArgControl } from "../../util/storybook-utils";
+import type { Meta, StoryObj } from '@storybook/react-vite'
+import { Link } from './link'
+import { ChatIcon, DiagramTreeIcon, LockIcon } from '../../icons'
+import { hiddenArgControl } from '../../util/storybook-utils'
-const icons = { undefined, ChatIcon, DiagramTreeIcon, LockIcon };
+const icons = { undefined, ChatIcon, DiagramTreeIcon, LockIcon }
const iconArg = {
- description: "Icon component",
- options: Object.keys(icons),
- mapping: icons,
-};
+ description: 'Icon component',
+ options: Object.keys(icons),
+ mapping: icons,
+}
const meta: Meta = {
- title: "Link",
- component: Link,
- args: {
- children: "Link Label",
- LeftIcon: undefined,
- RightIcon: undefined,
- },
- argTypes: {
- LeftIcon: iconArg,
- RightIcon: iconArg,
- asChild: hiddenArgControl,
- },
-};
+ title: 'Link',
+ component: Link,
+ args: {
+ children: 'Link Label',
+ LeftIcon: undefined,
+ RightIcon: undefined,
+ },
+ argTypes: {
+ LeftIcon: iconArg,
+ RightIcon: iconArg,
+ asChild: hiddenArgControl,
+ },
+}
-export default meta;
+export default meta
-type Story = StoryObj;
+type Story = StoryObj
export const Default: Story = {
- render: (args) => (
-
- {args.children}
-
- ),
-};
+ render: (args) => (
+
+ {args.children}
+
+ ),
+}
export const AsChild: Story = {
- render: (args) => (
-
- {args.children}
-
- ),
-};
+ render: (args) => (
+
+ {args.children}
+
+ ),
+}
export const WithChilds: Story = {
- argTypes: {
- children: hiddenArgControl,
- },
- render: (args) => (
-
-
- Nested
- Elements
-
-
- ),
-};
+ argTypes: {
+ children: hiddenArgControl,
+ },
+ render: (args) => (
+
+
+ Nested
+ Elements
+
+
+ ),
+}
diff --git a/src/components/link/link.test.tsx b/src/components/link/link.test.tsx
new file mode 100644
index 00000000..1b50597b
--- /dev/null
+++ b/src/components/link/link.test.tsx
@@ -0,0 +1,13 @@
+import { describe, expect, it } from 'vitest'
+import { render, screen } from '@testing-library/react'
+import { Link } from './link'
+
+describe('Link', () => {
+ it('renders an anchor with href', () => {
+ render( Read docs)
+
+ const link = screen.getByRole('link', { name: 'Read docs' })
+ expect(link).toBeInTheDocument()
+ expect(link).toHaveAttribute('href', '/docs')
+ })
+})
diff --git a/src/components/link/link.tsx b/src/components/link/link.tsx
index 24cf2913..3d0dff10 100644
--- a/src/components/link/link.tsx
+++ b/src/components/link/link.tsx
@@ -1,75 +1,84 @@
-import React from "react";
-import { AsChildProps, Slot } from "../slot/slot";
-import { classNames } from "../../util/class-names";
+import type { AnchorHTMLAttributes, ElementType, HTMLAttributes } from 'react'
+import { cloneElement, isValidElement } from 'react'
+import { AsChildProps, Slot } from '../slot/slot'
+import { classNames } from '../../util/class-names'
const linkVariants = {
- primary:
- "bg-primary-500 text-neutral-0 hover:bg-primary-600 active:bg-primary-600 focus:ring-2 focus:ring-primary-200 focus:bg-primary-600 disabled:bg-primary-200 fill-neutral-0",
- secondary:
- "text-neutral-700 bg-neutral-0 border border-neutral-400 hover:border-neutral-600 hover:text-neutral-800 active:bg-neutral-100 focus:ring-2 focus:ring-primary-200 focus:text-neutral-800 disabled:text-neutral-500 disabled:border-neutral-300 disabled:bg-neutral-0 fill-neutral-0",
- minimal:
- "text-neutral-700 hover:bg-neutral-100 hover:text-neutral-800 active:bg-neutral-200 focus:ring-2 focus:ring-primary-200 focus:text-neutral-800 disabled:text-neutral-500 disabled:bg-neutral-0 fill-neutral-0",
- danger: "text-neutral-0 bg-danger-500 hover:bg-danger-500 active:bg-danger-700 focus:ring-2 focus:ring-danger-100 focus:bg-danger-600 disabled:bg-danger-100 fill-neutral-0",
- "danger-secondary":
- "bg-neutral-0 text-danger-500 border border-danger-400 hover:bg-danger-50 hover:text-danger-600 active:border-danger-700 active:text-danger-700 active:bg-danger-100 focus:ring-2 focus:ring-danger-100 focus:text-danger-600 disabled:border-danger-100 disabled:text-danger-100 disabled:bg-neutral-0 fill-danger-600 disabled:fill-danger-100",
-};
+ primary:
+ 'bg-primary-500 text-neutral-0 hover:bg-primary-600 active:bg-primary-600 focus:ring-2 focus:ring-primary-200 focus:bg-primary-600 disabled:bg-primary-200 fill-neutral-0',
+ secondary:
+ 'text-neutral-700 bg-neutral-0 border border-neutral-400 hover:border-neutral-600 hover:text-neutral-800 active:bg-neutral-100 focus:ring-2 focus:ring-primary-200 focus:text-neutral-800 disabled:text-neutral-500 disabled:border-neutral-300 disabled:bg-neutral-0 fill-neutral-0',
+ minimal:
+ 'text-neutral-700 hover:bg-neutral-100 hover:text-neutral-800 active:bg-neutral-200 focus:ring-2 focus:ring-primary-200 focus:text-neutral-800 disabled:text-neutral-500 disabled:bg-neutral-0 fill-neutral-0',
+ danger:
+ 'text-neutral-0 bg-danger-500 hover:bg-danger-500 active:bg-danger-700 focus:ring-2 focus:ring-danger-100 focus:bg-danger-600 disabled:bg-danger-100 fill-neutral-0',
+ 'danger-secondary':
+ 'bg-neutral-0 text-danger-500 border border-danger-400 hover:bg-danger-50 hover:text-danger-600 active:border-danger-700 active:text-danger-700 active:bg-danger-100 focus:ring-2 focus:ring-danger-100 focus:text-danger-600 disabled:border-danger-100 disabled:text-danger-100 disabled:bg-neutral-0 fill-danger-600 disabled:fill-danger-100',
+}
const iconVariants = {
- primary: "text-neutral-0",
- secondary:
- "fill-neutral-600 group-hover:text-neutral-700 group-focus:text-neutral-700 group-disabled:text-neutral-400",
- minimal:
- "fill-neutral-600 group-hover:text-neutral-700 group-focus:text-neutral-700 group-disabled:text-neutral-400",
- danger: "",
- "danger-secondary": "",
-};
+ primary: 'text-neutral-0',
+ secondary:
+ 'fill-neutral-600 group-hover:text-neutral-700 group-focus:text-neutral-700 group-disabled:text-neutral-400',
+ minimal:
+ 'fill-neutral-600 group-hover:text-neutral-700 group-focus:text-neutral-700 group-disabled:text-neutral-400',
+ danger: '',
+ 'danger-secondary': '',
+}
-type LinkProps = AsChildProps> & {
- className?: string;
- variant?: keyof typeof linkVariants;
- LeftIcon?: React.ElementType;
- RightIcon?: React.ElementType;
-};
+type LinkProps = AsChildProps> & {
+ className?: string
+ variant?: keyof typeof linkVariants
+ LeftIcon?: ElementType
+ RightIcon?: ElementType
+}
export const Link = ({
- variant = "primary",
- className,
- children,
- LeftIcon,
- RightIcon,
- asChild = false,
- ...props
+ variant = 'primary',
+ className,
+ children,
+ LeftIcon,
+ RightIcon,
+ asChild = false,
+ ...props
}: LinkProps) => {
- const Comp = asChild ? Slot : "a";
- const commonClasses = classNames(
- `group flex h-8 items-center gap-2 whitespace-nowrap rounded-sm px-4 text-xs font-semibold focus:outline-hidden disabled:cursor-not-allowed `,
- linkVariants[variant],
- className
- );
+ const Comp = asChild ? Slot : 'a'
+ const commonClasses = classNames(
+ `group flex h-8 items-center gap-2 whitespace-nowrap rounded-sm px-4 text-xs font-semibold focus:outline-hidden disabled:cursor-not-allowed `,
+ linkVariants[variant],
+ className,
+ )
- if (React.isValidElement(children)) {
- return React.cloneElement(children, {
- ...children.props,
- children: (
- <>
- {LeftIcon ? : null}
- {children.props.children}
- {RightIcon ? (
-
- ) : null}
- >
- ),
- className: commonClasses,
- });
- }
+ if (isValidElement>(children)) {
+ const childProps = children.props
+ return cloneElement(children, {
+ ...childProps,
+ children: (
+ <>
+ {LeftIcon ?
+
+ : null}
+ {childProps.children}
+ {RightIcon ?
+
+ : null}
+ >
+ ),
+ className: classNames(commonClasses, childProps.className),
+ })
+ }
- return (
-
- <>
- {LeftIcon ? : null}
- {children}
- {RightIcon ? : null}
- >
-
- );
-};
+ return (
+
+ <>
+ {LeftIcon ?
+
+ : null}
+ {children}
+ {RightIcon ?
+
+ : null}
+ >
+
+ )
+}
diff --git a/src/components/menu/index.ts b/src/components/menu/index.ts
index 1f577526..de6ab9aa 100644
--- a/src/components/menu/index.ts
+++ b/src/components/menu/index.ts
@@ -1 +1 @@
-export { Menu } from "./menu";
+export { Menu } from './menu'
diff --git a/src/components/menu/menu-button/menu-button.tsx b/src/components/menu/menu-button/menu-button.tsx
index c60a394f..40e10964 100644
--- a/src/components/menu/menu-button/menu-button.tsx
+++ b/src/components/menu/menu-button/menu-button.tsx
@@ -1,15 +1,15 @@
-import { MenuButton as HeadlessUiMenuButton } from "@headlessui/react";
-import React from "react";
+import type { ReactNode } from 'react'
+import { MenuButton as HeadlessUiMenuButton } from '@headlessui/react'
export interface MenuButtonProps {
- children: React.ReactNode;
- className?: string;
+ children: ReactNode
+ className?: string
}
export const MenuButton = ({ children, className }: MenuButtonProps) => {
- return (
-
- {children}
-
- );
-};
+ return (
+
+ {children}
+
+ )
+}
diff --git a/src/components/menu/menu-info-item/menu-info-item.stories.tsx b/src/components/menu/menu-info-item/menu-info-item.stories.tsx
index aabab9dd..819c2a78 100644
--- a/src/components/menu/menu-info-item/menu-info-item.stories.tsx
+++ b/src/components/menu/menu-info-item/menu-info-item.stories.tsx
@@ -1,21 +1,32 @@
/* eslint-disable react/jsx-props-no-spreading */
-import type { Meta, StoryObj } from "@storybook/react";
+import type { Meta, StoryObj } from '@storybook/react-vite'
-import React from "react";
-import { MenuInfoItem } from "./menu-info-item";
+import { MenuInfoItem } from './menu-info-item'
const meta: Meta = {
- title: "Menu/MenuInfoItem",
- component: MenuInfoItem,
-};
+ title: 'Menu/MenuInfoItem',
+ component: MenuInfoItem,
+ args: {
+ title: 'Title',
+ subtitle: 'Subtitle',
+ containerWidth: 208,
+ },
+ argTypes: {
+ title: { control: 'text' },
+ subtitle: { control: 'text' },
+ containerWidth: {
+ control: { type: 'range', min: 120, max: 320, step: 8 },
+ },
+ },
+}
-export default meta;
-type Story = StoryObj;
+export default meta
+type Story = StoryObj
export const Default: Story = {
- render: () => (
-
-
-
- ),
-};
+ render: ({ title, subtitle, containerWidth }) => (
+
+
+
+ ),
+}
diff --git a/src/components/menu/menu-info-item/menu-info-item.tsx b/src/components/menu/menu-info-item/menu-info-item.tsx
index c3a49aee..c894b264 100644
--- a/src/components/menu/menu-info-item/menu-info-item.tsx
+++ b/src/components/menu/menu-info-item/menu-info-item.tsx
@@ -1,19 +1,17 @@
-import React from "react";
-
export interface MenuInfoItemProps {
- title: string;
- subtitle: string;
+ title: string
+ subtitle: string
}
export const MenuInfoItem = ({ title, subtitle }: MenuInfoItemProps) => {
- return (
-
-
- {title}
-
-
- {subtitle}
-
-
- );
-};
+ return (
+
+
+ {title}
+
+
+ {subtitle}
+
+
+ )
+}
diff --git a/src/components/menu/menu-item/menu-item.stories.tsx b/src/components/menu/menu-item/menu-item.stories.tsx
index ae4a47f7..240d7a16 100644
--- a/src/components/menu/menu-item/menu-item.stories.tsx
+++ b/src/components/menu/menu-item/menu-item.stories.tsx
@@ -1,32 +1,31 @@
-import type { Meta, StoryObj } from "@storybook/react";
-import React from "react";
-import { ChatIcon } from "../../../icons";
+import type { Meta, StoryObj } from '@storybook/react-vite'
+import { ChatIcon } from '../../../icons'
-import { Menu } from "../menu";
-import { MenuItem } from "./menu-item";
+import { Menu } from '../menu'
+import { MenuItem } from './menu-item'
const meta: Meta = {
- title: "Menu/MenuItem",
- component: MenuItem,
-};
+ title: 'Menu/MenuItem',
+ component: MenuItem,
+}
-export default meta;
-type Story = StoryObj;
+export default meta
+type Story = StoryObj
export const Default: Story = {
- argTypes: {
- disabled: {
- type: "boolean",
- defaultValue: false,
- },
+ argTypes: {
+ disabled: {
+ type: 'boolean',
+ defaultValue: false,
},
- render: (args) => (
-
-
-
- Label
-
-
-
- ),
-};
+ },
+ render: (args) => (
+
+
+
+ Label
+
+
+
+ ),
+}
diff --git a/src/components/menu/menu-item/menu-item.tsx b/src/components/menu/menu-item/menu-item.tsx
index df41bd0c..91e0a20e 100644
--- a/src/components/menu/menu-item/menu-item.tsx
+++ b/src/components/menu/menu-item/menu-item.tsx
@@ -1,30 +1,36 @@
-import { MenuItem as HeadlessUiMenuItem } from "@headlessui/react";
-import React from "react";
+import type { ElementType } from 'react'
+import { ReactNode } from 'react'
+import { MenuItem as HeadlessUiMenuItem } from '@headlessui/react'
export interface MenuItemProps {
- children: React.ReactNode;
- LeftIcon?: React.ElementType;
- disabled?: boolean;
- onClick?: () => void;
+ children: ReactNode
+ LeftIcon?: ElementType
+ disabled?: boolean
+ onClick?: () => void
}
-export const MenuItem = ({ children, LeftIcon, disabled = false, onClick }: MenuItemProps) => {
- return (
-
-
- {LeftIcon ? (
-
- ) : null}
-
- {children}
-
-
-
- );
-};
+export const MenuItem = ({
+ children,
+ LeftIcon,
+ disabled = false,
+ onClick,
+}: MenuItemProps) => {
+ return (
+
+
+ {LeftIcon ?
+
+ : null}
+
+ {children}
+
+
+
+ )
+}
diff --git a/src/components/menu/menu-items/menu-items.tsx b/src/components/menu/menu-items/menu-items.tsx
index d30981a2..cf7b0a9e 100644
--- a/src/components/menu/menu-items/menu-items.tsx
+++ b/src/components/menu/menu-items/menu-items.tsx
@@ -1,13 +1,13 @@
-import { MenuItems as HeadlessUiMenuItems } from "@headlessui/react";
-import React from "react";
+import type { ReactNode } from 'react'
+import { MenuItems as HeadlessUiMenuItems } from '@headlessui/react'
export interface MenuItemsProps {
- children: React.ReactNode;
+ children: ReactNode
}
export const MenuItems = ({ children }: MenuItemsProps) => {
- return (
-
- {children}
-
- );
-};
+ return (
+
+ {children}
+
+ )
+}
diff --git a/src/components/menu/menu-separator/menu-separator.stories.tsx b/src/components/menu/menu-separator/menu-separator.stories.tsx
index 338e9767..89cc7f7d 100644
--- a/src/components/menu/menu-separator/menu-separator.stories.tsx
+++ b/src/components/menu/menu-separator/menu-separator.stories.tsx
@@ -1,21 +1,28 @@
/* eslint-disable react/jsx-props-no-spreading */
-import type { Meta, StoryObj } from "@storybook/react";
+import type { Meta, StoryObj } from '@storybook/react-vite'
-import React from "react";
-import { MenuSeparator } from "./menu-separator";
+import { MenuSeparator } from './menu-separator'
const meta: Meta = {
- title: "Menu/MenuSeparator",
- component: MenuSeparator,
-};
+ title: 'Menu/MenuSeparator',
+ component: MenuSeparator,
+ args: {
+ containerWidth: 208,
+ },
+ argTypes: {
+ containerWidth: {
+ control: { type: 'range', min: 120, max: 320, step: 8 },
+ },
+ },
+}
-export default meta;
-type Story = StoryObj;
+export default meta
+type Story = StoryObj
export const Default: Story = {
- render: () => (
-
-
-
- ),
-};
+ render: ({ containerWidth }) => (
+
+
+
+ ),
+}
diff --git a/src/components/menu/menu-separator/menu-separator.tsx b/src/components/menu/menu-separator/menu-separator.tsx
index 05d94791..0d028765 100644
--- a/src/components/menu/menu-separator/menu-separator.tsx
+++ b/src/components/menu/menu-separator/menu-separator.tsx
@@ -1,5 +1,3 @@
-import React from "react";
-
export const MenuSeparator = () => {
- return ;
-};
+ return
+}
diff --git a/src/components/menu/menu-title/menu-title.stories.tsx b/src/components/menu/menu-title/menu-title.stories.tsx
index 8ae17a10..95dfe54d 100644
--- a/src/components/menu/menu-title/menu-title.stories.tsx
+++ b/src/components/menu/menu-title/menu-title.stories.tsx
@@ -1,20 +1,29 @@
/* eslint-disable react/jsx-props-no-spreading */
-import type { Meta, StoryObj } from "@storybook/react";
-import React from "react";
-import { MenuTitle } from "./menu-title";
+import type { Meta, StoryObj } from '@storybook/react-vite'
+import { MenuTitle } from './menu-title'
const meta: Meta = {
- title: "Menu/MenuTitle",
- component: MenuTitle,
-};
+ title: 'Menu/MenuTitle',
+ component: MenuTitle,
+ args: {
+ title: 'Title',
+ containerWidth: 208,
+ },
+ argTypes: {
+ title: { control: 'text' },
+ containerWidth: {
+ control: { type: 'range', min: 120, max: 320, step: 8 },
+ },
+ },
+}
-export default meta;
-type Story = StoryObj;
+export default meta
+type Story = StoryObj
export const Default: Story = {
- render: () => (
-
- Title
-
- ),
-};
+ render: ({ title, containerWidth }) => (
+
+ {title}
+
+ ),
+}
diff --git a/src/components/menu/menu-title/menu-title.tsx b/src/components/menu/menu-title/menu-title.tsx
index 6abb7b40..c7749d86 100644
--- a/src/components/menu/menu-title/menu-title.tsx
+++ b/src/components/menu/menu-title/menu-title.tsx
@@ -1,13 +1,11 @@
-import React from "react";
-
export interface MenuTitleProps {
- children: string;
+ children: string
}
export const MenuTitle = ({ children }: MenuTitleProps) => {
- return (
-
- {children}
-
- );
-};
+ return (
+
+ {children}
+
+ )
+}
diff --git a/src/components/menu/menu.stories.tsx b/src/components/menu/menu.stories.tsx
index 6b2eb94c..b4e13abb 100644
--- a/src/components/menu/menu.stories.tsx
+++ b/src/components/menu/menu.stories.tsx
@@ -1,27 +1,68 @@
-import type { Meta, StoryObj } from "@storybook/react";
-import React from "react";
-import { Menu } from "./menu";
+import type { Meta, StoryObj } from '@storybook/react-vite'
+import { Menu } from './menu'
-const meta: Meta = {
- title: "Menu",
- component: Menu,
-};
+type MenuStoryArgs = {
+ buttonLabel: string
+ menuTitle: string
+ itemLabel: string
+ itemCount: number
+ disabledFirstItem: boolean
+ containerWidth: number
+}
-export default meta;
-type Story = StoryObj;
+const MenuStory = ({
+ buttonLabel,
+ menuTitle,
+ itemLabel,
+ itemCount,
+ disabledFirstItem,
+ containerWidth,
+}: MenuStoryArgs) => (
+
+
+ {buttonLabel}
+
+ {menuTitle}
+ {Array.from({ length: itemCount }).map((_, index) => (
+
+ {itemLabel}
+
+ ))}
+
+
+
+)
+
+const meta: Meta = {
+ title: 'Menu',
+ component: MenuStory,
+ args: {
+ buttonLabel: 'Open Menu',
+ menuTitle: 'TITLE',
+ itemLabel: 'Label',
+ itemCount: 3,
+ disabledFirstItem: false,
+ containerWidth: 208,
+ },
+ argTypes: {
+ buttonLabel: { control: 'text' },
+ menuTitle: { control: 'text' },
+ itemLabel: { control: 'text' },
+ itemCount: { control: { type: 'number', min: 1, max: 6, step: 1 } },
+ disabledFirstItem: { control: 'boolean' },
+ containerWidth: {
+ control: { type: 'range', min: 120, max: 320, step: 8 },
+ },
+ },
+}
+
+export default meta
+type Story = StoryObj
export const Default: Story = {
- render: () => (
-
-
- Open Menu
-
- TITLE
- Label
- Label
- Label
-
-
-
- ),
-};
+ render: (args) => ,
+}
diff --git a/src/components/menu/menu.test.tsx b/src/components/menu/menu.test.tsx
new file mode 100644
index 00000000..c87dc67b
--- /dev/null
+++ b/src/components/menu/menu.test.tsx
@@ -0,0 +1,15 @@
+import { describe, expect, it } from 'vitest'
+import { render, screen } from '@testing-library/react'
+import { Menu } from './menu'
+
+describe('Menu', () => {
+ it('renders the menu trigger', () => {
+ render(
+
+ Open Menu
+ ,
+ )
+
+ expect(screen.getByText('Open Menu')).toBeInTheDocument()
+ })
+})
diff --git a/src/components/menu/menu.tsx b/src/components/menu/menu.tsx
index 217e7927..0cabda16 100644
--- a/src/components/menu/menu.tsx
+++ b/src/components/menu/menu.tsx
@@ -1,29 +1,29 @@
-import { Menu as HeadlessUiMenu } from "@headlessui/react";
-import React from "react";
-import { MenuButton } from "./menu-button/menu-button";
-import { MenuInfoItem } from "./menu-info-item/menu-info-item";
-import { MenuItem } from "./menu-item/menu-item";
-import { MenuItems } from "./menu-items/menu-items";
-import { MenuSeparator } from "./menu-separator/menu-separator";
-import { MenuTitle } from "./menu-title/menu-title";
+import type { ReactNode } from 'react'
+import { Menu as HeadlessUiMenu } from '@headlessui/react'
+import { MenuButton } from './menu-button/menu-button'
+import { MenuInfoItem } from './menu-info-item/menu-info-item'
+import { MenuItem } from './menu-item/menu-item'
+import { MenuItems } from './menu-items/menu-items'
+import { MenuSeparator } from './menu-separator/menu-separator'
+import { MenuTitle } from './menu-title/menu-title'
interface MenuProps {
- children: React.ReactNode;
+ children: ReactNode
}
const Menu = ({ children }: MenuProps) => {
- return (
-
- {children}
-
- );
-};
+ return (
+
+ {children}
+
+ )
+}
-Menu.Button = MenuButton;
-Menu.Items = MenuItems;
-Menu.Title = MenuTitle;
-Menu.Item = MenuItem;
-Menu.InfoItem = MenuInfoItem;
-Menu.Separator = MenuSeparator;
+Menu.Button = MenuButton
+Menu.Items = MenuItems
+Menu.Title = MenuTitle
+Menu.Item = MenuItem
+Menu.InfoItem = MenuInfoItem
+Menu.Separator = MenuSeparator
-export { Menu };
+export { Menu }
diff --git a/src/components/navigation/index.ts b/src/components/navigation/index.ts
index 896195ec..06da6947 100644
--- a/src/components/navigation/index.ts
+++ b/src/components/navigation/index.ts
@@ -1 +1 @@
-export { Navigation } from "./navigation";
+export { Navigation } from './navigation'
diff --git a/src/components/navigation/navigation-disclosure-panel.tsx b/src/components/navigation/navigation-disclosure-panel.tsx
index 6f4822db..cf3b728c 100644
--- a/src/components/navigation/navigation-disclosure-panel.tsx
+++ b/src/components/navigation/navigation-disclosure-panel.tsx
@@ -1,48 +1,52 @@
-import { DisclosurePanel as HeadlessUiDisclosurePanel } from "@headlessui/react";
-import React from "react";
-import { classNames } from "../../util/class-names";
+import type { ComponentPropsWithoutRef } from 'react'
+import { ReactNode } from 'react'
+import { DisclosurePanel as HeadlessUiDisclosurePanel } from '@headlessui/react'
+import { classNames } from '../../util/class-names'
-export interface NavigationDisclosurePanelItemProps extends React.ComponentPropsWithoutRef<"div"> {
- isActive?: boolean;
- /**
- * Give this navigation item extra indentation of width consistent with the `LeftIcon` of a `NavigationGroup`.
- * This should be used if the parent has an icon and the child item does not to make the margins consistent.
- * i.e. the child item intents on the text of the parent item, not the icon.
- */
- isIndented?: boolean;
+export interface NavigationDisclosurePanelItemProps extends ComponentPropsWithoutRef<'div'> {
+ isActive?: boolean
+ /**
+ * Give this navigation item extra indentation of width consistent with the `LeftIcon` of a `NavigationGroup`.
+ * This should be used if the parent has an icon and the child item does not to make the margins consistent.
+ * i.e. the child item intents on the text of the parent item, not the icon.
+ */
+ isIndented?: boolean
}
const NavigationDisclosurePanelItem = ({
- children,
- isActive,
- isIndented,
- ...props
+ children,
+ isActive,
+ isIndented,
+ ...props
}: NavigationDisclosurePanelItemProps) => {
- return (
-
- {children}
- {isActive && (
-
- )}
-
- );
-};
+ return (
+
+ {children}
+ {isActive && (
+
+ )}
+
+ )
+}
export interface NavigationDisclosurePanelProps {
- children: React.ReactNode;
+ children: ReactNode
}
-const NavigationDisclosurePanel = ({ children }: NavigationDisclosurePanelProps) => {
- return {children} ;
-};
+const NavigationDisclosurePanel = ({
+ children,
+}: NavigationDisclosurePanelProps) => {
+ return {children}
+}
-NavigationDisclosurePanel.Item = NavigationDisclosurePanelItem;
+NavigationDisclosurePanel.Item = NavigationDisclosurePanelItem
-export { NavigationDisclosurePanel };
+export { NavigationDisclosurePanel }
diff --git a/src/components/navigation/navigation-disclosure.tsx b/src/components/navigation/navigation-disclosure.tsx
index 2747b75c..b3d8e4f5 100644
--- a/src/components/navigation/navigation-disclosure.tsx
+++ b/src/components/navigation/navigation-disclosure.tsx
@@ -1,74 +1,82 @@
+import type { ElementType, MutableRefObject } from 'react'
+import { ReactNode } from 'react'
import {
- Disclosure as HeadlessUiDisclosure,
- DisclosureButton as HeadlessUiDisclosureButton,
-} from "@headlessui/react";
-import React from "react";
-import { classNames } from "../../util/class-names";
-import { NavigationDisclosurePanel } from "./navigation-disclosure-panel";
-import { NavigationGroupItemTag } from "./navigation-group-item-tag";
+ Disclosure as HeadlessUiDisclosure,
+ DisclosureButton as HeadlessUiDisclosureButton,
+} from '@headlessui/react'
+import { classNames } from '../../util/class-names'
+import { NavigationDisclosurePanel } from './navigation-disclosure-panel'
+import { NavigationGroupItemTag } from './navigation-group-item-tag'
export interface NavigationDisclosureButtonProps {
- children: React.ReactNode;
- LeftIcon?: React.ElementType;
- onClick?: () => void;
- tag?: string;
- className?: string;
+ children: ReactNode
+ LeftIcon?: ElementType
+ onClick?: () => void
+ tag?: string
+ className?: string
}
const NavigationDisclosureButton = ({
- children,
- LeftIcon,
- onClick,
- tag,
- className,
+ children,
+ LeftIcon,
+ onClick,
+ tag,
+ className,
}: NavigationDisclosureButtonProps) => {
- return (
-
- {LeftIcon ? : null}
- {children}
- {tag ? {tag} : null}
-
- );
-};
+ return (
+
+ {LeftIcon ?
+
+ : null}
+ {children}
+ {tag ?
+ {tag}
+ : null}
+
+ )
+}
interface CloseFunction {
- (focusableElement?: HTMLElement | React.MutableRefObject): void;
+ (focusableElement?: HTMLElement | MutableRefObject): void
}
interface ChildrenType {
- (props: { close: CloseFunction }): React.ReactNode;
+ (props: { close: CloseFunction }): ReactNode
}
export interface NavigationDisclosureProps {
- children: ChildrenType | React.ReactNode;
- defaultOpen?: boolean;
+ children: ChildrenType | ReactNode
+ defaultOpen?: boolean
}
const renderDisclosureChildren = ({
- children,
- close,
+ children,
+ close,
}: {
- children: ChildrenType | React.ReactNode;
- close: CloseFunction;
+ children: ChildrenType | ReactNode
+ close: CloseFunction
}) => {
- return typeof children === "function" ? children({ close }) : children;
-};
+ return typeof children === 'function' ? children({ close }) : children
+}
-const NavigationDisclosure = ({ children, defaultOpen }: NavigationDisclosureProps) => {
- return (
-
- {({ close }) => <>{renderDisclosureChildren({ children, close })}>}
-
- );
-};
+const NavigationDisclosure = ({
+ children,
+ defaultOpen,
+}: NavigationDisclosureProps) => {
+ return (
+
+ {({ close }) => <>{renderDisclosureChildren({ children, close })}>}
+
+ )
+}
-NavigationDisclosure.Panel = NavigationDisclosurePanel;
-NavigationDisclosure.Button = NavigationDisclosureButton;
+NavigationDisclosure.Panel = NavigationDisclosurePanel
+NavigationDisclosure.Button = NavigationDisclosureButton
-export { NavigationDisclosure };
+export { NavigationDisclosure }
diff --git a/src/components/navigation/navigation-group-item-tag.tsx b/src/components/navigation/navigation-group-item-tag.tsx
index a8f64a89..c88aec13 100644
--- a/src/components/navigation/navigation-group-item-tag.tsx
+++ b/src/components/navigation/navigation-group-item-tag.tsx
@@ -1,15 +1,15 @@
-import React from "react";
+import type { ReactNode } from 'react'
interface NavigationGroupItemTagProps {
- children: React.ReactNode;
+ children: ReactNode
}
const NavigationGroupItemTag = ({ children }: NavigationGroupItemTagProps) => {
- return (
-
- {children}
-
- );
-};
+ return (
+
+ {children}
+
+ )
+}
-export { NavigationGroupItemTag };
+export { NavigationGroupItemTag }
diff --git a/src/components/navigation/navigation-group.tsx b/src/components/navigation/navigation-group.tsx
index ee243dbb..f6f1482c 100644
--- a/src/components/navigation/navigation-group.tsx
+++ b/src/components/navigation/navigation-group.tsx
@@ -1,42 +1,48 @@
-import React from "react";
-import { classNames } from "../../util/class-names";
-import { NavigationGroupItemTag } from "./navigation-group-item-tag";
+import type { ComponentPropsWithoutRef, ElementType } from 'react'
+import { ReactNode } from 'react'
+import { classNames } from '../../util/class-names'
+import { NavigationGroupItemTag } from './navigation-group-item-tag'
-export interface NavigationGroupItemProps extends React.ComponentPropsWithoutRef<"div"> {
- isActive?: boolean;
- LeftIcon?: React.ElementType;
- tag?: string;
+export interface NavigationGroupItemProps extends ComponentPropsWithoutRef<'div'> {
+ isActive?: boolean
+ LeftIcon?: ElementType
+ tag?: string
}
const NavigationGroupItem = ({
- children,
- isActive,
- LeftIcon,
- tag,
- ...props
+ children,
+ isActive,
+ LeftIcon,
+ tag,
+ ...props
}: NavigationGroupItemProps) => {
- return (
-
- {LeftIcon ?
: null}
- {children}
- {tag ?
{tag} : null}
- {isActive && (
-
- )}
-
- );
-};
+ return (
+
+ {LeftIcon ?
+
+ : null}
+ {children}
+ {tag ?
+
{tag}
+ : null}
+ {isActive && (
+
+ )}
+
+ )
+}
-const NavigationGroup = ({ children }: { children: React.ReactNode }) => {
- return {children}
;
-};
+const NavigationGroup = ({ children }: { children: ReactNode }) => {
+ return {children}
+}
-NavigationGroup.Item = NavigationGroupItem;
+NavigationGroup.Item = NavigationGroupItem
-export { NavigationGroup };
+export { NavigationGroup }
diff --git a/src/components/navigation/navigation-popover-button.tsx b/src/components/navigation/navigation-popover-button.tsx
index 66b41247..9b9a2f23 100644
--- a/src/components/navigation/navigation-popover-button.tsx
+++ b/src/components/navigation/navigation-popover-button.tsx
@@ -1,32 +1,33 @@
-import { PopoverButton as HeadlessUiPopoverButton } from "@headlessui/react";
-import React from "react";
-import { useNavigationPopoverContext } from "./navigation-popover-context";
+import type { ElementType } from 'react'
+import { ReactNode } from 'react'
+import { PopoverButton as HeadlessUiPopoverButton } from '@headlessui/react'
+import { useNavigationPopoverContext } from './navigation-popover-context'
export interface NavigationPopoverButtonProps {
- LeftIcon?: React.ElementType;
- children: React.ReactNode;
- onClick?: () => void;
+ LeftIcon?: ElementType
+ children: ReactNode
+ onClick?: () => void
}
export const NavigationPopoverButton = ({
- children,
- LeftIcon,
- onClick,
+ children,
+ LeftIcon,
+ onClick,
}: NavigationPopoverButtonProps) => {
- const {
- popoverButton: { setReferenceElement },
- } = useNavigationPopoverContext();
+ const {
+ popoverButton: { setReferenceElement },
+ } = useNavigationPopoverContext()
- return (
- el && setReferenceElement(el)}
- className="text-neutral-0 hover:bg-primary-900-plus-10 ui-open:bg-primary-900-plus-8 ui-open:font-semibold flex w-full cursor-pointer items-center gap-x-3 px-4 py-3 text-left text-sm"
- onClick={onClick}
- >
- <>
- {LeftIcon && }
- {children}
- >
-
- );
-};
+ return (
+
+ <>
+ {LeftIcon && }
+ {children}
+ >
+
+ )
+}
diff --git a/src/components/navigation/navigation-popover-context.tsx b/src/components/navigation/navigation-popover-context.tsx
index b4515a4e..34b86156 100644
--- a/src/components/navigation/navigation-popover-context.tsx
+++ b/src/components/navigation/navigation-popover-context.tsx
@@ -1,25 +1,25 @@
-import { CSSProperties, createContext, useContext } from "react";
+import { CSSProperties, createContext, useContext } from 'react'
export const NavigationPopoverContext = createContext<{
- popoverButton: {
- setReferenceElement: React.Dispatch>;
- };
- popoverPanel: {
- setPopperElement: React.Dispatch>;
- styles: CSSProperties;
- attributes: { [key: string]: string } | undefined;
- };
+ popoverButton: {
+ setReferenceElement: (element: HTMLButtonElement | null) => void
+ }
+ popoverPanel: {
+ setFloatingElement: (element: HTMLElement | null) => void
+ styles: CSSProperties
+ }
}>({
- popoverButton: {
- setReferenceElement: () => {},
- },
- popoverPanel: {
- setPopperElement: () => {},
- styles: {},
- attributes: {},
- },
-});
+ popoverButton: {
+ setReferenceElement: () => {},
+ },
+ popoverPanel: {
+ setFloatingElement: () => {},
+ styles: {},
+ },
+})
-export const useNavigationPopoverContext = () => useContext(NavigationPopoverContext);
+export const useNavigationPopoverContext = () =>
+ useContext(NavigationPopoverContext)
-export const NavigationPopoverContextProvider = NavigationPopoverContext.Provider;
+export const NavigationPopoverContextProvider =
+ NavigationPopoverContext.Provider
diff --git a/src/components/navigation/navigation-popover-overlay.tsx b/src/components/navigation/navigation-popover-overlay.tsx
index 8d23c841..02436dc1 100644
--- a/src/components/navigation/navigation-popover-overlay.tsx
+++ b/src/components/navigation/navigation-popover-overlay.tsx
@@ -1,6 +1,5 @@
-import { PopoverBackdrop as HeadlessUiPopoverBackdrop } from "@headlessui/react";
-import * as React from "react";
+import { PopoverBackdrop as HeadlessUiPopoverBackdrop } from '@headlessui/react'
export const NavigationPopoverOverlay = () => (
-
-);
+
+)
diff --git a/src/components/navigation/navigation-popover-panel.tsx b/src/components/navigation/navigation-popover-panel.tsx
index ebe44440..f55b3b16 100644
--- a/src/components/navigation/navigation-popover-panel.tsx
+++ b/src/components/navigation/navigation-popover-panel.tsx
@@ -1,40 +1,41 @@
-import { PopoverPanel as HeadlessUiPopoverPanel } from "@headlessui/react";
-import React from "react";
-import { useNavigationPopoverContext } from "./navigation-popover-context";
+import type { ReactNode } from 'react'
+import { PopoverPanel as HeadlessUiPopoverPanel } from '@headlessui/react'
+import { useNavigationPopoverContext } from './navigation-popover-context'
export interface NavigationPopoverPanelItemProps {
- children: React.ReactNode;
+ children: ReactNode
}
-const NavigationPopoverPanelItem = ({ children }: NavigationPopoverPanelItemProps) => {
- return (
-
- );
-};
+const NavigationPopoverPanelItem = ({
+ children,
+}: NavigationPopoverPanelItemProps) => {
+ return (
+
+ )
+}
export interface NavigationPopoverPanelProps {
- children: React.ReactNode;
+ children: ReactNode
}
const NavigationPopoverPanel = ({ children }: NavigationPopoverPanelProps) => {
- const {
- popoverPanel: { setPopperElement, styles, attributes },
- } = useNavigationPopoverContext();
+ const {
+ popoverPanel: { setFloatingElement, styles },
+ } = useNavigationPopoverContext()
- return (
- el && setPopperElement(el)}
- style={styles}
- {...attributes}
- className="bg-neutral-0 z-40 ml-2 w-52 rounded-sm py-2 shadow-sm"
- >
- {children}
-
- );
-};
+ return (
+
+ {children}
+
+ )
+}
-NavigationPopoverPanel.Item = NavigationPopoverPanelItem;
+NavigationPopoverPanel.Item = NavigationPopoverPanelItem
-export { NavigationPopoverPanel };
+export { NavigationPopoverPanel }
diff --git a/src/components/navigation/navigation-popover.tsx b/src/components/navigation/navigation-popover.tsx
index 0adf7f2b..00740f3a 100644
--- a/src/components/navigation/navigation-popover.tsx
+++ b/src/components/navigation/navigation-popover.tsx
@@ -1,42 +1,40 @@
-import { Popover } from "@headlessui/react";
-import React, { useState } from "react";
-import { usePopper } from "react-popper";
-import { NavigationPopoverButton } from "./navigation-popover-button";
-import { NavigationPopoverContextProvider } from "./navigation-popover-context";
-import { NavigationPopoverOverlay } from "./navigation-popover-overlay";
-import { NavigationPopoverPanel } from "./navigation-popover-panel";
+import type { ReactNode } from 'react'
+import { Popover } from '@headlessui/react'
+import { autoUpdate, useFloating } from '@floating-ui/react'
+import { NavigationPopoverButton } from './navigation-popover-button'
+import { NavigationPopoverContextProvider } from './navigation-popover-context'
+import { NavigationPopoverOverlay } from './navigation-popover-overlay'
+import { NavigationPopoverPanel } from './navigation-popover-panel'
export interface NavigationPopoverProps {
- children: React.ReactNode;
+ children: ReactNode
}
const NavigationPopover = ({ children }: NavigationPopoverProps) => {
- const [referenceElement, setReferenceElement] = useState();
- const [popperElement, setPopperElement] = useState();
- const { styles, attributes } = usePopper(referenceElement, popperElement, {
- placement: "top-start",
- });
+ const { refs, floatingStyles } = useFloating({
+ placement: 'top-start',
+ whileElementsMounted: autoUpdate,
+ })
- const context = {
- popoverButton: {
- setReferenceElement,
- },
- popoverPanel: {
- setPopperElement,
- styles: styles.popper,
- attributes: attributes.popper,
- },
- };
+ const context = {
+ popoverButton: {
+ setReferenceElement: refs.setReference,
+ },
+ popoverPanel: {
+ setFloatingElement: refs.setFloating,
+ styles: floatingStyles,
+ },
+ }
- return (
-
- {children}
-
- );
-};
+ return (
+
+ {children}
+
+ )
+}
-NavigationPopover.Button = NavigationPopoverButton;
-NavigationPopover.Panel = NavigationPopoverPanel;
-NavigationPopover.Overlay = NavigationPopoverOverlay;
+NavigationPopover.Button = NavigationPopoverButton
+NavigationPopover.Panel = NavigationPopoverPanel
+NavigationPopover.Overlay = NavigationPopoverOverlay
-export { NavigationPopover };
+export { NavigationPopover }
diff --git a/src/components/navigation/navigation.stories.tsx b/src/components/navigation/navigation.stories.tsx
index dba86536..8b895ba9 100644
--- a/src/components/navigation/navigation.stories.tsx
+++ b/src/components/navigation/navigation.stories.tsx
@@ -1,125 +1,159 @@
-import type { Meta, StoryObj } from "@storybook/react";
-import React from "react";
-import { CogIcon, HelpIcon, InfoSignIcon } from "../../icons";
-import { Navigation } from "./navigation";
+import type { Meta, StoryObj } from '@storybook/react-vite'
+import { CogIcon, HelpIcon, InfoSignIcon } from '../../icons'
+import { Navigation } from './navigation'
-const meta: Meta = {
- title: "Navigation",
- component: Navigation,
- parameters: {
- options: {
- showPanel: false,
- },
+type NavigationStoryArgs = {
+ logoText: string
+ activeItemLabel: string
+ activeItemTag: string
+ supportLabel: string
+ containerWidth: number
+}
+
+const NavigationStory = ({
+ logoText,
+ activeItemLabel,
+ activeItemTag,
+ supportLabel,
+ containerWidth,
+}: NavigationStoryArgs) => (
+
+
+
+ {logoText}
+
+
+
+ Home
+ Dashboard
+
+ {activeItemLabel}
+
+
+
+ Lookup & Delist
+ Mail Intelligence
+
+
+ AbuseHQ
+
+
+
+ Cases
+
+
+ Event Inbox
+
+
+ Mailbox
+
+
+ Dashboard
+
+
+ Statistics
+
+
+ Settings
+
+
+
+
+
+ Networks
+ Data Channels
+
+
+
+
+ {supportLabel}
+
+
+
+
+ Documentation
+
+
+ Support request
+
+
+ System status
+
+
+ Blog posts
+
+
+
+
+
+ Plans & Billing
+
+
+
+ Subscriptions
+
+
+ Billing
+
+
+ Invoices
+
+
+
+
+
+ Settings
+
+
+
+ Profile
+
+
+ Team
+
+
+ Sign out
+
+
+
+
+
+
+
+)
+
+const meta: Meta = {
+ title: 'Navigation',
+ component: NavigationStory,
+ args: {
+ logoText: 'Abusix',
+ activeItemLabel: 'long text for this navigation menu option',
+ activeItemTag: 'Beta',
+ supportLabel: 'Support',
+ containerWidth: 384,
+ },
+ argTypes: {
+ logoText: { control: 'text' },
+ activeItemLabel: { control: 'text' },
+ activeItemTag: { control: 'text' },
+ supportLabel: { control: 'text' },
+ containerWidth: {
+ control: { type: 'range', min: 240, max: 520, step: 16 },
},
-};
+ },
+ parameters: {
+ options: {
+ showPanel: false,
+ },
+ },
+}
-export default meta;
-type Story = StoryObj;
+export default meta
+type Story = StoryObj
export const Default: Story = {
- render: () => (
-
-
-
- Abusix
-
-
-
- Home
- Dashboard
-
- long text for this navigation menu option
-
-
-
- Lookup & Delist
- Mail Intelligence
-
-
- AbuseHQ
-
-
-
- Cases
-
-
- Event Inbox
-
-
- Mailbox
-
-
- Dashboard
-
-
- Statistics
-
-
- Settings
-
-
-
-
-
- Networks
- Data Channels
-
-
-
-
- Support
-
-
-
-
- Documentation
-
-
- Support request
-
-
- System status
-
-
- Blog posts
-
-
-
-
-
- Plans & Billing
-
-
-
- Subscriptions
-
-
- Billing
-
-
- Invoices
-
-
-
-
-
- Settings
-
-
-
- Profile
-
-
- Team
-
-
- Sign out
-
-
-
-
-
-
-
- ),
-};
+ render: (args) => ,
+}
diff --git a/src/components/navigation/navigation.test.tsx b/src/components/navigation/navigation.test.tsx
new file mode 100644
index 00000000..c263e9ba
--- /dev/null
+++ b/src/components/navigation/navigation.test.tsx
@@ -0,0 +1,40 @@
+import { describe, expect, it } from 'vitest'
+import { fireEvent, render, screen } from '@testing-library/react'
+import { Navigation } from './navigation'
+
+describe('Navigation', () => {
+ it('renders a logo and a group item', () => {
+ render(
+
+
+ Brand
+
+
+ Home
+
+ ,
+ )
+
+ expect(screen.getByText('Brand')).toBeInTheDocument()
+ expect(screen.getByText('Home')).toBeInTheDocument()
+ })
+
+ it('renders a disclosure panel item', () => {
+ render(
+
+
+ Settings
+
+
+ Profile
+
+
+
+ ,
+ )
+
+ const trigger = screen.getByRole('button', { name: 'Settings' })
+ fireEvent.click(trigger)
+ expect(screen.getByText('Profile')).toBeInTheDocument()
+ })
+})
diff --git a/src/components/navigation/navigation.tsx b/src/components/navigation/navigation.tsx
index 15eab132..cde66ce0 100644
--- a/src/components/navigation/navigation.tsx
+++ b/src/components/navigation/navigation.tsx
@@ -1,31 +1,31 @@
-import React from "react";
-import { NavigationDisclosure } from "./navigation-disclosure";
-import { NavigationGroup } from "./navigation-group";
-import { NavigationPopover } from "./navigation-popover";
+import type { ReactNode } from 'react'
+import { NavigationDisclosure } from './navigation-disclosure'
+import { NavigationGroup } from './navigation-group'
+import { NavigationPopover } from './navigation-popover'
export interface NavigationLogoProps {
- children: React.ReactNode;
+ children: ReactNode
}
const NavigationLogo = ({ children }: NavigationLogoProps) => {
- return {children}
;
-};
+ return {children}
+}
export interface NavigationProps {
- children: React.ReactNode;
+ children: ReactNode
}
const Navigation = ({ children }: NavigationProps) => {
- return (
-
- {children}
-
- );
-};
+ return (
+
+ {children}
+
+ )
+}
-Navigation.Logo = NavigationLogo;
-Navigation.Group = NavigationGroup;
-Navigation.Disclosure = NavigationDisclosure;
-Navigation.Popover = NavigationPopover;
+Navigation.Logo = NavigationLogo
+Navigation.Group = NavigationGroup
+Navigation.Disclosure = NavigationDisclosure
+Navigation.Popover = NavigationPopover
-export { Navigation };
+export { Navigation }
diff --git a/src/components/page/index.ts b/src/components/page/index.ts
index 57b51d75..e760f7e1 100644
--- a/src/components/page/index.ts
+++ b/src/components/page/index.ts
@@ -1 +1 @@
-export { Page } from "./page";
+export { Page } from './page'
diff --git a/src/components/page/page-description.tsx b/src/components/page/page-description.tsx
index 28ff0fe9..75df055a 100644
--- a/src/components/page/page-description.tsx
+++ b/src/components/page/page-description.tsx
@@ -1,9 +1,9 @@
-import React from "react";
+import type { ReactNode } from 'react'
export interface PageDescriptionProps {
- children: React.ReactNode;
+ children: ReactNode
}
export const PageDescription = ({ children }: PageDescriptionProps) => {
- return {children}
;
-};
+ return {children}
+}
diff --git a/src/components/page/page-title.tsx b/src/components/page/page-title.tsx
index 40cc55bf..b00f5477 100644
--- a/src/components/page/page-title.tsx
+++ b/src/components/page/page-title.tsx
@@ -1,9 +1,9 @@
-import React from "react";
+import type { ReactNode } from 'react'
export interface TitleProps {
- children: React.ReactNode;
+ children: ReactNode
}
export const PageTitle = ({ children }: TitleProps) => {
- return {children} ;
-};
+ return {children}
+}
diff --git a/src/components/page/page.stories.tsx b/src/components/page/page.stories.tsx
index 897a8b45..f5db7ca0 100644
--- a/src/components/page/page.stories.tsx
+++ b/src/components/page/page.stories.tsx
@@ -1,20 +1,27 @@
-import type { Meta, StoryObj } from "@storybook/react";
-import React from "react";
-import { Page } from "./page";
+import type { Meta, StoryObj } from '@storybook/react-vite'
+import { Page } from './page'
const meta: Meta = {
- title: "Page",
- component: Page,
-};
+ title: 'Page',
+ component: Page,
+ args: {
+ title: 'Page Title',
+ description: 'Description',
+ },
+ argTypes: {
+ title: { control: 'text' },
+ description: { control: 'text' },
+ },
+}
-export default meta;
-type Story = StoryObj;
+export default meta
+type Story = StoryObj
export const Default: Story = {
- render: () => (
-
- Page Title
- Description
-
- ),
-};
+ render: ({ title, description }) => (
+
+ {title}
+ {description}
+
+ ),
+}
diff --git a/src/components/page/page.test.tsx b/src/components/page/page.test.tsx
new file mode 100644
index 00000000..2bdca391
--- /dev/null
+++ b/src/components/page/page.test.tsx
@@ -0,0 +1,17 @@
+import { describe, expect, it } from 'vitest'
+import { render, screen } from '@testing-library/react'
+import { Page } from './page'
+
+describe('Page', () => {
+ it('renders title and description slots', () => {
+ render(
+
+ Dashboard
+ Overview
+ ,
+ )
+
+ expect(screen.getByText('Dashboard')).toBeInTheDocument()
+ expect(screen.getByText('Overview')).toBeInTheDocument()
+ })
+})
diff --git a/src/components/page/page.tsx b/src/components/page/page.tsx
index f52f412b..8b4702dc 100644
--- a/src/components/page/page.tsx
+++ b/src/components/page/page.tsx
@@ -1,16 +1,16 @@
-import React from "react";
-import { PageDescription } from "./page-description";
-import { PageTitle } from "./page-title";
+import type { ReactNode } from 'react'
+import { PageDescription } from './page-description'
+import { PageTitle } from './page-title'
interface PageProps {
- children: React.ReactNode;
+ children: ReactNode
}
const Page = ({ children }: PageProps) => {
- return {children}
;
-};
+ return {children}
+}
-Page.Title = PageTitle;
-Page.Description = PageDescription;
+Page.Title = PageTitle
+Page.Description = PageDescription
-export { Page };
+export { Page }
diff --git a/src/components/panel/index.ts b/src/components/panel/index.ts
index 8628fc58..b400da84 100644
--- a/src/components/panel/index.ts
+++ b/src/components/panel/index.ts
@@ -1 +1 @@
-export { Panel } from "./panel";
+export { Panel } from './panel'
diff --git a/src/components/panel/panel.stories.tsx b/src/components/panel/panel.stories.tsx
index d429a275..270f0129 100644
--- a/src/components/panel/panel.stories.tsx
+++ b/src/components/panel/panel.stories.tsx
@@ -1,44 +1,45 @@
-import type { Meta, StoryObj } from "@storybook/react";
-import React from "react";
-import { Panel } from "./panel";
-import { Button } from "../button/button";
-import { getStoryDescription } from "../../util/storybook-utils";
-import { Toggle } from "../toggle";
+import type { Meta, StoryObj } from '@storybook/react-vite'
+import { Panel } from './panel'
+import { Button } from '../button/button'
+import { getStoryDescription } from '../../util/storybook-utils'
+import { Toggle } from '../toggle'
const meta: Meta = {
- title: "Panel",
- parameters: {
- ...getStoryDescription("Simple container used to group and organize elements in the UI."),
- backgrounds: {
- default: "light",
- },
+ title: 'Panel',
+ parameters: {
+ ...getStoryDescription(
+ 'Simple container used to group and organize elements in the UI.',
+ ),
+ backgrounds: {
+ default: 'light',
},
- component: Panel,
- args: {
- className: "",
- children: "Panel with text content",
- },
-};
+ },
+ component: Panel,
+ args: {
+ className: '',
+ children: 'Panel with text content',
+ },
+}
-export default meta;
-type Story = StoryObj;
+export default meta
+type Story = StoryObj
-export const Default: Story = {};
+export const Default: Story = {}
-const noop = () => undefined;
+const noop = () => undefined
export const WithComponents: Story = {
- args: {
- children: (
- <>
-
- Button A
-
-
-
- Button B
-
- Paragraph content
- >
- ),
- },
-};
+ args: {
+ children: (
+ <>
+
+ Button A
+
+
+
+ Button B
+
+ Paragraph content
+ >
+ ),
+ },
+}
diff --git a/src/components/panel/panel.test.tsx b/src/components/panel/panel.test.tsx
new file mode 100644
index 00000000..45921405
--- /dev/null
+++ b/src/components/panel/panel.test.tsx
@@ -0,0 +1,15 @@
+import { describe, expect, it } from 'vitest'
+import { render, screen } from '@testing-library/react'
+import { Panel } from './panel'
+
+describe('Panel', () => {
+ it('renders panel content', () => {
+ const { container } = render(Panel body )
+
+ const content = screen.getByText('Panel body')
+ const panel = container.firstElementChild
+
+ expect(content).toBeInTheDocument()
+ expect(panel).toHaveClass('bg-neutral-0')
+ })
+})
diff --git a/src/components/panel/panel.tsx b/src/components/panel/panel.tsx
index 84d711e7..7a9b4311 100644
--- a/src/components/panel/panel.tsx
+++ b/src/components/panel/panel.tsx
@@ -1,13 +1,18 @@
-import React, { FC, ReactNode } from "react";
-import { classNames } from "../../util/class-names";
+import { FC, ReactNode } from 'react'
+import { classNames } from '../../util/class-names'
interface PanelProps {
- children: ReactNode;
- className?: string;
+ children: ReactNode
+ className?: string
}
export const Panel: FC = ({ children, className }) => (
-
- {children}
-
-);
+
+ {children}
+
+)
diff --git a/src/components/popover-menu/index.ts b/src/components/popover-menu/index.ts
index e6eeeaa5..9b0d08ed 100644
--- a/src/components/popover-menu/index.ts
+++ b/src/components/popover-menu/index.ts
@@ -1 +1 @@
-export { PopoverMenu } from "./popover-menu";
+export { PopoverMenu } from './popover-menu'
diff --git a/src/components/popover-menu/popover-menu-button.tsx b/src/components/popover-menu/popover-menu-button.tsx
index 021f0e4b..0196889f 100644
--- a/src/components/popover-menu/popover-menu-button.tsx
+++ b/src/components/popover-menu/popover-menu-button.tsx
@@ -1,21 +1,46 @@
-import { PopoverButton as HeadlessUiPopoverButton } from "@headlessui/react";
-import React from "react";
-import { Button, ButtonProps } from "../button/button";
-import { usePopoverMenuContext } from "./popover-menu-context";
+import type { ReactNode } from 'react'
+import { PopoverButton as HeadlessUiPopoverButton } from '@headlessui/react'
+import {
+ ButtonProps,
+ getButtonClassName,
+ getButtonIconClassName,
+} from '../button/button'
+import { Spinner } from '../spinner'
+import { usePopoverMenuContext } from './popover-menu-context'
export interface PopoverMenuButtonProps extends ButtonProps {
- children: React.ReactNode;
- onClick?: () => void;
+ children: ReactNode
}
-export const PopoverMenuButton = ({ onClick, children, ...restProps }: PopoverMenuButtonProps) => {
- const {
- popoverButton: { setReferenceElement },
- } = usePopoverMenuContext();
+export const PopoverMenuButton = ({
+ variant = 'primary',
+ className,
+ children,
+ loading,
+ LeftIcon,
+ RightIcon,
+ ...restProps
+}: PopoverMenuButtonProps) => {
+ const {
+ popoverButton: { setReferenceElement },
+ } = usePopoverMenuContext()
- return (
- el && setReferenceElement(el)} onClick={onClick}>
- {children}
-
- );
-};
+ return (
+
+ {loading ?
+
+ : null}
+ {LeftIcon && !loading ?
+
+ : null}
+ {children}
+ {RightIcon ?
+
+ : null}
+
+ )
+}
diff --git a/src/components/popover-menu/popover-menu-context.ts b/src/components/popover-menu/popover-menu-context.ts
index 71fae18a..83b8d418 100644
--- a/src/components/popover-menu/popover-menu-context.ts
+++ b/src/components/popover-menu/popover-menu-context.ts
@@ -1,25 +1,23 @@
-import { CSSProperties, createContext, useContext } from "react";
+import { CSSProperties, createContext, useContext } from 'react'
export const PopoverMenuContext = createContext<{
- popoverButton: {
- setReferenceElement: React.Dispatch>;
- };
- popoverPanel: {
- setPopperElement: React.Dispatch>;
- styles: CSSProperties;
- attributes: { [key: string]: string } | undefined;
- };
+ popoverButton: {
+ setReferenceElement: (element: HTMLButtonElement | null) => void
+ }
+ popoverPanel: {
+ setFloatingElement: (element: HTMLElement | null) => void
+ styles: CSSProperties
+ }
}>({
- popoverButton: {
- setReferenceElement: () => {},
- },
- popoverPanel: {
- setPopperElement: () => {},
- styles: {},
- attributes: {},
- },
-});
+ popoverButton: {
+ setReferenceElement: () => {},
+ },
+ popoverPanel: {
+ setFloatingElement: () => {},
+ styles: {},
+ },
+})
-export const usePopoverMenuContext = () => useContext(PopoverMenuContext);
+export const usePopoverMenuContext = () => useContext(PopoverMenuContext)
-export const PopoverMenuContextProvider = PopoverMenuContext.Provider;
+export const PopoverMenuContextProvider = PopoverMenuContext.Provider
diff --git a/src/components/popover-menu/popover-menu-overlay.tsx b/src/components/popover-menu/popover-menu-overlay.tsx
index 815c0195..fb1cf145 100644
--- a/src/components/popover-menu/popover-menu-overlay.tsx
+++ b/src/components/popover-menu/popover-menu-overlay.tsx
@@ -1,6 +1,5 @@
-import { PopoverBackdrop as HeadlessUiPopoverBackdrop } from "@headlessui/react";
-import * as React from "react";
+import { PopoverBackdrop as HeadlessUiPopoverBackdrop } from '@headlessui/react'
export const PopoverMenuOverlay = () => (
-
-);
+
+)
diff --git a/src/components/popover-menu/popover-menu-panel-button.tsx b/src/components/popover-menu/popover-menu-panel-button.tsx
index bfc659ae..3ffac617 100644
--- a/src/components/popover-menu/popover-menu-panel-button.tsx
+++ b/src/components/popover-menu/popover-menu-panel-button.tsx
@@ -1,49 +1,51 @@
-import React from "react";
-import { PopoverButton as HeadlessUiPopoverButton } from "@headlessui/react";
-import { classNames } from "../../util/class-names";
+import type { ComponentType } from 'react'
+import { ReactNode } from 'react'
+import { PopoverButton as HeadlessUiPopoverButton } from '@headlessui/react'
+import { classNames } from '../../util/class-names'
const itemIntents = {
- neutral: "text-neutral-700 fill-neutral-700 hover:bg-neutral-100",
- danger: "text-danger-500 fill-danger-500 hover:bg-danger-100",
-};
+ neutral: 'text-neutral-700 fill-neutral-700 hover:bg-neutral-100',
+ danger: 'text-danger-500 fill-danger-500 hover:bg-danger-100',
+}
const activeItemIntents = {
- neutral: "bg-primary-100 fill-primary-400 text-primary-400 before:bg-primary-400",
- danger: "bg-danger-100 fill-danger-400 text-danger-500 before:bg-danger-400",
-};
+ neutral:
+ 'bg-primary-100 fill-primary-400 text-primary-400 before:bg-primary-400',
+ danger: 'bg-danger-100 fill-danger-400 text-danger-500 before:bg-danger-400',
+}
export interface PopoverMenuPanelButtonProps {
- children: React.ReactNode;
- onClick?: () => void;
- Icon?: React.ComponentType<{ className: string }>;
- variant?: keyof typeof itemIntents;
- selected?: boolean;
- disabled?: boolean;
+ children: ReactNode
+ onClick?: () => void
+ Icon?: ComponentType<{ className: string }>
+ variant?: keyof typeof itemIntents
+ selected?: boolean
+ disabled?: boolean
}
export const PopoverMenuPanelButton = ({
- children,
- onClick,
- Icon,
- variant = "neutral",
- selected,
- disabled,
+ children,
+ onClick,
+ Icon,
+ variant = 'neutral',
+ selected,
+ disabled,
}: PopoverMenuPanelButtonProps) => {
- return (
-
- {Icon && }
- {children}
-
- );
-};
+ return (
+
+ {Icon && }
+ {children}
+
+ )
+}
diff --git a/src/components/popover-menu/popover-menu-panel-divider.tsx b/src/components/popover-menu/popover-menu-panel-divider.tsx
index e4d70002..791003f6 100644
--- a/src/components/popover-menu/popover-menu-panel-divider.tsx
+++ b/src/components/popover-menu/popover-menu-panel-divider.tsx
@@ -1,5 +1,3 @@
-import React from "react";
-
export const PopoverMenuPanelDivider = () => {
- return
;
-};
+ return
+}
diff --git a/src/components/popover-menu/popover-menu-panel-group.tsx b/src/components/popover-menu/popover-menu-panel-group.tsx
index 1b93789d..95b64d1f 100644
--- a/src/components/popover-menu/popover-menu-panel-group.tsx
+++ b/src/components/popover-menu/popover-menu-panel-group.tsx
@@ -1,9 +1,9 @@
-import React from "react";
+import type { ReactNode } from 'react'
export interface PopoverMenuGroupProps {
- children: React.ReactNode;
+ children: ReactNode
}
export const PopoverMenuPanelGroup = ({ children }: PopoverMenuGroupProps) => {
- return {children}
;
-};
+ return {children}
+}
diff --git a/src/components/popover-menu/popover-menu-panel-item.tsx b/src/components/popover-menu/popover-menu-panel-item.tsx
index d8561aec..7791f2b6 100644
--- a/src/components/popover-menu/popover-menu-panel-item.tsx
+++ b/src/components/popover-menu/popover-menu-panel-item.tsx
@@ -1,52 +1,54 @@
-import React from "react";
-import { classNames } from "../../util/class-names";
+import type { ComponentType } from 'react'
+import { ReactNode } from 'react'
+import { classNames } from '../../util/class-names'
const itemIntents = {
- neutral: "text-neutral-700 fill-neutral-700 hover:bg-neutral-100",
- danger: "text-danger-500 fill-danger-500 hover:bg-danger-50",
-};
+ neutral: 'text-neutral-700 fill-neutral-700 hover:bg-neutral-100',
+ danger: 'text-danger-500 fill-danger-500 hover:bg-danger-50',
+}
const selectedItemIntents = {
- neutral:
- "bg-primary-100 fill-primary-400 text-primary-400 before:bg-primary-400 hover:text-primary-400 hover:fill-primary-400 hover:bg-primary-100",
- danger: "bg-danger-100 fill-danger-700 text-danger-500 before:bg-danger-400 hover:text-danger-500 hover:fill-danger-400 hover:bg-danger-100",
-};
+ neutral:
+ 'bg-primary-100 fill-primary-400 text-primary-400 before:bg-primary-400 hover:text-primary-400 hover:fill-primary-400 hover:bg-primary-100',
+ danger:
+ 'bg-danger-100 fill-danger-700 text-danger-500 before:bg-danger-400 hover:text-danger-500 hover:fill-danger-400 hover:bg-danger-100',
+}
export interface PopoverMenuPanelItemProps {
- children: React.ReactNode;
- onClick?: () => void;
- Icon?: React.ComponentType<{ className: string }>;
- variant?: keyof typeof itemIntents;
- selected?: boolean;
- disabled?: boolean;
+ children: ReactNode
+ onClick?: () => void
+ Icon?: ComponentType<{ className: string }>
+ variant?: keyof typeof itemIntents
+ selected?: boolean
+ disabled?: boolean
}
export const PopoverMenuPanelItem = ({
- children,
- onClick,
- Icon,
- variant = "neutral",
- selected,
- disabled,
+ children,
+ onClick,
+ Icon,
+ variant = 'neutral',
+ selected,
+ disabled,
}: PopoverMenuPanelItemProps) => {
- return (
-
- {Icon && }
- {children}
-
- );
-};
+ return (
+
+ {Icon && }
+ {children}
+
+ )
+}
diff --git a/src/components/popover-menu/popover-menu-panel-title.tsx b/src/components/popover-menu/popover-menu-panel-title.tsx
index c14ee618..562fadaf 100644
--- a/src/components/popover-menu/popover-menu-panel-title.tsx
+++ b/src/components/popover-menu/popover-menu-panel-title.tsx
@@ -1,11 +1,15 @@
-import React from "react";
+import type { ReactNode } from 'react'
export interface PopoverMenuPanelTitleProps {
- children: React.ReactNode;
+ children: ReactNode
}
-export const PopoverMenuPanelTitle = ({ children }: PopoverMenuPanelTitleProps) => {
- return (
- {children}
- );
-};
+export const PopoverMenuPanelTitle = ({
+ children,
+}: PopoverMenuPanelTitleProps) => {
+ return (
+
+ {children}
+
+ )
+}
diff --git a/src/components/popover-menu/popover-menu-panel.tsx b/src/components/popover-menu/popover-menu-panel.tsx
index 4bb3e5d1..e3d6ebe1 100644
--- a/src/components/popover-menu/popover-menu-panel.tsx
+++ b/src/components/popover-menu/popover-menu-panel.tsx
@@ -1,41 +1,40 @@
+import type { ReactNode } from 'react'
import {
- PopoverPanel as HeadlessUiPopoverPanel,
- PopoverPanelProps as HeadlessUiPopoverPanelProps,
-} from "@headlessui/react";
-import React from "react";
-import { usePopoverMenuContext } from "./popover-menu-context";
-import { PopoverMenuPanelButton } from "./popover-menu-panel-button";
-import { PopoverMenuPanelDivider } from "./popover-menu-panel-divider";
-import { PopoverMenuPanelGroup } from "./popover-menu-panel-group";
-import { PopoverMenuPanelItem } from "./popover-menu-panel-item";
-import { PopoverMenuPanelTitle } from "./popover-menu-panel-title";
+ PopoverPanel as HeadlessUiPopoverPanel,
+ PopoverPanelProps as HeadlessUiPopoverPanelProps,
+} from '@headlessui/react'
+import { usePopoverMenuContext } from './popover-menu-context'
+import { PopoverMenuPanelButton } from './popover-menu-panel-button'
+import { PopoverMenuPanelDivider } from './popover-menu-panel-divider'
+import { PopoverMenuPanelGroup } from './popover-menu-panel-group'
+import { PopoverMenuPanelItem } from './popover-menu-panel-item'
+import { PopoverMenuPanelTitle } from './popover-menu-panel-title'
export interface PopoverMenuPanelProps extends HeadlessUiPopoverPanelProps {
- children: React.ReactNode;
+ children: ReactNode
}
const PopoverMenuPanel = ({ children, ...rest }: PopoverMenuPanelProps) => {
- const {
- popoverPanel: { setPopperElement, styles, attributes },
- } = usePopoverMenuContext();
+ const {
+ popoverPanel: { setFloatingElement, styles },
+ } = usePopoverMenuContext()
- return (
- el && setPopperElement(el)}
- style={styles}
- {...attributes}
- className="bg-neutral-0 z-40 w-52 rounded-sm py-2 shadow-sm"
- {...rest}
- >
- {children}
-
- );
-};
+ return (
+
+ {children}
+
+ )
+}
-PopoverMenuPanel.Item = PopoverMenuPanelItem;
-PopoverMenuPanel.Button = PopoverMenuPanelButton;
-PopoverMenuPanel.Group = PopoverMenuPanelGroup;
-PopoverMenuPanel.Divider = PopoverMenuPanelDivider;
-PopoverMenuPanel.Title = PopoverMenuPanelTitle;
+PopoverMenuPanel.Item = PopoverMenuPanelItem
+PopoverMenuPanel.Button = PopoverMenuPanelButton
+PopoverMenuPanel.Group = PopoverMenuPanelGroup
+PopoverMenuPanel.Divider = PopoverMenuPanelDivider
+PopoverMenuPanel.Title = PopoverMenuPanelTitle
-export { PopoverMenuPanel };
+export { PopoverMenuPanel }
diff --git a/src/components/popover-menu/popover-menu.stories.tsx b/src/components/popover-menu/popover-menu.stories.tsx
index f288f039..338e9e0f 100644
--- a/src/components/popover-menu/popover-menu.stories.tsx
+++ b/src/components/popover-menu/popover-menu.stories.tsx
@@ -1,60 +1,104 @@
-import type { Meta, StoryObj } from "@storybook/react";
-import React from "react";
-import { ChatIcon, DeleteIcon, EditIcon } from "../../icons";
-import { PopoverMenu } from "./popover-menu";
-
-const meta: Meta = {
- title: "Popover Menu",
- component: PopoverMenu,
- parameters: {
- options: {
- showPanel: false,
- },
+import { useEffect, useState } from 'react'
+import type { Meta, StoryObj } from '@storybook/react-vite'
+import { ChatIcon, DeleteIcon, EditIcon } from '../../icons'
+import { PopoverMenu } from './popover-menu'
+
+type PopoverMenuStoryArgs = {
+ buttonLabel: string
+ title: string
+ itemLabel: string
+ supportLabel: string
+ dangerTitle: string
+ dangerButtonLabel: string
+ initialSelected: boolean
+}
+
+const PopoverMenuStory = ({
+ buttonLabel,
+ title,
+ itemLabel,
+ supportLabel,
+ dangerTitle,
+ dangerButtonLabel,
+ initialSelected,
+}: PopoverMenuStoryArgs) => {
+ const [isActive, setIsActive] = useState(initialSelected)
+
+ useEffect(() => {
+ setIsActive(initialSelected)
+ }, [initialSelected])
+
+ return (
+
+
+
+ {buttonLabel}
+
+
+
+
+
+ {title}
+
+ setIsActive(!isActive)}
+ >
+ {itemLabel}
+
+
+
+ {supportLabel}
+
+
+
+
+
+ {dangerTitle}
+
+
+ {dangerButtonLabel}
+
+
+
+
+
+ )
+}
+
+const meta: Meta = {
+ title: 'Popover Menu',
+ component: PopoverMenuStory,
+ args: {
+ buttonLabel: 'Open Popover Menu',
+ title: 'You',
+ itemLabel: 'Activate Mfa',
+ supportLabel: 'Support',
+ dangerTitle: 'Danger Zone',
+ dangerButtonLabel: 'Close Dialog',
+ initialSelected: false,
+ },
+ argTypes: {
+ buttonLabel: { control: 'text' },
+ title: { control: 'text' },
+ itemLabel: { control: 'text' },
+ supportLabel: { control: 'text' },
+ dangerTitle: { control: 'text' },
+ dangerButtonLabel: { control: 'text' },
+ initialSelected: { control: 'boolean' },
+ },
+ parameters: {
+ options: {
+ showPanel: false,
},
-};
+ },
+}
-export default meta;
+export default meta
-type Story = StoryObj;
+type Story = StoryObj
export const Default: Story = {
- render: () => {
- const [isActive, setIsActive] = React.useState(false);
-
- return (
-
-
- Open Popover Menu
-
-
-
-
- You
-
- setIsActive(!isActive)}
- >
- Activate Mfa
-
-
-
- Support
-
-
-
-
-
- Danger Zone
-
-
- Close Dialog
-
-
-
-
-
- );
- },
-};
+ render: (args) => ,
+}
diff --git a/src/components/popover-menu/popover-menu.test.tsx b/src/components/popover-menu/popover-menu.test.tsx
new file mode 100644
index 00000000..6f528722
--- /dev/null
+++ b/src/components/popover-menu/popover-menu.test.tsx
@@ -0,0 +1,17 @@
+import { describe, expect, it } from 'vitest'
+import { render, screen } from '@testing-library/react'
+import { PopoverMenu } from './popover-menu'
+
+describe('PopoverMenu', () => {
+ it('renders the popover trigger', () => {
+ render(
+
+
+ Open Popover
+
+ ,
+ )
+
+ expect(screen.getByText('Open Popover')).toBeInTheDocument()
+ })
+})
diff --git a/src/components/popover-menu/popover-menu.tsx b/src/components/popover-menu/popover-menu.tsx
index b4cbd67c..e096f6a9 100644
--- a/src/components/popover-menu/popover-menu.tsx
+++ b/src/components/popover-menu/popover-menu.tsx
@@ -1,42 +1,40 @@
-import { Popover, PopoverProps } from "@headlessui/react";
-import React, { useState } from "react";
-import { usePopper } from "react-popper";
-import { PopoverMenuButton } from "./popover-menu-button";
-import { PopoverMenuContextProvider } from "./popover-menu-context";
-import { PopoverMenuOverlay } from "./popover-menu-overlay";
-import { PopoverMenuPanel } from "./popover-menu-panel";
+import type { ReactNode } from 'react'
+import { Popover, PopoverProps } from '@headlessui/react'
+import { autoUpdate, useFloating } from '@floating-ui/react'
+import { PopoverMenuButton } from './popover-menu-button'
+import { PopoverMenuContextProvider } from './popover-menu-context'
+import { PopoverMenuOverlay } from './popover-menu-overlay'
+import { PopoverMenuPanel } from './popover-menu-panel'
export interface PopoverMenuProps extends PopoverProps {
- children: React.ReactNode;
+ children: ReactNode
}
const PopoverMenu = ({ children, ...rest }: PopoverMenuProps) => {
- const [referenceElement, setReferenceElement] = useState();
- const [popperElement, setPopperElement] = useState();
- const { styles, attributes } = usePopper(referenceElement, popperElement, {
- placement: "top-start",
- });
+ const { refs, floatingStyles } = useFloating({
+ placement: 'top-start',
+ whileElementsMounted: autoUpdate,
+ })
- const context = {
- popoverButton: {
- setReferenceElement,
- },
- popoverPanel: {
- setPopperElement,
- styles: styles.popper,
- attributes: attributes.popper,
- },
- };
+ const context = {
+ popoverButton: {
+ setReferenceElement: refs.setReference,
+ },
+ popoverPanel: {
+ setFloatingElement: refs.setFloating,
+ styles: floatingStyles,
+ },
+ }
- return (
-
- {children}
-
- );
-};
+ return (
+
+ {children}
+
+ )
+}
-PopoverMenu.Button = PopoverMenuButton;
-PopoverMenu.Panel = PopoverMenuPanel;
-PopoverMenu.Overlay = PopoverMenuOverlay;
+PopoverMenu.Button = PopoverMenuButton
+PopoverMenu.Panel = PopoverMenuPanel
+PopoverMenu.Overlay = PopoverMenuOverlay
-export { PopoverMenu };
+export { PopoverMenu }
diff --git a/src/components/section/index.ts b/src/components/section/index.ts
index 2e37b756..cbdd5c30 100644
--- a/src/components/section/index.ts
+++ b/src/components/section/index.ts
@@ -1 +1 @@
-export { Section } from "./section";
+export { Section } from './section'
diff --git a/src/components/section/section-description.tsx b/src/components/section/section-description.tsx
index 859f9485..b45aabaa 100644
--- a/src/components/section/section-description.tsx
+++ b/src/components/section/section-description.tsx
@@ -1,9 +1,9 @@
-import React from "react";
+import type { ReactNode } from 'react'
export interface DescriptionProps {
- children: React.ReactNode;
+ children: ReactNode
}
export const SectionDescription = ({ children }: DescriptionProps) => {
- return {children}
;
-};
+ return {children}
+}
diff --git a/src/components/section/section-panel.tsx b/src/components/section/section-panel.tsx
index 449058b8..551e5fc3 100644
--- a/src/components/section/section-panel.tsx
+++ b/src/components/section/section-panel.tsx
@@ -1,7 +1,7 @@
-import React from "react";
+import type { ReactNode } from 'react'
export interface SectionPanelProps {
- children: React.ReactNode;
+ children: ReactNode
}
/**
@@ -9,9 +9,9 @@ export interface SectionPanelProps {
* Delete me on version 3
*/
export const SectionPanel = ({ children }: SectionPanelProps) => {
- return (
-
- {children}
-
- );
-};
+ return (
+
+ {children}
+
+ )
+}
diff --git a/src/components/section/section-title-group.tsx b/src/components/section/section-title-group.tsx
index 0c5258d2..e6bea820 100644
--- a/src/components/section/section-title-group.tsx
+++ b/src/components/section/section-title-group.tsx
@@ -1,9 +1,9 @@
-import React from "react";
+import type { ReactNode } from 'react'
export interface SectionTitleGroupProps {
- children: React.ReactNode;
+ children: ReactNode
}
export const SectionTitleGroup = ({ children }: SectionTitleGroupProps) => {
- return {children}
;
-};
+ return {children}
+}
diff --git a/src/components/section/section-title.tsx b/src/components/section/section-title.tsx
index de86004e..af9bdfdd 100644
--- a/src/components/section/section-title.tsx
+++ b/src/components/section/section-title.tsx
@@ -1,9 +1,9 @@
-import React from "react";
+import type { ReactNode } from 'react'
export interface SectionTitleProps {
- children: React.ReactNode;
+ children: ReactNode
}
export const SectionTitle = ({ children }: SectionTitleProps) => {
- return {children} ;
-};
+ return {children}
+}
diff --git a/src/components/section/section.stories.tsx b/src/components/section/section.stories.tsx
index 5c2cb476..6cce268b 100644
--- a/src/components/section/section.stories.tsx
+++ b/src/components/section/section.stories.tsx
@@ -1,41 +1,52 @@
-import type { Meta, StoryObj } from "@storybook/react";
-import React from "react";
-import { Section } from "./section";
-import { Button } from "../button/button";
+import type { Meta, StoryObj } from '@storybook/react-vite'
+import { Section } from './section'
+import { Button } from '../button/button'
const meta: Meta = {
- title: "Section",
- component: Section,
-};
+ title: 'Section',
+ component: Section,
+ args: {
+ title: 'Section Title',
+ description: 'Description',
+ panelText: 'Place panel content here',
+ actionLabel: 'Button',
+ },
+ argTypes: {
+ title: { control: 'text' },
+ description: { control: 'text' },
+ panelText: { control: 'text' },
+ actionLabel: { control: 'text' },
+ },
+}
-export default meta;
-type Story = StoryObj;
+export default meta
+type Story = StoryObj
export const Default: Story = {
- render: () => (
-
-
- Section Title
- Description
-
- Place panel contet here
-
- ),
-};
+ render: ({ title, description, panelText }) => (
+
+
+ {title}
+ {description}
+
+ {panelText}
+
+ ),
+}
export const SectionWithAction: Story = {
- render: () => (
-
-
- Section Header
-
- Description
- alert("clicked")}>
- Button
-
-
-
- Place panel content here
-
- ),
-};
+ render: ({ title, description, panelText, actionLabel }) => (
+
+
+ {title}
+
+ {description}
+ alert('clicked')}>
+ {actionLabel}
+
+
+
+ {panelText}
+
+ ),
+}
diff --git a/src/components/section/section.test.tsx b/src/components/section/section.test.tsx
new file mode 100644
index 00000000..4007b526
--- /dev/null
+++ b/src/components/section/section.test.tsx
@@ -0,0 +1,17 @@
+import { describe, expect, it } from 'vitest'
+import { render, screen } from '@testing-library/react'
+import { Section } from './section'
+
+describe('Section', () => {
+ it('renders title and description slots', () => {
+ render(
+
+ Details
+ Extra context
+ ,
+ )
+
+ expect(screen.getByText('Details')).toBeInTheDocument()
+ expect(screen.getByText('Extra context')).toBeInTheDocument()
+ })
+})
diff --git a/src/components/section/section.tsx b/src/components/section/section.tsx
index 82c8a38f..a2c6a0b1 100644
--- a/src/components/section/section.tsx
+++ b/src/components/section/section.tsx
@@ -1,23 +1,23 @@
-import React from "react";
-import { SectionDescription } from "./section-description";
-import { SectionPanel } from "./section-panel";
-import { SectionTitle } from "./section-title";
-import { SectionTitleGroup } from "./section-title-group";
+import type { ReactNode } from 'react'
+import { SectionDescription } from './section-description'
+import { SectionPanel } from './section-panel'
+import { SectionTitle } from './section-title'
+import { SectionTitleGroup } from './section-title-group'
interface SectionProps {
- children: React.ReactNode;
+ children: ReactNode
}
const Section = ({ children }: SectionProps) => {
- return ;
-};
+ return
+}
-Section.TitleGroup = SectionTitleGroup;
-Section.Title = SectionTitle;
-Section.Description = SectionDescription;
+Section.TitleGroup = SectionTitleGroup
+Section.Title = SectionTitle
+Section.Description = SectionDescription
/**
* @deprecated Use the dedicated Panel component
*/
-Section.Panel = SectionPanel;
+Section.Panel = SectionPanel
-export { Section };
+export { Section }
diff --git a/src/components/sidebar-container/index.ts b/src/components/sidebar-container/index.ts
index 26da60a5..3d4752cf 100644
--- a/src/components/sidebar-container/index.ts
+++ b/src/components/sidebar-container/index.ts
@@ -1 +1 @@
-export { SidebarContainer } from "./sidebar-container";
+export { SidebarContainer } from './sidebar-container'
diff --git a/src/components/sidebar-container/sidebar-container.stories.tsx b/src/components/sidebar-container/sidebar-container.stories.tsx
index 87ad59cb..fe4e2fa4 100644
--- a/src/components/sidebar-container/sidebar-container.stories.tsx
+++ b/src/components/sidebar-container/sidebar-container.stories.tsx
@@ -1,20 +1,27 @@
-import React from "react";
-import type { Meta, StoryObj } from "@storybook/react";
-import { SidebarContainer } from "./sidebar-container";
+import type { Meta, StoryObj } from '@storybook/react-vite'
+import { SidebarContainer } from './sidebar-container'
const meta: Meta = {
- title: "SidebarContainer",
- component: SidebarContainer,
-};
+ title: 'SidebarContainer',
+ component: SidebarContainer,
+ args: {
+ sidebarLabel: 'Sidebar Content',
+ pageLabel: 'Page Content',
+ },
+ argTypes: {
+ sidebarLabel: { control: 'text' },
+ pageLabel: { control: 'text' },
+ },
+}
-export default meta;
-type Story = StoryObj;
+export default meta
+type Story = StoryObj
export const Header: Story = {
- render: () => (
- Sidebar Content }
- pageContent={Page Content
}
- />
- ),
-};
+ render: ({ sidebarLabel, pageLabel }) => (
+ {sidebarLabel} }
+ pageContent={{pageLabel}
}
+ />
+ ),
+}
diff --git a/src/components/sidebar-container/sidebar-container.test.tsx b/src/components/sidebar-container/sidebar-container.test.tsx
new file mode 100644
index 00000000..60fddd8b
--- /dev/null
+++ b/src/components/sidebar-container/sidebar-container.test.tsx
@@ -0,0 +1,17 @@
+import { describe, expect, it } from 'vitest'
+import { render, screen } from '@testing-library/react'
+import { SidebarContainer } from './sidebar-container'
+
+describe('SidebarContainer', () => {
+ it('renders sidebar and page content areas', () => {
+ render(
+ Sidebar}
+ pageContent={Page content }
+ />,
+ )
+
+ expect(screen.getByText('Sidebar')).toBeInTheDocument()
+ expect(screen.getByText('Page content')).toBeInTheDocument()
+ })
+})
diff --git a/src/components/sidebar-container/sidebar-container.tsx b/src/components/sidebar-container/sidebar-container.tsx
index 7a18def6..620501ff 100644
--- a/src/components/sidebar-container/sidebar-container.tsx
+++ b/src/components/sidebar-container/sidebar-container.tsx
@@ -1,17 +1,20 @@
-import React from "react";
+import type { ReactNode } from 'react'
interface SidebarContainerProps {
- sidebarContent: React.ReactNode;
- pageContent: React.ReactNode;
+ sidebarContent: ReactNode
+ pageContent: ReactNode
}
-export const SidebarContainer = ({ sidebarContent, pageContent }: SidebarContainerProps) => {
- return (
-
- );
-};
+export const SidebarContainer = ({
+ sidebarContent,
+ pageContent,
+}: SidebarContainerProps) => {
+ return (
+
+ )
+}
diff --git a/src/components/sidebar/index.ts b/src/components/sidebar/index.ts
index d37b3aa0..32ab12e2 100644
--- a/src/components/sidebar/index.ts
+++ b/src/components/sidebar/index.ts
@@ -1 +1 @@
-export { Sidebar } from "./sidebar";
+export { Sidebar } from './sidebar'
diff --git a/src/components/sidebar/sidebar-header/sidebar-header.tsx b/src/components/sidebar/sidebar-header/sidebar-header.tsx
index 664eeb9c..3a90c181 100644
--- a/src/components/sidebar/sidebar-header/sidebar-header.tsx
+++ b/src/components/sidebar/sidebar-header/sidebar-header.tsx
@@ -1,9 +1,9 @@
-import React from "react";
+import type { ReactNode } from 'react'
export interface SidebarHeaderProps {
- children: React.ReactNode;
+ children: ReactNode
}
export const SidebarHeader = ({ children }: SidebarHeaderProps) => {
- return {children} ;
-};
+ return {children}
+}
diff --git a/src/components/sidebar/sidebar-menu/sidebar-menu-header/sidebar-menu-header.tsx b/src/components/sidebar/sidebar-menu/sidebar-menu-header/sidebar-menu-header.tsx
index 76fbcc9c..1ce0c87f 100644
--- a/src/components/sidebar/sidebar-menu/sidebar-menu-header/sidebar-menu-header.tsx
+++ b/src/components/sidebar/sidebar-menu/sidebar-menu-header/sidebar-menu-header.tsx
@@ -1,13 +1,13 @@
-import React from "react";
+import type { ReactNode } from 'react'
export interface SidebarMenuHeaderProps {
- children: React.ReactNode;
+ children: ReactNode
}
export const SidebarMenuHeader = ({ children }: SidebarMenuHeaderProps) => {
- return (
-
- {children}
-
- );
-};
+ return (
+
+ {children}
+
+ )
+}
diff --git a/src/components/sidebar/sidebar-menu/sidebar-menu-link/sidebar-menu-link.tsx b/src/components/sidebar/sidebar-menu/sidebar-menu-link/sidebar-menu-link.tsx
index 971ea1c2..94a0f24d 100644
--- a/src/components/sidebar/sidebar-menu/sidebar-menu-link/sidebar-menu-link.tsx
+++ b/src/components/sidebar/sidebar-menu/sidebar-menu-link/sidebar-menu-link.tsx
@@ -1,21 +1,24 @@
-import React from "react";
-import { classNames } from "../../../../util/class-names";
+import type { ReactNode } from 'react'
+import { classNames } from '../../../../util/class-names'
export interface SidebarMenuLinkProps {
- isActive?: boolean;
- children: React.ReactNode;
+ isActive?: boolean
+ children: ReactNode
}
-export const SidebarMenuLink = ({ isActive, children }: SidebarMenuLinkProps) => {
- return (
-
- {children}
-
- );
-};
+export const SidebarMenuLink = ({
+ isActive,
+ children,
+}: SidebarMenuLinkProps) => {
+ return (
+
+ {children}
+
+ )
+}
diff --git a/src/components/sidebar/sidebar-menu/sidebar-menu.tsx b/src/components/sidebar/sidebar-menu/sidebar-menu.tsx
index 808afac5..6592ee10 100644
--- a/src/components/sidebar/sidebar-menu/sidebar-menu.tsx
+++ b/src/components/sidebar/sidebar-menu/sidebar-menu.tsx
@@ -1,9 +1,9 @@
-import React from "react";
+import type { ReactNode } from 'react'
export interface SidebarMenuProps {
- children: React.ReactNode;
+ children: ReactNode
}
export const SidebarMenu = ({ children }: SidebarMenuProps) => {
- return {children}
;
-};
+ return {children}
+}
diff --git a/src/components/sidebar/sidebar.stories.tsx b/src/components/sidebar/sidebar.stories.tsx
index e2d00caf..4fe5ea3d 100644
--- a/src/components/sidebar/sidebar.stories.tsx
+++ b/src/components/sidebar/sidebar.stories.tsx
@@ -1,21 +1,26 @@
-import React from "react";
-import type { Meta, StoryObj } from "@storybook/react";
-import { Sidebar } from "./sidebar";
+import type { Meta, StoryObj } from '@storybook/react-vite'
+import { Sidebar } from './sidebar'
const meta: Meta = {
- title: "Sidebar",
- component: Sidebar,
-};
+ title: 'Sidebar',
+ component: Sidebar,
+ args: {
+ label: 'Example',
+ },
+ argTypes: {
+ label: { control: 'text' },
+ },
+}
-export default meta;
-type Story = StoryObj;
+export default meta
+type Story = StoryObj
export const Header: Story = {
- render: () => Example ,
-};
+ render: ({ label }) => {label} ,
+}
export const Menu: Story = {
- render: () => Example ,
-};
+ render: ({ label }) => {label} ,
+}
export const MenuHeader: Story = {
- render: () => Example ,
-};
+ render: ({ label }) => {label} ,
+}
diff --git a/src/components/sidebar/sidebar.test.tsx b/src/components/sidebar/sidebar.test.tsx
new file mode 100644
index 00000000..b0134b76
--- /dev/null
+++ b/src/components/sidebar/sidebar.test.tsx
@@ -0,0 +1,21 @@
+import { describe, expect, it } from 'vitest'
+import { render, screen } from '@testing-library/react'
+import { Sidebar } from './sidebar'
+
+describe('Sidebar', () => {
+ it('renders header and menu content', () => {
+ render(
+
+ Navigation
+
+ General
+ Dashboard
+
+ ,
+ )
+
+ expect(screen.getByText('Navigation')).toBeInTheDocument()
+ expect(screen.getByText('General')).toBeInTheDocument()
+ expect(screen.getByText('Dashboard')).toBeInTheDocument()
+ })
+})
diff --git a/src/components/sidebar/sidebar.tsx b/src/components/sidebar/sidebar.tsx
index c34c4039..b63d6733 100644
--- a/src/components/sidebar/sidebar.tsx
+++ b/src/components/sidebar/sidebar.tsx
@@ -1,19 +1,19 @@
-import React from "react";
-import { SidebarHeader } from "./sidebar-header/sidebar-header";
-import { SidebarMenu } from "./sidebar-menu/sidebar-menu";
-import { SidebarMenuHeader } from "./sidebar-menu/sidebar-menu-header/sidebar-menu-header";
-import { SidebarMenuLink } from "./sidebar-menu/sidebar-menu-link/sidebar-menu-link";
+import type { ReactNode } from 'react'
+import { SidebarHeader } from './sidebar-header/sidebar-header'
+import { SidebarMenu } from './sidebar-menu/sidebar-menu'
+import { SidebarMenuHeader } from './sidebar-menu/sidebar-menu-header/sidebar-menu-header'
+import { SidebarMenuLink } from './sidebar-menu/sidebar-menu-link/sidebar-menu-link'
interface SidebarProps {
- children: React.ReactNode;
+ children: ReactNode
}
const Sidebar = ({ children }: SidebarProps) => {
- return {children}
;
-};
+ return {children}
+}
-Sidebar.Header = SidebarHeader;
-Sidebar.Menu = SidebarMenu;
-Sidebar.MenuHeader = SidebarMenuHeader;
-Sidebar.MenuLink = SidebarMenuLink;
+Sidebar.Header = SidebarHeader
+Sidebar.Menu = SidebarMenu
+Sidebar.MenuHeader = SidebarMenuHeader
+Sidebar.MenuLink = SidebarMenuLink
-export { Sidebar };
+export { Sidebar }
diff --git a/src/components/sidesheet/index.ts b/src/components/sidesheet/index.ts
index 8b9c262a..8097c705 100644
--- a/src/components/sidesheet/index.ts
+++ b/src/components/sidesheet/index.ts
@@ -1 +1 @@
-export { Sidesheet } from "./sidesheet";
+export { Sidesheet } from './sidesheet'
diff --git a/src/components/sidesheet/sidesheet-panel-content.tsx b/src/components/sidesheet/sidesheet-panel-content.tsx
index 6e5a6bae..cae48547 100644
--- a/src/components/sidesheet/sidesheet-panel-content.tsx
+++ b/src/components/sidesheet/sidesheet-panel-content.tsx
@@ -1,9 +1,11 @@
-import React from "react";
+import type { ReactNode } from 'react'
export interface SidesheetPanelContentProps {
- children: React.ReactNode;
+ children: ReactNode
}
-export const SidesheetPanelContent = ({ children }: SidesheetPanelContentProps) => {
- return {children}
;
-};
+export const SidesheetPanelContent = ({
+ children,
+}: SidesheetPanelContentProps) => {
+ return {children}
+}
diff --git a/src/components/sidesheet/sidesheet-panel-header-action-group.tsx b/src/components/sidesheet/sidesheet-panel-header-action-group.tsx
index dd85e5ad..ae7fc5c6 100644
--- a/src/components/sidesheet/sidesheet-panel-header-action-group.tsx
+++ b/src/components/sidesheet/sidesheet-panel-header-action-group.tsx
@@ -1,11 +1,11 @@
-import React from "react";
+import type { ReactNode } from 'react'
export interface SidesheetPanelHeaderActionGroupProps {
- children: React.ReactNode;
+ children: ReactNode
}
export const SidesheetPanelHeaderActionGroup = ({
- children,
+ children,
}: SidesheetPanelHeaderActionGroupProps) => {
- return {children}
;
-};
+ return {children}
+}
diff --git a/src/components/sidesheet/sidesheet-panel-header-title.tsx b/src/components/sidesheet/sidesheet-panel-header-title.tsx
index 445db14a..47a201bc 100644
--- a/src/components/sidesheet/sidesheet-panel-header-title.tsx
+++ b/src/components/sidesheet/sidesheet-panel-header-title.tsx
@@ -1,10 +1,16 @@
-import { DialogTitle as HeadlessUiDialogTitle } from "@headlessui/react";
-import React from "react";
+import type { ReactNode } from 'react'
+import { DialogTitle as HeadlessUiDialogTitle } from '@headlessui/react'
export interface PanelHeaderTitleProps {
- children: React.ReactNode;
+ children: ReactNode
}
-export const SidesheetPanelHeaderTitle = ({ children }: PanelHeaderTitleProps) => {
- return {children} ;
-};
+export const SidesheetPanelHeaderTitle = ({
+ children,
+}: PanelHeaderTitleProps) => {
+ return (
+
+ {children}
+
+ )
+}
diff --git a/src/components/sidesheet/sidesheet-panel-header.tsx b/src/components/sidesheet/sidesheet-panel-header.tsx
index bed267ec..812ee0a7 100644
--- a/src/components/sidesheet/sidesheet-panel-header.tsx
+++ b/src/components/sidesheet/sidesheet-panel-header.tsx
@@ -1,21 +1,23 @@
-import React from "react";
-import { SidesheetPanelHeaderActionGroup } from "./sidesheet-panel-header-action-group";
-import { SidesheetPanelHeaderTitle } from "./sidesheet-panel-header-title";
+import type { ReactNode } from 'react'
+import { SidesheetPanelHeaderActionGroup } from './sidesheet-panel-header-action-group'
+import { SidesheetPanelHeaderTitle } from './sidesheet-panel-header-title'
export interface SidesheetPanelHeaderProps {
- children: React.ReactNode;
+ children: ReactNode
}
const SidesheetPanelHeader = ({ children }: SidesheetPanelHeaderProps) => {
- return (
- <>
- {children}
-
- >
- );
-};
+ return (
+ <>
+
+ {children}
+
+
+ >
+ )
+}
-SidesheetPanelHeader.Title = SidesheetPanelHeaderTitle;
-SidesheetPanelHeader.ActionGroup = SidesheetPanelHeaderActionGroup;
+SidesheetPanelHeader.Title = SidesheetPanelHeaderTitle
+SidesheetPanelHeader.ActionGroup = SidesheetPanelHeaderActionGroup
-export { SidesheetPanelHeader };
+export { SidesheetPanelHeader }
diff --git a/src/components/sidesheet/sidesheet-panel.tsx b/src/components/sidesheet/sidesheet-panel.tsx
index 8afc9112..c7193c34 100644
--- a/src/components/sidesheet/sidesheet-panel.tsx
+++ b/src/components/sidesheet/sidesheet-panel.tsx
@@ -1,10 +1,10 @@
-import { DialogPanel as HeadlessUiDialogPanel } from "@headlessui/react";
-import React from "react";
+import type { ReactNode } from 'react'
+import { DialogPanel as HeadlessUiDialogPanel } from '@headlessui/react'
export interface SidesheetPanelProps {
- children: React.ReactNode;
+ children: ReactNode
}
export const SidesheetPanel = ({ children }: SidesheetPanelProps) => {
- return {children} ;
-};
+ return {children}
+}
diff --git a/src/components/sidesheet/sidesheet.stories.tsx b/src/components/sidesheet/sidesheet.stories.tsx
index be4ef511..b8b105c8 100644
--- a/src/components/sidesheet/sidesheet.stories.tsx
+++ b/src/components/sidesheet/sidesheet.stories.tsx
@@ -1,72 +1,74 @@
-import React from "react";
-import type { Meta, StoryObj } from "@storybook/react";
-import { Sidesheet, SidesheetProps } from "./sidesheet";
-import { Button } from "../button";
+import { useState, useEffect } from 'react'
+import type { Meta, StoryObj } from '@storybook/react-vite'
+import { Sidesheet, SidesheetProps } from './sidesheet'
+import { Button } from '../button'
const meta: Meta = {
- title: "Sidesheet",
- component: Sidesheet,
-};
+ title: 'Sidesheet',
+ component: Sidesheet,
+}
-export default meta;
-type Story = StoryObj;
+export default meta
+type Story = StoryObj
const SidesheetWithHooks = ({ isOpen }: SidesheetProps) => {
- const [internalIsOpen, setInternalIsOpen] = React.useState(false);
+ const [internalIsOpen, setInternalIsOpen] = useState(false)
- React.useEffect(() => {
- setInternalIsOpen(isOpen);
- }, [isOpen]);
+ useEffect(() => {
+ setInternalIsOpen(isOpen)
+ }, [isOpen])
- function handleCloseModal() {
- setInternalIsOpen(false);
- }
+ function handleCloseModal() {
+ setInternalIsOpen(false)
+ }
- function handleClickOpenModal() {
- setInternalIsOpen(true);
- }
+ function handleClickOpenModal() {
+ setInternalIsOpen(true)
+ }
- return (
-
-
{
- handleClickOpenModal();
- }}
- >
- Open Sidesheet
-
+ return (
+
+
{
+ handleClickOpenModal()
+ }}
+ >
+ Open Sidesheet
+
-
handleCloseModal()}>
-
-
- Modal Title
-
- null}>Button 1
-
-
-
- Sidesheet Content
-
-
-
-
- );
-};
+
handleCloseModal()}>
+
+
+
+ Modal Title
+
+
+ null}>Button 1
+
+
+
+ Sidesheet Content
+
+
+
+
+ )
+}
export const Default: Story = {
- render: (args) => (
-
- Hello
-
- ),
- args: {
- isOpen: false,
+ render: (args) => (
+
+ Hello
+
+ ),
+ args: {
+ isOpen: false,
+ },
+ argTypes: {
+ initialFocus: {
+ control: {
+ type: 'text',
+ },
},
- argTypes: {
- initialFocus: {
- control: {
- type: "text",
- },
- },
- },
-};
+ },
+}
diff --git a/src/components/sidesheet/sidesheet.test.tsx b/src/components/sidesheet/sidesheet.test.tsx
new file mode 100644
index 00000000..ba096e06
--- /dev/null
+++ b/src/components/sidesheet/sidesheet.test.tsx
@@ -0,0 +1,15 @@
+import { describe, expect, it, vi } from 'vitest'
+import { render, screen } from '@testing-library/react'
+import { Sidesheet } from './sidesheet'
+
+describe('Sidesheet', () => {
+ it('renders the sidesheet content when open', () => {
+ render(
+
+ Panel content
+ ,
+ )
+
+ expect(screen.getByText('Panel content')).toBeInTheDocument()
+ })
+})
diff --git a/src/components/sidesheet/sidesheet.tsx b/src/components/sidesheet/sidesheet.tsx
index d7cf5a1f..7ebc81d3 100644
--- a/src/components/sidesheet/sidesheet.tsx
+++ b/src/components/sidesheet/sidesheet.tsx
@@ -1,57 +1,66 @@
+import type { MutableRefObject, ReactNode } from 'react'
+import { Fragment } from 'react'
import {
- Dialog as HeadlessUiDialog,
- DialogPanel as HeadlessUiDialogPanel,
- Transition as HeadlessUiTransition,
- TransitionChild as HeadlessUiTransitionChild,
-} from "@headlessui/react";
-import React, { Fragment } from "react";
-import { SidesheetPanel } from "./sidesheet-panel";
-import { SidesheetPanelContent } from "./sidesheet-panel-content";
-import { SidesheetPanelHeader } from "./sidesheet-panel-header";
+ Dialog as HeadlessUiDialog,
+ DialogPanel as HeadlessUiDialogPanel,
+ Transition as HeadlessUiTransition,
+ TransitionChild as HeadlessUiTransitionChild,
+} from '@headlessui/react'
+import { SidesheetPanel } from './sidesheet-panel'
+import { SidesheetPanelContent } from './sidesheet-panel-content'
+import { SidesheetPanelHeader } from './sidesheet-panel-header'
export interface SidesheetProps {
- children: React.ReactNode;
- isOpen: boolean;
- onClose: () => void;
- initialFocus?: React.MutableRefObject;
+ children: ReactNode
+ isOpen: boolean
+ onClose: () => void
+ initialFocus?: MutableRefObject
}
-const Sidesheet = ({ children, isOpen, onClose, initialFocus }: SidesheetProps) => {
- return (
-
-
-
-
-
+const Sidesheet = ({
+ children,
+ isOpen,
+ onClose,
+ initialFocus,
+}: SidesheetProps) => {
+ return (
+
+
+
+
+
-
-
- {children}
-
-
-
-
- );
-};
+
+
+ {children}
+
+
+
+
+ )
+}
-Sidesheet.Panel = SidesheetPanel;
-Sidesheet.PanelHeader = SidesheetPanelHeader;
-Sidesheet.PanelContent = SidesheetPanelContent;
+Sidesheet.Panel = SidesheetPanel
+Sidesheet.PanelHeader = SidesheetPanelHeader
+Sidesheet.PanelContent = SidesheetPanelContent
-export { Sidesheet };
+export { Sidesheet }
diff --git a/src/components/skeleton/index.ts b/src/components/skeleton/index.ts
index 655ffcee..fe8d24ef 100644
--- a/src/components/skeleton/index.ts
+++ b/src/components/skeleton/index.ts
@@ -1 +1 @@
-export { Skeleton } from "./skeleton";
+export { Skeleton } from './skeleton'
diff --git a/src/components/skeleton/skeleton.stories.tsx b/src/components/skeleton/skeleton.stories.tsx
index 3a3d49a3..75f54554 100644
--- a/src/components/skeleton/skeleton.stories.tsx
+++ b/src/components/skeleton/skeleton.stories.tsx
@@ -1,21 +1,20 @@
-import type { Meta, StoryObj } from "@storybook/react";
+import type { Meta, StoryObj } from '@storybook/react-vite'
-import React from "react";
-import { Skeleton } from "./skeleton";
+import { Skeleton } from './skeleton'
const meta: Meta = {
- title: "Skeleton",
- component: Skeleton,
-};
+ title: 'Skeleton',
+ component: Skeleton,
+}
-export default meta;
+export default meta
-type Story = StoryObj;
+type Story = StoryObj
export const Default: Story = {
- render: (args) => ,
- args: {
- className: "w-96 h-8",
- isAnimated: false,
- },
-};
+ render: (args) => ,
+ args: {
+ className: 'w-96 h-8',
+ isAnimated: false,
+ },
+}
diff --git a/src/components/skeleton/skeleton.test.tsx b/src/components/skeleton/skeleton.test.tsx
new file mode 100644
index 00000000..e89fd1b1
--- /dev/null
+++ b/src/components/skeleton/skeleton.test.tsx
@@ -0,0 +1,16 @@
+import { describe, expect, it } from 'vitest'
+import { render } from '@testing-library/react'
+import { Skeleton } from './skeleton'
+
+describe('Skeleton', () => {
+ it('renders with optional animation', () => {
+ const { container, rerender } = render( )
+ const skeleton = container.querySelector('span')
+
+ expect(skeleton).toBeInTheDocument()
+ expect(skeleton).not.toHaveClass('animate-pulse')
+
+ rerender( )
+ expect(container.querySelector('span')).toHaveClass('animate-pulse')
+ })
+})
diff --git a/src/components/skeleton/skeleton.tsx b/src/components/skeleton/skeleton.tsx
index 627ebfbb..f9867f70 100644
--- a/src/components/skeleton/skeleton.tsx
+++ b/src/components/skeleton/skeleton.tsx
@@ -1,19 +1,18 @@
-import React from "react";
-import { classNames } from "../../util/class-names";
+import { classNames } from '../../util/class-names'
export interface SkeletonProps {
- className?: string;
- isAnimated?: boolean;
+ className?: string
+ isAnimated?: boolean
}
export const Skeleton = ({ className, isAnimated = false }: SkeletonProps) => {
- return (
-
- );
-};
+ return (
+
+ )
+}
diff --git a/src/components/slot/slot.stories.tsx b/src/components/slot/slot.stories.tsx
new file mode 100644
index 00000000..de6c0648
--- /dev/null
+++ b/src/components/slot/slot.stories.tsx
@@ -0,0 +1,40 @@
+import type { Meta, StoryObj } from '@storybook/react-vite'
+import { Slot } from './slot'
+
+const meta: Meta = {
+ title: 'Slot',
+ component: Slot,
+ args: {
+ label: 'Slotted link',
+ href: '/',
+ className: 'text-primary-500 underline',
+ },
+ argTypes: {
+ label: { control: 'text' },
+ href: { control: 'text' },
+ className: { control: 'text' },
+ },
+}
+
+export default meta
+
+type Story = StoryObj
+
+export const Default: Story = {
+ render: ({ label, href, className }) => (
+
+ {label}
+
+ ),
+}
+
+export const WithStyles: Story = {
+ render: () => (
+
+ Slotted button
+
+ ),
+}
diff --git a/src/components/slot/slot.tsx b/src/components/slot/slot.tsx
index aa5957a7..73c64e38 100644
--- a/src/components/slot/slot.tsx
+++ b/src/components/slot/slot.tsx
@@ -1,31 +1,33 @@
-import React from "react";
-import { classNames } from "../../util/class-names";
+import { cloneElement, isValidElement, Children } from 'react'
+import type { HTMLAttributes, ReactNode } from 'react'
+import { classNames } from '../../util/class-names'
export type AsChildProps =
- | ({ asChild?: false } & DefaultElementProps)
- | { asChild: true; children: React.ReactNode };
+ | ({ asChild?: false } & DefaultElementProps)
+ | { asChild: true; children: ReactNode }
export const Slot = ({
- children,
- ...props
-}: React.HTMLAttributes & {
- children?: React.ReactNode;
+ children,
+ ...props
+}: HTMLAttributes & {
+ children?: ReactNode
}) => {
- if (React.isValidElement(children)) {
- return React.cloneElement(children, {
- ...props,
- ...children.props,
- style: {
- ...props.style,
- ...children.props.style,
- },
- className: classNames(props.className, props.className, children.props.className),
- });
- }
+ if (isValidElement>(children)) {
+ const childProps = children.props
+ return cloneElement(children, {
+ ...props,
+ ...childProps,
+ style: {
+ ...props.style,
+ ...childProps.style,
+ },
+ className: classNames(props.className, childProps.className),
+ })
+ }
- if (React.Children.count(children) > 1) {
- React.Children.only(null);
- }
+ if (Children.count(children) > 1) {
+ Children.only(null)
+ }
- return null;
-};
+ return null
+}
diff --git a/src/components/spinner-overlay/index.ts b/src/components/spinner-overlay/index.ts
index e6b293db..a5fbc94b 100644
--- a/src/components/spinner-overlay/index.ts
+++ b/src/components/spinner-overlay/index.ts
@@ -1 +1 @@
-export { SpinnerOverlay } from "./spinner-overlay";
+export { SpinnerOverlay } from './spinner-overlay'
diff --git a/src/components/spinner-overlay/spinner-overlay.stories.tsx b/src/components/spinner-overlay/spinner-overlay.stories.tsx
index 9085210f..b9416a48 100644
--- a/src/components/spinner-overlay/spinner-overlay.stories.tsx
+++ b/src/components/spinner-overlay/spinner-overlay.stories.tsx
@@ -1,20 +1,32 @@
-import React from "react";
-import type { Meta, StoryObj } from "@storybook/react";
-import { SpinnerOverlay } from "./spinner-overlay";
+import type { Meta, StoryObj } from '@storybook/react-vite'
+import { SpinnerOverlay } from './spinner-overlay'
const meta: Meta = {
- title: "SpinnerOverlay",
- component: SpinnerOverlay,
-};
+ title: 'SpinnerOverlay',
+ component: SpinnerOverlay,
+ args: {
+ size: 'medium',
+ opacity: 0.5,
+ },
+ argTypes: {
+ size: {
+ control: 'select',
+ options: ['small', 'medium', 'large'],
+ },
+ opacity: {
+ control: { type: 'range', min: 0.1, max: 1, step: 0.1 },
+ },
+ },
+}
-export default meta;
+export default meta
-type Story = StoryObj;
+type Story = StoryObj
export const Default: Story = {
- render: () => (
-
-
-
- ),
-};
+ render: (args) => (
+
+
+
+ ),
+}
diff --git a/src/components/spinner-overlay/spinner-overlay.test.tsx b/src/components/spinner-overlay/spinner-overlay.test.tsx
new file mode 100644
index 00000000..66b6daa9
--- /dev/null
+++ b/src/components/spinner-overlay/spinner-overlay.test.tsx
@@ -0,0 +1,15 @@
+import { describe, expect, it } from 'vitest'
+import { render } from '@testing-library/react'
+import { SpinnerOverlay } from './spinner-overlay'
+
+describe('SpinnerOverlay', () => {
+ it('renders an overlay with a spinner', () => {
+ const { container } = render( )
+ const overlay = container.firstElementChild as HTMLElement | null
+ const spinner = container.querySelector('svg')
+
+ expect(overlay).toBeInTheDocument()
+ expect(overlay?.style.opacity).toBe('0.8')
+ expect(spinner).toBeInTheDocument()
+ })
+})
diff --git a/src/components/spinner-overlay/spinner-overlay.tsx b/src/components/spinner-overlay/spinner-overlay.tsx
index b4c5afe9..4e79d83b 100644
--- a/src/components/spinner-overlay/spinner-overlay.tsx
+++ b/src/components/spinner-overlay/spinner-overlay.tsx
@@ -1,10 +1,26 @@
-import React from "react";
-import { Spinner } from "../spinner/spinner";
+import { classNames } from '../../util/class-names'
+import { Spinner } from '../spinner/spinner'
-export const SpinnerOverlay = () => {
- return (
-
-
-
- );
-};
+interface SpinnerOverlayProps {
+ size?: 'small' | 'medium' | 'large'
+ opacity?: number
+ className?: string
+}
+
+export const SpinnerOverlay = ({
+ size = 'medium',
+ opacity = 0.5,
+ className,
+}: SpinnerOverlayProps) => {
+ return (
+
+
+
+ )
+}
diff --git a/src/components/spinner/index.ts b/src/components/spinner/index.ts
index 680b09e2..73e4a5ad 100644
--- a/src/components/spinner/index.ts
+++ b/src/components/spinner/index.ts
@@ -1 +1 @@
-export { Spinner } from "./spinner";
+export { Spinner } from './spinner'
diff --git a/src/components/spinner/spinner.stories.tsx b/src/components/spinner/spinner.stories.tsx
index dfbfdce6..781a67ea 100644
--- a/src/components/spinner/spinner.stories.tsx
+++ b/src/components/spinner/spinner.stories.tsx
@@ -1,20 +1,19 @@
-import type { Meta, StoryObj } from "@storybook/react";
+import type { Meta, StoryObj } from '@storybook/react-vite'
-import React from "react";
-import { Spinner } from "./spinner";
+import { Spinner } from './spinner'
const meta: Meta = {
- title: "Spinner",
- component: Spinner,
-};
+ title: 'Spinner',
+ component: Spinner,
+}
-export default meta;
+export default meta
-type Story = StoryObj;
+type Story = StoryObj
export const Default: Story = {
- render: (args) => ,
- args: {
- size: "small",
- },
-};
+ render: (args) => ,
+ args: {
+ size: 'small',
+ },
+}
diff --git a/src/components/spinner/spinner.test.tsx b/src/components/spinner/spinner.test.tsx
new file mode 100644
index 00000000..532695c0
--- /dev/null
+++ b/src/components/spinner/spinner.test.tsx
@@ -0,0 +1,17 @@
+import { render } from '@testing-library/react'
+import { describe, expect, it } from 'vitest'
+import { Spinner } from './spinner'
+
+describe('Spinner', () => {
+ it('renders with size classes', () => {
+ // ARRANGE
+ const { container } = render( )
+
+ // ASSERT
+ const spinner = container.querySelector('svg')
+ expect(spinner).toBeInTheDocument()
+ expect(spinner).toHaveClass('animate-spin')
+ expect(spinner).toHaveClass('h-4')
+ expect(spinner).toHaveClass('w-4')
+ })
+})
diff --git a/src/components/spinner/spinner.tsx b/src/components/spinner/spinner.tsx
index 7c1e3d86..0591a465 100644
--- a/src/components/spinner/spinner.tsx
+++ b/src/components/spinner/spinner.tsx
@@ -1,32 +1,31 @@
-import React from "react";
-import { classNames } from "../../util/class-names";
+import { classNames } from '../../util/class-names'
const spinnerVariants = {
- small: "h-4 w-4",
- medium: "h-6 w-6",
- large: "h-10 w-10",
-};
+ small: 'h-4 w-4',
+ medium: 'h-6 w-6',
+ large: 'h-10 w-10',
+}
interface SpinnerProps {
- size?: keyof typeof spinnerVariants;
+ size?: keyof typeof spinnerVariants
}
-export const Spinner = ({ size = "large" }: SpinnerProps) => {
- return (
-
-
-
-
- );
-};
+export const Spinner = ({ size = 'large' }: SpinnerProps) => {
+ return (
+
+
+
+
+ )
+}
diff --git a/src/components/tab/index.ts b/src/components/tab/index.ts
index 7e51dc1e..d1092009 100644
--- a/src/components/tab/index.ts
+++ b/src/components/tab/index.ts
@@ -1 +1 @@
-export { Tab } from "./tab";
+export { Tab } from './tab'
diff --git a/src/components/tab/tab-button.tsx b/src/components/tab/tab-button.tsx
index 2443a46c..4d8c8a09 100644
--- a/src/components/tab/tab-button.tsx
+++ b/src/components/tab/tab-button.tsx
@@ -1,44 +1,51 @@
-import { Tab as HeadlessUiTab, TabProps as HeadlessUiTabProps } from "@headlessui/react";
-import React from "react";
-import { classNames } from "../../util/class-names";
-import { TabType, useTabContext } from "./tab-context";
+import type { ElementType, ComponentPropsWithoutRef } from 'react'
+import { ReactNode } from 'react'
+import {
+ Tab as HeadlessUiTab,
+ TabProps as HeadlessUiTabProps,
+} from '@headlessui/react'
+import { classNames } from '../../util/class-names'
+import { TabType, useTabContext } from './tab-context'
-export interface TabButtonProps
- extends HeadlessUiTabProps {
- children: React.ReactNode;
- as?: TTag;
- hasIndicator?: boolean;
+export interface TabButtonProps<
+ TTag extends ElementType,
+> extends HeadlessUiTabProps {
+ children: ReactNode
+ as?: TTag
+ hasIndicator?: boolean
}
const buttonVariants: Record = {
- primary:
- "relative whitespace-nowrap focus:ring-2 ring:primary-200 hover:ui-not-selected:text-neutral-800 ui-not-selected:after:hidden ui-selected:after:absolute ui-selected:after:left-0 ui-selected:after:right-0 ui-selected:after:-bottom-2 ui-selected:after:block ui-selected:after:h-0.5 ui-selected:after:bg-primary-500 ui-selected:disabled:after:bg-primary-500 disabled:text-neutral-500",
- secondary:
- "px-4 py-2 ui-not-selected:bg-transparent hover:ui-not-selected:text-neutral-800 focus:ring-2 ring:primary-200 hover:ui-not-selected:bg-neutral-100 ui-selected:text-primary-500 ui-selected:bg-primary-100 disabled:text-neutral-500",
-};
+ primary:
+ 'relative whitespace-nowrap focus:ring-2 ring:primary-200 hover:ui-not-selected:text-neutral-800 ui-not-selected:after:hidden ui-selected:after:absolute ui-selected:after:left-0 ui-selected:after:right-0 ui-selected:after:-bottom-2 ui-selected:after:block ui-selected:after:h-0.5 ui-selected:after:bg-primary-500 ui-selected:disabled:after:bg-primary-500 disabled:text-neutral-500',
+ secondary:
+ 'px-4 py-2 ui-not-selected:bg-transparent hover:ui-not-selected:text-neutral-800 focus:ring-2 ring:primary-200 hover:ui-not-selected:bg-neutral-100 ui-selected:text-primary-500 ui-selected:bg-primary-100 disabled:text-neutral-500',
+}
-export const TabButton = ({
- children,
- hasIndicator = false,
- ...props
+export const TabButton = ({
+ children,
+ hasIndicator = false,
+ ...props
}: TabButtonProps &
- Omit, keyof TabButtonProps>) => {
- const { type } = useTabContext();
+ Omit, keyof TabButtonProps>) => {
+ const { type } = useTabContext()
- return (
- // eslint-disable-next-line react/jsx-props-no-spreading, @typescript-eslint/no-explicit-any
-
-
-
- {children}
- {hasIndicator &&
}
-
-
-
- );
-};
+ return (
+ // eslint-disable-next-line react/jsx-props-no-spreading, @typescript-eslint/no-explicit-any
+
+
+
+ {children}
+ {hasIndicator && (
+
+ )}
+
+
+
+ )
+}
diff --git a/src/components/tab/tab-context.tsx b/src/components/tab/tab-context.tsx
index ddae16a0..58a2b331 100644
--- a/src/components/tab/tab-context.tsx
+++ b/src/components/tab/tab-context.tsx
@@ -1,19 +1,21 @@
-import React from "react";
+import { createContext, useContext } from 'react'
-export type TabType = "primary" | "secondary";
+export type TabType = 'primary' | 'secondary'
interface TabContextProps {
- type: TabType;
+ type: TabType
}
-const TabContext = React.createContext({ type: "primary" });
+const TabContext = createContext({ type: 'primary' })
function useTabContext() {
- const context = React.useContext(TabContext);
- if (!context) {
- throw new Error(`Tab compound components cannot be rendered outside the Tab component`);
- }
- return context;
+ const context = useContext(TabContext)
+ if (!context) {
+ throw new Error(
+ `Tab compound components cannot be rendered outside the Tab component`,
+ )
+ }
+ return context
}
-export { TabContext, useTabContext };
+export { TabContext, useTabContext }
diff --git a/src/components/tab/tab-list.tsx b/src/components/tab/tab-list.tsx
index a406cd2e..fcbfadc2 100644
--- a/src/components/tab/tab-list.tsx
+++ b/src/components/tab/tab-list.tsx
@@ -1,23 +1,23 @@
-import { TabList as HeadlessUiTabList } from "@headlessui/react";
-import React from "react";
-import { classNames } from "../../util/class-names";
-import { TabType, useTabContext } from "./tab-context";
+import type { ReactNode } from 'react'
+import { TabList as HeadlessUiTabList } from '@headlessui/react'
+import { classNames } from '../../util/class-names'
+import { TabType, useTabContext } from './tab-context'
export interface TabListProps {
- children: React.ReactNode;
+ children: ReactNode
}
const listVariants: Record = {
- primary: "gap-5 pb-2",
- secondary: "gap-2",
-};
+ primary: 'gap-5 pb-2',
+ secondary: 'gap-2',
+}
export const TabList = ({ children }: TabListProps) => {
- const { type } = useTabContext();
+ const { type } = useTabContext()
- return (
-
- {children}
-
- );
-};
+ return (
+
+ {children}
+
+ )
+}
diff --git a/src/components/tab/tab-panel.tsx b/src/components/tab/tab-panel.tsx
index 02819230..6682a198 100644
--- a/src/components/tab/tab-panel.tsx
+++ b/src/components/tab/tab-panel.tsx
@@ -1,9 +1,8 @@
import {
- TabPanel as HeadlessUiTabPanel,
- TabPanelProps as HeadlessUiTabPanelProps,
-} from "@headlessui/react";
-import React from "react";
+ TabPanel as HeadlessUiTabPanel,
+ TabPanelProps as HeadlessUiTabPanelProps,
+} from '@headlessui/react'
export const TabPanel = ({ children, ...props }: HeadlessUiTabPanelProps) => {
- return {children} ;
-};
+ return {children}
+}
diff --git a/src/components/tab/tab-panels.tsx b/src/components/tab/tab-panels.tsx
index f0e140c3..60fff90f 100644
--- a/src/components/tab/tab-panels.tsx
+++ b/src/components/tab/tab-panels.tsx
@@ -1,11 +1,13 @@
-import { TabPanels as HeadlessUiTabPanels } from "@headlessui/react";
-import React from "react";
+import type { ReactNode } from 'react'
+import { TabPanels as HeadlessUiTabPanels } from '@headlessui/react'
export interface TabPanelsProps {
- children: React.ReactNode;
- className?: string;
+ children: ReactNode
+ className?: string
}
export const TabPanels = ({ children, className }: TabPanelsProps) => {
- return {children} ;
-};
+ return (
+ {children}
+ )
+}
diff --git a/src/components/tab/tab.stories.tsx b/src/components/tab/tab.stories.tsx
index bf2b4ed8..31410b56 100644
--- a/src/components/tab/tab.stories.tsx
+++ b/src/components/tab/tab.stories.tsx
@@ -1,176 +1,175 @@
-import type { Meta, StoryObj } from "@storybook/react";
-import React from "react";
-import { getStoryDescription } from "../../util/storybook-utils";
-import { Tab } from "./tab";
-import { TabType } from "./tab-context";
+import type { Meta, StoryObj } from '@storybook/react-vite'
+import { getStoryDescription } from '../../util/storybook-utils'
+import { Tab } from './tab'
+import { TabType } from './tab-context'
const meta: Meta = {
- title: "Tab",
- component: Tab,
- parameters: {
- ...getStoryDescription(
- "Tab component. For a detailed explanation on props, please visit the Headless UI [tab-group documentation](https://headlessui.com/react/tabs#tab-group)"
- ),
- docs: { source: { type: "auto" } },
- },
-};
+ title: 'Tab',
+ component: Tab,
+ parameters: {
+ ...getStoryDescription(
+ 'Tab component. For a detailed explanation on props, please visit the Headless UI [tab-group documentation](https://headlessui.com/react/tabs#tab-group)',
+ ),
+ docs: { source: { type: 'auto' } },
+ },
+}
-export default meta;
+export default meta
-type Story = StoryObj;
+type Story = StoryObj
export const Primary: Story = {
- render: (args) => (
-
-
- null}>Tab 1
- null}>Tab 2
- null}>Tab 3
-
-
-
-
- Content 1
-
-
-
- Content 2
-
-
-
- Content 3
-
-
-
- ),
- args: { type: "primary" as TabType },
-};
+ render: (args) => (
+
+
+ null}>Tab 1
+ null}>Tab 2
+ null}>Tab 3
+
+
+
+
+ Content 1
+
+
+
+ Content 2
+
+
+
+ Content 3
+
+
+
+ ),
+ args: { type: 'primary' as TabType },
+}
export const Secondary: Story = {
- render: (args) => (
-
-
- null}>Tab 1
- null}>Tab 2
- null}>Tab 3
-
-
-
-
- Content 1
-
-
-
- Content 2
-
-
-
- Content 3
-
-
-
- ),
- args: { type: "secondary" as TabType },
-};
+ render: (args) => (
+
+
+ null}>Tab 1
+ null}>Tab 2
+ null}>Tab 3
+
+
+
+
+ Content 1
+
+
+
+ Content 2
+
+
+
+ Content 3
+
+
+
+ ),
+ args: { type: 'secondary' as TabType },
+}
export const NestedTabs: Story = {
- render: (args) => (
-
-
- null}>Tab 1
- null}>Tab 2
- null}>Tab 3
-
-
-
+ render: (args) => (
+
+
+ null}>Tab 1
+ null}>Tab 2
+ null}>Tab 3
+
+
+
+
+
+
+
+ null}>Nested Tab 1
+ null}>Nested Tab 2
+
+
-
-
-
- null}>Nested Tab 1
- null}>Nested Tab 2
-
-
-
- Nested Content 1
-
-
- Nested Content 2
-
-
-
-
+ Nested Content 1
-
-
-
-
- null}>Nested Tab A
- null}>Nested Tab B
-
-
-
- Nested Content A
-
-
- Nested Content B
-
-
-
-
+ Nested Content 2
-
+
+
+
+
+
+
+
+
+
+ null}>Nested Tab A
+ null}>Nested Tab B
+
+
-
-
-
- null}>Nested Tab X
- null}>Nested Tab Y
-
-
-
- Nested Content X
-
-
- Nested Content Y
-
-
-
-
+ Nested Content A
-
-
- ),
- args: { type: "primary", vertical: false, manual: false, defaultIndex: 0 },
- argTypes: { className: { control: { type: "text" } } },
-};
-
-export const WithIndicator: Story = {
- render: (args) => (
-
-
- null}>All
- null} hasIndicator>
- With Indicator
-
- null}>Tab 3
-
-
-
- All content
+ Nested Content B
-
+
+
+
+
+
+
+
+
+
+ null}>Nested Tab X
+ null}>Nested Tab Y
+
+
- Decision needed content
+ Nested Content X
-
- Content 3
+ Nested Content Y
-
-
- ),
- args: { type: "primary" as TabType },
-};
+
+
+
+
+
+
+ ),
+ args: { type: 'primary', vertical: false, manual: false, defaultIndex: 0 },
+ argTypes: { className: { control: { type: 'text' } } },
+}
+
+export const WithIndicator: Story = {
+ render: (args) => (
+
+
+ null}>All
+ null} hasIndicator>
+ With Indicator
+
+ null}>Tab 3
+
+
+
+
+ All content
+
+
+
+ Decision needed content
+
+
+
+ Content 3
+
+
+
+ ),
+ args: { type: 'primary' as TabType },
+}
diff --git a/src/components/tab/tab.test.tsx b/src/components/tab/tab.test.tsx
new file mode 100644
index 00000000..0ad137ca
--- /dev/null
+++ b/src/components/tab/tab.test.tsx
@@ -0,0 +1,24 @@
+import { describe, expect, it } from 'vitest'
+import { render, screen } from '@testing-library/react'
+import { Tab } from './tab'
+
+describe('Tab', () => {
+ it('renders tab labels and the active panel', () => {
+ render(
+
+
+ Overview
+ Settings
+
+
+ Overview panel
+ Settings panel
+
+ ,
+ )
+
+ expect(screen.getByText('Overview')).toBeInTheDocument()
+ expect(screen.getByText('Settings')).toBeInTheDocument()
+ expect(screen.getByText('Overview panel')).toBeInTheDocument()
+ })
+})
diff --git a/src/components/tab/tab.tsx b/src/components/tab/tab.tsx
index 8930da95..0c65661d 100644
--- a/src/components/tab/tab.tsx
+++ b/src/components/tab/tab.tsx
@@ -1,30 +1,34 @@
-import { TabGroup as HeadlessUiTabGroup, TabGroupProps } from "@headlessui/react";
-import React from "react";
-import { TabButton } from "./tab-button";
-import { TabContext, TabType } from "./tab-context";
-import { TabList } from "./tab-list";
-import { TabPanel } from "./tab-panel";
-import { TabPanels } from "./tab-panels";
+import type { ElementType, ReactNode } from 'react'
+import { useMemo } from 'react'
+import {
+ TabGroup as HeadlessUiTabGroup,
+ TabGroupProps,
+} from '@headlessui/react'
+import { TabButton } from './tab-button'
+import { TabContext, TabType } from './tab-context'
+import { TabList } from './tab-list'
+import { TabPanel } from './tab-panel'
+import { TabPanels } from './tab-panels'
-interface TabProps extends TabGroupProps {
- type?: TabType;
- children: React.ReactNode;
+interface TabProps extends TabGroupProps {
+ type?: TabType
+ children: ReactNode
}
-const Tab = ({ type = "primary", children, ...props }: TabProps) => {
- const value = React.useMemo(() => ({ type }), [type]);
+const Tab = ({ type = 'primary', children, ...props }: TabProps) => {
+ const value = useMemo(() => ({ type }), [type])
- return (
-
- {/* eslint-disable-next-line react/jsx-props-no-spreading */}
- {children}
-
- );
-};
+ return (
+
+ {/* eslint-disable-next-line react/jsx-props-no-spreading */}
+ {children}
+
+ )
+}
-Tab.List = TabList;
-Tab.Button = TabButton;
-Tab.Panels = TabPanels;
-Tab.Panel = TabPanel;
+Tab.List = TabList
+Tab.Button = TabButton
+Tab.Panels = TabPanels
+Tab.Panel = TabPanel
-export { Tab };
+export { Tab }
diff --git a/src/components/table-key-value-pair/index.ts b/src/components/table-key-value-pair/index.ts
index 6b726d23..a76415b8 100644
--- a/src/components/table-key-value-pair/index.ts
+++ b/src/components/table-key-value-pair/index.ts
@@ -1 +1 @@
-export { TableKeyValuePair } from "./table-key-value-pair";
+export { TableKeyValuePair } from './table-key-value-pair'
diff --git a/src/components/table-key-value-pair/table-key-value-pair-body-key-cell.tsx b/src/components/table-key-value-pair/table-key-value-pair-body-key-cell.tsx
index 1441886d..5441c67a 100644
--- a/src/components/table-key-value-pair/table-key-value-pair-body-key-cell.tsx
+++ b/src/components/table-key-value-pair/table-key-value-pair-body-key-cell.tsx
@@ -1,25 +1,25 @@
-import React from "react";
-import { classNames } from "../../util/class-names";
+import type { DetailedHTMLProps, TdHTMLAttributes } from 'react'
+import { classNames } from '../../util/class-names'
-export type TableKeyValuePairBodyKeyProps = React.DetailedHTMLProps<
- React.TdHTMLAttributes,
- HTMLTableCellElement
->;
+export type TableKeyValuePairBodyKeyProps = DetailedHTMLProps<
+ TdHTMLAttributes,
+ HTMLTableCellElement
+>
export const TableKeyValuePairBodyKeyCell = ({
- children,
- className,
- ...props
+ children,
+ className,
+ ...props
}: TableKeyValuePairBodyKeyProps) => {
- return (
-
- {children}
-
- );
-};
+ return (
+
+ {children}
+
+ )
+}
diff --git a/src/components/table-key-value-pair/table-key-value-pair-body-row.tsx b/src/components/table-key-value-pair/table-key-value-pair-body-row.tsx
index ff68a9f7..012c3e3b 100644
--- a/src/components/table-key-value-pair/table-key-value-pair-body-row.tsx
+++ b/src/components/table-key-value-pair/table-key-value-pair-body-row.tsx
@@ -1,22 +1,23 @@
-import React from "react";
-import { classNames } from "../../util/class-names";
+import type { TableHTMLAttributes } from 'react'
+import { classNames } from '../../util/class-names'
-export type TableKeyValuePairBodyProps = React.TableHTMLAttributes;
+export type TableKeyValuePairBodyProps =
+ TableHTMLAttributes
export const TableKeyValuePairBodyRow = ({
- children,
- className,
- ...props
+ children,
+ className,
+ ...props
}: TableKeyValuePairBodyProps) => {
- return (
- _td:first-child]:rounded-bl-md [&:last-child_>_td:first-child]:border-l [&:last-child_>_td:first-child]:border-neutral-300 [&:last-child_>_td:last-child]:rounded-br-md",
- className
- )}
- {...props}
- >
- {children}
-
- );
-};
+ return (
+ _td:first-child]:rounded-bl-md [&:last-child_>_td:first-child]:border-l [&:last-child_>_td:first-child]:border-neutral-300 [&:last-child_>_td:last-child]:rounded-br-md',
+ className,
+ )}
+ {...props}
+ >
+ {children}
+
+ )
+}
diff --git a/src/components/table-key-value-pair/table-key-value-pair-body-value-cell.tsx b/src/components/table-key-value-pair/table-key-value-pair-body-value-cell.tsx
index 0b40b415..1e461c3a 100644
--- a/src/components/table-key-value-pair/table-key-value-pair-body-value-cell.tsx
+++ b/src/components/table-key-value-pair/table-key-value-pair-body-value-cell.tsx
@@ -1,25 +1,25 @@
-import React from "react";
-import { classNames } from "../../util/class-names";
+import type { DetailedHTMLProps, TdHTMLAttributes } from 'react'
+import { classNames } from '../../util/class-names'
-export type TableKeyValuePairBodyValueProps = React.DetailedHTMLProps<
- React.TdHTMLAttributes,
- HTMLTableCellElement
->;
+export type TableKeyValuePairBodyValueProps = DetailedHTMLProps<
+ TdHTMLAttributes,
+ HTMLTableCellElement
+>
export const TableKeyValuePairBodyValueCell = ({
- children,
- className,
- ...props
+ children,
+ className,
+ ...props
}: TableKeyValuePairBodyValueProps) => {
- return (
-
- {children}
-
- );
-};
+ return (
+
+ {children}
+
+ )
+}
diff --git a/src/components/table-key-value-pair/table-key-value-pair-body.tsx b/src/components/table-key-value-pair/table-key-value-pair-body.tsx
index f70c4e78..15502986 100644
--- a/src/components/table-key-value-pair/table-key-value-pair-body.tsx
+++ b/src/components/table-key-value-pair/table-key-value-pair-body.tsx
@@ -1,16 +1,20 @@
-import React from "react";
-import { TableKeyValuePairBodyRow } from "./table-key-value-pair-body-row";
-import { TableKeyValuePairBodyValueCell } from "./table-key-value-pair-body-value-cell";
-import { TableKeyValuePairBodyKeyCell } from "./table-key-value-pair-body-key-cell";
+import type { TableHTMLAttributes } from 'react'
+import { TableKeyValuePairBodyRow } from './table-key-value-pair-body-row'
+import { TableKeyValuePairBodyValueCell } from './table-key-value-pair-body-value-cell'
+import { TableKeyValuePairBodyKeyCell } from './table-key-value-pair-body-key-cell'
-export type TableKeyValuePairBodyProps = React.TableHTMLAttributes;
+export type TableKeyValuePairBodyProps =
+ TableHTMLAttributes
-const TableKeyValuePairBody = ({ children, ...props }: TableKeyValuePairBodyProps) => {
- return {children} ;
-};
+const TableKeyValuePairBody = ({
+ children,
+ ...props
+}: TableKeyValuePairBodyProps) => {
+ return {children}
+}
-TableKeyValuePairBody.Row = TableKeyValuePairBodyRow;
-TableKeyValuePairBody.Key = TableKeyValuePairBodyKeyCell;
-TableKeyValuePairBody.Value = TableKeyValuePairBodyValueCell;
+TableKeyValuePairBody.Row = TableKeyValuePairBodyRow
+TableKeyValuePairBody.Key = TableKeyValuePairBodyKeyCell
+TableKeyValuePairBody.Value = TableKeyValuePairBodyValueCell
-export { TableKeyValuePairBody };
+export { TableKeyValuePairBody }
diff --git a/src/components/table-key-value-pair/table-key-value-pair-header.tsx b/src/components/table-key-value-pair/table-key-value-pair-header.tsx
index 8571bbec..01fb2400 100644
--- a/src/components/table-key-value-pair/table-key-value-pair-header.tsx
+++ b/src/components/table-key-value-pair/table-key-value-pair-header.tsx
@@ -1,27 +1,26 @@
-import React from "react";
-import { classNames } from "../../util/class-names";
+import type { HTMLAttributes } from 'react'
+import { classNames } from '../../util/class-names'
-export interface TableKeyValuePairHeaderProps
- extends React.HTMLAttributes {
- colSpan?: number;
+export interface TableKeyValuePairHeaderProps extends HTMLAttributes {
+ colSpan?: number
}
export const TableKeyValuePairHeader = ({
- children,
- className,
- colSpan,
- ...props
+ children,
+ className,
+ colSpan,
+ ...props
}: TableKeyValuePairHeaderProps) => {
- return (
-
-
-
- {children}
-
-
-
- );
-};
+ return (
+
+
+
+ {children}
+
+
+
+ )
+}
diff --git a/src/components/table-key-value-pair/table-key-value-pair.stories.tsx b/src/components/table-key-value-pair/table-key-value-pair.stories.tsx
index 84b5821c..69e2bb24 100644
--- a/src/components/table-key-value-pair/table-key-value-pair.stories.tsx
+++ b/src/components/table-key-value-pair/table-key-value-pair.stories.tsx
@@ -1,97 +1,149 @@
-import React from "react";
-import type { Meta } from "@storybook/react";
-import { TableKeyValuePair } from "./table-key-value-pair";
-import { FormField } from "../form-field";
+import { useState } from 'react'
+import type { Meta, StoryObj } from '@storybook/react-vite'
+import { TableKeyValuePair } from './table-key-value-pair'
+import { FormField } from '../form-field'
const meta: Meta = {
- title: "Table / Key-Value Pairs",
- component: TableKeyValuePair,
-};
+ title: 'Table / Key-Value Pairs',
+ component: TableKeyValuePair,
+ args: {
+ header: 'Details',
+ firstNameLabel: 'First Name',
+ firstNameValue: 'John',
+ ageLabel: 'Age',
+ ageValue: '44',
+ lastNameLabel: 'Last Name',
+ lastNameValue: 'Doe',
+ birthLabel: 'Birth',
+ birthValue: '01.01.1970',
+ linkLabel: 'Open Comments',
+ },
+ argTypes: {
+ header: { control: 'text' },
+ firstNameLabel: { control: 'text' },
+ firstNameValue: { control: 'text' },
+ ageLabel: { control: 'text' },
+ ageValue: { control: 'text' },
+ lastNameLabel: { control: 'text' },
+ lastNameValue: { control: 'text' },
+ birthLabel: { control: 'text' },
+ birthValue: { control: 'text' },
+ linkLabel: { control: 'text' },
+ },
+}
-export default meta;
+export default meta
+type Story = StoryObj
interface Person {
- id: number;
- name: string;
- isDead?: boolean;
+ id: number
+ name: string
+ isDead?: boolean
}
const people: Person[] = [
- { id: 1, name: "John Lennon", isDead: true },
- { id: 2, name: "Kenton Towne" },
- { id: 3, name: "Therese Wunsch" },
- { id: 4, name: "Benedict Kessler" },
- { id: 5, name: "Katelyn Rohan" },
-];
+ { id: 1, name: 'John Lennon', isDead: true },
+ { id: 2, name: 'Kenton Towne' },
+ { id: 3, name: 'Therese Wunsch' },
+ { id: 4, name: 'Benedict Kessler' },
+ { id: 5, name: 'Katelyn Rohan' },
+]
const ExampleListbox = () => {
- const [selectedPerson, setSelectedPerson] = React.useState(null);
+ const [selectedPerson, setSelectedPerson] = useState(null)
- return (
-
-
-
-
-
+ return (
+
+
+
+
+
-
- {people.map((person) => (
-
-
- {person.name}
-
-
- ))}
-
-
-
- );
-};
+
+ {people.map((person) => (
+
+
+ {person.name}
+
+
+ ))}
+
+
+
+ )
+}
-export const Default = () => {
- return (
-
-
- Details
+export const Default: Story = {
+ render: ({
+ header,
+ firstNameLabel,
+ firstNameValue,
+ ageLabel,
+ ageValue,
+ lastNameLabel,
+ lastNameValue,
+ birthLabel,
+ birthValue,
+ linkLabel,
+ }) => (
+
+
+
+ {header}
+
-
-
- First Name
- John
+
+
+
+ {firstNameLabel}
+
+
+ {firstNameValue}
+
- Age
- John
-
+ {ageLabel}
+
+ {ageValue}
+
+
-
- Last Name
- Doe
+
+
+ {lastNameLabel}
+
+
+ {lastNameValue}
+
- Birth
- 01.01.1970
-
+
+ {birthLabel}
+
+
+ {birthValue}
+
+
-
- Status
-
-
-
+
+ Status
+
+
+
-
-
- Open Comments
-
-
-
-
-
-
- );
-};
+
+
+ {linkLabel}
+
+
+
+
+
+
+ ),
+}
diff --git a/src/components/table-key-value-pair/table-key-value-pair.test.tsx b/src/components/table-key-value-pair/table-key-value-pair.test.tsx
new file mode 100644
index 00000000..c038e202
--- /dev/null
+++ b/src/components/table-key-value-pair/table-key-value-pair.test.tsx
@@ -0,0 +1,23 @@
+import { describe, expect, it } from 'vitest'
+import { render, screen } from '@testing-library/react'
+import { TableKeyValuePair } from './table-key-value-pair'
+
+describe('TableKeyValuePair', () => {
+ it('renders header and body content', () => {
+ render(
+
+ Details
+
+
+ Key
+ Value
+
+
+ ,
+ )
+
+ expect(screen.getByText('Details')).toBeInTheDocument()
+ expect(screen.getByText('Key')).toBeInTheDocument()
+ expect(screen.getByText('Value')).toBeInTheDocument()
+ })
+})
diff --git a/src/components/table-key-value-pair/table-key-value-pair.tsx b/src/components/table-key-value-pair/table-key-value-pair.tsx
index c6637ef9..13317629 100644
--- a/src/components/table-key-value-pair/table-key-value-pair.tsx
+++ b/src/components/table-key-value-pair/table-key-value-pair.tsx
@@ -1,28 +1,32 @@
-import React from "react";
-import { classNames } from "../../util/class-names";
-import { TableKeyValuePairHeader } from "./table-key-value-pair-header";
-import { TableKeyValuePairBody } from "./table-key-value-pair-body";
+import type { DetailedHTMLProps, TableHTMLAttributes } from 'react'
+import { classNames } from '../../util/class-names'
+import { TableKeyValuePairHeader } from './table-key-value-pair-header'
+import { TableKeyValuePairBody } from './table-key-value-pair-body'
-export type TableKeyValuePairProps = React.DetailedHTMLProps<
- React.TableHTMLAttributes,
- HTMLTableElement
->;
+export type TableKeyValuePairProps = DetailedHTMLProps<
+ TableHTMLAttributes,
+ HTMLTableElement
+>
-const TableKeyValuePair = ({ children, className, ...props }: TableKeyValuePairProps) => {
- return (
-
- );
-};
+const TableKeyValuePair = ({
+ children,
+ className,
+ ...props
+}: TableKeyValuePairProps) => {
+ return (
+
+ )
+}
-TableKeyValuePair.Header = TableKeyValuePairHeader;
-TableKeyValuePair.Body = TableKeyValuePairBody;
+TableKeyValuePair.Header = TableKeyValuePairHeader
+TableKeyValuePair.Body = TableKeyValuePairBody
-export { TableKeyValuePair };
+export { TableKeyValuePair }
diff --git a/src/components/table-unvirtualized/index.ts b/src/components/table-unvirtualized/index.ts
index e84244f4..06533bfb 100644
--- a/src/components/table-unvirtualized/index.ts
+++ b/src/components/table-unvirtualized/index.ts
@@ -1 +1 @@
-export { TableUnvirtualized } from "./table-unvirtualized";
+export { TableUnvirtualized } from './table-unvirtualized'
diff --git a/src/components/table-unvirtualized/table-body/table-body-cell/table-body-cell.tsx b/src/components/table-unvirtualized/table-body/table-body-cell/table-body-cell.tsx
index 1e89742f..d6d04953 100644
--- a/src/components/table-unvirtualized/table-body/table-body-cell/table-body-cell.tsx
+++ b/src/components/table-unvirtualized/table-body/table-body-cell/table-body-cell.tsx
@@ -1,43 +1,51 @@
-import React, { CSSProperties, forwardRef, ReactNode } from "react";
-import { classNames } from "../../../../util/class-names";
+import { CSSProperties, forwardRef, ReactNode } from 'react'
+import { classNames } from '../../../../util/class-names'
export interface CellProps {
- // cells can be children-less (for e.g. placeholder columns)
- children?: ReactNode;
- style?: CSSProperties;
- colSpan?: number;
- className?: string;
- align?: CellAlignment;
- isTextContent?: boolean;
+ // cells can be children-less (for e.g. placeholder columns)
+ children?: ReactNode
+ style?: CSSProperties
+ colSpan?: number
+ className?: string
+ align?: CellAlignment
+ isTextContent?: boolean
}
-type CellAlignment = "left" | "center" | "right";
+type CellAlignment = 'left' | 'center' | 'right'
const cellAlignStyles: Record = {
- left: "",
- center: "flex justify-center items-center",
- right: "flex justify-end items-end",
-};
+ left: '',
+ center: 'flex justify-center items-center',
+ right: 'flex justify-end items-end',
+}
const TableBodyCell = forwardRef(
- ({ children, align = "left", isTextContent = true, className, colSpan, style }, ref) => {
- return (
-
- {isTextContent ? (
-
- {children}
-
- ) : (
- children
- )}
-
- );
- }
-);
+ (
+ {
+ children,
+ align = 'left',
+ isTextContent = true,
+ className,
+ colSpan,
+ style,
+ },
+ ref,
+ ) => {
+ return (
+
+ {isTextContent ?
+
+ {children}
+
+ : children}
+
+ )
+ },
+)
-export { TableBodyCell };
+export { TableBodyCell }
diff --git a/src/components/table-unvirtualized/table-body/table-body-loading-indicator/table-body-loading-indicator.tsx b/src/components/table-unvirtualized/table-body/table-body-loading-indicator/table-body-loading-indicator.tsx
index df196aab..5a03c2bc 100644
--- a/src/components/table-unvirtualized/table-body/table-body-loading-indicator/table-body-loading-indicator.tsx
+++ b/src/components/table-unvirtualized/table-body/table-body-loading-indicator/table-body-loading-indicator.tsx
@@ -1,20 +1,21 @@
-import React from "react";
-import { Spinner } from "../../../spinner/spinner";
-import { TableBodyRow } from "../table-body-row/table-body-row";
-import { TableBodyCell } from "../table-body-cell/table-body-cell";
+import { Spinner } from '../../../spinner/spinner'
+import { TableBodyRow } from '../table-body-row/table-body-row'
+import { TableBodyCell } from '../table-body-cell/table-body-cell'
export interface TableBodyLoadingIndicatorProps {
- colSpan: number;
+ colSpan: number
}
-export const TableBodyLoadingIndicator = ({ colSpan }: TableBodyLoadingIndicatorProps) => {
- return (
-
-
-
-
-
-
-
- );
-};
+export const TableBodyLoadingIndicator = ({
+ colSpan,
+}: TableBodyLoadingIndicatorProps) => {
+ return (
+
+
+
+
+
+
+
+ )
+}
diff --git a/src/components/table-unvirtualized/table-body/table-body-placeholder/table-body-placeholder.tsx b/src/components/table-unvirtualized/table-body/table-body-placeholder/table-body-placeholder.tsx
index 6932ef65..f7fa45d2 100644
--- a/src/components/table-unvirtualized/table-body/table-body-placeholder/table-body-placeholder.tsx
+++ b/src/components/table-unvirtualized/table-body/table-body-placeholder/table-body-placeholder.tsx
@@ -1,31 +1,31 @@
-import React from "react";
-import { TableBodyCell } from "../table-body-cell/table-body-cell";
-import { TableBodyRow } from "../table-body-row/table-body-row";
+import type { ReactNode } from 'react'
+import { TableBodyCell } from '../table-body-cell/table-body-cell'
+import { TableBodyRow } from '../table-body-row/table-body-row'
export interface EmptyPlaceholderProps {
- title: string;
- description: string;
- colSpan: number;
- children?: React.ReactNode;
+ title: string
+ description: string
+ colSpan: number
+ children?: ReactNode
}
export const TableEmptyPlaceholder = ({
- title,
- description,
- colSpan,
- children,
+ title,
+ description,
+ colSpan,
+ children,
}: EmptyPlaceholderProps) => {
- return (
-
-
-
-
{title}
+ return (
+
+
+
+
{title}
-
{description}
+
{description}
- {children}
-
-
-
- );
-};
+ {children}
+
+
+
+ )
+}
diff --git a/src/components/table-unvirtualized/table-body/table-body-row/table-body-row.tsx b/src/components/table-unvirtualized/table-body/table-body-row/table-body-row.tsx
index 7dc4a781..53be3aa4 100644
--- a/src/components/table-unvirtualized/table-body/table-body-row/table-body-row.tsx
+++ b/src/components/table-unvirtualized/table-body/table-body-row/table-body-row.tsx
@@ -1,28 +1,28 @@
-import React, { forwardRef, HTMLProps, ReactNode } from "react";
-import { classNames } from "../../../../util/class-names";
+import { forwardRef, HTMLProps, ReactNode } from 'react'
+import { classNames } from '../../../../util/class-names'
export interface RowProps extends HTMLProps {
- children: ReactNode;
- className?: string;
- isExpanded?: boolean;
+ children: ReactNode
+ className?: string
+ isExpanded?: boolean
}
const TableBodyRow = forwardRef(
- ({ children, className, isExpanded, style, ...props }, ref) => {
- return (
-
- {children}
-
- );
- }
-);
+ ({ children, className, isExpanded, style, ...props }, ref) => {
+ return (
+
+ {children}
+
+ )
+ },
+)
-export { TableBodyRow };
+export { TableBodyRow }
diff --git a/src/components/table-unvirtualized/table-body/table-body.tsx b/src/components/table-unvirtualized/table-body/table-body.tsx
index 0b6f317a..2154349e 100644
--- a/src/components/table-unvirtualized/table-body/table-body.tsx
+++ b/src/components/table-unvirtualized/table-body/table-body.tsx
@@ -1,20 +1,20 @@
-import React from "react";
-import { TableBodyRow } from "./table-body-row/table-body-row";
-import { TableBodyCell } from "./table-body-cell/table-body-cell";
-import { TableEmptyPlaceholder } from "./table-body-placeholder/table-body-placeholder";
-import { TableBodyLoadingIndicator } from "./table-body-loading-indicator/table-body-loading-indicator";
+import type { ReactNode } from 'react'
+import { TableBodyRow } from './table-body-row/table-body-row'
+import { TableBodyCell } from './table-body-cell/table-body-cell'
+import { TableEmptyPlaceholder } from './table-body-placeholder/table-body-placeholder'
+import { TableBodyLoadingIndicator } from './table-body-loading-indicator/table-body-loading-indicator'
export interface TableBodyProps {
- children: React.ReactNode;
+ children: ReactNode
}
const TableBody = ({ children }: TableBodyProps) => {
- return {children} ;
-};
+ return {children}
+}
-TableBody.LoadingIndicator = TableBodyLoadingIndicator;
-TableBody.EmptyPlaceholder = TableEmptyPlaceholder;
-TableBody.Row = TableBodyRow;
-TableBody.Cell = TableBodyCell;
+TableBody.LoadingIndicator = TableBodyLoadingIndicator
+TableBody.EmptyPlaceholder = TableEmptyPlaceholder
+TableBody.Row = TableBodyRow
+TableBody.Cell = TableBodyCell
-export { TableBody };
+export { TableBody }
diff --git a/src/components/table-unvirtualized/table-header/table-header-cell/table-header-cell.tsx b/src/components/table-unvirtualized/table-header/table-header-cell/table-header-cell.tsx
index 382dbb4b..cc8b4fd0 100644
--- a/src/components/table-unvirtualized/table-header/table-header-cell/table-header-cell.tsx
+++ b/src/components/table-unvirtualized/table-header/table-header-cell/table-header-cell.tsx
@@ -1,41 +1,41 @@
-import React, { CSSProperties, ReactNode } from "react";
-import { classNames } from "../../../../util/class-names";
+import { CSSProperties, ReactNode } from 'react'
+import { classNames } from '../../../../util/class-names'
-type TableCellAlign = "left" | "center" | "right";
+type TableCellAlign = 'left' | 'center' | 'right'
export interface TableHeaderCellProps {
- children: ReactNode;
- align?: TableCellAlign;
- className?: string;
- colSpan?: number;
- style?: CSSProperties;
+ children: ReactNode
+ align?: TableCellAlign
+ className?: string
+ colSpan?: number
+ style?: CSSProperties
}
const cellAlign: Record = {
- left: "text-left",
- center: "text-center",
- right: "text-right",
-};
+ left: 'text-left',
+ center: 'text-center',
+ right: 'text-right',
+}
export const TableHeaderCell = ({
- children,
- align = "left",
- colSpan,
- style,
- className,
+ children,
+ align = 'left',
+ colSpan,
+ style,
+ className,
}: TableHeaderCellProps) => {
- return (
-
- {children}
-
- );
-};
+ return (
+
+ {children}
+
+ )
+}
diff --git a/src/components/table-unvirtualized/table-header/table-header-row/table-header-row.tsx b/src/components/table-unvirtualized/table-header/table-header-row/table-header-row.tsx
index 731b4f3e..662537ae 100644
--- a/src/components/table-unvirtualized/table-header/table-header-row/table-header-row.tsx
+++ b/src/components/table-unvirtualized/table-header/table-header-row/table-header-row.tsx
@@ -1,9 +1,9 @@
-import React, { ReactNode } from "react";
+import { ReactNode } from 'react'
export interface TableHeaderRowProps {
- children: ReactNode;
+ children: ReactNode
}
export const TableHeaderRow = ({ children }: TableHeaderRowProps) => {
- return {children} ;
-};
+ return {children}
+}
diff --git a/src/components/table-unvirtualized/table-header/table-header.tsx b/src/components/table-unvirtualized/table-header/table-header.tsx
index 0d6779cc..8e5950f9 100644
--- a/src/components/table-unvirtualized/table-header/table-header.tsx
+++ b/src/components/table-unvirtualized/table-header/table-header.tsx
@@ -1,33 +1,36 @@
-import React, { ForwardedRef, forwardRef, ReactNode } from "react";
-import { TableHeaderCell } from "./table-header-cell/table-header-cell";
-import { TableHeaderRow } from "./table-header-row/table-header-row";
-import { classNames } from "../../../util/class-names";
+import { ForwardedRef, forwardRef, ReactNode } from 'react'
+import { TableHeaderCell } from './table-header-cell/table-header-cell'
+import { TableHeaderRow } from './table-header-row/table-header-row'
+import { classNames } from '../../../util/class-names'
export interface TableHeaderProps {
- children: ReactNode;
+ children: ReactNode
}
const TableHeader = forwardRef(
- ({ children }: TableHeaderProps, ref: ForwardedRef) => {
- return (
-
- {children}
-
- );
- }
-);
+ (
+ { children }: TableHeaderProps,
+ ref: ForwardedRef,
+ ) => {
+ return (
+
+ {children}
+
+ )
+ },
+)
-TableHeader.displayName = "TableHeader";
+TableHeader.displayName = 'TableHeader'
const TableHeaderNamespace = Object.assign(TableHeader, {
- Cell: TableHeaderCell,
- Row: TableHeaderRow,
-});
+ Cell: TableHeaderCell,
+ Row: TableHeaderRow,
+})
-export { TableHeaderNamespace as TableHeader };
+export { TableHeaderNamespace as TableHeader }
diff --git a/src/components/table-unvirtualized/table-unvirtualized.stories.tsx b/src/components/table-unvirtualized/table-unvirtualized.stories.tsx
index 21951fe3..63509b22 100644
--- a/src/components/table-unvirtualized/table-unvirtualized.stories.tsx
+++ b/src/components/table-unvirtualized/table-unvirtualized.stories.tsx
@@ -1,157 +1,192 @@
-import React, { useEffect, useMemo, useState } from "react";
-import type { Meta } from "@storybook/react";
-import { TableUnvirtualized } from "./table-unvirtualized";
-import { Button } from "../button/button";
-
-const meta: Meta = {
- title: "Table / Unvirtualized",
- component: TableUnvirtualized,
-};
+import { useEffect, useMemo, useState } from 'react'
+import type { Meta, StoryObj } from '@storybook/react-vite'
+import { TableUnvirtualized } from './table-unvirtualized'
+import { Button } from '../button/button'
interface ExampleData {
- id: string;
- name: string;
- description: string;
- price: number;
+ id: string
+ name: string
+ description: string
+ price: number
}
-export default meta;
-
-function createExampleData(): ExampleData[] {
- const data: ExampleData[] = [];
+function createExampleData(count = 20): ExampleData[] {
+ const data: ExampleData[] = []
- for (let i = 0; i < 20; i++) {
- const element: ExampleData = {
- id: i.toString(),
- name: `${i.toString()} name`,
- description: `${i.toString()} description`,
- price: i,
- };
- data.push(element);
+ for (let i = 0; i < count; i++) {
+ const element: ExampleData = {
+ id: i.toString(),
+ name: `${i.toString()} name`,
+ description: `${i.toString()} description`,
+ price: i,
}
- return data;
+ data.push(element)
+ }
+ return data
}
-export const Default = () => {
- const exampleData = useMemo(() => createExampleData(), []);
-
- return (
-
-
-
-
- Name
-
-
- Description
-
-
- Price
-
-
-
-
-
- {exampleData.map((item) => (
-
- {item.name}
-
-
- {item.description}
-
-
-
-
- $ {item.price.toFixed(2)}
-
-
-
- ))}
-
-
- );
-};
+const DefaultStory = ({
+ rowCount,
+ headerName,
+ headerDescription,
+ headerPrice,
+}: {
+ rowCount: number
+ headerName: string
+ headerDescription: string
+ headerPrice: string
+}) => {
+ const exampleData = useMemo(() => createExampleData(rowCount), [rowCount])
+
+ return (
+
+
+
+
+ {headerName}
+
+
+
+ {headerDescription}
+
+
+
+ {headerPrice}
+
+
+
+
+
+ {exampleData.map((item) => (
+
+
+ {item.name}
+
+
+
+ {item.description}
+
+
+
+
+ $ {item.price.toFixed(2)}
+
+
+
+ ))}
+
+
+ )
+}
+
+const meta: Meta = {
+ title: 'Table / Unvirtualized',
+ component: DefaultStory,
+ args: {
+ rowCount: 20,
+ headerName: 'Name',
+ headerDescription: 'Description',
+ headerPrice: 'Price',
+ },
+ argTypes: {
+ rowCount: { control: { type: 'number', min: 0, max: 100, step: 1 } },
+ headerName: { control: 'text' },
+ headerDescription: { control: 'text' },
+ headerPrice: { control: 'text' },
+ },
+}
+
+export default meta
+type Story = StoryObj
+
+export const Default: Story = {
+ render: (args) => ,
+}
export const EmptyTable = () => {
- return (
-
-
-
-
- Name
-
-
- Description
-
-
- Price
-
-
-
-
-
-
- alert("clicked")}>
- Add New Item
-
-
-
-
- );
-};
+ return (
+
+
+
+
+ Name
+
+
+
+ Description
+
+
+
+ Price
+
+
+
+
+
+
+ alert('clicked')}>
+ Add New Item
+
+
+
+
+ )
+}
export const LoadingTable = () => {
- const [data, setData] = useState([]);
-
- useEffect(() => {
- const timeout = setTimeout(() => {
- setData(createExampleData());
- }, 4000);
-
- return () => {
- clearTimeout(timeout);
- };
- }, []);
-
- return (
-
-
-
-
- Name
-
-
- Description
-
-
- Price
-
-
-
-
-
- {data.length ? (
- data.map((row) => (
-
- {row.name}
-
- {row.description}
-
-
-
- $ {row.price.toFixed(2)}
-
-
-
- ))
- ) : (
-
- )}
-
-
- );
-};
+ const [data, setData] = useState([])
+
+ useEffect(() => {
+ const timeout = setTimeout(() => {
+ setData(createExampleData())
+ }, 4000)
+
+ return () => {
+ clearTimeout(timeout)
+ }
+ }, [])
+
+ return (
+
+
+
+
+ Name
+
+
+
+ Description
+
+
+
+ Price
+
+
+
+
+
+ {data.length ?
+ data.map((row) => (
+
+
+ {row.name}
+
+
+ {row.description}
+
+
+
+ $ {row.price.toFixed(2)}
+
+
+
+ ))
+ : }
+
+
+ )
+}
diff --git a/src/components/table-unvirtualized/table-unvirtualized.test.tsx b/src/components/table-unvirtualized/table-unvirtualized.test.tsx
new file mode 100644
index 00000000..de3fb5ac
--- /dev/null
+++ b/src/components/table-unvirtualized/table-unvirtualized.test.tsx
@@ -0,0 +1,33 @@
+import { describe, expect, it } from 'vitest'
+import { render, screen } from '@testing-library/react'
+import { TableUnvirtualized } from './table-unvirtualized'
+
+describe('TableUnvirtualized', () => {
+ it('renders headers and body rows', () => {
+ render(
+
+
+
+
+ Name
+
+
+ Description
+
+
+
+
+
+ Alpha
+ First
+
+
+ ,
+ )
+
+ expect(screen.getByText('Name')).toBeInTheDocument()
+ expect(screen.getByText('Description')).toBeInTheDocument()
+ expect(screen.getByText('Alpha')).toBeInTheDocument()
+ expect(screen.getByText('First')).toBeInTheDocument()
+ })
+})
diff --git a/src/components/table-unvirtualized/table-unvirtualized.tsx b/src/components/table-unvirtualized/table-unvirtualized.tsx
index 68becce7..af809083 100644
--- a/src/components/table-unvirtualized/table-unvirtualized.tsx
+++ b/src/components/table-unvirtualized/table-unvirtualized.tsx
@@ -1,37 +1,43 @@
-import React from "react";
-import { TableBody } from "./table-body/table-body";
-import { TableHeader } from "./table-header/table-header";
-import { classNames } from "../../util/class-names";
+import type { ReactNode } from 'react'
+import { TableBody } from './table-body/table-body'
+import { TableHeader } from './table-header/table-header'
+import { classNames } from '../../util/class-names'
interface TableUnvirtualizedProps {
- children: React.ReactNode;
- height?: number;
- isContainerBordersShown?: boolean;
- hasFixedTableLayout?: boolean;
+ children: ReactNode
+ height?: number
+ isContainerBordersShown?: boolean
+ hasFixedTableLayout?: boolean
}
const TableUnvirtualized = ({
- children,
- height,
- isContainerBordersShown = true,
- hasFixedTableLayout,
+ children,
+ height,
+ isContainerBordersShown = true,
+ hasFixedTableLayout,
}: TableUnvirtualizedProps) => {
- return (
-
- );
-};
+ return (
+
+ )
+}
-TableUnvirtualized.Body = TableBody;
-TableUnvirtualized.Header = TableHeader;
+TableUnvirtualized.Body = TableBody
+TableUnvirtualized.Header = TableHeader
-export { TableUnvirtualized };
+export { TableUnvirtualized }
diff --git a/src/components/table-virtualized/draggable-row/draggable-row.tsx b/src/components/table-virtualized/draggable-row/draggable-row.tsx
index 6cdc6817..2a3e5943 100644
--- a/src/components/table-virtualized/draggable-row/draggable-row.tsx
+++ b/src/components/table-virtualized/draggable-row/draggable-row.tsx
@@ -1,96 +1,115 @@
-import React, { Fragment, ReactNode, Ref } from "react";
-import { DragSourceMonitor, useDrag, useDrop } from "react-dnd";
-import { Row as RowType } from "@tanstack/table-core";
-import { flexRender } from "@tanstack/react-table";
-import { TableUnvirtualized } from "../../table-unvirtualized";
-import { ExpandableButtonCell } from "../expandable-button-cell/expandable-button-cell";
-import { DragHandleVerticalIcon } from "../../../icons";
+import type { Ref, RefCallback, ReactNode } from 'react'
+import { Fragment } from 'react'
+import { DragSourceMonitor, useDrag, useDrop } from 'react-dnd'
+import { Row as RowType } from '@tanstack/table-core'
+import { flexRender } from '@tanstack/react-table'
+import { TableUnvirtualized } from '../../table-unvirtualized'
+import { ExpandableButtonCell } from '../expandable-button-cell/expandable-button-cell'
+import { DragHandleVerticalIcon } from '../../../icons'
export interface DraggableAndExpandableRow {
- getExpandableContent?: (row: RowType, index: number) => ReactNode;
+ getExpandableContent?: (row: RowType, index: number) => ReactNode
}
-export interface DraggableRowProps extends DraggableAndExpandableRow {
- row: RowType;
- reorderRow: (draggedRowIndex: number, targetRowIndex: number) => void;
- virtualMeasureRef?: Ref;
+export interface DraggableRowProps<
+ TableData,
+> extends DraggableAndExpandableRow {
+ row: RowType
+ reorderRow: (draggedRowIndex: number, targetRowIndex: number) => void
+ virtualMeasureRef?: Ref
}
export const DraggableRow = ({
- row,
- reorderRow,
- virtualMeasureRef,
- getExpandableContent,
+ row,
+ reorderRow,
+ virtualMeasureRef,
+ getExpandableContent,
}: DraggableRowProps) => {
- const [, dropRef] = useDrop({
- accept: "row",
- drop: (draggedRow: RowType) => {
- return reorderRow(draggedRow.index, row.index);
- },
- });
+ const [, dropRef] = useDrop({
+ accept: 'row',
+ drop: (draggedRow: RowType) => {
+ return reorderRow(draggedRow.index, row.index)
+ },
+ })
- const isExpanded = row.getIsExpanded();
- const isExpandableRowsEnabled = row.getCanExpand();
+ const isExpanded = row.getIsExpanded()
+ const isExpandableRowsEnabled = row.getCanExpand()
- const [{ isDragging }, dragRef, previewRef] = useDrag({
- collect: (monitor: DragSourceMonitor>) => ({
- isDragging: monitor.isDragging(),
- }),
- item: () => row,
- type: "row",
- isDragging: () => {
- if (row.getIsExpanded()) {
- row.toggleExpanded();
- }
- return true;
- },
- });
+ const [{ isDragging }, dragRef, previewRef] = useDrag({
+ collect: (monitor: DragSourceMonitor>) => ({
+ isDragging: monitor.isDragging(),
+ }),
+ item: () => row,
+ type: 'row',
+ isDragging: () => {
+ if (row.getIsExpanded()) {
+ row.toggleExpanded()
+ }
+ return true
+ },
+ })
- const [firstCell, ...restVisibleCells] = row.getVisibleCells();
+ const previewRefCallback: RefCallback = (node) => {
+ previewRef(node)
+ }
+ const dropRefCallback: RefCallback = (node) => {
+ dropRef(node)
+ }
+ const dragRefCallback: RefCallback = (node) => {
+ dragRef(node)
+ }
- return (
-
-
-
-
-
-
-
+ const [firstCell, ...restVisibleCells] = row.getVisibleCells()
-
- {flexRender(firstCell.column.columnDef.cell, firstCell.getContext())}
-
+ return (
+
+
+
+
+
+
+
- {restVisibleCells.map((cell) => (
-
- {flexRender(cell.column.columnDef.cell, cell.getContext())}
-
- ))}
+
+ {flexRender(firstCell.column.columnDef.cell, firstCell.getContext())}
+
- {isExpandableRowsEnabled && (
- row.toggleExpanded()}
- expanded={isExpanded}
- />
- )}
-
+ {restVisibleCells.map((cell) => (
+
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
+
+ ))}
- {isExpandableRowsEnabled && isExpanded && getExpandableContent ? (
-
-
-
- {getExpandableContent(row, row.index)}
-
-
- ) : null}
-
- );
-};
+ {isExpandableRowsEnabled && (
+ row.toggleExpanded()}
+ expanded={isExpanded}
+ />
+ )}
+
+
+ {isExpandableRowsEnabled && isExpanded && getExpandableContent ?
+
+
+
+ {getExpandableContent(row, row.index)}
+
+
+ : null}
+
+ )
+}
diff --git a/src/components/table-virtualized/expandable-button-cell/expandable-button-cell.tsx b/src/components/table-virtualized/expandable-button-cell/expandable-button-cell.tsx
index 515014b8..a0063180 100644
--- a/src/components/table-virtualized/expandable-button-cell/expandable-button-cell.tsx
+++ b/src/components/table-virtualized/expandable-button-cell/expandable-button-cell.tsx
@@ -1,27 +1,27 @@
-import React from "react";
-import { TableUnvirtualized } from "../../table-unvirtualized/table-unvirtualized";
-import { ChevronDownIcon, ChevronUpIcon } from "../../../icons";
+import { TableUnvirtualized } from '../../table-unvirtualized/table-unvirtualized'
+import { ChevronDownIcon, ChevronUpIcon } from '../../../icons'
interface ExpandableButtonCellProps {
- onClick: () => void;
- expanded: boolean;
+ onClick: () => void
+ expanded: boolean
}
-export const ExpandableButtonCell = ({ expanded, onClick }: ExpandableButtonCellProps) => {
- return (
-
-
- {expanded ? (
-
- ) : (
-
- )}
-
-
- );
-};
+export const ExpandableButtonCell = ({
+ expanded,
+ onClick,
+}: ExpandableButtonCellProps) => {
+ return (
+
+
+ {expanded ?
+
+ : }
+
+
+ )
+}
diff --git a/src/components/table-virtualized/header-cell/header-cell.tsx b/src/components/table-virtualized/header-cell/header-cell.tsx
index 0a1cdbf4..7e55dd64 100644
--- a/src/components/table-virtualized/header-cell/header-cell.tsx
+++ b/src/components/table-virtualized/header-cell/header-cell.tsx
@@ -1,43 +1,50 @@
-import React from "react";
-import { flexRender } from "@tanstack/react-table";
-import type { Header } from "@tanstack/react-table";
-import { SortIndicator, SortDirection } from "../header-sort-indicator/header-sort-indicator";
-import { TableUnvirtualized } from "../../table-unvirtualized/table-unvirtualized";
-import { classNames } from "../../../util/class-names";
+import { flexRender } from '@tanstack/react-table'
+import type { Header } from '@tanstack/react-table'
+import {
+ SortIndicator,
+ SortDirection,
+} from '../header-sort-indicator/header-sort-indicator'
+import { TableUnvirtualized } from '../../table-unvirtualized/table-unvirtualized'
+import { classNames } from '../../../util/class-names'
export interface VirtualizedHeaderCellProps {
- header: Header;
+ header: Header
}
-export const HeaderCell = ({ header }: VirtualizedHeaderCellProps) => {
- const canSort = header.column.getCanSort();
- const toggleSortHandler = canSort ? header.column.getToggleSortingHandler() : undefined;
+export const HeaderCell = ({
+ header,
+}: VirtualizedHeaderCellProps) => {
+ const canSort = header.column.getCanSort()
+ const toggleSortHandler =
+ canSort ? header.column.getToggleSortingHandler() : undefined
- const sortDirection = header.column.getIsSorted() as SortDirection;
+ const sortDirection = header.column.getIsSorted() as SortDirection
- return (
-
+ {header.isPlaceholder ? null : (
+
- {header.isPlaceholder ? null : (
-
- {flexRender(header.column.columnDef.header, header.getContext())}
+ {flexRender(header.column.columnDef.header, header.getContext())}
- {canSort && sortDirection ? : null}
-
- )}
-
- );
-};
+ {canSort && sortDirection ?
+
+ : null}
+
+ )}
+
+ )
+}
diff --git a/src/components/table-virtualized/header-group/header-group.tsx b/src/components/table-virtualized/header-group/header-group.tsx
index 54d28176..0e47d317 100644
--- a/src/components/table-virtualized/header-group/header-group.tsx
+++ b/src/components/table-virtualized/header-group/header-group.tsx
@@ -1,30 +1,33 @@
-import React from "react";
-import { HeaderGroup } from "@tanstack/react-table";
-import { HeaderCell } from "../header-cell/header-cell";
-import { TableUnvirtualized } from "../../table-unvirtualized/table-unvirtualized";
+import { HeaderGroup } from '@tanstack/react-table'
+import { HeaderCell } from '../header-cell/header-cell'
+import { TableUnvirtualized } from '../../table-unvirtualized/table-unvirtualized'
export interface VirtualizedHeaderGroupProps {
- group: HeaderGroup;
- isDraggableColumnEnabled?: boolean;
- isExpandableColumnEnabled?: boolean;
+ group: HeaderGroup
+ isDraggableColumnEnabled?: boolean
+ isExpandableColumnEnabled?: boolean
}
export const VirtualizedHeaderGroup = ({
- group,
- isDraggableColumnEnabled,
- isExpandableColumnEnabled,
+ group,
+ isDraggableColumnEnabled,
+ isExpandableColumnEnabled,
}: VirtualizedHeaderGroupProps) => {
- return (
-
- {/* eslint-disable-next-line jsx-a11y/control-has-associated-label */}
- {isDraggableColumnEnabled ? : null}
+ return (
+
+ {/* eslint-disable-next-line jsx-a11y/control-has-associated-label */}
+ {isDraggableColumnEnabled ?
+
+ : null}
- {group.headers.map((header) => (
- key={header.id} header={header} />
- ))}
+ {group.headers.map((header) => (
+ key={header.id} header={header} />
+ ))}
- {/* eslint-disable-next-line jsx-a11y/control-has-associated-label */}
- {isExpandableColumnEnabled ? : null}
-
- );
-};
+ {/* eslint-disable-next-line jsx-a11y/control-has-associated-label */}
+ {isExpandableColumnEnabled ?
+
+ : null}
+
+ )
+}
diff --git a/src/components/table-virtualized/header-sort-indicator/header-sort-indicator.tsx b/src/components/table-virtualized/header-sort-indicator/header-sort-indicator.tsx
index 8d2ac43d..8ad78727 100644
--- a/src/components/table-virtualized/header-sort-indicator/header-sort-indicator.tsx
+++ b/src/components/table-virtualized/header-sort-indicator/header-sort-indicator.tsx
@@ -1,21 +1,18 @@
-import React from "react";
-import { CaretDownIcon, CaretUpIcon } from "../../../icons";
+import { CaretDownIcon, CaretUpIcon } from '../../../icons'
-export type SortDirection = "asc" | "desc";
+export type SortDirection = 'asc' | 'desc'
export interface SortIndicatorProps {
- direction: SortDirection;
+ direction: SortDirection
}
export const SortIndicator = ({ direction }: SortIndicatorProps) => {
- const iconClassName = "ml-1 w-3.5 fill-neutral-600";
- const Icon =
- direction === "asc" ? (
-
- ) : (
-
- );
+ const iconClassName = 'ml-1 w-3.5 fill-neutral-600'
+ const Icon =
+ direction === 'asc' ?
+
+ :
- // eslint-disable-next-line react/jsx-no-useless-fragment
- return <>{Icon}>;
-};
+ // eslint-disable-next-line react/jsx-no-useless-fragment
+ return <>{Icon}>
+}
diff --git a/src/components/table-virtualized/index.ts b/src/components/table-virtualized/index.ts
index a86b0a13..1cbeae16 100644
--- a/src/components/table-virtualized/index.ts
+++ b/src/components/table-virtualized/index.ts
@@ -1 +1 @@
-export { TableVirtualized, TableVirtualizedProps } from "./table-virtualized";
+export { TableVirtualized, TableVirtualizedProps } from './table-virtualized'
diff --git a/src/components/table-virtualized/table-virtualized.stories.tsx b/src/components/table-virtualized/table-virtualized.stories.tsx
index 15ccdac8..247dc43c 100644
--- a/src/components/table-virtualized/table-virtualized.stories.tsx
+++ b/src/components/table-virtualized/table-virtualized.stories.tsx
@@ -1,264 +1,297 @@
-import React, { useEffect, useMemo, useState } from "react";
-import type { Meta, StoryObj } from "@storybook/react";
-import { createColumnHelper } from "@tanstack/react-table";
-import { TableVirtualized, WithDragAndDrop } from "./table-virtualized";
-import { Button } from "../button";
-import { IconButton } from "../icon-button";
-import { TableUnvirtualized } from "../table-unvirtualized";
-import { DividerLine } from "../divider-line";
-import { TrashIcon } from "../../icons";
-
-const meta: Meta = {
- title: "Table / Virtualized",
- component: TableVirtualized,
-};
+import { useEffect, useMemo, useState } from 'react'
+import type { Meta, StoryObj } from '@storybook/react-vite'
+import { createColumnHelper } from '@tanstack/react-table'
+import { TableVirtualized, WithDragAndDrop } from './table-virtualized'
+import { Button } from '../button'
+import { IconButton } from '../icon-button'
+import { TableUnvirtualized } from '../table-unvirtualized'
+import { DividerLine } from '../divider-line'
+import { TrashIcon } from '../../icons'
interface ExampleData {
- name: string;
- description: string;
- price: number;
- action: string;
+ name: string
+ description: string
+ price: number
+ action: string
}
-export default meta;
-type Story = StoryObj;
+function createExampleData(count = 20): ExampleData[] {
+ const data: ExampleData[] = []
-function createExampleData(): ExampleData[] {
- const data: ExampleData[] = [];
-
- for (let i = 0; i < 20; i++) {
- const element: ExampleData = {
- name: `${i.toString()} name`,
- description: `${i.toString()} description`,
- price: i,
- action: `${i.toString()} action`,
- };
- data.push(element);
+ for (let i = 0; i < count; i++) {
+ const element: ExampleData = {
+ name: `${i.toString()} name`,
+ description: `${i.toString()} description`,
+ price: i,
+ action: `${i.toString()} action`,
}
- return data;
+ data.push(element)
+ }
+ return data
+}
+
+const DefaultStory = ({
+ rowCount,
+ showDivider,
+ expandableRows,
+}: {
+ rowCount: number
+ showDivider: boolean
+ expandableRows: boolean
+}) => {
+ const columnHelper = createColumnHelper()
+ const exampleData = useMemo(() => createExampleData(rowCount), [rowCount])
+ const [data, setData] = useState(exampleData)
+
+ const columnDefs = [
+ columnHelper.accessor('name', {
+ header: 'Team Members',
+ enableSorting: true,
+ }),
+ columnHelper.accessor('description', {
+ header: 'Description',
+ enableSorting: true,
+ }),
+ ]
+
+ return (
+ <>
+
+
+ data={data}
+ columnDefs={columnDefs}
+ isExpandableRowsEnabled={expandableRows}
+ getExpandableContent={(row) => (
+ {row.original.description}
+ )}
+ />
+
+
+ {showDivider ?
+
+ : null}
+
+
+ Entries: {data.length.toLocaleString()}
+
+
+ setData(createExampleData(rowCount))}
+ >
+ Recreate Data
+
+ >
+ )
}
-const DefaultStory = () => {
- const columnHelper = createColumnHelper();
- const exampleData = useMemo(() => createExampleData(), []);
- const [data, setData] = useState(exampleData);
-
- const columnDefs = [
- columnHelper.accessor("name", {
- header: "Team Members",
- enableSorting: true,
- }),
- columnHelper.accessor("description", {
- header: "Description",
- enableSorting: true,
- }),
- ];
-
- return (
- <>
-
-
- data={data}
- columnDefs={columnDefs}
- isExpandableRowsEnabled
- getExpandableContent={(row) => (
- {row.original.description}
- )}
- />
-
-
-
-
- Entries: {data.length.toLocaleString()}
-
- setData(createExampleData())}>
- Recreate Data
-
- >
- );
-};
+const meta: Meta = {
+ title: 'Table / Virtualized',
+ component: DefaultStory,
+ args: {
+ rowCount: 20,
+ showDivider: true,
+ expandableRows: true,
+ },
+ argTypes: {
+ rowCount: { control: { type: 'number', min: 0, max: 100, step: 1 } },
+ showDivider: { control: 'boolean' },
+ expandableRows: { control: 'boolean' },
+ },
+}
+
+export default meta
+type Story = StoryObj
export const Default: Story = {
- render: () => ,
-};
+ render: (args) => ,
+}
export const Draggable = () => {
- const columnHelper = createColumnHelper();
- const exampleData = useMemo(() => createExampleData(), []);
- const [data, setData] = useState(exampleData);
-
- const columnDefs = [
- columnHelper.accessor("name", {
- header: "Team Members",
- enableSorting: true,
- meta: {
- width: "200px",
- },
- }),
- columnHelper.accessor("description", {
- header: "Description",
- enableSorting: true,
- }),
- columnHelper.accessor("action", {
- header: "Action",
- enableSorting: false,
- meta: {
- textAlign: "right",
- width: 80,
- },
- cell: (row) => (
- row.getValue()} Icon={TrashIcon} variant="danger" />
- ),
- }),
- ];
-
- return (
- <>
-
- isDraggableRowsEnabled
- data={data}
- columnDefs={columnDefs}
- />
-
-
-
- Entries: {data.length.toLocaleString()}
-
- setData(createExampleData())}>
- Recreate Data
-
- >
- );
-};
+ const columnHelper = createColumnHelper()
+ const exampleData = useMemo(() => createExampleData(), [])
+ const [data, setData] = useState(exampleData)
+
+ const columnDefs = [
+ columnHelper.accessor('name', {
+ header: 'Team Members',
+ enableSorting: true,
+ meta: {
+ width: '200px',
+ },
+ }),
+ columnHelper.accessor('description', {
+ header: 'Description',
+ enableSorting: true,
+ }),
+ columnHelper.accessor('action', {
+ header: 'Action',
+ enableSorting: false,
+ meta: {
+ textAlign: 'right',
+ width: 80,
+ },
+ cell: (row) => (
+ row.getValue()}
+ Icon={TrashIcon}
+ variant='danger'
+ />
+ ),
+ }),
+ ]
+
+ return (
+ <>
+
+ isDraggableRowsEnabled
+ data={data}
+ columnDefs={columnDefs}
+ />
+
+
+
+
+ Entries: {data.length.toLocaleString()}
+
+
+ setData(createExampleData())}>
+ Recreate Data
+
+ >
+ )
+}
export const DraggableAndExpandable = () => {
- const columnHelper = createColumnHelper();
- const exampleData = useMemo(() => createExampleData(), []);
- const [data, setData] = useState(exampleData);
-
- const columnDefs = [
- columnHelper.accessor("name", {
- header: "Team Members",
- enableSorting: true,
- meta: {
- width: "200px",
- },
- }),
- columnHelper.accessor("description", {
- header: "Description",
- enableSorting: true,
- }),
- ];
-
- return (
- <>
-
- isDraggableRowsEnabled
- isExpandableRowsEnabled
- // eslint-disable-next-line react/no-unstable-nested-components
- getExpandableContent={(row) => (
- {row.original.description}
- )}
- data={data}
- columnDefs={columnDefs}
- />
-
-
-
- Entries: {data.length.toLocaleString()}
-
- setData(createExampleData())}>
- Recreate Data
-
- >
- );
-};
+ const columnHelper = createColumnHelper()
+ const exampleData = useMemo(() => createExampleData(), [])
+ const [data, setData] = useState(exampleData)
+
+ const columnDefs = [
+ columnHelper.accessor('name', {
+ header: 'Team Members',
+ enableSorting: true,
+ meta: {
+ width: '200px',
+ },
+ }),
+ columnHelper.accessor('description', {
+ header: 'Description',
+ enableSorting: true,
+ }),
+ ]
+
+ return (
+ <>
+
+ isDraggableRowsEnabled
+ isExpandableRowsEnabled
+ // eslint-disable-next-line react/no-unstable-nested-components
+ getExpandableContent={(row) => (
+ {row.original.description}
+ )}
+ data={data}
+ columnDefs={columnDefs}
+ />
+
+
+
+
+ Entries: {data.length.toLocaleString()}
+
+
+ setData(createExampleData())}>
+ Recreate Data
+
+ >
+ )
+}
export const EmptyTable = () => {
- const columnHelper = createColumnHelper();
- const exampleData: ExampleData[] = [];
-
- const columnDefs = [
- columnHelper.accessor("name", {
- header: "Team Members",
- enableSorting: true,
- meta: {
- width: "200px",
- },
- }),
- columnHelper.accessor("description", {
- header: "Description",
- enableSorting: true,
- }),
- ];
-
- return (
-
- isDraggableRowsEnabled
- data={exampleData}
- columnDefs={columnDefs}
- isExpandableRowsEnabled
- // eslint-disable-next-line react/no-unstable-nested-components
- getExpandableContent={(row) => (
- {row.original.description}
- )}
- placeholder={
-
- alert("clicked")}>
- Add New Item
-
-
- }
- />
- );
-};
+ const columnHelper = createColumnHelper()
+ const exampleData: ExampleData[] = []
+
+ const columnDefs = [
+ columnHelper.accessor('name', {
+ header: 'Team Members',
+ enableSorting: true,
+ meta: {
+ width: '200px',
+ },
+ }),
+ columnHelper.accessor('description', {
+ header: 'Description',
+ enableSorting: true,
+ }),
+ ]
+
+ return (
+
+ isDraggableRowsEnabled
+ data={exampleData}
+ columnDefs={columnDefs}
+ isExpandableRowsEnabled
+ // eslint-disable-next-line react/no-unstable-nested-components
+ getExpandableContent={(row) => (
+ {row.original.description}
+ )}
+ placeholder={
+
+ alert('clicked')}>
+ Add New Item
+
+
+ }
+ />
+ )
+}
export const LoadingTable = () => {
- const columnHelper = createColumnHelper();
- const [data, setData] = useState([]);
-
- useEffect(() => {
- const timeout = setTimeout(() => {
- setData(createExampleData());
- }, 4000);
-
- return () => {
- clearTimeout(timeout);
- };
- }, []);
-
- const columnDefs = [
- columnHelper.accessor("name", {
- header: "Team Members",
- enableSorting: true,
- meta: {
- width: "200px",
- },
- }),
- columnHelper.accessor("description", {
- header: "Description",
- enableSorting: true,
- }),
- ];
-
- return (
-
-
- isDraggableRowsEnabled
- data={data}
- columnDefs={columnDefs}
- placeholder={
-
- }
- />
-
- );
-};
+ const columnHelper = createColumnHelper()
+ const [data, setData] = useState([])
+
+ useEffect(() => {
+ const timeout = setTimeout(() => {
+ setData(createExampleData())
+ }, 4000)
+
+ return () => {
+ clearTimeout(timeout)
+ }
+ }, [])
+
+ const columnDefs = [
+ columnHelper.accessor('name', {
+ header: 'Team Members',
+ enableSorting: true,
+ meta: {
+ width: '200px',
+ },
+ }),
+ columnHelper.accessor('description', {
+ header: 'Description',
+ enableSorting: true,
+ }),
+ ]
+
+ return (
+
+
+ isDraggableRowsEnabled
+ data={data}
+ columnDefs={columnDefs}
+ placeholder={
+
+ }
+ />
+
+ )
+}
diff --git a/src/components/table-virtualized/table-virtualized.test.tsx b/src/components/table-virtualized/table-virtualized.test.tsx
new file mode 100644
index 00000000..39581643
--- /dev/null
+++ b/src/components/table-virtualized/table-virtualized.test.tsx
@@ -0,0 +1,56 @@
+import { describe, expect, it } from 'vitest'
+import { render, screen } from '@testing-library/react'
+import { createColumnHelper } from '@tanstack/react-table'
+import { TableVirtualized } from './table-virtualized'
+
+interface RowData {
+ name: string
+ description: string
+}
+
+class ResizeObserverMock {
+ observe() {}
+ unobserve() {}
+ disconnect() {}
+}
+
+describe('TableVirtualized', () => {
+ it('renders headers and rows', async () => {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ ;(window as any).ResizeObserver = ResizeObserverMock
+
+ const columnHelper = createColumnHelper()
+ const columnDefs = [
+ columnHelper.accessor('name', { header: 'Name' }),
+ columnHelper.accessor('description', { header: 'Description' }),
+ ]
+ const data = [
+ { name: 'Alpha', description: 'First' },
+ { name: 'Beta', description: 'Second' },
+ ]
+
+ render(
+
+
+ data={data}
+ columnDefs={columnDefs}
+ virtualizerOptions={{
+ observeElementRect: (_instance, callback) => {
+ callback({ height: 300, width: 300 })
+ return () => {}
+ },
+ observeElementOffset: (_instance, callback) => {
+ callback(0)
+ return () => {}
+ },
+ }}
+ />
+ ,
+ )
+
+ expect(screen.getByText('Name')).toBeInTheDocument()
+ expect(screen.getByText('Description')).toBeInTheDocument()
+ expect(await screen.findByText('Alpha')).toBeInTheDocument()
+ expect(await screen.findByText('Second')).toBeInTheDocument()
+ })
+})
diff --git a/src/components/table-virtualized/table-virtualized.tsx b/src/components/table-virtualized/table-virtualized.tsx
index 0f8f9e56..258f7a1f 100644
--- a/src/components/table-virtualized/table-virtualized.tsx
+++ b/src/components/table-virtualized/table-virtualized.tsx
@@ -1,210 +1,217 @@
+import { Fragment, useState, useEffect, useRef } from 'react'
+import type { ReactNode } from 'react'
import {
- ColumnDef,
- flexRender,
- getCoreRowModel,
- getExpandedRowModel,
- getSortedRowModel,
- HeaderGroup,
- PartialKeys,
- Row as RowType,
- SortingState,
- useReactTable,
-} from "@tanstack/react-table";
-import { useVirtualizer, VirtualizerOptions } from "@tanstack/react-virtual";
-import React from "react";
-import { DndProvider } from "react-dnd";
-import { HTML5Backend } from "react-dnd-html5-backend";
-import { classNames } from "../../util/class-names";
-import { TableUnvirtualized } from "../table-unvirtualized";
-import { DraggableRow } from "./draggable-row/draggable-row";
-import { ExpandableButtonCell } from "./expandable-button-cell/expandable-button-cell";
-import { VirtualizedHeaderGroup } from "./header-group/header-group";
+ ColumnDef,
+ flexRender,
+ getCoreRowModel,
+ getExpandedRowModel,
+ getSortedRowModel,
+ HeaderGroup,
+ PartialKeys,
+ Row as RowType,
+ SortingState,
+ useReactTable,
+} from '@tanstack/react-table'
+import { useVirtualizer, VirtualizerOptions } from '@tanstack/react-virtual'
+import { DndProvider } from 'react-dnd'
+import { HTML5Backend } from 'react-dnd-html5-backend'
+import { classNames } from '../../util/class-names'
+import { TableUnvirtualized } from '../table-unvirtualized'
+import { DraggableRow } from './draggable-row/draggable-row'
+import { ExpandableButtonCell } from './expandable-button-cell/expandable-button-cell'
+import { VirtualizedHeaderGroup } from './header-group/header-group'
export interface TableVirtualizedProps {
- data: TableData[];
- columnDefs: ColumnDef[];
- showPlaceholder?: boolean;
- placeholder?: React.ReactNode;
- isDraggableRowsEnabled?: boolean;
- isExpandableRowsEnabled?: boolean;
- getExpandableContent?: (row: RowType, index: number) => React.ReactNode;
- virtualizerOptions?: PartialKeys<
- VirtualizerOptions,
- | "observeElementRect"
- | "observeElementOffset"
- | "scrollToFn"
- | "count"
- | "getScrollElement"
- | "estimateSize"
- >;
+ data: TableData[]
+ columnDefs: ColumnDef