diff --git a/src/components/layout/README.md b/src/components/layout/README.md index cf89141653e..203e3d7f7ff 100644 --- a/src/components/layout/README.md +++ b/src/components/layout/README.md @@ -70,11 +70,10 @@ like the ## Containers `` -Containers (``) are the most basic layout element in Bootstrap and is **required when -using the grid system**. Choose from a responsive, fixed-width container (meaning its max-width -changes at each breakpoint) by default, or fluid-width (meaning it's 100% wide all the time) by -setting 'fluid' prop, or responsive containers where the container is fluid up until a specific -breakpoint. +Containers (``) are the most basic layout element in Bootstrap. Choose from a +responsive, fixed-width container (meaning its max-width changes at each breakpoint) by default, or +fluid-width (meaning it's 100% wide all the time) by setting 'fluid' prop, or responsive containers +where the container is fluid up until a specific breakpoint (requires Bootstrap CSS `v4.4+`). While containers can be nested, most layouts do not require a nested container. @@ -85,7 +84,7 @@ The default breakpoint widths can be configured using Bootstrap V4.x SCSS variab ### Default container The default `` is a responsive, fixed-width container, meaning its `max-width` changes -at each breakpoint. +at each viewport width breakpoint. ```html @@ -109,7 +108,7 @@ Setting the `fluid` prop to true (or an empty string) is equivalent to the Boots ### Responsive fluid containers -Requires Bootstrap v4.4+ CSS +Requires Bootstrap v4.4+ CSS Responsive containers are new in Bootstrap v4.4. They allow you to specify a container that is 100% wide (fluid) until particular breakpoint is reached at which point a `max-width` is applied. For @@ -137,8 +136,9 @@ Setting the fluid prop to a breakpoint name translates to the Bootstrap class ## Rows `` and `` -`` components should be placed inside a `` component, or an element (such as a -`
`) that has the class `container` or `container-fluid` applied to it. +Rows are wrappers for [columns](#columns-b-col). Each column has horizontal padding (called a +gutter) for controlling the space between them. This padding is then counteracted on the rows with +negative margins. This way, all the content in your columns is visually aligned down the left side. You can remove the margin from `` and padding from `` by setting the `no-gutters` prop on ``. @@ -622,4 +622,83 @@ within an existing `` component. Nested rows should include a set of colu ``` +## Row columns + +Requires Bootstrap v4.4+ CSS + +Use the responsive `cols-*` props in `` to quickly set the number of columns that best render +your content and layout. Whereas normal column widths are apply to the individual `` columns +(e.g., ``), the row columns `col-*` props are set on the parent `` as a +shortcut. + +Use these row columns to quickly create basic grid layouts or to control your card layouts. The +default maximum number of row columns in Bootstrap v4.4 is `6` (unlike the regular columns which +have a default maximum of `12` columns) + +The value specified in the `` prop(s) is the number of columns to create per row (whereas the +props on `` refer to the number of columns to occupy). + +```html + + + Column + Column + Column + Column + + + + + + Column + Column + Column + Column + + + + + + Column + Column + Column + Column + + + + + + Column + Column + Column + Column + + + + +``` + +You can control the number of columns at each breakpoint level via the following `` props: + +- `cols` for `xs` and up screens +- `cols-sm` for `sm` and up screens +- `cols-md` for `md` and up screens +- `cols-lg` for `lg` and up screens +- `cols-xl` for `xl` and up screens + +```html + + + Column + Column + Column + Column + Column + Column + + + + +``` + diff --git a/src/components/layout/col.js b/src/components/layout/col.js index 52e06faf495..c10b1de2beb 100644 --- a/src/components/layout/col.js +++ b/src/components/layout/col.js @@ -3,9 +3,10 @@ import identity from '../../utils/identity' import memoize from '../../utils/memoize' import suffixPropName from '../../utils/suffix-prop-name' import { arrayIncludes } from '../../utils/array' -import { isUndefinedOrNull } from '../../utils/inspect' -import { keys, assign, create } from '../../utils/object' import { getBreakpointsUpCached } from '../../utils/config' +import { isUndefinedOrNull } from '../../utils/inspect' +import { assign, create, keys } from '../../utils/object' +import { lowerCase } from '../../utils/string' const RX_COL_CLASS = /^col-/ @@ -35,11 +36,11 @@ const computeBreakpoint = (type, breakpoint, val) => { // Since the default is false, an empty string indicates the prop's presence. if (type === 'col' && (val === '' || val === true)) { // .col-md - return className.toLowerCase() + return lowerCase(className) } // .order-md-6 className += `-${val}` - return className.toLowerCase() + return lowerCase(className) } // Memoized function for better performance on generating class names diff --git a/src/components/layout/package.json b/src/components/layout/package.json index 1c95fa424d3..79e3b7ad899 100644 --- a/src/components/layout/package.json +++ b/src/components/layout/package.json @@ -11,7 +11,7 @@ "props": [ { "prop": "fluid", - "description": "When set to true, makes the row 100% wide all the time, or set to one of the Bootstrap breakpoint names for 100% width up to the breakpoint" + "description": "When set to true, makes the row 100% wide all the time, or set to one of the Bootstrap breakpoint names for 100% width up to the breakpoint (requires Bootstrap v4.4+ CSS for breakpoint specific value)" } ] }, @@ -33,6 +33,31 @@ { "prop": "alignContent", "description": "Align columns items together on the cross axis: 'start', 'center', 'end', 'around', 'between' or 'stretch'. Has no effect on single rows of items" + }, + { + "prop": "cols", + "version": "2.2.0", + "description": "The number row columns to create at the 'xs' breakpoint. Requires Bootstrap v4.4 CSS" + }, + { + "prop": "colsSm", + "version": "2.2.0", + "description": "The number row columns to create at the 'sm' breakpoint. Requires Bootstrap v4.4 CSS" + }, + { + "prop": "colsMd", + "version": "2.2.0", + "description": "The number row columns to create at the 'md' breakpoint. Requires Bootstrap v4.4 CSS" + }, + { + "prop": "colsLg", + "version": "2.2.0", + "description": "The number row columns to create at the 'lg' breakpoint. Requires Bootstrap v4.4 CSS" + }, + { + "prop": "colsXl", + "version": "2.2.0", + "description": "The number row columns to create at the 'xl' breakpoint. Requires Bootstrap v4.4 CSS" } ] }, diff --git a/src/components/layout/row.js b/src/components/layout/row.js index 3fdbf6d997b..a204c839881 100644 --- a/src/components/layout/row.js +++ b/src/components/layout/row.js @@ -1,53 +1,110 @@ -import Vue from '../../utils/vue' import { mergeData } from 'vue-functional-data-merge' +import identity from '../../utils/identity' +import memoize from '../../utils/memoize' +import suffixPropName from '../../utils/suffix-prop-name' import { arrayIncludes } from '../../utils/array' +import { getBreakpointsUpCached } from '../../utils/config' +import { create, keys } from '../../utils/object' +import { lowerCase, toString, trim } from '../../utils/string' const COMMON_ALIGNMENT = ['start', 'end', 'center'] -export const props = { - tag: { - type: String, - default: 'div' - }, - noGutters: { - type: Boolean, - default: false - }, - alignV: { - type: String, - default: null, - validator: str => arrayIncludes(COMMON_ALIGNMENT.concat(['baseline', 'stretch']), str) - }, - alignH: { - type: String, - default: null, - validator: str => arrayIncludes(COMMON_ALIGNMENT.concat(['between', 'around']), str) - }, - alignContent: { - type: String, - default: null, - validator: str => arrayIncludes(COMMON_ALIGNMENT.concat(['between', 'around', 'stretch']), str) +// Generates a prop object with a type of `[String, Number]` +const strNum = () => ({ + type: [String, Number], + default: null +}) + +// Compute a `row-cols-{breakpoint}-{cols}` class name +// Memoized function for better performance on generating class names +const computeRowColsClass = memoize((breakpoint, cols) => { + cols = trim(toString(cols)) + return cols ? lowerCase(['row-cols', breakpoint, cols].filter(identity).join('-')) : null +}) + +// Get the breakpoint name from the `rowCols` prop name +// Memoized function for better performance on extracting breakpoint names +const computeRowColsBreakpoint = memoize(prop => lowerCase(prop.replace('cols', ''))) + +// Cached copy of the `row-cols` breakpoint prop names +// Will be populated when the props are generated +let rowColsPropList = [] + +// Lazy evaled props factory for (called only once, +// the first time the component is used) +const generateProps = () => { + // Grab the breakpoints from the cached config (including the '' (xs) breakpoint) + const breakpoints = getBreakpointsUpCached() + + // Supports classes like: `row-cols-2`, `row-cols-md-4`, `row-cols-xl-6` + const rowColsProps = breakpoints.reduce((props, breakpoint) => { + props[suffixPropName(breakpoint, 'cols')] = strNum() + return props + }, create(null)) + + // Cache the row-cols prop names + rowColsPropList = keys(rowColsProps) + + // Return the generated props + return { + tag: { + type: String, + default: 'div' + }, + noGutters: { + type: Boolean, + default: false + }, + alignV: { + type: String, + default: null, + validator: str => arrayIncludes(COMMON_ALIGNMENT.concat(['baseline', 'stretch']), str) + }, + alignH: { + type: String, + default: null, + validator: str => arrayIncludes(COMMON_ALIGNMENT.concat(['between', 'around']), str) + }, + alignContent: { + type: String, + default: null, + validator: str => + arrayIncludes(COMMON_ALIGNMENT.concat(['between', 'around', 'stretch']), str) + }, + ...rowColsProps } } +// We do not use `Vue.extend()` here as that would evaluate the props +// immediately, which we do not want to happen // @vue/component -export const BRow = /*#__PURE__*/ Vue.extend({ +export const BRow = { name: 'BRow', functional: true, - props, + get props() { + // Allow props to be lazy evaled on first access and + // then they become a non-getter afterwards + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/get#Smart_self-overwriting_lazy_getters + delete this.props + this.props = generateProps() + return this.props + }, render(h, { props, data, children }) { - return h( - props.tag, - mergeData(data, { - staticClass: 'row', - class: { - 'no-gutters': props.noGutters, - [`align-items-${props.alignV}`]: props.alignV, - [`justify-content-${props.alignH}`]: props.alignH, - [`align-content-${props.alignContent}`]: props.alignContent - } - }), - children - ) + const classList = [] + // Loop through row-cols breakpoint props and generate the classes + rowColsPropList.forEach(prop => { + const c = computeRowColsClass(computeRowColsBreakpoint(prop), props[prop]) + // If a class is returned, push it onto the array + if (c) { + classList.push(c) + } + }) + classList.push({ + 'no-gutters': props.noGutters, + [`align-items-${props.alignV}`]: props.alignV, + [`justify-content-${props.alignH}`]: props.alignH, + [`align-content-${props.alignContent}`]: props.alignContent + }) + return h(props.tag, mergeData(data, { staticClass: 'row', class: classList }), children) } -}) +} diff --git a/src/components/layout/row.spec.js b/src/components/layout/row.spec.js index bdeed6c3894..f4423608374 100644 --- a/src/components/layout/row.spec.js +++ b/src/components/layout/row.spec.js @@ -88,4 +88,51 @@ describe('layout > row', () => { expect(wrapper.classes()).toContain('align-content-stretch') expect(wrapper.classes().length).toBe(2) }) + + it('has class row-cols-6 when prop cols is set to 6', async () => { + const wrapper = mount(BRow, { + propsData: { + cols: 6 + } + }) + + expect(wrapper.is('div')).toBe(true) + expect(wrapper.classes()).toContain('row') + expect(wrapper.classes()).toContain('row-cols-6') + expect(wrapper.classes().length).toBe(2) + }) + + it('has class row-cols-md-3 when prop cols-md is set to 3', async () => { + const wrapper = mount(BRow, { + propsData: { + colsMd: '3' + } + }) + + expect(wrapper.is('div')).toBe(true) + expect(wrapper.classes()).toContain('row') + expect(wrapper.classes()).toContain('row-cols-md-3') + expect(wrapper.classes().length).toBe(2) + }) + + it('all cols-* props work', async () => { + const wrapper = mount(BRow, { + propsData: { + cols: 1, + colsSm: 2, + colsMd: 3, + colsLg: 4, + colsXl: 5 + } + }) + + expect(wrapper.is('div')).toBe(true) + expect(wrapper.classes()).toContain('row') + expect(wrapper.classes()).toContain('row-cols-1') + expect(wrapper.classes()).toContain('row-cols-sm-2') + expect(wrapper.classes()).toContain('row-cols-md-3') + expect(wrapper.classes()).toContain('row-cols-lg-4') + expect(wrapper.classes()).toContain('row-cols-xl-5') + expect(wrapper.classes().length).toBe(6) + }) })