Skip to content

Commit 981114b

Browse files
authored
feat(table): create table child element helper components, plus new sort-null-last and table-variant props. (bootstrap-vue#3808)
1 parent 195554f commit 981114b

32 files changed

+1183
-473
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<br>
77

88
<p align="center">
9-
BootstrapVue, with over 40 available plugins and more than 75 custom components, provides
9+
BootstrapVue, with over 40 available plugins and more than 80 custom components, provides
1010
one of the most comprehensive implementations of the Bootstrap v4.3 component and grid
1111
system for Vue.js, complete with extensive and automated WAI-ARIA accessibility markup.
1212
</p>

docs/pages/index.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@
130130
<b-row class="mb-4">
131131
<b-col lg="10" offset-lg="1">
132132
<b-card-text class="text-lg-center">
133-
With over 40 available plugins and more than 75 custom UI components,
133+
With over 40 available plugins and more than 80 custom UI components,
134134
<span class="bd-text-purple-bright">BootstrapVue</span> provides one of the most
135135
comprehensive implementations of the
136136
<span class="bd-text-purple-bright">Bootstrap v{{ bootstrapVersionMinor }}</span> component and grid system

src/components/index.esm.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,9 +284,17 @@ export { BSpinner } from './spinner/spinner'
284284

285285
// export * from './table'
286286
export { TablePlugin } from './table'
287+
export { TableLitePlugin } from './table'
288+
export { TableSimplePlugin } from './table'
287289
export { BTable } from './table/table'
288290
export { BTableLite } from './table/table-lite'
289291
export { BTableSimple } from './table/table-simple'
292+
export { BTbody } from './table/tbody'
293+
export { BThead } from './table/thead'
294+
export { BTfoot } from './table/tfoot'
295+
export { BTr } from './table/tr'
296+
export { BTh } from './table/th'
297+
export { BTd } from './table/td'
290298

291299
// export * from './tabs'
292300
export { TabsPlugin } from './tabs'

src/components/input-group/input-group.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export const props = {
3535
export const BInputGroup = /*#__PURE__*/ Vue.extend({
3636
name: 'BInputGroup',
3737
functional: true,
38-
props: props,
38+
props,
3939
render(h, { props, data, slots, scopedSlots }) {
4040
const $slots = slots()
4141
const $scopedSlots = scopedSlots || {}

src/components/table/README.md

Lines changed: 417 additions & 176 deletions
Large diffs are not rendered by default.

src/components/table/_table.scss

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@
2020
// Caption positioning
2121
> caption {
2222
caption-side: bottom;
23+
}
2324

24-
&.b-table-caption-top {
25+
&.b-table-caption-top {
26+
> caption {
2527
caption-side: top !important;
2628
}
2729
}
@@ -104,7 +106,7 @@
104106
> tbody,
105107
> tbody > tr,
106108
> tbody > tr > td,
107-
> tbody > tr > td {
109+
> tbody > tr > th {
108110
display: block;
109111
}
110112

@@ -138,7 +140,7 @@
138140
overflow-wrap: break-word;
139141
font-weight: bold;
140142
font-style: normal;
141-
padding: 0;
143+
padding: 0 calc(#{$b-table-stacked-gap} / 2) 0 0;
142144
margin: 0;
143145
}
144146

@@ -154,7 +156,7 @@
154156
display: inline-block;
155157
width: calc(100% - #{$b-table-stacked-heading-width});
156158
// Add "gap" between "cells"
157-
padding: 0 0 0 $b-table-stacked-gap;
159+
padding: 0 0 0 calc(#{$b-table-stacked-gap} / 2);
158160
margin: 0;
159161
}
160162
}
@@ -169,6 +171,12 @@
169171
> :first-child {
170172
border-top-width: (3 * $table-border-width);
171173
}
174+
175+
// Give any cell after a rowspan'ed cell a heavy top border
176+
> [rowspan] + td,
177+
> [rowspan] + th {
178+
border-top-width: (3 * $table-border-width);
179+
}
172180
}
173181
}
174182
}
Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import get from '../../../utils/get'
2-
import { isDate, isUndefined, isFunction, isNull, isNumber } from '../../../utils/inspect'
2+
import { isDate, isFunction, isNumber, isUndefinedOrNull } from '../../../utils/inspect'
33
import stringifyObjectValues from './stringify-object-values'
44

55
// Default sort compare routine
@@ -10,23 +10,28 @@ import stringifyObjectValues from './stringify-object-values'
1010
// or an array of arrays `[ ['foo','asc'], ['bar','desc'] ]`
1111
// Multisort will most likely be handled in mixin-sort.js by
1212
// calling this method for each sortBy
13-
const defaultSortCompare = (a, b, sortBy, formatter, localeOpts, locale) => {
14-
let aa = get(a, sortBy, '')
15-
let bb = get(b, sortBy, '')
13+
const defaultSortCompare = (a, b, sortBy, sortDesc, formatter, localeOpts, locale, nullLast) => {
14+
let aa = get(a, sortBy, null)
15+
let bb = get(b, sortBy, null)
1616
if (isFunction(formatter)) {
1717
aa = formatter(aa, sortBy, a)
1818
bb = formatter(bb, sortBy, b)
1919
}
20-
aa = isUndefined(aa) || isNull(aa) ? '' : aa
21-
bb = isUndefined(bb) || isNull(bb) ? '' : bb
20+
aa = isUndefinedOrNull(aa) ? '' : aa
21+
bb = isUndefinedOrNull(bb) ? '' : bb
2222
if ((isDate(aa) && isDate(bb)) || (isNumber(aa) && isNumber(bb))) {
2323
// Special case for comparing dates and numbers
2424
// Internally dates are compared via their epoch number values
2525
return aa < bb ? -1 : aa > bb ? 1 : 0
26-
} else {
27-
// Do localized string comparison
28-
return stringifyObjectValues(aa).localeCompare(stringifyObjectValues(bb), locale, localeOpts)
26+
} else if (nullLast && aa === '' && bb !== '') {
27+
// Special case when sorting null/undefined/empty string last
28+
return 1
29+
} else if (nullLast && aa !== '' && bb === '') {
30+
// Special case when sorting null/undefined/empty string last
31+
return -1
2932
}
33+
// Do localized string comparison
34+
return stringifyObjectValues(aa).localeCompare(stringifyObjectValues(bb), locale, localeOpts)
3035
}
3136

3237
export default defaultSortCompare

src/components/table/helpers/default-sort-compare.spec.js

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,33 @@ describe('table/helpers/default-sort-compare', () => {
5454
.join('')
5555
}
5656
expect(defaultSortCompare({ a: 'ab' }, { a: 'b' }, 'a')).toBe(-1)
57-
expect(defaultSortCompare({ a: 'ab' }, { a: 'b' }, 'a', formatter)).toBe(1)
57+
expect(defaultSortCompare({ a: 'ab' }, { a: 'b' }, 'a', false, formatter)).toBe(1)
58+
})
59+
60+
it('sorts nulls always last when sor-null-lasst is set', async () => {
61+
const x = { a: 'ab' }
62+
const y = { a: null }
63+
const z = {}
64+
const w = { a: '' }
65+
const u = undefined
66+
67+
// Without nullLast set (false)
68+
expect(defaultSortCompare(x, y, 'a', u, u, { numeric: true }, u, false)).toBe(1)
69+
expect(defaultSortCompare(y, x, 'a', u, u, { numeric: true }, u, false)).toBe(-1)
70+
expect(defaultSortCompare(x, z, 'a', u, u, { numeric: true }, u, false)).toBe(1)
71+
expect(defaultSortCompare(z, x, 'a', u, u, { numeric: true }, u, false)).toBe(-1)
72+
expect(defaultSortCompare(y, z, 'a', u, u, { numeric: true }, u, false)).toBe(0)
73+
expect(defaultSortCompare(z, y, 'a', u, u, { numeric: true }, u, false)).toBe(0)
74+
expect(defaultSortCompare(x, w, 'a', u, u, { numeric: true }, u, false)).toBe(1)
75+
expect(defaultSortCompare(w, x, 'a', u, u, { numeric: true }, u, false)).toBe(-1)
76+
// With nullLast set
77+
expect(defaultSortCompare(x, y, 'a', u, u, { numeric: true }, u, true)).toBe(-1)
78+
expect(defaultSortCompare(y, x, 'a', u, u, { numeric: true }, u, true)).toBe(1)
79+
expect(defaultSortCompare(x, z, 'a', u, u, { numeric: true }, u, true)).toBe(-1)
80+
expect(defaultSortCompare(z, x, 'a', u, u, { numeric: true }, u, true)).toBe(1)
81+
expect(defaultSortCompare(y, z, 'a', u, u, { numeric: true }, u, true)).toBe(0)
82+
expect(defaultSortCompare(z, y, 'a', u, u, { numeric: true }, u, true)).toBe(0)
83+
expect(defaultSortCompare(x, w, 'a', u, u, { numeric: true }, u, true)).toBe(-1)
84+
expect(defaultSortCompare(w, x, 'a', u, u, { numeric: true }, u, true)).toBe(1)
5885
})
5986
})

src/components/table/helpers/mixin-bottom-row.js

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,33 @@
11
import { isFunction } from '../../../utils/inspect'
2+
import { BTr } from '../tr'
3+
4+
const slotName = 'bottom-row'
25

36
export default {
47
methods: {
58
renderBottomRow() {
69
const h = this.$createElement
710

811
// Static bottom row slot (hidden in visibly stacked mode as we can't control the data-label)
9-
// If in always stacked mode, we don't bother rendering the row
10-
if (!this.hasNormalizedSlot('bottom-row') || this.isStacked === true) {
12+
// If in *always* stacked mode, we don't bother rendering the row
13+
if (!this.hasNormalizedSlot(slotName) || this.stacked === true || this.stacked === '') {
1114
return h()
1215
}
1316

1417
const fields = this.computedFields
1518

1619
return h(
17-
'tr',
20+
BTr,
1821
{
19-
key: '__b-table-bottom-row__',
22+
key: 'b-bottom-row',
2023
staticClass: 'b-table-bottom-row',
2124
class: [
2225
isFunction(this.tbodyTrClass)
2326
? this.tbodyTrClass(null, 'row-bottom')
2427
: this.tbodyTrClass
25-
],
26-
attrs: { role: 'row' }
28+
]
2729
},
28-
this.normalizeSlot('bottom-row', { columns: fields.length, fields: fields })
30+
this.normalizeSlot(slotName, { columns: fields.length, fields: fields })
2931
)
3032
}
3133
}

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

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
import { isFunction } from '../../../utils/inspect'
2+
import { BTr } from '../tr'
3+
import { BTd } from '../td'
4+
5+
const busySlotName = 'table-busy'
26

37
export default {
48
props: {
@@ -40,28 +44,24 @@ export default {
4044
const h = this.$createElement
4145

4246
// Return a busy indicator row, or `null` if not busy
43-
if (this.computedBusy && this.hasNormalizedSlot('table-busy')) {
47+
if (this.computedBusy && this.hasNormalizedSlot(busySlotName)) {
4448
// Show the busy slot
45-
const trAttrs = {
46-
role: this.isStacked ? 'row' : null
47-
}
48-
const tdAttrs = {
49-
colspan: String(this.computedFields.length),
50-
role: this.isStacked ? 'cell' : null
51-
}
5249
return h(
53-
'tr',
50+
BTr,
5451
{
5552
key: 'table-busy-slot',
5653
staticClass: 'b-table-busy-slot',
5754
class: [
5855
isFunction(this.tbodyTrClass)
59-
? this.tbodyTrClass(null, 'table-busy')
56+
? this.tbodyTrClass(null, busySlotName)
6057
: this.tbodyTrClass
61-
],
62-
attrs: trAttrs
58+
]
6359
},
64-
[h('td', { attrs: tdAttrs }, [this.normalizeSlot('table-busy')])]
60+
[
61+
h(BTd, { props: { colspan: this.computedFields.length || null } }, [
62+
this.normalizeSlot(busySlotName)
63+
])
64+
]
6565
)
6666
} else {
6767
// We return `null` here so that we can determine if we need to

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

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,20 @@ import { htmlOrText } from '../../../utils/html'
22

33
export default {
44
props: {
5+
// `caption-top` is part of table-redere mixin (styling)
6+
// captionTop: {
7+
// type: Boolean,
8+
// default: false
9+
// },
510
caption: {
611
type: String,
712
default: null
813
},
914
captionHtml: {
1015
type: String
11-
},
12-
captionTop: {
13-
type: Boolean,
14-
default: false
1516
}
1617
},
1718
computed: {
18-
captionClasses() {
19-
return {
20-
'b-table-caption-top': this.captionTop
21-
}
22-
},
2319
captionId() {
2420
// Even though `this.safeId` looks like a method, it is a computed prop
2521
// that returns a new function if the underlying ID changes
@@ -37,7 +33,6 @@ export default {
3733
if ($captionSlot || this.caption || this.captionHtml) {
3834
const data = {
3935
key: 'caption',
40-
class: this.captionClasses,
4136
attrs: { id: this.captionId }
4237
}
4338
if (!$captionSlot) {

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

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { htmlOrText } from '../../../utils/html'
22
import { isFunction } from '../../../utils/inspect'
3+
import { BTr } from '../tr'
4+
import { BTd } from '../td'
35

46
export default {
57
props: {
@@ -50,27 +52,19 @@ export default {
5052
: htmlOrText(this.emptyHtml, this.emptyText)
5153
})
5254
}
55+
$empty = h(BTd, { props: { colspan: this.computedFields.length || null } }, [
56+
h('div', { attrs: { role: 'alert', 'aria-live': 'polite' } }, [$empty])
57+
])
5358
$empty = h(
54-
'td',
59+
BTr,
5560
{
56-
attrs: {
57-
colspan: String(this.computedFields.length),
58-
role: 'cell'
59-
}
60-
},
61-
[h('div', { attrs: { role: 'alert', 'aria-live': 'polite' } }, [$empty])]
62-
)
63-
$empty = h(
64-
'tr',
65-
{
66-
key: this.isFiltered ? '_b-table-empty-filtered-row_' : '_b-table-empty-row_',
61+
key: this.isFiltered ? 'b-empty-filtered-row' : 'b-empty-row',
6762
staticClass: 'b-table-empty-row',
6863
class: [
6964
isFunction(this.tbodyTrClass)
7065
? this.tbodyTrClass(null, 'row-empty')
7166
: this.tbodyTrClass
72-
],
73-
attrs: { role: 'row' }
67+
]
7468
},
7569
[$empty]
7670
)

0 commit comments

Comments
 (0)