8000 feat(table): add sticky header support (closes #2085) (#3831) · shashankgaurav17/bootstrap-vue@a5f7266 · GitHub
[go: up one dir, main page]

Skip to content

Commit a5f7266

Browse files
authored
feat(table): add sticky header support (closes bootstrap-vue#2085) (bootstrap-vue#3831)
1 parent 06c6119 commit a5f7266

File tree

6 files changed

+201
-19
lines changed

6 files changed

+201
-19
lines changed

src/_variables.scss

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@ $b-table-sort-icon-descending: "\2191" !default; // Up arrow
6969
$b-table-sort-icon-margin-left: 0.5em !default;
7070
$b-table-sort-icon-width: 0.5em !default;
7171

72+
// Default max-height for tables with sticky headers
73+
$b-table-sticky-header-max-height: 300px !default;
74+
7275
// Flag to enable table stacked CSS generation
7376
$bv-enable-table-stacked: true !default;
7477
// Table stacked defaults

src/components/table/README.md

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,7 @@ details.
417417
| `dark` | Boolean | Invert the colors — with light text on dark backgrounds (equivalent to Bootstrap v4 class `.table-dark`) |
418418
| `fixed` | Boolean | Generate a table with equal fixed-width columns (`table-layout: fixed;`) |
419419
| `responsive` | Boolean or String | Generate a responsive table to make it scroll horizontally. Set to `true` for an always responsive table, or set it to one of the breakpoints `'sm'`, `'md'`, `'lg'`, or `'xl'` to make the table responsive (horizontally scroll) only on screens smaller than the breakpoint. See [Responsive tables](#responsive-tables) below for details. |
420+
| `sticky-header` | Boolean or String | Generates a vertically scrollable table with sticky headers. Set to `true` to enable sticky headers (default table max-height of `300px`), or set it to a string containing a height (with CSS units) to specify a maximum height other than `300px`. See the [Sticky header](#sticky-header) section below for details. |
420421
| `stacked` | Boolean or String | Generate a responsive stacked table. Set to `true` for an always stacked table, or set it to one of the breakpoints `'sm'`, `'md'`, `'lg'`, or `'xl'` to make the table visually stacked only on screens smaller than the breakpoint. See [Stacked tables](#stacked-tables) below for details. |
421422
| `caption-top` | Boolean | If the table has a caption, and this prop is set to `true`, the caption will be visually placed above the table. If `false` (the default), the caption will be visually placed below the table. |
422423
| `table-variant` | String | <span class="badge badge-info small">NEW in 2.0.0-rc.28</span> Give the table an overall theme color variant. |
@@ -631,6 +632,58 @@ values: `sm`, `md`, `lg`, or `xl`.
631632
clips off any content that goes beyond the bottom or top edges of the table. In particular, this
632633
may clip off dropdown menus and other third-party widgets.
633634

635+
### Sticky header
636+
637+
<span class="badge badge-info small">NEW in 2.0.0-rc.28</span>
638+
639+
Use the `sticky-header` prop to enable a vertically scrolling table with headers that remain fixed
640+
(sticky) as the table boxy scrolls. Setting the prop to `true` (or no explicit value) will generate
641+
a table that has a maximum height of `300px`. To specify a maximum height other than `300px`, set
642+
the `sticky-header` prop to a valid CSS height (including units).
643+
644+
```html
645+
<template>
646+
<div>
647+
<b-table sticky-header :items="items" head-variant="light"></b-table>
648+
</div>
649+
</template>
650+
651+
<script>
652+
export default {
653+
data() {
654+
return {
655+
items: [
656+
{ 'heading 1': 'table cell', 'heading 2': 'table cell', 'heading 3': 'table cell' },
657+
{ 'heading 1': 'table cell', 'heading 2': 'table cell', 'heading 3': 'table cell' },
658+
{ 'heading 1': 'table cell', 'heading 2': 'table cell', 'heading 3': 'table cell' },
659+
{ 'heading 1': 'table cell', 'heading 2': 'table cell', 'heading 3': 'table cell' },
660+
{ 'heading 1': 'table cell', 'heading 2': 'table cell', 'heading 3': 'table cell' },
661+
{ 'heading 1': 'table cell', 'heading 2': 'table cell', 'heading 3': 'table cell' },
662+
{ 'heading 1': 'table cell', 'heading 2': 'table cell', 'heading 3': 'table cell' },
663+
{ 'heading 1': 'table cell', 'heading 2': 'table cell', 'heading 3': 'table cell' },
664+
{ 'heading 1': 'table cell', 'heading 2': 'table cell', 'heading 3': 'table cell' },
665+
{ 'heading 1': 'table cell', 'heading 2': 'table cell', 'heading 3': 'table cell' },
666+
{ 'heading 1': 'table cell', 'heading 2': 'table cell', 'heading 3': 'table cell' },
667+
{ 'heading 1': 'table cell', 'heading 2': 'table cell', 'heading 3': 'table cell' }
668+
]
669+
}
670+
}
671+
}
672+
</script>
673+
674+
<!-- b-table-sticky-header.vue -->
675+
```
676+
677+
Fee free to combine `sticky-header` with `responsive`.
678+
679+
**Notes:**
680+
681+
- Sticky header tables are wrapped inside a vertically scrollable `<div>` with a maximum height set.
682+
- BootstrapVue's custom CSS is required in order to support `sticky-header`.
683+
- The sticky header feature uses CSS style `position: sticky` to position the headings.
684+
- Internet Explorer does not support `position: sticky`, hence for IE11 the table heading will
685+
scroll with the table body.
686+
634687
### Stacked tables
635688

636689
An alternative to responsive tables, BootstrapVue includes the stacked table option (using custom
@@ -642,7 +695,7 @@ breakpoint values `'sm'`, `'md'`, `'lg'`, or `'xl'`.
642695
Column header labels will be rendered to the left of each field value using a CSS `::before` pseudo
643696
element, with a width of 40%.
644697

645-
The prop `stacked` takes precedence over the `responsive` prop.
698+
The prop `stacked` takes precedence over the `responsive` and `sticky-header` props.
646699

647700
**Example: Always stacked table**
648701

@@ -684,6 +737,8 @@ The prop `stacked` takes precedence over the `responsive` prop.
684737
- In an always stacked table, the table header and footer, and the fixed top and bottom row slots
685738
will not be rendered.
686739

740+
BootstrapVue's custom CSS is required in order to support stacked tables.
741+
687742
### Table caption
688743

689744
Add an optional caption to your table via the prop `caption` or the named slot `table-caption` (the
@@ -1613,8 +1668,8 @@ if it is an object and then sorted.
16131668
`sortByFormatted` is set to `true`. The default is `false` which will sort by the original field
16141669
value. This is only applicable for the built-in sort-compare routine.
16151670
- <span class="badge badge-info small">NEW in v2.0.0-rc.28</span> By default, the internal sorting
1616-
routine will sort `null`, `undefined`, or emptry string values first (less than any other values).
1617-
To sort so that `null`, `undefined` or emptry string values appear last (greater than any other
1671+
routine will sort `null`, `undefined`, or empty string values first (less than any other values).
1672+
To sort so that `null`, `undefined` or empty string values appear last (greater than any other
16181673
value), set the `sort-null-last` prop to `true`.
16191674

16201675
For customizing the sort-compare handling, refer to the

src/components/table/_table.scss

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1+
// --- General styling ---
12
.table.b-table {
2-
// --- General styling ---
3-
43
// Table fixed header width layout
54
&.b-table-fixed {
65
// Fixed width columns
@@ -29,6 +28,56 @@
2928
}
3029
}
3130

31+
// --- Table sticky header styling ---
32+
.b-table-sticky-header {
33+
overflow-y: auto;
34+
// Default `max-height` before scrollbar will show
35+
// We don't use `height` as table could be shorter than this value
36+
max-height: $b-table-sticky-header-max-height;
37+
// Move the table bottom margin to the wrapper
38+
margin-bottom: $spacer;
39+
40+
> .table {
41+
// Reset `margin-bottom` to we don't get a space after
42+
// the table in the scroll area
43+
margin-bottom: 0;
44+
}
45+
}
46+
47+
@supports (position: sticky) {
48+
.b-table-sticky-header {
49+
> .table {
50+
> thead > tr {
51+
> th,
52+
> td {
53+
position: sticky;
54+
top: 0;
55+
z-index: 2;
56+
}
57+
}
58+
59+
// Default theme color background for table headers
60+
// Applied only when no variant is applied to the rows, or no head-variant
61+
// Needed because Bootstrap v4 doesn't have table child elements
62+
// set up to inherit their background from parent element by default
63+
> thead > tr > th.table-b-table-default,
64+
> thead > tr > td.table-b-table-default {
65+
color: $table-color;
66+
// Default header background
67+
// `$table-bg` is null by default in Bootstrap v4 variables
68+
background-color: if($table-bg, $table-bg, $body-bg);
69+
}
70+
71+
&.table-dark > thead > tr > th.bg-b-table-default,
72+
&.table-dark > thead > tr > td.bg-b-table-default {
73+
color: $table-dark-color;
74+
// Default header background in table dark mode
75+
background-color: $table-dark-bg;
76+
}
77+
}
78+
}
79+
}
80+
3281
// --- Header sort styling ---
3382
.table.b-table {
3483
> thead,
@@ -39,9 +88,8 @@
3988
// `&.sorting`
4089
cursor: pointer;
4190

42-
// Up/down sort=null indicator
91+
// Up/down `sort=null` indicator
4392
&::before {
44-
display: inline-block;
4593
float: right;
4694
margin-left: $b-table-sort-icon-margin-left;
4795
width: $b-table-sort-icon-width;
@@ -133,7 +181,6 @@
133181
// Cell header label pseudo element
134182
&::before {
135183
content: attr(data-label);
136-
display: inline-block;
137184
width: $b-table-stacked-heading-width;
138185
float: left;
139186
text-align: right;

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

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import { isBoolean } from '../../../utils/inspect'
2+
13
// Main `<table>` render mixin
2-
// Which includes all main table styling options
4+
// Includes all main table styling options
35

46
export default {
57
// Don't place attributes on root element automatically,
@@ -47,6 +49,11 @@ export default {
4749
type: [Boolean, String],
4850
default: false
4951
},
52+
stickyHeader: {
53+
// If a string, it is assumed to be the table `max-height` value
54+
type: [Boolean, String],
55+
default: false
56+
},
5057
captionTop: {
5158
type: Boolean,
5259
default: false
@@ -66,12 +73,24 @@ export default {
6673
const responsive = this.responsive === '' ? true : this.responsive
6774
return this.isStacked ? false : responsive
6875
},
69-
responsiveClass() {
70-
return this.isResponsive === true
71-
? 'table-responsive'
72-
: this.isResponsive
73-
? `table-responsive-${this.responsive}`
74-
: ''
76+
isStickyHeader() {
77+
const stickyHeader = this.stickyHeader === '' ? true : this.stickyHeader
78+
return this.isStacked ? false : stickyHeader
79+
},
80+
wrapperClasses() {
81+
return [
82+
this.isStickyHeader ? 'b-table-sticky-header' : '',
83+
this.isResponsive === true
84+
? 'table-responsive'
85+
: this.isResponsive
86+
? `table-responsive-${this.responsive}`
87+
: ''
88+
].filter(Boolean)
89+
},
90+
wrapperStyles() {
91+
return this.isStickyHeader && !isBoolean(this.isStickyHeader)
92+
? { maxHeight: this.isStickyHeader }
93+
: {}
7594
},
7695
tableClasses() {
7796
const hover = this.isTableSimple
@@ -169,9 +188,9 @@ export default {
169188
$content.filter(Boolean)
170189
)
171190

172-
// Add responsive wrapper if needed and return table
173-
return this.isResponsive
174-
? h('div', { key: 'b-table-responsive', class: this.responsiveClass }, [$table])
191+
// Add responsive/sticky wrapper if needed and return table
192+
return this.wrapperClasses.length > 0
193+
? h('div', { key: 'wrap', class: this.wrapperClasses, style: this.wrapperStyles }, [$table])
175194
: $table
176195
}
177196
}

src/components/table/helpers/table-cell.js

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ export const BTableCell = /*#__PURE__*/ Vue.extend({
5757
},
5858
bvTableTfoot: {
5959
default: null
60+
},
61+
bvTableTr: {
62+
default: null
6063
}
6164
},
6265
props,
@@ -68,10 +71,21 @@ export const BTableCell = /*#__PURE__*/ Vue.extend({
6871
// We only support stacked-heading in tbody in stacked mode
6972
return this.bvTableTbody && this.bvTable && this.bvTable.isStacked
7073
},
74+
isStickyHeader() {
75+
// Needed to handle header background classes, due to lack of
76+
// bg color inheritance with Bootstrap v4 tabl css
77+
return this.bvTable && this.bvTableThead && this.bvTableTr && this.bvTable.isStickyHeader
78+
},
7179
cellClasses() {
7280
// We use computed props here for improved performance by caching
7381
// the results of the string interpolation
74-
return [this.variant ? `${this.isDark ? 'bg' : 'table'}-${this.variant}` : null]
82+
let variant = this.variant
83+
if (this.isStickyHeader && !variant && !this.bvTableThead.headVariant) {
84+
// Needed for stickyheader mode as Bootstrap v4 table cells do
85+
// not inherit parent's background-color
86+
variant = this.bvTableTr.variant || this.bvTable.tableVariant || 'b-table-default'
87+
}
88+
return [variant ? `${this.isDark ? 'bg' : 'table'}-${variant}` : null]
7589
},
7690
computedColspan() {
7791
return parseSpan(this.colspan)

src/components/table/table.spec.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,50 @@ describe('table', () => {
223223
wrapper.destroy()
224224
})
225225

226+
it('has class "b-table-sticky-header" when sticky-header=true', async () => {
227+
const wrapper = mount(BTable, {
228+
propsData: {
229+
items: items1,
230+
fields: fields1,
231+
stickyHeader: true
232+
}
233+
})
234+
235+
expect(wrapper).toBeDefined()
236+
expect(wrapper.is(BTable)).toBe(true)
237+
expect(wrapper.is('div')).toBe(true)
238+
expect(wrapper.classes()).toContain('b-table-sticky-header')
239+
expect(wrapper.classes().length).toBe(1)
240+
expect(wrapper.find('table').classes()).toContain('table')
241+
expect(wrapper.find('table').classes()).toContain('b-table')
242+
expect(wrapper.find('table').classes().length).toBe(2)
243+
244+
wrapper.destroy()
245+
})
246+
247+
it('has class "b-table-sticky-header" when sticky-header=100px', async () => {
248+
const wrapper = mount(BTable, {
249+
propsData: {
250+
items: items1,
251+
fields: fields1,
252+
stickyHeader: '100px'
253+
}
254+
})
255+
256+
expect(wrapper).toBeDefined()
257+
expect(wrapper.is(BTable)).toBe(true)
258+
expect(wrapper.is('div')).toBe(true)
259+
expect(wrapper.classes()).toContain('b-table-sticky-header')
260+
expect(wrapper.classes().length).toBe(1)
261+
expect(wrapper.attributes('style')).toBeDefined()
262+
expect(wrapper.attributes('style')).toContain(`max-height: 100px;`)
263+
expect(wrapper.find('table').classes()).toContain('table')
264+
expect(wrapper.find('table').classes()).toContain('b-table')
265+
expect(wrapper.find('table').classes().length).toBe(2)
266+
267+
wrapper.destroy()
268+
})
269+
226270
it('has class "table-responsive" when responsive=true', async () => {
227271
const wrapper = mount(BTable, {
228272
propsData: {

0 commit comments

Comments
 (0)
0