core icon indicating copy to clipboard operation
core copied to clipboard

click event is registered delayed on components opened in new browser windows

Open danielSt-dev opened this issue 4 years ago • 9 comments

Version

3.1.1

Reproduction link

https://codesandbox.io/s/determined-yonath-3hccx

Steps to reproduce

  1. open child window with a click on "open,close window" for the first time and wait 3 seconds
  2. click on message --> nothing happens
  3. wait 3 seconds and click again --> nothing happens
  4. wait another 15 seconds and click again --> alert message in "main" window appears (chrome: if devtools are closed, a dot on the tab of main window notifies you, that an alert is there)

So you see, there is a relatively long time until the click handler will be registered (about 15-20 seconds)

Now close the window with a click on "open,close window" in main window.

If you repeat the above steps, the click handler will only be called after a few minutes (1-2 minutes; if it won't work, leave it open and try it after 5-10 minutes). With every close/open new window the time increases. Important: Do not click too often after opening the window! It seems that the click eventhandler never gets registered if you click too often.

Info: firefox appears to be faster than chrome. After repeating this 4 times firefox needs about 3 minute that the click event is handled. chrome much more.

What is expected?

click events should be fired/registered immediately

What is actually happening?

it seems the click event will be registered delayed.


I found have a problem with click events on components, that are opened in a new browser window with vue 3. I open a vue 3 component in a new window like the following code (copied and customized from https://github.com/Shamus03/vue-window-portal):

<template>
    <teleport to="body">
        <div v-if="open" v-show="windowLoaded" ref="teleportingContent">
            <slot></slot>
        </div>
    </teleport>
</template>
...
methods: {
  openPortal() {
    this.windowRef = window.open('', '', '');
    this.windowRef.document.body.innerHTML = '';
    const app = document.createElement('div');
    app.id = 'app';
    app.appendChild(this.$refs.teleportingContent);
    this.windowRef.document.body.appendChild(app);
  }
}
...

With this code the slot component will be attached to the DOM of the new opened window. This works and the vue objects/props are reactive in the new window.

The problem is if there is a click-eventhandler on an element, which is attached to DOM of the new window. It seems that the click event is registered delayed.

If the window is closed/opened multiple times, the time of the click registration increases every time.

I want to be able to "dock off" some content of the main window to a new window (e. g. from a video conference push two video participants to new windows; move a component to a new window for comparing content in two screens).

danielSt-dev avatar Jun 10 '21 09:06 danielSt-dev

bug related to fix made for https://github.com/vuejs/vue/issues/6566

https://github.com/vuejs/vue-next/blob/870f2a7ba35245fd8c008d2ff666ea130a7e4704/packages/runtime-dom/src/modules/events.ts#L118-L128

The issue is, that for browsers where the event uses the hi-res timestamp The child window time resets/start from 0, when the new window is opened, creating the discrepancy events triggered in the child will have a lower timestamp than the parent window.

The event handler is invoked only when the child window internal time catch up to the time the element was created in the parent.

lidlanca avatar Jun 11 '21 05:06 lidlanca

same story for elements moved to an iframe.

lidlanca avatar Jun 13 '21 19:06 lidlanca

For information, this is how we temporaly fix it for our usecase (App which load components inside iframe)

iframe.onload = () => { performance.now = iframe.contentWindow.performance.now }

Tested only on Chrome, this is a way to resynchronise the timestamps used by Vue

AurelieV avatar Oct 08 '21 10:10 AurelieV

@AurelieV thanks for your workaround! It works great! The only thing I had to do is wrapping the assignment in a new function: performance.now = () => { return this.windowRef.performance.now(); }; Otherwise the performance.now will not be overridden.

danielSt-dev avatar Oct 13 '21 14:10 danielSt-dev

when I use teleport whose to is shadow root, there is also a relatively long time until the click handler will be registered (about 5 seconds)

fnlearner avatar Nov 24 '21 02:11 fnlearner

This seems related to #2513.

semiaddict avatar Feb 23 '22 09:02 semiaddict

Hi, we updated our app to vue 3.2.37 and the workaround didn't work anymore.

It is because of merge #5944 (vue version 3.2.36). the call to performance.now through a function is replaced by a direct function assignment, so we can not override the performance.now anymore.

Current solution is downgrade to vue 3.2.35

danielSt-dev avatar Jun 14 '22 11:06 danielSt-dev

another workaround

    <script>
      // Skip timestamp check workaround.
      // Code must be called before vue runtime is imported/evaluated.
      
     /*
       const ffMatch = navigator.userAgent.match(/firefox\/(\d+)/i);
       skipTimestampCheck2 = !!(ffMatch && Number(ffMatch[1]) <= 53);
     */

      let ua = window.navigator.userAgent;
      Object.defineProperty(window.navigator, 'userAgent', {
        get: () => {
          let s = new String(ua); 
          s.match = function (re) {
            // only care about this particualr match call.
            if (re.toString() === '/firefox\\/(\\d+)/i') {
              return [, 53];
            }
            return ua.match(re);
          };
          return s;
        },
      });
      // if we don't mind mutating userAgent, shorter version
      // Object.defineProperty(window.navigator,'userAgent',{get:()=>'firefox/53'})
    </script>

https://stackblitz.com/edit/vitejs-vite-gwxddy

related issue: https://github.com/vuejs/core/issues/2513

lidlanca avatar Jun 16 '22 05:06 lidlanca

I encounter the same bug in Chrome. I have a main VueJS app that load, one by one, multiple apps in an iframe.

All works perfectly, except the events in iframe apps. At the first load, the events are fired with a little delay, and as navigation go, the delay increase and the apps become unusable.

Thanks @AurelieV , I have begun to understand with your comment where the problem originated.

Unfortunately your solution was not enough for me :

iframe.onload = () => { performance.now = iframe.contentWindow.performance.now }

After several hours of research, i found that the event.timeStamp of each events was the key. It must be relative to performance.now(), that was not the case for me, even if i replace the main performance.now by the one in the iframe.

Here the solution thats works for me :

iframe.onload = () => {
	iframe.contentWindow.performance.now = () => performance.now();
	iframe.contentWindow.addEventListener('click', function(e) {
		Object.defineProperty(e, "timeStamp", { 
			get: () => performance.now(); 
		})
	}, true);
}

This will make the click event works as soon as the application is mounted.

For all events, a little loop over onXXX properties could do the trick.

Here the final solution (all events type) :

const handler = (e) => {
    Object.defineProperty(e, "timeStamp", { get: () => performance.now() })
}
const events = Object.keys(window).filter(name => name.substring(0, 2) == 'on').map(name => name.substring(2));
events.forEach((name) => iframe.contentWindow.addEventListener(name, handler, true));

Hope it will help someone ;-)

aprovent avatar Aug 14 '22 22:08 aprovent