Fix standard React form event targets in front components (#20525)

Fixes #20354

## Problem

Front component form events currently expose serialized form state
through a sandbox-specific event shape, such as `event.detail.value` and
`event.detail.checked`.

That works for examples that explicitly read `event.detail`, but it is
surprising for app authors writing standard React form handlers:

```tsx
onChange={(event) => {
  setValue(event.target.value);
}}
Internal app code already has to defend against multiple possible shapes:

// Values may live on e.detail.value, e.value, or e.target.value.
This suggests the sandbox event shape is leaking into userland.

Solution
This change keeps the existing event.detail behavior, but also syncs serialized event target properties back onto the remote element before dispatching the event.

That means both styles work:

// Existing sandbox-specific style
event.detail.value;

// Standard React style
event.target.value;
The same applies to checked, files, scroll/media target properties, and similar serialized target state.

What Changed
Added a shared helper to apply serialized event target properties onto the remote element.
Updated generated remote element event configs to dispatch serialized events through a custom event config.
Updated the remote-dom element generator so regenerated files preserve this behavior.
Updated Storybook form-event examples to use standard React event target reads.
Added/updated Storybook coverage for input, checkbox, textarea, select, submit, and caret preservation flows.
Validation
Ran git diff --check
Ran a targeted TypeScript error scan for the changed front component renderer files
Manually verified the Storybook FrontComponent/EventForwarding form event story locally:
text input updates state
checkbox updates state
submit reflects the updated JSON
Note: local Storybook verification on Windows required temporary local build/cache fixes that are not included in this PR, to keep this change focused on front component event behavior.

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
qinglong
2026-05-22 22:58:33 +08:00
committed by GitHub
parent 59d69e2f5c
commit e3c79c803c
373 changed files with 13530 additions and 1366 deletions
@@ -66,10 +66,16 @@
],
"inputs": [
"{projectRoot}/scripts/front-component-stories/**/*",
"{projectRoot}/src/__stories__/example-sources/*",
"{projectRoot}/src/__stories__/html-tag/**/*",
"{projectRoot}/src/__stories__/host-api/**/*",
"{projectRoot}/src/__stories__/showcase/**/*",
"{projectRoot}/src/__stories__/shared/front-components/**/*",
"{workspaceRoot}/packages/twenty-sdk/src/cli/utilities/build/**/*"
],
"outputs": ["{projectRoot}/src/__stories__/example-sources-built/*"],
"outputs": [
"{projectRoot}/src/__stories__/example-sources-built/*",
"{projectRoot}/src/__stories__/example-sources-built-preact/*"
],
"options": {
"command": "tsx {projectRoot}/scripts/front-component-stories/build-source-examples.ts"
}
@@ -6,10 +6,7 @@ import { fileURLToPath } from 'node:url';
import { getFrontComponentBuildPlugins } from 'twenty-sdk/front-component-renderer/build';
const dirname = path.dirname(fileURLToPath(import.meta.url));
const exampleSourcesDir = path.resolve(
dirname,
'../../src/__stories__/example-sources',
);
const storiesDir = path.resolve(dirname, '../../src/__stories__');
const exampleSourcesBuiltDir = path.resolve(
dirname,
'../../src/__stories__/example-sources-built',
@@ -19,6 +16,8 @@ const exampleSourcesBuiltPreactDir = path.resolve(
'../../src/__stories__/example-sources-built-preact',
);
const SOURCE_SCAN_ROOTS = ['html-tag', 'host-api', 'showcase'];
const rootNodeModules = path.resolve(dirname, '../../../../node_modules');
const twentyUiIndividualIndex = path.resolve(
@@ -71,44 +70,69 @@ const storyAlias = {
...twentySharedAliases,
};
const STORY_COMPONENTS = [
'static.front-component',
'interactive.front-component',
'lifecycle.front-component',
'chakra-example.front-component',
'tailwind-example.front-component',
'emotion-example.front-component',
'styled-components-example.front-component',
'shadcn-example.front-component',
'mui-example.front-component',
'twenty-ui-example.front-component',
'sdk-context-example.front-component',
'form-events.front-component',
'keyboard-events.front-component',
'host-api-calls.front-component',
'caret-preservation.front-component',
'file-input.front-component',
];
const ENTRY_POINT_PATTERN = /\.front-component\.tsx$/;
const findEntryPointFiles = (directory: string): string[] => {
const result: string[] = [];
if (!fs.existsSync(directory)) {
return result;
}
for (const dirent of fs.readdirSync(directory, { withFileTypes: true })) {
const absolutePath = path.join(directory, dirent.name);
if (dirent.isDirectory()) {
if (dirent.name === 'shared') {
continue;
}
result.push(...findEntryPointFiles(absolutePath));
continue;
}
if (!dirent.isFile()) {
continue;
}
if (ENTRY_POINT_PATTERN.test(dirent.name)) {
result.push(absolutePath);
}
}
return result;
};
const resolveEntryPoints = (): Record<string, string> => {
const files = SOURCE_SCAN_ROOTS.flatMap((root) =>
findEntryPointFiles(path.join(storiesDir, root)),
);
const entryPoints: Record<string, string> = {};
for (const name of STORY_COMPONENTS) {
const filePath = path.join(exampleSourcesDir, `${name}.tsx`);
for (const filePath of files) {
const basename = path.basename(filePath).replace(/\.tsx$/, '');
if (!fs.existsSync(filePath)) {
if (entryPoints[basename] !== undefined) {
throw new Error(
`Story component source file not found: ${filePath}\n` +
`Ensure the file exists in ${exampleSourcesDir} and the name in STORY_COMPONENTS is correct.`,
`Duplicate front-component basename "${basename}" found at ${filePath} and ${entryPoints[basename]}`,
);
}
entryPoints[name] = filePath;
entryPoints[basename] = filePath;
}
if (Object.keys(entryPoints).length === 0) {
throw new Error(
`No front-component source files found under ${storiesDir} (scanned: ${SOURCE_SCAN_ROOTS.join(', ')})`,
);
}
return entryPoints;
};
const STORY_COMPONENTS = Object.keys(resolveEntryPoints());
type BundleSizeEntry = {
name: string;
reactBytes: number;
@@ -107,6 +107,66 @@ const generateCommonEventsType = (
},
],
});
sourceFile.addVariableStatement({
declarationKind: VariableDeclarationKind.Const,
declarations: [
{
name: 'createSerializedEventConfig',
initializer: (writer) => {
writer.write(
'(eventType: string): RemoteElementEventListenerDefinition => (',
);
writer.block(() => {
writer.writeLine(
'dispatchEvent(this: Element, eventData: SerializedEventData) {',
);
writer.indent(() => {
writer.writeLine('applySerializedEventTargetProperties(');
writer.indent(() => {
writer.writeLine('this as unknown as Record<string, unknown>,');
writer.writeLine('eventData,');
});
writer.writeLine(');');
writer.blankLine();
writer.writeLine('return new CustomEvent(eventType, {');
writer.indent(() => {
writer.writeLine('detail: eventData,');
});
writer.writeLine('}) as RemoteEvent<SerializedEventData>;');
});
writer.writeLine('},');
});
writer.write(')');
},
},
],
});
sourceFile.addVariableStatement({
declarationKind: VariableDeclarationKind.Const,
declarations: [
{
name: 'HTML_COMMON_EVENTS_CONFIG',
initializer: (writer) => {
writer.writeLine('Object.fromEntries(');
writer.indent(() => {
writer.writeLine(
`${TYPE_NAMES.COMMON_EVENTS_ARRAY}.map((eventType) => [`,
);
writer.indent(() => {
writer.writeLine('eventType,');
writer.writeLine('createSerializedEventConfig(eventType),');
});
writer.writeLine(']),');
});
writer.write(
`) as RemoteElementEventListenersDefinition<${TYPE_NAMES.COMMON_EVENTS}>`,
);
},
},
],
});
};
const generateCommonPropertiesConfig = (
@@ -246,17 +306,19 @@ const generateElementDefinition = (
}
}
if (hasEvents) {
const formattedCustomEvents = customEvents
.map((event) => `'${event}'`)
.join(', ');
writer.write('events: ');
writer.block(() => {
if (hasCommonHtmlEvents) {
writer.writeLine('...HTML_COMMON_EVENTS_CONFIG,');
}
writer.write(
hasCommonHtmlEvents && customEvents.length > 0
? `events: [...${TYPE_NAMES.COMMON_EVENTS_ARRAY}, ${formattedCustomEvents}],`
: hasCommonHtmlEvents
? `events: [...${TYPE_NAMES.COMMON_EVENTS_ARRAY}],`
: `events: [${formattedCustomEvents}],`,
);
for (const event of customEvents) {
writer.writeLine(
`'${event}': createSerializedEventConfig('${event}'),`,
);
}
});
writer.write(',');
writer.newLine();
}
});
@@ -332,12 +394,17 @@ export const generateRemoteElements = (
INTERNAL_ELEMENT_CLASSES.ROOT,
INTERNAL_ELEMENT_CLASSES.FRAGMENT,
{ name: 'RemoteEvent', isTypeOnly: true },
{ name: 'RemoteElementEventListenerDefinition', isTypeOnly: true },
{ name: 'RemoteElementEventListenersDefinition', isTypeOnly: true },
],
});
sourceFile.addImportDeclaration({
moduleSpecifier: '@/constants/SerializedEventData',
namedImports: [{ name: 'SerializedEventData', isTypeOnly: true }],
namedImports: [
'applySerializedEventTargetProperties',
{ name: 'SerializedEventData', isTypeOnly: true },
],
});
const commonPropertyNames = new Set(Object.keys(commonProperties));
@@ -1,6 +1,6 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import bundleSizes from './example-sources-built/bundle-sizes.json';
import bundleSizes from '@/__stories__/example-sources-built/bundle-sizes.json';
type BundleSizeEntry = {
name: string;
@@ -1,517 +0,0 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { expect, fn, userEvent, waitFor, within } from 'storybook/test';
import { FrontComponentRenderer } from '../host/components/FrontComponentRenderer';
import { getBuiltStoryComponentPathForRender } from './utils/getBuiltStoryComponentPathForRender';
const errorHandler = fn();
const createHostApiMocks = () => ({
navigate: fn().mockResolvedValue(undefined),
enqueueSnackbar: fn().mockResolvedValue(undefined),
openSidePanelPage: fn().mockResolvedValue(undefined),
closeSidePanel: fn().mockResolvedValue(undefined),
unmountFrontComponent: fn().mockResolvedValue(undefined),
updateProgress: fn().mockResolvedValue(undefined),
requestAccessTokenRefresh: fn().mockResolvedValue('refreshed-token'),
openCommandConfirmationModal: fn().mockResolvedValue(undefined),
});
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/EventForwarding',
component: FrontComponentRenderer,
parameters: {
layout: 'centered',
},
args: {
onError: errorHandler,
applicationAccessToken: 'fake-token',
executionContext: {
frontComponentId: 'storybook-test',
userId: null,
recordId: null,
selectedRecordIds: [],
},
colorScheme: 'light',
frontComponentHostCommunicationApi: createHostApiMocks(),
},
beforeEach: () => {
errorHandler.mockClear();
},
};
export default meta;
type Story = StoryObj<typeof FrontComponentRenderer>;
const MOUNT_TIMEOUT = 30000;
const INTERACTION_TIMEOUT = 5000;
const HOST_API_TIMEOUT = 10000;
const createComponentStory = (
name: string,
options?: { play?: Story['play'] },
): Story => ({
args: {
componentUrl: getBuiltStoryComponentPathForRender(
`${name}.front-component`,
),
},
...(options?.play ? { play: options.play } : {}),
});
const createHostApiStory = (play: Story['play']): Story => ({
...createComponentStory('host-api-calls'),
args: {
...createComponentStory('host-api-calls').args,
frontComponentHostCommunicationApi: createHostApiMocks(),
},
play,
});
export const FormTextInput: Story = createComponentStory('form-events', {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await canvas.findByTestId(
'form-events-component',
{},
{ timeout: MOUNT_TIMEOUT },
);
const textInput = await canvas.findByTestId('text-input');
await userEvent.type(textInput, 'hello');
expect(
await canvas.findByText('hello', {}, { timeout: INTERACTION_TIMEOUT }),
).toBeVisible();
},
});
export const FormCheckbox: Story = createComponentStory('form-events', {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await canvas.findByTestId(
'form-events-component',
{},
{ timeout: MOUNT_TIMEOUT },
);
const checkbox = await canvas.findByTestId('checkbox-input');
await userEvent.click(checkbox);
expect(
await canvas.findByText('true', {}, { timeout: INTERACTION_TIMEOUT }),
).toBeVisible();
},
});
export const FormFocusAndBlur: Story = createComponentStory('form-events', {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await canvas.findByTestId(
'form-events-component',
{},
{ timeout: MOUNT_TIMEOUT },
);
const textInput = await canvas.findByTestId('text-input');
await userEvent.click(textInput);
expect(
await canvas.findByText('focused', {}, { timeout: INTERACTION_TIMEOUT }),
).toBeVisible();
await userEvent.click(await canvas.findByTestId('form-events-component'));
expect(
await canvas.findByText('blurred', {}, { timeout: INTERACTION_TIMEOUT }),
).toBeVisible();
},
});
export const FormSubmission: Story = createComponentStory('form-events', {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await canvas.findByTestId(
'form-events-component',
{},
{ timeout: MOUNT_TIMEOUT },
);
const textInput = await canvas.findByTestId('text-input');
await userEvent.type(textInput, 'hello');
const checkbox = await canvas.findByTestId('checkbox-input');
await userEvent.click(checkbox);
const submitButton = await canvas.findByTestId('submit-button');
await userEvent.click(submitButton);
expect(
await canvas.findByText(
'{"text":"hello","checkbox":true}',
{},
{ timeout: INTERACTION_TIMEOUT },
),
).toBeVisible();
},
});
export const KeyboardBasicInput: Story = createComponentStory(
'keyboard-events',
{
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await canvas.findByTestId(
'keyboard-events-component',
{},
{ timeout: MOUNT_TIMEOUT },
);
const input = await canvas.findByTestId('keyboard-input');
await userEvent.click(input);
await userEvent.keyboard('a');
expect(
await canvas.findByText('a', {}, { timeout: INTERACTION_TIMEOUT }),
).toBeVisible();
expect(
await canvas.findByText('KeyA', {}, { timeout: INTERACTION_TIMEOUT }),
).toBeVisible();
expect(
await canvas.findByText(
/^[1-9]\d*$/,
{},
{ timeout: INTERACTION_TIMEOUT },
),
).toBeVisible();
},
},
);
export const KeyboardModifiers: Story = createComponentStory(
'keyboard-events',
{
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await canvas.findByTestId(
'keyboard-events-component',
{},
{ timeout: MOUNT_TIMEOUT },
);
const input = await canvas.findByTestId('keyboard-input');
await userEvent.click(input);
await userEvent.keyboard('{Shift>}b{/Shift}');
expect(
await canvas.findByText('shift', {}, { timeout: INTERACTION_TIMEOUT }),
).toBeVisible();
},
},
);
export const HostApiNavigate: Story = createHostApiStory(
async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
const api = args.frontComponentHostCommunicationApi!;
await canvas.findByTestId(
'host-api-calls-component',
{},
{ timeout: MOUNT_TIMEOUT },
);
const navigateBtn = await canvas.findByTestId('btn-navigate');
await userEvent.click(navigateBtn);
await waitFor(
() => {
expect(api.navigate).toHaveBeenCalled();
},
{ timeout: HOST_API_TIMEOUT },
);
expect(
await canvas.findByText(
'navigate:success',
{},
{ timeout: INTERACTION_TIMEOUT },
),
).toBeVisible();
},
);
export const HostApiSnackbar: Story = createHostApiStory(
async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
const api = args.frontComponentHostCommunicationApi!;
await canvas.findByTestId(
'host-api-calls-component',
{},
{ timeout: MOUNT_TIMEOUT },
);
const snackbarBtn = await canvas.findByTestId('btn-snackbar');
await userEvent.click(snackbarBtn);
await waitFor(
() => {
expect(api.enqueueSnackbar).toHaveBeenCalledWith({
message: 'Test notification',
variant: 'success',
});
},
{ timeout: HOST_API_TIMEOUT },
);
expect(
await canvas.findByText(
'snackbar:success',
{},
{ timeout: INTERACTION_TIMEOUT },
),
).toBeVisible();
},
);
export const HostApiProgress: Story = createHostApiStory(
async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
const api = args.frontComponentHostCommunicationApi!;
await canvas.findByTestId(
'host-api-calls-component',
{},
{ timeout: MOUNT_TIMEOUT },
);
const progressBtn = await canvas.findByTestId('btn-progress');
await userEvent.click(progressBtn);
await waitFor(
() => {
expect(api.updateProgress).toHaveBeenCalledWith(50);
},
{ timeout: HOST_API_TIMEOUT },
);
expect(
await canvas.findByText(
'progress:success',
{},
{ timeout: INTERACTION_TIMEOUT },
),
).toBeVisible();
},
);
const TYPING_TIMEOUT = 10000;
const expectCaretAt = async (
element: HTMLInputElement | HTMLTextAreaElement,
position: number,
): Promise<void> => {
await waitFor(
() => {
expect(element.selectionStart).toBe(position);
expect(element.selectionEnd).toBe(position);
},
{ timeout: TYPING_TIMEOUT },
);
};
export const InputCaretPreservedMidString: Story = createComponentStory(
'caret-preservation',
{
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await canvas.findByTestId(
'caret-preservation-component',
{},
{ timeout: MOUNT_TIMEOUT },
);
const input = (await canvas.findByTestId(
'caret-text-input',
)) as HTMLInputElement;
await waitFor(
() => {
expect(input.value).toBe('Hello world');
},
{ timeout: INTERACTION_TIMEOUT },
);
input.focus();
input.setSelectionRange(4, 4);
await userEvent.keyboard('X');
await waitFor(
() => {
expect(input.value).toBe('HellXo world');
expect(canvas.getByTestId('caret-text-value').textContent).toBe(
'HellXo world',
);
},
{ timeout: TYPING_TIMEOUT },
);
await expectCaretAt(input, 5);
},
},
);
export const TextareaCaretPreservedMidString: Story = createComponentStory(
'caret-preservation',
{
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await canvas.findByTestId(
'caret-preservation-component',
{},
{ timeout: MOUNT_TIMEOUT },
);
const textarea = (await canvas.findByTestId(
'caret-textarea-input',
)) as HTMLTextAreaElement;
await waitFor(
() => {
expect(textarea.value).toBe('Hello world');
},
{ timeout: INTERACTION_TIMEOUT },
);
textarea.focus();
textarea.setSelectionRange(4, 4);
await userEvent.keyboard('X');
await waitFor(
() => {
expect(textarea.value).toBe('HellXo world');
expect(canvas.getByTestId('caret-textarea-value').textContent).toBe(
'HellXo world',
);
},
{ timeout: TYPING_TIMEOUT },
);
await expectCaretAt(textarea, 5);
},
},
);
export const FileInputSingle: Story = createComponentStory('file-input', {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await canvas.findByTestId(
'file-input-component',
{},
{ timeout: MOUNT_TIMEOUT },
);
const input = (await canvas.findByTestId(
'single-file-input',
)) as HTMLInputElement;
const file = new File(['hello world'], 'hello.txt', {
type: 'text/plain',
lastModified: 1700000000000,
});
await userEvent.upload(input, file);
await waitFor(
() => {
expect(canvas.getByTestId('single-file-count').textContent).toBe('1');
expect(canvas.getByTestId('single-file-name').textContent).toContain(
'hello.txt',
);
expect(canvas.getByTestId('single-file-name').textContent).toContain(
'text/plain',
);
},
{ timeout: INTERACTION_TIMEOUT },
);
},
});
export const FileInputMultiple: Story = createComponentStory('file-input', {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await canvas.findByTestId(
'file-input-component',
{},
{ timeout: MOUNT_TIMEOUT },
);
const input = (await canvas.findByTestId(
'multi-file-input',
)) as HTMLInputElement;
const first = new File(['a'], 'one.png', { type: 'image/png' });
const second = new File(['bb'], 'two.png', { type: 'image/png' });
await userEvent.upload(input, [first, second]);
await waitFor(
() => {
expect(canvas.getByTestId('multi-file-count').textContent).toBe('2');
const list = canvas.getByTestId('multi-file-list');
expect(list.textContent).toContain('one.png');
expect(list.textContent).toContain('two.png');
},
{ timeout: INTERACTION_TIMEOUT },
);
},
});
export const HostApiClosePanel: Story = createHostApiStory(
async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
const api = args.frontComponentHostCommunicationApi!;
await canvas.findByTestId(
'host-api-calls-component',
{},
{ timeout: MOUNT_TIMEOUT },
);
const closePanelBtn = await canvas.findByTestId('btn-close-panel');
await userEvent.click(closePanelBtn);
await waitFor(
() => {
expect(api.closeSidePanel).toHaveBeenCalled();
},
{ timeout: HOST_API_TIMEOUT },
);
expect(
await canvas.findByText(
'closePanel:success',
{},
{ timeout: INTERACTION_TIMEOUT },
),
).toBeVisible();
},
);
@@ -1,9 +1,8 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { expect, fn, userEvent, waitFor, within } from 'storybook/test';
import { FrontComponentRenderer } from '../host/components/FrontComponentRenderer';
import { getBuiltStoryComponentPathForRender } from './utils/getBuiltStoryComponentPathForRender';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
import { getBuiltStoryComponentPathForRender } from '@/__stories__/utils/getBuiltStoryComponentPathForRender';
const errorHandler = fn();
@@ -1,9 +1,8 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { expect, fn, userEvent, within } from 'storybook/test';
import { FrontComponentRenderer } from '../host/components/FrontComponentRenderer';
import { getBuiltStoryComponentPathForRender } from './utils/getBuiltStoryComponentPathForRender';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
import { getBuiltStoryComponentPathForRender } from '@/__stories__/utils/getBuiltStoryComponentPathForRender';
const errorHandler = fn();
@@ -1,98 +0,0 @@
import { defineFrontComponent } from 'twenty-sdk/define';
import { type ChangeEvent, useState } from 'react';
const CARD_STYLE = {
padding: 24,
backgroundColor: '#eff6ff',
border: '2px solid #3b82f6',
borderRadius: 12,
fontFamily: 'system-ui, sans-serif',
display: 'flex',
flexDirection: 'column' as const,
gap: 16,
maxWidth: 400,
};
const HEADING_STYLE = {
color: '#1e3a8a',
fontWeight: 700,
fontSize: 18,
margin: 0,
};
const LABEL_STYLE = {
fontSize: 13,
fontWeight: 600,
color: '#374151',
};
const HINT_STYLE = {
fontSize: 13,
color: '#6b7280',
fontFamily: 'monospace',
};
const INPUT_STYLE = {
padding: '8px 12px',
border: '1px solid #d1d5db',
borderRadius: 6,
fontSize: 14,
fontFamily: 'monospace',
};
const INITIAL_VALUE = 'Hello world';
const CaretPreservationComponent = () => {
const [text, setText] = useState(INITIAL_VALUE);
const [textareaText, setTextareaText] = useState(INITIAL_VALUE);
return (
<div data-testid="caret-preservation-component" style={CARD_STYLE}>
<h2 style={HEADING_STYLE}>Caret Preservation</h2>
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
<label style={LABEL_STYLE}>Text input (pre-filled)</label>
<input
data-testid="caret-text-input"
type="text"
value={text}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
const detail = (event as unknown as { detail: { value?: string } })
.detail;
setText(detail?.value ?? '');
}}
style={INPUT_STYLE}
/>
<span data-testid="caret-text-value" style={HINT_STYLE}>
{text}
</span>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
<label style={LABEL_STYLE}>Textarea (pre-filled)</label>
<textarea
data-testid="caret-textarea-input"
value={textareaText}
onChange={(event: ChangeEvent<HTMLTextAreaElement>) => {
const detail = (event as unknown as { detail: { value?: string } })
.detail;
setTextareaText(detail?.value ?? '');
}}
style={INPUT_STYLE}
rows={3}
/>
<span data-testid="caret-textarea-value" style={HINT_STYLE}>
{textareaText}
</span>
</div>
</div>
);
};
export default defineFrontComponent({
universalIdentifier: 'test-caret-00000000-0000-0000-0000-000000000021',
name: 'caret-preservation-component',
description:
'Component verifying caret position is preserved during mid-string editing of <input>/<textarea>',
component: CaretPreservationComponent,
});
@@ -1,120 +0,0 @@
import { defineFrontComponent } from 'twenty-sdk/define';
import { type ChangeEvent, useState } from 'react';
type SelectedFile = {
name: string;
size: number;
type: string;
lastModified: number;
};
const CARD_STYLE = {
padding: 24,
backgroundColor: '#fef3c7',
border: '2px solid #f59e0b',
borderRadius: 12,
fontFamily: 'system-ui, sans-serif',
display: 'flex',
flexDirection: 'column' as const,
gap: 16,
maxWidth: 480,
};
const HEADING_STYLE = {
color: '#92400e',
fontWeight: 700,
fontSize: 18,
margin: 0,
};
const LABEL_STYLE = {
fontSize: 13,
fontWeight: 600,
color: '#374151',
};
const HINT_STYLE = {
fontSize: 13,
color: '#6b7280',
fontFamily: 'monospace',
};
const LIST_STYLE = {
margin: 0,
paddingLeft: 16,
fontSize: 13,
fontFamily: 'monospace',
color: '#374151',
display: 'flex',
flexDirection: 'column' as const,
gap: 4,
};
const FileInputComponent = () => {
const [singleFile, setSingleFile] = useState<SelectedFile | null>(null);
const [multiFiles, setMultiFiles] = useState<SelectedFile[]>([]);
return (
<div data-testid="file-input-component" style={CARD_STYLE}>
<h2 style={HEADING_STYLE}>File Input</h2>
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
<label style={LABEL_STYLE}>Single file</label>
<input
data-testid="single-file-input"
type="file"
onChange={(event: ChangeEvent<HTMLInputElement>) => {
const detail = (
event as unknown as { detail: { files?: SelectedFile[] } }
).detail;
setSingleFile(detail?.files?.[0] ?? null);
}}
/>
<span data-testid="single-file-count" style={HINT_STYLE}>
{singleFile === null ? 'none' : '1'}
</span>
{singleFile !== null && (
<span data-testid="single-file-name" style={HINT_STYLE}>
{singleFile.name} ({singleFile.size}B, {singleFile.type})
</span>
)}
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
<label style={LABEL_STYLE}>Multiple files (image/*)</label>
<input
data-testid="multi-file-input"
type="file"
multiple
accept="image/*"
onChange={(event: ChangeEvent<HTMLInputElement>) => {
const detail = (
event as unknown as { detail: { files?: SelectedFile[] } }
).detail;
setMultiFiles(detail?.files ?? []);
}}
/>
<span data-testid="multi-file-count" style={HINT_STYLE}>
{multiFiles.length}
</span>
{multiFiles.length > 0 && (
<ul data-testid="multi-file-list" style={LIST_STYLE}>
{multiFiles.map((file) => (
<li key={`${file.name}-${file.lastModified}`}>
{file.name} ({file.size}B)
</li>
))}
</ul>
)}
</div>
</div>
);
};
export default defineFrontComponent({
universalIdentifier: 'test-file-00000000-0000-0000-0000-000000000022',
name: 'file-input-component',
description:
'Component verifying file input metadata is forwarded from host to worker',
component: FileInputComponent,
});
@@ -1,141 +0,0 @@
import { defineFrontComponent } from 'twenty-sdk/define';
import { type ChangeEvent, useState } from 'react';
const CARD_STYLE = {
padding: 24,
backgroundColor: '#f0fdf4',
border: '2px solid #22c55e',
borderRadius: 12,
fontFamily: 'system-ui, sans-serif',
display: 'flex',
flexDirection: 'column' as const,
gap: 16,
maxWidth: 400,
};
const HEADING_STYLE = {
color: '#166534',
fontWeight: 700,
fontSize: 18,
margin: 0,
};
const LABEL_STYLE = {
fontSize: 13,
fontWeight: 600,
color: '#374151',
};
const HINT_STYLE = {
fontSize: 13,
color: '#6b7280',
};
const INPUT_STYLE = {
padding: '8px 12px',
border: '1px solid #d1d5db',
borderRadius: 6,
fontSize: 14,
};
const SUBMIT_BUTTON_STYLE = {
padding: '10px 20px',
backgroundColor: '#16a34a',
color: 'white',
border: 'none',
borderRadius: 6,
fontWeight: 600,
cursor: 'pointer',
};
const FormEventsComponent = () => {
const [textValue, setTextValue] = useState('');
const [checkboxValue, setCheckboxValue] = useState(false);
const [focusState, setFocusState] = useState('none');
const [submittedData, setSubmittedData] = useState<string | null>(null);
return (
<div data-testid="form-events-component" style={CARD_STYLE}>
<h2 style={HEADING_STYLE}>Form Events</h2>
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
<label style={LABEL_STYLE}>Text Input</label>
<input
data-testid="text-input"
type="text"
placeholder="Type here..."
onChange={(event: ChangeEvent<HTMLInputElement>) => {
const detail = (event as unknown as { detail: { value?: string } })
.detail;
setTextValue(detail?.value ?? '');
}}
onFocus={() => setFocusState('focused')}
onBlur={() => setFocusState('blurred')}
style={INPUT_STYLE}
/>
<span data-testid="text-value" style={HINT_STYLE}>
{textValue}
</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<input
data-testid="checkbox-input"
type="checkbox"
checked={checkboxValue}
onChange={(event: ChangeEvent<HTMLInputElement>) => {
const detail = (
event as unknown as { detail: { checked?: boolean } }
).detail;
setCheckboxValue(detail?.checked ?? false);
}}
/>
<label style={LABEL_STYLE}>Check me</label>
<span data-testid="checkbox-value" style={HINT_STYLE}>
{String(checkboxValue)}
</span>
</div>
<span data-testid="focus-state" style={HINT_STYLE}>
{focusState}
</span>
<button
data-testid="submit-button"
type="button"
onClick={() =>
setSubmittedData(
JSON.stringify({ text: textValue, checkbox: checkboxValue }),
)
}
style={SUBMIT_BUTTON_STYLE}
>
Submit
</button>
{submittedData !== null && (
<pre
data-testid="submitted-data"
style={{
fontSize: 13,
background: '#dcfce7',
padding: 12,
borderRadius: 8,
margin: 0,
overflow: 'auto',
}}
>
{submittedData}
</pre>
)}
</div>
);
};
export default defineFrontComponent({
universalIdentifier: 'test-form-00000000-0000-0000-0000-000000000020',
name: 'form-events-component',
description:
'Component testing form input events (onChange, onFocus, onBlur, submit)',
component: FormEventsComponent,
});
@@ -1,155 +0,0 @@
import { defineFrontComponent } from 'twenty-sdk/define';
import {
AppPath,
closeSidePanel,
enqueueSnackbar,
navigate,
openSidePanelPage,
SidePanelPages,
unmountFrontComponent,
updateProgress,
} from 'twenty-sdk/front-component';
import { useState } from 'react';
const CARD_STYLE = {
padding: 24,
backgroundColor: '#faf5ff',
border: '2px solid #a78bfa',
borderRadius: 12,
fontFamily: 'system-ui, sans-serif',
display: 'flex',
flexDirection: 'column' as const,
gap: 10,
maxWidth: 400,
};
const HEADING_STYLE = {
color: '#5b21b6',
fontWeight: 700,
fontSize: 18,
margin: 0,
};
const BUTTON_STYLE = {
padding: '8px 16px',
backgroundColor: '#7c3aed',
color: 'white',
border: 'none',
borderRadius: 6,
fontWeight: 600,
cursor: 'pointer',
fontSize: 13,
};
const STATUS_STYLE = {
fontSize: 13,
color: '#6b7280',
fontFamily: 'monospace',
};
const HostApiCallsComponent = () => {
const [apiStatus, setApiStatus] = useState('idle');
const callApi = async (name: string, apiFunction: () => Promise<void>) => {
try {
await apiFunction();
setApiStatus(`${name}:success`);
} catch (error) {
setApiStatus(
`${name}:error:${error instanceof Error ? error.message : String(error)}`,
);
}
};
return (
<div data-testid="host-api-calls-component" style={CARD_STYLE}>
<h2 style={HEADING_STYLE}>Host API Calls</h2>
<button
data-testid="btn-navigate"
type="button"
onClick={() =>
callApi('navigate', () =>
navigate(AppPath.RecordIndexPage, {
objectNamePlural: 'companies',
}),
)
}
style={BUTTON_STYLE}
>
Navigate
</button>
<button
data-testid="btn-snackbar"
type="button"
onClick={() =>
callApi('snackbar', () =>
enqueueSnackbar({
message: 'Test notification',
variant: 'success',
}),
)
}
style={BUTTON_STYLE}
>
Snackbar
</button>
<button
data-testid="btn-side-panel"
type="button"
onClick={() =>
callApi('sidePanel', () =>
openSidePanelPage({
page: SidePanelPages.ViewRecord,
pageTitle: 'Test Record',
}),
)
}
style={BUTTON_STYLE}
>
Open Side Panel
</button>
<button
data-testid="btn-close-panel"
type="button"
onClick={() => callApi('closePanel', () => closeSidePanel())}
style={BUTTON_STYLE}
>
Close Side Panel
</button>
<button
data-testid="btn-unmount"
type="button"
onClick={() => callApi('unmount', () => unmountFrontComponent())}
style={BUTTON_STYLE}
>
Unmount
</button>
<button
data-testid="btn-progress"
type="button"
onClick={() => callApi('progress', () => updateProgress(50))}
style={BUTTON_STYLE}
>
Update Progress (50)
</button>
<span data-testid="api-status" style={STATUS_STYLE}>
{apiStatus}
</span>
</div>
);
};
export default defineFrontComponent({
universalIdentifier: 'test-hapi-00000000-0000-0000-0000-000000000022',
name: 'host-api-calls-component',
description:
'Component testing host communication API calls (navigate, snackbar, side panel, etc.)',
component: HostApiCallsComponent,
});
@@ -1,125 +0,0 @@
import { defineFrontComponent } from 'twenty-sdk/define';
import { type KeyboardEvent, useState } from 'react';
type RemoteKeyboardEventDetail = {
key?: string;
code?: string;
shiftKey?: boolean;
ctrlKey?: boolean;
metaKey?: boolean;
altKey?: boolean;
};
const KeyboardEventsComponent = () => {
const [lastKey, setLastKey] = useState('');
const [lastCode, setLastCode] = useState('');
const [modifiers, setModifiers] = useState('');
const [keyCount, setKeyCount] = useState(0);
// remote-dom serializes keyboard events into CustomEvent.detail
const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
const data = (event as unknown as { detail: RemoteKeyboardEventDetail })
.detail;
setLastKey(data.key ?? '');
setLastCode(data.code ?? '');
setKeyCount((previousCount) => previousCount + 1);
const activeModifiers: string[] = [];
if (data.shiftKey) activeModifiers.push('shift');
if (data.ctrlKey) activeModifiers.push('ctrl');
if (data.metaKey) activeModifiers.push('meta');
if (data.altKey) activeModifiers.push('alt');
setModifiers(activeModifiers.join(','));
};
return (
<div
data-testid="keyboard-events-component"
style={{
padding: 24,
backgroundColor: '#fefce8',
border: '2px solid #eab308',
borderRadius: 12,
fontFamily: 'system-ui, sans-serif',
display: 'flex',
flexDirection: 'column',
gap: 12,
maxWidth: 400,
}}
>
<h2
style={{
color: '#854d0e',
fontWeight: 700,
fontSize: 18,
margin: 0,
}}
>
Keyboard Events
</h2>
<input
data-testid="keyboard-input"
type="text"
placeholder="Press keys here..."
onKeyDown={handleKeyDown}
style={{
padding: '8px 12px',
border: '1px solid #d1d5db',
borderRadius: 6,
fontSize: 14,
}}
/>
<div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
<div style={{ display: 'flex', gap: 16 }}>
<span style={{ fontSize: 13, color: '#6b7280' }}>
Key:{' '}
<span
data-testid="last-key"
style={{ fontWeight: 700, color: '#854d0e' }}
>
{lastKey}
</span>
</span>
<span style={{ fontSize: 13, color: '#6b7280' }}>
Code:{' '}
<span
data-testid="last-code"
style={{ fontWeight: 700, color: '#854d0e' }}
>
{lastCode}
</span>
</span>
</div>
<span style={{ fontSize: 13, color: '#6b7280' }}>
Modifiers:{' '}
<span
data-testid="modifiers"
style={{ fontWeight: 700, color: '#854d0e' }}
>
{modifiers}
</span>
</span>
<span style={{ fontSize: 13, color: '#6b7280' }}>
Key count:{' '}
<span
data-testid="key-count"
style={{ fontWeight: 700, color: '#854d0e' }}
>
{keyCount}
</span>
</span>
</div>
</div>
);
};
export default defineFrontComponent({
universalIdentifier: 'test-kbd0-00000000-0000-0000-0000-000000000021',
name: 'keyboard-events-component',
description:
'Component testing keyboard event serialization (key, code, modifiers)',
component: KeyboardEventsComponent,
});
@@ -0,0 +1,60 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { expect, userEvent, waitFor, within } from 'storybook/test';
import { isDefined } from 'twenty-shared/utils';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
import {
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
resetFrontComponentStoryMocks,
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
import { expectFrontComponentMounted } from '@/__stories__/shared/test-utils/matchers/expectFrontComponentMounted';
import { runFrontComponentStory } from '@/__stories__/shared/test-utils/runFrontComponentStory';
import {
HOST_API_TIMEOUT,
INTERACTION_TIMEOUT,
} from '@/__stories__/shared/test-utils/timeouts';
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/HostApi/ClosePanel',
component: FrontComponentRenderer,
parameters: { layout: 'centered' },
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
beforeEach: resetFrontComponentStoryMocks,
};
export default meta;
type Story = StoryObj<typeof FrontComponentRenderer>;
export const ClosePanel: Story = runFrontComponentStory({
frontComponentBundleName: 'host-api-side-panel-close',
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
const api = args.frontComponentHostCommunicationApi;
if (!isDefined(api)) {
throw new Error('frontComponentHostCommunicationApi is required');
}
await expectFrontComponentMounted(canvas);
const subject = await canvas.findByTestId('subject');
await userEvent.click(subject);
await waitFor(
() => {
expect(api.closeSidePanel).toHaveBeenCalled();
},
{ timeout: HOST_API_TIMEOUT },
);
expect(
await canvas.findByText(
'closePanel:success',
{},
{ timeout: INTERACTION_TIMEOUT },
),
).toBeVisible();
},
});
@@ -0,0 +1,52 @@
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import { AppPath, navigate } from 'twenty-sdk/front-component';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import { BUTTON_STYLE } from '@/__stories__/shared/front-components/styles';
const STATUS_STYLE = {
fontSize: 13,
color: '#1f2937',
fontFamily: 'monospace',
};
const HostApiNavigateFrontComponent = () => {
const [status, setStatus] = useState('idle');
const handleClick = async () => {
try {
await navigate(AppPath.RecordIndexPage, {
objectNamePlural: 'companies',
});
setStatus('navigate:success');
} catch (error) {
setStatus(
`navigate:error:${error instanceof Error ? error.message : String(error)}`,
);
}
};
return (
<FrontComponentCard title="host-api:navigate">
<button
data-testid="subject"
type="button"
onClick={handleClick}
style={BUTTON_STYLE}
>
Navigate
</button>
<span data-testid="api-status" style={STATUS_STYLE}>
{status}
</span>
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier: 'fc-host-navigate-00000000-0000-0000-0000-000000000020',
name: 'host-api-navigate-front-component',
description: 'Front component covering navigate host API',
component: HostApiNavigateFrontComponent,
});
@@ -0,0 +1,50 @@
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import { updateProgress } from 'twenty-sdk/front-component';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import { BUTTON_STYLE } from '@/__stories__/shared/front-components/styles';
const STATUS_STYLE = {
fontSize: 13,
color: '#1f2937',
fontFamily: 'monospace',
};
const HostApiProgressFrontComponent = () => {
const [status, setStatus] = useState('idle');
const handleClick = async () => {
try {
await updateProgress(50);
setStatus('progress:success');
} catch (error) {
setStatus(
`progress:error:${error instanceof Error ? error.message : String(error)}`,
);
}
};
return (
<FrontComponentCard title="host-api:progress">
<button
data-testid="subject"
type="button"
onClick={handleClick}
style={BUTTON_STYLE}
>
Update Progress
</button>
<span data-testid="api-status" style={STATUS_STYLE}>
{status}
</span>
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier: 'fc-host-progress-00000000-0000-0000-0000-000000000020',
name: 'host-api-progress-front-component',
description: 'Front component covering updateProgress host API',
component: HostApiProgressFrontComponent,
});
@@ -0,0 +1,50 @@
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import { closeSidePanel } from 'twenty-sdk/front-component';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import { BUTTON_STYLE } from '@/__stories__/shared/front-components/styles';
const STATUS_STYLE = {
fontSize: 13,
color: '#1f2937',
fontFamily: 'monospace',
};
const HostApiSidePanelCloseFrontComponent = () => {
const [status, setStatus] = useState('idle');
const handleClick = async () => {
try {
await closeSidePanel();
setStatus('closePanel:success');
} catch (error) {
setStatus(
`closePanel:error:${error instanceof Error ? error.message : String(error)}`,
);
}
};
return (
<FrontComponentCard title="host-api:side-panel:close">
<button
data-testid="subject"
type="button"
onClick={handleClick}
style={BUTTON_STYLE}
>
Close Side Panel
</button>
<span data-testid="api-status" style={STATUS_STYLE}>
{status}
</span>
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier: 'fc-host-spc-00000000-0000-0000-0000-000000000020',
name: 'host-api-side-panel-close-front-component',
description: 'Front component covering closeSidePanel host API',
component: HostApiSidePanelCloseFrontComponent,
});
@@ -0,0 +1,53 @@
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import { openSidePanelPage, SidePanelPages } from 'twenty-sdk/front-component';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import { BUTTON_STYLE } from '@/__stories__/shared/front-components/styles';
const STATUS_STYLE = {
fontSize: 13,
color: '#1f2937',
fontFamily: 'monospace',
};
const HostApiSidePanelOpenFrontComponent = () => {
const [status, setStatus] = useState('idle');
const handleClick = async () => {
try {
await openSidePanelPage({
page: SidePanelPages.ViewRecord,
pageTitle: 'Test Record',
});
setStatus('sidePanel:success');
} catch (error) {
setStatus(
`sidePanel:error:${error instanceof Error ? error.message : String(error)}`,
);
}
};
return (
<FrontComponentCard title="host-api:side-panel:open">
<button
data-testid="subject"
type="button"
onClick={handleClick}
style={BUTTON_STYLE}
>
Open Side Panel
</button>
<span data-testid="api-status" style={STATUS_STYLE}>
{status}
</span>
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier: 'fc-host-spo-00000000-0000-0000-0000-000000000020',
name: 'host-api-side-panel-open-front-component',
description: 'Front component covering openSidePanelPage host API',
component: HostApiSidePanelOpenFrontComponent,
});
@@ -0,0 +1,53 @@
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import { enqueueSnackbar } from 'twenty-sdk/front-component';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import { BUTTON_STYLE } from '@/__stories__/shared/front-components/styles';
const STATUS_STYLE = {
fontSize: 13,
color: '#1f2937',
fontFamily: 'monospace',
};
const HostApiSnackbarFrontComponent = () => {
const [status, setStatus] = useState('idle');
const handleClick = async () => {
try {
await enqueueSnackbar({
message: 'Test notification',
variant: 'success',
});
setStatus('snackbar:success');
} catch (error) {
setStatus(
`snackbar:error:${error instanceof Error ? error.message : String(error)}`,
);
}
};
return (
<FrontComponentCard title="host-api:snackbar">
<button
data-testid="subject"
type="button"
onClick={handleClick}
style={BUTTON_STYLE}
>
Snackbar
</button>
<span data-testid="api-status" style={STATUS_STYLE}>
{status}
</span>
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier: 'fc-host-snackbar-00000000-0000-0000-0000-000000000020',
name: 'host-api-snackbar-front-component',
description: 'Front component covering enqueueSnackbar host API',
component: HostApiSnackbarFrontComponent,
});
@@ -0,0 +1,50 @@
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import { unmountFrontComponent } from 'twenty-sdk/front-component';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import { BUTTON_STYLE } from '@/__stories__/shared/front-components/styles';
const STATUS_STYLE = {
fontSize: 13,
color: '#1f2937',
fontFamily: 'monospace',
};
const HostApiUnmountFrontComponent = () => {
const [status, setStatus] = useState('idle');
const handleClick = async () => {
try {
await unmountFrontComponent();
setStatus('unmount:success');
} catch (error) {
setStatus(
`unmount:error:${error instanceof Error ? error.message : String(error)}`,
);
}
};
return (
<FrontComponentCard title="host-api:unmount">
<button
data-testid="subject"
type="button"
onClick={handleClick}
style={BUTTON_STYLE}
>
Unmount
</button>
<span data-testid="api-status" style={STATUS_STYLE}>
{status}
</span>
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier: 'fc-host-unmount-00000000-0000-0000-0000-000000000020',
name: 'host-api-unmount-front-component',
description: 'Front component covering unmountFrontComponent host API',
component: HostApiUnmountFrontComponent,
});
@@ -0,0 +1,60 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { expect, userEvent, waitFor, within } from 'storybook/test';
import { isDefined } from 'twenty-shared/utils';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
import {
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
resetFrontComponentStoryMocks,
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
import { expectFrontComponentMounted } from '@/__stories__/shared/test-utils/matchers/expectFrontComponentMounted';
import { runFrontComponentStory } from '@/__stories__/shared/test-utils/runFrontComponentStory';
import {
HOST_API_TIMEOUT,
INTERACTION_TIMEOUT,
} from '@/__stories__/shared/test-utils/timeouts';
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/HostApi/Navigate',
component: FrontComponentRenderer,
parameters: { layout: 'centered' },
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
beforeEach: resetFrontComponentStoryMocks,
};
export default meta;
type Story = StoryObj<typeof FrontComponentRenderer>;
export const Navigate: Story = runFrontComponentStory({
frontComponentBundleName: 'host-api-navigate',
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
const api = args.frontComponentHostCommunicationApi;
if (!isDefined(api)) {
throw new Error('frontComponentHostCommunicationApi is required');
}
await expectFrontComponentMounted(canvas);
const subject = await canvas.findByTestId('subject');
await userEvent.click(subject);
await waitFor(
() => {
expect(api.navigate).toHaveBeenCalled();
},
{ timeout: HOST_API_TIMEOUT },
);
expect(
await canvas.findByText(
'navigate:success',
{},
{ timeout: INTERACTION_TIMEOUT },
),
).toBeVisible();
},
});
@@ -0,0 +1,60 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { expect, userEvent, waitFor, within } from 'storybook/test';
import { isDefined } from 'twenty-shared/utils';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
import {
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
resetFrontComponentStoryMocks,
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
import { expectFrontComponentMounted } from '@/__stories__/shared/test-utils/matchers/expectFrontComponentMounted';
import { runFrontComponentStory } from '@/__stories__/shared/test-utils/runFrontComponentStory';
import {
HOST_API_TIMEOUT,
INTERACTION_TIMEOUT,
} from '@/__stories__/shared/test-utils/timeouts';
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/HostApi/Progress',
component: FrontComponentRenderer,
parameters: { layout: 'centered' },
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
beforeEach: resetFrontComponentStoryMocks,
};
export default meta;
type Story = StoryObj<typeof FrontComponentRenderer>;
export const Progress: Story = runFrontComponentStory({
frontComponentBundleName: 'host-api-progress',
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
const api = args.frontComponentHostCommunicationApi;
if (!isDefined(api)) {
throw new Error('frontComponentHostCommunicationApi is required');
}
await expectFrontComponentMounted(canvas);
const subject = await canvas.findByTestId('subject');
await userEvent.click(subject);
await waitFor(
() => {
expect(api.updateProgress).toHaveBeenCalledWith(50);
},
{ timeout: HOST_API_TIMEOUT },
);
expect(
await canvas.findByText(
'progress:success',
{},
{ timeout: INTERACTION_TIMEOUT },
),
).toBeVisible();
},
});
@@ -0,0 +1,63 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { expect, userEvent, waitFor, within } from 'storybook/test';
import { isDefined } from 'twenty-shared/utils';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
import {
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
resetFrontComponentStoryMocks,
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
import { expectFrontComponentMounted } from '@/__stories__/shared/test-utils/matchers/expectFrontComponentMounted';
import { runFrontComponentStory } from '@/__stories__/shared/test-utils/runFrontComponentStory';
import {
HOST_API_TIMEOUT,
INTERACTION_TIMEOUT,
} from '@/__stories__/shared/test-utils/timeouts';
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/HostApi/Snackbar',
component: FrontComponentRenderer,
parameters: { layout: 'centered' },
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
beforeEach: resetFrontComponentStoryMocks,
};
export default meta;
type Story = StoryObj<typeof FrontComponentRenderer>;
export const Snackbar: Story = runFrontComponentStory({
frontComponentBundleName: 'host-api-snackbar',
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
const api = args.frontComponentHostCommunicationApi;
if (!isDefined(api)) {
throw new Error('frontComponentHostCommunicationApi is required');
}
await expectFrontComponentMounted(canvas);
const subject = await canvas.findByTestId('subject');
await userEvent.click(subject);
await waitFor(
() => {
expect(api.enqueueSnackbar).toHaveBeenCalledWith({
message: 'Test notification',
variant: 'success',
});
},
{ timeout: HOST_API_TIMEOUT },
);
expect(
await canvas.findByText(
'snackbar:success',
{},
{ timeout: INTERACTION_TIMEOUT },
),
).toBeVisible();
},
});
@@ -0,0 +1,37 @@
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
EventLog,
useEventLog,
} from '@/__stories__/shared/front-components/event-log';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import { FILL_RECT_STYLE } from '@/__stories__/shared/front-components/styles';
const IframeClickFrontComponent = () => {
const [interactionCount, setInteractionCount] = useState(0);
const { entries, pushEvent } = useEventLog();
return (
<FrontComponentCard title="iframe:click">
<iframe
data-testid="subject"
title="probe"
onClick={(event) => {
setInteractionCount((previous) => previous + 1);
pushEvent(event);
}}
tabIndex={0}
style={{ ...FILL_RECT_STYLE, height: 80 }}
/>
<span data-testid="front-component-value">{interactionCount}</span>
<EventLog entries={entries} />
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier: 'fc-iframe-c-click-00000000-0000-0000-0000-000000000020',
name: 'iframe-click-front-component',
description: 'Front component covering click on <iframe>',
component: IframeClickFrontComponent,
});
@@ -0,0 +1,29 @@
import { type Meta } from '@storybook/react-vite';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
import {
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
resetFrontComponentStoryMocks,
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
import {
createHtmlTagClickStory,
createHtmlTagFocusStory,
} from '@/__stories__/shared/test-utils/createHtmlElementStory';
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/HtmlTag/Embedded/Iframe/Events',
component: FrontComponentRenderer,
parameters: { layout: 'centered' },
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
beforeEach: resetFrontComponentStoryMocks,
};
export default meta;
export const Click = createHtmlTagClickStory({
frontComponentBundleName: 'iframe-click',
});
export const FocusBlur = createHtmlTagFocusStory({
frontComponentBundleName: 'iframe-focus-blur',
});
@@ -0,0 +1,38 @@
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
EventLog,
useEventLog,
} from '@/__stories__/shared/front-components/event-log';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import { FILL_RECT_STYLE } from '@/__stories__/shared/front-components/styles';
const IframeFocusBlurFrontComponent = () => {
const [interactionCount, setInteractionCount] = useState(0);
const { entries, pushEvent } = useEventLog();
return (
<FrontComponentCard title="iframe:focus-blur">
<iframe
data-testid="subject"
title="probe"
onFocus={(event) => {
setInteractionCount((previous) => previous + 1);
pushEvent(event);
}}
onBlur={(event) => pushEvent(event)}
tabIndex={0}
style={{ ...FILL_RECT_STYLE, height: 80 }}
/>
<span data-testid="front-component-value">{interactionCount}</span>
<EventLog entries={entries} />
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier: 'fc-iframe-fb-fb-00000000-0000-0000-0000-000000000020',
name: 'iframe-focus-blur-front-component',
description: 'Front component covering focus-blur on <iframe>',
component: IframeFocusBlurFrontComponent,
});
@@ -0,0 +1,38 @@
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
EventLog,
useEventLog,
} from '@/__stories__/shared/front-components/event-log';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import { FILL_RECT_STYLE } from '@/__stories__/shared/front-components/styles';
const ImgClickFrontComponent = () => {
const [interactionCount, setInteractionCount] = useState(0);
const { entries, pushEvent } = useEventLog();
return (
<FrontComponentCard title="img:click">
<img
data-testid="subject"
src="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='80' height='40'/>"
alt="probe"
onClick={(event) => {
setInteractionCount((previous) => previous + 1);
pushEvent(event);
}}
tabIndex={0}
style={{ ...FILL_RECT_STYLE, height: 40 }}
/>
<span data-testid="front-component-value">{interactionCount}</span>
<EventLog entries={entries} />
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier: 'fc-img-c-click-00000000-0000-0000-0000-000000000020',
name: 'img-click-front-component',
description: 'Front component covering click on <img>',
component: ImgClickFrontComponent,
});
@@ -0,0 +1,29 @@
import { type Meta } from '@storybook/react-vite';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
import {
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
resetFrontComponentStoryMocks,
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
import {
createHtmlTagClickStory,
createHtmlTagFocusStory,
} from '@/__stories__/shared/test-utils/createHtmlElementStory';
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/HtmlTag/Embedded/Img/Events',
component: FrontComponentRenderer,
parameters: { layout: 'centered' },
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
beforeEach: resetFrontComponentStoryMocks,
};
export default meta;
export const Click = createHtmlTagClickStory({
frontComponentBundleName: 'img-click',
});
export const FocusBlur = createHtmlTagFocusStory({
frontComponentBundleName: 'img-focus-blur',
});
@@ -0,0 +1,39 @@
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
EventLog,
useEventLog,
} from '@/__stories__/shared/front-components/event-log';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import { FILL_RECT_STYLE } from '@/__stories__/shared/front-components/styles';
const ImgFocusBlurFrontComponent = () => {
const [interactionCount, setInteractionCount] = useState(0);
const { entries, pushEvent } = useEventLog();
return (
<FrontComponentCard title="img:focus-blur">
<img
data-testid="subject"
src="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='80' height='40'/>"
alt="probe"
onFocus={(event) => {
setInteractionCount((previous) => previous + 1);
pushEvent(event);
}}
onBlur={(event) => pushEvent(event)}
tabIndex={0}
style={{ ...FILL_RECT_STYLE, height: 40 }}
/>
<span data-testid="front-component-value">{interactionCount}</span>
<EventLog entries={entries} />
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier: 'fc-img-fb-fb-00000000-0000-0000-0000-000000000020',
name: 'img-focus-blur-front-component',
description: 'Front component covering focus-blur on <img>',
component: ImgFocusBlurFrontComponent,
});
@@ -0,0 +1,27 @@
import { defineFrontComponent } from 'twenty-sdk/define';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import { PROPERTY_FIXTURE } from '@/__stories__/shared/front-components/property-fixture';
const ImgPropertiesFrontComponent = () => (
<FrontComponentCard title="img:properties">
<img
data-testid="subject"
id={PROPERTY_FIXTURE.id}
className={PROPERTY_FIXTURE.className}
title={PROPERTY_FIXTURE.title}
role={PROPERTY_FIXTURE.role}
aria-label={PROPERTY_FIXTURE.ariaLabel}
tabIndex={PROPERTY_FIXTURE.tabIndex}
src={PROPERTY_FIXTURE.src}
alt={PROPERTY_FIXTURE.alt}
/>
</FrontComponentCard>
);
export default defineFrontComponent({
universalIdentifier: 'fc-img-props-00000000-0000-0000-0000-000000000020',
name: 'img-properties-front-component',
description: 'Front component covering property reflection on <img>',
component: ImgPropertiesFrontComponent,
});
@@ -0,0 +1,26 @@
import { type Meta } from '@storybook/react-vite';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
import {
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
resetFrontComponentStoryMocks,
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
import { createPropertyReflectionStory } from '@/__stories__/shared/test-utils/createPropertyReflectionStory';
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/HtmlTag/Embedded/Img/Properties',
component: FrontComponentRenderer,
parameters: { layout: 'centered' },
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
beforeEach: resetFrontComponentStoryMocks,
};
export default meta;
export const Properties = createPropertyReflectionStory({
frontComponentBundleName: 'img-properties',
extraAttributes: {
src: 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="80" height="40"/>',
alt: 'subject-alt',
},
});
@@ -0,0 +1,42 @@
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
EventLog,
useEventLog,
} from '@/__stories__/shared/front-components/event-log';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import { FILL_RECT_STYLE } from '@/__stories__/shared/front-components/styles';
const PictureClickFrontComponent = () => {
const [interactionCount, setInteractionCount] = useState(0);
const { entries, pushEvent } = useEventLog();
return (
<FrontComponentCard title="picture:click">
<picture
data-testid="subject"
onClick={(event) => {
setInteractionCount((previous) => previous + 1);
pushEvent(event);
}}
tabIndex={0}
style={FILL_RECT_STYLE}
>
<img
src="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='80' height='40'/>"
alt="probe"
/>
</picture>
<span data-testid="front-component-value">{interactionCount}</span>
<EventLog entries={entries} />
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier:
'fc-picture-c-click-00000000-0000-0000-0000-000000000020',
name: 'picture-click-front-component',
description: 'Front component covering click on <picture>',
component: PictureClickFrontComponent,
});
@@ -0,0 +1,29 @@
import { type Meta } from '@storybook/react-vite';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
import {
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
resetFrontComponentStoryMocks,
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
import {
createHtmlTagClickStory,
createHtmlTagFocusStory,
} from '@/__stories__/shared/test-utils/createHtmlElementStory';
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/HtmlTag/Embedded/Picture/Events',
component: FrontComponentRenderer,
parameters: { layout: 'centered' },
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
beforeEach: resetFrontComponentStoryMocks,
};
export default meta;
export const Click = createHtmlTagClickStory({
frontComponentBundleName: 'picture-click',
});
export const FocusBlur = createHtmlTagFocusStory({
frontComponentBundleName: 'picture-focus-blur',
});
@@ -0,0 +1,42 @@
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
EventLog,
useEventLog,
} from '@/__stories__/shared/front-components/event-log';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import { FILL_RECT_STYLE } from '@/__stories__/shared/front-components/styles';
const PictureFocusBlurFrontComponent = () => {
const [interactionCount, setInteractionCount] = useState(0);
const { entries, pushEvent } = useEventLog();
return (
<FrontComponentCard title="picture:focus-blur">
<picture
data-testid="subject"
onFocus={(event) => {
setInteractionCount((previous) => previous + 1);
pushEvent(event);
}}
onBlur={(event) => pushEvent(event)}
tabIndex={0}
style={FILL_RECT_STYLE}
>
<img
src="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='80' height='40'/>"
alt="probe"
/>
</picture>
<span data-testid="front-component-value">{interactionCount}</span>
<EventLog entries={entries} />
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier: 'fc-picture-fb-fb-00000000-0000-0000-0000-000000000020',
name: 'picture-focus-blur-front-component',
description: 'Front component covering focus-blur on <picture>',
component: PictureFocusBlurFrontComponent,
});
@@ -0,0 +1,39 @@
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
EventLog,
useEventLog,
} from '@/__stories__/shared/front-components/event-log';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import { BUTTON_STYLE } from '@/__stories__/shared/front-components/styles';
const ButtonClickFrontComponent = () => {
const [clickCount, setClickCount] = useState(0);
const { entries, pushEvent } = useEventLog();
return (
<FrontComponentCard title="button:click">
<button
data-testid="subject"
type="button"
onClick={(event) => {
setClickCount((previous) => previous + 1);
pushEvent(event);
}}
style={BUTTON_STYLE}
>
Click me
</button>
<span data-testid="front-component-value">{clickCount}</span>
<EventLog entries={entries} />
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier: 'fc-btn-click-00000000-0000-0000-0000-000000000020',
name: 'button-click-front-component',
description: 'Front component covering click event on <button>',
component: ButtonClickFrontComponent,
});
@@ -0,0 +1,43 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { userEvent, within } from 'storybook/test';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
import {
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
resetFrontComponentStoryMocks,
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
import { expectEventLogged } from '@/__stories__/shared/test-utils/matchers/expectEventLogged';
import { expectFrontComponentMounted } from '@/__stories__/shared/test-utils/matchers/expectFrontComponentMounted';
import { expectFrontComponentValue } from '@/__stories__/shared/test-utils/matchers/expectFrontComponentValue';
import { runFrontComponentStory } from '@/__stories__/shared/test-utils/runFrontComponentStory';
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/HtmlTag/Form/Button/Events',
component: FrontComponentRenderer,
parameters: { layout: 'centered' },
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
beforeEach: resetFrontComponentStoryMocks,
};
export default meta;
type Story = StoryObj<typeof FrontComponentRenderer>;
export const ClickEvent: Story = runFrontComponentStory({
frontComponentBundleName: 'button-click',
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expectFrontComponentMounted(canvas);
const subject = await canvas.findByTestId('subject');
await userEvent.click(subject);
await expectFrontComponentValue({ canvas, expected: '1' });
await userEvent.click(subject);
await expectFrontComponentValue({ canvas, expected: '2' });
await expectEventLogged({ canvas, matcher: { type: 'click' } });
},
});
@@ -0,0 +1,28 @@
import { defineFrontComponent } from 'twenty-sdk/define';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import { PROPERTY_FIXTURE } from '@/__stories__/shared/front-components/property-fixture';
const ButtonPropertiesFrontComponent = () => (
<FrontComponentCard title="button:properties">
<button
data-testid="subject"
type="button"
id={PROPERTY_FIXTURE.id}
className={PROPERTY_FIXTURE.className}
title={PROPERTY_FIXTURE.title}
role={PROPERTY_FIXTURE.role}
aria-label={PROPERTY_FIXTURE.ariaLabel}
tabIndex={PROPERTY_FIXTURE.tabIndex}
>
button
</button>
</FrontComponentCard>
);
export default defineFrontComponent({
universalIdentifier: 'fc-btn-props-00000000-0000-0000-0000-000000000020',
name: 'button-properties-front-component',
description: 'Front component covering property reflection on <button>',
component: ButtonPropertiesFrontComponent,
});
@@ -0,0 +1,22 @@
import { type Meta } from '@storybook/react-vite';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
import {
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
resetFrontComponentStoryMocks,
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
import { createPropertyReflectionStory } from '@/__stories__/shared/test-utils/createPropertyReflectionStory';
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/HtmlTag/Form/Button/Properties',
component: FrontComponentRenderer,
parameters: { layout: 'centered' },
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
beforeEach: resetFrontComponentStoryMocks,
};
export default meta;
export const Properties = createPropertyReflectionStory({
frontComponentBundleName: 'button-properties',
});
@@ -0,0 +1,41 @@
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
EventLog,
useEventLog,
} from '@/__stories__/shared/front-components/event-log';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
const DatalistClickFrontComponent = () => {
const [interactionCount, setInteractionCount] = useState(0);
const { entries, pushEvent } = useEventLog();
return (
<FrontComponentCard title="datalist:click">
<>
<input list="probe-list" />
<datalist
id="probe-list"
data-testid="subject"
onClick={(event) => {
setInteractionCount((previous) => previous + 1);
pushEvent(event);
}}
>
<option value="alpha" />
<option value="beta" />
</datalist>
</>
<span data-testid="front-component-value">{interactionCount}</span>
<EventLog entries={entries} />
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier:
'fc-datalist-c-click-00000000-0000-0000-0000-000000000020',
name: 'datalist-click-front-component',
description: 'Front component covering click on <datalist>',
component: DatalistClickFrontComponent,
});
@@ -0,0 +1,22 @@
import { type Meta } from '@storybook/react-vite';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
import {
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
resetFrontComponentStoryMocks,
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
import { createHtmlTagClickStory } from '@/__stories__/shared/test-utils/createHtmlElementStory';
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/HtmlTag/Form/Datalist/Events',
component: FrontComponentRenderer,
parameters: { layout: 'centered' },
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
beforeEach: resetFrontComponentStoryMocks,
};
export default meta;
export const Click = createHtmlTagClickStory({
frontComponentBundleName: 'datalist-click',
});
@@ -0,0 +1,39 @@
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
EventLog,
useEventLog,
} from '@/__stories__/shared/front-components/event-log';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import { FILL_RECT_STYLE } from '@/__stories__/shared/front-components/styles';
const FieldsetClickFrontComponent = () => {
const [interactionCount, setInteractionCount] = useState(0);
const { entries, pushEvent } = useEventLog();
return (
<FrontComponentCard title="fieldset:click">
<fieldset
data-testid="subject"
onClick={(event) => {
setInteractionCount((previous) => previous + 1);
pushEvent(event);
}}
tabIndex={0}
style={FILL_RECT_STYLE}
>
fieldset
</fieldset>
<span data-testid="front-component-value">{interactionCount}</span>
<EventLog entries={entries} />
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier:
'fc-fieldset-c-click-00000000-0000-0000-0000-000000000020',
name: 'fieldset-click-front-component',
description: 'Front component covering click on <fieldset>',
component: FieldsetClickFrontComponent,
});
@@ -0,0 +1,29 @@
import { type Meta } from '@storybook/react-vite';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
import {
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
resetFrontComponentStoryMocks,
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
import {
createHtmlTagClickStory,
createHtmlTagFocusStory,
} from '@/__stories__/shared/test-utils/createHtmlElementStory';
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/HtmlTag/Form/Fieldset/Events',
component: FrontComponentRenderer,
parameters: { layout: 'centered' },
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
beforeEach: resetFrontComponentStoryMocks,
};
export default meta;
export const Click = createHtmlTagClickStory({
frontComponentBundleName: 'fieldset-click',
});
export const FocusBlur = createHtmlTagFocusStory({
frontComponentBundleName: 'fieldset-focus-blur',
});
@@ -0,0 +1,39 @@
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
EventLog,
useEventLog,
} from '@/__stories__/shared/front-components/event-log';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import { FILL_RECT_STYLE } from '@/__stories__/shared/front-components/styles';
const FieldsetFocusBlurFrontComponent = () => {
const [interactionCount, setInteractionCount] = useState(0);
const { entries, pushEvent } = useEventLog();
return (
<FrontComponentCard title="fieldset:focus-blur">
<fieldset
data-testid="subject"
onFocus={(event) => {
setInteractionCount((previous) => previous + 1);
pushEvent(event);
}}
onBlur={(event) => pushEvent(event)}
tabIndex={0}
style={FILL_RECT_STYLE}
>
fieldset
</fieldset>
<span data-testid="front-component-value">{interactionCount}</span>
<EventLog entries={entries} />
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier: 'fc-fieldset-fb-fb-00000000-0000-0000-0000-000000000020',
name: 'fieldset-focus-blur-front-component',
description: 'Front component covering focus-blur on <fieldset>',
component: FieldsetFocusBlurFrontComponent,
});
@@ -0,0 +1,27 @@
import { defineFrontComponent } from 'twenty-sdk/define';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import { PROPERTY_FIXTURE } from '@/__stories__/shared/front-components/property-fixture';
const FieldsetPropertiesFrontComponent = () => (
<FrontComponentCard title="fieldset:properties">
<fieldset
data-testid="subject"
id={PROPERTY_FIXTURE.id}
className={PROPERTY_FIXTURE.className}
title={PROPERTY_FIXTURE.title}
role={PROPERTY_FIXTURE.role}
aria-label={PROPERTY_FIXTURE.ariaLabel}
tabIndex={PROPERTY_FIXTURE.tabIndex}
>
field
</fieldset>
</FrontComponentCard>
);
export default defineFrontComponent({
universalIdentifier: 'fc-fieldset-props-00000000-0000-0000-0000-000000000020',
name: 'fieldset-properties-front-component',
description: 'Front component covering property reflection on <fieldset>',
component: FieldsetPropertiesFrontComponent,
});
@@ -0,0 +1,22 @@
import { type Meta } from '@storybook/react-vite';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
import {
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
resetFrontComponentStoryMocks,
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
import { createPropertyReflectionStory } from '@/__stories__/shared/test-utils/createPropertyReflectionStory';
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/HtmlTag/Form/Fieldset/Properties',
component: FrontComponentRenderer,
parameters: { layout: 'centered' },
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
beforeEach: resetFrontComponentStoryMocks,
};
export default meta;
export const Properties = createPropertyReflectionStory({
frontComponentBundleName: 'fieldset-properties',
});
@@ -0,0 +1,40 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { userEvent, within } from 'storybook/test';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
import {
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
resetFrontComponentStoryMocks,
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
import { expectEventLogged } from '@/__stories__/shared/test-utils/matchers/expectEventLogged';
import { expectFrontComponentMounted } from '@/__stories__/shared/test-utils/matchers/expectFrontComponentMounted';
import { expectFrontComponentValue } from '@/__stories__/shared/test-utils/matchers/expectFrontComponentValue';
import { runFrontComponentStory } from '@/__stories__/shared/test-utils/runFrontComponentStory';
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/HtmlTag/Form/Form/Events',
component: FrontComponentRenderer,
parameters: { layout: 'centered' },
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
beforeEach: resetFrontComponentStoryMocks,
};
export default meta;
type Story = StoryObj<typeof FrontComponentRenderer>;
export const SubmitEvent: Story = runFrontComponentStory({
frontComponentBundleName: 'form-submit',
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expectFrontComponentMounted(canvas);
const submitButton = await canvas.findByTestId('submit-button');
await userEvent.click(submitButton);
await expectFrontComponentValue({ canvas, expected: 'value-from-form' });
await expectEventLogged({ canvas, matcher: { type: 'submit' } });
},
});
@@ -0,0 +1,27 @@
import { defineFrontComponent } from 'twenty-sdk/define';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import { PROPERTY_FIXTURE } from '@/__stories__/shared/front-components/property-fixture';
const FormPropertiesFrontComponent = () => (
<FrontComponentCard title="form:properties">
<form
data-testid="subject"
id={PROPERTY_FIXTURE.id}
className={PROPERTY_FIXTURE.className}
title={PROPERTY_FIXTURE.title}
role={PROPERTY_FIXTURE.role}
aria-label={PROPERTY_FIXTURE.ariaLabel}
tabIndex={PROPERTY_FIXTURE.tabIndex}
>
form
</form>
</FrontComponentCard>
);
export default defineFrontComponent({
universalIdentifier: 'fc-form-props-00000000-0000-0000-0000-000000000020',
name: 'form-properties-front-component',
description: 'Front component covering property reflection on <form>',
component: FormPropertiesFrontComponent,
});
@@ -0,0 +1,22 @@
import { type Meta } from '@storybook/react-vite';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
import {
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
resetFrontComponentStoryMocks,
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
import { createPropertyReflectionStory } from '@/__stories__/shared/test-utils/createPropertyReflectionStory';
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/HtmlTag/Form/Form/Properties',
component: FrontComponentRenderer,
parameters: { layout: 'centered' },
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
beforeEach: resetFrontComponentStoryMocks,
};
export default meta;
export const Properties = createPropertyReflectionStory({
frontComponentBundleName: 'form-properties',
});
@@ -0,0 +1,57 @@
import { type FormEvent, useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
EventLog,
useEventLog,
} from '@/__stories__/shared/front-components/event-log';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import {
BUTTON_STYLE,
INPUT_STYLE,
} from '@/__stories__/shared/front-components/styles';
const FormSubmitFrontComponent = () => {
const [fieldValue, setFieldValue] = useState('value-from-form');
const [submittedValue, setSubmittedValue] = useState<string | null>(null);
const { entries, pushEvent } = useEventLog();
const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
setSubmittedValue(fieldValue);
pushEvent(event);
};
return (
<FrontComponentCard title="form:submit">
<form
data-testid="subject"
action="javascript:void(0);"
onSubmit={handleSubmit}
>
<input
data-testid="form-field"
type="text"
name="field"
value={fieldValue}
onChange={(event) => setFieldValue(event.target.value)}
style={INPUT_STYLE}
/>
<button data-testid="submit-button" type="submit" style={BUTTON_STYLE}>
Submit
</button>
</form>
<span data-testid="front-component-value">
{submittedValue ?? 'none'}
</span>
<EventLog entries={entries} />
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier: 'fc-form-submit-00000000-0000-0000-0000-000000000020',
name: 'form-submit-front-component',
description: 'Front component covering submit event on <form>',
component: FormSubmitFrontComponent,
});
@@ -0,0 +1,38 @@
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import {
INPUT_STYLE,
LABEL_STYLE,
SUBJECT_WRAPPER_STYLE,
} from '@/__stories__/shared/front-components/styles';
const CARET_INITIAL_VALUE = 'Hello world';
const InputCaretFrontComponent = () => {
const [value, setValue] = useState(CARET_INITIAL_VALUE);
return (
<FrontComponentCard title="input:text:caret">
<div style={SUBJECT_WRAPPER_STYLE}>
<label style={LABEL_STYLE}>Text input (pre-filled)</label>
<input
data-testid="subject"
type="text"
value={value}
onChange={(event) => setValue(event.target.value)}
style={INPUT_STYLE}
/>
<span data-testid="front-component-value">{value}</span>
</div>
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier: 'fc-input-caret-00000000-0000-0000-0000-000000000020',
name: 'input-caret-front-component',
description: 'Front component covering caret behavior on <input type="text">',
component: InputCaretFrontComponent,
});
@@ -0,0 +1,61 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { expect, userEvent, waitFor, within } from 'storybook/test';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
import {
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
resetFrontComponentStoryMocks,
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
import { expectFrontComponentMounted } from '@/__stories__/shared/test-utils/matchers/expectFrontComponentMounted';
import { runFrontComponentStory } from '@/__stories__/shared/test-utils/runFrontComponentStory';
import {
INTERACTION_TIMEOUT,
TYPING_TIMEOUT,
} from '@/__stories__/shared/test-utils/timeouts';
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/HtmlTag/Form/Input/Caret',
component: FrontComponentRenderer,
parameters: { layout: 'centered' },
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
beforeEach: resetFrontComponentStoryMocks,
};
export default meta;
type Story = StoryObj<typeof FrontComponentRenderer>;
export const CaretPreservedMidString: Story = runFrontComponentStory({
frontComponentBundleName: 'input-caret',
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expectFrontComponentMounted(canvas);
const subject = (await canvas.findByTestId('subject')) as HTMLInputElement;
await waitFor(
() => {
expect(subject.value).toBe('Hello world');
},
{ timeout: INTERACTION_TIMEOUT },
);
subject.focus();
subject.setSelectionRange(4, 4);
await userEvent.keyboard('X');
await waitFor(
() => {
expect(subject.value).toBe('HellXo world');
expect(canvas.getByTestId('front-component-value').textContent).toBe(
'HellXo world',
);
expect(subject.selectionStart).toBe(5);
expect(subject.selectionEnd).toBe(5);
},
{ timeout: TYPING_TIMEOUT },
);
},
});
@@ -0,0 +1,43 @@
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
EventLog,
useEventLog,
} from '@/__stories__/shared/front-components/event-log';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import {
LABEL_STYLE,
ROW_STYLE,
} from '@/__stories__/shared/front-components/styles';
const InputCheckboxFrontComponent = () => {
const [checked, setChecked] = useState(false);
const { entries, pushEvent } = useEventLog();
return (
<FrontComponentCard title="input:checkbox:checked">
<div style={ROW_STYLE}>
<input
data-testid="subject"
type="checkbox"
checked={checked}
onChange={(event) => {
setChecked(event.target.checked);
pushEvent(event);
}}
/>
<label style={LABEL_STYLE}>Check me</label>
<span data-testid="front-component-value">{String(checked)}</span>
</div>
<EventLog entries={entries} />
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier: 'fc-input-checkbox-00000000-0000-0000-0000-000000000020',
name: 'input-checkbox-front-component',
description: 'Front component covering <input type="checkbox">',
component: InputCheckboxFrontComponent,
});
@@ -0,0 +1,51 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { userEvent, within } from 'storybook/test';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
import {
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
resetFrontComponentStoryMocks,
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
import { expectEventLogged } from '@/__stories__/shared/test-utils/matchers/expectEventLogged';
import { expectFrontComponentMounted } from '@/__stories__/shared/test-utils/matchers/expectFrontComponentMounted';
import { expectFrontComponentValue } from '@/__stories__/shared/test-utils/matchers/expectFrontComponentValue';
import { runFrontComponentStory } from '@/__stories__/shared/test-utils/runFrontComponentStory';
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/HtmlTag/Form/Input/Checkbox',
component: FrontComponentRenderer,
parameters: { layout: 'centered' },
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
beforeEach: resetFrontComponentStoryMocks,
};
export default meta;
type Story = StoryObj<typeof FrontComponentRenderer>;
export const CheckedRoundTrip: Story = runFrontComponentStory({
frontComponentBundleName: 'input-checkbox',
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expectFrontComponentMounted(canvas);
const subject = await canvas.findByTestId('subject');
await userEvent.click(subject);
await expectFrontComponentValue({ canvas, expected: 'true' });
await expectEventLogged({
canvas,
matcher: { type: 'change', checked: true },
});
await userEvent.click(subject);
await expectFrontComponentValue({ canvas, expected: 'false' });
await expectEventLogged({
canvas,
matcher: { type: 'change', checked: false },
});
},
});
@@ -0,0 +1,63 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { userEvent, within } from 'storybook/test';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
import {
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
resetFrontComponentStoryMocks,
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
import { expectEventLogged } from '@/__stories__/shared/test-utils/matchers/expectEventLogged';
import { expectFrontComponentMounted } from '@/__stories__/shared/test-utils/matchers/expectFrontComponentMounted';
import { expectFrontComponentValue } from '@/__stories__/shared/test-utils/matchers/expectFrontComponentValue';
import { runFrontComponentStory } from '@/__stories__/shared/test-utils/runFrontComponentStory';
import { TYPING_DELAY } from '@/__stories__/shared/test-utils/timeouts';
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/HtmlTag/Form/Input/Events',
component: FrontComponentRenderer,
parameters: { layout: 'centered' },
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
beforeEach: resetFrontComponentStoryMocks,
};
export default meta;
type Story = StoryObj<typeof FrontComponentRenderer>;
export const TextValueRoundTrip: Story = runFrontComponentStory({
frontComponentBundleName: 'input-text-value',
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expectFrontComponentMounted(canvas);
const subject = await canvas.findByTestId('subject');
await userEvent.type(subject, 'hello', { delay: TYPING_DELAY });
await expectFrontComponentValue({ canvas, expected: 'hello' });
await expectEventLogged({
canvas,
matcher: { type: 'change', value: 'hello' },
});
},
});
export const FocusBlurEvents: Story = runFrontComponentStory({
frontComponentBundleName: 'input-text-focus-blur',
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expectFrontComponentMounted(canvas);
const subject = await canvas.findByTestId('subject');
await userEvent.click(subject);
await expectEventLogged({ canvas, matcher: { type: 'focus' } });
const blurTarget = await canvas.findByTestId('blur-target');
await userEvent.click(blurTarget);
await expectEventLogged({ canvas, matcher: { type: 'blur' } });
},
});
@@ -0,0 +1,39 @@
import { defineFrontComponent } from 'twenty-sdk/define';
import {
EventLog,
useEventLog,
} from '@/__stories__/shared/front-components/event-log';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import {
LABEL_STYLE,
SUBJECT_WRAPPER_STYLE,
} from '@/__stories__/shared/front-components/styles';
const InputFileMultipleFrontComponent = () => {
const { entries, pushEvent } = useEventLog();
return (
<FrontComponentCard title="input:file:multiple">
<div style={SUBJECT_WRAPPER_STYLE}>
<label style={LABEL_STYLE}>Multiple files</label>
<input
data-testid="subject"
type="file"
multiple
accept="image/*"
onChange={(event) => pushEvent(event)}
/>
</div>
<EventLog entries={entries} />
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier:
'fc-input-file-multiple-00000000-0000-0000-0000-000000000020',
name: 'input-file-multiple-front-component',
description: 'Front component covering multi-file <input type="file">',
component: InputFileMultipleFrontComponent,
});
@@ -0,0 +1,37 @@
import { defineFrontComponent } from 'twenty-sdk/define';
import {
EventLog,
useEventLog,
} from '@/__stories__/shared/front-components/event-log';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import {
LABEL_STYLE,
SUBJECT_WRAPPER_STYLE,
} from '@/__stories__/shared/front-components/styles';
const InputFileSingleFrontComponent = () => {
const { entries, pushEvent } = useEventLog();
return (
<FrontComponentCard title="input:file:single">
<div style={SUBJECT_WRAPPER_STYLE}>
<label style={LABEL_STYLE}>Single file</label>
<input
data-testid="subject"
type="file"
onChange={(event) => pushEvent(event)}
/>
</div>
<EventLog entries={entries} />
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier:
'fc-input-file-single-00000000-0000-0000-0000-000000000020',
name: 'input-file-single-front-component',
description: 'Front component covering single-file <input type="file">',
component: InputFileSingleFrontComponent,
});
@@ -0,0 +1,76 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { userEvent, within } from 'storybook/test';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
import {
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
resetFrontComponentStoryMocks,
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
import { expectEventLogged } from '@/__stories__/shared/test-utils/matchers/expectEventLogged';
import { expectFrontComponentMounted } from '@/__stories__/shared/test-utils/matchers/expectFrontComponentMounted';
import { runFrontComponentStory } from '@/__stories__/shared/test-utils/runFrontComponentStory';
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/HtmlTag/Form/Input/File',
component: FrontComponentRenderer,
parameters: { layout: 'centered' },
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
beforeEach: resetFrontComponentStoryMocks,
};
export default meta;
type Story = StoryObj<typeof FrontComponentRenderer>;
export const SingleFile: Story = runFrontComponentStory({
frontComponentBundleName: 'input-file-single',
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expectFrontComponentMounted(canvas);
const subject = (await canvas.findByTestId('subject')) as HTMLInputElement;
const file = new File(['hello world'], 'hello.txt', {
type: 'text/plain',
lastModified: 1700000000000,
});
await userEvent.upload(subject, file);
await expectEventLogged({
canvas,
matcher: {
type: 'change',
files: [{ name: 'hello.txt', type: 'text/plain' }],
},
});
},
});
export const MultipleFiles: Story = runFrontComponentStory({
frontComponentBundleName: 'input-file-multiple',
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expectFrontComponentMounted(canvas);
const subject = (await canvas.findByTestId('subject')) as HTMLInputElement;
const first = new File(['a'], 'one.png', { type: 'image/png' });
const second = new File(['bb'], 'two.png', { type: 'image/png' });
await userEvent.upload(subject, [first, second]);
await expectEventLogged({
canvas,
matcher: {
type: 'change',
files: [
{ name: 'one.png', type: 'image/png' },
{ name: 'two.png', type: 'image/png' },
],
},
});
},
});
@@ -0,0 +1,45 @@
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
EventLog,
useEventLog,
} from '@/__stories__/shared/front-components/event-log';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import {
INPUT_STYLE,
LABEL_STYLE,
SUBJECT_WRAPPER_STYLE,
} from '@/__stories__/shared/front-components/styles';
const InputKeyboardFrontComponent = () => {
const [lastKey, setLastKey] = useState('');
const { entries, pushEvent } = useEventLog();
return (
<FrontComponentCard title="input:text:keyboard">
<div style={SUBJECT_WRAPPER_STYLE}>
<label style={LABEL_STYLE}>Keyboard input</label>
<input
data-testid="subject"
type="text"
onKeyDown={(event) => {
setLastKey(event.key);
pushEvent(event);
}}
onKeyUp={(event) => pushEvent(event)}
style={INPUT_STYLE}
/>
<span data-testid="front-component-value">{lastKey}</span>
</div>
<EventLog entries={entries} />
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier: 'fc-input-keyboard-00000000-0000-0000-0000-000000000020',
name: 'input-keyboard-front-component',
description: 'Front component covering keyboard events on <input>',
component: InputKeyboardFrontComponent,
});
@@ -0,0 +1,65 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { userEvent, within } from 'storybook/test';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
import {
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
resetFrontComponentStoryMocks,
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
import { expectEventLogged } from '@/__stories__/shared/test-utils/matchers/expectEventLogged';
import { expectFrontComponentMounted } from '@/__stories__/shared/test-utils/matchers/expectFrontComponentMounted';
import { runFrontComponentStory } from '@/__stories__/shared/test-utils/runFrontComponentStory';
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/HtmlTag/Form/Input/Keyboard',
component: FrontComponentRenderer,
parameters: { layout: 'centered' },
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
beforeEach: resetFrontComponentStoryMocks,
};
export default meta;
type Story = StoryObj<typeof FrontComponentRenderer>;
export const BasicKey: Story = runFrontComponentStory({
frontComponentBundleName: 'input-keyboard',
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expectFrontComponentMounted(canvas);
const subject = await canvas.findByTestId('subject');
await userEvent.click(subject);
await userEvent.keyboard('a');
await expectEventLogged({
canvas,
matcher: { type: 'keydown', key: 'a', code: 'KeyA' },
});
await expectEventLogged({
canvas,
matcher: { type: 'keyup', key: 'a', code: 'KeyA' },
});
},
});
export const ShiftModifier: Story = runFrontComponentStory({
frontComponentBundleName: 'input-keyboard',
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expectFrontComponentMounted(canvas);
const subject = await canvas.findByTestId('subject');
await userEvent.click(subject);
await userEvent.keyboard('{Shift>}b{/Shift}');
await expectEventLogged({
canvas,
matcher: { type: 'keydown', shiftKey: true },
});
},
});
@@ -0,0 +1,33 @@
import { defineFrontComponent } from 'twenty-sdk/define';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import { PROPERTY_FIXTURE } from '@/__stories__/shared/front-components/property-fixture';
const noop = () => undefined;
const InputNumberPropertiesFrontComponent = () => (
<FrontComponentCard title="input:number:properties">
<input
data-testid="subject"
id={PROPERTY_FIXTURE.id}
className={PROPERTY_FIXTURE.className}
title={PROPERTY_FIXTURE.title}
role={PROPERTY_FIXTURE.role}
aria-label={PROPERTY_FIXTURE.ariaLabel}
tabIndex={PROPERTY_FIXTURE.tabIndex}
type="number"
name={PROPERTY_FIXTURE.name}
value={String(PROPERTY_FIXTURE.numberValue)}
onChange={noop}
/>
</FrontComponentCard>
);
export default defineFrontComponent({
universalIdentifier:
'fc-input-num-props-00000000-0000-0000-0000-000000000020',
name: 'input-number-properties-front-component',
description:
'Front component covering property reflection on <input type="number">',
component: InputNumberPropertiesFrontComponent,
});
@@ -0,0 +1,38 @@
import { type Meta } from '@storybook/react-vite';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
import { PROPERTY_FIXTURE } from '@/__stories__/shared/front-components/property-fixture';
import {
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
resetFrontComponentStoryMocks,
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
import { createPropertyReflectionStory } from '@/__stories__/shared/test-utils/createPropertyReflectionStory';
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/HtmlTag/Form/Input/Properties',
component: FrontComponentRenderer,
parameters: { layout: 'centered' },
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
beforeEach: resetFrontComponentStoryMocks,
};
export default meta;
export const Text = createPropertyReflectionStory({
frontComponentBundleName: 'input-text-properties',
extraAttributes: {
type: PROPERTY_FIXTURE.type,
name: PROPERTY_FIXTURE.name,
placeholder: PROPERTY_FIXTURE.placeholder,
},
extraProperties: { value: PROPERTY_FIXTURE.textValue },
});
export const Number = createPropertyReflectionStory({
frontComponentBundleName: 'input-number-properties',
extraAttributes: {
type: 'number',
name: PROPERTY_FIXTURE.name,
},
extraProperties: { value: String(PROPERTY_FIXTURE.numberValue) },
});
@@ -0,0 +1,40 @@
import { defineFrontComponent } from 'twenty-sdk/define';
import {
EventLog,
useEventLog,
} from '@/__stories__/shared/front-components/event-log';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import {
INPUT_STYLE,
LABEL_STYLE,
SUBJECT_WRAPPER_STYLE,
} from '@/__stories__/shared/front-components/styles';
const InputTextFocusBlurFrontComponent = () => {
const { entries, pushEvent } = useEventLog();
return (
<FrontComponentCard title="input:text:focus-blur">
<div style={SUBJECT_WRAPPER_STYLE}>
<label style={LABEL_STYLE}>Focus / Blur</label>
<input
data-testid="subject"
type="text"
onFocus={(event) => pushEvent(event)}
onBlur={(event) => pushEvent(event)}
style={INPUT_STYLE}
/>
</div>
<input data-testid="blur-target" type="text" style={INPUT_STYLE} />
<EventLog entries={entries} />
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier: 'fc-input-text-fb-00000000-0000-0000-0000-000000000020',
name: 'input-text-focus-blur-front-component',
description: 'Front component covering focus/blur on text <input>',
component: InputTextFocusBlurFrontComponent,
});
@@ -0,0 +1,34 @@
import { defineFrontComponent } from 'twenty-sdk/define';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import { PROPERTY_FIXTURE } from '@/__stories__/shared/front-components/property-fixture';
const noop = () => undefined;
const InputTextPropertiesFrontComponent = () => (
<FrontComponentCard title="input:text:properties">
<input
data-testid="subject"
id={PROPERTY_FIXTURE.id}
className={PROPERTY_FIXTURE.className}
title={PROPERTY_FIXTURE.title}
role={PROPERTY_FIXTURE.role}
aria-label={PROPERTY_FIXTURE.ariaLabel}
tabIndex={PROPERTY_FIXTURE.tabIndex}
type={PROPERTY_FIXTURE.type}
name={PROPERTY_FIXTURE.name}
placeholder={PROPERTY_FIXTURE.placeholder}
value={PROPERTY_FIXTURE.textValue}
onChange={noop}
/>
</FrontComponentCard>
);
export default defineFrontComponent({
universalIdentifier:
'fc-input-text-props-00000000-0000-0000-0000-000000000020',
name: 'input-text-properties-front-component',
description:
'Front component covering property reflection on <input type="text">',
component: InputTextPropertiesFrontComponent,
});
@@ -0,0 +1,47 @@
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
EventLog,
useEventLog,
} from '@/__stories__/shared/front-components/event-log';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import {
INPUT_STYLE,
LABEL_STYLE,
SUBJECT_WRAPPER_STYLE,
} from '@/__stories__/shared/front-components/styles';
const InputTextValueFrontComponent = () => {
const [value, setValue] = useState('');
const { entries, pushEvent } = useEventLog();
return (
<FrontComponentCard title="input:text:value">
<div style={SUBJECT_WRAPPER_STYLE}>
<label style={LABEL_STYLE}>Text input</label>
<input
data-testid="subject"
type="text"
placeholder="Type here..."
value={value}
onChange={(event) => {
setValue(event.target.value);
pushEvent(event);
}}
style={INPUT_STYLE}
/>
<span data-testid="front-component-value">{value}</span>
</div>
<EventLog entries={entries} />
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier:
'fc-input-text-value-00000000-0000-0000-0000-000000000020',
name: 'input-text-value-front-component',
description: 'Front component covering <input type="text"> value round-trip',
component: InputTextValueFrontComponent,
});
@@ -0,0 +1,38 @@
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
EventLog,
useEventLog,
} from '@/__stories__/shared/front-components/event-log';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import { FILL_INLINE_STYLE } from '@/__stories__/shared/front-components/styles';
const LabelClickFrontComponent = () => {
const [interactionCount, setInteractionCount] = useState(0);
const { entries, pushEvent } = useEventLog();
return (
<FrontComponentCard title="label:click">
<label
data-testid="subject"
onClick={(event) => {
setInteractionCount((previous) => previous + 1);
pushEvent(event);
}}
tabIndex={0}
style={FILL_INLINE_STYLE}
>
label
</label>
<span data-testid="front-component-value">{interactionCount}</span>
<EventLog entries={entries} />
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier: 'fc-label-c-click-00000000-0000-0000-0000-000000000020',
name: 'label-click-front-component',
description: 'Front component covering click on <label>',
component: LabelClickFrontComponent,
});
@@ -0,0 +1,29 @@
import { type Meta } from '@storybook/react-vite';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
import {
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
resetFrontComponentStoryMocks,
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
import {
createHtmlTagClickStory,
createHtmlTagFocusStory,
} from '@/__stories__/shared/test-utils/createHtmlElementStory';
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/HtmlTag/Form/Label/Events',
component: FrontComponentRenderer,
parameters: { layout: 'centered' },
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
beforeEach: resetFrontComponentStoryMocks,
};
export default meta;
export const Click = createHtmlTagClickStory({
frontComponentBundleName: 'label-click',
});
export const FocusBlur = createHtmlTagFocusStory({
frontComponentBundleName: 'label-focus-blur',
});
@@ -0,0 +1,39 @@
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
EventLog,
useEventLog,
} from '@/__stories__/shared/front-components/event-log';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import { FILL_INLINE_STYLE } from '@/__stories__/shared/front-components/styles';
const LabelFocusBlurFrontComponent = () => {
const [interactionCount, setInteractionCount] = useState(0);
const { entries, pushEvent } = useEventLog();
return (
<FrontComponentCard title="label:focus-blur">
<label
data-testid="subject"
onFocus={(event) => {
setInteractionCount((previous) => previous + 1);
pushEvent(event);
}}
onBlur={(event) => pushEvent(event)}
tabIndex={0}
style={FILL_INLINE_STYLE}
>
label
</label>
<span data-testid="front-component-value">{interactionCount}</span>
<EventLog entries={entries} />
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier: 'fc-label-fb-fb-00000000-0000-0000-0000-000000000020',
name: 'label-focus-blur-front-component',
description: 'Front component covering focus-blur on <label>',
component: LabelFocusBlurFrontComponent,
});
@@ -0,0 +1,27 @@
import { defineFrontComponent } from 'twenty-sdk/define';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import { PROPERTY_FIXTURE } from '@/__stories__/shared/front-components/property-fixture';
const LabelPropertiesFrontComponent = () => (
<FrontComponentCard title="label:properties">
<label
data-testid="subject"
id={PROPERTY_FIXTURE.id}
className={PROPERTY_FIXTURE.className}
title={PROPERTY_FIXTURE.title}
role={PROPERTY_FIXTURE.role}
aria-label={PROPERTY_FIXTURE.ariaLabel}
tabIndex={PROPERTY_FIXTURE.tabIndex}
>
content
</label>
</FrontComponentCard>
);
export default defineFrontComponent({
universalIdentifier: 'fc-label-props-00000000-0000-0000-0000-000000000020',
name: 'label-properties-front-component',
description: 'Front component covering property reflection on <label>',
component: LabelPropertiesFrontComponent,
});
@@ -0,0 +1,22 @@
import { type Meta } from '@storybook/react-vite';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
import {
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
resetFrontComponentStoryMocks,
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
import { createPropertyReflectionStory } from '@/__stories__/shared/test-utils/createPropertyReflectionStory';
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/HtmlTag/Form/Label/Properties',
component: FrontComponentRenderer,
parameters: { layout: 'centered' },
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
beforeEach: resetFrontComponentStoryMocks,
};
export default meta;
export const Properties = createPropertyReflectionStory({
frontComponentBundleName: 'label-properties',
});
@@ -0,0 +1,40 @@
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
EventLog,
useEventLog,
} from '@/__stories__/shared/front-components/event-log';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import { FILL_INLINE_STYLE } from '@/__stories__/shared/front-components/styles';
const LegendClickFrontComponent = () => {
const [interactionCount, setInteractionCount] = useState(0);
const { entries, pushEvent } = useEventLog();
return (
<FrontComponentCard title="legend:click">
<fieldset>
<legend
data-testid="subject"
onClick={(event) => {
setInteractionCount((previous) => previous + 1);
pushEvent(event);
}}
tabIndex={0}
style={FILL_INLINE_STYLE}
>
legend
</legend>
</fieldset>
<span data-testid="front-component-value">{interactionCount}</span>
<EventLog entries={entries} />
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier: 'fc-legend-c-click-00000000-0000-0000-0000-000000000020',
name: 'legend-click-front-component',
description: 'Front component covering click on <legend>',
component: LegendClickFrontComponent,
});
@@ -0,0 +1,29 @@
import { type Meta } from '@storybook/react-vite';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
import {
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
resetFrontComponentStoryMocks,
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
import {
createHtmlTagClickStory,
createHtmlTagFocusStory,
} from '@/__stories__/shared/test-utils/createHtmlElementStory';
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/HtmlTag/Form/Legend/Events',
component: FrontComponentRenderer,
parameters: { layout: 'centered' },
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
beforeEach: resetFrontComponentStoryMocks,
};
export default meta;
export const Click = createHtmlTagClickStory({
frontComponentBundleName: 'legend-click',
});
export const FocusBlur = createHtmlTagFocusStory({
frontComponentBundleName: 'legend-focus-blur',
});
@@ -0,0 +1,41 @@
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
EventLog,
useEventLog,
} from '@/__stories__/shared/front-components/event-log';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import { FILL_INLINE_STYLE } from '@/__stories__/shared/front-components/styles';
const LegendFocusBlurFrontComponent = () => {
const [interactionCount, setInteractionCount] = useState(0);
const { entries, pushEvent } = useEventLog();
return (
<FrontComponentCard title="legend:focus-blur">
<fieldset>
<legend
data-testid="subject"
onFocus={(event) => {
setInteractionCount((previous) => previous + 1);
pushEvent(event);
}}
onBlur={(event) => pushEvent(event)}
tabIndex={0}
style={FILL_INLINE_STYLE}
>
legend
</legend>
</fieldset>
<span data-testid="front-component-value">{interactionCount}</span>
<EventLog entries={entries} />
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier: 'fc-legend-fb-fb-00000000-0000-0000-0000-000000000020',
name: 'legend-focus-blur-front-component',
description: 'Front component covering focus-blur on <legend>',
component: LegendFocusBlurFrontComponent,
});
@@ -0,0 +1,39 @@
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
EventLog,
useEventLog,
} from '@/__stories__/shared/front-components/event-log';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import { FILL_INLINE_STYLE } from '@/__stories__/shared/front-components/styles';
const MeterClickFrontComponent = () => {
const [interactionCount, setInteractionCount] = useState(0);
const { entries, pushEvent } = useEventLog();
return (
<FrontComponentCard title="meter:click">
<meter
data-testid="subject"
value={0.5}
min={0}
max={1}
onClick={(event) => {
setInteractionCount((previous) => previous + 1);
pushEvent(event);
}}
tabIndex={0}
style={FILL_INLINE_STYLE}
/>
<span data-testid="front-component-value">{interactionCount}</span>
<EventLog entries={entries} />
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier: 'fc-meter-c-click-00000000-0000-0000-0000-000000000020',
name: 'meter-click-front-component',
description: 'Front component covering click on <meter>',
component: MeterClickFrontComponent,
});
@@ -0,0 +1,29 @@
import { type Meta } from '@storybook/react-vite';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
import {
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
resetFrontComponentStoryMocks,
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
import {
createHtmlTagClickStory,
createHtmlTagFocusStory,
} from '@/__stories__/shared/test-utils/createHtmlElementStory';
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/HtmlTag/Form/Meter/Events',
component: FrontComponentRenderer,
parameters: { layout: 'centered' },
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
beforeEach: resetFrontComponentStoryMocks,
};
export default meta;
export const Click = createHtmlTagClickStory({
frontComponentBundleName: 'meter-click',
});
export const FocusBlur = createHtmlTagFocusStory({
frontComponentBundleName: 'meter-focus-blur',
});
@@ -0,0 +1,40 @@
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
EventLog,
useEventLog,
} from '@/__stories__/shared/front-components/event-log';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import { FILL_INLINE_STYLE } from '@/__stories__/shared/front-components/styles';
const MeterFocusBlurFrontComponent = () => {
const [interactionCount, setInteractionCount] = useState(0);
const { entries, pushEvent } = useEventLog();
return (
<FrontComponentCard title="meter:focus-blur">
<meter
data-testid="subject"
value={0.5}
min={0}
max={1}
onFocus={(event) => {
setInteractionCount((previous) => previous + 1);
pushEvent(event);
}}
onBlur={(event) => pushEvent(event)}
tabIndex={0}
style={FILL_INLINE_STYLE}
/>
<span data-testid="front-component-value">{interactionCount}</span>
<EventLog entries={entries} />
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier: 'fc-meter-fb-fb-00000000-0000-0000-0000-000000000020',
name: 'meter-focus-blur-front-component',
description: 'Front component covering focus-blur on <meter>',
component: MeterFocusBlurFrontComponent,
});
@@ -0,0 +1,28 @@
import { defineFrontComponent } from 'twenty-sdk/define';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import { PROPERTY_FIXTURE } from '@/__stories__/shared/front-components/property-fixture';
const MeterPropertiesFrontComponent = () => (
<FrontComponentCard title="meter:properties">
<meter
data-testid="subject"
id={PROPERTY_FIXTURE.id}
className={PROPERTY_FIXTURE.className}
title={PROPERTY_FIXTURE.title}
role={PROPERTY_FIXTURE.role}
aria-label={PROPERTY_FIXTURE.ariaLabel}
tabIndex={PROPERTY_FIXTURE.tabIndex}
value={0.5}
min={0}
max={1}
/>
</FrontComponentCard>
);
export default defineFrontComponent({
universalIdentifier: 'fc-meter-props-00000000-0000-0000-0000-000000000020',
name: 'meter-properties-front-component',
description: 'Front component covering property reflection on <meter>',
component: MeterPropertiesFrontComponent,
});
@@ -0,0 +1,27 @@
import { type Meta } from '@storybook/react-vite';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
import {
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
resetFrontComponentStoryMocks,
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
import { createPropertyReflectionStory } from '@/__stories__/shared/test-utils/createPropertyReflectionStory';
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/HtmlTag/Form/Meter/Properties',
component: FrontComponentRenderer,
parameters: { layout: 'centered' },
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
beforeEach: resetFrontComponentStoryMocks,
};
export default meta;
export const Properties = createPropertyReflectionStory({
frontComponentBundleName: 'meter-properties',
extraAttributes: {
value: '0.5',
min: '0',
max: '1',
},
});
@@ -0,0 +1,39 @@
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
EventLog,
useEventLog,
} from '@/__stories__/shared/front-components/event-log';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
const OptgroupClickFrontComponent = () => {
const [interactionCount, setInteractionCount] = useState(0);
const { entries, pushEvent } = useEventLog();
return (
<FrontComponentCard title="optgroup:click">
<select defaultValue="x">
<optgroup
data-testid="subject"
label="group"
onClick={(event) => {
setInteractionCount((previous) => previous + 1);
pushEvent(event);
}}
>
<option value="x">inside</option>
</optgroup>
</select>
<span data-testid="front-component-value">{interactionCount}</span>
<EventLog entries={entries} />
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier:
'fc-optgroup-c-click-00000000-0000-0000-0000-000000000020',
name: 'optgroup-click-front-component',
description: 'Front component covering click on <optgroup>',
component: OptgroupClickFrontComponent,
});
@@ -0,0 +1,22 @@
import { type Meta } from '@storybook/react-vite';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
import {
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
resetFrontComponentStoryMocks,
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
import { createHtmlTagClickStory } from '@/__stories__/shared/test-utils/createHtmlElementStory';
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/HtmlTag/Form/Optgroup/Events',
component: FrontComponentRenderer,
parameters: { layout: 'centered' },
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
beforeEach: resetFrontComponentStoryMocks,
};
export default meta;
export const Click = createHtmlTagClickStory({
frontComponentBundleName: 'optgroup-click',
});
@@ -0,0 +1,41 @@
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
EventLog,
useEventLog,
} from '@/__stories__/shared/front-components/event-log';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
const OptionClickFrontComponent = () => {
const [interactionCount, setInteractionCount] = useState(0);
const { entries, pushEvent } = useEventLog();
return (
<FrontComponentCard title="option:click">
<select defaultValue="alpha">
<>
<option
data-testid="subject"
value="alpha"
onClick={(event) => {
setInteractionCount((previous) => previous + 1);
pushEvent(event);
}}
>
alpha
</option>
<option value="beta">beta</option>
</>
</select>
<span data-testid="front-component-value">{interactionCount}</span>
<EventLog entries={entries} />
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier: 'fc-option-c-click-00000000-0000-0000-0000-000000000020',
name: 'option-click-front-component',
description: 'Front component covering click on <option>',
component: OptionClickFrontComponent,
});
@@ -0,0 +1,22 @@
import { type Meta } from '@storybook/react-vite';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
import {
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
resetFrontComponentStoryMocks,
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
import { createHtmlTagClickStory } from '@/__stories__/shared/test-utils/createHtmlElementStory';
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/HtmlTag/Form/Option/Events',
component: FrontComponentRenderer,
parameters: { layout: 'centered' },
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
beforeEach: resetFrontComponentStoryMocks,
};
export default meta;
export const Click = createHtmlTagClickStory({
frontComponentBundleName: 'option-click',
});
@@ -0,0 +1,38 @@
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
EventLog,
useEventLog,
} from '@/__stories__/shared/front-components/event-log';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import { FILL_INLINE_STYLE } from '@/__stories__/shared/front-components/styles';
const OutputClickFrontComponent = () => {
const [interactionCount, setInteractionCount] = useState(0);
const { entries, pushEvent } = useEventLog();
return (
<FrontComponentCard title="output:click">
<output
data-testid="subject"
onClick={(event) => {
setInteractionCount((previous) => previous + 1);
pushEvent(event);
}}
tabIndex={0}
style={FILL_INLINE_STYLE}
>
output
</output>
<span data-testid="front-component-value">{interactionCount}</span>
<EventLog entries={entries} />
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier: 'fc-output-c-click-00000000-0000-0000-0000-000000000020',
name: 'output-click-front-component',
description: 'Front component covering click on <output>',
component: OutputClickFrontComponent,
});
@@ -0,0 +1,29 @@
import { type Meta } from '@storybook/react-vite';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
import {
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
resetFrontComponentStoryMocks,
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
import {
createHtmlTagClickStory,
createHtmlTagFocusStory,
} from '@/__stories__/shared/test-utils/createHtmlElementStory';
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/HtmlTag/Form/Output/Events',
component: FrontComponentRenderer,
parameters: { layout: 'centered' },
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
beforeEach: resetFrontComponentStoryMocks,
};
export default meta;
export const Click = createHtmlTagClickStory({
frontComponentBundleName: 'output-click',
});
export const FocusBlur = createHtmlTagFocusStory({
frontComponentBundleName: 'output-focus-blur',
});
@@ -0,0 +1,39 @@
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
EventLog,
useEventLog,
} from '@/__stories__/shared/front-components/event-log';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import { FILL_INLINE_STYLE } from '@/__stories__/shared/front-components/styles';
const OutputFocusBlurFrontComponent = () => {
const [interactionCount, setInteractionCount] = useState(0);
const { entries, pushEvent } = useEventLog();
return (
<FrontComponentCard title="output:focus-blur">
<output
data-testid="subject"
onFocus={(event) => {
setInteractionCount((previous) => previous + 1);
pushEvent(event);
}}
onBlur={(event) => pushEvent(event)}
tabIndex={0}
style={FILL_INLINE_STYLE}
>
output
</output>
<span data-testid="front-component-value">{interactionCount}</span>
<EventLog entries={entries} />
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier: 'fc-output-fb-fb-00000000-0000-0000-0000-000000000020',
name: 'output-focus-blur-front-component',
description: 'Front component covering focus-blur on <output>',
component: OutputFocusBlurFrontComponent,
});
@@ -0,0 +1,39 @@
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
EventLog,
useEventLog,
} from '@/__stories__/shared/front-components/event-log';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import { FILL_INLINE_STYLE } from '@/__stories__/shared/front-components/styles';
const ProgressClickFrontComponent = () => {
const [interactionCount, setInteractionCount] = useState(0);
const { entries, pushEvent } = useEventLog();
return (
<FrontComponentCard title="progress:click">
<progress
data-testid="subject"
value={30}
max={100}
onClick={(event) => {
setInteractionCount((previous) => previous + 1);
pushEvent(event);
}}
tabIndex={0}
style={FILL_INLINE_STYLE}
/>
<span data-testid="front-component-value">{interactionCount}</span>
<EventLog entries={entries} />
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier:
'fc-progress-c-click-00000000-0000-0000-0000-000000000020',
name: 'progress-click-front-component',
description: 'Front component covering click on <progress>',
component: ProgressClickFrontComponent,
});
@@ -0,0 +1,29 @@
import { type Meta } from '@storybook/react-vite';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
import {
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
resetFrontComponentStoryMocks,
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
import {
createHtmlTagClickStory,
createHtmlTagFocusStory,
} from '@/__stories__/shared/test-utils/createHtmlElementStory';
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/HtmlTag/Form/Progress/Events',
component: FrontComponentRenderer,
parameters: { layout: 'centered' },
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
beforeEach: resetFrontComponentStoryMocks,
};
export default meta;
export const Click = createHtmlTagClickStory({
frontComponentBundleName: 'progress-click',
});
export const FocusBlur = createHtmlTagFocusStory({
frontComponentBundleName: 'progress-focus-blur',
});
@@ -0,0 +1,39 @@
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
EventLog,
useEventLog,
} from '@/__stories__/shared/front-components/event-log';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import { FILL_INLINE_STYLE } from '@/__stories__/shared/front-components/styles';
const ProgressFocusBlurFrontComponent = () => {
const [interactionCount, setInteractionCount] = useState(0);
const { entries, pushEvent } = useEventLog();
return (
<FrontComponentCard title="progress:focus-blur">
<progress
data-testid="subject"
value={30}
max={100}
onFocus={(event) => {
setInteractionCount((previous) => previous + 1);
pushEvent(event);
}}
onBlur={(event) => pushEvent(event)}
tabIndex={0}
style={FILL_INLINE_STYLE}
/>
<span data-testid="front-component-value">{interactionCount}</span>
<EventLog entries={entries} />
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier: 'fc-progress-fb-fb-00000000-0000-0000-0000-000000000020',
name: 'progress-focus-blur-front-component',
description: 'Front component covering focus-blur on <progress>',
component: ProgressFocusBlurFrontComponent,
});
@@ -0,0 +1,27 @@
import { defineFrontComponent } from 'twenty-sdk/define';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import { PROPERTY_FIXTURE } from '@/__stories__/shared/front-components/property-fixture';
const ProgressPropertiesFrontComponent = () => (
<FrontComponentCard title="progress:properties">
<progress
data-testid="subject"
id={PROPERTY_FIXTURE.id}
className={PROPERTY_FIXTURE.className}
title={PROPERTY_FIXTURE.title}
role={PROPERTY_FIXTURE.role}
aria-label={PROPERTY_FIXTURE.ariaLabel}
tabIndex={PROPERTY_FIXTURE.tabIndex}
value={30}
max={PROPERTY_FIXTURE.max}
/>
</FrontComponentCard>
);
export default defineFrontComponent({
universalIdentifier: 'fc-progress-props-00000000-0000-0000-0000-000000000020',
name: 'progress-properties-front-component',
description: 'Front component covering property reflection on <progress>',
component: ProgressPropertiesFrontComponent,
});
@@ -0,0 +1,26 @@
import { type Meta } from '@storybook/react-vite';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
import {
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
resetFrontComponentStoryMocks,
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
import { createPropertyReflectionStory } from '@/__stories__/shared/test-utils/createPropertyReflectionStory';
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/HtmlTag/Form/Progress/Properties',
component: FrontComponentRenderer,
parameters: { layout: 'centered' },
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
beforeEach: resetFrontComponentStoryMocks,
};
export default meta;
export const Properties = createPropertyReflectionStory({
frontComponentBundleName: 'progress-properties',
extraAttributes: {
value: '30',
max: '100',
},
});
@@ -0,0 +1,51 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { userEvent, within } from 'storybook/test';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
import {
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
resetFrontComponentStoryMocks,
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
import { expectEventLogged } from '@/__stories__/shared/test-utils/matchers/expectEventLogged';
import { expectFrontComponentMounted } from '@/__stories__/shared/test-utils/matchers/expectFrontComponentMounted';
import { expectFrontComponentValue } from '@/__stories__/shared/test-utils/matchers/expectFrontComponentValue';
import { runFrontComponentStory } from '@/__stories__/shared/test-utils/runFrontComponentStory';
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/HtmlTag/Form/Select/Events',
component: FrontComponentRenderer,
parameters: { layout: 'centered' },
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
beforeEach: resetFrontComponentStoryMocks,
};
export default meta;
type Story = StoryObj<typeof FrontComponentRenderer>;
export const ValueRoundTrip: Story = runFrontComponentStory({
frontComponentBundleName: 'select-value',
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expectFrontComponentMounted(canvas);
const subject = (await canvas.findByTestId('subject')) as HTMLSelectElement;
await userEvent.selectOptions(subject, 'beta');
await expectFrontComponentValue({ canvas, expected: 'beta' });
await expectEventLogged({
canvas,
matcher: { type: 'change', value: 'beta' },
});
await userEvent.selectOptions(subject, 'gamma');
await expectFrontComponentValue({ canvas, expected: 'gamma' });
await expectEventLogged({
canvas,
matcher: { type: 'change', value: 'gamma' },
});
},
});
@@ -0,0 +1,33 @@
import { defineFrontComponent } from 'twenty-sdk/define';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import { PROPERTY_FIXTURE } from '@/__stories__/shared/front-components/property-fixture';
const noop = () => undefined;
const SelectPropertiesFrontComponent = () => (
<FrontComponentCard title="select:properties">
<select
data-testid="subject"
id={PROPERTY_FIXTURE.id}
className={PROPERTY_FIXTURE.className}
title={PROPERTY_FIXTURE.title}
role={PROPERTY_FIXTURE.role}
aria-label={PROPERTY_FIXTURE.ariaLabel}
tabIndex={PROPERTY_FIXTURE.tabIndex}
name={PROPERTY_FIXTURE.name}
value="alpha"
onChange={noop}
>
<option value="alpha">alpha</option>
<option value="beta">beta</option>
</select>
</FrontComponentCard>
);
export default defineFrontComponent({
universalIdentifier: 'fc-slct-props-00000000-0000-0000-0000-000000000020',
name: 'select-properties-front-component',
description: 'Front component covering property reflection on <select>',
component: SelectPropertiesFrontComponent,
});
@@ -0,0 +1,25 @@
import { type Meta } from '@storybook/react-vite';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
import { PROPERTY_FIXTURE } from '@/__stories__/shared/front-components/property-fixture';
import {
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
resetFrontComponentStoryMocks,
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
import { createPropertyReflectionStory } from '@/__stories__/shared/test-utils/createPropertyReflectionStory';
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/HtmlTag/Form/Select/Properties',
component: FrontComponentRenderer,
parameters: { layout: 'centered' },
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
beforeEach: resetFrontComponentStoryMocks,
};
export default meta;
export const Properties = createPropertyReflectionStory({
frontComponentBundleName: 'select-properties',
extraAttributes: { name: PROPERTY_FIXTURE.name },
extraProperties: { value: 'alpha' },
});
@@ -0,0 +1,48 @@
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
EventLog,
useEventLog,
} from '@/__stories__/shared/front-components/event-log';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import {
INPUT_STYLE,
LABEL_STYLE,
SUBJECT_WRAPPER_STYLE,
} from '@/__stories__/shared/front-components/styles';
const SelectValueFrontComponent = () => {
const [value, setValue] = useState('alpha');
const { entries, pushEvent } = useEventLog();
return (
<FrontComponentCard title="select:value">
<div style={SUBJECT_WRAPPER_STYLE}>
<label style={LABEL_STYLE}>Select</label>
<select
data-testid="subject"
value={value}
onChange={(event) => {
setValue(event.target.value);
pushEvent(event);
}}
style={INPUT_STYLE}
>
<option value="alpha">Alpha</option>
<option value="beta">Beta</option>
<option value="gamma">Gamma</option>
</select>
<span data-testid="front-component-value">{value}</span>
</div>
<EventLog entries={entries} />
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier: 'fc-slct-value-00000000-0000-0000-0000-000000000020',
name: 'select-value-front-component',
description: 'Front component covering value round-trip on <select>',
component: SelectValueFrontComponent,
});
@@ -0,0 +1,38 @@
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import {
INPUT_STYLE,
LABEL_STYLE,
SUBJECT_WRAPPER_STYLE,
} from '@/__stories__/shared/front-components/styles';
const CARET_INITIAL_VALUE = 'Hello world';
const TextareaCaretFrontComponent = () => {
const [value, setValue] = useState(CARET_INITIAL_VALUE);
return (
<FrontComponentCard title="textarea:caret">
<div style={SUBJECT_WRAPPER_STYLE}>
<label style={LABEL_STYLE}>Textarea (pre-filled)</label>
<textarea
data-testid="subject"
value={value}
onChange={(event) => setValue(event.target.value)}
style={INPUT_STYLE}
rows={3}
/>
<span data-testid="front-component-value">{value}</span>
</div>
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier: 'fc-txta-caret-00000000-0000-0000-0000-000000000020',
name: 'textarea-caret-front-component',
description: 'Front component covering caret behavior on <textarea>',
component: TextareaCaretFrontComponent,
});
@@ -0,0 +1,63 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { expect, userEvent, waitFor, within } from 'storybook/test';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
import {
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
resetFrontComponentStoryMocks,
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
import { expectFrontComponentMounted } from '@/__stories__/shared/test-utils/matchers/expectFrontComponentMounted';
import { runFrontComponentStory } from '@/__stories__/shared/test-utils/runFrontComponentStory';
import {
INTERACTION_TIMEOUT,
TYPING_TIMEOUT,
} from '@/__stories__/shared/test-utils/timeouts';
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/HtmlTag/Form/Textarea/Caret',
component: FrontComponentRenderer,
parameters: { layout: 'centered' },
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
beforeEach: resetFrontComponentStoryMocks,
};
export default meta;
type Story = StoryObj<typeof FrontComponentRenderer>;
export const CaretPreservedMidString: Story = runFrontComponentStory({
frontComponentBundleName: 'textarea-caret',
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expectFrontComponentMounted(canvas);
const subject = (await canvas.findByTestId(
'subject',
)) as HTMLTextAreaElement;
await waitFor(
() => {
expect(subject.value).toBe('Hello world');
},
{ timeout: INTERACTION_TIMEOUT },
);
subject.focus();
subject.setSelectionRange(4, 4);
await userEvent.keyboard('X');
await waitFor(
() => {
expect(subject.value).toBe('HellXo world');
expect(canvas.getByTestId('front-component-value').textContent).toBe(
'HellXo world',
);
expect(subject.selectionStart).toBe(5);
expect(subject.selectionEnd).toBe(5);
},
{ timeout: TYPING_TIMEOUT },
);
},
});
@@ -0,0 +1,44 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { userEvent, within } from 'storybook/test';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
import {
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
resetFrontComponentStoryMocks,
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
import { expectEventLogged } from '@/__stories__/shared/test-utils/matchers/expectEventLogged';
import { expectFrontComponentMounted } from '@/__stories__/shared/test-utils/matchers/expectFrontComponentMounted';
import { expectFrontComponentValue } from '@/__stories__/shared/test-utils/matchers/expectFrontComponentValue';
import { runFrontComponentStory } from '@/__stories__/shared/test-utils/runFrontComponentStory';
import { TYPING_DELAY } from '@/__stories__/shared/test-utils/timeouts';
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/HtmlTag/Form/Textarea/Events',
component: FrontComponentRenderer,
parameters: { layout: 'centered' },
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
beforeEach: resetFrontComponentStoryMocks,
};
export default meta;
type Story = StoryObj<typeof FrontComponentRenderer>;
export const ValueRoundTrip: Story = runFrontComponentStory({
frontComponentBundleName: 'textarea-value',
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expectFrontComponentMounted(canvas);
const subject = await canvas.findByTestId('subject');
await userEvent.type(subject, 'hello note', { delay: TYPING_DELAY });
await expectFrontComponentValue({ canvas, expected: 'hello note' });
await expectEventLogged({
canvas,
matcher: { type: 'change', value: 'hello note' },
});
},
});
@@ -0,0 +1,33 @@
import { defineFrontComponent } from 'twenty-sdk/define';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import { PROPERTY_FIXTURE } from '@/__stories__/shared/front-components/property-fixture';
const noop = () => undefined;
const TextareaPropertiesFrontComponent = () => (
<FrontComponentCard title="textarea:properties">
<textarea
data-testid="subject"
id={PROPERTY_FIXTURE.id}
className={PROPERTY_FIXTURE.className}
title={PROPERTY_FIXTURE.title}
role={PROPERTY_FIXTURE.role}
aria-label={PROPERTY_FIXTURE.ariaLabel}
tabIndex={PROPERTY_FIXTURE.tabIndex}
name={PROPERTY_FIXTURE.name}
placeholder={PROPERTY_FIXTURE.placeholder}
rows={PROPERTY_FIXTURE.rows}
cols={PROPERTY_FIXTURE.cols}
value={PROPERTY_FIXTURE.textValue}
onChange={noop}
/>
</FrontComponentCard>
);
export default defineFrontComponent({
universalIdentifier: 'fc-txta-props-00000000-0000-0000-0000-000000000020',
name: 'textarea-properties-front-component',
description: 'Front component covering property reflection on <textarea>',
component: TextareaPropertiesFrontComponent,
});
@@ -0,0 +1,30 @@
import { type Meta } from '@storybook/react-vite';
import { FrontComponentRenderer } from '@/host/components/FrontComponentRenderer';
import { PROPERTY_FIXTURE } from '@/__stories__/shared/front-components/property-fixture';
import {
FRONT_COMPONENT_STORY_DEFAULT_ARGS,
resetFrontComponentStoryMocks,
} from '@/__stories__/shared/test-utils/createFrontComponentStoryMeta';
import { createPropertyReflectionStory } from '@/__stories__/shared/test-utils/createPropertyReflectionStory';
const meta: Meta<typeof FrontComponentRenderer> = {
title: 'FrontComponent/HtmlTag/Form/Textarea/Properties',
component: FrontComponentRenderer,
parameters: { layout: 'centered' },
args: FRONT_COMPONENT_STORY_DEFAULT_ARGS,
beforeEach: resetFrontComponentStoryMocks,
};
export default meta;
export const Properties = createPropertyReflectionStory({
frontComponentBundleName: 'textarea-properties',
extraAttributes: {
name: PROPERTY_FIXTURE.name,
placeholder: PROPERTY_FIXTURE.placeholder,
rows: String(PROPERTY_FIXTURE.rows),
cols: String(PROPERTY_FIXTURE.cols),
},
extraProperties: { value: PROPERTY_FIXTURE.textValue },
});
@@ -0,0 +1,46 @@
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
EventLog,
useEventLog,
} from '@/__stories__/shared/front-components/event-log';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
import {
INPUT_STYLE,
LABEL_STYLE,
SUBJECT_WRAPPER_STYLE,
} from '@/__stories__/shared/front-components/styles';
const TextareaValueFrontComponent = () => {
const [value, setValue] = useState('');
const { entries, pushEvent } = useEventLog();
return (
<FrontComponentCard title="textarea:value">
<div style={SUBJECT_WRAPPER_STYLE}>
<label style={LABEL_STYLE}>Textarea</label>
<textarea
data-testid="subject"
placeholder="Type a note..."
value={value}
onChange={(event) => {
setValue(event.target.value);
pushEvent(event);
}}
style={INPUT_STYLE}
rows={3}
/>
<span data-testid="front-component-value">{value}</span>
</div>
<EventLog entries={entries} />
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier: 'fc-txta-value-00000000-0000-0000-0000-000000000020',
name: 'textarea-value-front-component',
description: 'Front component covering value round-trip on <textarea>',
component: TextareaValueFrontComponent,
});
@@ -0,0 +1,50 @@
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
EventLog,
useEventLog,
} from '@/__stories__/shared/front-components/event-log';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
const SURFACE_STYLE = {
width: 200,
height: 80,
border: '1px solid #d1d5db',
borderRadius: 8,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
cursor: 'pointer',
userSelect: 'none' as const,
backgroundColor: '#f3f4f6',
};
const DivClickFrontComponent = () => {
const [clickCount, setClickCount] = useState(0);
const { entries, pushEvent } = useEventLog();
return (
<FrontComponentCard title="div:click">
<div
data-testid="subject"
onClick={(event) => {
setClickCount((previous) => previous + 1);
pushEvent(event);
}}
style={SURFACE_STYLE}
>
Click me
</div>
<span data-testid="front-component-value">{clickCount}</span>
<EventLog entries={entries} />
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier: 'fc-div-click-00000000-0000-0000-0000-000000000020',
name: 'div-click-front-component',
description: 'Front component covering click event on <div>',
component: DivClickFrontComponent,
});
@@ -0,0 +1,50 @@
import { useState } from 'react';
import { defineFrontComponent } from 'twenty-sdk/define';
import {
EventLog,
useEventLog,
} from '@/__stories__/shared/front-components/event-log';
import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card';
const SURFACE_STYLE = {
width: 200,
height: 80,
border: '1px solid #d1d5db',
borderRadius: 8,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
cursor: 'pointer',
userSelect: 'none' as const,
backgroundColor: '#f3f4f6',
};
const DivDoubleClickFrontComponent = () => {
const [doubleClickCount, setDoubleClickCount] = useState(0);
const { entries, pushEvent } = useEventLog();
return (
<FrontComponentCard title="div:dblclick">
<div
data-testid="subject"
onDoubleClick={(event) => {
setDoubleClickCount((previous) => previous + 1);
pushEvent(event);
}}
style={SURFACE_STYLE}
>
Double click me
</div>
<span data-testid="front-component-value">{doubleClickCount}</span>
<EventLog entries={entries} />
</FrontComponentCard>
);
};
export default defineFrontComponent({
universalIdentifier: 'fc-div-dblclick-00000000-0000-0000-0000-000000000020',
name: 'div-dblclick-front-component',
description: 'Front component covering double click event on <div>',
component: DivDoubleClickFrontComponent,
});

Some files were not shown because too many files have changed in this diff Show More