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:
@@ -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"
|
||||
}
|
||||
|
||||
+52
-28
@@ -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;
|
||||
|
||||
+78
-11
@@ -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();
|
||||
},
|
||||
);
|
||||
+2
-3
@@ -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();
|
||||
|
||||
|
||||
-98
@@ -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,
|
||||
});
|
||||
-120
@@ -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,
|
||||
});
|
||||
-141
@@ -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,
|
||||
});
|
||||
-155
@@ -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,
|
||||
});
|
||||
-125
@@ -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,
|
||||
});
|
||||
+60
@@ -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();
|
||||
},
|
||||
});
|
||||
+52
@@ -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,
|
||||
});
|
||||
+50
@@ -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,
|
||||
});
|
||||
+50
@@ -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,
|
||||
});
|
||||
+53
@@ -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,
|
||||
});
|
||||
+53
@@ -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,
|
||||
});
|
||||
+50
@@ -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,
|
||||
});
|
||||
+60
@@ -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();
|
||||
},
|
||||
});
|
||||
+60
@@ -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();
|
||||
},
|
||||
});
|
||||
+63
@@ -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();
|
||||
},
|
||||
});
|
||||
+37
@@ -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,
|
||||
});
|
||||
+29
@@ -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',
|
||||
});
|
||||
+38
@@ -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,
|
||||
});
|
||||
+38
@@ -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,
|
||||
});
|
||||
+29
@@ -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',
|
||||
});
|
||||
+39
@@ -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,
|
||||
});
|
||||
+27
@@ -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,
|
||||
});
|
||||
+26
@@ -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',
|
||||
},
|
||||
});
|
||||
+42
@@ -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,
|
||||
});
|
||||
+29
@@ -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',
|
||||
});
|
||||
+42
@@ -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,
|
||||
});
|
||||
+39
@@ -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,
|
||||
});
|
||||
+43
@@ -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' } });
|
||||
},
|
||||
});
|
||||
+28
@@ -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,
|
||||
});
|
||||
+22
@@ -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',
|
||||
});
|
||||
+41
@@ -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,
|
||||
});
|
||||
+22
@@ -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',
|
||||
});
|
||||
+39
@@ -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,
|
||||
});
|
||||
+29
@@ -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',
|
||||
});
|
||||
+39
@@ -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,
|
||||
});
|
||||
+27
@@ -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,
|
||||
});
|
||||
+22
@@ -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',
|
||||
});
|
||||
+40
@@ -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' } });
|
||||
},
|
||||
});
|
||||
+27
@@ -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,
|
||||
});
|
||||
+22
@@ -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',
|
||||
});
|
||||
+57
@@ -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,
|
||||
});
|
||||
+38
@@ -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,
|
||||
});
|
||||
+61
@@ -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 },
|
||||
);
|
||||
},
|
||||
});
|
||||
+43
@@ -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,
|
||||
});
|
||||
+51
@@ -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 },
|
||||
});
|
||||
},
|
||||
});
|
||||
+63
@@ -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' } });
|
||||
},
|
||||
});
|
||||
+39
@@ -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,
|
||||
});
|
||||
+37
@@ -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,
|
||||
});
|
||||
+76
@@ -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' },
|
||||
],
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
+45
@@ -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,
|
||||
});
|
||||
+65
@@ -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 },
|
||||
});
|
||||
},
|
||||
});
|
||||
+33
@@ -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,
|
||||
});
|
||||
+38
@@ -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) },
|
||||
});
|
||||
+40
@@ -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,
|
||||
});
|
||||
+34
@@ -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,
|
||||
});
|
||||
+47
@@ -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,
|
||||
});
|
||||
+38
@@ -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,
|
||||
});
|
||||
+29
@@ -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',
|
||||
});
|
||||
+39
@@ -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,
|
||||
});
|
||||
+27
@@ -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,
|
||||
});
|
||||
+22
@@ -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',
|
||||
});
|
||||
+40
@@ -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,
|
||||
});
|
||||
+29
@@ -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',
|
||||
});
|
||||
+41
@@ -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,
|
||||
});
|
||||
+39
@@ -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,
|
||||
});
|
||||
+29
@@ -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',
|
||||
});
|
||||
+40
@@ -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,
|
||||
});
|
||||
+28
@@ -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,
|
||||
});
|
||||
+27
@@ -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',
|
||||
},
|
||||
});
|
||||
+39
@@ -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,
|
||||
});
|
||||
+22
@@ -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',
|
||||
});
|
||||
+41
@@ -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,
|
||||
});
|
||||
+22
@@ -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',
|
||||
});
|
||||
+38
@@ -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,
|
||||
});
|
||||
+29
@@ -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',
|
||||
});
|
||||
+39
@@ -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,
|
||||
});
|
||||
+39
@@ -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,
|
||||
});
|
||||
+29
@@ -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',
|
||||
});
|
||||
+39
@@ -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,
|
||||
});
|
||||
+27
@@ -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,
|
||||
});
|
||||
+26
@@ -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',
|
||||
},
|
||||
});
|
||||
+51
@@ -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' },
|
||||
});
|
||||
},
|
||||
});
|
||||
+33
@@ -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,
|
||||
});
|
||||
+25
@@ -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' },
|
||||
});
|
||||
+48
@@ -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,
|
||||
});
|
||||
+38
@@ -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,
|
||||
});
|
||||
+63
@@ -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 },
|
||||
);
|
||||
},
|
||||
});
|
||||
+44
@@ -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' },
|
||||
});
|
||||
},
|
||||
});
|
||||
+33
@@ -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,
|
||||
});
|
||||
+30
@@ -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 },
|
||||
});
|
||||
+46
@@ -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,
|
||||
});
|
||||
+50
@@ -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,
|
||||
});
|
||||
+50
@@ -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
Reference in New Issue
Block a user