diff --git a/packages/twenty-front-component-renderer/scripts/remote-dom/generators/remote-elements.generator.ts b/packages/twenty-front-component-renderer/scripts/remote-dom/generators/remote-elements.generator.ts index 18d5d9b632b19..1d4c58c024989 100644 --- a/packages/twenty-front-component-renderer/scripts/remote-dom/generators/remote-elements.generator.ts +++ b/packages/twenty-front-component-renderer/scripts/remote-dom/generators/remote-elements.generator.ts @@ -129,11 +129,22 @@ const generateCommonEventsType = ( }); writer.writeLine(');'); writer.blankLine(); - writer.writeLine('return new CustomEvent(eventType, {'); + writer.writeLine('const event = new CustomEvent(eventType, {'); writer.indent(() => { writer.writeLine('detail: eventData,'); }); writer.writeLine('}) as RemoteEvent;'); + writer.blankLine(); + writer.writeLine('applySerializedEventProperties('); + writer.indent(() => { + writer.writeLine( + 'event as unknown as Record,', + ); + writer.writeLine('eventData,'); + }); + writer.writeLine(');'); + writer.blankLine(); + writer.writeLine('return event;'); }); writer.writeLine('},'); }); @@ -400,11 +411,18 @@ export const generateRemoteElements = ( }); sourceFile.addImportDeclaration({ - moduleSpecifier: '@/constants/SerializedEventData', - namedImports: [ - 'applySerializedEventTargetProperties', - { name: 'SerializedEventData', isTypeOnly: true }, - ], + moduleSpecifier: '@/constants/applySerializedEventProperties', + namedImports: ['applySerializedEventProperties'], + }); + + sourceFile.addImportDeclaration({ + moduleSpecifier: '@/constants/applySerializedEventTargetProperties', + namedImports: ['applySerializedEventTargetProperties'], + }); + + sourceFile.addImportDeclaration({ + moduleSpecifier: '@/types/SerializedEventData', + namedImports: [{ name: 'SerializedEventData', isTypeOnly: true }], }); const commonPropertyNames = new Set(Object.keys(commonProperties)); diff --git a/packages/twenty-front-component-renderer/src/__stories__/html-tag/svg/svg/svg-events.stories.tsx b/packages/twenty-front-component-renderer/src/__stories__/html-tag/svg/svg/svg-events.stories.tsx index 87dec6cf243bc..81a79be8d55a1 100644 --- a/packages/twenty-front-component-renderer/src/__stories__/html-tag/svg/svg/svg-events.stories.tsx +++ b/packages/twenty-front-component-renderer/src/__stories__/html-tag/svg/svg/svg-events.stories.tsx @@ -8,6 +8,7 @@ import { import { createHtmlTagClickStory, createHtmlTagFocusStory, + createHtmlTagPointerStory, } from '@/__stories__/shared/test-utils/createHtmlElementStory'; const meta: Meta = { @@ -27,3 +28,7 @@ export const Click = createHtmlTagClickStory({ export const FocusBlur = createHtmlTagFocusStory({ frontComponentBundleName: 'svg-focus-blur', }); + +export const Pointer = createHtmlTagPointerStory({ + frontComponentBundleName: 'svg-pointer', +}); diff --git a/packages/twenty-front-component-renderer/src/__stories__/html-tag/svg/svg/svg-pointer.front-component.tsx b/packages/twenty-front-component-renderer/src/__stories__/html-tag/svg/svg/svg-pointer.front-component.tsx new file mode 100644 index 0000000000000..79a91e0960fa4 --- /dev/null +++ b/packages/twenty-front-component-renderer/src/__stories__/html-tag/svg/svg/svg-pointer.front-component.tsx @@ -0,0 +1,45 @@ +import { + EventLog, + useEventLog, +} from '@/__stories__/shared/front-components/event-log'; +import { FrontComponentCard } from '@/__stories__/shared/front-components/front-component-card'; +import { SVG_ROOT_STYLE } from '@/__stories__/shared/front-components/styles'; +import { useState } from 'react'; +import { defineFrontComponent } from 'twenty-sdk/define'; + +const SvgPointerFrontComponent = () => { + const [interactionCount, setInteractionCount] = useState(0); + const [pointerCoordinates, setPointerCoordinates] = useState(''); + const { entries, pushEvent } = useEventLog(); + + return ( + + { + setInteractionCount((previous) => previous + 1); + setPointerCoordinates(`${event.clientX},${event.clientY}`); + pushEvent(event); + }} + onPointerMove={(event) => { + pushEvent(event); + }} + tabIndex={0} + style={SVG_ROOT_STYLE} + > + + + {interactionCount} + {pointerCoordinates} + + + ); +}; + +export default defineFrontComponent({ + universalIdentifier: 'fc-svg-c-pointer-0000000-0000-0000-0000-000000000021', + name: 'svg-pointer-front-component', + description: 'Front component covering pointer events on ', + component: SvgPointerFrontComponent, +}); diff --git a/packages/twenty-front-component-renderer/src/__stories__/shared/front-components/event-log.tsx b/packages/twenty-front-component-renderer/src/__stories__/shared/front-components/event-log.tsx index 4ef380b60b65a..5daef98aa948d 100644 --- a/packages/twenty-front-component-renderer/src/__stories__/shared/front-components/event-log.tsx +++ b/packages/twenty-front-component-renderer/src/__stories__/shared/front-components/event-log.tsx @@ -24,6 +24,12 @@ export type LoggedEventEntry = { scrollLeft?: number; deltaX?: number; deltaY?: number; + clientX?: number; + clientY?: number; + offsetX?: number; + offsetY?: number; + movementX?: number; + movementY?: number; }; const EVENT_LOG_STYLE = { @@ -216,6 +222,36 @@ export const useEventLog = () => { entry.deltaY = deltaY; } + const clientX = pickFromRecords(records, 'clientX', isNumberValue); + if (isDefined(clientX)) { + entry.clientX = clientX; + } + + const clientY = pickFromRecords(records, 'clientY', isNumberValue); + if (isDefined(clientY)) { + entry.clientY = clientY; + } + + const offsetX = pickFromRecords(records, 'offsetX', isNumberValue); + if (isDefined(offsetX)) { + entry.offsetX = offsetX; + } + + const offsetY = pickFromRecords(records, 'offsetY', isNumberValue); + if (isDefined(offsetY)) { + entry.offsetY = offsetY; + } + + const movementX = pickFromRecords(records, 'movementX', isNumberValue); + if (isDefined(movementX)) { + entry.movementX = movementX; + } + + const movementY = pickFromRecords(records, 'movementY', isNumberValue); + if (isDefined(movementY)) { + entry.movementY = movementY; + } + return [...previousEntries, entry]; }); }; diff --git a/packages/twenty-front-component-renderer/src/__stories__/shared/test-utils/createHtmlElementStory.ts b/packages/twenty-front-component-renderer/src/__stories__/shared/test-utils/createHtmlElementStory.ts index 84dddf23e4c88..c632ee0a3deb3 100644 --- a/packages/twenty-front-component-renderer/src/__stories__/shared/test-utils/createHtmlElementStory.ts +++ b/packages/twenty-front-component-renderer/src/__stories__/shared/test-utils/createHtmlElementStory.ts @@ -1,10 +1,10 @@ import { type StoryObj } from '@storybook/react-vite'; -import { userEvent, within } from 'storybook/test'; +import { expect, fireEvent, userEvent, waitFor, within } from 'storybook/test'; -import { type FrontComponentRenderer } from '@/host/components/FrontComponentRenderer'; 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'; +import { type FrontComponentRenderer } from '@/host/components/FrontComponentRenderer'; type Story = StoryObj; @@ -30,6 +30,33 @@ export const createHtmlTagClickStory = ({ }, }); +export const createHtmlTagPointerStory = ({ + frontComponentBundleName, +}: CreateHtmlTagStoryParams): Story => + runFrontComponentStory({ + frontComponentBundleName, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + await expectFrontComponentMounted(canvas); + + const subject = await canvas.findByTestId('subject'); + + fireEvent.pointerDown(subject, { clientX: 123, clientY: 45 }); + + await expectEventLogged({ + canvas, + matcher: { type: 'pointerdown', clientX: 123, clientY: 45 }, + }); + + await waitFor(() => { + const coordinates = canvas.getByTestId('pointer-coordinates'); + + expect(coordinates.textContent).toBe('123,45'); + }); + }, + }); + export const createHtmlTagFocusStory = ({ frontComponentBundleName, }: CreateHtmlTagStoryParams): Story => diff --git a/packages/twenty-front-component-renderer/src/__stories__/shared/test-utils/matchers/expectEventLogged.ts b/packages/twenty-front-component-renderer/src/__stories__/shared/test-utils/matchers/expectEventLogged.ts index b31d4ed46c830..1f5bc4d453e51 100644 --- a/packages/twenty-front-component-renderer/src/__stories__/shared/test-utils/matchers/expectEventLogged.ts +++ b/packages/twenty-front-component-renderer/src/__stories__/shared/test-utils/matchers/expectEventLogged.ts @@ -17,6 +17,12 @@ type LoggedEventMatcher = { ctrlKey?: boolean; metaKey?: boolean; altKey?: boolean; + clientX?: number; + clientY?: number; + offsetX?: number; + offsetY?: number; + movementX?: number; + movementY?: number; files?: { name?: string; type?: string }[]; }; diff --git a/packages/twenty-front-component-renderer/src/constants/SerializedEventData.ts b/packages/twenty-front-component-renderer/src/constants/SerializedEventData.ts deleted file mode 100644 index 57a7e033e0939..0000000000000 --- a/packages/twenty-front-component-renderer/src/constants/SerializedEventData.ts +++ /dev/null @@ -1,108 +0,0 @@ -export type SerializedFileData = { - name: string; - size: number; - type: string; - lastModified: number; -}; - -export type SerializedEventData = { - type: string; - altKey?: boolean; - ctrlKey?: boolean; - metaKey?: boolean; - shiftKey?: boolean; - clientX?: number; - clientY?: number; - pageX?: number; - pageY?: number; - screenX?: number; - screenY?: number; - offsetX?: number; - offsetY?: number; - movementX?: number; - movementY?: number; - button?: number; - buttons?: number; - pointerId?: number; - pointerType?: string; - pressure?: number; - tangentialPressure?: number; - tiltX?: number; - tiltY?: number; - twist?: number; - width?: number; - height?: number; - isPrimary?: boolean; - key?: string; - code?: string; - repeat?: boolean; - value?: string; - checked?: boolean; - scrollTop?: number; - scrollLeft?: number; - deltaX?: number; - deltaY?: number; - deltaZ?: number; - deltaMode?: number; - currentTime?: number; - duration?: number; - paused?: boolean; - ended?: boolean; - volume?: number; - muted?: boolean; - playbackRate?: number; - files?: SerializedFileData[]; -}; - -export const applySerializedEventTargetProperties = ( - element: Record, - eventData: SerializedEventData, -): void => { - if ('value' in eventData) { - element.value = eventData.value; - } - - if ('checked' in eventData) { - element.checked = eventData.checked; - } - - if ('files' in eventData) { - element.files = eventData.files; - } - - if ('scrollTop' in eventData) { - element.scrollTop = eventData.scrollTop; - } - - if ('scrollLeft' in eventData) { - element.scrollLeft = eventData.scrollLeft; - } - - if ('currentTime' in eventData) { - element.currentTime = eventData.currentTime; - } - - if ('duration' in eventData) { - element.duration = eventData.duration; - } - - if ('paused' in eventData) { - element.paused = eventData.paused; - } - - if ('ended' in eventData) { - element.ended = eventData.ended; - } - - if ('volume' in eventData) { - element.volume = eventData.volume; - } - - if ('muted' in eventData) { - element.muted = eventData.muted; - } - - if ('playbackRate' in eventData) { - element.playbackRate = eventData.playbackRate; - } -}; diff --git a/packages/twenty-front-component-renderer/src/constants/applySerializedEventProperties.ts b/packages/twenty-front-component-renderer/src/constants/applySerializedEventProperties.ts new file mode 100644 index 0000000000000..385d18bafae21 --- /dev/null +++ b/packages/twenty-front-component-renderer/src/constants/applySerializedEventProperties.ts @@ -0,0 +1,50 @@ +import { type SerializedEventData } from '@/types/SerializedEventData'; + +const SERIALIZED_EVENT_PROPERTY_KEYS = [ + 'altKey', + 'ctrlKey', + 'metaKey', + 'shiftKey', + 'clientX', + 'clientY', + 'x', + 'y', + 'pageX', + 'pageY', + 'screenX', + 'screenY', + 'offsetX', + 'offsetY', + 'movementX', + 'movementY', + 'button', + 'buttons', + 'pointerId', + 'pointerType', + 'pressure', + 'tangentialPressure', + 'tiltX', + 'tiltY', + 'twist', + 'width', + 'height', + 'isPrimary', + 'key', + 'code', + 'repeat', + 'deltaX', + 'deltaY', + 'deltaZ', + 'deltaMode', +] as const satisfies readonly (keyof SerializedEventData)[]; + +export const applySerializedEventProperties = ( + event: Record, + eventData: SerializedEventData, +): void => { + for (const key of SERIALIZED_EVENT_PROPERTY_KEYS) { + if (key in eventData) { + event[key] = eventData[key]; + } + } +}; diff --git a/packages/twenty-front-component-renderer/src/constants/applySerializedEventTargetProperties.ts b/packages/twenty-front-component-renderer/src/constants/applySerializedEventTargetProperties.ts new file mode 100644 index 0000000000000..1cc6ea51805b8 --- /dev/null +++ b/packages/twenty-front-component-renderer/src/constants/applySerializedEventTargetProperties.ts @@ -0,0 +1,54 @@ +import { type SerializedEventData } from '@/types/SerializedEventData'; + +export const applySerializedEventTargetProperties = ( + element: Record, + eventData: SerializedEventData, +): void => { + if ('value' in eventData) { + element.value = eventData.value; + } + + if ('checked' in eventData) { + element.checked = eventData.checked; + } + + if ('files' in eventData) { + element.files = eventData.files; + } + + if ('scrollTop' in eventData) { + element.scrollTop = eventData.scrollTop; + } + + if ('scrollLeft' in eventData) { + element.scrollLeft = eventData.scrollLeft; + } + + if ('currentTime' in eventData) { + element.currentTime = eventData.currentTime; + } + + if ('duration' in eventData) { + element.duration = eventData.duration; + } + + if ('paused' in eventData) { + element.paused = eventData.paused; + } + + if ('ended' in eventData) { + element.ended = eventData.ended; + } + + if ('volume' in eventData) { + element.volume = eventData.volume; + } + + if ('muted' in eventData) { + element.muted = eventData.muted; + } + + if ('playbackRate' in eventData) { + element.playbackRate = eventData.playbackRate; + } +}; diff --git a/packages/twenty-front-component-renderer/src/host/utils/createHtmlHostWrapper.ts b/packages/twenty-front-component-renderer/src/host/utils/createHtmlHostWrapper.ts index afeef639d28ca..c4c698987fac2 100644 --- a/packages/twenty-front-component-renderer/src/host/utils/createHtmlHostWrapper.ts +++ b/packages/twenty-front-component-renderer/src/host/utils/createHtmlHostWrapper.ts @@ -11,10 +11,8 @@ import React, { useContext } from 'react'; import { isDefined } from 'twenty-shared/utils'; import { EVENT_TO_REACT } from '@/constants/EventToReact'; -import { - type SerializedEventData, - type SerializedFileData, -} from '@/constants/SerializedEventData'; +import { type SerializedEventData } from '@/types/SerializedEventData'; +import { type SerializedFileData } from '@/types/SerializedFileData'; import { FrontComponentInputFocusContext, type SetEditableFocused, @@ -145,6 +143,12 @@ const serializeEvent = (event: unknown): SerializedEventData => { if (isNumber(domEvent.clientY)) { serialized.clientY = domEvent.clientY; } + if (isNumber(domEvent.x)) { + serialized.x = domEvent.x; + } + if (isNumber(domEvent.y)) { + serialized.y = domEvent.y; + } if (isNumber(domEvent.pageX)) { serialized.pageX = domEvent.pageX; } diff --git a/packages/twenty-front-component-renderer/src/remote/generated/remote-elements.ts b/packages/twenty-front-component-renderer/src/remote/generated/remote-elements.ts index f9842a3c2c05a..51f0f92fe4dca 100644 --- a/packages/twenty-front-component-renderer/src/remote/generated/remote-elements.ts +++ b/packages/twenty-front-component-renderer/src/remote/generated/remote-elements.ts @@ -6,10 +6,9 @@ import { type RemoteElementEventListenerDefinition, type RemoteElementEventListenersDefinition, } from '@remote-dom/core/elements'; -import { - applySerializedEventTargetProperties, - type SerializedEventData, -} from '@/constants/SerializedEventData'; +import { applySerializedEventProperties } from '@/constants/applySerializedEventProperties'; +import { applySerializedEventTargetProperties } from '@/constants/applySerializedEventTargetProperties'; +import { type SerializedEventData } from '@/types/SerializedEventData'; export type HtmlCommonProperties = { id?: string; @@ -94,9 +93,16 @@ const createSerializedEventConfig = ( eventData, ); - return new CustomEvent(eventType, { + const event = new CustomEvent(eventType, { detail: eventData, }) as RemoteEvent; + + applySerializedEventProperties( + event as unknown as Record, + eventData, + ); + + return event; }, }); const HTML_COMMON_EVENTS_CONFIG = Object.fromEntries( diff --git a/packages/twenty-front-component-renderer/src/types/SerializedEventData.ts b/packages/twenty-front-component-renderer/src/types/SerializedEventData.ts new file mode 100644 index 0000000000000..80863bfa625e4 --- /dev/null +++ b/packages/twenty-front-component-renderer/src/types/SerializedEventData.ts @@ -0,0 +1,52 @@ +import { type SerializedFileData } from '@/types/SerializedFileData'; + +export type SerializedEventData = { + type: string; + altKey?: boolean; + ctrlKey?: boolean; + metaKey?: boolean; + shiftKey?: boolean; + clientX?: number; + clientY?: number; + x?: number; + y?: number; + pageX?: number; + pageY?: number; + screenX?: number; + screenY?: number; + offsetX?: number; + offsetY?: number; + movementX?: number; + movementY?: number; + button?: number; + buttons?: number; + pointerId?: number; + pointerType?: string; + pressure?: number; + tangentialPressure?: number; + tiltX?: number; + tiltY?: number; + twist?: number; + width?: number; + height?: number; + isPrimary?: boolean; + key?: string; + code?: string; + repeat?: boolean; + value?: string; + checked?: boolean; + scrollTop?: number; + scrollLeft?: number; + deltaX?: number; + deltaY?: number; + deltaZ?: number; + deltaMode?: number; + currentTime?: number; + duration?: number; + paused?: boolean; + ended?: boolean; + volume?: number; + muted?: boolean; + playbackRate?: number; + files?: SerializedFileData[]; +}; diff --git a/packages/twenty-front-component-renderer/src/types/SerializedFileData.ts b/packages/twenty-front-component-renderer/src/types/SerializedFileData.ts new file mode 100644 index 0000000000000..e913f7abd6438 --- /dev/null +++ b/packages/twenty-front-component-renderer/src/types/SerializedFileData.ts @@ -0,0 +1,6 @@ +export type SerializedFileData = { + name: string; + size: number; + type: string; + lastModified: number; +};