Files
element-plus/packages/components/table/src/config.ts
qiang cba30d8cd2 feat(components): [table] accessibility enhancement (#22432)
* feat(components): [table] accessibility enhancemen

closed #14301

* chore: sync locale

* chore: add test

* chore: optimize style

* chore: optimize style

* feat(components): [table] sort and filter a11y
2025-12-02 10:41:16 +08:00

287 lines
6.4 KiB
TypeScript

import { h } from 'vue'
import ElCheckbox from '@element-plus/components/checkbox'
import { ElIcon } from '@element-plus/components/icon'
import { ArrowRight, Loading } from '@element-plus/icons-vue'
import { getProp, isBoolean, isFunction, isNumber } from '@element-plus/utils'
import type { VNode } from 'vue'
import type { TableColumnCtx } from './table-column/defaults'
import type { Store } from './store'
import type { DefaultRow, TreeNode } from './table/defaults'
const defaultClassNames = {
selection: 'table-column--selection',
expand: 'table__expand-column',
} as const
export const cellStarts = {
default: {
order: '',
},
selection: {
width: 48,
minWidth: 48,
realWidth: 48,
order: '',
},
expand: {
width: 48,
minWidth: 48,
realWidth: 48,
order: '',
},
index: {
width: 48,
minWidth: 48,
realWidth: 48,
order: '',
},
}
export const getDefaultClassName = (type: string) => {
return defaultClassNames[type as keyof typeof defaultClassNames] || ''
}
// 这些选项不应该被覆盖
export const cellForced = {
selection: {
renderHeader<T extends DefaultRow>({ store }: { store: Store<T> }) {
function isDisabled() {
return store.states.data.value && store.states.data.value.length === 0
}
return h(ElCheckbox, {
disabled: isDisabled(),
size: store.states.tableSize.value,
indeterminate:
store.states.selection.value.length > 0 &&
!store.states.isAllSelected.value,
'onUpdate:modelValue': store.toggleAllSelection ?? undefined,
modelValue: store.states.isAllSelected.value,
ariaLabel: store.t('el.table.selectAllLabel'),
})
},
renderCell<T extends DefaultRow>({
row,
column,
store,
$index,
}: {
row: T
column: TableColumnCtx<T>
store: Store<T>
$index: number
}) {
return h(ElCheckbox, {
disabled: column.selectable
? !column.selectable.call(null, row, $index)
: false,
size: store.states.tableSize.value,
onChange: () => {
store.commit('rowSelectedChanged', row)
},
onClick: (event: Event) => event.stopPropagation(),
modelValue: store.isSelected(row),
ariaLabel: store.t('el.table.selectRowLabel'),
})
},
sortable: false,
resizable: false,
},
index: {
renderHeader<T extends DefaultRow>({
column,
}: {
column: TableColumnCtx<T>
}) {
return column.label || '#'
},
renderCell<T extends DefaultRow>({
column,
$index,
}: {
column: TableColumnCtx<T>
$index: number
}) {
let i = $index + 1
const index = column.index
if (isNumber(index)) {
i = $index + index
} else if (isFunction(index)) {
i = index($index)
}
return h('div', {}, [i])
},
sortable: false,
},
expand: {
renderHeader<T extends DefaultRow>({
column,
}: {
column: TableColumnCtx<T>
}) {
return column.label || ''
},
renderCell<T extends DefaultRow>({
column,
row,
store,
expanded,
}: {
column: TableColumnCtx<T>
row: T
store: Store<T>
expanded: boolean
}) {
const { ns } = store
const classes = [ns.e('expand-icon')]
if (!column.renderExpand && expanded) {
classes.push(ns.em('expand-icon', 'expanded'))
}
const callback = function (e: Event) {
e.stopPropagation()
store.toggleRowExpansion(row)
}
return h(
'button',
{
type: 'button',
'aria-label': store.t(
expanded ? 'el.table.collapseRowLabel' : 'el.table.expandRowLabel'
),
'aria-expanded': expanded,
class: classes,
onClick: callback,
},
{
default: () => {
if (column.renderExpand) {
return [
column.renderExpand({
expanded,
}),
]
}
return [
h(ElIcon, null, {
default: () => {
return [h(ArrowRight)]
},
}),
]
},
}
)
},
sortable: false,
resizable: false,
},
}
export function defaultRenderCell<T extends DefaultRow>({
row,
column,
$index,
}: {
row: T
column: TableColumnCtx<T>
$index: number
}) {
const property = column.property
const value = property && getProp(row, property).value
if (column && column.formatter) {
return column.formatter(row, column, value, $index)
}
return value?.toString?.() || ''
}
export function treeCellPrefix<T extends DefaultRow>(
{
row,
treeNode,
store,
}: {
row: T
treeNode: TreeNode
store: Store<T>
},
createPlaceholder = false
) {
const { ns } = store
if (!treeNode) {
if (createPlaceholder) {
return [
h('span', {
class: ns.e('placeholder'),
}),
]
}
return null
}
const ele: VNode[] = []
const callback = function (e: Event) {
e.stopPropagation()
if (treeNode.loading) {
return
}
store.loadOrToggle(row)
}
if (treeNode.indent) {
ele.push(
h('span', {
class: ns.e('indent'),
style: { 'padding-left': `${treeNode.indent}px` },
})
)
}
if (isBoolean(treeNode.expanded) && !treeNode.noLazyChildren) {
const expandClasses = [
ns.e('expand-icon'),
treeNode.expanded ? ns.em('expand-icon', 'expanded') : '',
]
let icon = ArrowRight
if (treeNode.loading) {
icon = Loading
}
ele.push(
h(
'button',
{
type: 'button',
'aria-label': store.t(
treeNode.expanded
? 'el.table.collapseRowLabel'
: 'el.table.expandRowLabel'
),
'aria-expanded': treeNode.expanded,
class: expandClasses,
onClick: callback,
},
{
default: () => {
return [
h(
ElIcon,
{ class: ns.is('loading', treeNode.loading) },
{
default: () => [h(icon)],
}
),
]
},
}
)
)
} else {
ele.push(
h('span', {
class: ns.e('placeholder'),
})
)
}
return ele
}