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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 150 additions and 112 deletions

View File

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

View File

@ -1,5 +1,5 @@
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 { Icon, type IconProps } from "../../icon/Icon";
import { Typography } from "../../typography/Typography";
@ -10,7 +10,7 @@ export type AccordionHeaderProps = Omit<
> & {
icon: IconProps["icon"];
expanded: boolean;
};
} & BaseProps;
export const AccordionHeader = ({
className,
@ -43,7 +43,8 @@ export const AccordionHeader = ({
// hover modifier
"data-[expanded=true]:hover:bg-light-neutral-900",
// 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")} />

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
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 { Typography } from "../typography/Typography";
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"> & {
color?: ChipColor;
variant?: ChipVariant;
};
} & BaseProps;
export const Chip = ({
className,
color = "gray",
variant = "pill",
children,
testId,
...props
}: PropsWithChildren<ChipProps>) => {
return (
<div
{...props}
data-testid={testId}
className={cn(
"flex flex-row items-center px-1.5 py-1",
variant === "pill" ? "rounded-full" : "rounded-lg",

View File

@ -1,14 +1,7 @@
import {
useEffect,
useId,
useRef,
useState,
type PropsWithChildren,
} from "react";
import type { HTMLProps } from "../../shared/types";
import { useId, type PropsWithChildren } from "react";
import type { BaseProps, HTMLProps } from "../../shared/types";
import { cn } from "../../shared/utils/cn";
import { Icon } from "../icon/Icon";
import { createPortal } from "react-dom";
import {
FloatingOverlay,
FloatingPortal,
@ -24,13 +17,14 @@ import { FocusTrap } from "focus-trap-react";
export type DialogProps = HTMLProps<"div"> & {
open: boolean;
onOpenChange(value: boolean): void;
};
} & BaseProps;
export const Dialog = ({
open,
onOpenChange,
className,
children,
testId,
}: PropsWithChildren<DialogProps>) => {
const id = useId();
@ -80,6 +74,7 @@ export const Dialog = ({
aria-describedby={`${id}-description`}
{...getFloatingProps()}
style={styles}
data-testid={testId}
className={cn(
"rounded-4xl border-1 border-light-neutral-500 outline-none",
"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";
export type DividerProps = Omit<
@ -6,11 +6,12 @@ export type DividerProps = Omit<
"role" | "aria-orientation"
> & {
type?: "horizontal" | "vertical";
};
} & BaseProps;
export const Divider = ({
type = "horizontal",
className,
testId,
...props
}: DividerProps) => {
return (
@ -23,6 +24,7 @@ export const Divider = ({
)}
role="separator"
aria-orientation={type}
data-testid={testId}
/>
);
};

View File

@ -5,7 +5,7 @@ import {
type ReactElement,
type ReactNode,
} from "react";
import type { HTMLProps } from "../../shared/types";
import type { BaseProps, HTMLProps } from "../../shared/types";
import { cn } from "../../shared/utils/cn";
import { Typography } from "../typography/Typography";
import { cloneIcon } from "../../shared/utils/clone-icon";
@ -19,7 +19,7 @@ export type InputProps = Omit<
end?: ReactElement<HTMLProps<"svg">>;
error?: string;
hint?: string;
};
} & BaseProps;
export const Input = ({
className,
@ -34,6 +34,7 @@ export const Input = ({
type,
hint,
readOnly,
testId,
...props
}: InputProps) => {
const generatedId = useId();
@ -45,9 +46,9 @@ export const Input = ({
);
return (
<div>
<label
htmlFor={id}
data-testid={testId}
className={cn(
"flex flex-col gap-y-2",
disabled ? "cursor-not-allowed opacity-50" : "cursor-pointer"
@ -104,6 +105,5 @@ export const Input = ({
{error ?? hint}
</Typography.Text>
</label>
</div>
);
};

View File

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

View File

@ -1,5 +1,5 @@
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 { RadioOption } from "./RadioOption";
@ -11,7 +11,7 @@ export type RadioGroupProps<T extends string> = Omit<
value: T;
onChange: (option: IOption<T>) => void;
labelClassName?: string;
};
} & BaseProps;
export const RadioGroup = <T extends string>({
value,
@ -21,13 +21,17 @@ export const RadioGroup = <T extends string>({
labelClassName,
disabled,
id: propId,
testId,
...props
}: RadioGroupProps<T>) => {
const generatedId = useId();
const id = propId ?? generatedId;
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) => (
<RadioOption
{...props}

View File

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

View File

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

View File

@ -1,5 +1,5 @@
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 ReactSelect, { createFilter } from "react-select";
import { Typography } from "../typography/Typography";
@ -16,7 +16,7 @@ export type SelectProps<T> = Omit<HTMLProps<"input">, "value" | "onChange"> & {
options: IOption<T>[];
noOptionsText?: string;
onChange(value: IOption<T> | null): void;
};
} & BaseProps;
export const Select = <T extends string>(props: SelectProps<T>) => {
const {
@ -32,6 +32,8 @@ export const Select = <T extends string>(props: SelectProps<T>) => {
onChange,
readOnly,
noOptionsText,
className,
testId,
} = props;
const [inputValue, setInputValue] = useState("");
const generatedId = useId();
@ -50,10 +52,12 @@ export const Select = <T extends string>(props: SelectProps<T>) => {
return (
<label
data-testid={testId}
htmlFor={id}
className={cn(
"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">

View File

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

View File

@ -4,7 +4,7 @@ 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 React from "react";
import {
@ -16,13 +16,13 @@ import { useElementOverflow } from "./hooks/use-element-overflow";
import { useElementScroll } from "./hooks/use-element-scroll";
import { TabScroller } from "./components/TabScroller";
export type TabsProps = HTMLProps<"div">;
export type TabsProps = HTMLProps<"div"> & BaseProps;
type TabsType = React.FC<PropsWithChildren<TabsProps>> & {
Item: React.FC<PropsWithChildren<TabItemPropsPublic>>;
};
const Tabs: TabsType = ({ children, ...props }) => {
const Tabs: TabsType = ({ children, className, testId, ...props }) => {
const [activeIndex, setActiveIndex] = useState(0);
const containerRef = useRef<HTMLDivElement>(null);
const tabListRef = useRef<HTMLDivElement>(null);
@ -55,7 +55,7 @@ const Tabs: TabsType = ({ children, ...props }) => {
}) ?? [];
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}>
{canScrollLeft && isOverflowing && (
<TabScroller onScroll={scrollLeft} position="left" />

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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