Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 46 additions & 14 deletions packages/components/table/BaseTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,9 @@ const BaseTable = forwardRef<BaseTableRef, BaseTableProps>((originalProps, ref)
allTableClasses;
// 表格基础样式类
const { tableClasses, sizeClassNames, tableContentStyles, tableElementStyles } = useStyle(props);
const { isMultipleHeader, spansAndLeafNodes, thList } = useTableHeader({ columns: props.columns });
const { isMultipleHeader, spansAndLeafNodes, thList } = useTableHeader({
columns: props.columns,
});
const finalColumns = useMemo(
() => spansAndLeafNodes?.leafColumns || columns,
[spansAndLeafNodes?.leafColumns, columns],
Expand Down Expand Up @@ -365,6 +367,10 @@ const BaseTable = forwardRef<BaseTableRef, BaseTableProps>((originalProps, ref)
const headUseMemoDependencies = [
resizable,
thWidthList,
tableWidth,
virtualConfig.isVirtualScroll,
props.headerAffixedTop,
props.scroll?.type,
isFixedHeader,
rowAndColFixedPosition,
isMultipleHeader,
Expand Down Expand Up @@ -394,20 +400,26 @@ const BaseTable = forwardRef<BaseTableRef, BaseTableProps>((originalProps, ref)
height: `${affixHeaderWrapHeight}px`,
opacity: headerOpacity,
};
const affixedHeader = Boolean((headerAffixedTop || virtualConfig.isVirtualScroll) && tableWidth) && (
// 配置了虚拟滚动时,无论数据是否超过 threshold,吸顶表头都需常驻
// 避免过滤导致行数跨越阈值时表头与筛选浮层被整体卸载
const isVirtual = virtualConfig.isVirtualScroll || Boolean(props.scroll && props.scroll.type === 'virtual');
const affixedHeader = Boolean((headerAffixedTop || isVirtual) && tableWidth) && (
<div
ref={onAffixHeaderMount}
style={{ width: `${tableWidth}px`, opacity: headerOpacity }}
className={classNames([
'scrollbar',
{
[tableBaseClass.affixedHeaderElm]: props.headerAffixedTop || virtualConfig.isVirtualScroll,
[tableBaseClass.affixedHeaderElm]: props.headerAffixedTop || isVirtual,
},
])}
>
<table
className={classNames(tableElmClasses)}
style={{ ...tableElementStyles, width: tableElmWidth ? `${tableElmWidth}px` : undefined }}
style={{
...tableElementStyles,
width: tableElmWidth ? `${tableElmWidth}px` : undefined,
}}
>
{renderColGroup(true)}
{showHeader && <THead {...headProps} />}
Expand All @@ -427,8 +439,9 @@ const BaseTable = forwardRef<BaseTableRef, BaseTableProps>((originalProps, ref)

const renderAffixedHeader = () => {
if (!showHeader) return null;
const isVirtual = virtualConfig.isVirtualScroll || Boolean(props.scroll && props.scroll.type === 'virtual');
return (
!!(virtualConfig.isVirtualScroll || props.headerAffixedTop) &&
!!(isVirtual || props.headerAffixedTop) &&
(props.headerAffixedTop ? (
<Affix
offsetTop={0}
Expand Down Expand Up @@ -468,15 +481,23 @@ const BaseTable = forwardRef<BaseTableRef, BaseTableProps>((originalProps, ref)
>
<div
ref={affixFooterRef}
style={{ width: `${tableWidth}px`, opacity: Number(showAffixFooter) }}
style={{
width: `${tableWidth}px`,
opacity: Number(showAffixFooter),
}}
className={classNames([
'scrollbar',
{ [tableBaseClass.affixedFooterElm]: props.footerAffixedBottom || virtualConfig.isVirtualScroll },
{
[tableBaseClass.affixedFooterElm]: props.footerAffixedBottom || virtualConfig.isVirtualScroll,
},
])}
>
<table
className={tableElmClasses}
style={{ ...tableElementStyles, width: tableElmWidth ? `${tableElmWidth}px` : undefined }}
style={{
...tableElementStyles,
width: tableElmWidth ? `${tableElmWidth}px` : undefined,
}}
>
{renderColGroup(true)}
<TFoot
Expand Down Expand Up @@ -545,16 +566,27 @@ const BaseTable = forwardRef<BaseTableRef, BaseTableProps>((originalProps, ref)
{renderColGroup(false)}
{useMemo(() => {
if (!showHeader) return null;
return <THead {...{ ...headProps, thWidthList: resizable ? thWidthList.current : {} }} />;
const isVirtual = virtualConfig.isVirtualScroll || Boolean(props.scroll && props.scroll.type === 'virtual');
// 存在吸顶表头时,内层表头仅用于撑开列宽,不渲染筛选等交互组件,避免双实例竞争
const hasAffixedHeader = Boolean((props.headerAffixedTop || isVirtual) && tableWidth);
return (
<THead
{...{
...headProps,
thWidthList: resizable ? thWidthList.current : {},
}}
isInnerHeader={hasAffixedHeader}
/>
);
// eslint-disable-next-line
}, headUseMemoDependencies)}
}, headUseMemoDependencies)}

{useMemo(
() => (
<TBody {...tableBodyProps} />
),
// eslint-disable-next-line
[
[
allTableClasses,
tableBodyProps.ellipsisOverlayClassName,
tableBodyProps.rowAndColFixedPosition,
Expand Down Expand Up @@ -597,7 +629,7 @@ const BaseTable = forwardRef<BaseTableRef, BaseTableProps>((originalProps, ref)
></TFoot>
),
// eslint-disable-next-line
[
[
isFixedHeader,
rowAndColFixedPosition,
spansAndLeafNodes,
Expand Down Expand Up @@ -643,9 +675,9 @@ const BaseTable = forwardRef<BaseTableRef, BaseTableProps>((originalProps, ref)
const affixedHeaderContent = useMemo(
renderAffixedHeader,
// eslint-disable-next-line
[
[
// eslint-disable-next-line
...headUseMemoDependencies,
...headUseMemoDependencies,
showAffixHeader,
tableWidth,
tableElmWidth,
Expand Down
10 changes: 7 additions & 3 deletions packages/components/table/FilterController.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ function TableFilterController(props: TableFilterControllerProps) {
const [locale, t] = useLocaleReceiver('table');

const triggerElementRef = useRef<HTMLDivElement>(null);
const [filterPopupVisible, setFilterPopupVisible] = useState(visible);
const [filterPopupVisible, setFilterPopupVisible] = useState(!!visible);

const onFilterVisibleChange = (visible: boolean, ctx: PopupVisibleChangeContext) => {
const isDocClick = ctx?.trigger === 'document' && ctx?.e?.target;
Expand All @@ -64,7 +64,7 @@ function TableFilterController(props: TableFilterControllerProps) {
* 过滤后的数据量在跨越虚拟滚动的 threshold 时
* 表头重建导致原始点击的元素被误判为不属于 Popup 内部从而触发关闭
*/
const isInsideFilter = el.closest(`.${tableFilterClasses.popupContent}`) !== null;
const isInsideFilter = !el?.isConnected || el.closest(`.${tableFilterClasses.popupContent}`) !== null;
if (isInsideFilter) {
setFilterPopupVisible(true);
props.onVisibleChange?.(true, column.colKey);
Expand Down Expand Up @@ -159,6 +159,7 @@ function TableFilterController(props: TableFilterControllerProps) {
const isObjectTrue = typeof filterValue === 'object' && !isEmpty(filterValue);
// false is a valid filter value
const isValueExist = ![null, undefined, ''].includes(filterValue) && typeof filterValue !== 'object';

return (
<div className={classNames([tableFilterClasses.icon, { [isFocusClass]: isObjectTrue || isValueExist }])}>
<Popup
Expand All @@ -179,7 +180,10 @@ function TableFilterController(props: TableFilterControllerProps) {
{...props.popupProps}
>
<div ref={triggerElementRef}>
{parseContentTNode(props.filterIcon, { col: column, colIndex: props.colIndex }) || defaultFilterIcon}
{parseContentTNode(props.filterIcon, {
col: column,
colIndex: props.colIndex,
}) || defaultFilterIcon}
</div>
</Popup>
</div>
Expand Down
11 changes: 8 additions & 3 deletions packages/components/table/PrimaryTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import type { CheckboxGroupValue } from '../checkbox';
import type { StyledProps } from '../common';
import type { PageInfo, PaginationProps } from '../pagination';
import type { EditableCellProps } from './EditableCell';
import type { RenderTitleParams } from './hooks/useTableHeader';
import type { BaseTableProps, PrimaryTableProps, PrimaryTableRef } from './interface';
import type { PrimaryTableCellParams, PrimaryTableCol, TableRowData, TdPrimaryTableProps } from './type';

Expand Down Expand Up @@ -104,7 +105,9 @@ const PrimaryTable = forwardRef<PrimaryTableRef, InternalPrimaryTableProps>((ori
const tRowAttributes = (() => {
const tAttributes = [props.rowAttributes];
if (isRowHandlerDraggable || isRowDraggable) {
tAttributes.push(({ row }) => ({ 'data-id': get(row, props.rowKey || 'id') }));
tAttributes.push(({ row }) => ({
'data-id': get(row, props.rowKey || 'id'),
}));
}
return tAttributes.filter((v) => v);
})();
Expand Down Expand Up @@ -146,9 +149,11 @@ const PrimaryTable = forwardRef<PrimaryTableRef, InternalPrimaryTableProps>((ori
if (item.sorter || item.filter) {
const titleContent = renderTitle(item, i);
const { ellipsisTitle } = item;
item.title = (p) => {
item.title = (p: RenderTitleParams) => {
const isInnerHeader = Boolean(p && p.isInnerHeader);
const sortIcon = item.sorter ? renderSortIcon(p) : null;
const filterIcon = item.filter ? renderFilterIcon(p) : null;
// 内层占位表头仅用于撑开列宽,不重复创建筛选浮层实例
const filterIcon = item.filter && !isInnerHeader ? renderFilterIcon(p) : null;
const attach = primaryTableRef.current?.tableContentRef;
return renderTitleWidthIcon([titleContent, sortIcon, filterIcon], p.col, p.colIndex, ellipsisTitle, attach, {
classPrefix,
Expand Down
14 changes: 11 additions & 3 deletions packages/components/table/THead.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ export interface TheadProps {
thList: BaseTableCol<TableRowData>[][];
resizable?: boolean;
attach?: AttachNode;
// 是否为撑开列宽的内层占位表头(存在吸顶表头时),内层不渲染交互型组件
isInnerHeader?: boolean;
showColumnShadow?: { left: boolean; right: boolean };
columnResizeParams?: {
resizeLineRef: MutableRefObject<HTMLDivElement>;
Expand Down Expand Up @@ -117,7 +119,10 @@ export default function THead(props: TheadProps) {
const isLeftFixedActive = showColumnShadow.left && col.fixed === 'left';
const isRightFixedActive = showColumnShadow.right && col.fixed === 'right';
const canDragSort = props.thDraggable && !(isLeftFixedActive || isRightFixedActive);
const customClasses = formatClassNames(col.className, { ...colParams, type: 'th' });
const customClasses = formatClassNames(col.className, {
...colParams,
type: 'th',
});
const thCustomClasses = formatClassNames(col.thClassName, colParams);
const thClasses = [
thStyles.classes,
Expand All @@ -135,7 +140,7 @@ export default function THead(props: TheadProps) {
const withoutChildren = !col.children?.length;
const width = withoutChildren && thWidthList?.[col.colKey] ? `${thWidthList?.[col.colKey]}px` : undefined;
const styles = { ...(thStyles.style || {}), width };
const innerTh = renderTitle(col, index);
const innerTh = renderTitle(col, index, props.isInnerHeader);
if (!col.colKey) return null;
const resizeColumnListener =
props.resizable || !canDragSort
Expand Down Expand Up @@ -169,7 +174,10 @@ export default function THead(props: TheadProps) {
data-colkey={col.colKey}
className={classNames(thClasses)}
style={styles}
{...{ rowSpan: rowspanAndColspan.rowspan, colSpan: rowspanAndColspan.colspan }}
{...{
rowSpan: rowspanAndColspan.rowspan,
colSpan: rowspanAndColspan.colspan,
}}
{...attrs}
{...resizeColumnListener}
>
Expand Down
31 changes: 24 additions & 7 deletions packages/components/table/hooks/useFixed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,10 @@ export default function useFixed(
right: false,
});
// 虚拟滚动无法使用 CSS sticky 固定表头
const [virtualScrollHeaderPos, setVirtualScrollHeaderPos] = useState<{ left: number; top: number }>({
const [virtualScrollHeaderPos, setVirtualScrollHeaderPos] = useState<{
left: number;
top: number;
}>({
left: 0,
top: 0,
});
Expand Down Expand Up @@ -239,7 +242,10 @@ export default function useFixed(
}
const obj = initialColumnMap.get(colKey || j);
if (obj?.col?.fixed) {
initialColumnMap.set(colKey, { ...obj, width: th?.getBoundingClientRect?.().width });
initialColumnMap.set(colKey, {
...obj,
width: th?.getBoundingClientRect?.().width,
});
}
}
}
Expand Down Expand Up @@ -267,7 +273,10 @@ export default function useFixed(
defaultBottom = thead?.getBoundingClientRect?.().height || 0;
}
thisRowInfo.top = (lastRowInfo.top || defaultBottom) + (lastRowInfo.height || 0);
initialColumnMap.set(rowId, { ...thisRowInfo, height: tr?.getBoundingClientRect?.().height });
initialColumnMap.set(rowId, {
...thisRowInfo,
height: tr?.getBoundingClientRect?.().height,
});
}
for (let i = data.length - 1; i >= data.length - fixedBottomRows; i--) {
/**
Expand All @@ -285,7 +294,10 @@ export default function useFixed(
defaultBottom = tfoot?.getBoundingClientRect?.().height || 0;
}
thisRowInfo.bottom = (lastRowInfo.bottom || defaultBottom) + (lastRowInfo.height || 0);
initialColumnMap.set(rowId, { ...thisRowInfo, height: tr?.getBoundingClientRect?.().height });
initialColumnMap.set(rowId, {
...thisRowInfo,
height: tr?.getBoundingClientRect?.().height,
});
}
};

Expand Down Expand Up @@ -354,15 +366,18 @@ export default function useFixed(
const tRef = tableContentRef?.current;
if (!tRef) return;

const isHeightOverflow = tRef.scrollHeight > tRef.clientHeight;
// 虚拟滚动表头需要常驻,不受数据行数是否超过容器高度的影响
// 否则过滤后行数跨越 threshold 时,吸顶表头连同筛选浮层会被整体卸载
const isVirtual = props.scroll?.type === 'virtual';
const isHeightOverflow = isVirtual || tRef.scrollHeight > tRef.clientHeight;
setIsFixedHeader(isHeightOverflow);
setIsWidthOverflow(tRef.scrollWidth > tRef.clientWidth);
const pos = tRef?.getBoundingClientRect?.();
setVirtualScrollHeaderPos({
top: pos?.top,
left: pos?.left,
});
}, []);
}, [props.scroll?.type]);

const setTableElmWidth = (width: number) => {
if (tableElmWidth === width) return;
Expand Down Expand Up @@ -523,7 +538,9 @@ export default function useFixed(

if (isFixedColumn || isFixedHeader) {
updateFixedStatus();
updateColumnFixedShadow(tableContentRef.current, { skipScrollLimit: true });
updateColumnFixedShadow(tableContentRef.current, {
skipScrollLimit: true,
});
}
};

Expand Down
10 changes: 8 additions & 2 deletions packages/components/table/hooks/useTableHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,15 @@ import type { ReactNode } from 'react';
import type { BaseTableCol, PrimaryTableCol, TableRowData, TdBaseTableProps } from '../type';
import type { TableColumns } from './useMultiHeader';

export interface RenderTitleParams {
col: TableColumns[0];
colIndex: number;
isInnerHeader?: boolean;
}

// 渲染表头的通用方法
export function renderTitle(col: TableColumns[0], index: number) {
const params = { col, colIndex: index };
export function renderTitle(col: TableColumns[0], index: number, isInnerHeader?: boolean) {
const params: RenderTitleParams = { col, colIndex: index, isInnerHeader };
if (isFunction(col.title)) {
return col.title(params);
}
Expand Down
Loading
Loading