handleOutsideClick should drop events that started inside

This commit is contained in:
Jamie Kyle 2023-06-21 16:28:31 -07:00 committed by GitHub
parent 96de08cef9
commit 6f1d824c3d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 67 additions and 32 deletions

View file

@ -303,6 +303,7 @@ export function ForwardMessagesModal({
useFocusTrap={false} useFocusTrap={false}
padded={false} padded={false}
modalFooter={footer} modalFooter={footer}
noMouseClose
> >
{isEditingMessage && lonelyDraft != null ? ( {isEditingMessage && lonelyDraft != null ? (
<ForwardMessageEditor <ForwardMessageEditor

View file

@ -2,39 +2,50 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import type { RefObject } from 'react'; import type { RefObject } from 'react';
import * as log from '../logging/log';
export type ClickHandlerType = (target: Node) => boolean; export type HandlerType = (target: Node) => boolean;
export type HandlersType = {
name: string;
handleClick: HandlerType;
handlePointerDown: HandlerType;
};
export type ContainerElementType = Node | RefObject<Node> | null | undefined; export type ContainerElementType = Node | RefObject<Node> | null | undefined;
// TODO(indutny): DESKTOP-4177 // TODO(indutny): DESKTOP-4177
// A stack of handlers. Handlers are executed from the top to the bottom // A stack of handlers. Handlers are executed from the top to the bottom
const fakeClickHandlers = new Array<{ const fakeHandlers = new Array<HandlersType>();
name: string;
handleEvent: (event: MouseEvent) => boolean;
}>();
function runFakeClickHandlers(event: MouseEvent): void {
for (const entry of fakeClickHandlers.slice().reverse()) {
const { handleEvent } = entry;
if (handleEvent(event)) {
break;
}
}
}
export type HandleOutsideClickOptionsType = Readonly<{ export type HandleOutsideClickOptionsType = Readonly<{
name: string; name: string;
containerElements: ReadonlyArray<ContainerElementType>; containerElements: ReadonlyArray<ContainerElementType>;
}>; }>;
function handleGlobalPointerDown(event: MouseEvent) {
for (const handlers of fakeHandlers) {
// continue even if handled, so that we can detect if the click was inside
handlers.handlePointerDown(event.target as Node);
}
}
function handleGlobalClick(event: MouseEvent) {
for (const handlers of fakeHandlers.slice().reverse()) {
const handled = handlers.handleClick(event.target as Node);
if (handled) {
log.info(`handleOutsideClick: ${handlers.name} handled click`);
break;
}
}
}
const eventOptions = { capture: true };
export const handleOutsideClick = ( export const handleOutsideClick = (
handler: ClickHandlerType, handler: HandlerType,
{ name, containerElements }: HandleOutsideClickOptionsType { name, containerElements }: HandleOutsideClickOptionsType
): (() => void) => { ): (() => void) => {
const handleEvent = (event: MouseEvent) => { function isInside(target: Node) {
const target = event.target as Node; return containerElements.some(elem => {
const isInside = containerElements.some(elem => {
if (!elem) { if (!elem) {
return false; return false;
} }
@ -43,30 +54,53 @@ export const handleOutsideClick = (
} }
return elem.current?.contains(target); return elem.current?.contains(target);
}); });
}
let startedInside = false;
function handlePointerDown(target: Node) {
startedInside = isInside(target);
return false;
}
function handleClick(target: Node) {
const endedInside = isInside(target);
// Clicked inside of one of container elements - stop processing // Clicked inside of one of container elements - stop processing
if (isInside) { if (startedInside || endedInside) {
return true; return false;
} }
// Stop processing if requested by handler function // Stop processing if requested by handler function
return handler(target); return handler(target);
}
const fakeHandler = {
name,
handleClick,
handlePointerDown,
}; };
const fakeHandler = { name, handleEvent }; fakeHandlers.push(fakeHandler);
fakeClickHandlers.push(fakeHandler);
if (fakeClickHandlers.length === 1) { if (fakeHandlers.length === 1) {
const useCapture = true; document.addEventListener(
document.addEventListener('click', runFakeClickHandlers, useCapture); 'pointerdown',
handleGlobalPointerDown,
eventOptions
);
document.addEventListener('click', handleGlobalClick, eventOptions);
} }
return () => { return () => {
const index = fakeClickHandlers.indexOf(fakeHandler); const index = fakeHandlers.indexOf(fakeHandler);
fakeClickHandlers.splice(index, 1); fakeHandlers.splice(index, 1);
if (fakeClickHandlers.length === 0) { if (fakeHandlers.length === 0) {
const useCapture = true; document.removeEventListener(
document.removeEventListener('click', runFakeClickHandlers, useCapture); 'pointerdown',
handleGlobalPointerDown,
eventOptions
);
document.removeEventListener('click', handleGlobalClick, eventOptions);
} }
}; };
}; };