From 77a5c078f613245623900c1f97ba50cb7aeee886 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Thu, 29 Dec 2022 15:10:24 +0800 Subject: [PATCH 1/2] chore: hover with hooks --- src/FixedHolder/index.tsx | 256 +++++++++++++++++------------------ src/Table.tsx | 75 +++++----- src/hooks/useHover.ts | 15 ++ src/hooks/useRenderTimes.tsx | 37 +++++ 4 files changed, 214 insertions(+), 169 deletions(-) create mode 100644 src/hooks/useHover.ts create mode 100644 src/hooks/useRenderTimes.tsx diff --git a/src/FixedHolder/index.tsx b/src/FixedHolder/index.tsx index 565c1ae29..3b2871896 100644 --- a/src/FixedHolder/index.tsx +++ b/src/FixedHolder/index.tsx @@ -1,11 +1,12 @@ +import { useContext } from '@rc-component/context'; import classNames from 'classnames'; import { fillRef } from 'rc-util/lib/ref'; import * as React from 'react'; import { useMemo } from 'react'; import ColGroup from '../ColGroup'; import TableContext from '../context/TableContext'; -import { useContext } from '@rc-component/context'; import type { HeaderProps } from '../Header/Header'; +import useRenderTimes from '../hooks/useRenderTimes'; import type { ColumnsType, ColumnType } from '../interface'; function useColumnWidth(colWidths: readonly number[], columCount: number) { @@ -38,138 +39,137 @@ export interface FixedHeaderProps extends HeaderProps { children: (info: HeaderProps) => React.ReactNode; } -const FixedHolder = React.forwardRef>( - ( - { - className, - noData, - columns, - flattenColumns, - colWidths, - columCount, - stickyOffsets, - direction, - fixHeader, - stickyTopOffset, - stickyBottomOffset, - stickyClassName, - onScroll, - maxContentScroll, - children, - ...props - }, - ref, - ) => { - const { prefixCls, scrollbarSize, isSticky } = useContext(TableContext, [ - 'prefixCls', - 'scrollbarSize', - 'isSticky', - ]); - - const combinationScrollBarSize = isSticky && !fixHeader ? 0 : scrollbarSize; - - // Pass wheel to scroll event - const scrollRef = React.useRef(null); - - const setScrollRef = React.useCallback((element: HTMLElement) => { - fillRef(ref, element); - fillRef(scrollRef, element); - }, []); - - React.useEffect(() => { - function onWheel(e: WheelEvent) { - const { currentTarget, deltaX } = e as unknown as React.WheelEvent; - if (deltaX) { - onScroll({ currentTarget, scrollLeft: currentTarget.scrollLeft + deltaX }); - e.preventDefault(); - } +const FixedHolder = React.forwardRef>((props, ref) => { + useRenderTimes(props); + + const { + className, + noData, + columns, + flattenColumns, + colWidths, + columCount, + stickyOffsets, + direction, + fixHeader, + stickyTopOffset, + stickyBottomOffset, + stickyClassName, + onScroll, + maxContentScroll, + children, + ...restProps + } = props; + + const { prefixCls, scrollbarSize, isSticky } = useContext(TableContext, [ + 'prefixCls', + 'scrollbarSize', + 'isSticky', + ]); + + const combinationScrollBarSize = isSticky && !fixHeader ? 0 : scrollbarSize; + + // Pass wheel to scroll event + const scrollRef = React.useRef(null); + + const setScrollRef = React.useCallback((element: HTMLElement) => { + fillRef(ref, element); + fillRef(scrollRef, element); + }, []); + + React.useEffect(() => { + function onWheel(e: WheelEvent) { + const { currentTarget, deltaX } = e as unknown as React.WheelEvent; + if (deltaX) { + onScroll({ currentTarget, scrollLeft: currentTarget.scrollLeft + deltaX }); + e.preventDefault(); } - scrollRef.current?.addEventListener('wheel', onWheel); - - return () => { - scrollRef.current?.removeEventListener('wheel', onWheel); - }; - }, []); - - // Check if all flattenColumns has width - const allFlattenColumnsWithWidth = React.useMemo( - () => flattenColumns.every(column => column.width >= 0), - [flattenColumns], - ); - - // Add scrollbar column - const lastColumn = flattenColumns[flattenColumns.length - 1]; - const ScrollBarColumn: ColumnType & { scrollbar: true } = { - fixed: lastColumn ? lastColumn.fixed : null, - scrollbar: true, - onHeaderCell: () => ({ - className: `${prefixCls}-cell-scrollbar`, - }), - }; + } + scrollRef.current?.addEventListener('wheel', onWheel); - const columnsWithScrollbar = useMemo>( - () => (combinationScrollBarSize ? [...columns, ScrollBarColumn] : columns), - [combinationScrollBarSize, columns], - ); - - const flattenColumnsWithScrollbar = useMemo( - () => (combinationScrollBarSize ? [...flattenColumns, ScrollBarColumn] : flattenColumns), - [combinationScrollBarSize, flattenColumns], - ); - - // Calculate the sticky offsets - const headerStickyOffsets = useMemo(() => { - const { right, left } = stickyOffsets; - return { - ...stickyOffsets, - left: - direction === 'rtl' ? [...left.map(width => width + combinationScrollBarSize), 0] : left, - right: - direction === 'rtl' - ? right - : [...right.map(width => width + combinationScrollBarSize), 0], - isSticky, - }; - }, [combinationScrollBarSize, stickyOffsets, isSticky]); - - const mergedColumnWidth = useColumnWidth(colWidths, columCount); - - return ( -
{ + scrollRef.current?.removeEventListener('wheel', onWheel); + }; + }, []); + + // Check if all flattenColumns has width + const allFlattenColumnsWithWidth = React.useMemo( + () => flattenColumns.every(column => column.width >= 0), + [flattenColumns], + ); + + // Add scrollbar column + const lastColumn = flattenColumns[flattenColumns.length - 1]; + const ScrollBarColumn: ColumnType & { scrollbar: true } = { + fixed: lastColumn ? lastColumn.fixed : null, + scrollbar: true, + onHeaderCell: () => ({ + className: `${prefixCls}-cell-scrollbar`, + }), + }; + + const columnsWithScrollbar = useMemo>( + () => (combinationScrollBarSize ? [...columns, ScrollBarColumn] : columns), + [combinationScrollBarSize, columns], + ); + + const flattenColumnsWithScrollbar = useMemo( + () => (combinationScrollBarSize ? [...flattenColumns, ScrollBarColumn] : flattenColumns), + [combinationScrollBarSize, flattenColumns], + ); + + // Calculate the sticky offsets + const headerStickyOffsets = useMemo(() => { + const { right, left } = stickyOffsets; + return { + ...stickyOffsets, + left: + direction === 'rtl' ? [...left.map(width => width + combinationScrollBarSize), 0] : left, + right: + direction === 'rtl' ? right : [...right.map(width => width + combinationScrollBarSize), 0], + isSticky, + }; + }, [combinationScrollBarSize, stickyOffsets, isSticky]); + + const mergedColumnWidth = useColumnWidth(colWidths, columCount); + + return ( +
+ -
- {(!noData || !maxContentScroll || allFlattenColumnsWithWidth) && ( - - )} - {children({ - ...props, - stickyOffsets: headerStickyOffsets, - columns: columnsWithScrollbar, - flattenColumns: flattenColumnsWithScrollbar, - })} -
-
- ); - }, -); + {(!noData || !maxContentScroll || allFlattenColumnsWithWidth) && ( + + )} + {children({ + ...restProps, + stickyOffsets: headerStickyOffsets, + columns: columnsWithScrollbar, + flattenColumns: flattenColumnsWithScrollbar, + })} + +
+ ); +}); FixedHolder.displayName = 'FixedHolder'; -export default FixedHolder; +/** Return a table in div as fixed element which contains sticky info */ +// export default responseImmutable(FixedHolder); +export default React.memo(FixedHolder); diff --git a/src/Table.tsx b/src/Table.tsx index e8a934007..215675bf4 100644 --- a/src/Table.tsx +++ b/src/Table.tsx @@ -30,6 +30,7 @@ import ResizeObserver from 'rc-resize-observer'; import isVisible from 'rc-util/lib/Dom/isVisible'; import { isStyleSupport } from 'rc-util/lib/Dom/styleChecker'; import { getTargetScrollBarSize } from 'rc-util/lib/getScrollBarSize'; +import useEvent from 'rc-util/lib/hooks/useEvent'; import isEqual from 'rc-util/lib/isEqual'; import pickAttrs from 'rc-util/lib/pickAttrs'; import getValue from 'rc-util/lib/utils/get'; @@ -46,6 +47,7 @@ import Summary from './Footer/Summary'; import Header from './Header/Header'; import useColumns from './hooks/useColumns'; import { useLayoutState, useTimeoutLock } from './hooks/useFrame'; +import useHover from './hooks/useHover'; import useSticky from './hooks/useSticky'; import useStickyOffsets from './hooks/useStickyOffsets'; import type { @@ -259,13 +261,7 @@ function Table(tableProps: TableProps { - setStartRow(start); - setEndRow(end); - }, []); + const [startRow, endRow, onHover] = useHover(); // ====================== Expand ====================== const expandableConfig = getExpandableProps(props); @@ -485,43 +481,40 @@ function Table(tableProps: TableProps { - const isRTL = direction === 'rtl'; - const mergedScrollLeft = typeof scrollLeft === 'number' ? scrollLeft : currentTarget.scrollLeft; - - const compareTarget = currentTarget || EMPTY_SCROLL_TARGET; - if (!getScrollTarget() || getScrollTarget() === compareTarget) { - setScrollTarget(compareTarget); - - forceScroll(mergedScrollLeft, scrollHeaderRef.current); - forceScroll(mergedScrollLeft, scrollBodyRef.current); - forceScroll(mergedScrollLeft, scrollSummaryRef.current); - forceScroll(mergedScrollLeft, stickyRef.current?.setScrollLeft); - } + const onScroll = useEvent( + ({ currentTarget, scrollLeft }: { currentTarget: HTMLElement; scrollLeft?: number }) => { + const isRTL = direction === 'rtl'; + const mergedScrollLeft = + typeof scrollLeft === 'number' ? scrollLeft : currentTarget.scrollLeft; - if (currentTarget) { - const { scrollWidth, clientWidth } = currentTarget; - // There is no space to scroll - if (scrollWidth === clientWidth) { - setPingedLeft(false); - setPingedRight(false); - return; + const compareTarget = currentTarget || EMPTY_SCROLL_TARGET; + if (!getScrollTarget() || getScrollTarget() === compareTarget) { + setScrollTarget(compareTarget); + + forceScroll(mergedScrollLeft, scrollHeaderRef.current); + forceScroll(mergedScrollLeft, scrollBodyRef.current); + forceScroll(mergedScrollLeft, scrollSummaryRef.current); + forceScroll(mergedScrollLeft, stickyRef.current?.setScrollLeft); } - if (isRTL) { - setPingedLeft(-mergedScrollLeft < scrollWidth - clientWidth); - setPingedRight(-mergedScrollLeft > 0); - } else { - setPingedLeft(mergedScrollLeft > 0); - setPingedRight(mergedScrollLeft < scrollWidth - clientWidth); + + if (currentTarget) { + const { scrollWidth, clientWidth } = currentTarget; + // There is no space to scroll + if (scrollWidth === clientWidth) { + setPingedLeft(false); + setPingedRight(false); + return; + } + if (isRTL) { + setPingedLeft(-mergedScrollLeft < scrollWidth - clientWidth); + setPingedRight(-mergedScrollLeft > 0); + } else { + setPingedLeft(mergedScrollLeft > 0); + setPingedRight(mergedScrollLeft < scrollWidth - clientWidth); + } } - } - }; + }, + ); const triggerOnScroll = () => { if (horizonScroll && scrollBodyRef.current) { diff --git a/src/hooks/useHover.ts b/src/hooks/useHover.ts new file mode 100644 index 000000000..f67cb0591 --- /dev/null +++ b/src/hooks/useHover.ts @@ -0,0 +1,15 @@ +import * as React from 'react'; + +export type OnHover = (start: number, end: number) => void; + +export default function useHover(): [startRow: number, endRow: number, onHover: OnHover] { + const [startRow, setStartRow] = React.useState(-1); + const [endRow, setEndRow] = React.useState(-1); + + const onHover = React.useCallback((start, end) => { + setStartRow(start); + setEndRow(end); + }, []); + + return [startRow, endRow, onHover]; +} diff --git a/src/hooks/useRenderTimes.tsx b/src/hooks/useRenderTimes.tsx new file mode 100644 index 000000000..a95a67a23 --- /dev/null +++ b/src/hooks/useRenderTimes.tsx @@ -0,0 +1,37 @@ +/* istanbul ignore file */ +import * as React from 'react'; + +function useRenderTimes(props?: T) { + // Render times + const timesRef = React.useRef(0); + timesRef.current += 1; + + // Props changed + const propsRef = React.useRef(props); + const keys: string[] = []; + Object.keys(props || {}).map(key => { + if (props?.[key] !== propsRef.current?.[key]) { + keys.push(key); + } + }); + propsRef.current = props; + + // Cache keys since React rerender may cause it lost + const keysRef = React.useRef([]); + if (keys.length) { + keysRef.current = keys; + } + + React.useDebugValue(timesRef.current); + React.useDebugValue(keysRef.current.join(', ')); + + return timesRef.current; +} + +export default process.env.NODE_ENV !== 'production' ? useRenderTimes : () => {}; + +export const RenderBlock = React.memo(() => { + const times = useRenderTimes(); + return

Render Times: {times}

; +}); +RenderBlock.displayName = 'RenderBlock'; From f11f0826dbd2dd1d8220d2a305e7c70adbd9c29d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=8C=E8=B4=A7=E6=9C=BA=E5=99=A8=E4=BA=BA?= Date: Thu, 29 Dec 2022 15:40:29 +0800 Subject: [PATCH 2/2] chore: useExpand --- src/Table.tsx | 138 +++++++---------------------------------- src/hooks/useExpand.ts | 127 +++++++++++++++++++++++++++++++++++++ 2 files changed, 150 insertions(+), 115 deletions(-) create mode 100644 src/hooks/useExpand.ts diff --git a/src/Table.tsx b/src/Table.tsx index 215675bf4..3ae608f09 100644 --- a/src/Table.tsx +++ b/src/Table.tsx @@ -46,6 +46,7 @@ import type { SummaryProps } from './Footer/Summary'; import Summary from './Footer/Summary'; import Header from './Header/Header'; import useColumns from './hooks/useColumns'; +import useExpand from './hooks/useExpand'; import { useLayoutState, useTimeoutLock } from './hooks/useFrame'; import useHover from './hooks/useHover'; import useSticky from './hooks/useSticky'; @@ -56,26 +57,21 @@ import type { CustomizeScrollBody, DefaultRecordType, ExpandableConfig, - ExpandableType, GetComponent, GetComponentProps, GetRowKey, - Key, LegacyExpandableProps, PanelRender, RowClassName, TableComponents, TableLayout, TableSticky, - TriggerEventHandler, } from './interface'; import Panel from './Panel'; import StickyScrollBar from './stickyScrollBar'; import Column from './sugar/Column'; import ColumnGroup from './sugar/ColumnGroup'; -import { findAllChildrenKeys, renderExpandIcon } from './utils/expandUtil'; import { getCellFixedInfo } from './utils/fixUtil'; -import { getExpandableProps } from './utils/legacyUtil'; import { getColumnsKey, validateValue } from './utils/valueUtil'; // Used for conditions cache @@ -264,102 +260,14 @@ function Table(tableProps: TableProps(() => { - if (expandedRowRender) { - return 'row'; - } - /* eslint-disable no-underscore-dangle */ - /** - * Fix https://github.com/ant-design/ant-design/issues/21154 - * This is a workaround to not to break current behavior. - * We can remove follow code after final release. - * - * To other developer: - * Do not use `__PARENT_RENDER_ICON__` in prod since we will remove this when refactor - */ - if ( - (props.expandable && - internalHooks === INTERNAL_HOOKS && - (props.expandable as any).__PARENT_RENDER_ICON__) || - mergedData.some( - record => record && typeof record === 'object' && record[mergedChildrenColumnName], - ) - ) { - return 'nest'; - } - /* eslint-enable */ - return false; - }, [!!expandedRowRender, mergedData]); - - const [innerExpandedKeys, setInnerExpandedKeys] = React.useState(() => { - if (defaultExpandedRowKeys) { - return defaultExpandedRowKeys; - } - if (defaultExpandAllRows) { - return findAllChildrenKeys(mergedData, getRowKey, mergedChildrenColumnName); - } - return []; - }); - const mergedExpandedKeys = React.useMemo( - () => new Set(expandedRowKeys || innerExpandedKeys || []), - [expandedRowKeys, innerExpandedKeys], - ); - - const onTriggerExpand: TriggerEventHandler = React.useCallback( - (record: RecordType) => { - const key = getRowKey(record, mergedData.indexOf(record)); - - let newExpandedKeys: Key[]; - const hasKey = mergedExpandedKeys.has(key); - if (hasKey) { - mergedExpandedKeys.delete(key); - newExpandedKeys = [...mergedExpandedKeys]; - } else { - newExpandedKeys = [...mergedExpandedKeys, key]; - } - - setInnerExpandedKeys(newExpandedKeys); - if (onExpand) { - onExpand(!hasKey, record); - } - if (onExpandedRowsChange) { - onExpandedRowsChange(newExpandedKeys); - } - }, - [getRowKey, mergedExpandedKeys, mergedData, onExpand, onExpandedRowsChange], - ); - - // Warning if use `expandedRowRender` and nest children in the same time - if ( - process.env.NODE_ENV !== 'production' && - expandedRowRender && - mergedData.some((record: RecordType) => { - return Array.isArray(record?.[mergedChildrenColumnName]); - }) - ) { - warning(false, '`expandedRowRender` should not use with nested Table'); - } + const [ + expandableConfig, + expandableType, + mergedExpandedKeys, + mergedExpandIcon, + mergedChildrenColumnName, + onTriggerExpand, + ] = useExpand(props, mergedData, getRowKey); // ====================== Column ====================== const [componentWidth, setComponentWidth] = React.useState(0); @@ -368,14 +276,14 @@ function Table(tableProps: TableProps(tableProps: TableProps(tableProps: TableProps col.fixed === 'left'), // Column @@ -885,14 +793,14 @@ function Table(tableProps: TableProps( + props: TableProps, + mergedData: readonly RecordType[], + getRowKey: GetRowKey, +): [ + expandableConfig: ExpandableConfig, + expandableType: ExpandableType, + expandedKeys: Set, + expandIcon: RenderExpandIcon, + childrenColumnName: string, + onTriggerExpand: TriggerEventHandler, +] { + const expandableConfig = getExpandableProps(props); + + const { + expandIcon, + expandedRowKeys, + defaultExpandedRowKeys, + defaultExpandAllRows, + expandedRowRender, + onExpand, + onExpandedRowsChange, + childrenColumnName, + } = expandableConfig; + + const mergedExpandIcon = expandIcon || renderExpandIcon; + const mergedChildrenColumnName = childrenColumnName || 'children'; + const expandableType = React.useMemo(() => { + if (expandedRowRender) { + return 'row'; + } + /* eslint-disable no-underscore-dangle */ + /** + * Fix https://github.com/ant-design/ant-design/issues/21154 + * This is a workaround to not to break current behavior. + * We can remove follow code after final release. + * + * To other developer: + * Do not use `__PARENT_RENDER_ICON__` in prod since we will remove this when refactor + */ + if ( + (props.expandable && + props.internalHooks === INTERNAL_HOOKS && + (props.expandable as any).__PARENT_RENDER_ICON__) || + mergedData.some( + record => record && typeof record === 'object' && record[mergedChildrenColumnName], + ) + ) { + return 'nest'; + } + /* eslint-enable */ + return false; + }, [!!expandedRowRender, mergedData]); + + const [innerExpandedKeys, setInnerExpandedKeys] = React.useState(() => { + if (defaultExpandedRowKeys) { + return defaultExpandedRowKeys; + } + if (defaultExpandAllRows) { + return findAllChildrenKeys(mergedData, getRowKey, mergedChildrenColumnName); + } + return []; + }); + const mergedExpandedKeys = React.useMemo( + () => new Set(expandedRowKeys || innerExpandedKeys || []), + [expandedRowKeys, innerExpandedKeys], + ); + + const onTriggerExpand: TriggerEventHandler = React.useCallback( + (record: RecordType) => { + const key = getRowKey(record, mergedData.indexOf(record)); + + let newExpandedKeys: Key[]; + const hasKey = mergedExpandedKeys.has(key); + if (hasKey) { + mergedExpandedKeys.delete(key); + newExpandedKeys = [...mergedExpandedKeys]; + } else { + newExpandedKeys = [...mergedExpandedKeys, key]; + } + + setInnerExpandedKeys(newExpandedKeys); + if (onExpand) { + onExpand(!hasKey, record); + } + if (onExpandedRowsChange) { + onExpandedRowsChange(newExpandedKeys); + } + }, + [getRowKey, mergedExpandedKeys, mergedData, onExpand, onExpandedRowsChange], + ); + + // Warning if use `expandedRowRender` and nest children in the same time + if ( + process.env.NODE_ENV !== 'production' && + expandedRowRender && + mergedData.some((record: RecordType) => { + return Array.isArray(record?.[mergedChildrenColumnName]); + }) + ) { + warning(false, '`expandedRowRender` should not use with nested Table'); + } + + return [ + expandableConfig, + expandableType, + mergedExpandedKeys, + mergedExpandIcon, + mergedChildrenColumnName, + onTriggerExpand, + ]; +}