diff --git a/src/components/table/README.md b/src/components/table/README.md index 6cf0b986c8d..17fda1cbeae 100644 --- a/src/components/table/README.md +++ b/src/components/table/README.md @@ -894,6 +894,8 @@ The slot's scope variable (`data` in the above sample) will have the following p | `detailsShowing` | Boolean | Will be `true` if the row's `row-details` scoped slot is visible. See section [Row details support](#row-details-support) below for additional information | | `toggleDetails` | Function | Can be called to toggle the visibility of the rows `row-details` scoped slot. See section [Row details support](#row-details-support) below for additional information | | `rowSelected` | Boolean | Will be `true` if the row has been selected. See section [Row select support](#row-select-support) for additional information | +| `selectRow` | Function | When called, selects the current row. See section [Row select support](#row-select-support) for additional information | +| `unselectRow` | Function | When called, unselects the current row. See section [Row select support](#row-select-support) for additional information | **Notes:** @@ -1407,12 +1409,17 @@ for proper reactive detection of changes to it's value. Read more about **Available `row-details` scoped variable properties:** -| Property | Type | Description | -| --------------- | -------- | ------------------------------------------------------------------------- | -| `item` | Object | The entire row record data object | -| `index` | Number | The current visible row number | -| `fields` | Array | The normalized fields definition array (in the _array of objects_ format) | -| `toggleDetails` | Function | Function to toggle visibility of the row's details slot | +| Property | Type | Description | +| --------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------- | +| `item` | Object | The entire row record data object | +| `index` | Number | The current visible row number | +| `fields` | Array | The normalized fields definition array (in the _array of objects_ format) | +| `toggleDetails` | Function | Function to toggle visibility of the row's details slot | +| `rowSelected` | Boolean | Will be `true` if the row has been selected. See section [Row select support](#row-select-support) for additional information | +| `selectRow` | Function | When called, selects the current row. See section [Row select support](#row-select-support) for additional information | +| `unselectRow` | Function | When called, unselects the current row. See section [Row select support](#row-select-support) for additional information | + +Note: the row select related scope properties are only available in ``. In the following example, we show two methods of toggling the visibility of the details: one via a button, and one via a checkbox. We also have the third row details defaulting to have details @@ -1510,6 +1517,8 @@ Rows can also be programmatically selected and unselected via the following expo - 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. +- You can disable selection of rows via click events by setting the `no-select-on-click` prop. Rows + will then only be selectable programmatically. **Row select notes:** @@ -2757,10 +2766,11 @@ cells. ### Data row accessibility -When the table is in `selectable` mode (`` only), or if there is a `row-clicked` event -listener registered (`` and ``), all data item rows (`` elements) will be -placed into the document tab sequence (via `tabindex="0"`) to allow keyboard-only and screen reader -users the ability to click the rows by pressing ENTER. +When the table is in `selectable` mode (`` only, and prop `no-select-on-click` is not set), +or if there is a `row-clicked` event listener registered (`` and ``), all +data item rows (`` elements) will be placed into the document tab sequence (via `tabindex="0"`) +to allow keyboard-only and screen reader users the ability to click the rows by pressing +ENTER or SPACE. When the table items rows are placed in the document tab sequence (`` and ``), they will also support basic keyboard navigation when focused: @@ -2770,8 +2780,6 @@ When the table items rows are placed in the document tab sequence (`` a - END or DOWN+SHIFT will move to the last row - HOME or UP+SHIFT will move to the first row - ENTER or SPACE to click the row. -- SHIFT and CTRL modifiers will also work (depending on the table selectable - mode, for `` only). ### Row event accessibility diff --git a/src/components/table/_table.scss b/src/components/table/_table.scss index 34c905cbe25..a5a8ac33448 100644 --- a/src/components/table/_table.scss +++ b/src/components/table/_table.scss @@ -361,7 +361,7 @@ $bv-escaped-characters: (("<", "%3c"), (">", "%3e"), ("#", "%23")); // --- Selectable rows --- .table.b-table { - &.b-table-selectable { + &.b-table-selectable:not(.b-table-selectable-no-click) { & > tbody > tr { cursor: pointer; } diff --git a/src/components/table/helpers/mixin-selectable.js b/src/components/table/helpers/mixin-selectable.js index 65fa7ccfe85..e31bec32127 100644 --- a/src/components/table/helpers/mixin-selectable.js +++ b/src/components/table/helpers/mixin-selectable.js @@ -19,6 +19,11 @@ export default { selectedVariant: { type: String, default: () => getComponentConfig('BTable', 'selectedVariant') + }, + noSelectOnClick: { + // Disable use of click handlers for row selection + type: Boolean, + default: false } }, data() { @@ -31,6 +36,12 @@ export default { isSelectable() { return this.selectable && this.selectMode }, + hasSelectableRowClick() { + return this.isSelectable && !this.noSelectOnClick + }, + supportsSelectableRows() { + return true + }, selectableHasSelection() { return ( this.isSelectable && @@ -46,11 +57,15 @@ export default { return { 'b-table-selectable': this.isSelectable, [`b-table-select-${this.selectMode}`]: this.isSelectable, - 'b-table-selecting': this.selectableHasSelection + 'b-table-selecting': this.selectableHasSelection, + 'b-table-selectable-no-click': this.isSelectable && !this.hasSelectableRowClick } }, selectableTableAttrs() { return { + // TODO: + // Should this attribute not be included when no-select-on-click is set + // since this attribute implies keyboard navigation? 'aria-multiselectable': !this.isSelectable ? null : this.selectableIsMultiSelect @@ -82,6 +97,10 @@ export default { selectMode(newVal, oldVal) { this.clearSelected() }, + hasSelectableRowClick(newVal, oldVal) { + this.clearSelected() + this.setSelectionHandlers(!newVal) + }, selectedRows(selectedRows, oldVal) { if (this.isSelectable && !looseEqual(selectedRows, oldVal)) { const items = [] @@ -96,7 +115,7 @@ export default { } }, beforeMount() { - // Set up handlers + // Set up handlers if needed if (this.isSelectable) { this.setSelectionHandlers(true) } @@ -161,7 +180,7 @@ export default { } }, setSelectionHandlers(on) { - const method = on ? '$on' : '$off' + const method = on && !this.noSelectOnClick ? '$on' : '$off' // Handle row-clicked event this[method]('row-clicked', this.selectionHandler) // Clear selection on filter, pagination, and sort changes @@ -170,11 +189,9 @@ export default { }, selectionHandler(item, index, evt) { /* istanbul ignore if: should never happen */ - if (!this.isSelectable) { + if (!this.isSelectable || this.noSelectOnClick) { // 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 diff --git a/src/components/table/helpers/mixin-tbody-row.js b/src/components/table/helpers/mixin-tbody-row.js index a79b9b3c631..a955da76470 100644 --- a/src/components/table/helpers/mixin-tbody-row.js +++ b/src/components/table/helpers/mixin-tbody-row.js @@ -146,9 +146,12 @@ export default { toggleDetails: this.toggleDetailsFactory(hasDetailsSlot, item), detailsShowing: Boolean(item._showDetails) } - if (this.selectedRows) { - // Add in rowSelected scope property if selectable rows supported + // If table supports selectable mode, then add in the following scope + // this.supportsSelectableRows will be undefined if mixin isn't loaded + if (this.supportsSelectableRows) { slotScope.rowSelected = this.isRowSelected(rowIndex) + slotScope.selectRow = () => this.selectRow(rowIndex) + slotScope.unselectRow = () => this.unselectRow(rowIndex) } // The new `v-slot` syntax doesn't like a slot name starting with // a square bracket and if using in-document HTML templates, the @@ -174,7 +177,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.isSelectable + const hasRowClickHandler = this.$listeners['row-clicked'] || this.hasSelectableRowClick // We can return more than one TR if rowDetails enabled const $rows = [] @@ -253,6 +256,13 @@ export default { fields: fields, toggleDetails: this.toggleDetailsFactory(hasDetailsSlot, item) } + // If table supports selectable mode, then add in the following scope + // this.supportsSelectableRows will be undefined if mixin isn't loaded + if (this.supportsSelectableRows) { + detailsScope.rowSelected = this.isRowSelected(rowIndex) + detailsScope.selectRow = () => this.selectRow(rowIndex) + detailsScope.unselectRow = () => this.unselectRow(rowIndex) + } // Render the details slot in a TD const $details = h(BTd, { props: { colspan: fields.length }, class: this.detailsTdClass }, [ diff --git a/src/components/table/helpers/mixin-tbody.js b/src/components/table/helpers/mixin-tbody.js index 56b308ed084..322ada1b189 100644 --- a/src/components/table/helpers/mixin-tbody.js +++ b/src/components/table/helpers/mixin-tbody.js @@ -133,7 +133,7 @@ export default { const items = this.computedItems // Shortcut to `createElement` (could use `this._c()` instead) const h = this.$createElement - const hasRowClickHandler = this.$listeners['row-clicked'] || this.isSelectable + const hasRowClickHandler = this.$listeners['row-clicked'] || this.hasSelectableRowClick // Prepare the tbody rows const $rows = [] diff --git a/src/components/table/package.json b/src/components/table/package.json index b6995a5e20d..74c0af9cdd0 100644 --- a/src/components/table/package.json +++ b/src/components/table/package.json @@ -252,6 +252,11 @@ "prop": "selectedVariant", "description": "Bootstrap color theme variant to set selected rows to. Use any of the standard Bootstrap theme color variants, or the special table row variant 'active' (default). Set to an empty string to not use a variant" }, + { + "prop": "noSelectOnClick", + "version": "2.1.0", + "description": "Disables row selection via click events. Row selection will be only available programmatically" + }, { "prop": "showEmpty", "description": "When enabled, and there are no item records to show, shows a message that there are no rows to show" @@ -551,7 +556,19 @@ { "prop": "rowSelected", "type": "Boolean", - "description": "Will be true if the row has been selected." + "description": "Will be true if the row has been selected. Only applicable when table is in selectable mode" + }, + { + "prop": "selectRow", + "type": "Function", + "version": "2.1.0", + "description": "Can be called to select the current row. Only applicable when table is in selectable mode" + }, + { + "prop": "unselectRow", + "type": "Function", + "version": "2.1.0", + "description": "Can be called to unselect the current row. Only applicable when table is in selectable mode" } ] }, @@ -595,7 +612,19 @@ { "prop": "rowSelected", "type": "Boolean", - "description": "Will be true if the row has been selected." + "description": "Will be true if the row has been selected. Only applicable when table is in selectable mode" + }, + { + "prop": "selectRow", + "type": "Function", + "version": "2.1.0", + "description": "Can be called to select the current row. Only applicable when table is in selectable mode" + }, + { + "prop": "unselectRow", + "type": "Function", + "version": "2.1.0", + "description": "Can be called to unselect the current row. Only applicable when table is in selectable mode" } ] }, @@ -782,6 +811,24 @@ "prop": "toggleDetails", "type": "Function", "description": "Function to toggle visibility of the row's details slot" + }, + { + "prop": "rowSelected", + "type": "Boolean", + "version": "2.1.0", + "description": "Will be true if the row has been selected. Only applicable when table is in selectable mode" + }, + { + "prop": "selectRow", + "type": "Function", + "version": "2.1.0", + "description": "Can be called to select the current row. Only applicable when table is in selectable mode" + }, + { + "prop": "unselectRow", + "type": "Function", + "version": "2.1.0", + "description": "Can be called to unselect the current row. Only applicable when table is in selectable mode" } ] }, diff --git a/src/components/table/table-selectable.spec.js b/src/components/table/table-selectable.spec.js index dfab7823c1c..0f1ce80c44b 100644 --- a/src/components/table/table-selectable.spec.js +++ b/src/components/table/table-selectable.spec.js @@ -31,6 +31,7 @@ describe('table > row select', () => { await waitNT(wrapper.vm) expect(wrapper.attributes('aria-multiselectable')).not.toBeDefined() expect(wrapper.classes()).not.toContain('b-table-selectable') + expect(wrapper.classes()).not.toContain('b-table-selectable-no-click') 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') @@ -59,6 +60,7 @@ describe('table > row select', () => { await waitNT(wrapper.vm) expect(wrapper.attributes('aria-multiselectable')).not.toBeDefined() expect(wrapper.classes()).not.toContain('b-table-selectable') + expect(wrapper.classes()).not.toContain('b-table-selectable-no-click') 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') @@ -73,6 +75,31 @@ describe('table > row select', () => { wrapper.destroy() }) + it('has class b-table-selectable-no-click when prop no-select-on-click set', async () => { + const wrapper = mount(BTable, { + propsData: { + fields: testFields, + items: testItems, + selectable: true, + selectMode: 'single', + noSelectOnClick: true + } + }) + + expect(wrapper).toBeDefined() + await waitNT(wrapper.vm) + 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()).toContain('b-table-selectable-no-click') + 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() + + wrapper.destroy() + }) + it('select mode single works', async () => { const wrapper = mount(BTable, { propsData: { @@ -89,6 +116,7 @@ describe('table > row select', () => { 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-selectable-no-click') 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')