Add events to be tracked by the Storybook while interacting with the components

This commit is contained in:
Arnaud 2024-08-22 20:04:24 +02:00
parent b99008ba3d
commit c4d5c6e864
No known key found for this signature in database
GPG Key ID: 69D6CE281FCAE663
17 changed files with 211 additions and 33 deletions

View File

@ -23,6 +23,10 @@ type Props = {
onClick?: () => unknown | Promise<unknown>; onClick?: () => unknown | Promise<unknown>;
onMouseEnter?: () => unknown | Promise<unknown>;
onMouseLeave?: () => unknown | Promise<unknown>;
label: string; label: string;
/** /**
@ -67,6 +71,8 @@ export function Button({
label, label,
className = "", className = "",
Icon, Icon,
onMouseEnter,
onMouseLeave,
fetching = false, fetching = false,
disabled = false, disabled = false,
style, style,
@ -76,6 +82,8 @@ export function Button({
return ( return (
<button <button
onClick={onClick} onClick={onClick}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
style={style} style={style}
className={`button ${className} button--${variant}`} className={`button ${className} button--${variant}`}
{...attributes({ {...attributes({

View File

@ -15,6 +15,10 @@ type Props = {
onClick?: () => void; onClick?: () => void;
onMouseEnter?: () => unknown | Promise<unknown>;
onMouseLeave?: () => unknown | Promise<unknown>;
disabled?: boolean; disabled?: boolean;
/** /**
@ -32,12 +36,16 @@ export function ButtonIcon({
Icon, Icon,
onClick, onClick,
style, style,
onMouseEnter,
onMouseLeave,
disabled = false, disabled = false,
variant = "big", variant = "big",
}: Props) { }: Props) {
return ( return (
<button <button
className={`buttonIcon buttonIcon--${variant}`} className={`buttonIcon buttonIcon--${variant}`}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
onClick={onClick} onClick={onClick}
style={style} style={style}
{...attributes({ disabled: disabled, "aria-disabled": disabled })} {...attributes({ disabled: disabled, "aria-disabled": disabled })}

View File

@ -52,6 +52,14 @@ type Props = {
*/ */
onSelected?: (o: DropdownOption) => void; onSelected?: (o: DropdownOption) => void;
onBlur?: () => void;
onFocus?: () => void;
onMouseEnter?: () => void;
onMouseLeave?: () => void;
/** /**
* Apply custom css variables. * Apply custom css variables.
* --codex-dropdown-panel-background * --codex-dropdown-panel-background
@ -65,6 +73,10 @@ export function Dropdown({
placeholder, placeholder,
style, style,
options, options,
onMouseEnter,
onMouseLeave,
onFocus,
onBlur,
onChange, onChange,
onSelected, onSelected,
value = "", value = "",
@ -78,11 +90,17 @@ export function Dropdown({
); );
const [focused, setFocused] = useState(false); const [focused, setFocused] = useState(false);
const onFocus = () => setFocused(true); const onInternalFocus = () => {
setFocused(true);
onFocus?.();
};
const onBlur = () => () => window.setTimeout(() => setFocused(false), 150); const onInternalBlur = () => {
onBlur?.();
window.setTimeout(() => setFocused(false), 150);
};
const onClick = (o: DropdownOption) => { const onSelect = (o: DropdownOption) => {
onSelected?.(o); onSelected?.(o);
setFocused(false); setFocused(false);
}; };
@ -98,12 +116,14 @@ export function Dropdown({
<Input <Input
className="dropdown-input" className="dropdown-input"
onChange={onChange} onChange={onChange}
onFocus={onFocus} onFocus={onInternalFocus}
onBlur={onBlur} onBlur={onInternalBlur}
placeholder={placeholder} placeholder={placeholder}
value={value} value={value}
label={""} label={""}
id={""} id={""}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
/> />
<div className="dropdown-panel" {...attr}> <div className="dropdown-panel" {...attr}>
@ -111,7 +131,7 @@ export function Dropdown({
filtered.map((o) => ( filtered.map((o) => (
<div <div
className="dropdown-option" className="dropdown-option"
onClick={() => onClick(o)} onClick={() => onSelect(o)}
key={o.title} key={o.title}
> >
{o.Icon && <o.Icon />} {o.Icon && <o.Icon />}

View File

@ -27,6 +27,12 @@ type Props = {
onBlur?: () => unknown | Promise<unknown>; onBlur?: () => unknown | Promise<unknown>;
onClick?: (() => void) | undefined;
onMouseEnter?: () => void;
onMouseLeave?: () => void;
placeholder?: string; placeholder?: string;
value?: string; value?: string;
@ -74,6 +80,9 @@ export function Input({
onFocus, onFocus,
placeholder, placeholder,
onChange, onChange,
onMouseEnter,
onMouseLeave,
onClick,
className, className,
style, style,
Icon, Icon,
@ -95,6 +104,9 @@ export function Input({
</div> </div>
)} )}
<input <input
onClick={onClick}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
className={classnames( className={classnames(
["input"], ["input"],
["input-icon-input", !!Icon], ["input-icon-input", !!Icon],

View File

@ -43,6 +43,14 @@ type Props = {
*/ */
onGroupChange?: (e: ChangeEvent<HTMLSelectElement>) => void; onGroupChange?: (e: ChangeEvent<HTMLSelectElement>) => void;
onBlur?: () => void;
onFocus?: () => void;
onMouseEnter?: () => void;
onMouseLeave?: () => void;
id: string; id: string;
step?: string; step?: string;
@ -63,9 +71,13 @@ export function InputGroup({
className, className,
onChange, onChange,
onGroupChange, onGroupChange,
onMouseEnter,
onMouseLeave,
onFocus,
onBlur,
id, id,
step, step,
value = "", value = undefined,
groupValue = "", groupValue = "",
}: Props) { }: Props) {
return ( return (
@ -81,6 +93,10 @@ export function InputGroup({
type={type} type={type}
value={value} value={value}
step={step} step={step}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
onFocus={onFocus}
onBlur={onBlur}
/> />
</div> </div>

View File

@ -26,7 +26,7 @@ select.inputGroup-select {
border-top-right-radius: var(--codex-border-radius); border-top-right-radius: var(--codex-border-radius);
border-bottom-right-radius: var(--codex-border-radius); border-bottom-right-radius: var(--codex-border-radius);
background-color: var(--codex-border-color); background-color: var(--codex-border-color);
padding: 0.75rem; padding: calc(0.5rem + 0.5px);
} }
.inputGroup-inputContainer { .inputGroup-inputContainer {

View File

@ -24,6 +24,14 @@ type Props = {
*/ */
onChange?: (e: ChangeEvent<HTMLSelectElement>) => void | Promise<void>; onChange?: (e: ChangeEvent<HTMLSelectElement>) => void | Promise<void>;
onFocus?: () => void | Promise<void>;
onBlur?: () => unknown | Promise<unknown>;
onMouseEnter?: () => void;
onMouseLeave?: () => void;
/** /**
* Apply custom css variables. * Apply custom css variables.
*/ */
@ -39,6 +47,10 @@ export function Select({
id, id,
options, options,
onChange, onChange,
onBlur,
onFocus,
onMouseEnter,
onMouseLeave,
style, style,
className, className,
defaultValue, defaultValue,
@ -53,6 +65,10 @@ export function Select({
id={id} id={id}
className={`select ${className}`} className={`select ${className}`}
onChange={onChange} onChange={onChange}
onBlur={onBlur}
onFocus={onFocus}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
style={style} style={style}
defaultValue={defaultValue} defaultValue={defaultValue}
> >

View File

@ -1,4 +1,3 @@
import { ReactNode } from "react";
import "./Table.css"; import "./Table.css";
import { CellRender } from "./CellRender"; import { CellRender } from "./CellRender";

View File

@ -32,12 +32,23 @@ type Props = {
*/ */
onSuccess?: (cid: string) => void; onSuccess?: (cid: string) => void;
/*
* Event triggered when the user selected files to upload.
*/
onFileChange?: (e: ChangeEvent<HTMLInputElement>) => void;
/** /**
* Event triggered when a file is deleted. * Event triggered when a file is deleted.
* The id is generated after the file are selected by the user. * The id is generated after the file are selected by the user.
*/ */
onDeleteItem?: (id: string) => void; onDeleteItem?: (id: string) => void;
onClick?: (() => void) | undefined;
onMouseEnter?: () => void;
onMouseLeave?: () => void;
/** /**
* Allow to override the previous file(s). * Allow to override the previous file(s).
* If false, the user cannot upload a new file(s) until he deletes the previous file(s). * If false, the user cannot upload a new file(s) until he deletes the previous file(s).
@ -86,6 +97,10 @@ const defaultProvider = () =>
); );
export function Upload({ export function Upload({
onMouseEnter,
onMouseLeave,
onClick,
onFileChange,
multiple = true, multiple = true,
editable = true, editable = true,
onDeleteItem, onDeleteItem,
@ -109,7 +124,7 @@ export function Upload({
uploadFiles(e.dataTransfer.files); uploadFiles(e.dataTransfer.files);
}; };
const onFileChange = (e: ChangeEvent<HTMLInputElement>) => { const onInternalFileChange = (e: ChangeEvent<HTMLInputElement>) => {
if (e.target.files) { if (e.target.files) {
uploadFiles(e.target.files); uploadFiles(e.target.files);
} }
@ -117,6 +132,8 @@ export function Upload({
if (input.current) { if (input.current) {
input.current.value = ""; input.current.value = "";
} }
onFileChange?.(e);
}; };
const onClose = (id: string) => { const onClose = (id: string) => {
@ -124,17 +141,22 @@ export function Upload({
onDeleteItem?.(id); onDeleteItem?.(id);
}; };
const onClick = () => input.current?.click(); const onInternalClick = () => {
onClick?.();
input.current?.click();
};
return ( return (
<> <>
<div <div
className={classnames(["upload"], ["upload-warning", !!warning])} className={classnames(["upload"], ["upload-warning", !!warning])}
tabIndex={1} tabIndex={1}
onClick={onClick} onClick={onInternalClick}
onDragOver={onDragPrevents} onDragOver={onDragPrevents}
onDragEnter={onDragPrevents} onDragEnter={onDragPrevents}
onDrop={onDrop} onDrop={onDrop}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
> >
<ButtonIcon Icon={multiple ? FileStack : UploadIcon}></ButtonIcon> <ButtonIcon Icon={multiple ? FileStack : UploadIcon}></ButtonIcon>
<div className="upload-text"> <div className="upload-text">
@ -152,7 +174,7 @@ export function Upload({
type="file" type="file"
hidden hidden
ref={input} ref={input}
onChange={onFileChange} onChange={onInternalFileChange}
{...attributes({ multiple: multiple })} {...attributes({ multiple: multiple })}
/> />

View File

@ -20,7 +20,11 @@ const meta = {
}, },
}, },
// Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args // Use `fn` to spy on the onClick arg, which will appear in the actions panel once invoked: https://storybook.js.org/docs/essentials/actions#action-args
args: { onClick: fn() }, args: {
onClick: fn(),
onMouseEnter: fn(),
onMouseLeave: fn(),
},
} satisfies Meta<typeof Button>; } satisfies Meta<typeof Button>;
export default meta; export default meta;

View File

@ -15,7 +15,7 @@ const meta = {
control: { type: "select" }, control: { type: "select" },
}, },
}, },
args: { onClick: fn() }, args: { onClick: fn(), onMouseEnter: fn(), onMouseLeave: fn() },
} satisfies Meta<typeof ButtonIcon>; } satisfies Meta<typeof ButtonIcon>;
export default meta; export default meta;

View File

@ -3,6 +3,7 @@ import { Dropdown, DropdownOption } from "../src/components/Dropdown/Dropdown";
import { PdfIcon } from "../src/components/WebFileIcon/PdfIcon"; import { PdfIcon } from "../src/components/WebFileIcon/PdfIcon";
import { ImageIcon } from "../src/components/WebFileIcon/ImageIcon"; import { ImageIcon } from "../src/components/WebFileIcon/ImageIcon";
import { ChangeEvent, useState } from "react"; import { ChangeEvent, useState } from "react";
import { fn } from "@storybook/test";
const meta = { const meta = {
title: "Forms/Dropdown", title: "Forms/Dropdown",
@ -12,20 +13,46 @@ const meta = {
}, },
tags: ["autodocs"], tags: ["autodocs"],
argTypes: {}, argTypes: {},
args: {
onChange: fn(),
onSelected: fn(),
onFocus: fn(),
onBlur: fn(),
onMouseEnter: fn(),
onMouseLeave: fn(),
},
} satisfies Meta<typeof Dropdown>; } satisfies Meta<typeof Dropdown>;
export default meta; export default meta;
type Story = StoryObj<typeof meta>; type Story = StoryObj<typeof meta>;
const Template = () => { type Props = {
const [value, setValue] = useState<string>(""); onChange: (e: ChangeEvent<HTMLInputElement>) => void;
const onChange = (e: ChangeEvent<HTMLInputElement>) =>
setValue(e.currentTarget.value);
const onSelected = (o: DropdownOption) => setValue(o.title); onSelected?: (o: DropdownOption) => void;
onClick?: () => void;
onMouseEnter?: () => void;
onBlur?: () => void;
};
const Template = (p: Props) => {
const [value, setValue] = useState<string>("");
const onChange = (e: ChangeEvent<HTMLInputElement>) => {
p.onChange(e);
setValue(e.currentTarget.value);
};
const onSelected = (o: DropdownOption) => {
setValue(o.title);
p.onSelected?.(o);
};
return ( return (
<Dropdown <Dropdown
{...p}
placeholder="Select your file" placeholder="Select your file"
onChange={onChange} onChange={onChange}
onSelected={onSelected} onSelected={onSelected}
@ -46,7 +73,7 @@ const Template = () => {
); );
}; };
export const Default = Template.bind({}); export const Default = Template;
export const CustomStyle: Story = { export const CustomStyle: Story = {
args: { args: {
@ -54,6 +81,5 @@ export const CustomStyle: Story = {
options: [], options: [],
style: { "--codex-input-border": "1px solid red" }, style: { "--codex-input-border": "1px solid red" },
value: "", value: "",
onChange: () => "",
}, },
}; };

View File

@ -11,7 +11,13 @@ const meta = {
}, },
tags: ["autodocs"], tags: ["autodocs"],
argTypes: {}, argTypes: {},
args: { onChange: fn() }, args: {
onFocus: fn(),
onBlur: fn(),
onMouseEnter: fn(),
onMouseLeave: fn(),
onChange: fn(),
},
} satisfies Meta<typeof Input>; } satisfies Meta<typeof Input>;
export default meta; export default meta;

View File

@ -10,7 +10,14 @@ const meta = {
}, },
tags: ["autodocs"], tags: ["autodocs"],
argTypes: {}, argTypes: {},
args: { onChange: fn() }, args: {
onGroupChange: fn(),
onChange: fn(),
onFocus: fn(),
onBlur: fn(),
onMouseEnter: fn(),
onMouseLeave: fn(),
},
} satisfies Meta<typeof InputGroup>; } satisfies Meta<typeof InputGroup>;
export default meta; export default meta;

View File

@ -10,7 +10,13 @@ const meta = {
}, },
tags: ["autodocs"], tags: ["autodocs"],
argTypes: {}, argTypes: {},
args: { onChange: fn() }, args: {
onFocus: fn(),
onBlur: fn(),
onMouseEnter: fn(),
onMouseLeave: fn(),
onChange: fn(),
},
} satisfies Meta<typeof Select>; } satisfies Meta<typeof Select>;
export default meta; export default meta;

View File

@ -1,6 +1,7 @@
import type { Meta, StoryObj } from "@storybook/react"; import type { Meta, StoryObj } from "@storybook/react";
import { Stepper } from "../src/components/Stepper/Stepper"; import { Stepper } from "../src/components/Stepper/Stepper";
import React, { useState } from "react"; import React, { useState } from "react";
import { fn } from "@storybook/test";
const meta = { const meta = {
title: "Advanced/Stepper", title: "Advanced/Stepper",
@ -10,13 +11,16 @@ const meta = {
}, },
tags: ["autodocs"], tags: ["autodocs"],
argTypes: {}, argTypes: {},
// args: { onClick: fn() }, args: { onChangeStep: fn() },
} satisfies Meta<typeof Stepper>; } satisfies Meta<typeof Stepper>;
export default meta; export default meta;
type Story = StoryObj<typeof meta>;
const Template = () => { type Props = {
onChangeStep: (s: number, state: "before" | "end") => void | Promise<void>;
};
const Template = (p: Props) => {
const [step, setStep] = useState(0); const [step, setStep] = useState(0);
const [progress, setProgress] = useState(false); const [progress, setProgress] = useState(false);
@ -24,6 +28,8 @@ const Template = () => {
const title = titles[step]; const title = titles[step];
const onChangeStep = (newStep: number, event: "before" | "end") => { const onChangeStep = (newStep: number, event: "before" | "end") => {
p.onChangeStep(newStep, event);
if (event === "before") { if (event === "before") {
setProgress(true); setProgress(true);
return; return;

View File

@ -2,6 +2,7 @@ import type { Meta } from "@storybook/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { UploadResponse } from "@codex/sdk-js"; import { UploadResponse } from "@codex/sdk-js";
import { Upload } from "../src/components/Upload/Upload"; import { Upload } from "../src/components/Upload/Upload";
import { fn } from "@storybook/test";
const meta = { const meta = {
title: "Advanced/Upload", title: "Advanced/Upload",
@ -11,16 +12,36 @@ const meta = {
}, },
tags: ["autodocs"], tags: ["autodocs"],
argTypes: {}, argTypes: {},
args: {
onMouseEnter: fn(),
onMouseLeave: fn(),
onClick: fn(),
onSuccess: fn(),
onDeleteItem: fn(),
onFileChange: fn(),
},
} satisfies Meta<typeof Upload>; } satisfies Meta<typeof Upload>;
export default meta; export default meta;
const queryClient = new QueryClient(); const queryClient = new QueryClient();
const Template = () => { type Props = {
onClick?: () => void;
onMouseEnter?: () => void;
onMouseLeave?: () => void;
onSuccess?: () => void;
onDeletedItem?: () => void;
};
const Template = (p: Props) => {
return ( return (
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
{<Upload useWorker={false} multiple />} {<Upload useWorker={false} multiple {...p} />}
</QueryClientProvider> </QueryClientProvider>
); );
}; };
@ -59,11 +80,11 @@ const slowProvider = () =>
} }
); );
const SlowTemplate = () => { const SlowTemplate = (p: Props) => {
return ( return (
<div className="demo"> <div className="demo">
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
{<Upload useWorker={false} multiple provider={slowProvider} />} {<Upload useWorker={false} multiple provider={slowProvider} {...p} />}
</QueryClientProvider> </QueryClientProvider>
</div> </div>
); );
@ -71,7 +92,7 @@ const SlowTemplate = () => {
export const Slow = SlowTemplate.bind({}); export const Slow = SlowTemplate.bind({});
const SingleTemplate = () => { const SingleTemplate = (p: Props) => {
return ( return (
<div className="demo"> <div className="demo">
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
@ -81,6 +102,7 @@ const SingleTemplate = () => {
multiple={false} multiple={false}
editable={false} editable={false}
provider={slowProvider} provider={slowProvider}
{...p}
/> />
} }
</QueryClientProvider> </QueryClientProvider>