Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use pointer events instead of mouse and touch events. Support multi touch #150

Closed
wants to merge 2 commits into from

Conversation

xnimorz
Copy link
Contributor

@xnimorz xnimorz commented Jul 31, 2021

Hey! Thanks for the library ;)

I was looking at the library after our Twitter discussion: https://twitter.com/Omgovich/status/1421136814954385418

Overview

This pull request introduces several changes:

Removed MouseEvent and TouchEvent in favor of PointerEvent.

It has several advantages:

  1. Smaller library size (about 170 bytes):
Before After
image image
image image
  1. We don't have browser hacks in code to preventDefault behavior and etc.

  2. We don't need to resolve conflicts between touches and clicks.

Improved library performance

  1. I removed useState, which reduces component re-renders,
  2. got rid of excess useEffects along with useCallbacks which also optimizes comparisons and overall performance

Multi touch support

(sorry, didn't find a better way to attach the screencasts)

Before After
https://user-images.githubusercontent.com/1178825/127752546-d7169520-c6d8-4495-9ab4-5af16505edd5.MP4 https://user-images.githubusercontent.com/1178825/127752561-12864f6e-7cad-4a56-b3de-dd6b069eb568.MP4

Unit tests support

  1. Removed unit test which no more needed 🙈

  2. Added PointerEvents support

What about browsers support?

It should work in all browsers including IE11: (and partly IE10): https://caniuse.com/pointer
image

Some screencasts:
Chrome: https://user-images.githubusercontent.com/1178825/127753055-25ba0bb7-8dd0-481d-8776-4f94bd0898a9.mp4

Firefox: https://user-images.githubusercontent.com/1178825/127752834-2da54a31-7c57-47a6-bfc8-3be510af0ace.mov

IPhone safari: https://user-images.githubusercontent.com/1178825/127752561-12864f6e-7cad-4a56-b3de-dd6b069eb568.MP4

src/components/common/Interactive.tsx Outdated Show resolved Hide resolved
// Interrupt "mousedown" call on mobiles
if (!isValid(nativeEvent) || !el) return;
// testing-library doesn't have setPointerCapture implemented
el.setPointerCapture && el.setPointerCapture(nativeEvent.pointerId);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This simple line makes:

  1. Multi touch support
  2. All subsequent pointermove and pointerup events will be captured by this particular element, hence if the pointer is released outside the browser it still will be captured (see screencasts section in description)

More info: https://developer.mozilla.org/en-US/docs/Web/API/Element/setPointerCapture

src/components/common/Interactive.tsx Show resolved Hide resolved
src/components/common/Interactive.tsx Show resolved Hide resolved
const handleMove = useCallback(
(event: MouseEvent | TouchEvent) => {
// Prevent text selection
preventDefaultMove(event);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Don't need it anymore, as there is no intervention for pointer events (as I know)

Comment on lines 56 to 66
el.addEventListener("pointermove", handleMove);
el.addEventListener("pointerup", handleMoveEnd);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Events are attached to the element 'cause it captures all subsequent calls

// Saves incoming handler to the ref in order to avoid "useCallback hell"
function useEventCallback<T>(handler?: (value: T) => void): (value: T) => void {
const callbackRef = useRef(handler);

useEffect(() => {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

There is no need in pause, when a new function was passed, it doesn't have any async side effects, therefore removing useEffect makes the performance better

Copy link
Owner

Choose a reason for hiding this comment

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

Smart!

Copy link
Owner

Choose a reason for hiding this comment

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

@jeetiss check this out. You might find this approach interesting.


return useCallback((value: T) => callbackRef.current && callbackRef.current(value), []);
return callbackRef.current || noop;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

noop is to support backward compatibility

Comment on lines -157 to -173
// Fast clicks on mobile devices
// See https://github.com/omgovich/react-colorful/issues/55
it("Doesn't react on mouse events after a touch interaction", () => {
const handleChange = jest.fn((hslString) => hslString);
const result = render(<HslStringColorPicker color="hsl(100, 0%, 0%)" onChange={handleChange} />);
const hue = result.container.querySelector(".react-colorful__hue .react-colorful__interactive");

fireEvent.touchStart(hue, { touches: [{ pageX: 0, pageY: 0, bubbles: true }] }); // 1
fireEvent.touchMove(hue, { touches: [{ pageX: 55, pageY: 0, bubbles: true }] }); // 2

// Should be skipped
fireEvent(hue, new FakeMouseEvent("mousedown", { pageX: 35, pageY: 0 })); // 3
fireEvent(hue, new FakeMouseEvent("mousemove", { pageX: 105, pageY: 0 })); // 4

expect(handleChange).toHaveReturnedTimes(2);
expect(handleChange).toHaveReturnedWith("hsl(180, 0%, 0%)");
});
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Pointer events make it unified

Comment on lines -125 to -140
it("Pointer doesn't follow the mouse if it was released outside of the document bounds", async () => {
const handleChange = jest.fn();
const result = render(<RgbaColorPicker onChange={handleChange} />);
const saturation = result.container.querySelector(
".react-colorful__saturation .react-colorful__interactive"
);

// User presses and moves the cursor inside the window
fireEvent(saturation, new FakeMouseEvent("mousedown", { pageX: 20, pageY: 10 })); // 1
fireEvent(saturation, new FakeMouseEvent("mousemove", { pageX: 10, pageY: 10 })); // 2
// User releases the mouse button outside of the document bounds with no `mouseup` event fired
// User moves the cursor back to the document with no button pressed
fireEvent(saturation, new FakeMouseEvent("mousemove", { pageX: 1, pageY: 50, buttons: 0 })); // 3

expect(handleChange).toHaveReturnedTimes(2); // the last `mousemove` has to be ignored
});
Copy link
Contributor Author

Choose a reason for hiding this comment

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

fulfilled by using pointerevents

Copy link
Owner

@omgovich omgovich Aug 2, 2021

Choose a reason for hiding this comment

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

Can we emulate the same test case (pointer outside of the window) with PointerEvent? 🤔

@xnimorz
Copy link
Contributor Author

xnimorz commented Jul 31, 2021

Added separate commit, which adds TouchEvent support only for browsers that don't support PointerEvents.

The size gets bigger than after the first commit, but it is still smaller than the master state:
image

Also, the second commit removes all useCallbacks from the Interactive.tsx and makes some changes for unit tests setup (because ts-jest has big problems with mocking modules)

The only problem I have atm (didn't figure out it yet) is that Captures arrow keys only gets failed (I'll check it tomorrow)

UPD: it's all working:
image

package.json Show resolved Hide resolved
@codecov-commenter
Copy link

Codecov Report

Merging #150 (ac80cc0) into master (dd56e5d) will decrease coverage by 0.70%.
The diff coverage is 94.44%.

Impacted file tree graph

@@             Coverage Diff             @@
##            master     #150      +/-   ##
===========================================
- Coverage   100.00%   99.29%   -0.71%     
===========================================
  Files           33       33              
  Lines          433      428       -5     
  Branches        64       61       -3     
===========================================
- Hits           433      425       -8     
- Misses           0        3       +3     
Impacted Files Coverage Δ
src/components/common/Interactive.tsx 95.91% <93.54%> (-4.09%) ⬇️
src/hooks/useEventCallback.ts 100.00% <100.00%> (ø)
src/components/HslStringColorPicker.tsx 85.71% <0.00%> (-14.29%) ⬇️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update dd56e5d...ac80cc0. Read the comment docs.

@omgovich
Copy link
Owner

omgovich commented Aug 2, 2021

Hey @xnimorz. I'm still not sure about migrating to PointerEvent. I try to support as many browsers as possible and used to drop support of browsers that have less than 0.5% of the marker (the same rule as browserslist's defaults use). But there are a lot of browsers that don't support PointerEvent and have more than 0.5% like...

  • Firefox <=52 — 0.51% (needs MouseEvent)
  • Chrome <=54 — 0.6% (needs MouseEvent)
  • macOS Safari <=12.1 — ~0.5% (needs MouseEvent)
  • iOS Safari <=12.4 — 0.9% (you covered that by the fallback to TouchEvent)

@xnimorz
Copy link
Contributor Author

xnimorz commented Aug 2, 2021

I'll update the diff this week later and return mouse events than

@xnimorz
Copy link
Contributor Author

xnimorz commented Aug 7, 2021

I'm going to publish 2 separate diff

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.

None yet

3 participants