diff --git a/docs/src/modules/components/AppWrapper.js b/docs/src/modules/components/AppWrapper.js index 4d5f73f7940ad2..4b438d01e1c1b6 100644 --- a/docs/src/modules/components/AppWrapper.js +++ b/docs/src/modules/components/AppWrapper.js @@ -2,7 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { StylesProvider, jssPreset } from '@material-ui/styles'; -import { Provider as ThemeProvider } from 'docs/src/modules/components/ThemeContext'; +import { ThemeProvider } from 'docs/src/modules/components/ThemeContext'; import { getCookie } from 'docs/src/modules/utils/helpers'; import { ACTION_TYPES, CODE_VARIANTS } from 'docs/src/modules/constants'; import { create } from 'jss'; diff --git a/docs/src/modules/components/ThemeContext.js b/docs/src/modules/components/ThemeContext.js index c9732b63a12fe5..8b3ac2c9565805 100644 --- a/docs/src/modules/components/ThemeContext.js +++ b/docs/src/modules/components/ThemeContext.js @@ -1,37 +1,131 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { ThemeProvider } from '@material-ui/styles'; +import { ThemeProvider as MuiThemeProvider } from '@material-ui/styles'; import { createMuiTheme, darken } from '@material-ui/core/styles'; import useMediaQuery from '@material-ui/core/useMediaQuery'; import { blue, pink } from '@material-ui/core/colors'; import { getCookie } from 'docs/src/modules/utils/helpers'; import { darkTheme, setPrismTheme } from 'docs/src/modules/components/prism'; +import deepmerge from 'deepmerge'; export const themeColor = blue[700]; const themeInitialOptions = { + dense: false, direction: 'ltr', paletteColors: {}, + spacing: 8, // spacing unit }; +/** + * @typedef {import('@material-ui/core/src/styles/createMuiTheme').ThemeOptions} ThemeOptions + * + * + * @param {ThemeOptions} themeOptions + * @returns {ThemeOptions} + */ +function usingHighDensity(themeOptions) { + return deepmerge(themeOptions, { + props: { + MuiButton: { + size: 'small', + }, + MuiFilledInput: { + margin: 'dense', + }, + MuiFormControl: { + margin: 'dense', + }, + MuiFormHelperText: { + margin: 'dense', + }, + MuiIconButton: { + size: 'small', + }, + MuiInputBase: { + margin: 'dense', + }, + MuiInputLabel: { + margin: 'dense', + }, + MuiListItem: { + dense: true, + }, + MuiOutlinedInput: { + margin: 'dense', + }, + MuiFab: { + size: 'small', + }, + MuiTable: { + size: 'small', + }, + MuiTextField: { + margin: 'dense', + }, + MuiToolbar: { + variant: 'dense', + }, + }, + overrides: { + MuiIconButton: { + sizeSmall: { + // minimal touch target hit spacing + marginLeft: 4, + marginRight: 4, + padding: 12, + }, + }, + }, + }); +} + +function usingIdentity(themeOptions) { + return themeOptions; +} + export const DispatchContext = React.createContext(() => { throw new Error('Forgot to wrap component in ThemeContext.Provider'); }); const useEnhancedEffect = typeof window === 'undefined' ? React.useEffect : React.useLayoutEffect; -export function Provider(props) { +export function ThemeProvider(props) { const { children } = props; const [themeOptions, dispatch] = React.useReducer((state, action) => { switch (action.type) { - case 'RESET_COLORS': + case 'SET_SPACING': + return { + ...state, + spacing: action.payload, + }; + case 'INCREASE_SPACING': { + return { + ...state, + spacing: state.spacing + 1, + }; + } + case 'DECREASE_SPACING': { + return { + ...state, + spacing: state.spacing - 1, + }; + } + case 'SET_DENSE': + return { + ...state, + dense: action.payload, + }; + case 'RESET_DENSITY': return { ...state, - paletteColors: themeInitialOptions.paletteColors, + dense: themeInitialOptions.dense, + spacing: themeInitialOptions.spacing, }; case 'CHANGE': return { + ...state, paletteType: action.payload.paletteType || state.paletteType, direction: action.payload.direction || state.direction, paletteColors: action.payload.paletteColors || state.paletteColors, @@ -43,7 +137,7 @@ export function Provider(props) { const prefersDarkMode = useMediaQuery('@media (prefers-color-scheme: dark)'); const preferredType = prefersDarkMode ? 'dark' : 'light'; - const { direction, paletteColors, paletteType = preferredType } = themeOptions; + const { dense, direction, paletteColors, paletteType = preferredType, spacing } = themeOptions; React.useEffect(() => { setPrismTheme(darkTheme); @@ -71,25 +165,29 @@ export function Provider(props) { }, [direction]); const theme = React.useMemo(() => { - const nextTheme = createMuiTheme({ - direction, - nprogress: { - color: paletteType === 'light' ? '#000' : '#fff', - }, - palette: { - primary: { - main: paletteType === 'light' ? blue[700] : blue[200], + const themeDecorator = dense ? usingHighDensity : usingIdentity; + const nextTheme = createMuiTheme( + themeDecorator({ + direction, + nprogress: { + color: paletteType === 'light' ? '#000' : '#fff', }, - secondary: { - main: paletteType === 'light' ? darken(pink.A400, 0.1) : pink[200], + palette: { + primary: { + main: paletteType === 'light' ? blue[700] : blue[200], + }, + secondary: { + main: paletteType === 'light' ? darken(pink.A400, 0.1) : pink[200], + }, + type: paletteType, + background: { + default: paletteType === 'light' ? '#fff' : '#121212', + }, + ...paletteColors, }, - type: paletteType, - background: { - default: paletteType === 'light' ? '#fff' : '#121212', - }, - ...paletteColors, - }, - }); + spacing, + }), + ); nextTheme.palette.background.level2 = paletteType === 'light' ? nextTheme.palette.grey[100] : '#333'; @@ -98,7 +196,7 @@ export function Provider(props) { paletteType === 'light' ? '#fff' : nextTheme.palette.grey[900]; return nextTheme; - }, [direction, paletteColors, paletteType]); + }, [dense, direction, paletteColors, paletteType, spacing]); React.useEffect(() => { // Expose the theme as a global variable so people can play with it. @@ -108,13 +206,13 @@ export function Provider(props) { }, [theme]); return ( - + {children} - + ); } -Provider.propTypes = { +ThemeProvider.propTypes = { children: PropTypes.node, }; diff --git a/docs/src/pages.js b/docs/src/pages.js index 31c38da88e8350..23dc14b618af23 100644 --- a/docs/src/pages.js +++ b/docs/src/pages.js @@ -155,6 +155,7 @@ const pages = [ { pathname: '/customization/typography' }, { pathname: '/customization/spacing' }, { pathname: '/customization/breakpoints' }, + { pathname: '/customization/density' }, { pathname: '/customization/z-index', title: 'z-index' }, { pathname: '/customization/globals' }, ], diff --git a/docs/src/pages/customization/density/DensityTool.js b/docs/src/pages/customization/density/DensityTool.js new file mode 100644 index 00000000000000..28587a0e153c22 --- /dev/null +++ b/docs/src/pages/customization/density/DensityTool.js @@ -0,0 +1,94 @@ +import React from 'react'; +import { useTheme } from '@material-ui/core/styles'; +import Grid from '@material-ui/core/Grid'; +import Input from '@material-ui/core/Input'; +import Typography from '@material-ui/core/Typography'; +import Button from '@material-ui/core/Button'; +import IconButton from '@material-ui/core/IconButton'; +import FormControlLabel from '@material-ui/core/FormControlLabel'; +import Switch from '@material-ui/core/Switch'; +import { DispatchContext } from 'docs/src/modules/components/ThemeContext'; +import IncreaseIcon from '@material-ui/icons/AddCircleOutline'; +import DecreaseIcon from '@material-ui/icons/RemoveCircleOutline'; +import { useSelector } from 'react-redux'; + +const minSpacing = 0; +const maxSpacing = 20; + +export default function DensityTool() { + const dispatch = React.useContext(DispatchContext); + function handleDensityChange(event) { + dispatch({ type: 'SET_DENSE', payload: event.target.checked }); + } + + function handleSpacingChange(event, value) { + dispatch({ type: 'SET_SPACING', payload: value || +event.target.value }); + } + + function increaseSpacing() { + dispatch({ type: 'INCREASE_SPACING' }); + } + + function decreaseSpacing() { + dispatch({ type: 'DECREASE_SPACING' }); + } + + function resetDensity() { + dispatch({ type: 'RESET_DENSITY' }); + } + + const theme = useTheme(); + const spacingUnit = theme.spacing(1); + + const t = useSelector(state => state.options.t); + + return ( + + + + } + label={t('useHighDensity')} + /> + + + + + {t('spacingUnit')} + + + + + + + + + + + + + + + + + ); +} diff --git a/docs/src/pages/customization/density/density.md b/docs/src/pages/customization/density/density.md new file mode 100644 index 00000000000000..3572980951224b --- /dev/null +++ b/docs/src/pages/customization/density/density.md @@ -0,0 +1,102 @@ +# Density + +

How to apply density to Material-UI components.

+ +## Applying density + +We won't cover possible use cases, or considerations for using density in your application. +The Material design guidelines have a [comprehensive guide](https://material.io/design/layout/applying-density.html#typographic-density) covering these topics in more detail. + +## Implementing density + +Higher density can be applied to some components via props. The component pages +have at least one example using the respective component with higher density applied. + +Depending on the component, density is applied either via lower spacing, or simply by +reducing the size. + +The following components have props applying higher density: + +- [Button](/api/button) +- [Fab](/api/fab) +- [FilledInput](/api/filled-input) +- [FormControl](/api/form-control) +- [FormHelperText](/api/form-helper-text) +- [IconButton](/api/icon-button) +- [InputBase](/api/input-base) +- [InputLabel](/api/input-label) +- [ListItem](/api/list-item) +- [OutlinedInput](/api/outlined-input) +- [Table](/api/table) +- [TextField](/api/text-field) +- [Toolbar](/api/toolbar) + +## Explore theme density + +This tool allows you to apply density via spacing and component props. You can browse +around and see how this applies to the overall feel of Material-UI components. + +If you enable high density a custom theme is applied to the docs. This theme is only +for demonstration purposes. You _should not_ apply this theme to your whole application +as this might negatively impact user experience. The [Material design guidelines](https://material.io/design/layout/applying-density.html#typographic-density) has examples +for when not to apply density. + +The theme is configured with the following options: + +```js +const theme = createMuiTheme({ + props: { + MuiButton: { + size: 'small', + }, + MuiFilledInput: { + margin: 'dense', + }, + MuiFormControl: { + margin: 'dense', + }, + MuiFormHelperText: { + margin: 'dense', + }, + MuiIconButton: { + size: 'small', + }, + MuiInputBase: { + margin: 'dense', + }, + MuiInputLabel: { + margin: 'dense', + }, + MuiListItem: { + dense: true, + }, + MuiOutlinedInput: { + margin: 'dense', + }, + MuiFab: { + size: 'small', + }, + MuiTable: { + size: 'small', + }, + MuiTextField: { + margin: 'dense', + }, + MuiToolbar: { + variant: 'dense', + }, + }, + overrides: { + MuiIconButton: { + sizeSmall: { + // Adjust spacing to reach minimal touch target hitbox + marginLeft: 4, + marginRight: 4, + padding: 12, + }, + }, + }, +}); +``` + +{{"demo": "pages/customization/density/DensityTool.js", "hideHeader": true}} diff --git a/docs/translations/translations.json b/docs/translations/translations.json index a44346fd3c0a92..26b706d1055008 100644 --- a/docs/translations/translations.json +++ b/docs/translations/translations.json @@ -77,6 +77,11 @@ "showJSSource": "Show JavaScript source", "showTSSource": "Show TypeScript source", "close": "Close", + "useHighDensity": "Apply higher density via props", + "spacingUnit": "Spacing unit", + "resetDensity": "Reset density", + "increaseSpacing": "increase spacing", + "decreaseSpacing": "decrease spacing", "pages": { "/getting-started": "Getting Started", "/getting-started/installation": "Installation", diff --git a/packages/material-ui/src/InputLabel/InputLabel.d.ts b/packages/material-ui/src/InputLabel/InputLabel.d.ts index 7cb5088c418c4b..4fd58506110278 100644 --- a/packages/material-ui/src/InputLabel/InputLabel.d.ts +++ b/packages/material-ui/src/InputLabel/InputLabel.d.ts @@ -7,6 +7,7 @@ export interface InputLabelProps extends StandardProps; +} + +export default Page; diff --git a/test/regressions/index.js b/test/regressions/index.js index e68d912523a893..b04977859621a3 100644 --- a/test/regressions/index.js +++ b/test/regressions/index.js @@ -82,6 +82,7 @@ const blacklistFilename = [ 'docs-components-popover/AnchorPlayground.png', 'docs-components-popper/ScrollPlayground.png', 'docs-components-grid/InteractiveGrid.png', + 'docs-customization-density/DensityTool.png', ]; // Also use some of the demos to avoid code duplication.