Skip to content

Commit 2d0d403

Browse files
authored
feat: row hover style (#686)
* feat: row hover style * fix: TS definition * test: fix test case
1 parent a9beafc commit 2d0d403

File tree

7 files changed

+137
-41
lines changed

7 files changed

+137
-41
lines changed

assets/index.less

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,10 @@
131131
}
132132
}
133133
}
134+
135+
&&-row-hover {
136+
background: rgba(255, 0, 0, 0.05);
137+
}
134138
}
135139

136140
&-ping-left {

docs/examples/colspan-rowspan.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,10 @@ const columns: ColumnsType<RecordType> = [
9191
obj.props.rowSpan = 0;
9292
}
9393

94+
if (index === 5) {
95+
obj.props.colSpan = 0;
96+
}
97+
9498
return obj;
9599
},
96100
},

src/Body/BodyRow.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,7 @@ function BodyRow<RecordType extends { children?: readonly RecordType[] }>(
9999
onInternalTriggerExpand(record, event);
100100
}
101101

102-
if (additionalProps && additionalProps.onClick) {
103-
additionalProps.onClick(event, ...args);
104-
}
102+
additionalProps?.onClick(event, ...args);
105103
};
106104

107105
// ======================== Base tr row ========================

src/Body/index.tsx

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import ResizeContext from '../context/ResizeContext';
88
import MeasureCell from './MeasureCell';
99
import BodyRow from './BodyRow';
1010
import useFlattenRecords from '../hooks/useFlattenRecords';
11+
import HoverContext from '../context/HoverContext';
1112

1213
export interface BodyProps<RecordType> {
1314
data: readonly RecordType[];
@@ -30,6 +31,8 @@ function Body<RecordType>({
3031
emptyNode,
3132
childrenColumnName,
3233
}: BodyProps<RecordType>) {
34+
const [startRow, setStartRow] = React.useState(-1);
35+
const [endRow, setEndRow] = React.useState(-1);
3336
const { onColumnResize } = React.useContext(ResizeContext);
3437
const { prefixCls, getComponent } = React.useContext(TableContext);
3538
const { fixHeader, horizonScroll, flattenColumns, componentWidth } =
@@ -42,6 +45,16 @@ function Body<RecordType>({
4245
getRowKey,
4346
);
4447

48+
const onHover = React.useCallback((start: number, end: number) => {
49+
setStartRow(start);
50+
setEndRow(end);
51+
}, []);
52+
53+
const hoverContext = React.useMemo(
54+
() => ({ startRow, endRow, onHover }),
55+
[onHover, startRow, endRow],
56+
);
57+
4558
return React.useMemo(() => {
4659
const WrapperComponent = getComponent(['body', 'wrapper'], 'tbody');
4760
const trComponent = getComponent(['body', 'row'], 'tr');
@@ -94,22 +107,28 @@ function Body<RecordType>({
94107
const columnsKey = getColumnsKey(flattenColumns);
95108

96109
return (
97-
<WrapperComponent className={`${prefixCls}-tbody`}>
98-
{/* Measure body column width with additional hidden col */}
99-
{measureColumnWidth && (
100-
<tr
101-
aria-hidden="true"
102-
className={`${prefixCls}-measure-row`}
103-
style={{ height: 0, fontSize: 0 }}
104-
>
105-
{columnsKey.map(columnKey => (
106-
<MeasureCell key={columnKey} columnKey={columnKey} onColumnResize={onColumnResize} />
107-
))}
108-
</tr>
109-
)}
110+
<HoverContext.Provider value={hoverContext}>
111+
<WrapperComponent className={`${prefixCls}-tbody`}>
112+
{/* Measure body column width with additional hidden col */}
113+
{measureColumnWidth && (
114+
<tr
115+
aria-hidden="true"
116+
className={`${prefixCls}-measure-row`}
117+
style={{ height: 0, fontSize: 0 }}
118+
>
119+
{columnsKey.map(columnKey => (
120+
<MeasureCell
121+
key={columnKey}
122+
columnKey={columnKey}
123+
onColumnResize={onColumnResize}
124+
/>
125+
))}
126+
</tr>
127+
)}
110128

111-
{rows}
112-
</WrapperComponent>
129+
{rows}
130+
</WrapperComponent>
131+
</HoverContext.Provider>
113132
);
114133
}, [
115134
data,
@@ -128,6 +147,7 @@ function Body<RecordType>({
128147
onColumnResize,
129148
rowExpandable,
130149
flattenData,
150+
hoverContext,
131151
]);
132152
}
133153

src/Cell/index.tsx

Lines changed: 75 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,14 @@ import type {
1313
} from '../interface';
1414
import { getPathValue, validateValue } from '../utils/valueUtil';
1515
import StickyContext from '../context/StickyContext';
16+
import HoverContext from '../context/HoverContext';
17+
import type { HoverContextProps } from '../context/HoverContext';
18+
19+
/** Check if cell is in hover range */
20+
function inHoverRange(cellStartRow: number, cellRowSpan: number, startRow: number, endRow: number) {
21+
const cellEndRow = cellStartRow + cellRowSpan - 1;
22+
return cellStartRow <= endRow && cellEndRow >= startRow;
23+
}
1624

1725
function isRenderCell<RecordType>(
1826
data: React.ReactNode | RenderedCell<RecordType>,
@@ -28,7 +36,7 @@ function isRefComponent(component: CustomizeComponent) {
2836
return supportRef(component);
2937
}
3038

31-
export interface CellProps<RecordType extends DefaultRecordType> {
39+
interface InternalCellProps<RecordType extends DefaultRecordType> extends HoverContextProps {
3240
prefixCls?: string;
3341
className?: string;
3442
record?: RecordType;
@@ -53,7 +61,7 @@ export interface CellProps<RecordType extends DefaultRecordType> {
5361
firstFixRight?: boolean;
5462
lastFixRight?: boolean;
5563

56-
// Additional
64+
// ====================== Private Props ======================
5765
/** @private Used for `expandable` with nest tree */
5866
appendNode?: React.ReactNode;
5967
additionalProps?: React.HTMLAttributes<HTMLElement>;
@@ -65,6 +73,11 @@ export interface CellProps<RecordType extends DefaultRecordType> {
6573
isSticky?: boolean;
6674
}
6775

76+
export type CellProps<RecordType extends DefaultRecordType> = Omit<
77+
InternalCellProps<RecordType>,
78+
keyof HoverContextProps
79+
>;
80+
6881
function Cell<RecordType extends DefaultRecordType>(
6982
{
7083
prefixCls,
@@ -76,7 +89,7 @@ function Cell<RecordType extends DefaultRecordType>(
7689
children,
7790
component: Component = 'td',
7891
colSpan,
79-
rowSpan,
92+
rowSpan, // This is already merged on WrapperCell
8093
fixLeft,
8194
fixRight,
8295
firstFixLeft,
@@ -89,7 +102,12 @@ function Cell<RecordType extends DefaultRecordType>(
89102
align,
90103
rowType,
91104
isSticky,
92-
}: CellProps<RecordType>,
105+
106+
// Hover
107+
startRow,
108+
endRow,
109+
onHover,
110+
}: InternalCellProps<RecordType>,
93111
ref: React.Ref<any>,
94112
): React.ReactElement {
95113
const cellPrefixCls = `${prefixCls}-cell`;
@@ -139,8 +157,8 @@ function Cell<RecordType extends DefaultRecordType>(
139157
className: cellClassName,
140158
...restCellProps
141159
} = cellProps || {};
142-
const mergedColSpan = cellColSpan !== undefined ? cellColSpan : colSpan;
143-
const mergedRowSpan = cellRowSpan !== undefined ? cellRowSpan : rowSpan;
160+
const mergedColSpan = (cellColSpan !== undefined ? cellColSpan : colSpan) ?? 1;
161+
const mergedRowSpan = (cellRowSpan !== undefined ? cellRowSpan : rowSpan) ?? 1;
144162

145163
if (mergedColSpan === 0 || mergedRowSpan === 0) {
146164
return null;
@@ -167,6 +185,25 @@ function Cell<RecordType extends DefaultRecordType>(
167185
alignStyle.textAlign = align;
168186
}
169187

188+
// ====================== Hover =======================
189+
const hovering = inHoverRange(index, mergedRowSpan, startRow, endRow);
190+
191+
const onMouseEnter: React.MouseEventHandler<HTMLElement> = event => {
192+
if (record) {
193+
onHover(index, index + mergedRowSpan - 1);
194+
}
195+
196+
additionalProps?.onMouseEnter?.(event);
197+
};
198+
199+
const onMouseLeave: React.MouseEventHandler<HTMLElement> = event => {
200+
if (record) {
201+
onHover(-1, -1);
202+
}
203+
204+
additionalProps?.onMouseLeave?.(event);
205+
};
206+
170207
// ====================== Render ======================
171208
let title: string;
172209
const ellipsisConfig: CellEllipsisType = ellipsis === true ? { showTitle: true } : ellipsis;
@@ -178,12 +215,14 @@ function Cell<RecordType extends DefaultRecordType>(
178215
}
179216
}
180217

181-
const componentProps = {
218+
const componentProps: React.TdHTMLAttributes<HTMLTableCellElement> & {
219+
ref: React.Ref<any>;
220+
} = {
182221
title,
183222
...restCellProps,
184223
...additionalProps,
185-
colSpan: mergedColSpan && mergedColSpan !== 1 ? mergedColSpan : null,
186-
rowSpan: mergedRowSpan && mergedRowSpan !== 1 ? mergedRowSpan : null,
224+
colSpan: mergedColSpan !== 1 ? mergedColSpan : null,
225+
rowSpan: mergedRowSpan !== 1 ? mergedRowSpan : null,
187226
className: classNames(
188227
cellPrefixCls,
189228
className,
@@ -197,11 +236,14 @@ function Cell<RecordType extends DefaultRecordType>(
197236
[`${cellPrefixCls}-ellipsis`]: ellipsis,
198237
[`${cellPrefixCls}-with-append`]: appendNode,
199238
[`${cellPrefixCls}-fix-sticky`]: (isFixLeft || isFixRight) && isSticky && supportSticky,
239+
[`${cellPrefixCls}-row-hover`]: hovering,
200240
},
201241
additionalProps.className,
202242
cellClassName,
203243
),
204244
style: { ...additionalProps.style, ...alignStyle, ...fixedStyle, ...cellStyle },
245+
onMouseEnter,
246+
onMouseLeave,
205247
ref: isRefComponent(Component) ? ref : null,
206248
};
207249

@@ -213,22 +255,33 @@ function Cell<RecordType extends DefaultRecordType>(
213255
);
214256
}
215257

216-
const RefCell = React.forwardRef<any, CellProps<any>>(Cell);
258+
const RefCell = React.forwardRef<any, InternalCellProps<any>>(Cell);
217259
RefCell.displayName = 'Cell';
218260

219-
const comparePropList: (keyof CellProps<any>)[] = ['expanded', 'className'];
261+
const comparePropList: (keyof InternalCellProps<any>)[] = ['expanded', 'className'];
220262

221-
const MemoCell = React.memo(RefCell, (prev: CellProps<any>, next: CellProps<any>) => {
222-
if (next.shouldCellUpdate) {
223-
return (
224-
// Additional handle of expanded logic
225-
comparePropList.every(propName => prev[propName] === next[propName]) &&
226-
// User control update logic
227-
!next.shouldCellUpdate(next.record, prev.record)
228-
);
229-
}
263+
const MemoCell = React.memo(
264+
RefCell,
265+
(prev: InternalCellProps<any>, next: InternalCellProps<any>) => {
266+
if (next.shouldCellUpdate) {
267+
return (
268+
// Additional handle of expanded logic
269+
comparePropList.every(propName => prev[propName] === next[propName]) &&
270+
// User control update logic
271+
!next.shouldCellUpdate(next.record, prev.record)
272+
);
273+
}
274+
275+
return false;
276+
},
277+
);
278+
279+
/** Inject hover data here, we still wish MemoCell keep simple `shouldCellUpdate` logic */
280+
const WrappedCell = React.forwardRef((props: CellProps<any>, ref: React.Ref<any>) => {
281+
const { onHover, startRow, endRow } = React.useContext(HoverContext);
230282

231-
return false;
283+
return <MemoCell {...props} ref={ref} onHover={onHover} startRow={startRow} endRow={endRow} />;
232284
});
285+
WrappedCell.displayName = 'WrappedCell';
233286

234-
export default MemoCell;
287+
export default WrappedCell;

src/context/HoverContext.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import * as React from 'react';
2+
3+
export interface HoverContextProps {
4+
startRow: number;
5+
endRow: number;
6+
onHover: (start: number, end: number) => void;
7+
}
8+
9+
const HoverContext = React.createContext<HoverContextProps>({} as any);
10+
11+
export default HoverContext;

tests/Table.spec.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -731,7 +731,7 @@ describe('Table.Basic', () => {
731731
}),
732732
);
733733

734-
expect(wrapper.find('tbody Cell').first().key()).toBeTruthy();
734+
expect(wrapper.find('tbody WrappedCell').first().key()).toBeTruthy();
735735
});
736736

737737
it('syntactic sugar', () => {
@@ -931,4 +931,10 @@ describe('Table.Basic', () => {
931931
expect(onExpandedRowsChange).toHaveBeenCalledWith(['parent', 'bamboo']);
932932
});
933933
});
934+
935+
it('hover', () => {
936+
const wrapper = mount(createTable());
937+
wrapper.find('tbody td').first().simulate('mouseEnter');
938+
expect(wrapper.exists('.rc-table-cell-row-hover')).toBeTruthy();
939+
});
934940
});

0 commit comments

Comments
 (0)