diff --git a/src/core/components/layout/README.md b/src/core/components/layout/README.md index 1ff0112db..702f02bfe 100644 --- a/src/core/components/layout/README.md +++ b/src/core/components/layout/README.md @@ -86,9 +86,16 @@ Columns will be stacked one on top of the other at viewport widths lower than th #### `width` -**`number`** +**`number | number[]`** -Fraction of the parent container's width that the column will occupy. If no value is provided, the column width will be fluid (i.e. take up remaining space, divided between all fluid columns) +Fraction of the parent container's width that the column will occupy. + +Pass 0 to hide the column completely. + +Pass an array of fractions to set the width that the column occupies at different breakpoints. The first value in the array will +reflect the width at mobile, the second value at tablet, then desktop, leftCol and wide. + +If no value is provided, the column width will be fluid (i.e. take up remaining space, divided between all fluid columns) ## Hide diff --git a/src/core/components/layout/columns.stories.tsx b/src/core/components/layout/columns.stories.tsx index 1a9f6b25f..483cf5b20 100644 --- a/src/core/components/layout/columns.stories.tsx +++ b/src/core/components/layout/columns.stories.tsx @@ -7,5 +7,6 @@ export default { export * from './stories/columns/default'; export * from './stories/columns/collapse-below'; +export * from './stories/columns/responsive'; export * from './stories/columns/with-container'; export * from './stories/columns/with-width'; diff --git a/src/core/components/layout/components/columns/columns.tsx b/src/core/components/layout/components/columns/columns.tsx index 09a38f0a6..a666fb9f6 100644 --- a/src/core/components/layout/components/columns/columns.tsx +++ b/src/core/components/layout/components/columns/columns.tsx @@ -69,17 +69,12 @@ const Columns = ({ }; interface ColumnProps extends HTMLAttributes, Props { - width?: number; + width?: number | number[]; cssOverrides?: SerializedStyles | SerializedStyles[]; children: ReactNode; } -const Column = ({ - width = 0, - cssOverrides, - children, - ...props -}: ColumnProps) => { +const Column = ({ width, cssOverrides, children, ...props }: ColumnProps) => { return (
{children} diff --git a/src/core/components/layout/components/columns/styles.ts b/src/core/components/layout/components/columns/styles.ts index fe784af96..c67baa5c1 100644 --- a/src/core/components/layout/components/columns/styles.ts +++ b/src/core/components/layout/components/columns/styles.ts @@ -1,6 +1,6 @@ import { css } from '@emotion/react'; import { space } from '@guardian/src-foundations'; -import { until } from '@guardian/src-foundations/mq'; +import { Breakpoint, from, until } from '@guardian/src-foundations/mq'; export const columns = css` box-sizing: border-box; @@ -74,27 +74,84 @@ export const collapseBelowWide = css` } `; -export const column = (width: number) => css` - box-sizing: border-box; - /* If a width is specified, don't allow column to grow. Use the width property */ - flex: ${width ? '0 0 auto' : 1}; - /* - A set of Columns has n columns and n-1 gutters: - | |g| |g| |g| | - This means if we take a simple fraction of the width of the set of Columns, - our Column will stop part-way through a gutter: - | |g| |g| |g| | - |====50%=====|====50%=====| - To calculate width of a Column correctly, we must add an imaginary extra gutter - and take a fraction of the whole: - | |g| |g| |g| |g| - |=====50%=====||====50%=====| - This will create a Column which is x columns and x gutters wide. We really want the - Column to be x columns and x-1 gutters, so we must subtract a gutter at the end: - | |g| |g| |g| |g| - |====50%====| |====50%====| - */ - ${width - ? `width: calc((100% + ${space[5]}px) * ${width} - ${space[5]}px);` - : ''} -`; +/* + A set of Columns has n columns and n-1 gutters: + | |g| |g| |g| | + This means if we take a simple fraction of the width of the set of Columns, + our Column will stop part-way through a gutter: + | |g| |g| |g| | + |====50%=====|====50%=====| + To calculate width of a Column correctly, we must add an imaginary extra gutter + and take a fraction of the whole: + | |g| |g| |g| |g| + |=====50%=====||====50%=====| + This will create a Column which is x columns and x gutters wide. We really want the + Column to be x columns and x-1 gutters, so we must subtract a gutter at the end: + | |g| |g| |g| |g| + |====50%====| |====50%====| +*/ +const calculateWidth = (width: number) => { + if (width === 0) { + return css` + width: 0; + + /* Hide the column from screen readers */ + visibility: hidden; + + /* offset the margin-left on the next sibling */ + margin-right: ${-space[5]}px; + `; + } + + return css` + width: calc((100% + ${space[5]}px) * ${width} - ${space[5]}px); + + /* Reset values that might have been set at a lower breakpoint */ + visibility: visible; + margin-right: 0; + `; +}; + +const generateWidthCSS = (width: number | number[]) => { + if (Array.isArray(width)) { + const breakpoints: Breakpoint[] = [ + 'mobile', + 'tablet', + 'desktop', + 'leftCol', + 'wide', + ]; + + return width.reduce((styles, w, i) => { + return css` + ${styles} + ${from[breakpoints[i]]} { + ${calculateWidth(w)}; + } + `; + }, css``); + } + + return calculateWidth(width); +}; + +export const column = (width?: number | number[]) => { + let flex; + let widthCSS; + + if (width == null || (Array.isArray(width) && width.length === 0)) { + // If no width is specified, allow the column to grow + flex = 1; + widthCSS = css``; + } else { + // If a width is specified, don't allow column to grow. Use the width property + flex = '0 0 auto'; + widthCSS = generateWidthCSS(width); + } + + return css` + box-sizing: border-box; + flex: ${flex}; + ${widthCSS}; + `; +}; diff --git a/src/core/components/layout/stories/columns/responsive.tsx b/src/core/components/layout/stories/columns/responsive.tsx new file mode 100644 index 000000000..e7400f255 --- /dev/null +++ b/src/core/components/layout/stories/columns/responsive.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import { Columns, Column, Container } from '../../index'; +import { sport } from '@guardian/src-foundations/palette'; +import { css } from '@emotion/react'; + +const contents = css` + text-align: center; + background-color: ${sport[600]}; +`; + +export const responsive = () => ( + + + +
50% at mobile, 25% above tablet
+
+ +
50% width at mobile, 75% above tablet
+
+
+
+); + +responsive.story = { + name: 'responsive columns', + parameters: { + viewport: { defaultViewport: 'tablet' }, + }, +}; + +export const responsivelyHide = () => ( + + + +
+ Not visible at mobile, 25% above tablet +
+
+ +
100% at mobile, 75% above tablet
+
+
+
+); + +responsivelyHide.story = { + name: 'responsively hide columns', + parameters: { + viewport: { defaultViewport: 'tablet' }, + }, +};