fix(ds): add test id support (#9904)

This commit is contained in:
Mislav Lukach
2025-07-25 17:37:25 +02:00
committed by GitHub
parent b8b4f58a79
commit 59b8009d7a
23 changed files with 150 additions and 112 deletions

View File

@@ -5,13 +5,13 @@ import {
type AccordionItemPropsPublic, type AccordionItemPropsPublic,
} from "./components/AccordionItem"; } from "./components/AccordionItem";
import { cn } from "../../shared/utils/cn"; import { cn } from "../../shared/utils/cn";
import type { HTMLProps } from "../../shared/types"; import type { BaseProps, HTMLProps } from "../../shared/types";
export type AccordionProps = HTMLProps<"div"> & { export type AccordionProps = HTMLProps<"div"> & {
expandedKeys: string[]; expandedKeys: string[];
type?: "multi" | "single"; type?: "multi" | "single";
setExpandedKeys(keys: string[]): void; setExpandedKeys(keys: string[]): void;
}; } & BaseProps;
type AccordionType = React.FC<PropsWithChildren<AccordionProps>> & { type AccordionType = React.FC<PropsWithChildren<AccordionProps>> & {
Item: React.FC<PropsWithChildren<AccordionItemPropsPublic>>; Item: React.FC<PropsWithChildren<AccordionItemPropsPublic>>;
@@ -23,6 +23,7 @@ const Accordion: AccordionType = ({
setExpandedKeys, setExpandedKeys,
children, children,
type = "multi", type = "multi",
testId,
...props ...props
}) => { }) => {
const onChange = useCallback( const onChange = useCallback(
@@ -54,6 +55,7 @@ const Accordion: AccordionType = ({
return ( return (
<div <div
className={cn("flex flex-col gap-y-2.5 items-start", className)} className={cn("flex flex-col gap-y-2.5 items-start", className)}
data-testid={testId}
{...props} {...props}
> >
{items} {items}

View File

@@ -1,5 +1,5 @@
import type { PropsWithChildren } from "react"; import type { PropsWithChildren } from "react";
import type { HTMLProps } from "../../../shared/types"; import type { BaseProps, HTMLProps } from "../../../shared/types";
import { cn } from "../../../shared/utils/cn"; import { cn } from "../../../shared/utils/cn";
import { Icon, type IconProps } from "../../icon/Icon"; import { Icon, type IconProps } from "../../icon/Icon";
import { Typography } from "../../typography/Typography"; import { Typography } from "../../typography/Typography";
@@ -10,7 +10,7 @@ export type AccordionHeaderProps = Omit<
> & { > & {
icon: IconProps["icon"]; icon: IconProps["icon"];
expanded: boolean; expanded: boolean;
}; } & BaseProps;
export const AccordionHeader = ({ export const AccordionHeader = ({
className, className,
@@ -43,7 +43,8 @@ export const AccordionHeader = ({
// hover modifier // hover modifier
"data-[expanded=true]:hover:bg-light-neutral-900", "data-[expanded=true]:hover:bg-light-neutral-900",
// focus modifier // focus modifier
"data-[expanded=false]:focus:bg-light-neutral-900" "data-[expanded=false]:focus:bg-light-neutral-900",
className
)} )}
> >
<Icon icon={icon} className={cn(iconCss, "w-6 h-6")} /> <Icon icon={icon} className={cn(iconCss, "w-6 h-6")} />

View File

@@ -1,5 +1,5 @@
import type { PropsWithChildren } from "react"; import type { PropsWithChildren } from "react";
import type { HTMLProps } from "../../../shared/types"; import type { BaseProps, HTMLProps } from "../../../shared/types";
import { cn } from "../../../shared/utils/cn"; import { cn } from "../../../shared/utils/cn";
import { type IconProps } from "../../icon/Icon"; import { type IconProps } from "../../icon/Icon";
import { AccordionHeader } from "./AccordionHeader"; import { AccordionHeader } from "./AccordionHeader";
@@ -11,10 +11,10 @@ export type AccordionItemProps = HTMLProps<"div"> & {
value: string; value: string;
label: React.ReactNode; label: React.ReactNode;
onExpandedChange(value: boolean): void; onExpandedChange(value: boolean): void;
}; } & BaseProps;
export type AccordionItemPropsPublic = Omit< export type AccordionItemPropsPublic = Omit<
AccordionItemProps, AccordionItemProps,
"expanded" | "onExpandedChange" "expanded" | "onExpandedChange" | "className" | "style" | "testId"
>; >;
export const AccordionItem = ({ export const AccordionItem = ({

View File

@@ -1,10 +1,10 @@
import type { PropsWithChildren } from "react"; import type { PropsWithChildren } from "react";
import type { HTMLProps } from "../../../shared/types"; import type { BaseProps, HTMLProps } from "../../../shared/types";
import { cn } from "../../../shared/utils/cn"; import { cn } from "../../../shared/utils/cn";
export type AccordionPanelProps = Omit<HTMLProps<"div">, "aria-expanded"> & { export type AccordionPanelProps = Omit<HTMLProps<"div">, "aria-expanded"> & {
expanded: boolean; expanded: boolean;
}; } & BaseProps;
export const AccordionPanel = ({ export const AccordionPanel = ({
className, className,

View File

@@ -4,7 +4,11 @@ import {
type PropsWithChildren, type PropsWithChildren,
type ReactElement, type ReactElement,
} from "react"; } from "react";
import type { ComponentVariant, HTMLProps } from "../../shared/types"; import type {
BaseProps,
ComponentVariant,
HTMLProps,
} from "../../shared/types";
import { cn } from "../../shared/utils/cn"; import { cn } from "../../shared/utils/cn";
import { buttonStyles, useAndApplyBoldTextWidth } from "./utils"; import { buttonStyles, useAndApplyBoldTextWidth } from "./utils";
import { cloneIcon } from "../../shared/utils/clone-icon"; import { cloneIcon } from "../../shared/utils/clone-icon";
@@ -15,7 +19,7 @@ export type ButtonProps = Omit<HTMLProps<"button">, "aria-disabled"> & {
variant?: ComponentVariant; variant?: ComponentVariant;
start?: ReactElement<HTMLProps<"svg">>; start?: ReactElement<HTMLProps<"svg">>;
end?: ReactElement<HTMLProps<"svg">>; end?: ReactElement<HTMLProps<"svg">>;
}; } & BaseProps;
export const Button = ({ export const Button = ({
size = "small", size = "small",
@@ -24,6 +28,7 @@ export const Button = ({
children, children,
start, start,
end, end,
testId,
...props ...props
}: PropsWithChildren<ButtonProps>) => { }: PropsWithChildren<ButtonProps>) => {
const buttonClassNames = buttonStyles[variant]; const buttonClassNames = buttonStyles[variant];
@@ -35,6 +40,7 @@ export const Button = ({
<button <button
{...props} {...props}
aria-disabled={props.disabled ? "true" : "false"} aria-disabled={props.disabled ? "true" : "false"}
data-testid={testId}
className={cn( className={cn(
size === "small" ? "px-2 py-3 min-w-32" : "px-3 py-4 min-w-64", size === "small" ? "px-2 py-3 min-w-32" : "px-3 py-4 min-w-64",
"flex flex-row items-center gap-x-8", "flex flex-row items-center gap-x-8",

View File

@@ -34,6 +34,7 @@ export const Checkbox = ({
disabled && "cursor-not-allowed", disabled && "cursor-not-allowed",
className className
)} )}
data-testid={testId}
> >
<input <input
id={id} id={id}
@@ -42,7 +43,6 @@ export const Checkbox = ({
onChange={onChange} onChange={onChange}
disabled={disabled} disabled={disabled}
className="sr-only peer" className="sr-only peer"
data-testid={testId}
{...props} {...props}
/> />
<div <div

View File

@@ -1,5 +1,5 @@
import { type PropsWithChildren } from "react"; import { type PropsWithChildren } from "react";
import type { HTMLProps } from "../../shared/types"; import type { BaseProps, HTMLProps } from "../../shared/types";
import { cn } from "../../shared/utils/cn"; import { cn } from "../../shared/utils/cn";
import { Typography } from "../typography/Typography"; import { Typography } from "../typography/Typography";
import { chipStyles, type ChipColor, type ChipVariant } from "./utils"; import { chipStyles, type ChipColor, type ChipVariant } from "./utils";
@@ -7,18 +7,20 @@ import { chipStyles, type ChipColor, type ChipVariant } from "./utils";
export type ChipProps = Omit<HTMLProps<"div">, "label"> & { export type ChipProps = Omit<HTMLProps<"div">, "label"> & {
color?: ChipColor; color?: ChipColor;
variant?: ChipVariant; variant?: ChipVariant;
}; } & BaseProps;
export const Chip = ({ export const Chip = ({
className, className,
color = "gray", color = "gray",
variant = "pill", variant = "pill",
children, children,
testId,
...props ...props
}: PropsWithChildren<ChipProps>) => { }: PropsWithChildren<ChipProps>) => {
return ( return (
<div <div
{...props} {...props}
data-testid={testId}
className={cn( className={cn(
"flex flex-row items-center px-1.5 py-1", "flex flex-row items-center px-1.5 py-1",
variant === "pill" ? "rounded-full" : "rounded-lg", variant === "pill" ? "rounded-full" : "rounded-lg",

View File

@@ -1,14 +1,7 @@
import { import { useId, type PropsWithChildren } from "react";
useEffect, import type { BaseProps, HTMLProps } from "../../shared/types";
useId,
useRef,
useState,
type PropsWithChildren,
} from "react";
import type { HTMLProps } from "../../shared/types";
import { cn } from "../../shared/utils/cn"; import { cn } from "../../shared/utils/cn";
import { Icon } from "../icon/Icon"; import { Icon } from "../icon/Icon";
import { createPortal } from "react-dom";
import { import {
FloatingOverlay, FloatingOverlay,
FloatingPortal, FloatingPortal,
@@ -24,13 +17,14 @@ import { FocusTrap } from "focus-trap-react";
export type DialogProps = HTMLProps<"div"> & { export type DialogProps = HTMLProps<"div"> & {
open: boolean; open: boolean;
onOpenChange(value: boolean): void; onOpenChange(value: boolean): void;
}; } & BaseProps;
export const Dialog = ({ export const Dialog = ({
open, open,
onOpenChange, onOpenChange,
className, className,
children, children,
testId,
}: PropsWithChildren<DialogProps>) => { }: PropsWithChildren<DialogProps>) => {
const id = useId(); const id = useId();
@@ -80,6 +74,7 @@ export const Dialog = ({
aria-describedby={`${id}-description`} aria-describedby={`${id}-description`}
{...getFloatingProps()} {...getFloatingProps()}
style={styles} style={styles}
data-testid={testId}
className={cn( className={cn(
"rounded-4xl border-1 border-light-neutral-500 outline-none", "rounded-4xl border-1 border-light-neutral-500 outline-none",
"transition-all will-change-transform", "transition-all will-change-transform",

View File

@@ -1,4 +1,4 @@
import type { HTMLProps } from "../../shared/types"; import type { BaseProps, HTMLProps } from "../../shared/types";
import { cn } from "../../shared/utils/cn"; import { cn } from "../../shared/utils/cn";
export type DividerProps = Omit< export type DividerProps = Omit<
@@ -6,11 +6,12 @@ export type DividerProps = Omit<
"role" | "aria-orientation" "role" | "aria-orientation"
> & { > & {
type?: "horizontal" | "vertical"; type?: "horizontal" | "vertical";
}; } & BaseProps;
export const Divider = ({ export const Divider = ({
type = "horizontal", type = "horizontal",
className, className,
testId,
...props ...props
}: DividerProps) => { }: DividerProps) => {
return ( return (
@@ -23,6 +24,7 @@ export const Divider = ({
)} )}
role="separator" role="separator"
aria-orientation={type} aria-orientation={type}
data-testid={testId}
/> />
); );
}; };

View File

@@ -5,7 +5,7 @@ import {
type ReactElement, type ReactElement,
type ReactNode, type ReactNode,
} from "react"; } from "react";
import type { HTMLProps } from "../../shared/types"; import type { BaseProps, HTMLProps } from "../../shared/types";
import { cn } from "../../shared/utils/cn"; import { cn } from "../../shared/utils/cn";
import { Typography } from "../typography/Typography"; import { Typography } from "../typography/Typography";
import { cloneIcon } from "../../shared/utils/clone-icon"; import { cloneIcon } from "../../shared/utils/clone-icon";
@@ -19,7 +19,7 @@ export type InputProps = Omit<
end?: ReactElement<HTMLProps<"svg">>; end?: ReactElement<HTMLProps<"svg">>;
error?: string; error?: string;
hint?: string; hint?: string;
}; } & BaseProps;
export const Input = ({ export const Input = ({
className, className,
@@ -34,6 +34,7 @@ export const Input = ({
type, type,
hint, hint,
readOnly, readOnly,
testId,
...props ...props
}: InputProps) => { }: InputProps) => {
const generatedId = useId(); const generatedId = useId();
@@ -45,65 +46,64 @@ export const Input = ({
); );
return ( return (
<div> <label
<label htmlFor={id}
htmlFor={id} data-testid={testId}
className={cn(
"flex flex-col gap-y-2",
disabled ? "cursor-not-allowed opacity-50" : "cursor-pointer"
)}
>
<Typography.Text fontSize="s" className="text-light-neutral-200">
{label}
</Typography.Text>
<div
className={cn( className={cn(
"flex flex-col gap-y-2", "flex flex-row items-center gap-x-2.5",
disabled ? "cursor-not-allowed opacity-50" : "cursor-pointer" "py-4.25 px-4",
"border-light-neutral-500 border-1 rounded-2xl",
// base
"bg-light-neutral-950",
// hover modifier
"hover:bg-light-neutral-900",
// focus modifier
"focus-within:bg-light-neutral-900",
// error state
error && " border-red-400 bg-light-neutral-970",
readOnly &&
"bg-light-neutral-985 border-none hover:bg-light-neutral-985 cursor-auto",
// disabled modifier
disabled && "hover:bg-light-neutral-950"
)} )}
> >
<Typography.Text fontSize="s" className="text-light-neutral-200"> {cloneIcon(start, {
{label} className: iconCss,
</Typography.Text> })}
<div <input
id={id}
type={type}
value={value}
onChange={onChange}
disabled={disabled}
aria-invalid={error ? "true" : "false"}
readOnly={readOnly}
className={cn( className={cn(
"flex flex-row items-center gap-x-2.5", "flex-1 outline-none caret-primary-500 text-white",
"py-4.25 px-4", "placeholder:text-light-neutral-300",
"border-light-neutral-500 border-1 rounded-2xl", error && "text-red-400"
// base
"bg-light-neutral-950",
// hover modifier
"hover:bg-light-neutral-900",
// focus modifier
"focus-within:bg-light-neutral-900",
// error state
error && " border-red-400 bg-light-neutral-970",
readOnly &&
"bg-light-neutral-985 border-none hover:bg-light-neutral-985 cursor-auto",
// disabled modifier
disabled && "hover:bg-light-neutral-950"
)} )}
> {...props}
{cloneIcon(start, { />
className: iconCss, {cloneIcon(end, {
})} className: iconCss,
<input })}
id={id} </div>
type={type} <Typography.Text
value={value} fontSize="xs"
onChange={onChange} className={cn("text-light-neutral-600 ml-4", error && "text-red-400")}
disabled={disabled} >
aria-invalid={error ? "true" : "false"} {error ?? hint}
readOnly={readOnly} </Typography.Text>
className={cn( </label>
"flex-1 outline-none caret-primary-500 text-white",
"placeholder:text-light-neutral-300",
error && "text-red-400"
)}
{...props}
/>
{cloneIcon(end, {
className: iconCss,
})}
</div>
<Typography.Text
fontSize="xs"
className={cn("text-light-neutral-600 ml-4", error && "text-red-400")}
>
{error ?? hint}
</Typography.Text>
</label>
</div>
); );
}; };

View File

@@ -1,5 +1,5 @@
import { type PropsWithChildren, type ReactElement } from "react"; import { type PropsWithChildren, type ReactElement } from "react";
import type { HTMLProps } from "../../shared/types"; import type { BaseProps, HTMLProps } from "../../shared/types";
import { cn } from "../../shared/utils/cn"; import { cn } from "../../shared/utils/cn";
import { cloneIcon } from "../../shared/utils/clone-icon"; import { cloneIcon } from "../../shared/utils/clone-icon";
import "./index.css"; import "./index.css";
@@ -16,7 +16,7 @@ export type InteractiveChipProps = Omit<
chipType?: InteractiveChipType; chipType?: InteractiveChipType;
start?: ReactElement<HTMLProps<"svg">>; start?: ReactElement<HTMLProps<"svg">>;
end?: ReactElement<HTMLProps<"svg">>; end?: ReactElement<HTMLProps<"svg">>;
}; } & BaseProps;
export const InteractiveChip = ({ export const InteractiveChip = ({
chipType = "elevated", chipType = "elevated",
@@ -24,6 +24,7 @@ export const InteractiveChip = ({
children, children,
start, start,
end, end,
testId,
...props ...props
}: PropsWithChildren<InteractiveChipProps>) => { }: PropsWithChildren<InteractiveChipProps>) => {
const buttonClassNames = buttonStyles[chipType]; const buttonClassNames = buttonStyles[chipType];
@@ -34,6 +35,7 @@ export const InteractiveChip = ({
return ( return (
<button <button
{...props} {...props}
data-testid={testId}
aria-disabled={props.disabled ? "true" : "false"} aria-disabled={props.disabled ? "true" : "false"}
className={cn( className={cn(
"px-1.5 py-1 min-w-32", "px-1.5 py-1 min-w-32",

View File

@@ -1,5 +1,5 @@
import { useId } from "react"; import { useId } from "react";
import type { HTMLProps, IOption } from "../../shared/types"; import type { BaseProps, HTMLProps, IOption } from "../../shared/types";
import { cn } from "../../shared/utils/cn"; import { cn } from "../../shared/utils/cn";
import { RadioOption } from "./RadioOption"; import { RadioOption } from "./RadioOption";
@@ -11,7 +11,7 @@ export type RadioGroupProps<T extends string> = Omit<
value: T; value: T;
onChange: (option: IOption<T>) => void; onChange: (option: IOption<T>) => void;
labelClassName?: string; labelClassName?: string;
}; } & BaseProps;
export const RadioGroup = <T extends string>({ export const RadioGroup = <T extends string>({
value, value,
@@ -21,13 +21,17 @@ export const RadioGroup = <T extends string>({
labelClassName, labelClassName,
disabled, disabled,
id: propId, id: propId,
testId,
...props ...props
}: RadioGroupProps<T>) => { }: RadioGroupProps<T>) => {
const generatedId = useId(); const generatedId = useId();
const id = propId ?? generatedId; const id = propId ?? generatedId;
return ( return (
<div className={cn("flex flex-col gap-y-1", className)}> <div
data-testid={testId}
className={cn("flex flex-col gap-y-1", className)}
>
{options.map((o) => ( {options.map((o) => (
<RadioOption <RadioOption
{...props} {...props}

View File

@@ -1,5 +1,5 @@
import { useId } from "react"; import { useId } from "react";
import type { HTMLProps } from "../../shared/types"; import type { BaseProps, HTMLProps } from "../../shared/types";
import { Typography } from "../typography/Typography"; import { Typography } from "../typography/Typography";
import { cn } from "../../shared/utils/cn"; import { cn } from "../../shared/utils/cn";
@@ -7,7 +7,7 @@ type RadioOptionProps = Omit<HTMLProps<"input">, "id" | "checked"> & {
label: React.ReactNode; label: React.ReactNode;
labelClassName?: string; labelClassName?: string;
id: string; id: string;
}; } & BaseProps;
export const RadioOption = ({ export const RadioOption = ({
className, className,
@@ -17,6 +17,7 @@ export const RadioOption = ({
id: propId, id: propId,
disabled, disabled,
onChange, onChange,
testId,
...props ...props
}: RadioOptionProps) => { }: RadioOptionProps) => {
const generatedId = useId(); const generatedId = useId();
@@ -25,6 +26,7 @@ export const RadioOption = ({
return ( return (
<label <label
htmlFor={id} htmlFor={id}
data-testid={testId}
className={cn( className={cn(
"flex items-center gap-x-4", "flex items-center gap-x-4",
disabled ? "cursor-not-allowed" : "cursor-pointer" disabled ? "cursor-not-allowed" : "cursor-pointer"

View File

@@ -1,5 +1,5 @@
import type { PropsWithChildren } from "react"; import type { PropsWithChildren } from "react";
import type { HTMLProps } from "../../shared/types"; import type { BaseProps, HTMLProps } from "../../shared/types";
import { cn } from "../../shared/utils/cn"; import { cn } from "../../shared/utils/cn";
export type ScrollableMode = "auto" | "scroll"; export type ScrollableMode = "auto" | "scroll";
@@ -8,7 +8,7 @@ export type ScrollableType = "horizontal" | "vertical";
export type ScrollableProps = HTMLProps<"div"> & { export type ScrollableProps = HTMLProps<"div"> & {
mode?: ScrollableMode; mode?: ScrollableMode;
type?: ScrollableType; type?: ScrollableType;
}; } & BaseProps;
const scrollableStyles: Record< const scrollableStyles: Record<
ScrollableType, ScrollableType,
@@ -30,11 +30,13 @@ export const Scrollable = ({
tabIndex, tabIndex,
mode = "auto", mode = "auto",
type = "vertical", type = "vertical",
testId,
...props ...props
}: PropsWithChildren<ScrollableProps>) => { }: PropsWithChildren<ScrollableProps>) => {
const style = scrollableStyles[type][mode]; const style = scrollableStyles[type][mode];
return ( return (
<div <div
data-testid={testId}
tabIndex={tabIndex ?? 0} tabIndex={tabIndex ?? 0}
{...props} {...props}
className={cn( className={cn(

View File

@@ -1,5 +1,5 @@
import { useId, useMemo, useState } from "react"; import { useId, useMemo, useState } from "react";
import type { HTMLProps, IOption } from "../../shared/types"; import type { BaseProps, HTMLProps, IOption } from "../../shared/types";
import { cn } from "../../shared/utils/cn"; import { cn } from "../../shared/utils/cn";
import ReactSelect, { createFilter } from "react-select"; import ReactSelect, { createFilter } from "react-select";
import { Typography } from "../typography/Typography"; import { Typography } from "../typography/Typography";
@@ -16,7 +16,7 @@ export type SelectProps<T> = Omit<HTMLProps<"input">, "value" | "onChange"> & {
options: IOption<T>[]; options: IOption<T>[];
noOptionsText?: string; noOptionsText?: string;
onChange(value: IOption<T> | null): void; onChange(value: IOption<T> | null): void;
}; } & BaseProps;
export const Select = <T extends string>(props: SelectProps<T>) => { export const Select = <T extends string>(props: SelectProps<T>) => {
const { const {
@@ -32,6 +32,8 @@ export const Select = <T extends string>(props: SelectProps<T>) => {
onChange, onChange,
readOnly, readOnly,
noOptionsText, noOptionsText,
className,
testId,
} = props; } = props;
const [inputValue, setInputValue] = useState(""); const [inputValue, setInputValue] = useState("");
const generatedId = useId(); const generatedId = useId();
@@ -50,10 +52,12 @@ export const Select = <T extends string>(props: SelectProps<T>) => {
return ( return (
<label <label
data-testid={testId}
htmlFor={id} htmlFor={id}
className={cn( className={cn(
"flex flex-col gap-y-2", "flex flex-col gap-y-2",
disabled ? "cursor-not-allowed opacity-50" : "cursor-pointer" disabled ? "cursor-not-allowed opacity-50" : "cursor-pointer",
className
)} )}
> >
<Typography.Text fontSize="s" className="text-light-neutral-200"> <Typography.Text fontSize="s" className="text-light-neutral-200">

View File

@@ -1,5 +1,5 @@
import { useMemo } from "react"; import { useMemo } from "react";
import type { HTMLProps } from "../../shared/types"; import type { BaseProps, HTMLProps } from "../../shared/types";
import { cn } from "../../shared/utils/cn"; import { cn } from "../../shared/utils/cn";
import "./index.css"; import "./index.css";
@@ -15,7 +15,11 @@ export type IndeterminateSpinnerProps = BaseSpinnerProps & {
value?: never; value?: never;
}; };
export type SpinnerProps = DeterminateSpinnerProps | IndeterminateSpinnerProps; export type SpinnerProps = (
| DeterminateSpinnerProps
| IndeterminateSpinnerProps
) &
BaseProps;
const SIZE = 48; const SIZE = 48;
const STROKE_WIDTH = 6; const STROKE_WIDTH = 6;
@@ -26,6 +30,7 @@ export const Spinner = ({
value = 10, value = 10,
determinate = false, determinate = false,
className, className,
testId,
...props ...props
}: SpinnerProps) => { }: SpinnerProps) => {
const offset = useMemo( const offset = useMemo(
@@ -34,7 +39,13 @@ export const Spinner = ({
); );
return ( return (
<svg width={SIZE} height={SIZE} className={className} {...props}> <svg
data-testid={testId}
width={SIZE}
height={SIZE}
className={className}
{...props}
>
<circle <circle
cx={SIZE / 2} cx={SIZE / 2}
cy={SIZE / 2} cy={SIZE / 2}

View File

@@ -4,7 +4,7 @@ import {
type PropsWithChildren, type PropsWithChildren,
type ReactElement, type ReactElement,
} from "react"; } from "react";
import type { HTMLProps } from "../../shared/types"; import type { BaseProps, HTMLProps } from "../../shared/types";
import { cn } from "../../shared/utils/cn"; import { cn } from "../../shared/utils/cn";
import React from "react"; import React from "react";
import { import {
@@ -16,13 +16,13 @@ import { useElementOverflow } from "./hooks/use-element-overflow";
import { useElementScroll } from "./hooks/use-element-scroll"; import { useElementScroll } from "./hooks/use-element-scroll";
import { TabScroller } from "./components/TabScroller"; import { TabScroller } from "./components/TabScroller";
export type TabsProps = HTMLProps<"div">; export type TabsProps = HTMLProps<"div"> & BaseProps;
type TabsType = React.FC<PropsWithChildren<TabsProps>> & { type TabsType = React.FC<PropsWithChildren<TabsProps>> & {
Item: React.FC<PropsWithChildren<TabItemPropsPublic>>; Item: React.FC<PropsWithChildren<TabItemPropsPublic>>;
}; };
const Tabs: TabsType = ({ children, ...props }) => { const Tabs: TabsType = ({ children, className, testId, ...props }) => {
const [activeIndex, setActiveIndex] = useState(0); const [activeIndex, setActiveIndex] = useState(0);
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const tabListRef = useRef<HTMLDivElement>(null); const tabListRef = useRef<HTMLDivElement>(null);
@@ -55,7 +55,7 @@ const Tabs: TabsType = ({ children, ...props }) => {
}) ?? []; }) ?? [];
return ( return (
<div className={cn("w-full")}> <div data-testid={testId} className={cn("w-full", className)}>
<div className={cn("flex flex-row items-stretch")} ref={containerRef}> <div className={cn("flex flex-row items-stretch")} ref={containerRef}>
{canScrollLeft && isOverflowing && ( {canScrollLeft && isOverflowing && (
<TabScroller onScroll={scrollLeft} position="left" /> <TabScroller onScroll={scrollLeft} position="left" />

View File

@@ -5,6 +5,7 @@ import { Typography } from "../typography/Typography";
import { toastStyles } from "./utils"; import { toastStyles } from "./utils";
import type { JSX } from "react"; import type { JSX } from "react";
import { invariant } from "../../shared/utils/invariant"; import { invariant } from "../../shared/utils/invariant";
import type { BaseProps } from "../../shared/types";
type RenderContentProps = { type RenderContentProps = {
onDismiss: () => void; onDismiss: () => void;

View File

@@ -17,6 +17,7 @@ import {
} from "@floating-ui/react"; } from "@floating-ui/react";
import { useRef, useState, type PropsWithChildren } from "react"; import { useRef, useState, type PropsWithChildren } from "react";
import { Typography } from "../typography/Typography"; import { Typography } from "../typography/Typography";
import type { BaseProps } from "../../shared/types";
type ControlledTooltipProps = { type ControlledTooltipProps = {
open: boolean; open: boolean;
@@ -33,10 +34,9 @@ type TooltipTriggerType = "click" | "hover";
type BaseTooltipProps = { type BaseTooltipProps = {
text: string; text: string;
withArrow?: boolean; withArrow?: boolean;
className?: string;
placement?: UseFloatingOptions["placement"]; placement?: UseFloatingOptions["placement"];
trigger?: TooltipTriggerType; trigger?: TooltipTriggerType;
}; } & BaseProps;
export type TooltipProps = BaseTooltipProps & export type TooltipProps = BaseTooltipProps &
(ControlledTooltipProps | UncontrolledTooltipProps); (ControlledTooltipProps | UncontrolledTooltipProps);
@@ -50,6 +50,7 @@ export const Tooltip = ({
open, open,
setOpen: setOpenProp, setOpen: setOpenProp,
trigger = "hover", trigger = "hover",
testId,
}: PropsWithChildren<TooltipProps>) => { }: PropsWithChildren<TooltipProps>) => {
const [localOpen, setLocalOpen] = useState(false); const [localOpen, setLocalOpen] = useState(false);
const arrowRef = useRef(null); const arrowRef = useRef(null);
@@ -95,6 +96,7 @@ export const Tooltip = ({
ref={refs.setReference} ref={refs.setReference}
{...getReferenceProps()} {...getReferenceProps()}
className={className} className={className}
data-testid={testId}
> >
{children} {children}
</button> </button>

View File

@@ -6,6 +6,7 @@ import {
type FontWeight, type FontWeight,
} from "./utils"; } from "./utils";
import { cn } from "../../shared/utils/cn"; import { cn } from "../../shared/utils/cn";
import type { BaseProps } from "../../shared/types";
type SupportedReactNodes = "h6" | "h5" | "h4" | "h3" | "h2" | "h1" | "span"; type SupportedReactNodes = "h6" | "h5" | "h4" | "h3" | "h2" | "h1" | "span";
@@ -13,7 +14,7 @@ export type BaseTypographyProps = React.HTMLAttributes<HTMLElement> & {
fontSize?: FontSize; fontSize?: FontSize;
fontWeight?: FontWeight; fontWeight?: FontWeight;
as: SupportedReactNodes; as: SupportedReactNodes;
}; } & BaseProps;
export const BaseTypography = ({ export const BaseTypography = ({
fontSize, fontSize,
@@ -21,6 +22,7 @@ export const BaseTypography = ({
className, className,
children, children,
as, as,
testId,
...props ...props
}: PropsWithChildren<BaseTypographyProps>) => { }: PropsWithChildren<BaseTypographyProps>) => {
const Component = as; const Component = as;
@@ -28,6 +30,7 @@ export const BaseTypography = ({
return ( return (
<Component <Component
{...props} {...props}
data-testid={testId}
className={cn( className={cn(
"tg-family-outfit text-white leading-[100%]", "tg-family-outfit text-white leading-[100%]",
fontSize ? fontSizes[fontSize] : undefined, fontSize ? fontSizes[fontSize] : undefined,

View File

@@ -14,7 +14,7 @@
"email": "stephan@all-hands.dev" "email": "stephan@all-hands.dev"
} }
], ],
"version": "1.0.0-beta.7", "version": "1.0.0-beta.8",
"description": "OpenHands UI Components", "description": "OpenHands UI Components",
"keywords": [ "keywords": [
"openhands", "openhands",

View File

@@ -1,12 +1,11 @@
export type BaseProps = { export type BaseProps = {
className?: string; className?: string;
style?: React.CSSProperties;
testId?: string; testId?: string;
}; };
export type HTMLProps<T extends React.ElementType> = Omit< export type HTMLProps<T extends React.ElementType> = Omit<
React.ComponentPropsWithoutRef<T>, React.ComponentPropsWithoutRef<T>,
"children" "children" | "style" | "className"
>; >;
export type ComponentVariant = "primary" | "secondary" | "tertiary"; export type ComponentVariant = "primary" | "secondary" | "tertiary";

View File

@@ -1,9 +1,9 @@
import { cloneElement, isValidElement, type ReactElement } from "react"; import { cloneElement, isValidElement, type ReactElement } from "react";
import type { ComponentVariant, HTMLProps } from "../../shared/types"; import type { BaseProps, HTMLProps } from "../../shared/types";
export const cloneIcon = ( export const cloneIcon = (
icon?: ReactElement<HTMLProps<"svg">>, icon?: ReactElement<HTMLProps<"svg"> & BaseProps>,
props?: HTMLProps<"svg"> props?: HTMLProps<"svg"> & BaseProps
) => { ) => {
if (!icon) { if (!icon) {
return null; return null;