Skip to content

Add CSSOM View extensions to MouseEvent#3484

Merged
domenic merged 4 commits intojsdom:masterfrom
jenseng:mouseevent-cssom
Jan 10, 2023
Merged

Add CSSOM View extensions to MouseEvent#3484
domenic merged 4 commits intojsdom:masterfrom
jenseng:mouseevent-cssom

Conversation

@jenseng
Copy link
Copy Markdown
Contributor

@jenseng jenseng commented Jan 5, 2023

Ensure that the CSSOM View MouseEvent attributes are set. Specifically this ensures that x/y mirror clientX/clientY and that offsetX/offsetY are set (always 0 since there is no layout).

Fixes #3483, #3470
Builds on and supersedes #3472 (reworks it to fix the default case and the tests)

optional EventTarget? relatedTargetArg = null);
};

// https://www.w3.org/TR/cssom-view/#extensions-to-the-mouseevent-interface
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Use https://drafts.csswg.org/cssom-view/ here and below; we don't use TR URLs.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done

};

// https://www.w3.org/TR/cssom-view/#extensions-to-the-mouseevent-interface
// Changes existing coordinate entries to doubles
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm not sure how well this works in webidl2js. I guess it didn't break the build, which is good, but does it properly "override" the existing ones?

I think before merging, I need a test to ensure that these correctly come out as doubles instead of longs. So, construct a new MouseEvent with its screenX/screenY/clientX/clientY constructor options set to something fractional (e.g. 1.5), and then read off the results from the object and ensure they're still fractional.

Bonus points if you add a test that using initMouseEvent does not preserve the fractionalness, since the arguments are still defined to be longs.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Interestingly, it seems that although major browsers (chromium/firefox/webkit) have implemented these MouseEvent properties, they still convert doubles to longs (possibly for legacy compatibility reasons?) I don't know if that means jsdom shouldn't support doubles, but perhaps something to think about.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added tests around this

get y() {
return this.clientY;
}
get offsetX() {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Per spec these should be the same as pageX/pageY. A test, or enabling of an existing test, would be good.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done

get offsetY() {
return 0;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

You're missing getters for pageX and pageY. See how they are defined in steps 2-3 of https://drafts.csswg.org/cssom-view/#dom-mouseevent-pagex .

Copy link
Copy Markdown
Contributor Author

@jenseng jenseng Jan 9, 2023

Choose a reason for hiding this comment

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

i'm not sure if the getters are actually needed, since (i think) by default the various impl properties are just regular properties (e.g. see clientX usage above, which works and doesn't have an explicit getter) Nevermind, I overlooked that bit of logic in the spec, I'll add them 👍

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done, and added a test

* Add tests for long/double handling
* Fully implement pageX/pageY/etc, add tests
* Fix links in comments
@domenic domenic self-assigned this Jan 9, 2023
@domenic domenic merged commit eb82a27 into jsdom:master Jan 10, 2023
get pageX() {
if (this._dispatchFlag) {
return 0;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@jenseng @domenic I'm wondering why pageX returns 0 here when _dispatchFlag is true. This behavior leads to wrong results when accessing the event in a listener:

document.body.addEventListener('mousemove', e => {
  console.log('event', e.pageX, e.pageY);
});
const ev = new MouseEvent('mousemove', { clientX: 10, clientY: 10 });
document.body.dispatchEvent(ev);

Running this in a JSDOM environment logs event 0 0 because the pageX getter is called inside a dispatch.

On the other hand, running the same code in Chrome logs the correct event 10 10.

I see that the spec says things like "if the event’s dispatch flag is set, return A, otherwise return B", but that doesn't mean 0 is the right value. It should still be the coordinate of the event relative to some origin.

Looking at how Chrome handles this, I see that pageX is

  1. Initialized to clientX
  2. Incremented by a scrollX offset if available.

There is no mention of the dispatch flag, at least not explicitly. That leaves me with the conclusion that JSDOM shouldn't implement any special behavior when _dispatchFlag is true, always returning clientX + scrollX is fine.

This is quite a serious bug when JSDOM is used with Jest and React Testing Library, as it breaks unit tests that create MouseEvents to simulate moving the mouse.

I discovered this when trying out Jest upgraded to JSDOM 21 (jestjs/jest#13825) to run unit tests of WordPress Gutenberg project.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@jsnajdr when implementing this, my goal was to:

  1. Follow the spec as closely as possible, and
  2. Return something reasonable where we can't fully implement it, i.e. since jsdom doesn't implement layout/rendering, some things are unknowable so perhaps 0 is the least bad option

A (minor) risk with returning the current clientX + scrollX is that it could be inconsistent with browsers, since scrollX/Y could change between the time of event initialization and when pageX/pageY are read during dispatch (e.g. if explicitly set or scrollTo is called during an earlier click handler).

That said, it's probably still a better default than zero, and assuming scrollX/Y hasn't changed, it should be synonymous with the position where the event occurred relative to the origin of the initial containing block.

@domenic wdyt? Seems like a reasonable change to me, though perhaps it's still a good idea to keep the dispatch branch even if the return value is the same. That way it's a good placeholder/indicator for when layout support is added.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I do have an existing PR that is closely related to this, and I think we can solve the minor risk outlined above by setting the new _initialContainingBlockRelativeX/Y to clientX/Y + scrollX/Y.

That way the values will be persisted at init time, and not risk changing if scrollX/Y changes later before/during dispatch.

Maybe I'll just split it out into its own PR

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

#3514 should resolve this

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.

MouseEvent: offsetX and offsetY return undefined

4 participants