Skip to content

fix(design-system): keep resizing when the cursor crosses an iframe - #531

Merged
aymericzip merged 2 commits into
aymericzip:mainfrom
yangguan-xue:fix/withresizer-iframe-drag
Oct 31, 2026
Merged

aymericzip merged 2 commits into
aymericzip:mainfrom
yangguan-xue:fix/withresizer-iframe-drag

Conversation

@yangguan-xue

Copy link
Copy Markdown
Contributor

Fixes #524

Problem

On the dashboard editor page, dragging the right panel's resize handle breaks as soon as the cursor crosses the visual editor <iframe>: mousemove / mouseup get dispatched to the iframe's document, so the parent window stops receiving them. The panel freezes mid-drag, and releasing the mouse over the iframe leaves it stuck in resize mode.

Fix

Adds a useIframePointerEventsShield hook — the "iframe shield" the issue mentions. When a drag starts it saves every iframe's inline pointer-events and sets it to none; on release, and on unmount mid-drag, the previous values are restored. The parent window keeps receiving events no matter what is under the cursor.

The hook is wired into both WithResizer and HeightResizer, since they share the same window-listener drag pattern. (In HeightResizer the restore lives in an unmount-only effect: its listener effect re-runs when the drag starts, so restoring there would undo the shield instantly.)

This also makes the extra ms-3 md:mx-2 spacing workaround on the editor <aside> unnecessary — left in place here, can be removed in a follow-up.

Tests

  • 4 new tests across both resizers: iframes get pointer-events: none on drag start and are restored on release / unmount, preserving pre-existing inline values.
  • bun run build (turbo, 17 packages), tsc --noEmit (typecheck_), biome lint and the design-system vitest suite (341 tests) all pass.
  • Manually verified against a minimal repro of the dashboard editor layout (iframe preview + resizable right panel): without the fix the drag freezes and sticks over the iframe; with it, resizing tracks smoothly and releases cleanly.

While dragging a resizer handle, mousemove/mouseup are dispatched to an
iframe's document once the cursor crosses it, so the parent window stops
receiving them: the resize freezes, and releasing over the iframe leaves
the panel stuck in resize mode.

Add a useIframePointerEventsShield hook that disables pointer events on
every iframe in the document for the duration of the drag and restores
the previous inline values on release (and on unmount mid-drag), and use
it in both WithResizer and HeightResizer, which share the same
window-listener drag pattern.

Fixes aymericzip#524
The suite loads esbuild through the config bundler, whose startup
invariant requires TextEncoder/Uint8Array from a single realm. Under
jsdom those globals come from jsdom's own realm, so esbuild fails to
load and the whole design-system test task exits 1 on PR CI runs.
The test only asserts request batching and needs no DOM.

@tornado-softwares tornado-softwares left a comment •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I confirm that this PR fixes the initial issue without changing the existing behavior of WithResizer and HeightResizer.

Notes :

  • Calling disableIframePointerEvents multiple times could prevent the original styles from being restored.
  • If two resizers use the hook at the same time, they could interfere with each other.

I think these cases are worth checking before merging. Other than that, the approach looks good to me. Good work @yangguan-xue

// globals for its own realm's versions, which makes esbuild fail to load
// intermittently. The test asserts on request batching only and needs no DOM,
// so run it in node.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, why not..
We should check that this works in Github CI.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

 RUN  v5.0.3 /home/tornado-softwares/Bureau/projects/JS/intlayer

 ✓ packages/@intlayer/design-system/src/api/pushDictionariesInBatches.test.ts (6 tests) 20ms
   ✓ pushDictionariesInBatches (6)
     ✓ pushes every dictionary in batches and merges the results 4ms
     ✓ keeps at most PUSH_CONCURRENCY requests in flight 11ms
     ✓ reports the progress of the push 1ms
     ✓ keeps pushing when a batch throws, and reports its dictionaries 1ms
     ✓ throws when every batch fails 1ms
     ✓ reports a failed batch response 0ms

 Test Files  1 passed (1)
      Tests  6 passed (6)
   Start at  21:05:15
   Duration  729ms (transform 67%, import 30%, tests 3%, worker 1%)

Okay !

import { fireEvent, render } from '@testing-library/react';
import { afterEach, describe, expect, it } from 'vitest';
import { WithResizer } from './index';

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

 RUN  v5.0.3 /home/tornado-softwares/Bureau/projects/JS/intlayer/packages/@intlayer/design-system

 ✓ src/components/WithResizer/withResizer.test.tsx (2 tests) 140ms
   ✓ WithResizer (2)
     ✓ disables pointer events on iframes while dragging and restores them on release 124ms
     ✓ restores iframe pointer events when unmounted mid-drag 13ms

 Test Files  1 passed (1)
      Tests  2 passed (2)
   Start at  21:10:13
   Duration  1.14s (environment 72%, tests 13%, import 8%, transform 5%, setup 1%, worker 1%)

Ok, Your tests pass and seem relevant to me.

import { fireEvent, render, screen } from '@testing-library/react';
import { afterEach, describe, expect, it } from 'vitest';
import { HeightResizer } from './index';

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

 RUN  v5.0.3 /home/tornado-softwares/Bureau/projects/JS/intlayer/packages/@intlayer/design-system

 ✓ src/components/HeightResizer/heightResizer.test.tsx (2 tests) 124ms
   ✓ HeightResizer (2)
     ✓ disables pointer events on iframes while dragging and restores them on release 108ms
     ✓ restores iframe pointer events when unmounted mid-drag 13ms

 Test Files  1 passed (1)
      Tests  2 passed (2)
   Start at  21:12:16
   Duration  829ms (environment 65%, tests 17%, import 10%, transform 6%, setup 1%, worker 1%)

Ok

/** Disable pointer events on all iframes so the drag keeps tracking over them. */
const disableIframePointerEvents = useCallback(() => {
const saved = new Map<HTMLIFrameElement, string>();
for (const iframe of Array.from(document.querySelectorAll('iframe'))) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

document.querySelectorAll('iframe')

This can create side effects, but it shouldn't be an issue for us, provided the state is restored at the end of the drag operation.

}, []);

/** Restore the pointer events saved when the interaction started. */
const restoreIframePointerEvents = useCallback(() => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is called correctly in our case, but if someone else uses this function and calls it a second time, it appears to overwrite the previous state.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry I merged quickly and did not take consideration of that comment @tornado-softwares

Let's see how it can be fixed

@@ -0,0 +1,59 @@
import { useCallback, useRef } from 'react';

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, it seems generally fine, except for the fact that if multiple resizers are used on a single page, I’m concerned that this hook might conflict with other instances of itself I’m not entirely sure, though; perhaps this behavior should be tested.

@aymericzip

Copy link
Copy Markdown
Owner

Thanks @yangguan-xue
Nice approach

The code seems good to me too, let's merge and try it in live

Thanks @yangguan-xue @tornado-softwares

@aymericzip
aymericzip merged commit db0a07d into aymericzip:main Oct 31, 2026
1 of 2 checks passed
@yangguan-xue

Copy link
Copy Markdown
Contributor Author

Good catch @tornado-softwares — both points check out. Right now every disableIframePointerEvents call snapshots the current value, so a second call before restore would snapshot the already-cleared 'none', and two resizers would restore out of order. I'll open a follow-up PR: move the saved state into a module-level WeakMap with a reference count, so nested/concurrent disables stack and only the last restore writes the original values back.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Dashboard editor: side panel resizer loses the drag when the cursor crosses the visual editor iframe

3 participants