diff --git a/src/SideEffect.tsx b/src/SideEffect.tsx index 4a6264d..bed991f 100644 --- a/src/SideEffect.tsx +++ b/src/SideEffect.tsx @@ -104,45 +104,52 @@ export function RemoveScrollSideCar(props: IRemoveScrollEffectProps) { return handleScroll(cancelingAxis, parent, event, cancelingAxis === 'h' ? deltaX : deltaY, true); }, []); - const shouldPrevent = React.useCallback((_event: Event) => { - const event: WheelEvent | TouchEvent = _event as any; + const shouldPrevent = React.useCallback( + (subscriberElement: Document | ShadowRoot) => (_event: Event) => { + const event: WheelEvent | TouchEvent = _event as any; - if (!lockStack.length || lockStack[lockStack.length - 1] !== Style) { - // not the last active - return; - } + if (!lockStack.length || lockStack[lockStack.length - 1] !== Style) { + // not the last active + return; + } - const delta = 'deltaY' in event ? getDeltaXY(event) : getTouchXY(event); - const sourceEvent = shouldPreventQueue.current.filter( - (e) => e.name === event.type && e.target === event.target && deltaCompare(e.delta, delta) - )[0]; + const delta = 'deltaY' in event ? getDeltaXY(event) : getTouchXY(event); + const sourceEvent = shouldPreventQueue.current.filter( + (e) => e.name === event.type && e.target === event.target && deltaCompare(e.delta, delta) + )[0]; - // self event, and should be canceled - if (sourceEvent && sourceEvent.should) { - if (event.cancelable) { - event.preventDefault(); + // self event, and should be canceled + if (sourceEvent && sourceEvent.should) { + if (event.cancelable) { + event.preventDefault(); + } + + return; } - return; - } + const shouldPreventQueueContainsEventButSubscriberDoesntContainTarget = shouldPreventQueue.current.some( + (e) => e.name === event.type && deltaCompare(e.delta, delta) && !subscriberElement.contains(e.target) + ); - // outside or shard event - if (!sourceEvent) { - const shardNodes = (lastProps.current.shards || []) - .map(extractRef) - .filter(Boolean) - .filter((node) => node.contains(event.target as any)); + // outside or shard event + if (!sourceEvent && !shouldPreventQueueContainsEventButSubscriberDoesntContainTarget) { + const shardNodes = (lastProps.current.shards || []) + .map(extractRef) + .filter(Boolean) + .filter((node) => node.contains(event.target as any)); - const shouldStop = - shardNodes.length > 0 ? shouldCancelEvent(event, shardNodes[0]) : !lastProps.current.noIsolation; + const shouldStop = + shardNodes.length > 0 ? shouldCancelEvent(event, shardNodes[0]) : !lastProps.current.noIsolation; - if (shouldStop) { - if (event.cancelable) { - event.preventDefault(); + if (shouldStop) { + if (event.cancelable) { + event.preventDefault(); + } } } - } - }, []); + }, + [] + ); const shouldCancel = React.useCallback((name: string, delta: number[], target: any, should: boolean) => { const event = { name, delta, target, should }; @@ -175,15 +182,28 @@ export function RemoveScrollSideCar(props: IRemoveScrollEffectProps) { onTouchMoveCapture: scrollTouchMove, }); - document.addEventListener('wheel', shouldPrevent, nonPassive); - document.addEventListener('touchmove', shouldPrevent, nonPassive); + const documentAndShadowRootsSubscribers = [document, ...getShadowRootsInNode(document)].map( + (documentOrShadowRoot) => ({ + shouldPrevent: shouldPrevent(documentOrShadowRoot), + elementToSubscribeTo: documentOrShadowRoot, + }) + ); + + documentAndShadowRootsSubscribers.forEach(({ shouldPrevent, elementToSubscribeTo }) => { + elementToSubscribeTo.addEventListener('wheel', shouldPrevent, nonPassive); + elementToSubscribeTo.addEventListener('touchmove', shouldPrevent, nonPassive); + }); + document.addEventListener('touchstart', scrollTouchStart, nonPassive); return () => { lockStack = lockStack.filter((inst) => inst !== Style); - document.removeEventListener('wheel', shouldPrevent, nonPassive as any); - document.removeEventListener('touchmove', shouldPrevent, nonPassive as any); + documentAndShadowRootsSubscribers.forEach(({ shouldPrevent, elementToSubscribeTo }) => { + elementToSubscribeTo.removeEventListener('wheel', shouldPrevent, nonPassive as any); + elementToSubscribeTo.removeEventListener('touchmove', shouldPrevent, nonPassive as any); + }); + document.removeEventListener('touchstart', scrollTouchStart, nonPassive as any); }; }, []); @@ -197,3 +217,28 @@ export function RemoveScrollSideCar(props: IRemoveScrollEffectProps) { ); } + +function getShadowRootsInNode(node: Node): ShadowRoot[] { + const shadowRoots: ShadowRoot[] = []; + + walkNodeTree(node, (currentNode) => { + const maybeShadowRoot = (currentNode as HTMLElement).shadowRoot; + + if (maybeShadowRoot) { + shadowRoots.push(maybeShadowRoot); + } + }); + + return shadowRoots; +} + +function walkNodeTree(node: Node, cb: (node: Node) => void) { + cb(node); + node.childNodes.forEach((childNode) => walkNodeTree(childNode, cb)); + + const maybeShadowRoot = (node as HTMLElement).shadowRoot; + + if (maybeShadowRoot) { + maybeShadowRoot.childNodes.forEach((childNode) => walkNodeTree(childNode, cb)); + } +}