Skip to content
This repository has been archived by the owner on Dec 6, 2022. It is now read-only.

Support responsive column widths #726

Merged
merged 7 commits into from
Feb 26, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions src/core/components/layout/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions src/core/components/layout/columns.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
9 changes: 2 additions & 7 deletions src/core/components/layout/components/columns/columns.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,17 +69,12 @@ const Columns = ({
};

interface ColumnProps extends HTMLAttributes<HTMLDivElement>, 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) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if width no longer has a default value of 0, is this a breaking change?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you were relying on the default before then I think you still get the same behaviour (due to these changes) but if you're explicitly passing 0 you will now get different behaviour so maybe it's a breaking change? Also, if you pass an empty array I don't think it's applying any styling. Maybe that should be the same as not passing a width?

Copy link
Contributor Author

@SiAdcock SiAdcock Feb 18, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jamie-lynch is correct, if you were explicitly passing width={0} before, the behaviour would have been the same as if you didn't pass a width (i.e. the column's width would be the remainder of container width shared between all zero-width columns). This behaviour was not documented. Arguably the behaviour is bizarre and unexpected, even a bug.

The new behaviour is to hide a column that has a 0 width. I think this is more intuitive.

Although this is technically a breaking change, I'm inclined not to treat it as such from a semver / release perspective. Nobody is currently relying on this behaviour. Let's close this loophole before people start relying on it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch with the empty array 🙏

I've updated the logic to treat an empty array the same as passing not passing a width. I hope that makes sense!

return (
<div css={[column(width), cssOverrides]} {...props}>
{children}
Expand Down
107 changes: 82 additions & 25 deletions src/core/components/layout/components/columns/styles.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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};
`;
};
51 changes: 51 additions & 0 deletions src/core/components/layout/stories/columns/responsive.tsx
Original file line number Diff line number Diff line change
@@ -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 = () => (
<Container>
<Columns>
<Column width={[1 / 2, 1 / 4]}>
<div css={contents}>50% at mobile, 25% above tablet</div>
</Column>
<Column width={[1 / 2, 3 / 4]}>
<div css={contents}>50% width at mobile, 75% above tablet</div>
</Column>
</Columns>
</Container>
);

responsive.story = {
name: 'responsive columns',
parameters: {
viewport: { defaultViewport: 'tablet' },
},
};

export const responsivelyHide = () => (
<Container>
<Columns>
<Column width={[0, 1 / 4]}>
<div css={contents}>
Not visible at mobile, 25% above tablet
</div>
</Column>
<Column width={[1, 3 / 4]}>
<div css={contents}>100% at mobile, 75% above tablet</div>
</Column>
</Columns>
</Container>
);

responsivelyHide.story = {
name: 'responsively hide columns',
parameters: {
viewport: { defaultViewport: 'tablet' },
},
};