diff --git a/src/components/table/README.md b/src/components/table/README.md
index 74aef89bd59..46f71fafcc9 100644
--- a/src/components/table/README.md
+++ b/src/components/table/README.md
@@ -1188,6 +1188,16 @@ as read-only.**
```
+When table is selectable, it will have class `b-table-selectable`, and one of the following three
+classes (depending on which mode is in use), on the `
` element:
+
+- `b-table-select-single`
+- `b-table-select-multi`
+- `b-table-select-range`
+
+When at least one row is selected the class `b-table-selecting` will be active on the ``
+element.
+
**Notes:**
- _Paging, filtering, or sorting will clear the selection. The `row-selected` event will be emitted
diff --git a/src/components/table/_table.scss b/src/components/table/_table.scss
index 14f20c57192..aa6e24c516d 100644
--- a/src/components/table/_table.scss
+++ b/src/components/table/_table.scss
@@ -159,7 +159,15 @@
}
/* b-table: selectable rows */
-table.b-table.b-table-selectable > tbody > tr {
- cursor: pointer;
- // user-select: none;
+.b-table.table.b-table-selectable {
+ & > tbody > tr {
+ cursor: pointer;
+ }
+
+ &.b-table-selecting {
+ // Disabled text-selection when in mode range when at least one row selected
+ &.b-table-select-range > tbody > tr {
+ user-select: none;
+ }
+ }
}
diff --git a/src/components/table/helpers/mixin-selectable.js b/src/components/table/helpers/mixin-selectable.js
index b350dc9e893..2eaba14ee03 100644
--- a/src/components/table/helpers/mixin-selectable.js
+++ b/src/components/table/helpers/mixin-selectable.js
@@ -1,5 +1,5 @@
import looseEqual from '../../../utils/loose-equal'
-import { isArray } from '../../../utils/array'
+import { isArray, arrayIncludes } from '../../../utils/array'
import sanitizeRow from './sanitize-row'
export default {
@@ -23,6 +23,29 @@ export default {
selectedLastRow: -1
}
},
+ computed: {
+ selectableTableClasses() {
+ const selectable = this.selectable
+ const isSelecting = selectable && this.selectedRows && this.selectedRows.some(Boolean)
+ return {
+ 'b-table-selectable': selectable,
+ [`b-table-select-${this.selectMode}`]: selectable,
+ 'b-table-selecting': isSelecting
+ }
+ },
+ selectableTableAttrs() {
+ return {
+ 'aria-multiselectable': this.selectableIsMultiSelect
+ }
+ },
+ selectableIsMultiSelect() {
+ if (this.selectable) {
+ return arrayIncludes(['range', 'multi'], this.selectMode) ? 'true' : 'false'
+ } else {
+ return null
+ }
+ }
+ },
watch: {
computedItems(newVal, oldVal) {
// Reset for selectable
@@ -72,17 +95,18 @@ export default {
isRowSelected(idx) {
return Boolean(this.selectedRows[idx])
},
- rowSelectedClasses(idx) {
- if (this.selectable) {
- const rowSelected = this.isRowSelected(idx)
- const base = this.dark ? 'bg' : 'table'
- const variant = this.selectedVariant
- return {
- 'b-row-selected': rowSelected,
- [`${base}-${variant}`]: rowSelected && variant
- }
- } else {
- return {}
+ selectableRowClasses(idx) {
+ const rowSelected = this.isRowSelected(idx)
+ const base = this.dark ? 'bg' : 'table'
+ const variant = this.selectedVariant
+ return {
+ 'b-table-row-selected': this.selectable && rowSelected,
+ [`${base}-${variant}`]: this.selectable && rowSelected && variant
+ }
+ },
+ selectableRowAttrs(idx) {
+ return {
+ 'aria-selected': !this.selectable ? null : this.isRowSelected(idx) ? 'true' : 'false'
}
},
clearSelected() {
@@ -125,7 +149,6 @@ export default {
idx <= Math.max(this.selectedLastRow, index);
idx++
) {
- // this.$set(this.selectedRows, idx, true)
selectedRows[idx] = true
}
selected = true
@@ -138,7 +161,6 @@ export default {
this.selectedLastRow = selected ? index : -1
}
}
- // this.$set(this.selectedRows, index, selected)
selectedRows[index] = selected
this.selectedRows = selectedRows
}
diff --git a/src/components/table/helpers/mixin-tbody-row.js b/src/components/table/helpers/mixin-tbody-row.js
index a53a4e4519e..17b61256ffc 100644
--- a/src/components/table/helpers/mixin-tbody-row.js
+++ b/src/components/table/helpers/mixin-tbody-row.js
@@ -195,7 +195,6 @@ export default {
const hasRowClickHandler = this.$listeners['row-clicked'] || this.selectable
const $detailsSlot = $scoped['row-details']
const rowShowDetails = Boolean(item._showDetails && $detailsSlot)
- const rowSelected = this.isRowSelected(rowIndex) /* from selctable mixin */
// We can return more than one TR if rowDetails enabled
const $rows = []
@@ -244,7 +243,7 @@ export default {
key: `__b-table-row-${rowKey}__`,
class: [
this.rowClasses(item),
- this.rowSelectedClasses(rowIndex),
+ this.selectableRowClasses(rowIndex),
{
'b-table-has-details': rowShowDetails
}
@@ -256,8 +255,8 @@ export default {
'aria-describedby': detailsId,
'aria-owns': detailsId,
'aria-rowindex': ariaRowIndex,
- 'aria-selected': this.selectable ? (rowSelected ? 'true' : 'false') : null,
- role: 'row'
+ role: 'row',
+ ...this.selectableRowAttrs(rowIndex)
},
on: {
// TODO: only instatiate handlers if we have registered listeners (except row-clicked)
diff --git a/src/components/table/table-selectable.spec.js b/src/components/table/table-selectable.spec.js
index 5ebf113454e..b495d528900 100644
--- a/src/components/table/table-selectable.spec.js
+++ b/src/components/table/table-selectable.spec.js
@@ -28,6 +28,12 @@ describe('table row select', () => {
})
expect(wrapper).toBeDefined()
await wrapper.vm.$nextTick()
+ expect(wrapper.attributes('aria-multiselectable')).not.toBeDefined()
+ expect(wrapper.classes()).not.toContain('b-table-selectable')
+ expect(wrapper.classes()).not.toContain('b-table-selecting')
+ expect(wrapper.classes()).not.toContain('b-table-select-single')
+ expect(wrapper.classes()).not.toContain('b-table-select-multi')
+ expect(wrapper.classes()).not.toContain('b-table-select-range')
const $rows = wrapper.findAll('tbody > tr')
expect($rows.length).toBe(4)
// Doesn't have aria-selected attribute on all TRs
@@ -50,6 +56,12 @@ describe('table row select', () => {
})
expect(wrapper).toBeDefined()
await wrapper.vm.$nextTick()
+ expect(wrapper.attributes('aria-multiselectable')).not.toBeDefined()
+ expect(wrapper.classes()).not.toContain('b-table-selectable')
+ expect(wrapper.classes()).not.toContain('b-table-selecting')
+ expect(wrapper.classes()).not.toContain('b-table-select-single')
+ expect(wrapper.classes()).not.toContain('b-table-select-multi')
+ expect(wrapper.classes()).not.toContain('b-table-select-range')
const $rows = wrapper.findAll('tbody > tr')
expect($rows.length).toBe(4)
// Doesn't have aria-selected attribute on all TRs
@@ -73,6 +85,12 @@ describe('table row select', () => {
expect(wrapper).toBeDefined()
await wrapper.vm.$nextTick()
+ expect(wrapper.attributes('aria-multiselectable')).toBe('false')
+ expect(wrapper.classes()).toContain('b-table-selectable')
+ expect(wrapper.classes()).toContain('b-table-select-single')
+ expect(wrapper.classes()).not.toContain('b-table-selecting')
+ expect(wrapper.classes()).not.toContain('b-table-select-multi')
+ expect(wrapper.classes()).not.toContain('b-table-select-range')
expect(wrapper.emitted('row-selected')).not.toBeDefined()
$rows = wrapper.findAll('tbody > tr')
expect($rows.length).toBe(4)
@@ -94,8 +112,13 @@ describe('table row select', () => {
expect($rows.at(1).is('[aria-selected="false"]')).toBe(true)
expect($rows.at(2).is('[aria-selected="false"]')).toBe(true)
expect($rows.at(3).is('[aria-selected="false"]')).toBe(true)
+ expect(wrapper.classes()).toContain('b-table-selectable')
+ expect(wrapper.classes()).toContain('b-table-select-single')
+ expect(wrapper.classes()).toContain('b-table-selecting')
+ expect(wrapper.classes()).not.toContain('b-table-select-multi')
+ expect(wrapper.classes()).not.toContain('b-table-select-range')
- // Click third row
+ // Click third row to select it
wrapper
.findAll('tbody > tr')
.at(2)
@@ -109,8 +132,13 @@ describe('table row select', () => {
expect($rows.at(1).is('[aria-selected="false"]')).toBe(true)
expect($rows.at(2).is('[aria-selected="true"]')).toBe(true)
expect($rows.at(3).is('[aria-selected="false"]')).toBe(true)
+ expect(wrapper.classes()).toContain('b-table-selectable')
+ expect(wrapper.classes()).toContain('b-table-select-single')
+ expect(wrapper.classes()).toContain('b-table-selecting')
+ expect(wrapper.classes()).not.toContain('b-table-select-multi')
+ expect(wrapper.classes()).not.toContain('b-table-select-range')
- // Click third row again
+ // Click third row again to clear selection
wrapper
.findAll('tbody > tr')
.at(2)
@@ -124,6 +152,11 @@ describe('table row select', () => {
expect($rows.at(1).is('[aria-selected="false"]')).toBe(true)
expect($rows.at(2).is('[aria-selected="false"]')).toBe(true)
expect($rows.at(3).is('[aria-selected="false"]')).toBe(true)
+ expect(wrapper.classes()).toContain('b-table-selectable')
+ expect(wrapper.classes()).toContain('b-table-select-single')
+ expect(wrapper.classes()).not.toContain('b-table-selecting')
+ expect(wrapper.classes()).not.toContain('b-table-select-multi')
+ expect(wrapper.classes()).not.toContain('b-table-select-range')
wrapper.destroy()
})
@@ -140,6 +173,12 @@ describe('table row select', () => {
let $rows
expect(wrapper).toBeDefined()
await wrapper.vm.$nextTick()
+ expect(wrapper.attributes('aria-multiselectable')).toBe('true')
+ expect(wrapper.classes()).toContain('b-table-selectable')
+ expect(wrapper.classes()).toContain('b-table-select-multi')
+ expect(wrapper.classes()).not.toContain('b-table-select-single')
+ expect(wrapper.classes()).not.toContain('b-table-select-range')
+ expect(wrapper.classes()).not.toContain('b-table-selecting')
expect(wrapper.emitted('row-selected')).not.toBeDefined()
// Click first row
@@ -157,6 +196,11 @@ describe('table row select', () => {
expect($rows.at(1).is('[aria-selected="false"]')).toBe(true)
expect($rows.at(2).is('[aria-selected="false"]')).toBe(true)
expect($rows.at(3).is('[aria-selected="false"]')).toBe(true)
+ expect(wrapper.classes()).toContain('b-table-selectable')
+ expect(wrapper.classes()).toContain('b-table-selecting')
+ expect(wrapper.classes()).toContain('b-table-select-multi')
+ expect(wrapper.classes()).not.toContain('b-table-select-single')
+ expect(wrapper.classes()).not.toContain('b-table-select-range')
// Click third row
wrapper
@@ -172,6 +216,11 @@ describe('table row select', () => {
expect($rows.at(1).is('[aria-selected="false"]')).toBe(true)
expect($rows.at(2).is('[aria-selected="true"]')).toBe(true)
expect($rows.at(3).is('[aria-selected="false"]')).toBe(true)
+ expect(wrapper.classes()).toContain('b-table-selectable')
+ expect(wrapper.classes()).toContain('b-table-selecting')
+ expect(wrapper.classes()).toContain('b-table-select-multi')
+ expect(wrapper.classes()).not.toContain('b-table-select-single')
+ expect(wrapper.classes()).not.toContain('b-table-select-range')
// Click third row again
wrapper
@@ -187,6 +236,11 @@ describe('table row select', () => {
expect($rows.at(1).is('[aria-selected="false"]')).toBe(true)
expect($rows.at(2).is('[aria-selected="false"]')).toBe(true)
expect($rows.at(3).is('[aria-selected="false"]')).toBe(true)
+ expect(wrapper.classes()).toContain('b-table-selectable')
+ expect(wrapper.classes()).toContain('b-table-selecting')
+ expect(wrapper.classes()).toContain('b-table-select-multi')
+ expect(wrapper.classes()).not.toContain('b-table-select-single')
+ expect(wrapper.classes()).not.toContain('b-table-select-range')
// Click first row again
wrapper
@@ -202,6 +256,11 @@ describe('table row select', () => {
expect($rows.at(1).is('[aria-selected="false"]')).toBe(true)
expect($rows.at(2).is('[aria-selected="false"]')).toBe(true)
expect($rows.at(3).is('[aria-selected="false"]')).toBe(true)
+ expect(wrapper.classes()).toContain('b-table-selectable')
+ expect(wrapper.classes()).toContain('b-table-select-multi')
+ expect(wrapper.classes()).not.toContain('b-table-selecting')
+ expect(wrapper.classes()).not.toContain('b-table-select-single')
+ expect(wrapper.classes()).not.toContain('b-table-select-range')
wrapper.destroy()
})
@@ -218,6 +277,12 @@ describe('table row select', () => {
let $rows
expect(wrapper).toBeDefined()
await wrapper.vm.$nextTick()
+ expect(wrapper.attributes('aria-multiselectable')).toBe('true')
+ expect(wrapper.classes()).toContain('b-table-selectable')
+ expect(wrapper.classes()).toContain('b-table-select-range')
+ expect(wrapper.classes()).not.toContain('b-table-selecting')
+ expect(wrapper.classes()).not.toContain('b-table-select-single')
+ expect(wrapper.classes()).not.toContain('b-table-select-multi')
expect(wrapper.emitted('row-selected')).not.toBeDefined()
$rows = wrapper.findAll('tbody > tr')
expect($rows.is('[tabindex="0"]')).toBe(true)
@@ -238,6 +303,11 @@ describe('table row select', () => {
expect($rows.at(1).is('[aria-selected="false"]')).toBe(true)
expect($rows.at(2).is('[aria-selected="false"]')).toBe(true)
expect($rows.at(3).is('[aria-selected="false"]')).toBe(true)
+ expect(wrapper.classes()).toContain('b-table-selectable')
+ expect(wrapper.classes()).toContain('b-table-select-range')
+ expect(wrapper.classes()).toContain('b-table-selecting')
+ expect(wrapper.classes()).not.toContain('b-table-select-single')
+ expect(wrapper.classes()).not.toContain('b-table-select-multi')
// Shift-Click third row
wrapper
@@ -257,6 +327,11 @@ describe('table row select', () => {
expect($rows.at(1).is('[aria-selected="true"]')).toBe(true)
expect($rows.at(2).is('[aria-selected="true"]')).toBe(true)
expect($rows.at(3).is('[aria-selected="false"]')).toBe(true)
+ expect(wrapper.classes()).toContain('b-table-selectable')
+ expect(wrapper.classes()).toContain('b-table-select-range')
+ expect(wrapper.classes()).toContain('b-table-selecting')
+ expect(wrapper.classes()).not.toContain('b-table-select-single')
+ expect(wrapper.classes()).not.toContain('b-table-select-multi')
// Click third row again
wrapper
@@ -272,6 +347,11 @@ describe('table row select', () => {
expect($rows.at(1).is('[aria-selected="false"]')).toBe(true)
expect($rows.at(2).is('[aria-selected="true"]')).toBe(true)
expect($rows.at(3).is('[aria-selected="false"]')).toBe(true)
+ expect(wrapper.classes()).toContain('b-table-selectable')
+ expect(wrapper.classes()).toContain('b-table-select-range')
+ expect(wrapper.classes()).toContain('b-table-selecting')
+ expect(wrapper.classes()).not.toContain('b-table-select-single')
+ expect(wrapper.classes()).not.toContain('b-table-select-multi')
// Click fourth row
wrapper
@@ -287,6 +367,11 @@ describe('table row select', () => {
expect($rows.at(1).is('[aria-selected="false"]')).toBe(true)
expect($rows.at(2).is('[aria-selected="false"]')).toBe(true)
expect($rows.at(3).is('[aria-selected="true"]')).toBe(true)
+ expect(wrapper.classes()).toContain('b-table-selectable')
+ expect(wrapper.classes()).toContain('b-table-select-range')
+ expect(wrapper.classes()).toContain('b-table-selecting')
+ expect(wrapper.classes()).not.toContain('b-table-select-single')
+ expect(wrapper.classes()).not.toContain('b-table-select-multi')
// Click fourth row again
wrapper
@@ -302,6 +387,11 @@ describe('table row select', () => {
expect($rows.at(1).is('[aria-selected="false"]')).toBe(true)
expect($rows.at(2).is('[aria-selected="false"]')).toBe(true)
expect($rows.at(3).is('[aria-selected="true"]')).toBe(true)
+ expect(wrapper.classes()).toContain('b-table-selectable')
+ expect(wrapper.classes()).toContain('b-table-select-range')
+ expect(wrapper.classes()).toContain('b-table-selecting')
+ expect(wrapper.classes()).not.toContain('b-table-select-single')
+ expect(wrapper.classes()).not.toContain('b-table-select-multi')
// Ctrl-Click second row
wrapper
@@ -317,6 +407,11 @@ describe('table row select', () => {
expect($rows.at(1).is('[aria-selected="true"]')).toBe(true)
expect($rows.at(2).is('[aria-selected="false"]')).toBe(true)
expect($rows.at(3).is('[aria-selected="true"]')).toBe(true)
+ expect(wrapper.classes()).toContain('b-table-selectable')
+ expect(wrapper.classes()).toContain('b-table-select-range')
+ expect(wrapper.classes()).toContain('b-table-selecting')
+ expect(wrapper.classes()).not.toContain('b-table-select-single')
+ expect(wrapper.classes()).not.toContain('b-table-select-multi')
// Ctrl-Click second row
wrapper
@@ -332,6 +427,11 @@ describe('table row select', () => {
expect($rows.at(1).is('[aria-selected="false"]')).toBe(true)
expect($rows.at(2).is('[aria-selected="false"]')).toBe(true)
expect($rows.at(3).is('[aria-selected="true"]')).toBe(true)
+ expect(wrapper.classes()).toContain('b-table-selectable')
+ expect(wrapper.classes()).toContain('b-table-select-range')
+ expect(wrapper.classes()).toContain('b-table-selecting')
+ expect(wrapper.classes()).not.toContain('b-table-select-single')
+ expect(wrapper.classes()).not.toContain('b-table-select-multi')
// Ctrl-Click fourth row
wrapper
@@ -347,6 +447,11 @@ describe('table row select', () => {
expect($rows.at(1).is('[aria-selected="false"]')).toBe(true)
expect($rows.at(2).is('[aria-selected="false"]')).toBe(true)
expect($rows.at(3).is('[aria-selected="false"]')).toBe(true)
+ expect(wrapper.classes()).toContain('b-table-selectable')
+ expect(wrapper.classes()).toContain('b-table-select-range')
+ expect(wrapper.classes()).not.toContain('b-table-selecting')
+ expect(wrapper.classes()).not.toContain('b-table-select-single')
+ expect(wrapper.classes()).not.toContain('b-table-select-multi')
wrapper.destroy()
})
@@ -572,6 +677,8 @@ describe('table row select', () => {
expect($rows.at(1).is('[aria-selected="false"]')).toBe(true)
expect($rows.at(2).is('[aria-selected="false"]')).toBe(true)
expect($rows.at(3).is('[aria-selected="false"]')).toBe(true)
+ expect(wrapper.classes()).toContain('b-table-selectable')
+ expect(wrapper.classes()).not.toContain('b-table-selecting-range')
// Disabled selectable
wrapper.setProps({
@@ -584,6 +691,8 @@ describe('table row select', () => {
// Should remove tabindex and aria-selected attributes
expect($rows.is('[tabindex]')).toBe(false)
expect($rows.is('[aria-selected]')).toBe(false)
+ expect(wrapper.classes()).not.toContain('b-table-selectable')
+ expect(wrapper.classes()).not.toContain('b-table-selecting-range')
wrapper.destroy()
})
diff --git a/src/components/table/table.js b/src/components/table/table.js
index fb484250fd7..1678a43955e 100644
--- a/src/components/table/table.js
+++ b/src/components/table/table.js
@@ -186,19 +186,43 @@ export default {
: ''
},
tableClasses() {
+ return [
+ {
+ 'table-striped': this.striped,
+ 'table-hover': this.hover,
+ 'table-dark': this.dark,
+ 'table-bordered': this.bordered,
+ 'table-borderless': this.borderless,
+ 'table-sm': this.small,
+ border: this.outlined,
+ // The following are b-table custom styles
+ 'b-table-fixed': this.fixed,
+ 'b-table-stacked': this.stacked === true || this.stacked === '',
+ [`b-table-stacked-${this.stacked}`]: this.stacked !== true && this.stacked
+ },
+ // Selectable classes
+ this.selectableTableClasses
+ ]
+ },
+ tableAttrs() {
+ // Preserve user supplied aria-describedby, if provided in $attrs
+ const adb =
+ [(this.$attrs || {})['aria-describedby'], this.captionId].filter(Boolean).join(' ') || null
+ const items = this.computedItems
+ const fields = this.computedFields
return {
- 'table-striped': this.striped,
- 'table-hover': this.hover,
- 'table-dark': this.dark,
- 'table-bordered': this.bordered,
- 'table-borderless': this.borderless,
- 'table-sm': this.small,
- border: this.outlined,
- // The following are b-table custom styles
- 'b-table-fixed': this.fixed,
- 'b-table-stacked': this.stacked === true || this.stacked === '',
- [`b-table-stacked-${this.stacked}`]: this.stacked !== true && this.stacked,
- 'b-table-selectable': this.selectable
+ // We set aria-rowcount before merging in $attrs, in case user has supplied their own
+ 'aria-rowcount':
+ this.filteredItems.length > items.length ? String(this.filteredItems.length) : null,
+ // Merge in user supplied $attrs if any
+ ...this.$attrs,
+ // Now we can override any $attrs here
+ id: this.safeId(),
+ role: this.isStacked ? 'table' : null,
+ 'aria-busy': this.computedBusy ? 'true' : 'false',
+ 'aria-colcount': String(fields.length),
+ 'aria-describedby': adb,
+ ...this.selectableTableAttrs
}
},
// Items related computed props
@@ -477,9 +501,6 @@ export default {
}
},
render(h) {
- const fields = this.computedFields
- const items = this.computedItems
-
// Build the caption (from caption mixin)
const $caption = this.renderCaption()
@@ -502,31 +523,7 @@ export default {
key: 'b-table',
staticClass: 'table b-table',
class: this.tableClasses,
- attrs: {
- // We set aria-rowcount before merging in $attrs, in case user has supplied their own
- 'aria-rowcount':
- this.filteredItems.length > items.length ? String(this.filteredItems.length) : null,
- // Merge in user supplied $attrs if any
- ...this.$attrs,
- // Now we can override any $attrs here
- id: this.safeId(),
- role: this.isStacked ? 'table' : null,
- 'aria-multiselectable': this.selectable
- ? this.selectMode === 'single'
- ? 'false'
- : 'true'
- : null,
- 'aria-busy': this.computedBusy ? 'true' : 'false',
- 'aria-colcount': String(fields.length),
- 'aria-describedby':
- [
- // Preserve user supplied aria-describedby, if provided in $attrs
- (this.$attrs || {})['aria-describedby'],
- this.captionId
- ]
- .filter(a => a)
- .join(' ') || null
- }
+ attrs: this.tableAttrs
},
[$caption, $colgroup, $thead, $tfoot, $tbody]
)