-
-
Notifications
You must be signed in to change notification settings - Fork 621
fix: sync table scroll by hover source #1495
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -68,6 +68,7 @@ import type { | |
| PanelRender, | ||
| Reference, | ||
| RowClassName, | ||
| ScrollSource, | ||
| TableComponents, | ||
| TableLayout, | ||
| TableSticky, | ||
|
|
@@ -91,6 +92,18 @@ const EMPTY_DATA = []; | |
| // Used for customize scroll | ||
| const EMPTY_SCROLL_TARGET = {}; | ||
|
|
||
| const getScrollSource = ( | ||
| source: ScrollSource | undefined, | ||
| currentTarget: HTMLElement | undefined, | ||
| refs: Record<ScrollSource, HTMLElement | null | undefined>, | ||
| ): ScrollSource | undefined => { | ||
| if (source) { | ||
| return source; | ||
| } | ||
|
|
||
| return (Object.keys(refs) as ScrollSource[]).find(key => refs[key] === currentTarget); | ||
| }; | ||
|
|
||
| export type SemanticName = 'section' | 'title' | 'footer' | 'content'; | ||
| export type ComponentsSemantic = 'wrapper' | 'cell' | 'row'; | ||
|
|
||
|
|
@@ -350,6 +363,13 @@ const Table = <RecordType extends DefaultRecordType>( | |
| const scrollHeaderRef = React.useRef<HTMLDivElement>(null); | ||
| const scrollBodyRef = React.useRef<HTMLDivElement>(null); | ||
| const scrollBodyContainerRef = React.useRef<HTMLDivElement>(null); | ||
| const [hoverEle, setHoverEle] = React.useState<ScrollSource | null>(null); | ||
| const onHoverScrollSource = React.useCallback( | ||
| (source: ScrollSource) => () => { | ||
| setHoverEle(source); | ||
| }, | ||
| [], | ||
| ); | ||
|
Comment on lines
+366
to
+372
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If a user hovers the header (setting To fix this, we should reset |
||
|
|
||
| React.useImperativeHandle(ref, () => { | ||
| return { | ||
|
|
@@ -476,12 +496,32 @@ const Table = <RecordType extends DefaultRecordType>( | |
| const [scrollInfo, setScrollInfo] = React.useState<ScrollInfoType>([0, 0]); | ||
|
|
||
| const onInternalScroll = useEvent( | ||
| ({ currentTarget, scrollLeft }: { currentTarget: HTMLElement; scrollLeft?: number }) => { | ||
| ({ | ||
| currentTarget, | ||
| scrollLeft, | ||
| source, | ||
| }: { | ||
| currentTarget?: HTMLElement; | ||
| scrollLeft?: number; | ||
| source?: ScrollSource; | ||
| }) => { | ||
| const scrollSource = getScrollSource(source, currentTarget, { | ||
| body: getDOM(scrollBodyRef.current) as HTMLElement, | ||
| header: scrollHeaderRef.current, | ||
| summary: scrollSummaryRef.current, | ||
| sticky: undefined, | ||
| }); | ||
|
|
||
| if (source && hoverEle && scrollSource && hoverEle !== scrollSource) { | ||
| return; | ||
| } | ||
|
|
||
| const mergedScrollLeft = | ||
| typeof scrollLeft === 'number' ? scrollLeft : currentTarget.scrollLeft; | ||
| typeof scrollLeft === 'number' ? scrollLeft : (currentTarget?.scrollLeft ?? 0); | ||
|
|
||
| const compareTarget = currentTarget || EMPTY_SCROLL_TARGET; | ||
| if (!getScrollTarget() || getScrollTarget() === compareTarget) { | ||
| const isHoverSource = hoverEle && scrollSource && hoverEle === scrollSource; | ||
| if (isHoverSource || !getScrollTarget() || getScrollTarget() === compareTarget) { | ||
| setScrollTarget(compareTarget); | ||
|
|
||
| forceScroll(mergedScrollLeft, scrollHeaderRef.current); | ||
|
|
@@ -517,8 +557,12 @@ const Table = <RecordType extends DefaultRecordType>( | |
| }, | ||
| ); | ||
|
|
||
| const onBodyInternalScroll = useEvent((e: React.UIEvent<HTMLDivElement>) => { | ||
| onInternalScroll({ currentTarget: e.currentTarget, source: 'body' }); | ||
| }); | ||
|
|
||
| const onBodyScroll = useEvent((e: React.UIEvent<HTMLDivElement>) => { | ||
| onInternalScroll(e); | ||
| onBodyInternalScroll(e); | ||
| onScroll?.(e); | ||
| }); | ||
|
|
||
|
|
@@ -676,6 +720,7 @@ const Table = <RecordType extends DefaultRecordType>( | |
| scrollbarSize, | ||
| ref: scrollBodyRef, | ||
| onScroll: onInternalScroll, | ||
| onMouseEnter: onHoverScrollSource('body'), | ||
| }); | ||
|
|
||
| headerProps.colWidths = flattenColumns.map(({ width }, index) => { | ||
|
|
@@ -701,6 +746,7 @@ const Table = <RecordType extends DefaultRecordType>( | |
| ...scrollYStyle, | ||
| }} | ||
| onScroll={onBodyScroll} | ||
| onMouseEnter={onHoverScrollSource('body')} | ||
| ref={scrollBodyRef} | ||
| className={`${prefixCls}-body`} | ||
| > | ||
|
|
@@ -747,6 +793,8 @@ const Table = <RecordType extends DefaultRecordType>( | |
| className={`${prefixCls}-header`} | ||
| ref={scrollHeaderRef} | ||
| colGroup={bodyColGroup} | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. React Doctor · This child redraws every render because the prop gets brand new JSX each time. Fix → Move the JSX outside the component or wrap it in |
||
| scrollSource="header" | ||
| onMouseEnter={onHoverScrollSource('header')} | ||
| > | ||
| {renderFixedHeaderTable} | ||
| </FixedHolder> | ||
|
|
@@ -763,6 +811,8 @@ const Table = <RecordType extends DefaultRecordType>( | |
| className={`${prefixCls}-summary`} | ||
| ref={scrollSummaryRef} | ||
| colGroup={bodyColGroup} | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. React Doctor · This child redraws every render because the prop gets brand new JSX each time. Fix → Move the JSX outside the component or wrap it in |
||
| scrollSource="summary" | ||
| onMouseEnter={onHoverScrollSource('summary')} | ||
| > | ||
| {renderFixedFooterTable} | ||
| </FixedHolder> | ||
|
|
@@ -774,6 +824,7 @@ const Table = <RecordType extends DefaultRecordType>( | |
| offsetScroll={offsetScroll} | ||
| scrollBodyRef={scrollBodyRef} | ||
| onScroll={onInternalScroll} | ||
| onMouseEnter={onHoverScrollSource('sticky')} | ||
| container={container} | ||
| direction={direction} | ||
| /> | ||
|
|
@@ -786,7 +837,8 @@ const Table = <RecordType extends DefaultRecordType>( | |
| <div | ||
| style={{ ...scrollXStyle, ...scrollYStyle, ...styles?.content }} | ||
| className={clsx(`${prefixCls}-content`, classNames?.content)} | ||
| onScroll={onInternalScroll} | ||
| onScroll={onBodyInternalScroll} | ||
| onMouseEnter={onHoverScrollSource('body')} | ||
| ref={scrollBodyRef} | ||
| > | ||
| <TableComponent | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,7 @@ import { getDOM, getScrollBarSize, raf } from '@rc-component/util'; | |
| import * as React from 'react'; | ||
| import TableContext from './context/TableContext'; | ||
| import { useLayoutState } from './hooks/useFrame'; | ||
| import type { OnCustomizeScroll } from './interface'; | ||
| import { getOffset } from './utils/offsetUtil'; | ||
|
|
||
| const MOUSEUP_EVENT: keyof WindowEventMap = 'mouseup'; | ||
|
|
@@ -13,7 +14,8 @@ const RESIZE_EVENT: keyof WindowEventMap = 'resize'; | |
|
|
||
| interface StickyScrollBarProps { | ||
| scrollBodyRef: React.RefObject<HTMLDivElement>; | ||
| onScroll: (params: { scrollLeft?: number }) => void; | ||
| onScroll: OnCustomizeScroll; | ||
| onMouseEnter?: React.MouseEventHandler<HTMLDivElement>; | ||
| offsetScroll: number; | ||
| container: HTMLElement | Window; | ||
| direction: string; | ||
|
|
@@ -23,7 +25,7 @@ const StickyScrollBar: React.ForwardRefRenderFunction<unknown, StickyScrollBarPr | |
| props, | ||
| ref, | ||
| ) => { | ||
| const { scrollBodyRef, onScroll, offsetScroll, container, direction } = props; | ||
| const { scrollBodyRef, onScroll, onMouseEnter, offsetScroll, container, direction } = props; | ||
| const prefixCls = useContext(TableContext, 'prefixCls'); | ||
| const bodyScrollWidth = scrollBodyRef.current?.scrollWidth || 0; | ||
| const bodyWidth = scrollBodyRef.current?.clientWidth || 0; | ||
|
|
@@ -80,6 +82,7 @@ const StickyScrollBar: React.ForwardRefRenderFunction<unknown, StickyScrollBarPr | |
| if (shouldScroll) { | ||
| onScroll({ | ||
| scrollLeft: (left / bodyWidth) * (bodyScrollWidth + 2), | ||
| source: 'sticky', | ||
| }); | ||
| refState.current.x = event.pageX; | ||
| } | ||
|
|
@@ -190,6 +193,7 @@ const StickyScrollBar: React.ForwardRefRenderFunction<unknown, StickyScrollBarPr | |
| <div | ||
| style={{ height: getScrollBarSize(), width: bodyWidth, bottom: offsetScroll }} | ||
| className={`${prefixCls}-sticky-scroll`} | ||
| onMouseEnter={onMouseEnter} | ||
| > | ||
| <div | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. React Doctor · Screen reader users can't tell this click handler is interactive because it has no Fix → Give clickable static elements a |
||
| onMouseDown={onMouseDown} | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In
getScrollSource, ifcurrentTargetisundefinedornull, and any reference inrefsis alsoundefinedornull(e.g.,refs.stickyis explicitlyundefined, orrefs.summaryisnullwhen no summary is rendered), the comparisonrefs[key] === currentTargetwill evaluate totrue.This will incorrectly resolve the scroll source to that key (e.g.,
'sticky'or'summary'). To prevent this, we should ensurecurrentTargetis truthy before performing the comparison.