🌐 AI搜索 & 代理 主页
Skip to content

Commit d4e66fa

Browse files
tmorehousejacobmllr95
authored andcommitted
feat(b-table): better sort labeling for screen readers (closes #4487) (#4488)
* feat(b-table): better sort labeling for screen readers * Update mixin-thead.js * Update mixin-thead.js * Update mixin-thead.js * Update mixin-thead.js * Update mixin-thead.js * Update mixin-sorting.js * Update mixin-sorting.js * Update table-sorting.spec.js * Update package.json * Update table-sorting.spec.js * Update table-sorting.spec.js * Update mixin-thead.js * Update mixin-thead.js
1 parent 69edc0c commit d4e66fa

File tree

4 files changed

+144
-72
lines changed

4 files changed

+144
-72
lines changed

src/components/table/helpers/mixin-sorting.js

Lines changed: 29 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import stableSort from '../../../utils/stable-sort'
2-
import startCase from '../../../utils/startcase'
32
import { arrayIncludes } from '../../../utils/array'
43
import { isFunction, isUndefinedOrNull } from '../../../utils/inspect'
4+
import { trim } from '../../../utils/string'
55
import defaultSortCompare from './default-sort-compare'
66

77
export default {
@@ -242,55 +242,52 @@ export default {
242242
return {}
243243
}
244244
const sortable = field.sortable
245-
let ariaLabel = ''
246-
if ((!field.label || !field.label.trim()) && !field.headerTitle) {
247-
// In case field's label and title are empty/blank, we need to
248-
// add a hint about what the column is about for non-sighted users.
249-
// This is duplicated code from tbody-row mixin, but we need it
250-
// here as well, since we overwrite the original aria-label.
251-
/* istanbul ignore next */
252-
ariaLabel = startCase(key)
245+
// Assemble the aria-sort attribute value
246+
const ariaSort =
247+
sortable && this.localSortBy === key
248+
? this.localSortDesc
249+
? 'descending'
250+
: 'ascending'
251+
: sortable
252+
? 'none'
253+
: null
254+
// Return the attribute
255+
return {
256+
'aria-sort': ariaSort
257+
}
258+
},
259+
sortTheadThLabel(key, field, isFoot) {
260+
// A label to be placed in an `.sr-only` element in the header cell
261+
if (!this.isSortable || (isFoot && this.noFooterSorting)) {
262+
// No label if not a sortable table
263+
return null
253264
}
265+
const sortable = field.sortable
254266
// The correctness of these labels is very important for screen-reader users.
255-
let ariaLabelSorting = ''
267+
let labelSorting = ''
256268
if (sortable) {
257269
if (this.localSortBy === key) {
258270
// currently sorted sortable column.
259-
ariaLabelSorting = this.localSortDesc ? this.labelSortAsc : this.labelSortDesc
271+
labelSorting = this.localSortDesc ? this.labelSortAsc : this.labelSortDesc
260272
} else {
261273
// Not currently sorted sortable column.
262274
// Not using nested ternary's here for clarity/readability
263275
// Default for ariaLabel
264-
ariaLabelSorting = this.localSortDesc ? this.labelSortDesc : this.labelSortAsc
276+
labelSorting = this.localSortDesc ? this.labelSortDesc : this.labelSortAsc
265277
// Handle sortDirection setting
266278
const sortDirection = this.sortDirection || field.sortDirection
267279
if (sortDirection === 'asc') {
268-
ariaLabelSorting = this.labelSortAsc
280+
labelSorting = this.labelSortAsc
269281
} else if (sortDirection === 'desc') {
270-
ariaLabelSorting = this.labelSortDesc
282+
labelSorting = this.labelSortDesc
271283
}
272284
}
273285
} else if (!this.noSortReset) {
274286
// Non sortable column
275-
ariaLabelSorting = this.localSortBy ? this.labelSortClear : ''
276-
}
277-
// Assemble the aria-label attribute value
278-
ariaLabel = [ariaLabel.trim(), ariaLabelSorting.trim()].filter(Boolean).join(': ')
279-
// Assemble the aria-sort attribute value
280-
const ariaSort =
281-
sortable && this.localSortBy === key
282-
? this.localSortDesc
283-
? 'descending'
284-
: 'ascending'
285-
: sortable
286-
? 'none'
287-
: null
288-
// Return the attributes
289-
// (All the above just to get these two values)
290-
return {
291-
'aria-label': ariaLabel || null,
292-
'aria-sort': ariaSort
287+
labelSorting = this.localSortBy ? this.labelSortClear : ''
293288
}
289+
// Return the sr-only sort label or null if no label
290+
return trim(labelSorting) || null
294291
}
295292
}
296293
}

src/components/table/helpers/mixin-thead.js

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ export default {
8989
}
9090
const sortAttrs = this.isSortable ? this.sortTheadThAttrs(field.key, field, isFoot) : {}
9191
const sortClass = this.isSortable ? this.sortTheadThClasses(field.key, field, isFoot) : null
92+
const sortLabel = this.isSortable ? this.sortTheadThLabel(field.key, field, isFoot) : null
9293
const data = {
9394
key: field.key,
9495
class: [this.fieldClasses(field), sortClass],
@@ -124,22 +125,21 @@ export default {
124125
...slotNames
125126
]
126127
}
127-
const hasSlot = this.hasNormalizedSlot(slotNames)
128-
let slot = field.label
129-
if (hasSlot) {
130-
slot = this.normalizeSlot(slotNames, {
131-
label: field.label,
132-
column: field.key,
133-
field,
134-
isFoot,
135-
// Add in row select methods
136-
selectAllRows,
137-
clearSelected
138-
})
139-
} else {
140-
data.domProps = htmlOrText(field.labelHtml)
128+
const scope = {
129+
label: field.label,
130+
column: field.key,
131+
field,
132+
isFoot,
133+
// Add in row select methods
134+
selectAllRows,
135+
clearSelected
141136
}
142-
return h(BTh, data, slot)
137+
const content =
138+
this.normalizeSlot(slotNames, scope) ||
139+
(field.labelHtml ? h('div', { domProps: htmlOrText(field.labelHtml) }) : field.label)
140+
const srLabel = sortLabel ? h('span', { staticClass: 'sr-only' }, ` (${sortLabel})`) : null
141+
// Return the header cell
142+
return h(BTh, data, [content, srLabel].filter(identity))
143143
}
144144

145145
// Generate the array of <th> cells

src/components/table/package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -235,15 +235,15 @@
235235
},
236236
{
237237
"prop": "labelSortAsc",
238-
"description": "String to place in the header cell's 'aria-label' when clicking the cell will change the sort direction to ascending"
238+
"description": "Hidden string to place in the header cell when clicking the cell will change the sort direction to ascending"
239239
},
240240
{
241241
"prop": "labelSortDesc",
242-
"description": "String to place in the header cell's 'aria-label' when clicking the cell will change the sort direction to descending"
242+
"description": "Hidden string to place in the header cell when clicking the cell will change the sort direction to descending"
243243
},
244244
{
245245
"prop": "labelSortClear",
246-
"description": "String to place in the header cell's 'aria-label' when clicking the cell will clear the current sorting direction"
246+
"description": "Hidden string to place in the header cell when clicking the cell will clear the current sorting direction"
247247
},
248248
{
249249
"prop": "selectable",

src/components/table/table-sorting.spec.js

Lines changed: 97 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -77,17 +77,32 @@ describe('table > sorting', () => {
7777
// Currently sorted as ascending
7878
expect($ths.at(0).attributes('aria-sort')).toBe('ascending')
7979
// For switching to descending
80-
expect($ths.at(0).attributes('aria-label')).toBe(wrapper.vm.labelSortDesc)
80+
expect(
81+
$ths
82+
.at(0)
83+
.find('.sr-only')
84+
.text()
85+
).toContain(wrapper.vm.labelSortDesc)
8186

8287
// Not sorted by this column
8388
expect($ths.at(1).attributes('aria-sort')).toBe('none')
8489
// For sorting by ascending
85-
expect($ths.at(1).attributes('aria-label')).toBe(wrapper.vm.labelSortAsc)
90+
expect(
91+
$ths
92+
.at(1)
93+
.find('.sr-only')
94+
.text()
95+
).toContain(wrapper.vm.labelSortAsc)
8696

8797
// Not a sortable column
8898
expect($ths.at(2).attributes('aria-sort')).not.toBeDefined()
8999
// For clearing sorting
90-
expect($ths.at(2).attributes('aria-label')).toBe(wrapper.vm.labelSortClear)
100+
expect(
101+
$ths
102+
.at(2)
103+
.find('.sr-only')
104+
.text()
105+
).toContain(wrapper.vm.labelSortClear)
91106

92107
// Change sort direction
93108
wrapper.setProps({
@@ -113,17 +128,32 @@ describe('table > sorting', () => {
113128
// Currently sorted as descending
114129
expect($ths.at(0).attributes('aria-sort')).toBe('descending')
115130
// For switching to ascending
116-
expect($ths.at(0).attributes('aria-label')).toBe(wrapper.vm.labelSortAsc)
131+
expect(
132+
$ths
133+
.at(0)
134+
.find('.sr-only')
135+
.text()
136+
).toContain(wrapper.vm.labelSortAsc)
117137

118138
// Not sorted by this column
119139
expect($ths.at(1).attributes('aria-sort')).toBe('none')
120140
// For sorting by ascending
121-
expect($ths.at(1).attributes('aria-label')).toBe(wrapper.vm.labelSortAsc)
141+
expect(
142+
$ths
143+
.at(1)
144+
.find('.sr-only')
145+
.text()
146+
).toContain(wrapper.vm.labelSortAsc)
122147

123148
// Not a sortable column
124149
expect($ths.at(2).attributes('aria-sort')).not.toBeDefined()
125150
// For clearing sorting
126-
expect($ths.at(2).attributes('aria-label')).toBe(wrapper.vm.labelSortClear)
151+
expect(
152+
$ths
153+
.at(2)
154+
.find('.sr-only')
155+
.text()
156+
).toContain(wrapper.vm.labelSortClear)
127157

128158
// Clear sort
129159
wrapper.setProps({
@@ -150,17 +180,32 @@ describe('table > sorting', () => {
150180
// Currently not sorted
151181
expect($ths.at(0).attributes('aria-sort')).toBe('none')
152182
// For sorting by ascending
153-
expect($ths.at(0).attributes('aria-label')).toBe(wrapper.vm.labelSortAsc)
183+
expect(
184+
$ths
185+
.at(0)
186+
.find('.sr-only')
187+
.text()
188+
).toContain(wrapper.vm.labelSortAsc)
154189

155190
// Not sorted by this column
156191
expect($ths.at(1).attributes('aria-sort')).toBe('none')
157192
// For sorting by ascending
158-
expect($ths.at(1).attributes('aria-label')).toBe(wrapper.vm.labelSortAsc)
193+
expect(
194+
$ths
195+
.at(1)
196+
.find('.sr-only')
197+
.text()
198+
).toContain(wrapper.vm.labelSortAsc)
159199

160200
// Not a sortable column
161201
expect($ths.at(2).attributes('aria-sort')).not.toBeDefined()
162202
// For clearing sorting
163-
expect($ths.at(2).attributes('aria-label')).not.toBeDefined()
203+
expect(
204+
$ths
205+
.at(2)
206+
.find('.sr-only')
207+
.exists()
208+
).toBe(false)
164209

165210
wrapper.destroy()
166211
})
@@ -351,7 +396,7 @@ describe('table > sorting', () => {
351396
expect(columnA[2]).toBe('2')
352397
// Should have aria-* labels
353398
expect(wrapper.findAll('tfoot > tr > th[aria-sort]').length).toBe(2)
354-
expect(wrapper.findAll('tfoot > tr > th[aria-label]').length).toBe(2)
399+
expect(wrapper.findAll('tfoot > tr > th > .sr-only').length).toBe(2)
355400

356401
// Sort by first column
357402
wrapper
@@ -376,7 +421,7 @@ describe('table > sorting', () => {
376421
expect(columnA[2]).toBe('3')
377422
// Should have aria-* labels
378423
expect(wrapper.findAll('tfoot > tr > th[aria-sort]').length).toBe(2)
379-
expect(wrapper.findAll('tfoot > tr > th[aria-label]').length).toBe(3)
424+
expect(wrapper.findAll('tfoot > tr > th > .sr-only').length).toBe(3)
380425

381426
// Click first column header again to reverse sort
382427
wrapper
@@ -400,7 +445,7 @@ describe('table > sorting', () => {
400445
expect(columnA[2]).toBe('1')
401446
// Should have aria-* labels
402447
expect(wrapper.findAll('tfoot > tr > th[aria-sort]').length).toBe(2)
403-
expect(wrapper.findAll('tfoot > tr > th[aria-label]').length).toBe(3)
448+
expect(wrapper.findAll('tfoot > tr > th > .sr-only').length).toBe(3)
404449

405450
// Click second column header to sort by it (by using keydown.enter)
406451
wrapper
@@ -445,7 +490,7 @@ describe('table > sorting', () => {
445490
expect(columnA[2]).toBe('2')
446491
// Should have aria-* labels
447492
expect(wrapper.findAll('tfoot > tr > th[aria-sort]').length).toBe(2)
448-
expect(wrapper.findAll('tfoot > tr > th[aria-label]').length).toBe(2)
493+
expect(wrapper.findAll('tfoot > tr > th > .sr-only').length).toBe(2)
449494

450495
wrapper.destroy()
451496
})
@@ -483,7 +528,7 @@ describe('table > sorting', () => {
483528
expect(columnA[2]).toBe('2')
484529
// Shouldn't have aria-* labels
485530
expect(wrapper.findAll('tfoot > tr > th[aria-sort]').length).toBe(0)
486-
expect(wrapper.findAll('tfoot > tr > th[aria-label]').length).toBe(0)
531+
expect(wrapper.findAll('tfoot > tr > th > .sr-only').length).toBe(0)
487532

488533
// Click first column
489534
wrapper
@@ -506,7 +551,7 @@ describe('table > sorting', () => {
506551
expect(columnA[2]).toBe('2')
507552
// Shouldn't have aria-* labels
508553
expect(wrapper.findAll('tfoot > tr > th[aria-sort]').length).toBe(0)
509-
expect(wrapper.findAll('tfoot > tr > th[aria-label]').length).toBe(0)
554+
expect(wrapper.findAll('tfoot > tr > th > .sr-only').length).toBe(0)
510555

511556
// Click third column header
512557
wrapper
@@ -529,7 +574,7 @@ describe('table > sorting', () => {
529574
expect(columnA[2]).toBe('2')
530575
// Shouldn't have aria-* labels
531576
expect(wrapper.findAll('tfoot > tr > th[aria-sort]').length).toBe(0)
532-
expect(wrapper.findAll('tfoot > tr > th[aria-label]').length).toBe(0)
577+
expect(wrapper.findAll('tfoot > tr > th > .sr-only').length).toBe(0)
533578

534579
wrapper.destroy()
535580
})
@@ -568,17 +613,32 @@ describe('table > sorting', () => {
568613
// Currently not sorted
569614
expect($ths.at(0).attributes('aria-sort')).toBe('none')
570615
// For switching to descending
571-
expect($ths.at(0).attributes('aria-label')).toBe(wrapper.vm.labelSortDesc)
616+
expect(
617+
$ths
618+
.at(0)
619+
.find('.sr-only')
620+
.text()
621+
).toContain(wrapper.vm.labelSortDesc)
572622

573623
// Not sorted by this column
574624
expect($ths.at(1).attributes('aria-sort')).toBe('none')
575625
// For sorting by ascending
576-
expect($ths.at(1).attributes('aria-label')).toBe(wrapper.vm.labelSortDesc)
626+
expect(
627+
$ths
628+
.at(1)
629+
.find('.sr-only')
630+
.text()
631+
).toContain(wrapper.vm.labelSortDesc)
577632

578633
// Not a sortable column
579634
expect($ths.at(2).attributes('aria-sort')).not.toBeDefined()
580635
// For clearing sorting
581-
expect($ths.at(2).attributes('aria-label')).not.toBeDefined()
636+
expect(
637+
$ths
638+
.at(2)
639+
.find('.sr-only')
640+
.exists()
641+
).toBe(false)
582642

583643
// Change sort direction (should be descending first)
584644
wrapper
@@ -605,17 +665,32 @@ describe('table > sorting', () => {
605665
// Currently sorted as descending
606666
expect($ths.at(0).attributes('aria-sort')).toBe('descending')
607667
// For switching to ascending
608-
expect($ths.at(0).attributes('aria-label')).toBe(wrapper.vm.labelSortAsc)
668+
expect(
669+
$ths
670+
.at(0)
671+
.find('.sr-only')
672+
.text()
673+
).toContain(wrapper.vm.labelSortAsc)
609674

610675
// Not sorted by this column
611676
expect($ths.at(1).attributes('aria-sort')).toBe('none')
612677
// For sorting by ascending
613-
expect($ths.at(1).attributes('aria-label')).toBe(wrapper.vm.labelSortDesc)
678+
expect(
679+
$ths
680+
.at(1)
681+
.find('.sr-only')
682+
.text()
683+
).toContain(wrapper.vm.labelSortDesc)
614684

615685
// Not a sortable column
616686
expect($ths.at(2).attributes('aria-sort')).not.toBeDefined()
617687
// For clearing sorting
618-
expect($ths.at(2).attributes('aria-label')).toBe(wrapper.vm.labelSortClear)
688+
expect(
689+
$ths
690+
.at(2)
691+
.find('.sr-only')
692+
.text()
693+
).toContain(wrapper.vm.labelSortClear)
619694

620695
wrapper.destroy()
621696
})

0 commit comments

Comments
 (0)