Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[theme] Responsive font sizes #14573

Merged
merged 2 commits into from
May 2, 2019
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
3 changes: 1 addition & 2 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ commands:
command: |
sudo apt-get update
sudo apt-get install -y --force-yes gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget


jobs:
checkout:
Expand Down Expand Up @@ -156,7 +155,7 @@ jobs:
name: Can we generate the documentation?
command: yarn docs:api
- run:
name: '`yarn docs:api` changes commited?'
name: '`yarn docs:api` changes committed?'
command: git diff --exit-code
- run:
name: Can we generate the @material-ui/core build?
Expand Down
15 changes: 15 additions & 0 deletions docs/src/pages/customization/themes/ResponsiveFontSizes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import React from 'react';
import { createMuiTheme, responsiveFontSizes } from '@material-ui/core/styles';
import { ThemeProvider } from '@material-ui/styles';
import Typography from '@material-ui/core/Typography';

let theme = createMuiTheme();
theme = responsiveFontSizes(theme);

export default function ResponsiveFontSizes() {
return (
<ThemeProvider theme={theme}>
<Typography variant="h3">Responsive h3</Typography>
</ThemeProvider>
);
}
59 changes: 59 additions & 0 deletions docs/src/pages/customization/themes/themes.md
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,35 @@ html {

{{"demo": "pages/customization/themes/FontSizeTheme.js"}}

### Responsive font sizes

The typography variants properties map directly to the generated CSS.
You can use [media queries](/layout/breakpoints/#api) inside them:

```js
const theme = createMuiTheme();

theme.typography.h1 = {
fontSize: '3rem',
'@media (min-width:600px)': {
fontSize: '4.5rem',
},
[theme.breakpoints.up('md')]: {
fontSize: '6rem',
},
};
```

To automate this setup, you can use the [`responsiveFontSizes()`](#responsivefontsizes-theme-options-theme) helper to make Typography font sizes in the theme responsive.

You can see this in action in the example below. adjust your browser's window size, and notice how the font size changes as the width crosses the different [breakpoints](/layout/breakpoints/):

{{"demo": "pages/customization/themes/ResponsiveFontSizes.js"}}

### Fluid font sizes

To be done: [#15251](https://github.com/mui-org/material-ui/issues/15251).

## Spacing

We encourage you to use the `theme.spacing()` helper to create consistent spacing between the elements of your UI.
Expand Down Expand Up @@ -512,3 +541,33 @@ const theme = createMuiTheme({
},
});
```

### `responsiveFontSizes(theme, options) => theme`

Generate responsive typography settings based on the options received.

#### Arguments

1. `theme` (*Object*): The theme object to enhance.
2. `options` (*Object* [optional]):

- `breakpoints` (*Array<String>* [optional]): Default to `['sm', 'md', 'lg']`. Array of [breakpoints](/layout/breakpoints/) (identifiers).
- `disableAlign` (*Boolean* [optional]): Default to `false`. Whether font sizes change slightly so line
heights are preserved and align to Material Design's 4px line height grid.
This requires a unitless line height in the theme's styles.
- `factor` (*Number* [optional]): Default to `2`. This value determines the strength of font size resizing. The higher the value, the less difference there is between font sizes on small screens.
The lower the value, the bigger font sizes for small screens. The value must me greater than 1.
- `variants` (*Array<String>* [optional]): Default to all. The typography variants to handle.

#### Returns

`theme` (*Object*): The new theme with a responsive typography.

#### Examples

```js
import { createMuiTheme, responsiveFontSizes } from '@material-ui/core/styles';

let theme = createMuiTheme();
theme = responsiveFontSizes(theme);
```
1 change: 1 addition & 0 deletions packages/material-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"@types/react-transition-group": "^2.0.16",
"clsx": "^1.0.2",
"csstype": "^2.5.2",
"convert-css-length": "^1.0.2",
"debounce": "^1.1.0",
"deepmerge": "^3.0.0",
"hoist-non-react-statics": "^3.2.1",
Expand Down
1 change: 1 addition & 0 deletions packages/material-ui/src/styles/createTypography.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export default function createTypography(palette, typography) {

return deepmerge(
{
htmlFontSize,
pxToRem,
round,
fontFamily,
Expand Down
73 changes: 73 additions & 0 deletions packages/material-ui/src/styles/cssUtils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
export function alignProperty({ size, grid }) {
const sizeBelow = size - (size % grid);
const sizeAbove = sizeBelow + grid;

return size - sizeBelow < sizeAbove - size ? sizeBelow : sizeAbove;
}

// fontGrid finds a minimal grid (in rem) for the fontSize values so that the
// lineHeight falls under a x pixels grid, 4px in the case of Material Design,
// without changing the relative line height
export function fontGrid({ lineHeight, pixels, htmlFontSize }) {
return pixels / (lineHeight * htmlFontSize);
}

/**
* generate a responsive version of a given CSS property
* @example
* responsiveProperty({
* cssProperty: 'fontSize',
* min: 15,
* max: 20,
* unit: 'px',
* breakpoints: [300, 600],
* })
*
* // this returns
*
* {
* fontSize: '15px',
* '@media (min-width:300px)': {
* fontSize: '17.5px',
* },
* '@media (min-width:600px)': {
* fontSize: '20px',
* },
* }
*
* @param {Object} params
* @param {string} params.cssProperty - The CSS property to be made responsive
* @param {number} params.min - The smallest value of the CSS property
* @param {number} params.max - The largest value of the CSS property
* @param {number} [params.unit] - The unit to be used for the CSS property
eps1lon marked this conversation as resolved.
Show resolved Hide resolved
* @param {Array.number} [params.breakpoints] - An array of breakpoints
* @param {number} [params.alignStep] - Round scaled value to fall under this grid
* @returns {Object} responsive styles for {params.cssProperty}
*/
export function responsiveProperty({
cssProperty,
min,
max,
unit = 'rem',
breakpoints = [600, 960, 1280],
transform = null,
}) {
const output = {
[cssProperty]: `${min}${unit}`,
};

const factor = (max - min) / breakpoints[breakpoints.length - 1];
breakpoints.forEach(breakpoint => {
let value = min + factor * breakpoint;

if (transform !== null) {
value = transform(value);
}

output[`@media (min-width:${breakpoint}px)`] = {
[cssProperty]: `${Math.round(value * 10000) / 10000}${unit}`,
};
});

return output;
}
99 changes: 99 additions & 0 deletions packages/material-ui/src/styles/cssUtils.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { assert } from 'chai';
import { alignProperty, fontGrid, responsiveProperty } from './cssUtils';

describe('cssUtils', () => {
describe('alignProperty', () => {
const tests = [
{ args: { size: 8, grid: 4 }, expected: 8 },
{ args: { size: 8, grid: 1 }, expected: 8 },
{ args: { size: 8, grid: 9 }, expected: 9 },
{ args: { size: 8, grid: 7 }, expected: 7 },
{ args: { size: 8, grid: 17 }, expected: 0 },
];

tests.forEach(test => {
const {
args: { size, grid },
expected,
} = test;

it(`aligns ${size} on grid ${grid} to ${expected}`, () => {
const sizeAligned = alignProperty({ size, grid });
assert.strictEqual(sizeAligned, expected);
});
});
});

describe('fontGrid', () => {
const tests = [
{ lineHeight: 1.3, pixels: 4, htmlFontSize: 16 },
{ lineHeight: 1.6, pixels: 9, htmlFontSize: 15 },
{ lineHeight: 1.0, pixels: 3, htmlFontSize: 14 },
];

tests.forEach(test => {
const { lineHeight, pixels, htmlFontSize } = test;

describe(`when ${lineHeight} lineHeight, ${pixels} pixels,
${htmlFontSize} htmlFontSize`, () => {
const grid = fontGrid({ lineHeight, pixels, htmlFontSize });

it(`should return a font grid such that the relative lineHeight is aligned`, () => {
const absoluteLineHeight = grid * lineHeight * htmlFontSize;
assert.strictEqual(Math.round((absoluteLineHeight % pixels) * 100000) / 100000, 0);
});
});

it(`with ${lineHeight} lineHeight, ${pixels} pixels,
${htmlFontSize} htmlFontSize, the font grid is such that
there is no smaller font aligning the lineHeight`, () => {
const grid = fontGrid({ lineHeight, pixels, htmlFontSize });
const absoluteLineHeight = grid * lineHeight * htmlFontSize;
assert.strictEqual(Math.floor(absoluteLineHeight / pixels), 1);
});
});
});

describe('responsiveProperty', () => {
describe('when providing two breakpoints and pixel units', () => {
it('should respond with three styles in pixels', () => {
const result = responsiveProperty({
cssProperty: 'fontSize',
min: 15,
max: 20,
unit: 'px',
breakpoints: [300, 600],
});

assert.deepEqual(result, {
fontSize: '15px',
'@media (min-width:300px)': {
fontSize: '17.5px',
},
'@media (min-width:600px)': {
fontSize: '20px',
},
});
});
});

describe('when providing one breakpoint and requesting rem units', () => {
it('should respond with two styles in rem', () => {
const result = responsiveProperty({
cssProperty: 'fontSize',
min: 0.875,
max: 1,
unit: 'rem',
breakpoints: [500],
});

assert.deepEqual(result, {
fontSize: '0.875rem',
'@media (min-width:500px)': {
fontSize: '1rem',
},
});
});
});
});
});
1 change: 1 addition & 0 deletions packages/material-ui/src/styles/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export { default as createMuiTheme } from './createMuiTheme';
export { default as createStyles } from './createStyles';
export { default as makeStyles } from './makeStyles';
export { default as MuiThemeProvider } from './MuiThemeProvider';
export { default as responsiveFontSizes } from './responsiveFontSizes';
export { default as styled } from './styled';
export * from './transitions';
export { default as useTheme } from './useTheme';
Expand Down
90 changes: 90 additions & 0 deletions packages/material-ui/src/styles/responsiveFontSizes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import convertLength from 'convert-css-length';
import { responsiveProperty, alignProperty, fontGrid } from './cssUtils';

function isUnitless(value) {
return String(parseFloat(value)).length === String(value).length;
}

export default function responsiveFontSizes(themeInput, options = {}) {
const {
breakpoints = ['sm', 'md', 'lg'],
disableAlign = false,
factor = 2,
variants = [
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'subtitle1',
'subtitle2',
'body1',
'body2',
'caption',
'button',
'overline',
],
} = options;

const theme = { ...themeInput };
theme.typography = { ...theme.typography };
const typography = theme.typography;

// Convert between css lengths e.g. em->px or px->rem
// Set the baseFontSize for your project. Defaults to 16px (also the browser default).
const convert = convertLength(typography.htmlFontSize);
const breakpointValues = breakpoints.map(x => theme.breakpoints.values[x]);

variants.forEach(variant => {
const style = typography[variant];
const remFontSize = parseFloat(convert(style.fontSize, 'rem'));

if (remFontSize <= 1) {
return;
}

const maxFontSize = remFontSize;
const minFontSize = 1 + (maxFontSize - 1) / factor;

let { lineHeight } = style;

if (!isUnitless(lineHeight) && !disableAlign) {
throw new Error(
[
`Material-UI: unsupported non-unitless line height with grid alignment.`,
'Use unitless line heights instead.',
].join('\n'),
);
}

if (!isUnitless(lineHeight)) {
// make it unitless
lineHeight = parseFloat(convert(lineHeight, 'rem')) / parseFloat(remFontSize);
}

let transform = null;

if (!disableAlign) {
transform = value =>
alignProperty({
size: value,
grid: fontGrid({ pixels: 4, lineHeight, htmlFontSize: typography.htmlFontSize }),
});
}

typography[variant] = {
...style,
...responsiveProperty({
cssProperty: 'fontSize',
min: minFontSize,
max: maxFontSize,
unit: 'rem',
breakpoints: breakpointValues,
transform,
}),
};
});

return theme;
}
Loading