feat(components): [table] add tooltip-formatter table & table-column prop (#19524)

* feat(components): [table] add `tooltip-formatter` table-column prop

closed #19507

* docs: add example

* docs: demo remove tooltip formatter index

* refactor(components): refactor code

* docs: simpify tooltip demo

* fix: fix warning error

* feat(components): add table tooltip-formatter prop & merge slot

* refactor: reuse variables

* test: add tooltip-formatter test

* docs: upgrade version

* feat(components): [table] add `tooltip-formatter` table-column prop

closed #19507

* docs: add example

* docs: demo remove tooltip formatter index

* refactor(components): refactor code

* docs: simpify tooltip demo

* fix: fix warning error

* feat(components): add table tooltip-formatter prop & merge slot

* refactor: reuse variables

* test: add tooltip-formatter test

* docs: upgrade version

* refactor: change parameter to obj args

* fix: property change to prop

* fix: export `TableOverflowTooltipFormatterData`

* refactor: `TableOverflowTooltipFormatterData` to `TableTooltipData`

* fix: remove useless import

* fix: refactor code

---------

Co-authored-by: btea <2356281422@qq.com>
This commit is contained in:
知晓同丶 2025-01-23 20:15:12 +08:00 committed by GitHub
parent b6e076ee95
commit 462bff18de
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 309 additions and 25 deletions

View File

@ -243,6 +243,16 @@ table/table-layout
:::
## Tooltip formatter ^(2.9.4)
You can use `tooltip-formatter` to customize the tooltip content.
:::demo
table/tooltip-formatter
:::
## Table API
### Table Attributes
@ -290,6 +300,7 @@ table/table-layout
| flexible ^(2.2.1) | ensure main axis minimum-size doesn't follow the content | ^[boolean] | false |
| scrollbar-tabindex ^(2.8.3) | body scrollbar's wrap container tabindex | ^[string] / ^[number] | — |
| allow-drag-last-column ^(2.9.2) | whether to allow drag the last column | ^[boolean] | true |
| tooltip-formatter ^(2.9.4) | customize tooltip content when using `show-overflow-tooltip` | ^[Function]`(data: { row: any, column: any, cellValue: any }) => VNode \| string` | — |
### Table Events
@ -377,6 +388,7 @@ table/table-layout
| filter-multiple | whether data filtering supports multiple options | ^[boolean] | true |
| filter-method | data filtering method. If `filter-multiple` is on, this method will be called multiple times for each row, and a row will display if one of the calls returns `true` | ^[function]`(value: any, row: any, column: any) => void` | — |
| filtered-value | filter value for selected data, might be useful when table header is rendered with `render-header` | ^[object]`string[]` | — |
| tooltip-formatter ^(2.9.4) | customize tooltip content when using `show-overflow-tooltip` | ^[Function]`(data: { row: any, column: any, cellValue: any }) => VNode \| string` | — |
### Table-column Slots

View File

@ -0,0 +1,91 @@
<template>
<el-table
:data="tableData"
show-overflow-tooltip
:tooltip-formatter="tableRowFormatter"
style="width: 100%"
>
<el-table-column
prop="address"
label="extends table formatter"
width="240"
/>
<el-table-column
prop="tags"
label="formatter object"
width="240"
:tooltip-formatter="({ row }) => row.tags.join(', ')"
>
<template #default="{ row }">
<el-tag
v-for="tag in row.tags"
:key="tag"
class="tag-item"
type="primary"
>
{{ tag }}
</el-tag>
</template>
</el-table-column>
<el-table-column
prop="url"
label="with vnode"
width="240"
:tooltip-formatter="withVNode"
/>
</el-table>
</template>
<script lang="ts" setup>
import { h } from 'vue'
import { ElLink, type TableTooltipData } from 'element-plus'
type TableData = {
address: string
tags: string[]
url: string
}
const tableData: TableData[] = [
{
address: 'Lohrbergstr. 86c, Süd Lilli, Saarland',
tags: ['Office', 'Home', 'Park', 'Garden'],
url: 'https://github.com/element-plus/element-plus/issues',
},
{
address: '760 A Street, South Frankfield, Illinois',
tags: ['error', 'warning', 'success', 'info'],
url: 'https://github.com/element-plus/element-plus/pulls',
},
{
address: 'Arnold-Ohletz-Str. 41a, Alt Malinascheid, Thüringen',
tags: ['one', 'two', 'three', 'four', 'five'],
url: 'https://github.com/element-plus/element-plus/discussions',
},
{
address: '23618 Windsor Drive, West Ricardoview, Idaho',
tags: ['blue', 'white', 'dark', 'gray', 'red', 'bright'],
url: 'https://github.com/element-plus/element-plus/actions',
},
]
const tableRowFormatter = (data: TableTooltipData<TableData>) => {
return `${data.cellValue}: table formatter`
}
const withVNode = (data: TableTooltipData<TableData>) => {
return h(ElLink, { type: 'primary', href: data.cellValue }, () =>
h('span', null, data.cellValue)
)
}
</script>
<style scoped>
p {
margin: 10px;
padding: 0;
}
.tag-item + .tag-item {
margin-left: 5px;
}
</style>

View File

@ -1,5 +1,5 @@
// @ts-nocheck
import { nextTick } from 'vue'
import { h, nextTick } from 'vue'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import ElCheckbox from '@element-plus/components/checkbox'
import triggerEvent from '@element-plus/test-utils/trigger-event'
@ -2166,4 +2166,102 @@ describe('Table.vue', () => {
mockCellRect.mockRestore()
mockCellRect2.mockRestore()
})
it('use-tooltip-formatter', async () => {
const testData = getTestData() as any
const mockRangeRect = vi
.spyOn(Range.prototype, 'getBoundingClientRect')
.mockReturnValue({
width: 150,
height: 30,
} as DOMRect)
const wrapper = mount({
components: {
ElTable,
ElTableColumn,
},
template: `
<el-table :data="testData" show-overflow-tooltip :tooltip-formatter="tooltipFormatter">
<el-table-column class-name="overflow-tooltip-formatter" prop="name" label="name"/>
<el-table-column class-name="overflow-tooltip-formatter-cell" prop="director" label="director" :tooltip-formatter="cellTooltipFormatter" />
<el-table-column class-name="vnode-formatter-cell" prop="runtime" label="runtime" :tooltip-formatter="vnodeFormmatter" />
</el-table>
`,
data() {
const testData = getTestData() as any
return {
testData,
}
},
methods: {
tooltipFormatter({ row }) {
return `${row.name}:formattered`
},
cellTooltipFormatter({ cellValue }) {
return `${cellValue}:hello world`
},
vnodeFormmatter({ cellValue }) {
return h(
'a',
{ type: 'primary', href: `http://www.baidu.com?q=${cellValue}` },
() => h('span', null, cellValue)
)
},
},
})
await doubleWait()
const baseFormatterTds = wrapper.findAll('.overflow-tooltip-formatter')
const childFormatterTds = wrapper.findAll(
'.overflow-tooltip-formatter-cell'
)
const vnodeFormatterTds = wrapper.findAll('.vnode-formatter-cell')
// Enter the cell
await baseFormatterTds[1].trigger('mouseenter')
await rAF()
expect(document.querySelector('.el-popper span')?.innerHTML).equals(
`${testData[0].name}:formattered`
)
// From cell1 to cell2
await childFormatterTds[1].trigger('mouseenter')
await rAF()
expect(document.querySelector('.el-popper span')?.innerHTML).equals(
`${testData[0].director}:hello world`
)
await baseFormatterTds[2].trigger('mouseenter')
await rAF()
expect(document.querySelector('.el-popper span')?.innerHTML).equals(
`${testData[1].name}:formattered`
)
// vnode
await vnodeFormatterTds[1].trigger('mouseenter')
await rAF()
expect(document.querySelector('.el-popper a')?.getAttribute('href')).equals(
`http://www.baidu.com?q=${testData[0].runtime}`
)
// leave and enter again
vi.useFakeTimers()
await vnodeFormatterTds[1].trigger('mouseleave')
vi.runAllTimers()
vi.useRealTimers()
await rAF()
expect(
document.querySelector('.el-popper')?.getAttribute('aria-hidden')
).toEqual('true')
// Enter the cell again
await vnodeFormatterTds[1].trigger('mouseenter')
await rAF()
expect(document.querySelector('.el-popper a')?.getAttribute('href')).equals(
`http://www.baidu.com?q=${testData[0].runtime}`
)
mockRangeRect.mockRestore()
})
})

View File

@ -30,4 +30,5 @@ export type {
Sort,
Filter,
TableColumnCtx,
TableTooltipData,
} from './src/table/defaults'

View File

@ -92,8 +92,9 @@ function useEvents<T>(props: Partial<TableBodyProps<T>>) {
const table = parent
const cell = getCell(event)
const namespace = table?.vnode.el?.dataset.prefix
let column: TableColumnCtx<T>
if (cell) {
const column = getColumnByCell(
column = getColumnByCell(
{
columns: props.store.states.columns.value,
},
@ -158,6 +159,8 @@ function useEvents<T>(props: Partial<TableBodyProps<T>>) {
createTablePopper(
tooltipOptions,
cell.innerText || cell.textContent,
row,
column,
cell,
table
)

View File

@ -1,7 +1,10 @@
// @ts-nocheck
import type { ComponentInternalInstance, PropType, Ref, VNode } from 'vue'
import type { DefaultRow, Table } from '../table/defaults'
import type { TableOverflowTooltipOptions } from '../util'
import type {
TableOverflowTooltipFormatter,
TableOverflowTooltipOptions,
} from '../util'
type CI<T> = { column: TableColumnCtx<T>; $index: number }
@ -35,6 +38,7 @@ interface TableColumnCtx<T> {
align: string
headerAlign: string
showOverflowTooltip?: boolean | TableOverflowTooltipOptions
tooltipFormatter?: TableOverflowTooltipFormatter<T>
fixed: boolean | string
formatter: (
row: T,
@ -171,6 +175,12 @@ export default {
>,
default: undefined,
},
/**
* @description function that formats cell tooltip content, works when `show-overflow-tooltip` is `true`
*/
tooltipFormatter: Function as PropType<
TableColumnCtx<DefaultRow>['tooltipFormatter']
>,
/**
* @description whether column is fixed at left / right. Will be fixed at left if `true`
*/

View File

@ -70,6 +70,9 @@ export default defineComponent({
const showOverflowTooltip = isUndefined(props.showOverflowTooltip)
? parent.props.showOverflowTooltip
: props.showOverflowTooltip
const tooltipFormatter = isUndefined(props.tooltipFormatter)
? parent.props.tooltipFormatter
: props.tooltipFormatter
const defaults = {
...cellStarts[type],
id: columnId.value,
@ -78,6 +81,7 @@ export default defineComponent({
align: realAlign,
headerAlign: realHeaderAlign,
showOverflowTooltip,
tooltipFormatter,
// filter 相关属性
filterable: props.filters || props.filterMethod,
filteredValue: [],

View File

@ -59,6 +59,7 @@ function useWatcher<T>(
'labelClassName',
'filterClassName',
'showOverflowTooltip',
'tooltipFormatter',
]
const aliases = {
property: 'prop',

View File

@ -12,7 +12,10 @@ import type { Nullable } from '@element-plus/utils'
import type { Store } from '../store'
import type { TableColumnCtx } from '../table-column/defaults'
import type TableLayout from '../table-layout'
import type { TableOverflowTooltipOptions } from '../util'
import type {
TableOverflowTooltipFormatter,
TableOverflowTooltipOptions,
} from '../util'
export type DefaultRow = any
@ -149,10 +152,13 @@ interface TableProps<T> {
scrollbarAlwaysOn?: boolean
flexible?: boolean
showOverflowTooltip?: boolean | TableOverflowTooltipOptions
tooltipFormatter?: TableOverflowTooltipFormatter<T>
appendFilterPanelTo?: string
scrollbarTabindex?: number | string
}
type TableTooltipData<T = any> = Parameters<TableOverflowTooltipFormatter<T>>[0]
interface Sort {
prop: string
order: 'ascending' | 'descending'
@ -390,6 +396,12 @@ export default {
showOverflowTooltip: [Boolean, Object] as PropType<
TableProps<DefaultRow>['showOverflowTooltip']
>,
/**
* @description function that formats cell tooltip content, works when `show-overflow-tooltip` is `true`
*/
tooltipFormatter: Function as PropType<
TableProps<DefaultRow>['tooltipFormatter']
>,
appendFilterPanelTo: String,
scrollbarTabindex: {
type: [Number, String],
@ -418,4 +430,5 @@ export type {
Filter,
TableColumnCtx,
TreeProps,
TableTooltipData,
}

View File

@ -1,7 +1,8 @@
// @ts-nocheck
import { createVNode, render } from 'vue'
import { createVNode, isVNode, render } from 'vue'
import { flatMap, get, isNull, merge } from 'lodash-unified'
import {
getProp,
hasOwn,
isArray,
isBoolean,
@ -36,6 +37,12 @@ export type TableOverflowTooltipOptions = Partial<
>
>
export type TableOverflowTooltipFormatter<T = any> = (data: {
row: T
column: TableColumnCtx<T>
cellValue
}) => VNode | string
type RemovePopperFn = (() => void) & {
trigger?: HTMLElement
vm?: VNode
@ -371,15 +378,38 @@ export function walkTreeNode(
const getTableOverflowTooltipProps = (
props: TableOverflowTooltipOptions,
content: string
innerText: string,
row: T,
column: TableColumnCtx<T>
) => {
// merge popperOptions
const popperOptions = {
strategy: 'fixed',
...props.popperOptions,
}
const tooltipFormatterContent = isFunction(column.tooltipFormatter)
? column.tooltipFormatter({
row,
column,
cellValue: getProp(row, column.property).value,
})
: undefined
if (isVNode(tooltipFormatterContent)) {
return {
slotContent: tooltipFormatterContent,
content: null,
...props,
popperOptions,
}
}
return {
content,
slotContent: null,
content: tooltipFormatterContent ?? innerText,
...props,
popperOptions: {
strategy: 'fixed',
...props.popperOptions,
},
popperOptions,
}
}
@ -388,29 +418,50 @@ export let removePopper: RemovePopperFn | null = null
export function createTablePopper(
props: TableOverflowTooltipOptions,
popperContent: string,
row: T,
column: TableColumnCtx<T>,
trigger: HTMLElement,
table: Table<[]>
) {
const tableOverflowTooltipProps = getTableOverflowTooltipProps(
props,
popperContent,
row,
column
)
const mergedProps = {
...tableOverflowTooltipProps,
slotContent: undefined,
}
if (removePopper?.trigger === trigger) {
merge(
removePopper!.vm.component.props,
getTableOverflowTooltipProps(props, popperContent)
)
const comp = removePopper!.vm.component
merge(comp.props, mergedProps)
if (tableOverflowTooltipProps.slotContent) {
comp.slots.content = () => [tableOverflowTooltipProps.slotContent]
}
return
}
removePopper?.()
const parentNode = table?.refs.tableWrapper
const ns = parentNode?.dataset.prefix
const vm = createVNode(ElTooltip, {
virtualTriggering: true,
virtualRef: trigger,
appendTo: parentNode,
placement: 'top',
transition: 'none', // Default does not require transition
offset: 0,
hideAfter: 0,
...getTableOverflowTooltipProps(props, popperContent),
})
const vm = createVNode(
ElTooltip,
{
virtualTriggering: true,
virtualRef: trigger,
appendTo: parentNode,
placement: 'top',
transition: 'none', // Default does not require transition
offset: 0,
hideAfter: 0,
...mergedProps,
},
tableOverflowTooltipProps.slotContent
? {
content: () => tableOverflowTooltipProps.slotContent,
}
: undefined
)
vm.appContext = { ...table.appContext, ...table }
const container = document.createElement('div')
render(vm, container)