Merge branch 'master' into unread-counts
This commit is contained in:
@@ -40,16 +40,20 @@ export type ButtonVariant = VariantProps<typeof buttonVariants>["variant"];
|
||||
|
||||
export interface ButtonProps
|
||||
extends
|
||||
React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {}
|
||||
React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> { }
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, ...props }, ref) => {
|
||||
({ className, variant, size, disabled, ...props }, ref) => {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
className={cn(
|
||||
buttonVariants({ variant, size, className }),
|
||||
{ "cursor-not-allowed": disabled }
|
||||
)}
|
||||
ref={ref}
|
||||
disabled={disabled}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
|
||||
import { Check } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@core/utils/cn.ts";
|
||||
|
||||
const Checkbox = React.forwardRef<
|
||||
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CheckboxPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"peer h-4 w-4 shrink-0 rounded-xs border border-slate-300 focus:outline-hidden focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-slate-700 dark:text-slate-50 dark:focus:ring-slate-400 dark:focus:ring-offset-slate-900",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
className={cn("flex items-center justify-center")}
|
||||
>
|
||||
<Check className="h-4 w-4" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
));
|
||||
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
|
||||
|
||||
export { Checkbox };
|
||||
@@ -0,0 +1,120 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { render, screen, fireEvent, cleanup } from '@testing-library/react';
|
||||
import { Checkbox } from '@components/UI/Checkbox/index.tsx';
|
||||
import React from "react";
|
||||
|
||||
vi.mock('@components/UI/Label.tsx', () => ({
|
||||
Label: ({ children, className, htmlFor, id }: { children: React.ReactNode; className: string; htmlFor: string; id: string }) => (
|
||||
<label data-testid="label-component" className={className} htmlFor={htmlFor} id={id}>
|
||||
{children}
|
||||
</label>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('@core/utils/cn.ts', () => ({
|
||||
cn: (...args: any) => args.filter(Boolean).join(' '),
|
||||
}));
|
||||
|
||||
vi.mock('react', async () => {
|
||||
const actual = await vi.importActual('react');
|
||||
return {
|
||||
...actual,
|
||||
useId: () => 'test-id',
|
||||
};
|
||||
});
|
||||
|
||||
describe('Checkbox', () => {
|
||||
beforeEach(cleanup);
|
||||
|
||||
it('renders unchecked by default', () => {
|
||||
render(<Checkbox />);
|
||||
const checkbox = screen.getByRole('checkbox');
|
||||
expect(checkbox).not.toBeChecked();
|
||||
expect(screen.queryByText('Check')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders checked when checked prop is true', () => {
|
||||
render(<Checkbox checked={true} />);
|
||||
expect(screen.getByRole('checkbox')).toBeChecked();
|
||||
expect(screen.getByRole('presentation')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls onChange when clicked', () => {
|
||||
const onChange = vi.fn();
|
||||
render(<Checkbox onChange={onChange} />);
|
||||
|
||||
fireEvent.click(screen.getByRole('presentation'));
|
||||
expect(onChange).toHaveBeenCalledWith(true);
|
||||
|
||||
fireEvent.click(screen.getByRole('presentation'));
|
||||
expect(onChange).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
it('uses provided id', () => {
|
||||
render(<Checkbox id="custom-id" />);
|
||||
expect(screen.getByRole('checkbox').id).toBe('custom-id');
|
||||
});
|
||||
|
||||
it('generates id when not provided', () => {
|
||||
render(<Checkbox />);
|
||||
expect(screen.getByRole('checkbox').id).toBe('test-id');
|
||||
});
|
||||
|
||||
it('renders children in Label component', () => {
|
||||
render(<Checkbox>Test Label</Checkbox>);
|
||||
expect(screen.getByTestId('label-component')).toHaveTextContent('Test Label');
|
||||
});
|
||||
|
||||
it('applies custom className', () => {
|
||||
const { container } = render(<Checkbox className="custom-class" />);
|
||||
expect(container.firstChild).toHaveClass('custom-class');
|
||||
});
|
||||
|
||||
it('applies labelClassName to Label', () => {
|
||||
render(<Checkbox labelClassName="label-class">Test</Checkbox>);
|
||||
expect(screen.getByTestId('label-component')).toHaveClass('label-class');
|
||||
});
|
||||
|
||||
it('disables checkbox when disabled prop is true', () => {
|
||||
render(<Checkbox disabled />);
|
||||
expect(screen.getByRole('checkbox')).toBeDisabled();
|
||||
expect(screen.getByRole('presentation')).toHaveClass('opacity-50');
|
||||
});
|
||||
|
||||
it('does not call onChange when disabled', () => {
|
||||
const onChange = vi.fn();
|
||||
render(<Checkbox onChange={onChange} disabled />);
|
||||
|
||||
fireEvent.click(screen.getByRole('presentation'));
|
||||
expect(onChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('sets required attribute when required prop is true', () => {
|
||||
render(<Checkbox required />);
|
||||
expect(screen.getByRole('checkbox')).toHaveAttribute('required');
|
||||
});
|
||||
|
||||
it('sets name attribute when name prop is provided', () => {
|
||||
render(<Checkbox name="test-name" />);
|
||||
expect(screen.getByRole('checkbox')).toHaveAttribute('name', 'test-name');
|
||||
});
|
||||
|
||||
it('passes through additional props', () => {
|
||||
render(<Checkbox data-testid="extra-prop" />);
|
||||
expect(screen.getByRole('checkbox')).toHaveAttribute('data-testid', 'extra-prop');
|
||||
});
|
||||
|
||||
it('toggles checked state correctly', () => {
|
||||
render(<Checkbox />);
|
||||
const checkbox = screen.getByRole('checkbox');
|
||||
const presentation = screen.getByRole('presentation');
|
||||
|
||||
expect(checkbox).not.toBeChecked();
|
||||
|
||||
fireEvent.click(presentation);
|
||||
expect(checkbox).toBeChecked();
|
||||
|
||||
fireEvent.click(presentation);
|
||||
expect(checkbox).not.toBeChecked();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,93 @@
|
||||
import { useState, useId } from "react";
|
||||
import { Check } from "lucide-react";
|
||||
import { Label } from "@components/UI/Label.tsx";
|
||||
import { cn } from "@core/utils/cn.ts";
|
||||
|
||||
interface CheckboxProps {
|
||||
checked?: boolean;
|
||||
onChange?: (checked: boolean) => void;
|
||||
className?: string;
|
||||
labelClassName?: string;
|
||||
id?: string;
|
||||
children?: React.ReactNode;
|
||||
disabled?: boolean;
|
||||
required?: boolean;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export function Checkbox({
|
||||
checked,
|
||||
onChange,
|
||||
className,
|
||||
labelClassName,
|
||||
id: propId,
|
||||
children,
|
||||
disabled = false,
|
||||
required = false,
|
||||
name,
|
||||
...rest
|
||||
}: CheckboxProps) {
|
||||
const generatedId = useId();
|
||||
const id = propId || generatedId;
|
||||
|
||||
const [isChecked, setIsChecked] = useState(checked || false);
|
||||
|
||||
const handleToggle = () => {
|
||||
if (disabled) return;
|
||||
|
||||
const newChecked = !isChecked;
|
||||
setIsChecked(newChecked);
|
||||
onChange?.(newChecked);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn("flex items-center", className)}>
|
||||
<div className="relative flex items-start">
|
||||
<div className="flex items-center h-5">
|
||||
<input
|
||||
type="checkbox"
|
||||
id={id}
|
||||
checked={isChecked}
|
||||
onChange={handleToggle}
|
||||
disabled={disabled}
|
||||
required={required}
|
||||
name={name}
|
||||
className="sr-only"
|
||||
{...rest}
|
||||
/>
|
||||
<div
|
||||
onClick={handleToggle}
|
||||
role="presentation"
|
||||
className={cn(
|
||||
"w-6 h-6 border-2 border-gray-500 rounded-md flex items-center justify-center",
|
||||
disabled ? "opacity-50 cursor-not-allowed" : "cursor-pointer focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2",
|
||||
isChecked ? "" : ""
|
||||
)}
|
||||
>
|
||||
{isChecked && (
|
||||
<div className="animate-fade-in scale-100 opacity-100">
|
||||
<Check className="w-4 h-4 text-slate-900 dark:text-slate-900" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{children && (
|
||||
<div className="ml-3 text-sm">
|
||||
<Label
|
||||
htmlFor={id}
|
||||
id={`${id}-label`}
|
||||
className={cn(
|
||||
"text-gray-900 dark:text-gray-900",
|
||||
disabled ? "opacity-50 cursor-not-allowed" : "cursor-pointer",
|
||||
labelClassName
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</Label>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -50,15 +50,28 @@ const DialogContent = React.forwardRef<
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:outline-hidden focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-slate-100 dark:focus:ring-slate-400 dark:focus:ring-offset-slate-900">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
));
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
||||
|
||||
const DialogClose = ({
|
||||
className,
|
||||
...props
|
||||
}: DialogPrimitive.DialogCloseProps & React.RefAttributes<HTMLButtonElement> & { className?: string }) => (
|
||||
<DialogPrimitive.Close
|
||||
name="close"
|
||||
className={cn(
|
||||
"absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:outline-hidden focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-slate-100 dark:focus:ring-slate-400 dark:focus:ring-offset-slate-900",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
);
|
||||
|
||||
const DialogHeader = ({
|
||||
className,
|
||||
...props
|
||||
@@ -119,4 +132,5 @@ export {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
DialogClose,
|
||||
};
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import newGithubIssueUrl from "../../core/utils/github.ts";
|
||||
import newGithubIssueUrl from "@core/utils/github.ts";
|
||||
import { ExternalLink } from "lucide-react";
|
||||
import { Heading } from "./Typography/Heading.tsx";
|
||||
import { Link } from "./Typography/Link.tsx";
|
||||
import { P } from "./Typography/P.tsx";
|
||||
import { Heading } from "@components/UI/Typography/Heading.tsx";
|
||||
import { Link } from "@components/UI/Typography/Link.tsx";
|
||||
import { P } from "@components/UI/Typography/P.tsx";
|
||||
|
||||
|
||||
export function ErrorPage({ error }: { error: Error }) {
|
||||
@@ -11,8 +11,8 @@ export function ErrorPage({ error }: { error: Error }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<article className="w-full overflow-y-auto">
|
||||
<section className="flex shrink md:flex-row gap-16 mt-20 px-4 md:px-8 text-lg md:text-xl space-y-2 place-items-center">
|
||||
<article className="w-full h-screen overflow-y-auto dark:bg-background-primary dark:text-text-primary">
|
||||
<section className="flex shrink md:flex-row gap-16 mt-20 px-4 md:px-8 text-lg md:text-xl space-y-2 place-items-center dark:bg-background-primary text-slate-900 dark:text-text-primary">
|
||||
<div>
|
||||
<Heading as="h2" className="text-text-primary">
|
||||
This is a little embarrassing...
|
||||
|
||||
@@ -12,7 +12,7 @@ export const Link = ({ href, children, className }: LinkProps) => (
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={cn(
|
||||
"font-medium text-slate-900 underline underline-offset-4 dark:text-slate-50",
|
||||
"font-medium text-slate-900 underline underline-offset-4 dark:text-slate-900",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user