From b3d413cbf8b5dd908054ca5d8a1750ee9c878afc Mon Sep 17 00:00:00 2001 From: Chris Meiller Date: Fri, 5 Sep 2025 07:19:43 +0000 Subject: [PATCH] Fix scrollbar issue not triggering onMenuScrollToBottom --- packages/react-select/src/Select.tsx | 4 +- .../src/internal/ScrollManager.tsx | 8 ++-- .../src/internal/useScrollCapture.ts | 39 ++++++++++++------- 3 files changed, 30 insertions(+), 21 deletions(-) diff --git a/packages/react-select/src/Select.tsx b/packages/react-select/src/Select.tsx index 146d084e20..dfd182e631 100644 --- a/packages/react-select/src/Select.tsx +++ b/packages/react-select/src/Select.tsx @@ -242,9 +242,9 @@ export interface Props< /** Handle the menu closing */ onMenuClose: () => void; /** Fired when the user scrolls to the top of the menu */ - onMenuScrollToTop?: (event: WheelEvent | TouchEvent) => void; + onMenuScrollToTop?: (event: Event) => void; /** Fired when the user scrolls to the bottom of the menu */ - onMenuScrollToBottom?: (event: WheelEvent | TouchEvent) => void; + onMenuScrollToBottom?: (event: Event) => void; /** Allows control of whether the menu is opened when the Select is focused */ openMenuOnFocus: boolean; /** Allows control of whether the menu is opened when the Select is clicked */ diff --git a/packages/react-select/src/internal/ScrollManager.tsx b/packages/react-select/src/internal/ScrollManager.tsx index 4e1440008f..47210eb55e 100644 --- a/packages/react-select/src/internal/ScrollManager.tsx +++ b/packages/react-select/src/internal/ScrollManager.tsx @@ -8,10 +8,10 @@ interface Props { readonly children: (ref: RefCallback) => ReactElement; readonly lockEnabled: boolean; readonly captureEnabled: boolean; - readonly onBottomArrive?: (event: WheelEvent | TouchEvent) => void; - readonly onBottomLeave?: (event: WheelEvent | TouchEvent) => void; - readonly onTopArrive?: (event: WheelEvent | TouchEvent) => void; - readonly onTopLeave?: (event: WheelEvent | TouchEvent) => void; + readonly onBottomArrive?: (event: Event) => void; + readonly onBottomLeave?: (event: Event) => void; + readonly onTopArrive?: (event: Event) => void; + readonly onTopLeave?: (event: Event) => void; } const blurSelectInput = (event: MouseEvent) => { diff --git a/packages/react-select/src/internal/useScrollCapture.ts b/packages/react-select/src/internal/useScrollCapture.ts index 03b8bd9f59..bd1778db6d 100644 --- a/packages/react-select/src/internal/useScrollCapture.ts +++ b/packages/react-select/src/internal/useScrollCapture.ts @@ -1,17 +1,17 @@ import { useCallback, useEffect, useRef } from 'react'; import { supportsPassiveEvents } from '../utils'; -const cancelScroll = (event: WheelEvent | TouchEvent) => { +const cancelScroll = (event: Event) => { if (event.cancelable) event.preventDefault(); event.stopPropagation(); }; interface Options { readonly isEnabled: boolean; - readonly onBottomArrive?: (event: WheelEvent | TouchEvent) => void; - readonly onBottomLeave?: (event: WheelEvent | TouchEvent) => void; - readonly onTopArrive?: (event: WheelEvent | TouchEvent) => void; - readonly onTopLeave?: (event: WheelEvent | TouchEvent) => void; + readonly onBottomArrive?: (event: Event) => void; + readonly onBottomLeave?: (event: Event) => void; + readonly onTopArrive?: (event: Event) => void; + readonly onTopLeave?: (event: Event) => void; } export default function useScrollCapture({ @@ -25,9 +25,10 @@ export default function useScrollCapture({ const isTop = useRef(false); const touchStart = useRef(0); const scrollTarget = useRef(null); + const previousScrollTop = useRef(0); const handleEventDelta = useCallback( - (event: WheelEvent | TouchEvent, delta: number) => { + (event: Event, delta: number) => { if (scrollTarget.current === null) return; const { scrollTop, scrollHeight, clientHeight } = scrollTarget.current; @@ -73,16 +74,24 @@ export default function useScrollCapture({ [onBottomArrive, onBottomLeave, onTopArrive, onTopLeave] ); - const onWheel = useCallback( - (event: WheelEvent) => { - handleEventDelta(event, event.deltaY); + const onScroll = useCallback( + (event: Event) => { + const target = event.currentTarget; + + if (target instanceof HTMLElement) { + const deltaY = target.scrollTop - previousScrollTop.current; + previousScrollTop.current = target.scrollTop; + handleEventDelta(event, deltaY); + } }, [handleEventDelta] ); + const onTouchStart = useCallback((event: TouchEvent) => { // set touch start so we can calculate touchmove delta touchStart.current = event.changedTouches[0].clientY; }, []); + const onTouchMove = useCallback( (event: TouchEvent) => { const deltaY = touchStart.current - event.changedTouches[0].clientY; @@ -92,28 +101,28 @@ export default function useScrollCapture({ ); const startListening = useCallback( - (el) => { + (el: HTMLElement | null) => { // bail early if no element is available to attach to if (!el) return; const notPassive = supportsPassiveEvents ? { passive: false } : false; - el.addEventListener('wheel', onWheel, notPassive); + el.addEventListener('scroll', onScroll, notPassive); el.addEventListener('touchstart', onTouchStart, notPassive); el.addEventListener('touchmove', onTouchMove, notPassive); }, - [onTouchMove, onTouchStart, onWheel] + [onTouchMove, onTouchStart, onScroll] ); const stopListening = useCallback( - (el) => { + (el: HTMLElement | null) => { // bail early if no element is available to detach from if (!el) return; - el.removeEventListener('wheel', onWheel, false); + el.removeEventListener('scroll', onScroll, false); el.removeEventListener('touchstart', onTouchStart, false); el.removeEventListener('touchmove', onTouchMove, false); }, - [onTouchMove, onTouchStart, onWheel] + [onTouchMove, onTouchStart, onScroll] ); useEffect(() => {