Skip to content

Commit

Permalink
feat(table): add sticky header support (closes #2085) (#3831)
Browse files Browse the repository at this point in the history
  • Loading branch information
tmorehouse committed Aug 8, 2019
1 parent 06c6119 commit a5f7266
Show file tree
Hide file tree
Showing 6 changed files with 201 additions and 19 deletions.
3 changes: 3 additions & 0 deletions src/_variables.scss
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ $b-table-sort-icon-descending: "\2191" !default; // Up arrow
$b-table-sort-icon-margin-left: 0.5em !default;
$b-table-sort-icon-width: 0.5em !default;

// Default max-height for tables with sticky headers
$b-table-sticky-header-max-height: 300px !default;

// Flag to enable table stacked CSS generation
$bv-enable-table-stacked: true !default;
// Table stacked defaults
Expand Down
61 changes: 58 additions & 3 deletions src/components/table/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,7 @@ details.
| `dark` | Boolean | Invert the colors — with light text on dark backgrounds (equivalent to Bootstrap v4 class `.table-dark`) |
| `fixed` | Boolean | Generate a table with equal fixed-width columns (`table-layout: fixed;`) |
| `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. |
| `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. |
| `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. |
| `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. |
| `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. |
Expand Down Expand Up @@ -631,6 +632,58 @@ values: `sm`, `md`, `lg`, or `xl`.
clips off any content that goes beyond the bottom or top edges of the table. In particular, this
may clip off dropdown menus and other third-party widgets.

### Sticky header

<span class="badge badge-info small">NEW in 2.0.0-rc.28</span>

Use the `sticky-header` prop to enable a vertically scrolling table with headers that remain fixed
(sticky) as the table boxy scrolls. Setting the prop to `true` (or no explicit value) will generate
a table that has a maximum height of `300px`. To specify a maximum height other than `300px`, set
the `sticky-header` prop to a valid CSS height (including units).

```html
<template>
<div>
<b-table sticky-header :items="items" head-variant="light"></b-table>
</div>
</template>

<script>
export default {
data() {
return {
items: [
{ 'heading 1': 'table cell', 'heading 2': 'table cell', 'heading 3': 'table cell' },
{ 'heading 1': 'table cell', 'heading 2': 'table cell', 'heading 3': 'table cell' },
{ 'heading 1': 'table cell', 'heading 2': 'table cell', 'heading 3': 'table cell' },
{ 'heading 1': 'table cell', 'heading 2': 'table cell', 'heading 3': 'table cell' },
{ 'heading 1': 'table cell', 'heading 2': 'table cell', 'heading 3': 'table cell' },
{ 'heading 1': 'table cell', 'heading 2': 'table cell', 'heading 3': 'table cell' },
{ 'heading 1': 'table cell', 'heading 2': 'table cell', 'heading 3': 'table cell' },
{ 'heading 1': 'table cell', 'heading 2': 'table cell', 'heading 3': 'table cell' },
{ 'heading 1': 'table cell', 'heading 2': 'table cell', 'heading 3': 'table cell' },
{ 'heading 1': 'table cell', 'heading 2': 'table cell', 'heading 3': 'table cell' },
{ 'heading 1': 'table cell', 'heading 2': 'table cell', 'heading 3': 'table cell' },
{ 'heading 1': 'table cell', 'heading 2': 'table cell', 'heading 3': 'table cell' }
]
}
}
}
</script>

<!-- b-table-sticky-header.vue -->
```

Fee free to combine `sticky-header` with `responsive`.

**Notes:**

- Sticky header tables are wrapped inside a vertically scrollable `<div>` with a maximum height set.
- BootstrapVue's custom CSS is required in order to support `sticky-header`.
- The sticky header feature uses CSS style `position: sticky` to position the headings.
- Internet Explorer does not support `position: sticky`, hence for IE11 the table heading will
scroll with the table body.

### Stacked tables

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

The prop `stacked` takes precedence over the `responsive` prop.
The prop `stacked` takes precedence over the `responsive` and `sticky-header` props.

**Example: Always stacked table**

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

BootstrapVue's custom CSS is required in order to support stacked tables.

### Table caption

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

For customizing the sort-compare handling, refer to the
Expand Down
57 changes: 52 additions & 5 deletions src/components/table/_table.scss
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
// --- General styling ---
.table.b-table {
// --- General styling ---

// Table fixed header width layout
&.b-table-fixed {
// Fixed width columns
Expand Down Expand Up @@ -29,6 +28,56 @@
}
}

// --- Table sticky header styling ---
.b-table-sticky-header {
overflow-y: auto;
// Default `max-height` before scrollbar will show
// We don't use `height` as table could be shorter than this value
max-height: $b-table-sticky-header-max-height;
// Move the table bottom margin to the wrapper
margin-bottom: $spacer;

> .table {
// Reset `margin-bottom` to we don't get a space after
// the table in the scroll area
margin-bottom: 0;
}
}

@supports (position: sticky) {
.b-table-sticky-header {
> .table {
> thead > tr {
> th,
> td {
position: sticky;
top: 0;
z-index: 2;
}
}

// Default theme color background for table headers
// Applied only when no variant is applied to the rows, or no head-variant
// Needed because Bootstrap v4 doesn't have table child elements
// set up to inherit their background from parent element by default
> thead > tr > th.table-b-table-default,
> thead > tr > td.table-b-table-default {
color: $table-color;
// Default header background
// `$table-bg` is null by default in Bootstrap v4 variables
background-color: if($table-bg, $table-bg, $body-bg);
}

&.table-dark > thead > tr > th.bg-b-table-default,
&.table-dark > thead > tr > td.bg-b-table-default {
color: $table-dark-color;
// Default header background in table dark mode
background-color: $table-dark-bg;
}
}
}
}

// --- Header sort styling ---
.table.b-table {
> thead,
Expand All @@ -39,9 +88,8 @@
// `&.sorting`
cursor: pointer;

// Up/down sort=null indicator
// Up/down `sort=null` indicator
&::before {
display: inline-block;
float: right;
margin-left: $b-table-sort-icon-margin-left;
width: $b-table-sort-icon-width;
Expand Down Expand Up @@ -133,7 +181,6 @@
// Cell header label pseudo element
&::before {
content: attr(data-label);
display: inline-block;
width: $b-table-stacked-heading-width;
float: left;
text-align: right;
Expand Down
39 changes: 29 additions & 10 deletions src/components/table/helpers/mixin-table-renderer.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { isBoolean } from '../../../utils/inspect'

// Main `<table>` render mixin
// Which includes all main table styling options
// Includes all main table styling options

export default {
// Don't place attributes on root element automatically,
Expand Down Expand Up @@ -47,6 +49,11 @@ export default {
type: [Boolean, String],
default: false
},
stickyHeader: {
// If a string, it is assumed to be the table `max-height` value
type: [Boolean, String],
default: false
},
captionTop: {
type: Boolean,
default: false
Expand All @@ -66,12 +73,24 @@ export default {
const responsive = this.responsive === '' ? true : this.responsive
return this.isStacked ? false : responsive
},
responsiveClass() {
return this.isResponsive === true
? 'table-responsive'
: this.isResponsive
? `table-responsive-${this.responsive}`
: ''
isStickyHeader() {
const stickyHeader = this.stickyHeader === '' ? true : this.stickyHeader
return this.isStacked ? false : stickyHeader
},
wrapperClasses() {
return [
this.isStickyHeader ? 'b-table-sticky-header' : '',
this.isResponsive === true
? 'table-responsive'
: this.isResponsive
? `table-responsive-${this.responsive}`
: ''
].filter(Boolean)
},
wrapperStyles() {
return this.isStickyHeader && !isBoolean(this.isStickyHeader)
? { maxHeight: this.isStickyHeader }
: {}
},
tableClasses() {
const hover = this.isTableSimple
Expand Down Expand Up @@ -169,9 +188,9 @@ export default {
$content.filter(Boolean)
)

// Add responsive wrapper if needed and return table
return this.isResponsive
? h('div', { key: 'b-table-responsive', class: this.responsiveClass }, [$table])
// Add responsive/sticky wrapper if needed and return table
return this.wrapperClasses.length > 0
? h('div', { key: 'wrap', class: this.wrapperClasses, style: this.wrapperStyles }, [$table])
: $table
}
}
16 changes: 15 additions & 1 deletion src/components/table/helpers/table-cell.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ export const BTableCell = /*#__PURE__*/ Vue.extend({
},
bvTableTfoot: {
default: null
},
bvTableTr: {
default: null
}
},
props,
Expand All @@ -68,10 +71,21 @@ export const BTableCell = /*#__PURE__*/ Vue.extend({
// We only support stacked-heading in tbody in stacked mode
return this.bvTableTbody && this.bvTable && this.bvTable.isStacked
},
isStickyHeader() {
// Needed to handle header background classes, due to lack of
// bg color inheritance with Bootstrap v4 tabl css
return this.bvTable && this.bvTableThead && this.bvTableTr && this.bvTable.isStickyHeader
},
cellClasses() {
// We use computed props here for improved performance by caching
// the results of the string interpolation
return [this.variant ? `${this.isDark ? 'bg' : 'table'}-${this.variant}` : null]
let variant = this.variant
if (this.isStickyHeader && !variant && !this.bvTableThead.headVariant) {
// Needed for stickyheader mode as Bootstrap v4 table cells do
// not inherit parent's background-color
variant = this.bvTableTr.variant || this.bvTable.tableVariant || 'b-table-default'
}
return [variant ? `${this.isDark ? 'bg' : 'table'}-${variant}` : null]
},
computedColspan() {
return parseSpan(this.colspan)
Expand Down
44 changes: 44 additions & 0 deletions src/components/table/table.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,50 @@ describe('table', () => {
wrapper.destroy()
})

it('has class "b-table-sticky-header" when sticky-header=true', async () => {
const wrapper = mount(BTable, {
propsData: {
items: items1,
fields: fields1,
stickyHeader: true
}
})

expect(wrapper).toBeDefined()
expect(wrapper.is(BTable)).toBe(true)
expect(wrapper.is('div')).toBe(true)
expect(wrapper.classes()).toContain('b-table-sticky-header')
expect(wrapper.classes().length).toBe(1)
expect(wrapper.find('table').classes()).toContain('table')
expect(wrapper.find('table').classes()).toContain('b-table')
expect(wrapper.find('table').classes().length).toBe(2)

wrapper.destroy()
})

it('has class "b-table-sticky-header" when sticky-header=100px', async () => {
const wrapper = mount(BTable, {
propsData: {
items: items1,
fields: fields1,
stickyHeader: '100px'
}
})

expect(wrapper).toBeDefined()
expect(wrapper.is(BTable)).toBe(true)
expect(wrapper.is('div')).toBe(true)
expect(wrapper.classes()).toContain('b-table-sticky-header')
expect(wrapper.classes().length).toBe(1)
expect(wrapper.attributes('style')).toBeDefined()
expect(wrapper.attributes('style')).toContain(`max-height: 100px;`)
expect(wrapper.find('table').classes()).toContain('table')
expect(wrapper.find('table').classes()).toContain('b-table')
expect(wrapper.find('table').classes().length).toBe(2)

wrapper.destroy()
})

it('has class "table-responsive" when responsive=true', async () => {
const wrapper = mount(BTable, {
propsData: {
Expand Down

0 comments on commit a5f7266

Please sign in to comment.