From 3ec8ecaf590ac5efa215f7e76529282d85e6a79d Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Fri, 9 Aug 2019 13:04:42 -0300 Subject: [PATCH 01/15] feat(b-table): programmatic row selection --- .../table/helpers/mixin-selectable.js | 120 ++++++++++++------ 1 file changed, 78 insertions(+), 42 deletions(-) diff --git a/src/components/table/helpers/mixin-selectable.js b/src/components/table/helpers/mixin-selectable.js index 355245ce577..3de3debd592 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,34 +28,35 @@ 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) { // Quick check against array length @@ -74,7 +78,7 @@ 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) selectedRows.forEach((v, idx) => { @@ -88,35 +92,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 +165,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 ( From aa60a5a26a579ec237d993ef963802d70f569396 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Fri, 9 Aug 2019 13:15:53 -0300 Subject: [PATCH 02/15] Update mixin-selectable.js --- src/components/table/helpers/mixin-selectable.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/components/table/helpers/mixin-selectable.js b/src/components/table/helpers/mixin-selectable.js index 3de3debd592..717db599be7 100644 --- a/src/components/table/helpers/mixin-selectable.js +++ b/src/components/table/helpers/mixin-selectable.js @@ -32,8 +32,7 @@ export default { return this.selectable && this.selectMode }, selectableHasSelection() { - return - this.isSelectable && + return this.isSelectable && this.selectedRows && this.selectedRows.length > 0 && this.selectedRows.some(Boolean) @@ -50,7 +49,11 @@ export default { }, selectableTableAttrs() { return { - 'aria-multiselectable': !this.isSelectable ? null : this.selectableIsMultiSelect ? 'true' : 'false' + 'aria-multiselectable': !this.isSelectable + ? null + : this.selectableIsMultiSelect + ? 'true' + : 'false' } } }, @@ -58,7 +61,7 @@ export default { computedItems(newVal, oldVal) { // Reset for selectable 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++) { From 403c9d4119ea9b7cf9e4f1d3931aff371aacdf1d Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Fri, 9 Aug 2019 13:23:07 -0300 Subject: [PATCH 03/15] Update mixin-selectable.js --- src/components/table/helpers/mixin-selectable.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/table/helpers/mixin-selectable.js b/src/components/table/helpers/mixin-selectable.js index 717db599be7..66b641725f9 100644 --- a/src/components/table/helpers/mixin-selectable.js +++ b/src/components/table/helpers/mixin-selectable.js @@ -32,10 +32,12 @@ export default { return this.selectable && this.selectMode }, selectableHasSelection() { - return this.isSelectable && + return ( + this.isSelectable && this.selectedRows && this.selectedRows.length > 0 && this.selectedRows.some(Boolean) + ) }, selectableIsMultiSelect() { return this.isSelectable && arrayIncludes(['range', 'multi'], this.selectMode) From 84a5c3a66e3c84db8c9bfb4803e9fb0ca52b4023 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Fri, 9 Aug 2019 13:23:47 -0300 Subject: [PATCH 04/15] Update index.d.ts --- src/components/table/index.d.ts | 4 ++++ 1 file changed, 4 insertions(+) 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 From 452bb7d6c3c4c8ba8e0a7169b033110d8f852b5d Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Fri, 9 Aug 2019 13:27:29 -0300 Subject: [PATCH 05/15] Update mixin-tbody-row.js --- src/components/table/helpers/mixin-tbody-row.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 = [] From 25df6e98161e1373cd5b0884fdca3b2b6612c6a7 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Fri, 9 Aug 2019 13:40:57 -0300 Subject: [PATCH 06/15] Update table-selectable.spec.js --- src/components/table/table-selectable.spec.js | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/src/components/table/table-selectable.spec.js b/src/components/table/table-selectable.spec.js index 4b9769b517b..22b03798a34 100644 --- a/src/components/table/table-selectable.spec.js +++ b/src/components/table/table-selectable.spec.js @@ -697,4 +697,98 @@ 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() + }) }) From 01e61531fd196e2415b38e51205534e623c0b2fc Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Fri, 9 Aug 2019 13:45:38 -0300 Subject: [PATCH 07/15] Update table-selectable.spec.js --- src/components/table/table-selectable.spec.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/table/table-selectable.spec.js b/src/components/table/table-selectable.spec.js index 22b03798a34..760d8336a9d 100644 --- a/src/components/table/table-selectable.spec.js +++ b/src/components/table/table-selectable.spec.js @@ -715,7 +715,7 @@ describe('table > row select', () => { // 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) @@ -739,6 +739,7 @@ describe('table > row select', () => { selectMode: 'multi' } }) + expect(wrapper).toBeDefined() await waitNT(wrapper.vm) expect(wrapper.emitted('row-selected')).not.toBeDefined() @@ -746,7 +747,7 @@ describe('table > row select', () => { // 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) @@ -770,6 +771,7 @@ describe('table > row select', () => { selectMode: 'range' } }) + expect(wrapper).toBeDefined() await waitNT(wrapper.vm) expect(wrapper.emitted('row-selected')).not.toBeDefined() @@ -777,7 +779,7 @@ describe('table > row select', () => { // 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) From f77f11f6e7acb1a39e354c1f4c43b4c9ccd6ed08 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Fri, 9 Aug 2019 13:58:43 -0300 Subject: [PATCH 08/15] Update table-selectable.spec.js --- src/components/table/table-selectable.spec.js | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/src/components/table/table-selectable.spec.js b/src/components/table/table-selectable.spec.js index 760d8336a9d..81b80a00788 100644 --- a/src/components/table/table-selectable.spec.js +++ b/src/components/table/table-selectable.spec.js @@ -793,4 +793,81 @@ describe('table > row select', () => { 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' + } + }) + + 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]]) + const $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]]) + const $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]]) + const $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([]) + const $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() + }) }) From fabbfe6749a766679ca583a18212069206ebb91a Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Fri, 9 Aug 2019 14:01:47 -0300 Subject: [PATCH 09/15] Update table-selectable.spec.js --- src/components/table/table-selectable.spec.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/components/table/table-selectable.spec.js b/src/components/table/table-selectable.spec.js index 81b80a00788..21227e31689 100644 --- a/src/components/table/table-selectable.spec.js +++ b/src/components/table/table-selectable.spec.js @@ -804,6 +804,7 @@ describe('table > row select', () => { } }) + let $rows expect(wrapper).toBeDefined() await waitNT(wrapper.vm) expect(wrapper.emitted('row-selected')).not.toBeDefined() @@ -816,7 +817,7 @@ describe('table > row select', () => { 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]]) - const $rows = wrapper.findAll('tbody > tr') + $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) @@ -831,7 +832,7 @@ describe('table > row select', () => { 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]]) - const $rows = wrapper.findAll('tbody > tr') + $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) @@ -846,7 +847,7 @@ describe('table > row select', () => { 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]]) - const $rows = wrapper.findAll('tbody > tr') + $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) @@ -861,7 +862,7 @@ describe('table > row select', () => { 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([]) - const $rows = wrapper.findAll('tbody > tr') + $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) From d8b3f802981bec4eef7a3a02ac0fc57db074ff54 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Fri, 9 Aug 2019 14:08:06 -0300 Subject: [PATCH 10/15] Update table-selectable.spec.js --- src/components/table/table-selectable.spec.js | 156 ++++++++++++++++++ 1 file changed, 156 insertions(+) diff --git a/src/components/table/table-selectable.spec.js b/src/components/table/table-selectable.spec.js index 21227e31689..4fc3f708d90 100644 --- a/src/components/table/table-selectable.spec.js +++ b/src/components/table/table-selectable.spec.js @@ -871,4 +871,160 @@ describe('table > row select', () => { 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() + }) }) From 31506ebd6c12c0e9d82747ca4e72714f918ad22f Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Fri, 9 Aug 2019 14:46:27 -0300 Subject: [PATCH 11/15] Update README.md --- src/components/table/README.md | 52 ++++++++++++++++++++++++++++++---- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/src/components/table/README.md b/src/components/table/README.md index 389223eef19..eaa511c5613 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 `unseletRow(index)` on a non-existant row will be ignored. +- The table must be `selectable` for any of these methods to have effect. + ```html - - {{ selected }} +

+ Select all + Clear selected + Select 3rd row + Unselect 3rd row +

+

+ Selected Rows:
+ {{ selected }} +

@@ -1455,8 +1483,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) } } } @@ -1481,7 +1523,7 @@ element. 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. ### Table body transition support From 8f5a4a77b895b499e4950606ede742005aecf7cf Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Fri, 9 Aug 2019 15:13:18 -0300 Subject: [PATCH 12/15] Update README.md --- src/components/table/README.md | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/components/table/README.md b/src/components/table/README.md index eaa511c5613..97caec42d52 100644 --- a/src/components/table/README.md +++ b/src/components/table/README.md @@ -1433,7 +1433,7 @@ Programmatic selection notes: ref="selectableTable" selectable :select-mode="selectMode" - selectedVariant="success" + selected-variant="success" :items="items" :fields="fields" @row-selected="onRowSelected" @@ -1507,7 +1507,7 @@ Programmatic selection notes: ``` -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` @@ -1517,13 +1517,23 @@ 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](#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 From b741942c5ae90c0532cf853c1be10e6c15eda044 Mon Sep 17 00:00:00 2001 From: Troy Morehouse Date: Sun, 11 Aug 2019 21:48:48 -0300 Subject: [PATCH 13/15] Update README.md --- src/components/table/README.md | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/components/table/README.md b/src/components/table/README.md index 0b422819a57..6429dd87985 100644 --- a/src/components/table/README.md +++ b/src/components/table/README.md @@ -1439,18 +1439,16 @@ Programmatic selection notes: @row-selected="onRowSelected" responsive="sm" > - -

From c46a217d5940aa414c9f84a3056c8e5d57ef2de2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacob=20M=C3=BCller?= Date: Mon, 12 Aug 2019 08:58:32 +0200 Subject: [PATCH 14/15] Update README.md --- src/components/table/README.md | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/components/table/README.md b/src/components/table/README.md index 6429dd87985..da42b05158b 100644 --- a/src/components/table/README.md +++ b/src/components/table/README.md @@ -1419,7 +1419,7 @@ 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 `unseletRow(index)` on a non-existant row will be ignored. +- 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 @@ -1491,11 +1491,11 @@ Programmatic selection notes: this.$refs.selectableTable.clearSelected() }, selectThirdRow() { - // rows are indexed from 0, so the third row is index 2 + // 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 + // Rows are indexed from 0, so the third row is index 2 this.$refs.selectableTable.unselectRow(2) } } @@ -1518,8 +1518,8 @@ 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). +will be using other means to convey that a row is selected (such as a scoped field slot in the above +example). **Notes:** @@ -1527,9 +1527,8 @@ above example). 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](#accessibility) reasons, and will have the - attribute `aria-selected` set to either `'true'` or `'false'` depending on the selected state of - the row. + 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. From 42d3fbb849fcb950e36bc07e5438b4510bd25815 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacob=20M=C3=BCller?= Date: Mon, 12 Aug 2019 08:58:36 +0200 Subject: [PATCH 15/15] Update mixin-selectable.js --- src/components/table/helpers/mixin-selectable.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/table/helpers/mixin-selectable.js b/src/components/table/helpers/mixin-selectable.js index 66b641725f9..65fa7ccfe85 100644 --- a/src/components/table/helpers/mixin-selectable.js +++ b/src/components/table/helpers/mixin-selectable.js @@ -85,7 +85,7 @@ export default { selectedRows(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]) @@ -119,7 +119,7 @@ export default { } }, unselectRow(index) { - // Un-select a particular row (indexed based on computedItems) + // 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 @@ -135,7 +135,7 @@ export default { } }, isRowSelected(index) { - // Determine if a row is selected (indexed based on computedItems) + // Determine if a row is selected (indexed based on `computedItems`) return Boolean(isNumber(index) && this.selectedRows[index]) }, clearSelected() { @@ -196,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 }