diff --git a/packages/react/select/src/Select.tsx b/packages/react/select/src/Select.tsx index 53fbd1ff5..cc151b2a2 100644 --- a/packages/react/select/src/Select.tsx +++ b/packages/react/select/src/Select.tsx @@ -1382,43 +1382,54 @@ SelectItemIndicator.displayName = ITEM_INDICATOR_NAME; const SCROLL_UP_BUTTON_NAME = 'SelectScrollUpButton'; type SelectScrollUpButtonElement = SelectScrollButtonImplElement; -interface SelectScrollUpButtonProps extends Omit {} +interface SelectScrollUpButtonProps extends Omit { + hoverScrollFactor?: number; + touchScrollFactor?: number; +} const SelectScrollUpButton = React.forwardRef< SelectScrollUpButtonElement, SelectScrollUpButtonProps ->((props: ScopedProps, forwardedRef) => { - const contentContext = useSelectContentContext(SCROLL_UP_BUTTON_NAME, props.__scopeSelect); - const viewportContext = useSelectViewportContext(SCROLL_UP_BUTTON_NAME, props.__scopeSelect); - const [canScrollUp, setCanScrollUp] = React.useState(false); - const composedRefs = useComposedRefs(forwardedRef, viewportContext.onScrollButtonChange); +>( + ( + { hoverScrollFactor, touchScrollFactor, ...props }: ScopedProps, + forwardedRef + ) => { + const contentContext = useSelectContentContext(SCROLL_UP_BUTTON_NAME, props.__scopeSelect); + const viewportContext = useSelectViewportContext(SCROLL_UP_BUTTON_NAME, props.__scopeSelect); + const [canScrollUp, setCanScrollUp] = React.useState(false); + const composedRefs = useComposedRefs(forwardedRef, viewportContext.onScrollButtonChange); - useLayoutEffect(() => { - if (contentContext.viewport && contentContext.isPositioned) { - const viewport = contentContext.viewport; - function handleScroll() { - const canScrollUp = viewport.scrollTop > 0; - setCanScrollUp(canScrollUp); - } - handleScroll(); - viewport.addEventListener('scroll', handleScroll); - return () => viewport.removeEventListener('scroll', handleScroll); - } - }, [contentContext.viewport, contentContext.isPositioned]); - - return canScrollUp ? ( - { - const { viewport, selectedItem } = contentContext; - if (viewport && selectedItem) { - viewport.scrollTop = viewport.scrollTop - selectedItem.offsetHeight; + useLayoutEffect(() => { + if (contentContext.viewport && contentContext.isPositioned) { + const viewport = contentContext.viewport; + function handleScroll() { + const canScrollUp = viewport.scrollTop > 0; + setCanScrollUp(canScrollUp); } - }} - /> - ) : null; -}); + handleScroll(); + viewport.addEventListener('scroll', handleScroll); + return () => viewport.removeEventListener('scroll', handleScroll); + } + }, [contentContext.viewport, contentContext.isPositioned]); + + return canScrollUp ? ( + { + const { viewport, selectedItem } = contentContext; + if (viewport && selectedItem) { + const factor = + pointerAction && (pointerAction === 'move' ? hoverScrollFactor : touchScrollFactor); + viewport.scrollTop = + viewport.scrollTop - selectedItem.offsetHeight * Math.abs(factor ?? 1); + } + }} + /> + ) : null; + } +); SelectScrollUpButton.displayName = SCROLL_UP_BUTTON_NAME; @@ -1429,52 +1440,63 @@ SelectScrollUpButton.displayName = SCROLL_UP_BUTTON_NAME; const SCROLL_DOWN_BUTTON_NAME = 'SelectScrollDownButton'; type SelectScrollDownButtonElement = SelectScrollButtonImplElement; -interface SelectScrollDownButtonProps extends Omit {} +interface SelectScrollDownButtonProps extends Omit { + hoverScrollFactor?: number; + touchScrollFactor?: number; +} const SelectScrollDownButton = React.forwardRef< SelectScrollDownButtonElement, SelectScrollDownButtonProps ->((props: ScopedProps, forwardedRef) => { - const contentContext = useSelectContentContext(SCROLL_DOWN_BUTTON_NAME, props.__scopeSelect); - const viewportContext = useSelectViewportContext(SCROLL_DOWN_BUTTON_NAME, props.__scopeSelect); - const [canScrollDown, setCanScrollDown] = React.useState(false); - const composedRefs = useComposedRefs(forwardedRef, viewportContext.onScrollButtonChange); +>( + ( + { hoverScrollFactor, touchScrollFactor, ...props }: ScopedProps, + forwardedRef + ) => { + const contentContext = useSelectContentContext(SCROLL_DOWN_BUTTON_NAME, props.__scopeSelect); + const viewportContext = useSelectViewportContext(SCROLL_DOWN_BUTTON_NAME, props.__scopeSelect); + const [canScrollDown, setCanScrollDown] = React.useState(false); + const composedRefs = useComposedRefs(forwardedRef, viewportContext.onScrollButtonChange); - useLayoutEffect(() => { - if (contentContext.viewport && contentContext.isPositioned) { - const viewport = contentContext.viewport; - function handleScroll() { - const maxScroll = viewport.scrollHeight - viewport.clientHeight; - // we use Math.ceil here because if the UI is zoomed-in - // `scrollTop` is not always reported as an integer - const canScrollDown = Math.ceil(viewport.scrollTop) < maxScroll; - setCanScrollDown(canScrollDown); - } - handleScroll(); - viewport.addEventListener('scroll', handleScroll); - return () => viewport.removeEventListener('scroll', handleScroll); - } - }, [contentContext.viewport, contentContext.isPositioned]); - - return canScrollDown ? ( - { - const { viewport, selectedItem } = contentContext; - if (viewport && selectedItem) { - viewport.scrollTop = viewport.scrollTop + selectedItem.offsetHeight; + useLayoutEffect(() => { + if (contentContext.viewport && contentContext.isPositioned) { + const viewport = contentContext.viewport; + function handleScroll() { + const maxScroll = viewport.scrollHeight - viewport.clientHeight; + // we use Math.ceil here because if the UI is zoomed-in + // `scrollTop` is not always reported as an integer + const canScrollDown = Math.ceil(viewport.scrollTop) < maxScroll; + setCanScrollDown(canScrollDown); } - }} - /> - ) : null; -}); + handleScroll(); + viewport.addEventListener('scroll', handleScroll); + return () => viewport.removeEventListener('scroll', handleScroll); + } + }, [contentContext.viewport, contentContext.isPositioned]); + + return canScrollDown ? ( + { + const { viewport, selectedItem } = contentContext; + if (viewport && selectedItem) { + const factor = + pointerAction && (pointerAction === 'move' ? hoverScrollFactor : touchScrollFactor); + viewport.scrollTop = + viewport.scrollTop + selectedItem.offsetHeight * Math.abs(factor ?? 1); + } + }} + /> + ) : null; + } +); SelectScrollDownButton.displayName = SCROLL_DOWN_BUTTON_NAME; type SelectScrollButtonImplElement = React.ElementRef; interface SelectScrollButtonImplProps extends PrimitiveDivProps { - onAutoScroll(): void; + onAutoScroll(pointerAction?: 'move' | 'down'): void; } const SelectScrollButtonImpl = React.forwardRef< @@ -1514,13 +1536,13 @@ const SelectScrollButtonImpl = React.forwardRef< style={{ flexShrink: 0, ...scrollIndicatorProps.style }} onPointerDown={composeEventHandlers(scrollIndicatorProps.onPointerDown, () => { if (autoScrollTimerRef.current === null) { - autoScrollTimerRef.current = window.setInterval(onAutoScroll, 50); + autoScrollTimerRef.current = window.setInterval(onAutoScroll, 50, 'down'); } })} onPointerMove={composeEventHandlers(scrollIndicatorProps.onPointerMove, () => { contentContext.onItemLeave?.(); if (autoScrollTimerRef.current === null) { - autoScrollTimerRef.current = window.setInterval(onAutoScroll, 50); + autoScrollTimerRef.current = window.setInterval(onAutoScroll, 50, 'move'); } })} onPointerLeave={composeEventHandlers(scrollIndicatorProps.onPointerLeave, () => {