mirror of
https://github.com/element-plus/element-plus.git
synced 2025-02-05 11:21:11 +08:00
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:
parent
b6e076ee95
commit
462bff18de
@ -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
|
||||
|
||||
|
91
docs/examples/table/tooltip-formatter.vue
Normal file
91
docs/examples/table/tooltip-formatter.vue
Normal 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>
|
@ -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()
|
||||
})
|
||||
})
|
||||
|
@ -30,4 +30,5 @@ export type {
|
||||
Sort,
|
||||
Filter,
|
||||
TableColumnCtx,
|
||||
TableTooltipData,
|
||||
} from './src/table/defaults'
|
||||
|
@ -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
|
||||
)
|
||||
|
@ -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`
|
||||
*/
|
||||
|
@ -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: [],
|
||||
|
@ -59,6 +59,7 @@ function useWatcher<T>(
|
||||
'labelClassName',
|
||||
'filterClassName',
|
||||
'showOverflowTooltip',
|
||||
'tooltipFormatter',
|
||||
]
|
||||
const aliases = {
|
||||
property: 'prop',
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user