From bf165b0926bb8d5762e429f95e727a748c813fe3 Mon Sep 17 00:00:00 2001 From: jogibear9988 Date: Fri, 28 Aug 2026 11:29:16 +0200 Subject: [PATCH] fix: preserve projective mode after activation Forward the originating selection-action event into plugin commands so interactive plugins can distinguish their activation pointer sequence from later outside clicks. Track the activation pointer ID in the projective overlay and defer outside-click handling until its pointerup and synthetic click have completed. This prevents a normal-duration menu click from immediately closing 3D transform mode and restoring resize handles. Extend browser coverage to hold the activation pointer before release, reproducing real user timing while retaining outside-click exit behavior and the existing transform regressions. --- app/components/selection/actions.element.js | 11 +++++--- .../selection/projective-transform.element.js | 25 +++++++++++++++---- app/plugins/projective-transform.js | 4 ++- app/plugins/projective-transform.test.js | 5 +++- 4 files changed, 34 insertions(+), 11 deletions(-) diff --git a/app/components/selection/actions.element.js b/app/components/selection/actions.element.js index 3c747821..b9b7c4ea 100644 --- a/app/components/selection/actions.element.js +++ b/app/components/selection/actions.element.js @@ -55,7 +55,7 @@ export class SelectionActions extends HTMLElement { event.preventDefault() event.stopPropagation() if (this.hasAttribute('busy')) return - this.runAction(button) + this.runAction(button, event) } on_click(event) { @@ -69,10 +69,10 @@ export class SelectionActions extends HTMLElement { // can be light-dismissed. A zero-detail click is keyboard/programmatic. if (event.detail) return if (this.hasAttribute('busy')) return - this.runAction(button) + this.runAction(button, event) } - async runAction(button) { + async runAction(button, activationEvent) { const label = button.textContent button.ariaDisabled = 'true' button.textContent = `${label}…` @@ -81,7 +81,10 @@ export class SelectionActions extends HTMLElement { try { const visbug = document.querySelector('vis-bug') if (!visbug) throw new Error('VisBug is not connected') - await visbug.execCommand(button.dataset.command, {source: this.source_el}) + await visbug.execCommand(button.dataset.command, { + source: this.source_el, + activationEvent, + }) this.close() } catch (error) { diff --git a/app/components/selection/projective-transform.element.js b/app/components/selection/projective-transform.element.js index c1bb0446..83972e82 100644 --- a/app/components/selection/projective-transform.element.js +++ b/app/components/selection/projective-transform.element.js @@ -33,6 +33,7 @@ export class ProjectiveTransform extends HTMLElement { this.on_transition_run = this.on_transition_run.bind(this) this.on_transition_end = this.on_transition_end.bind(this) this.refresh_transition = this.refresh_transition.bind(this) + this.on_activation_pointer_end = this.on_activation_pointer_end.bind(this) } connectedCallback() { @@ -43,11 +44,12 @@ export class ProjectiveTransform extends HTMLElement { this.svg.addEventListener('pointerdown', this.on_pointer_down) window.addEventListener('resize', this.on_position_change) window.addEventListener('scroll', this.on_position_change, true) - this.outside_click_timer = setTimeout(() => { - if (this.isConnected) - document.addEventListener('click', this.on_document_click, true) - }) + document.addEventListener('click', this.on_document_click, true) document.addEventListener('visbug-tool-change', this.on_tool_change) + if (this.activation_pointer_id !== undefined) { + document.addEventListener('pointerup', this.on_activation_pointer_end, true) + document.addEventListener('pointercancel', this.on_activation_pointer_end, true) + } this.source_observer = new MutationObserver(this.on_position_change) this.observe_source() @@ -64,9 +66,11 @@ export class ProjectiveTransform extends HTMLElement { this.svg?.removeEventListener('pointerdown', this.on_pointer_down) window.removeEventListener('resize', this.on_position_change) window.removeEventListener('scroll', this.on_position_change, true) - clearTimeout(this.outside_click_timer) + clearTimeout(this.activation_pointer_timer) document.removeEventListener('click', this.on_document_click, true) document.removeEventListener('visbug-tool-change', this.on_tool_change) + document.removeEventListener('pointerup', this.on_activation_pointer_end, true) + document.removeEventListener('pointercancel', this.on_activation_pointer_end, true) this.source_observer?.disconnect() this.source_el?.removeEventListener('transitionrun', this.on_transition_run) this.source_el?.removeEventListener('transitionend', this.on_transition_end) @@ -147,9 +151,20 @@ export class ProjectiveTransform extends HTMLElement { } on_document_click(event) { + if (this.activation_pointer_id !== undefined) return if (!event.composedPath().includes(this)) this.remove() } + on_activation_pointer_end(event) { + if (event.pointerId !== this.activation_pointer_id) return + + document.removeEventListener('pointerup', this.on_activation_pointer_end, true) + document.removeEventListener('pointercancel', this.on_activation_pointer_end, true) + this.activation_pointer_timer = setTimeout(() => { + this.activation_pointer_id = undefined + }) + } + on_tool_change() { this.remove() } diff --git a/app/plugins/projective-transform.js b/app/plugins/projective-transform.js index 8043d8ac..ed6df478 100644 --- a/app/plugins/projective-transform.js +++ b/app/plugins/projective-transform.js @@ -17,7 +17,7 @@ const selectionOverlays = [ 'visbug-corners', ].join(',') -export default function projectiveTransform({selected, source}) { +export default function projectiveTransform({selected, source, activationEvent}) { const element = source || selected[0] if (!element) throw new Error('Select an element before using 3D transform') @@ -26,6 +26,8 @@ export default function projectiveTransform({selected, source}) { const overlay = document.createElement('visbug-projective-transform') overlay.source = element + if (activationEvent?.type === 'pointerdown') + overlay.activation_pointer_id = activationEvent.pointerId overlay.suppress_overlays(Array.from( document.querySelectorAll(selectionOverlays))) document.body.appendChild(overlay) diff --git a/app/plugins/projective-transform.test.js b/app/plugins/projective-transform.test.js index 23ebe8b4..d2e0a336 100644 --- a/app/plugins/projective-transform.test.js +++ b/app/plugins/projective-transform.test.js @@ -137,7 +137,10 @@ test('selection action replaces the regular selection overlays', async t => { return {x: bounds.left + bounds.width / 2, y: bounds.top + bounds.height / 2} }) - await page.mouse.click(action.x, action.y) + await page.mouse.move(action.x, action.y) + await page.mouse.down() + await new Promise(resolve => setTimeout(resolve, 75)) + await page.mouse.up() await page.waitForSelector('visbug-projective-transform') t.is(await page.$$('visbug-projective-transform').then(items => items.length), 1)