diff --git a/src/components/table/README.md b/src/components/table/README.md index 9a17209cdea..da42b05158b 100644 --- a/src/components/table/README.md +++ b/src/components/table/README.md @@ -1403,6 +1403,25 @@ When a table is `selectable` and the user clicks on a row, `` will emit event, passing a single argument which is the complete list of selected items. **Treat this argument as read-only.** +Rows can also be programmatically selected and unselected via the following exposed methods on the +`` instance (i.e. via a reference to the table instance via `this.$refs`): + +| Method | Description | +| ---------------------- | ---------------------------------------------------------------------------------------------------- | +| `selectRow(index)` | Selects a row with the given `index` number. | +| `unselectRow(index)` | Unselects a row with the given `index` number. | +| `selectAllRows()` | Selects all rows in the table, except in `single` mode in which case only the first row is selected. | +| `clearSelected()` | Unselects all rows. | +| `isRowSelected(index)` | Returns `true` if the row with the given `index` is selected, otherwise it returns `false`. | + +Programmatic selection notes: + +- `index` the zero-based index of the table's **visible rows**, after filtering, sorting, and + pagination have been applied. +- In `single` mode, `selectRow(index)` will unselect any previous selected row. +- Attempting to `selectRow(index)` or `unselectRow(index)` on a non-existent row will be ignored. +- The table must be `selectable` for any of these methods to have effect. + ```html @@ -1455,8 +1481,22 @@ as read-only.** } }, methods: { - rowSelected(items) { + onRowSelected(items) { this.selected = items + }, + selectAllRows() { + this.$refs.selectableTable.selectAllRows() + }, + clearSelected() { + this.$refs.selectableTable.clearSelected() + }, + selectThirdRow() { + // Rows are indexed from 0, so the third row is index 2 + this.$refs.selectableTable.selectRow(2) + }, + unselectThirdRow() { + // Rows are indexed from 0, so the third row is index 2 + this.$refs.selectableTable.unselectRow(2) } } } @@ -1465,7 +1505,7 @@ as read-only.** ``` -When table is selectable, it will have class `b-table-selectable`, and one of the following three +When a 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` @@ -1475,13 +1515,22 @@ classes (depending on which mode is in use), on the `
` element: When at least one row is selected the class `b-table-selecting` will be active on the `
` element. +Use the prop `selected-variant` to apply a Bootstrap theme color to the selected row(s). Note, due +to the order that the table variants are defined in Bootstrap's CSS, any row-variant's may take +precedence over the `selected-variant`. You can set `selected-variant` to an empty string if you +will be using other means to convey that a row is selected (such as a scoped field slot in the above +example). + **Notes:** - Paging, filtering, or sorting will clear the selection. The `row-selected` event will be emitted with an empty array if needed. - Selected rows will have a class of `b-row-selected` added to them. - When the table is in `selectable` mode, all data item `` elements will be in the document tab - sequence (`tabindex="0"`) for accessibility reasons. + sequence (`tabindex="0"`) for [accessibility](#accessibility) reasons, and will have the attribute + `aria-selected` set to either `'true'` or `'false'` depending on the selected state of the row. +- When a table is `selectable`, the table will have the attribute `aria-multiselect` set to either + `'false'` for `single` mode, and `'true'` for either `multi` or `range` modes. ### Table body transition support diff --git a/src/components/table/helpers/mixin-selectable.js b/src/components/table/helpers/mixin-selectable.js index 355245ce577..65fa7ccfe85 100644 --- a/src/components/table/helpers/mixin-selectable.js +++ b/src/components/table/helpers/mixin-selectable.js @@ -1,6 +1,8 @@ import looseEqual from '../../../utils/loose-equal' +import range from '../../../utils/range' import { isArray, arrayIncludes } from '../../../utils/array' import { getComponentConfig } from '../../../utils/config' +import { isNumber } from '../../../utils/inspect' import sanitizeRow from './sanitize-row' export default { @@ -11,7 +13,8 @@ export default { }, selectMode: { type: String, - default: 'multi' + default: 'multi', + validator: val => arrayIncludes(['range', 'multi', 'single'], val) }, selectedVariant: { type: String, @@ -25,36 +28,42 @@ export default { } }, computed: { + isSelectable() { + return this.selectable && this.selectMode + }, + selectableHasSelection() { + return ( + this.isSelectable && + this.selectedRows && + this.selectedRows.length > 0 && + this.selectedRows.some(Boolean) + ) + }, + selectableIsMultiSelect() { + return this.isSelectable && arrayIncludes(['range', 'multi'], this.selectMode) + }, 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 + 'b-table-selectable': this.isSelectable, + [`b-table-select-${this.selectMode}`]: this.isSelectable, + 'b-table-selecting': this.selectableHasSelection } }, selectableTableAttrs() { return { - 'aria-multiselectable': this.selectableIsMultiSelect - } - }, - selectableIsMultiSelect() { - if (this.selectable) { - return arrayIncludes(['range', 'multi'], this.selectMode) ? 'true' : 'false' - } else { - return null + 'aria-multiselectable': !this.isSelectable + ? null + : this.selectableIsMultiSelect + ? 'true' + : 'false' } } }, watch: { computedItems(newVal, oldVal) { // Reset for selectable - // TODO: Should selectedLastClicked be reset here? - // As changes to _showDetails would trigger it to reset - this.selectedLastRow = -1 let equal = false - if (this.selectable && this.selectedRows.length > 0) { + if (this.isSelectable && this.selectedRows.length > 0) { // Quick check against array length equal = isArray(newVal) && isArray(oldVal) && newVal.length === oldVal.length for (let i = 0; equal && i < newVal.length; i++) { @@ -74,9 +83,9 @@ export default { this.clearSelected() }, selectedRows(selectedRows, oldVal) { - if (this.selectable && !looseEqual(selectedRows, oldVal)) { + if (this.isSelectable && !looseEqual(selectedRows, oldVal)) { const items = [] - // forEach skips over non-existant indicies (on sparse arrays) + // `.forEach()` skips over non-existent indices (on sparse arrays) selectedRows.forEach((v, idx) => { if (v) { items.push(this.computedItems[idx]) @@ -88,35 +97,67 @@ export default { }, beforeMount() { // Set up handlers - if (this.selectable) { + if (this.isSelectable) { this.setSelectionHandlers(true) } }, methods: { - isRowSelected(idx) { - return Boolean(this.selectedRows[idx]) + // Public methods + selectRow(index) { + // Select a particular row (indexed based on computedItems) + if ( + this.isSelectable && + isNumber(index) && + index >= 0 && + index < this.computedItems.length && + !this.isRowSelected(index) + ) { + const selectedRows = this.selectableIsMultiSelect ? this.selectedRows.slice() : [] + selectedRows[index] = true + this.selectedLastClicked = -1 + this.selectedRows = selectedRows + } }, - 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 + unselectRow(index) { + // Un-select a particular row (indexed based on `computedItems`) + if (this.isSelectable && isNumber(index) && this.isRowSelected(index)) { + const selectedRows = this.selectedRows.slice() + selectedRows[index] = false + this.selectedLastClicked = -1 + this.selectedRows = selectedRows } }, - selectableRowAttrs(idx) { - return { - 'aria-selected': !this.selectable ? null : this.isRowSelected(idx) ? 'true' : 'false' + selectAllRows() { + const length = this.computedItems.length + if (this.isSelectable && length > 0) { + this.selectedLastClicked = -1 + this.selectedRows = this.selectableIsMultiSelect ? range(length).map(i => true) : [true] } }, + isRowSelected(index) { + // Determine if a row is selected (indexed based on `computedItems`) + return Boolean(isNumber(index) && this.selectedRows[index]) + }, clearSelected() { - const hasSelection = this.selectedRows.reduce((prev, v) => { - return prev || v - }, false) - if (hasSelection) { - this.selectedLastClicked = -1 - this.selectedRows = [] + // Clear any active selected row(s) + this.selectedLastClicked = -1 + this.selectedRows = [] + }, + // Internal private methods + selectableRowClasses(index) { + if (this.isSelectable && this.isRowSelected(index)) { + const variant = this.selectedVariant + return { + 'b-table-row-selected': true, + [`${this.dark ? 'bg' : 'table'}-${variant}`]: variant + } + } else { + return {} + } + }, + selectableRowAttrs(index) { + return { + 'aria-selected': !this.isSelectable ? null : this.isRowSelected(index) ? 'true' : 'false' } }, setSelectionHandlers(on) { @@ -129,20 +170,20 @@ export default { }, selectionHandler(item, index, evt) { /* istanbul ignore if: should never happen */ - if (!this.selectable) { + if (!this.isSelectable) { // Don't do anything if table is not in selectable mode /* istanbul ignore next: should never happen */ this.clearSelected() /* istanbul ignore next: should never happen */ return } + const selectMode = this.selectMode let selectedRows = this.selectedRows.slice() let selected = !selectedRows[index] - const mode = this.selectMode - // Note 'multi' mode needs no special handling - if (mode === 'single') { + // Note 'multi' mode needs no special event handling + if (selectMode === 'single') { selectedRows = [] - } else if (mode === 'range') { + } else if (selectMode === 'range') { if (this.selectedLastRow > -1 && evt.shiftKey) { // range for ( @@ -155,7 +196,7 @@ export default { selected = true } else { if (!(evt.ctrlKey || evt.metaKey)) { - // clear range selection if any + // Clear range selection if any selectedRows = [] selected = true } diff --git a/src/components/table/helpers/mixin-tbody-row.js b/src/components/table/helpers/mixin-tbody-row.js index 9af7e6a9ca1..4c95667a563 100644 --- a/src/components/table/helpers/mixin-tbody-row.js +++ b/src/components/table/helpers/mixin-tbody-row.js @@ -182,7 +182,7 @@ export default { } if (this.selectedRows) { // Add in rowSelected scope property if selectable rows supported - slotScope.rowSelected = Boolean(this.selectedRows[rowIndex]) + slotScope.rowSelected = this.isRowSelected(rowIndex) } // TODO: // Using `field.key` as scoped slot name is deprecated, to be removed in future release @@ -203,7 +203,7 @@ export default { const tableStriped = this.striped const hasDetailsSlot = this.hasNormalizedSlot(detailsSlotName) const rowShowDetails = Boolean(item._showDetails && hasDetailsSlot) - const hasRowClickHandler = this.$listeners['row-clicked'] || this.selectable + const hasRowClickHandler = this.$listeners['row-clicked'] || this.isSelectable // We can return more than one TR if rowDetails enabled const $rows = [] diff --git a/src/components/table/index.d.ts b/src/components/table/index.d.ts index f0c9900f5a2..353d7412608 100644 --- a/src/components/table/index.d.ts +++ b/src/components/table/index.d.ts @@ -15,6 +15,10 @@ export declare class BTable extends BvComponent { // Public methods refresh: () => void clearSelected: () => void + selectAllRows: () => void + isRowSelected: (index: number) => boolean + selectRow: (index: number) => void + unselectRow: (index: number) => void // Props id?: string items: Array | BvTableProviderCallback diff --git a/src/components/table/table-selectable.spec.js b/src/components/table/table-selectable.spec.js index 4b9769b517b..4fc3f708d90 100644 --- a/src/components/table/table-selectable.spec.js +++ b/src/components/table/table-selectable.spec.js @@ -697,4 +697,334 @@ describe('table > row select', () => { wrapper.destroy() }) + + it('method `selectAllRows()` in single mode selects only first row', async () => { + const wrapper = mount(BTable, { + propsData: { + fields: testFields, + items: testItems, + selectable: true, + selectMode: 'single' + } + }) + + expect(wrapper).toBeDefined() + await waitNT(wrapper.vm) + expect(wrapper.emitted('row-selected')).not.toBeDefined() + + // Execute selectAllRows() method + wrapper.vm.selectAllRows() + await waitNT(wrapper.vm) + + expect(wrapper.emitted('row-selected')).toBeDefined() + expect(wrapper.emitted('row-selected').length).toBe(1) + expect(wrapper.emitted('row-selected')[0][0].length).toBe(1) + expect(wrapper.emitted('row-selected')[0][0]).toEqual([testItems[0]]) + const $rows = wrapper.findAll('tbody > tr') + expect($rows.is('[tabindex="0"]')).toBe(true) + expect($rows.at(0).is('[aria-selected="true"]')).toBe(true) + 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) + + wrapper.destroy() + }) + + it('method `selectAllRows()` in multi mode selects all rows', async () => { + const wrapper = mount(BTable, { + propsData: { + fields: testFields, + items: testItems, + selectable: true, + selectMode: 'multi' + } + }) + + expect(wrapper).toBeDefined() + await waitNT(wrapper.vm) + expect(wrapper.emitted('row-selected')).not.toBeDefined() + + // Execute selectAllRows() method + wrapper.vm.selectAllRows() + await waitNT(wrapper.vm) + + expect(wrapper.emitted('row-selected')).toBeDefined() + expect(wrapper.emitted('row-selected').length).toBe(1) + expect(wrapper.emitted('row-selected')[0][0].length).toBe(4) + expect(wrapper.emitted('row-selected')[0][0]).toEqual(testItems) + const $rows = wrapper.findAll('tbody > tr') + expect($rows.is('[tabindex="0"]')).toBe(true) + expect($rows.at(0).is('[aria-selected="true"]')).toBe(true) + 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="true"]')).toBe(true) + + wrapper.destroy() + }) + + it('method `selectAllRows()` in range mode selects all rows', async () => { + const wrapper = mount(BTable, { + propsData: { + fields: testFields, + items: testItems, + selectable: true, + selectMode: 'range' + } + }) + + expect(wrapper).toBeDefined() + await waitNT(wrapper.vm) + expect(wrapper.emitted('row-selected')).not.toBeDefined() + + // Execute selectAllRows() method + wrapper.vm.selectAllRows() + await waitNT(wrapper.vm) + + expect(wrapper.emitted('row-selected')).toBeDefined() + expect(wrapper.emitted('row-selected').length).toBe(1) + expect(wrapper.emitted('row-selected')[0][0].length).toBe(4) + expect(wrapper.emitted('row-selected')[0][0]).toEqual(testItems) + const $rows = wrapper.findAll('tbody > tr') + expect($rows.is('[tabindex="0"]')).toBe(true) + expect($rows.at(0).is('[aria-selected="true"]')).toBe(true) + 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="true"]')).toBe(true) + + wrapper.destroy() + }) + + it('method `selectRow()` and `unselectRow()` in single mode works', async () => { + const wrapper = mount(BTable, { + propsData: { + fields: testFields, + items: testItems, + selectable: true, + selectMode: 'single' + } + }) + + let $rows + expect(wrapper).toBeDefined() + await waitNT(wrapper.vm) + expect(wrapper.emitted('row-selected')).not.toBeDefined() + + // Execute selectRow() method (second row) + wrapper.vm.selectRow(1) + await waitNT(wrapper.vm) + + expect(wrapper.emitted('row-selected')).toBeDefined() + expect(wrapper.emitted('row-selected').length).toBe(1) + expect(wrapper.emitted('row-selected')[0][0].length).toBe(1) + expect(wrapper.emitted('row-selected')[0][0]).toEqual([testItems[1]]) + $rows = wrapper.findAll('tbody > tr') + expect($rows.is('[tabindex="0"]')).toBe(true) + expect($rows.at(0).is('[aria-selected="false"]')).toBe(true) + 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="false"]')).toBe(true) + + // Execute selectRow() method (fourth row) + wrapper.vm.selectRow(3) + await waitNT(wrapper.vm) + + expect(wrapper.emitted('row-selected')).toBeDefined() + expect(wrapper.emitted('row-selected').length).toBe(2) + expect(wrapper.emitted('row-selected')[1][0].length).toBe(1) + expect(wrapper.emitted('row-selected')[1][0]).toEqual([testItems[3]]) + $rows = wrapper.findAll('tbody > tr') + expect($rows.is('[tabindex="0"]')).toBe(true) + expect($rows.at(0).is('[aria-selected="false"]')).toBe(true) + 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) + + // Execute unselectRow() method on non-selected row (should not change anything) + wrapper.vm.unselectRow(0) + await waitNT(wrapper.vm) + + expect(wrapper.emitted('row-selected')).toBeDefined() + expect(wrapper.emitted('row-selected').length).toBe(2) + expect(wrapper.emitted('row-selected')[1][0].length).toBe(1) + expect(wrapper.emitted('row-selected')[1][0]).toEqual([testItems[3]]) + $rows = wrapper.findAll('tbody > tr') + expect($rows.is('[tabindex="0"]')).toBe(true) + expect($rows.at(0).is('[aria-selected="false"]')).toBe(true) + 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) + + // Execute unselectRow() method on selected row + wrapper.vm.unselectRow(3) + await waitNT(wrapper.vm) + + expect(wrapper.emitted('row-selected')).toBeDefined() + expect(wrapper.emitted('row-selected').length).toBe(3) + expect(wrapper.emitted('row-selected')[2][0].length).toBe(0) + expect(wrapper.emitted('row-selected')[2][0]).toEqual([]) + $rows = wrapper.findAll('tbody > tr') + expect($rows.is('[tabindex="0"]')).toBe(true) + expect($rows.at(0).is('[aria-selected="false"]')).toBe(true) + 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) + + wrapper.destroy() + }) + + it('method `selectRow()` and `unselectRow()` in multi mode works', async () => { + const wrapper = mount(BTable, { + propsData: { + fields: testFields, + items: testItems, + selectable: true, + selectMode: 'multi' + } + }) + + let $rows + expect(wrapper).toBeDefined() + await waitNT(wrapper.vm) + expect(wrapper.emitted('row-selected')).not.toBeDefined() + + // Execute selectRow() method (second row) + wrapper.vm.selectRow(1) + await waitNT(wrapper.vm) + + expect(wrapper.emitted('row-selected')).toBeDefined() + expect(wrapper.emitted('row-selected').length).toBe(1) + expect(wrapper.emitted('row-selected')[0][0].length).toBe(1) + expect(wrapper.emitted('row-selected')[0][0]).toEqual([testItems[1]]) + $rows = wrapper.findAll('tbody > tr') + expect($rows.is('[tabindex="0"]')).toBe(true) + expect($rows.at(0).is('[aria-selected="false"]')).toBe(true) + 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="false"]')).toBe(true) + + // Execute selectRow() method (fourth row) + wrapper.vm.selectRow(3) + await waitNT(wrapper.vm) + + expect(wrapper.emitted('row-selected')).toBeDefined() + expect(wrapper.emitted('row-selected').length).toBe(2) + expect(wrapper.emitted('row-selected')[1][0].length).toBe(2) + expect(wrapper.emitted('row-selected')[1][0]).toEqual([testItems[1], testItems[3]]) + $rows = wrapper.findAll('tbody > tr') + expect($rows.is('[tabindex="0"]')).toBe(true) + expect($rows.at(0).is('[aria-selected="false"]')).toBe(true) + 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) + + // Execute unselectRow() method on non-selected row (should not change anything) + wrapper.vm.unselectRow(0) + await waitNT(wrapper.vm) + + expect(wrapper.emitted('row-selected')).toBeDefined() + expect(wrapper.emitted('row-selected').length).toBe(2) + expect(wrapper.emitted('row-selected')[1][0].length).toBe(2) + expect(wrapper.emitted('row-selected')[1][0]).toEqual([testItems[1], testItems[3]]) + $rows = wrapper.findAll('tbody > tr') + expect($rows.is('[tabindex="0"]')).toBe(true) + expect($rows.at(0).is('[aria-selected="false"]')).toBe(true) + 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) + + // Execute unselectRow() method on selected row + wrapper.vm.unselectRow(3) + await waitNT(wrapper.vm) + + expect(wrapper.emitted('row-selected')).toBeDefined() + expect(wrapper.emitted('row-selected').length).toBe(3) + expect(wrapper.emitted('row-selected')[2][0].length).toBe(1) + expect(wrapper.emitted('row-selected')[2][0]).toEqual([testItems[1]]) + $rows = wrapper.findAll('tbody > tr') + expect($rows.is('[tabindex="0"]')).toBe(true) + expect($rows.at(0).is('[aria-selected="false"]')).toBe(true) + 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="false"]')).toBe(true) + + wrapper.destroy() + }) + + it('method `selectRow()` and `unselectRow()` in range mode works', async () => { + const wrapper = mount(BTable, { + propsData: { + fields: testFields, + items: testItems, + selectable: true, + selectMode: 'range' + } + }) + + let $rows + expect(wrapper).toBeDefined() + await waitNT(wrapper.vm) + expect(wrapper.emitted('row-selected')).not.toBeDefined() + + // Execute selectRow() method (second row) + wrapper.vm.selectRow(1) + await waitNT(wrapper.vm) + + expect(wrapper.emitted('row-selected')).toBeDefined() + expect(wrapper.emitted('row-selected').length).toBe(1) + expect(wrapper.emitted('row-selected')[0][0].length).toBe(1) + expect(wrapper.emitted('row-selected')[0][0]).toEqual([testItems[1]]) + $rows = wrapper.findAll('tbody > tr') + expect($rows.is('[tabindex="0"]')).toBe(true) + expect($rows.at(0).is('[aria-selected="false"]')).toBe(true) + 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="false"]')).toBe(true) + + // Execute selectRow() method (fourth row) + wrapper.vm.selectRow(3) + await waitNT(wrapper.vm) + + expect(wrapper.emitted('row-selected')).toBeDefined() + expect(wrapper.emitted('row-selected').length).toBe(2) + expect(wrapper.emitted('row-selected')[1][0].length).toBe(2) + expect(wrapper.emitted('row-selected')[1][0]).toEqual([testItems[1], testItems[3]]) + $rows = wrapper.findAll('tbody > tr') + expect($rows.is('[tabindex="0"]')).toBe(true) + expect($rows.at(0).is('[aria-selected="false"]')).toBe(true) + 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) + + // Execute unselectRow() method on non-selected row (should not change anything) + wrapper.vm.unselectRow(0) + await waitNT(wrapper.vm) + + expect(wrapper.emitted('row-selected')).toBeDefined() + expect(wrapper.emitted('row-selected').length).toBe(2) + expect(wrapper.emitted('row-selected')[1][0].length).toBe(2) + expect(wrapper.emitted('row-selected')[1][0]).toEqual([testItems[1], testItems[3]]) + $rows = wrapper.findAll('tbody > tr') + expect($rows.is('[tabindex="0"]')).toBe(true) + expect($rows.at(0).is('[aria-selected="false"]')).toBe(true) + 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) + + // Execute unselectRow() method on selected row + wrapper.vm.unselectRow(3) + await waitNT(wrapper.vm) + + expect(wrapper.emitted('row-selected')).toBeDefined() + expect(wrapper.emitted('row-selected').length).toBe(3) + expect(wrapper.emitted('row-selected')[2][0].length).toBe(1) + expect(wrapper.emitted('row-selected')[2][0]).toEqual([testItems[1]]) + $rows = wrapper.findAll('tbody > tr') + expect($rows.is('[tabindex="0"]')).toBe(true) + expect($rows.at(0).is('[aria-selected="false"]')).toBe(true) + 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="false"]')).toBe(true) + + wrapper.destroy() + }) })