From 5a75afc457bbefb1fa9630dfe623fd827c3616da Mon Sep 17 00:00:00 2001 From: samuelgja Date: Sat, 30 May 2026 18:49:22 +0700 Subject: [PATCH 1/2] chore(joint-react): Refactor Paper component styles and event handling across various examples - Updated Paper component style properties to use single quotes for consistency. - Replaced usePaperEvents with direct event handlers for onLinkMouseEnter, onLinkMouseLeave, onElementMouseEnter, and onElementMouseLeave in multiple examples. - Added eslint-disable comments for performance-related rules in several files. - Cleaned up unused imports and improved code readability in various components. --- .../paper/__tests__/paper-events.test.tsx | 376 ++++++++++++++++++ .../src/components/paper/paper.types.ts | 28 +- .../__tests__/paper-element-item.test.tsx | 3 +- .../__tests__/paper-html-overlay.test.tsx | 3 +- .../src/hooks/__tests__/use-cells.test.tsx | 6 +- .../__tests__/use-create-features.test.tsx | 3 +- .../use-create-portal-paper.test.tsx | 1 + .../src/hooks/__tests__/use-markup.test.tsx | 1 + .../hooks/__tests__/use-measure-node.test.tsx | 1 + .../use-nodes-measured-effect.test.tsx | 2 +- .../hooks/__tests__/use-paper-events.test.tsx | 1 + .../src/hooks/__tests__/use-paper.test.tsx | 1 + .../src/hooks/use-create-portal-paper.tsx | 13 +- .../presets/__tests__/extra-coverage.test.ts | 4 +- .../src/presets/__tests__/link-labels.test.ts | 35 +- .../joint-react/src/presets/paper-events.ts | 242 ++++++++--- .../src/state/__tests__/data-mapper.test.ts | 2 +- .../src/store/__tests__/paper-store.test.ts | 2 +- .../demos/automatic-layout-storage/code.tsx | 2 +- .../src/stories/demos/collaboration/code.tsx | 30 +- .../src/stories/demos/flowchart/code.tsx | 73 ++-- .../stories/demos/introduction-demo/code.tsx | 18 +- .../demos/investment-calculator/code.tsx | 2 +- .../src/stories/demos/link-arrows/code.tsx | 26 +- .../src/stories/demos/pulsing-port/code.tsx | 10 +- .../src/stories/demos/saasflow/code.tsx | 2 +- .../examples/markup-selectors-html/code.tsx | 2 +- .../examples/markup-selectors/code.tsx | 2 +- .../src/stories/examples/stress/code.tsx | 1 + .../examples/with-auto-layout/code.tsx | 1 + .../examples/with-auto-size-origin/code.tsx | 3 +- .../stories/examples/with-cell-json/code.tsx | 1 + .../with-collapsible-container/code.tsx | 24 +- .../examples/with-default-element/code.tsx | 1 + .../examples/with-default-link/code.tsx | 2 +- .../with-dynamic-interactivity/code.tsx | 67 +--- .../examples/with-element-controls/code.tsx | 3 +- .../examples/with-element-defaults/code.tsx | 1 + .../with-element-ports-groups/code.tsx | 1 + .../with-fixed-connection-points/code.tsx | 69 ++-- .../examples/with-graph-neighbors/code.tsx | 39 +- .../examples/with-highlighter/code.tsx | 13 +- .../examples/with-link-labels/code.tsx | 3 +- .../examples/with-link-markers-named/code.tsx | 3 +- .../examples/with-link-markers/code.tsx | 1 + .../examples/with-link-routing/code.tsx | 1 + .../stories/examples/with-link-tools/code.tsx | 10 +- .../stories/examples/with-list-node/code.tsx | 1 + .../stories/examples/with-minimap/code.tsx | 2 +- .../examples/with-native-ports/code.tsx | 1 + .../examples/with-portal-selector/code.tsx | 21 +- .../examples/with-shape-animations/code.tsx | 2 +- .../examples/with-tailwind-theme/code.tsx | 1 + .../examples/with-theme-editor/code.tsx | 86 ++-- .../code-controlled-mode-jotai.tsx | 1 + .../code-controlled-mode-peerjs.tsx | 1 + .../code-controlled-mode-redux.tsx | 1 + .../code-controlled-mode-zustand.tsx | 1 + .../step-by-step/code-html-renderer.tsx | 1 + .../utils/joint-jsx/jsx-to-markup.stories.tsx | 2 +- .../joint-react/src/utils/test-wrappers.tsx | 1 + 61 files changed, 875 insertions(+), 382 deletions(-) create mode 100644 packages/joint-react/src/components/paper/__tests__/paper-events.test.tsx diff --git a/packages/joint-react/src/components/paper/__tests__/paper-events.test.tsx b/packages/joint-react/src/components/paper/__tests__/paper-events.test.tsx new file mode 100644 index 0000000000..9311d46302 --- /dev/null +++ b/packages/joint-react/src/components/paper/__tests__/paper-events.test.tsx @@ -0,0 +1,376 @@ +/* eslint-disable react-perf/jsx-no-new-object-as-prop */ +/* eslint-disable react-perf/jsx-no-new-function-as-prop */ +import { useCallback, useState } from 'react'; +import { render, waitFor, act } from '@testing-library/react'; +import { mvc, type dia } from '@joint/core'; +import { Paper } from '../paper'; +import { GraphProvider } from '../../graph/graph-provider'; +import { ELEMENT_MODEL_TYPE } from '../../../models/element-model'; +import type { CellRecord } from '../../../types/cell.types'; + +const CELLS: readonly CellRecord[] = [ + { + id: '1', + type: ELEMENT_MODEL_TYPE, + position: { x: 0, y: 0 }, + size: { width: 50, height: 50 }, + } as CellRecord, +]; + +const renderRectElement = () => ; +const fakeEvent = {} as dia.Event; + +interface HarnessProps { + readonly setRef: (paper: dia.Paper | null) => void; +} + +/** + * Mounts a paper and resolves once the `dia.Paper` instance is available. + * @param ui - Factory receiving a ref callback; returns the tree under test. + * @returns The created paper. + */ +async function mountPaper( + ui: (ref: (paper: dia.Paper | null) => void) => React.ReactElement +): Promise<{ paper: dia.Paper }> { + let current: dia.Paper | null = null; + const setRef = (paper: dia.Paper | null) => { + if (paper) current = paper; + }; + render({ui(setRef)}); + await waitFor(() => { + expect(current).not.toBeNull(); + }); + return { paper: current as unknown as dia.Paper }; +} + +const clickTestId = (testId: string) => + act(() => { + (document.querySelector(`[data-testid="${testId}"]`) as HTMLButtonElement).click(); + }); + +describe('Paper normalized event props', () => { + it('fires a normalized handler with a context object', async () => { + const onBlankContextMenu = jest.fn(); + const { paper } = await mountPaper((ref) => ( + + )); + + act(() => { + paper.trigger('blank:contextmenu', fakeEvent, 10, 20); + }); + + expect(onBlankContextMenu).toHaveBeenCalledTimes(1); + expect(onBlankContextMenu).toHaveBeenCalledWith( + expect.objectContaining({ paper, graph: paper.model, event: fakeEvent, x: 10, y: 20 }) + ); + }); + + it('does NOT re-subscribe across unrelated re-renders when handlers are stable refs', async () => { + const listenToSpy = jest.spyOn(mvc.Listener.prototype, 'listenTo'); + const bindCount = (eventName: string) => + listenToSpy.mock.calls.filter((call) => String(call[1]) === eventName).length; + + const handler = jest.fn(); + function Harness({ setRef }: Readonly) { + const [, force] = useState(0); + // eslint-disable-next-line sonarjs/no-nested-functions + const rerender = () => force((n) => n + 1); + // Stable reference across renders — this is the documented contract. + const onBlankContextMenu = useCallback(handler, []); + return ( + <> +