Skip to content

Commit 0bfd0e7

Browse files
authored
feat(Table): support rows highlight (#3442)
* feat(Table): support row highlight * chore: update snapshot * chore: update snapshot * chore: update snapshot
1 parent 6b99c82 commit 0bfd0e7

File tree

13 files changed

+853
-40
lines changed

13 files changed

+853
-40
lines changed

src/table/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
因此,表格组件有三个:`BaseTable`(基础表格)、`PrimaryTable`(主表格)、`EnhancedTable`(增强型表格),三种表格都会导出。默认导出 `PrimaryTable`
66

7-
- `BaseTable`(基础表格)包含一些基础功能:固定表头、固定列、冻结行、加载态、分页、多级表头、合并单元格、自定义单元格、自定义表头、自定义表尾、文本省略、对齐方式、表格事件、尺寸、行类名、边框、斑马线、悬浮态、空数据等
7+
- `BaseTable`(基础表格)包含一些基础功能:行高亮、固定表头、固定列、冻结行、加载态、分页、多级表头、合并单元格、自定义单元格、自定义表头、自定义表尾、文本省略、对齐方式、表格事件、尺寸、行类名、边框、斑马线、悬浮态、空数据等
88
- `PrimaryTable``Table`(主表格)包含一些更高级的功能:行展开/收起、过滤、排序、异步加载、拖拽排序等。`PrimaryTable``Table` 包含 `BaseTable` 的所有功能。`Table``PrimaryTable` 完全等价。
99
- `EnhancedTable`(增强型表格)包含一些更复杂的功能:树形结构等。`EnhancedTable` 包含 `BaseTable``PrimaryTable` 的所有功能
1010

@@ -22,6 +22,7 @@
2222
- hooks/useMultiHeader 多级表头,BaseTable
2323
- hooks/usePagination 分页,BaseTable
2424
- hooks/useLazyLoad 懒加载,BaseTable
25+
- hooks/useRowHighlight 行高亮,BaseTable
2526

2627
- hooks/useAsyncLoading 异步加载功能,PrimaryTable
2728
- hooks/useColumnController 自定义列配置,PrimaryTable

src/table/__tests__/__snapshots__/vitest-table.test.jsx.snap

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
exports[`BaseTable Component > props.showHeader: BaseTable contains element \`thead\` 1`] = `
44
<div
55
class="t-table"
6-
style="position: relative;"
6+
tabindex="0"
77
>
88
<div
99
class="t-table__content"
@@ -205,7 +205,7 @@ exports[`BaseTable Component > props.showHeader: BaseTable contains element \`th
205205
exports[`BaseTable Component > props.size is equal to large 1`] = `
206206
<div
207207
class="t-table t-size-l"
208-
style="position: relative;"
208+
tabindex="0"
209209
>
210210
<div
211211
class="t-table__content"
@@ -407,7 +407,7 @@ exports[`BaseTable Component > props.size is equal to large 1`] = `
407407
exports[`BaseTable Component > props.size is equal to medium 1`] = `
408408
<div
409409
class="t-table"
410-
style="position: relative;"
410+
tabindex="0"
411411
>
412412
<div
413413
class="t-table__content"
@@ -609,7 +609,7 @@ exports[`BaseTable Component > props.size is equal to medium 1`] = `
609609
exports[`BaseTable Component > props.size is equal to small 1`] = `
610610
<div
611611
class="t-table t-size-s"
612-
style="position: relative;"
612+
tabindex="0"
613613
>
614614
<div
615615
class="t-table__content"
@@ -811,7 +811,7 @@ exports[`BaseTable Component > props.size is equal to small 1`] = `
811811
exports[`BaseTable Component > props.tableLayout is equal to auto 1`] = `
812812
<div
813813
class="t-table"
814-
style="position: relative;"
814+
tabindex="0"
815815
>
816816
<div
817817
class="t-table__content"
@@ -1007,7 +1007,7 @@ exports[`BaseTable Component > props.tableLayout is equal to auto 1`] = `
10071007
exports[`BaseTable Component > props.tableLayout is equal to fixed 1`] = `
10081008
<div
10091009
class="t-table"
1010-
style="position: relative;"
1010+
tabindex="0"
10111011
>
10121012
<div
10131013
class="t-table__content"
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<template>
2+
<t-space direction="vertical">
3+
<t-space align="center">
4+
<t-radio-group v-model="activeRowType" variant="default-filled">
5+
<t-radio-button value="">不高亮</t-radio-button>
6+
<t-radio-button value="single">单行高亮</t-radio-button>
7+
<t-radio-button value="multiple">多行高亮</t-radio-button>
8+
</t-radio-group>
9+
<t-checkbox v-model="hover"> 显示悬浮效果 </t-checkbox>
10+
</t-space>
11+
12+
<!-- v-model:activeRowKeys 父组件控制高亮行 -->
13+
<!-- defaultActiveRowKeys 组件内部控制高亮行,父组件无法使用这个属性控制 -->
14+
<t-table
15+
row-key="key"
16+
:data="tableData"
17+
:columns="columns"
18+
:active-row-type="activeRowType"
19+
:hover="hover"
20+
@active-change="onActiveChange"
21+
></t-table>
22+
</t-space>
23+
</template>
24+
25+
<script></script>
26+
27+
<script setup>
28+
import { ref, watch } from 'vue';
29+
30+
export default {
31+
name: 'HighlightTable',
32+
};
33+
34+
const activeRowType = ref('single');
35+
const hover = ref(false);
36+
const tableData = getTableData();
37+
const columns = [
38+
{ colKey: 'applicant', title: '申请人', width: '100' },
39+
{ colKey: 'channel', title: '签署方式' },
40+
{ colKey: 'detail.email', title: '邮箱地址', ellipsis: true },
41+
{ colKey: 'createTime', title: '申请时间' },
42+
];
43+
const onActiveChange = (highlightRowKeys, ctx) => {
44+
console.log(highlightRowKeys, ctx);
45+
};
46+
watch([activeRowType], ([activeRowType]) => {
47+
if (!activeRowType) {
48+
hover.value = true;
49+
}
50+
});
51+
function getTableData(total = 5) {
52+
const data = [];
53+
for (let i = 0; i < total; i++) {
54+
data.push({
55+
key: i + 1,
56+
applicant: ['贾明', '张三', '王芳'][i % 3],
57+
status: i % 3,
58+
channel: ['电子签署', '纸质签署', '纸质签署'][i % 3],
59+
detail: {
60+
61+
},
62+
matters: ['宣传物料制作费用', 'algolia 服务报销', '相关周边制作费', '激励奖品快递费'][i % 4],
63+
time: [2, 3, 1, 4][i % 4],
64+
createTime: ['2022-01-01', '2022-02-01', '2022-03-01', '2022-04-01', '2022-05-01'][i % 4],
65+
});
66+
}
67+
return data;
68+
}
69+
</script>

src/table/_example/highlight.vue

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<template>
2+
<t-space direction="vertical">
3+
<t-space align="center">
4+
<t-radio-group v-model="activeRowType" variant="default-filled">
5+
<t-radio-button value="">不高亮</t-radio-button>
6+
<t-radio-button value="single">单行高亮</t-radio-button>
7+
<t-radio-button value="multiple">多行高亮</t-radio-button>
8+
</t-radio-group>
9+
<t-checkbox v-model="hover"> 显示悬浮效果 </t-checkbox>
10+
</t-space>
11+
12+
<!-- v-model:activeRowKeys 父组件控制高亮行 -->
13+
<!-- defaultActiveRowKeys 组件内部控制高亮行,父组件无法使用这个属性控制 -->
14+
<t-table
15+
row-key="key"
16+
:data="tableData"
17+
:columns="columns"
18+
:active-row-type="activeRowType"
19+
:hover="hover"
20+
@active-change="onActiveChange"
21+
></t-table>
22+
</t-space>
23+
</template>
24+
25+
<script>
26+
export default {
27+
data() {
28+
return {
29+
hover: false,
30+
tableData: [],
31+
activeRowType: 'single',
32+
columns: [
33+
{ colKey: 'applicant', title: '申请人', width: '100' },
34+
{ colKey: 'channel', title: '签署方式' },
35+
{ colKey: 'detail.email', title: '邮箱地址', ellipsis: true },
36+
{ colKey: 'createTime', title: '申请时间' },
37+
],
38+
};
39+
},
40+
watch: {
41+
activeRowType(v) {
42+
if (!v) {
43+
this.hover = true;
44+
}
45+
},
46+
},
47+
mounted() {
48+
this.tableData = this.getTableData();
49+
},
50+
methods: {
51+
getTableData(total = 5) {
52+
const data = [];
53+
for (let i = 0; i < total; i++) {
54+
data.push({
55+
key: i + 1,
56+
applicant: ['贾明', '张三', '王芳'][i % 3],
57+
status: i % 3,
58+
channel: ['电子签署', '纸质签署', '纸质签署'][i % 3],
59+
detail: {
60+
61+
},
62+
matters: ['宣传物料制作费用', 'algolia 服务报销', '相关周边制作费', '激励奖品快递费'][i % 4],
63+
time: [2, 3, 1, 4][i % 4],
64+
createTime: ['2022-01-01', '2022-02-01', '2022-03-01', '2022-04-01', '2022-05-01'][i % 4],
65+
});
66+
}
67+
return data;
68+
},
69+
onActiveChange(highlightRowKeys, ctx) {
70+
console.log(highlightRowKeys, ctx);
71+
},
72+
},
73+
};
74+
</script>

src/table/base-table.tsx

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ import { renderTNodeJSX, useElementLazyRender } from '../hooks';
2626
import useStyle, { formatCSSUnit } from './hooks/useStyle';
2727
import useClassName from './hooks/useClassName';
2828
import { useConfig } from '../config-provider/useConfig';
29+
import { useRowHighlight } from './hooks/useRowHighlight';
30+
import useHoverKeyboardEvent from './hooks/useHoverKeyboardEvent';
31+
2932
import { Affix } from '../affix';
3033
import { ROW_LISTENERS } from './tr';
3134
import THead from './thead';
@@ -157,6 +160,7 @@ export default defineComponent({
157160
[tableColFixedClasses.leftShadow]: showColumnShadow.left,
158161
[tableColFixedClasses.rightShadow]: showColumnShadow.right,
159162
[tableBaseClass.columnResizableTable]: props.resizable,
163+
[`${classPrefix}-table__row--active-${props.activeRowType}`]: props.activeRowType,
160164
},
161165
]);
162166

@@ -177,6 +181,19 @@ export default defineComponent({
177181

178182
const columnResizable = computed(() => props.allowResizeColumnWidth ?? props.resizable);
179183

184+
// 行高亮
185+
const {
186+
tActiveRow, onHighlightRow, addHighlightKeyboardListener, removeHighlightKeyboardListener,
187+
} = useRowHighlight(props, tableRef);
188+
189+
const {
190+
hoverRow,
191+
needKeyboardRowHover,
192+
clearHoverRow,
193+
addRowHoverKeyboardListener,
194+
removeRowHoverKeyboardListener,
195+
} = useHoverKeyboardEvent(props, tableRef);
196+
180197
watch(tableElmRef, () => {
181198
setUseFixedTableElmRef(tableElmRef.value);
182199
});
@@ -276,6 +293,22 @@ export default defineComponent({
276293
addTableResizeObserver(tableRef.value);
277294
});
278295

296+
const onTableFocus = () => {
297+
props.activeRowType && addHighlightKeyboardListener();
298+
needKeyboardRowHover.value && addRowHoverKeyboardListener();
299+
};
300+
301+
const onTableBlur = () => {
302+
props.activeRowType && removeHighlightKeyboardListener();
303+
needKeyboardRowHover.value && removeRowHoverKeyboardListener();
304+
};
305+
306+
const onInnerRowClick: BaseTableProps['onRowClick'] = (ctx) => {
307+
props.onRowClick?.(ctx);
308+
props.activeRowType && onHighlightRow(ctx);
309+
needKeyboardRowHover.value && clearHoverRow();
310+
};
311+
279312
const tableData = computed(() => (isPaginateData.value ? dataSource.value : props.data));
280313

281314
const scrollToElement = (params: ComponentScrollToElementParams) => {
@@ -349,6 +382,8 @@ export default defineComponent({
349382
horizontalScrollbarRef,
350383
tableBodyRef,
351384
showAffixPagination,
385+
tActiveRow,
386+
hoverRow,
352387
showElement,
353388
getListener,
354389
renderPagination,
@@ -358,6 +393,9 @@ export default defineComponent({
358393
refreshTable,
359394
onInnerVirtualScroll,
360395
scrollColumnIntoView,
396+
onTableFocus,
397+
onTableBlur,
398+
onInnerRowClick,
361399
paginationAffixRef,
362400
horizontalScrollAffixRef,
363401
headerTopAffixRef,
@@ -576,6 +614,9 @@ export default defineComponent({
576614
// 内部使用分页信息必须取 innerPagination
577615
pagination: this.innerPagination,
578616
attach: this.attach,
617+
hoverRow: this.hoverRow,
618+
activeRow: this.tActiveRow,
619+
onRowClick: this.onInnerRowClick,
579620
};
580621
// Vue3 do not need getListener
581622
const tBodyListener = this.getListener();
@@ -650,7 +691,13 @@ export default defineComponent({
650691
);
651692

652693
return (
653-
<div ref="tableRef" class={this.dynamicBaseTableClasses} style="position: relative">
694+
<div
695+
ref="tableRef"
696+
tabindex="0"
697+
class={this.dynamicBaseTableClasses}
698+
onFocus={this.onTableFocus}
699+
onBlur={this.onTableBlur}
700+
>
654701
{!!topContent && <div class={this.tableBaseClass.topContent}>{topContent}</div>}
655702

656703
{this.renderAffixedHeader(columns)}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import {
2+
toRefs, Ref, ref, computed,
3+
} from '@vue/composition-api';
4+
import get from 'lodash/get';
5+
import { BaseTableProps } from '../interface';
6+
import { on, off } from '../../utils/dom';
7+
import { ARROW_DOWN_REG, ARROW_UP_REG, SPACE_REG } from '../../_common/js/common';
8+
import { RowEventContext, TableRowData } from '../type';
9+
10+
/**
11+
* 需要进行表格行操作时,则需要键盘操作的悬浮效果来表达当前的哪一行
12+
* 如:高亮多行、行选中、行展开等功能
13+
*/
14+
export function useHoverKeyboardEvent(props: BaseTableProps, tableRef: Ref<HTMLDivElement>) {
15+
const {
16+
hover, data, activeRowType, keyboardRowHover,
17+
} = toRefs(props);
18+
const hoverRow = ref<string | number>();
19+
const currentHoverRowIndex = ref(-1);
20+
21+
// 单行高亮场景,不需要键盘悬浮效果
22+
const needKeyboardRowHover = computed(() => {
23+
if (activeRowType.value === 'single') return false;
24+
if (activeRowType.value === 'multiple') return true;
25+
return hover.value || keyboardRowHover.value;
26+
});
27+
28+
const onHoverRow = (ctx: RowEventContext<TableRowData>, extra?: { action?: 'hover' }) => {
29+
const rowValue = get(ctx.row, props.rowKey);
30+
if (hoverRow.value === rowValue && extra?.action !== 'hover') {
31+
hoverRow.value = undefined;
32+
} else {
33+
hoverRow.value = rowValue;
34+
}
35+
currentHoverRowIndex.value = ctx.index;
36+
};
37+
38+
const clearHoverRow = () => {
39+
hoverRow.value = undefined;
40+
currentHoverRowIndex.value = -1;
41+
};
42+
43+
const keyboardDownListener = (e: KeyboardEvent) => {
44+
if (!needKeyboardRowHover.value) return;
45+
const code = e.key?.trim() || e.code;
46+
if (ARROW_DOWN_REG.test(code)) {
47+
e.preventDefault();
48+
const index = Math.min(data.value.length - 1, currentHoverRowIndex.value + 1);
49+
onHoverRow({ row: data.value[index], index, e }, { action: 'hover' });
50+
} else if (ARROW_UP_REG.test(code)) {
51+
e.preventDefault();
52+
const index = Math.max(0, currentHoverRowIndex.value - 1);
53+
onHoverRow({ row: data.value[index], index, e }, { action: 'hover' });
54+
} else if (SPACE_REG.test(code) && props.activeRowType !== 'multiple') {
55+
const index = currentHoverRowIndex.value;
56+
onHoverRow({ row: data.value[index], index, e });
57+
}
58+
};
59+
60+
const addRowHoverKeyboardListener = () => {
61+
on(tableRef.value, 'keydown', keyboardDownListener);
62+
};
63+
64+
const removeRowHoverKeyboardListener = () => {
65+
off(tableRef.value, 'keydown', keyboardDownListener);
66+
};
67+
68+
return {
69+
hoverRow,
70+
needKeyboardRowHover,
71+
clearHoverRow,
72+
addRowHoverKeyboardListener,
73+
removeRowHoverKeyboardListener,
74+
};
75+
}
76+
77+
export default useHoverKeyboardEvent;

0 commit comments

Comments
 (0)