Skip to content

Instantly share code, notes, and snippets.

@maradondt
Last active November 28, 2024 08:01
Show Gist options
  • Save maradondt/ddbf6536d967f29fe9790e8ceb3f419f to your computer and use it in GitHub Desktop.
Save maradondt/ddbf6536d967f29fe9790e8ceb3f419f to your computer and use it in GitHub Desktop.
react HOC withContextMenu returns the same component which accept onContextMenu prop that fixed bug with context menu on safari on Iphone
// reference and demo https://github.com/facebook/react/issues/17596#issuecomment-565524946
import React, { ComponentProps, PropsWithChildren } from 'react';
const longPressDuration = 610;
export default class ContextMenuHandler {
private callback: (e: any) => void;
private longPressCountdown: NodeJS.Timeout | null;
private shouldStopPropagation: boolean;
constructor(callback: (e: any) => void, shouldStopPropagation = false) {
this.callback = callback;
this.longPressCountdown = null;
this.shouldStopPropagation = shouldStopPropagation;
}
onTouchStart = (e: TouchEvent) => {
if (this.shouldStopPropagation) {
e.stopPropagation();
}
const touch = e.touches[0];
this.longPressCountdown = setTimeout(() => {
this.callback(touch);
}, longPressDuration);
};
onTouchMove = () => {
this.longPressCountdown && clearTimeout(this.longPressCountdown);
};
onTouchCancel = () => {
this.longPressCountdown && clearTimeout(this.longPressCountdown);
};
onTouchEnd = () => {
this.longPressCountdown && clearTimeout(this.longPressCountdown);
};
onContextMenu = (e: Event) => {
if (this.shouldStopPropagation) {
e.stopPropagation();
}
this.longPressCountdown && clearTimeout(this.longPressCountdown);
this.callback(e);
e.preventDefault();
};
}
type RequiredComponentProps<T extends HTMLElement = HTMLElement> = {
onTouchStart?: React.TouchEventHandler<T>;
onTouchMove?: React.TouchEventHandler<T>;
onTouchCancel?: React.TouchEventHandler<T>;
onTouchEnd?: React.TouchEventHandler<T>;
};
type RequiredWrappedComponentProps<T extends HTMLElement = HTMLElement> = {
onContextMenu?: React.MouseEventHandler<T>;
};
/**
* withContextMenu returns the same component which accept onContextMenu prop that fixed
* bug with context menu on safari on Iphone
* @example
* ```
* const RowWithContextMenu = withContextMenu<HTMLTableRowElement, typeof Row>(Row);
*
* ```
*
*/
export const withContextMenu = <
D extends HTMLElement = HTMLElement,
T extends React.FC<
PropsWithChildren<RequiredComponentProps<D> & RequiredWrappedComponentProps<D>>
> = PropsWithChildren<React.FC<RequiredComponentProps<D> & RequiredWrappedComponentProps<D>>>
>(
WrappedComponent: T
) => {
const displayName = WrappedComponent.displayName || WrappedComponent.name || 'Component';
const ComponentWithContextMenu = ({
onContextMenu,
...props
}: PropsWithChildren<Omit<ComponentProps<T>, keyof RequiredComponentProps>>) => {
const handler = new ContextMenuHandler(onContextMenu);
const Component = WrappedComponent as any;
return <Component {...(handler as RequiredComponentProps & RequiredWrappedComponentProps)} {...props} />;
};
ComponentWithContextMenu.displayName = `withContextMenu(${displayName})`;
return ComponentWithContextMenu;
};
@caspg
Copy link

caspg commented Nov 27, 2024

What is this.contextMenuPossible used for?

@maradondt
Copy link
Author

What is this.contextMenuPossible used for?

When the menu is open, to prevent opening twice or something like this.
I use it almost 2 years in prod, still no issues)

@caspg
Copy link

caspg commented Nov 27, 2024

🤔 I don't see how it's used in the code that's why I asked. Value is only set not read.

@maradondt
Copy link
Author

Yep, you are right) Gist was updated, thx)

@caspg
Copy link

caspg commented Nov 27, 2024

Thanks. Thanks!

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