diff --git a/docs/pages/api-docs/masonry-item.js b/docs/pages/api-docs/masonry-item.js new file mode 100644 index 00000000000000..c9dce04e3c9d82 --- /dev/null +++ b/docs/pages/api-docs/masonry-item.js @@ -0,0 +1,23 @@ +import * as React from 'react'; +import ApiPage from 'docs/src/modules/components/ApiPage'; +import mapApiPageTranslations from 'docs/src/modules/utils/mapApiPageTranslations'; +import jsonPageContent from './masonry-item.json'; + +export default function Page(props) { + const { descriptions, pageContent } = props; + return ; +} + +Page.getInitialProps = () => { + const req = require.context( + 'docs/translations/api-docs/masonry-item', + false, + /masonry-item.*.json$/, + ); + const descriptions = mapApiPageTranslations(req); + + return { + descriptions, + pageContent: jsonPageContent, + }; +}; diff --git a/docs/pages/api-docs/masonry-item.json b/docs/pages/api-docs/masonry-item.json new file mode 100644 index 00000000000000..0789482ca0eb35 --- /dev/null +++ b/docs/pages/api-docs/masonry-item.json @@ -0,0 +1,19 @@ +{ + "props": { + "children": { "type": { "name": "element" }, "required": true }, + "classes": { "type": { "name": "object" } }, + "columnSpan": { "type": { "name": "number" }, "default": "1" }, + "component": { "type": { "name": "elementType" } }, + "defaultHeight": { "type": { "name": "number" } }, + "sx": { "type": { "name": "object" } } + }, + "name": "MasonryItem", + "styles": { "classes": ["root"], "globalClasses": {}, "name": "MuiMasonryItem" }, + "spread": true, + "forwardsRefTo": "HTMLDivElement", + "filename": "/packages/material-ui-lab/src/MasonryItem/MasonryItem.js", + "inheritance": null, + "demos": "", + "styledComponent": true, + "cssComponent": false +} diff --git a/docs/pages/api-docs/masonry.js b/docs/pages/api-docs/masonry.js new file mode 100644 index 00000000000000..6fb94801e7b9ea --- /dev/null +++ b/docs/pages/api-docs/masonry.js @@ -0,0 +1,19 @@ +import * as React from 'react'; +import ApiPage from 'docs/src/modules/components/ApiPage'; +import mapApiPageTranslations from 'docs/src/modules/utils/mapApiPageTranslations'; +import jsonPageContent from './masonry.json'; + +export default function Page(props) { + const { descriptions, pageContent } = props; + return ; +} + +Page.getInitialProps = () => { + const req = require.context('docs/translations/api-docs/masonry', false, /masonry.*.json$/); + const descriptions = mapApiPageTranslations(req); + + return { + descriptions, + pageContent: jsonPageContent, + }; +}; diff --git a/docs/pages/api-docs/masonry.json b/docs/pages/api-docs/masonry.json new file mode 100644 index 00000000000000..fc83112a6a8a9a --- /dev/null +++ b/docs/pages/api-docs/masonry.json @@ -0,0 +1,31 @@ +{ + "props": { + "children": { "type": { "name": "node" }, "required": true }, + "classes": { "type": { "name": "object" } }, + "columns": { + "type": { + "name": "union", + "description": "Array<number
| string>
| number
| object
| string" + }, + "default": "4" + }, + "component": { "type": { "name": "elementType" } }, + "spacing": { + "type": { + "name": "union", + "description": "Array<number
| string>
| number
| object
| string" + }, + "default": "1" + }, + "sx": { "type": { "name": "object" } } + }, + "name": "Masonry", + "styles": { "classes": ["root"], "globalClasses": {}, "name": "MuiMasonry" }, + "spread": true, + "forwardsRefTo": "HTMLDivElement", + "filename": "/packages/material-ui-lab/src/Masonry/Masonry.js", + "inheritance": null, + "demos": "", + "styledComponent": true, + "cssComponent": false +} diff --git a/docs/pages/components/masonry.js b/docs/pages/components/masonry.js new file mode 100644 index 00000000000000..2a4f8621fd449b --- /dev/null +++ b/docs/pages/components/masonry.js @@ -0,0 +1,11 @@ +import * as React from 'react'; +import MarkdownDocs from 'docs/src/modules/components/MarkdownDocs'; +import { + demos, + docs, + demoComponents, +} from 'docs/src/pages/components/masonry/masonry.md?@material-ui/markdown'; + +export default function Page() { + return ; +} diff --git a/docs/src/pages.ts b/docs/src/pages.ts index 12843eac4b7995..37f70258c1adbe 100644 --- a/docs/src/pages.ts +++ b/docs/src/pages.ts @@ -187,6 +187,7 @@ const pages: readonly MuiPage[] = [ { pathname: '/components/time-picker' }, ], }, + { pathname: '/components/masonry' }, { pathname: '/components/timeline' }, { pathname: '/components/trap-focus' }, { pathname: '/components/tree-view' }, diff --git a/docs/src/pages/components/masonry/BasicMasonry.js b/docs/src/pages/components/masonry/BasicMasonry.js new file mode 100644 index 00000000000000..803ca3b1380160 --- /dev/null +++ b/docs/src/pages/components/masonry/BasicMasonry.js @@ -0,0 +1,29 @@ +import * as React from 'react'; +import Box from '@material-ui/core/Box'; +import Masonry from '@material-ui/lab/Masonry'; +import MasonryItem from '@material-ui/lab/MasonryItem'; + +const heights = [150, 30, 90, 70, 110, 150, 130, 80, 50, 90, 100, 150, 30, 50, 80]; + +export default function BasicMasonry() { + return ( + + + {heights.map((height, index) => ( + + + {index + 1} + + + ))} + + + ); +} diff --git a/docs/src/pages/components/masonry/BasicMasonry.tsx b/docs/src/pages/components/masonry/BasicMasonry.tsx new file mode 100644 index 00000000000000..803ca3b1380160 --- /dev/null +++ b/docs/src/pages/components/masonry/BasicMasonry.tsx @@ -0,0 +1,29 @@ +import * as React from 'react'; +import Box from '@material-ui/core/Box'; +import Masonry from '@material-ui/lab/Masonry'; +import MasonryItem from '@material-ui/lab/MasonryItem'; + +const heights = [150, 30, 90, 70, 110, 150, 130, 80, 50, 90, 100, 150, 30, 50, 80]; + +export default function BasicMasonry() { + return ( + + + {heights.map((height, index) => ( + + + {index + 1} + + + ))} + + + ); +} diff --git a/docs/src/pages/components/masonry/DiffColSizeMasonry.js b/docs/src/pages/components/masonry/DiffColSizeMasonry.js new file mode 100644 index 00000000000000..de587bab4be47d --- /dev/null +++ b/docs/src/pages/components/masonry/DiffColSizeMasonry.js @@ -0,0 +1,46 @@ +/* eslint-disable @typescript-eslint/no-use-before-define */ +import * as React from 'react'; +import Box from '@material-ui/core/Box'; +import Masonry from '@material-ui/lab/Masonry'; +import MasonryItem from '@material-ui/lab/MasonryItem'; + +export default function DiffColSizeMasonry() { + return ( + + + {itemData.map((item, index) => ( + + + {index + 1} + + + ))} + + + ); +} + +const itemData = [ + { height: 150 }, + { height: 30 }, + { height: 90, span: 2 }, + { height: 110 }, + { height: 150 }, + { height: 150 }, + { height: 130, span: 2 }, + { height: 80, span: 2 }, + { height: 50 }, + { height: 90 }, + { height: 100, span: 2 }, + { height: 150 }, + { height: 50 }, + { height: 50, span: 2 }, + { height: 50 }, +]; diff --git a/docs/src/pages/components/masonry/DiffColSizeMasonry.tsx b/docs/src/pages/components/masonry/DiffColSizeMasonry.tsx new file mode 100644 index 00000000000000..de587bab4be47d --- /dev/null +++ b/docs/src/pages/components/masonry/DiffColSizeMasonry.tsx @@ -0,0 +1,46 @@ +/* eslint-disable @typescript-eslint/no-use-before-define */ +import * as React from 'react'; +import Box from '@material-ui/core/Box'; +import Masonry from '@material-ui/lab/Masonry'; +import MasonryItem from '@material-ui/lab/MasonryItem'; + +export default function DiffColSizeMasonry() { + return ( + + + {itemData.map((item, index) => ( + + + {index + 1} + + + ))} + + + ); +} + +const itemData = [ + { height: 150 }, + { height: 30 }, + { height: 90, span: 2 }, + { height: 110 }, + { height: 150 }, + { height: 150 }, + { height: 130, span: 2 }, + { height: 80, span: 2 }, + { height: 50 }, + { height: 90 }, + { height: 100, span: 2 }, + { height: 150 }, + { height: 50 }, + { height: 50, span: 2 }, + { height: 50 }, +]; diff --git a/docs/src/pages/components/masonry/DiffColSizeMasonryBroken.js b/docs/src/pages/components/masonry/DiffColSizeMasonryBroken.js new file mode 100644 index 00000000000000..628cdfb39e4f60 --- /dev/null +++ b/docs/src/pages/components/masonry/DiffColSizeMasonryBroken.js @@ -0,0 +1,46 @@ +/* eslint-disable @typescript-eslint/no-use-before-define */ +import * as React from 'react'; +import Box from '@material-ui/core/Box'; +import Masonry from '@material-ui/lab/Masonry'; +import MasonryItem from '@material-ui/lab/MasonryItem'; + +export default function DiffColSizeMasonryBroken() { + return ( + + + {itemData.map((item, index) => ( + + + {index + 1} + + + ))} + + + ); +} + +const itemData = [ + { height: 150 }, + { height: 30 }, + { height: 90, span: 2 }, + { height: 70 }, + { height: 150 }, + { height: 120 }, + { height: 100, span: 2 }, + { height: 80, span: 2 }, + { height: 35 }, + { height: 70 }, + { height: 100, span: 2 }, + { height: 157 }, + { height: 50 }, + { height: 50, span: 2 }, + { height: 50 }, +]; diff --git a/docs/src/pages/components/masonry/DiffColSizeMasonryBroken.tsx b/docs/src/pages/components/masonry/DiffColSizeMasonryBroken.tsx new file mode 100644 index 00000000000000..628cdfb39e4f60 --- /dev/null +++ b/docs/src/pages/components/masonry/DiffColSizeMasonryBroken.tsx @@ -0,0 +1,46 @@ +/* eslint-disable @typescript-eslint/no-use-before-define */ +import * as React from 'react'; +import Box from '@material-ui/core/Box'; +import Masonry from '@material-ui/lab/Masonry'; +import MasonryItem from '@material-ui/lab/MasonryItem'; + +export default function DiffColSizeMasonryBroken() { + return ( + + + {itemData.map((item, index) => ( + + + {index + 1} + + + ))} + + + ); +} + +const itemData = [ + { height: 150 }, + { height: 30 }, + { height: 90, span: 2 }, + { height: 70 }, + { height: 150 }, + { height: 120 }, + { height: 100, span: 2 }, + { height: 80, span: 2 }, + { height: 35 }, + { height: 70 }, + { height: 100, span: 2 }, + { height: 157 }, + { height: 50 }, + { height: 50, span: 2 }, + { height: 50 }, +]; diff --git a/docs/src/pages/components/masonry/FixedColumns.js b/docs/src/pages/components/masonry/FixedColumns.js new file mode 100644 index 00000000000000..4ccc5c1243ecd0 --- /dev/null +++ b/docs/src/pages/components/masonry/FixedColumns.js @@ -0,0 +1,29 @@ +import * as React from 'react'; +import Box from '@material-ui/core/Box'; +import Masonry from '@material-ui/lab/Masonry'; +import MasonryItem from '@material-ui/lab/MasonryItem'; + +const heights = [150, 30, 90, 70, 90, 100, 150, 30, 50, 80]; + +export default function FixedColumns() { + return ( + + + {heights.map((height, index) => ( + + + {index + 1} + + + ))} + + + ); +} diff --git a/docs/src/pages/components/masonry/FixedColumns.tsx b/docs/src/pages/components/masonry/FixedColumns.tsx new file mode 100644 index 00000000000000..4ccc5c1243ecd0 --- /dev/null +++ b/docs/src/pages/components/masonry/FixedColumns.tsx @@ -0,0 +1,29 @@ +import * as React from 'react'; +import Box from '@material-ui/core/Box'; +import Masonry from '@material-ui/lab/Masonry'; +import MasonryItem from '@material-ui/lab/MasonryItem'; + +const heights = [150, 30, 90, 70, 90, 100, 150, 30, 50, 80]; + +export default function FixedColumns() { + return ( + + + {heights.map((height, index) => ( + + + {index + 1} + + + ))} + + + ); +} diff --git a/docs/src/pages/components/masonry/FixedSpacing.js b/docs/src/pages/components/masonry/FixedSpacing.js new file mode 100644 index 00000000000000..58489b4c539738 --- /dev/null +++ b/docs/src/pages/components/masonry/FixedSpacing.js @@ -0,0 +1,29 @@ +import * as React from 'react'; +import Box from '@material-ui/core/Box'; +import Masonry from '@material-ui/lab/Masonry'; +import MasonryItem from '@material-ui/lab/MasonryItem'; + +const heights = [150, 30, 90, 70, 90, 100, 150, 30, 50, 80]; + +export default function FixedSpacing() { + return ( + + + {heights.map((height, index) => ( + + + {index + 1} + + + ))} + + + ); +} diff --git a/docs/src/pages/components/masonry/FixedSpacing.tsx b/docs/src/pages/components/masonry/FixedSpacing.tsx new file mode 100644 index 00000000000000..58489b4c539738 --- /dev/null +++ b/docs/src/pages/components/masonry/FixedSpacing.tsx @@ -0,0 +1,29 @@ +import * as React from 'react'; +import Box from '@material-ui/core/Box'; +import Masonry from '@material-ui/lab/Masonry'; +import MasonryItem from '@material-ui/lab/MasonryItem'; + +const heights = [150, 30, 90, 70, 90, 100, 150, 30, 50, 80]; + +export default function FixedSpacing() { + return ( + + + {heights.map((height, index) => ( + + + {index + 1} + + + ))} + + + ); +} diff --git a/docs/src/pages/components/masonry/ImageMasonry.js b/docs/src/pages/components/masonry/ImageMasonry.js new file mode 100644 index 00000000000000..ebb5eda20a923e --- /dev/null +++ b/docs/src/pages/components/masonry/ImageMasonry.js @@ -0,0 +1,95 @@ +/* eslint-disable @typescript-eslint/no-use-before-define */ +import * as React from 'react'; +import Box from '@material-ui/core/Box'; +import Masonry from '@material-ui/lab/Masonry'; +import MasonryItem from '@material-ui/lab/MasonryItem'; + +export default function ImageMasonry() { + return ( + + + {itemData.map((item) => ( + + {item.title} + + ))} + + + ); +} + +const itemData = [ + { + img: 'https://images.unsplash.com/photo-1518756131217-31eb79b20e8f', + title: 'Fern', + }, + { + img: 'https://images.unsplash.com/photo-1627308595229-7830a5c91f9f', + title: 'Snacks', + }, + { + img: 'https://images.unsplash.com/photo-1597645587822-e99fa5d45d25', + title: 'Mushrooms', + }, + { + img: 'https://images.unsplash.com/photo-1529655683826-aba9b3e77383', + title: 'Tower', + }, + { + img: 'https://images.unsplash.com/photo-1471357674240-e1a485acb3e1', + title: 'Sea star', + }, + { + img: 'https://images.unsplash.com/photo-1558642452-9d2a7deb7f62', + title: 'Honey', + }, + { + img: 'https://images.unsplash.com/photo-1516802273409-68526ee1bdd6', + title: 'Basketball', + }, + { + img: 'https://images.unsplash.com/photo-1551963831-b3b1ca40c98e', + title: 'Breakfast', + }, + { + img: 'https://images.unsplash.com/photo-1627328715728-7bcc1b5db87d', + title: 'Tree', + }, + { + img: 'https://images.unsplash.com/photo-1551782450-a2132b4ba21d', + title: 'Burger', + }, + { + img: 'https://images.unsplash.com/photo-1522770179533-24471fcdba45', + title: 'Camera', + }, + { + img: 'https://images.unsplash.com/photo-1444418776041-9c7e33cc5a9c', + title: 'Coffee', + }, + { + img: 'https://images.unsplash.com/photo-1627000086207-76eabf23aa2e', + title: 'Camping Car', + }, + { + img: 'https://images.unsplash.com/photo-1533827432537-70133748f5c8', + title: 'Hats', + }, + { + img: 'https://images.unsplash.com/photo-1567306301408-9b74779a11af', + title: 'Tomato basil', + }, + { + img: 'https://images.unsplash.com/photo-1627328561499-a3584d4ee4f7', + title: 'Mountain', + }, + { + img: 'https://images.unsplash.com/photo-1589118949245-7d38baf380d6', + title: 'Bike', + }, +]; diff --git a/docs/src/pages/components/masonry/ImageMasonry.tsx b/docs/src/pages/components/masonry/ImageMasonry.tsx new file mode 100644 index 00000000000000..ebb5eda20a923e --- /dev/null +++ b/docs/src/pages/components/masonry/ImageMasonry.tsx @@ -0,0 +1,95 @@ +/* eslint-disable @typescript-eslint/no-use-before-define */ +import * as React from 'react'; +import Box from '@material-ui/core/Box'; +import Masonry from '@material-ui/lab/Masonry'; +import MasonryItem from '@material-ui/lab/MasonryItem'; + +export default function ImageMasonry() { + return ( + + + {itemData.map((item) => ( + + {item.title} + + ))} + + + ); +} + +const itemData = [ + { + img: 'https://images.unsplash.com/photo-1518756131217-31eb79b20e8f', + title: 'Fern', + }, + { + img: 'https://images.unsplash.com/photo-1627308595229-7830a5c91f9f', + title: 'Snacks', + }, + { + img: 'https://images.unsplash.com/photo-1597645587822-e99fa5d45d25', + title: 'Mushrooms', + }, + { + img: 'https://images.unsplash.com/photo-1529655683826-aba9b3e77383', + title: 'Tower', + }, + { + img: 'https://images.unsplash.com/photo-1471357674240-e1a485acb3e1', + title: 'Sea star', + }, + { + img: 'https://images.unsplash.com/photo-1558642452-9d2a7deb7f62', + title: 'Honey', + }, + { + img: 'https://images.unsplash.com/photo-1516802273409-68526ee1bdd6', + title: 'Basketball', + }, + { + img: 'https://images.unsplash.com/photo-1551963831-b3b1ca40c98e', + title: 'Breakfast', + }, + { + img: 'https://images.unsplash.com/photo-1627328715728-7bcc1b5db87d', + title: 'Tree', + }, + { + img: 'https://images.unsplash.com/photo-1551782450-a2132b4ba21d', + title: 'Burger', + }, + { + img: 'https://images.unsplash.com/photo-1522770179533-24471fcdba45', + title: 'Camera', + }, + { + img: 'https://images.unsplash.com/photo-1444418776041-9c7e33cc5a9c', + title: 'Coffee', + }, + { + img: 'https://images.unsplash.com/photo-1627000086207-76eabf23aa2e', + title: 'Camping Car', + }, + { + img: 'https://images.unsplash.com/photo-1533827432537-70133748f5c8', + title: 'Hats', + }, + { + img: 'https://images.unsplash.com/photo-1567306301408-9b74779a11af', + title: 'Tomato basil', + }, + { + img: 'https://images.unsplash.com/photo-1627328561499-a3584d4ee4f7', + title: 'Mountain', + }, + { + img: 'https://images.unsplash.com/photo-1589118949245-7d38baf380d6', + title: 'Bike', + }, +]; diff --git a/docs/src/pages/components/masonry/ResponsiveColumns.js b/docs/src/pages/components/masonry/ResponsiveColumns.js new file mode 100644 index 00000000000000..b920086df750be --- /dev/null +++ b/docs/src/pages/components/masonry/ResponsiveColumns.js @@ -0,0 +1,29 @@ +import * as React from 'react'; +import Box from '@material-ui/core/Box'; +import Masonry from '@material-ui/lab/Masonry'; +import MasonryItem from '@material-ui/lab/MasonryItem'; + +const heights = [150, 30, 90, 70, 90, 100, 150, 30, 50, 80]; + +export default function ResponsiveColumns() { + return ( + + + {heights.map((height, index) => ( + + + {index + 1} + + + ))} + + + ); +} diff --git a/docs/src/pages/components/masonry/ResponsiveColumns.tsx b/docs/src/pages/components/masonry/ResponsiveColumns.tsx new file mode 100644 index 00000000000000..b920086df750be --- /dev/null +++ b/docs/src/pages/components/masonry/ResponsiveColumns.tsx @@ -0,0 +1,29 @@ +import * as React from 'react'; +import Box from '@material-ui/core/Box'; +import Masonry from '@material-ui/lab/Masonry'; +import MasonryItem from '@material-ui/lab/MasonryItem'; + +const heights = [150, 30, 90, 70, 90, 100, 150, 30, 50, 80]; + +export default function ResponsiveColumns() { + return ( + + + {heights.map((height, index) => ( + + + {index + 1} + + + ))} + + + ); +} diff --git a/docs/src/pages/components/masonry/ResponsiveSpacing.js b/docs/src/pages/components/masonry/ResponsiveSpacing.js new file mode 100644 index 00000000000000..3e65472251b86d --- /dev/null +++ b/docs/src/pages/components/masonry/ResponsiveSpacing.js @@ -0,0 +1,29 @@ +import * as React from 'react'; +import Box from '@material-ui/core/Box'; +import Masonry from '@material-ui/lab/Masonry'; +import MasonryItem from '@material-ui/lab/MasonryItem'; + +const heights = [150, 30, 90, 70, 90, 100, 150, 30, 50, 80]; + +export default function ResponsiveSpacing() { + return ( + + + {heights.map((height, index) => ( + + + {index + 1} + + + ))} + + + ); +} diff --git a/docs/src/pages/components/masonry/ResponsiveSpacing.tsx b/docs/src/pages/components/masonry/ResponsiveSpacing.tsx new file mode 100644 index 00000000000000..3e65472251b86d --- /dev/null +++ b/docs/src/pages/components/masonry/ResponsiveSpacing.tsx @@ -0,0 +1,29 @@ +import * as React from 'react'; +import Box from '@material-ui/core/Box'; +import Masonry from '@material-ui/lab/Masonry'; +import MasonryItem from '@material-ui/lab/MasonryItem'; + +const heights = [150, 30, 90, 70, 90, 100, 150, 30, 50, 80]; + +export default function ResponsiveSpacing() { + return ( + + + {heights.map((height, index) => ( + + + {index + 1} + + + ))} + + + ); +} diff --git a/docs/src/pages/components/masonry/SSRMasonry.js b/docs/src/pages/components/masonry/SSRMasonry.js new file mode 100644 index 00000000000000..32682ed8484f7d --- /dev/null +++ b/docs/src/pages/components/masonry/SSRMasonry.js @@ -0,0 +1,28 @@ +import * as React from 'react'; +import Box from '@material-ui/core/Box'; +import Masonry from '@material-ui/lab/Masonry'; +import MasonryItem from '@material-ui/lab/MasonryItem'; + +const heights = [150, 30, 90, 70, 90, 100, 150, 30, 50, 80]; + +export default function SSRMasonry() { + return ( + + + {heights.map((height, index) => ( + + + {index + 1} + + + ))} + + + ); +} diff --git a/docs/src/pages/components/masonry/SSRMasonry.tsx b/docs/src/pages/components/masonry/SSRMasonry.tsx new file mode 100644 index 00000000000000..32682ed8484f7d --- /dev/null +++ b/docs/src/pages/components/masonry/SSRMasonry.tsx @@ -0,0 +1,28 @@ +import * as React from 'react'; +import Box from '@material-ui/core/Box'; +import Masonry from '@material-ui/lab/Masonry'; +import MasonryItem from '@material-ui/lab/MasonryItem'; + +const heights = [150, 30, 90, 70, 90, 100, 150, 30, 50, 80]; + +export default function SSRMasonry() { + return ( + + + {heights.map((height, index) => ( + + + {index + 1} + + + ))} + + + ); +} diff --git a/docs/src/pages/components/masonry/masonry.md b/docs/src/pages/components/masonry/masonry.md new file mode 100644 index 00000000000000..5be1e7de3e42cd --- /dev/null +++ b/docs/src/pages/components/masonry/masonry.md @@ -0,0 +1,70 @@ +--- +title: React Masonry component +components: Masonry, MasonryItem +githubLabel: 'component: Masonry' +--- + +# Masonry + +

Masonry lays out contents of different sizes as blocks of the same width and variable height with configurable gaps.

+ +Masonry maintains a list of content blocks with a consistent width but variable height. +The contents are ordered by row. +If a row is already filled with the specified number of columns, the next item starts another row, and it is added to the shortest column. + +{{"component": "modules/components/ComponentLinkHeader.js", "design": true}} + +> Warning: This component has been developed with the use of CSS Grid Level 2. Unfortunately, Chrome only allows to render at most 1,000 rows for each grid. +> Hence, with the current design, a masonry component has a maximum height of 2,000px, and the items beyond this height will fail to be rendered. +> An [issue](https://github.com/mui-org/material-ui/issues/27934) has been created on GitHub to gather workarounds for this limitation. It is worth noting that this limitation does not exist on Firefox or Safari. + +## Basic masonry + +A simple example of a ``. `` is a container for one or more ``s. `` can receive any element including `
` and ``. Also, it is important to note that each `` accepts only one element. + +{{"demo": "pages/components/masonry/BasicMasonry.js", "bg": true}} + +## Image masonry + +This example demonstrates the use of `` for images. `` orders its children by row. +If you would like to order images by column, you can use ``. More details on this component can be found in [Masonry Image List](/components/image-list/#masonry-image-list). + +{{"demo": "pages/components/masonry/ImageMasonry.js", "bg": true}} + +## Columns + +This example demonstrates the use of the `columns` to configure the number of columns of a ``. + +{{"demo": "pages/components/masonry/FixedColumns.js", "bg": true}} + +`columns` accepts responsive values: + +{{"demo": "pages/components/masonry/ResponsiveColumns.js", "bg": true}} + +## Spacing + +This example demonstrates the use of the `spacing` to configure the spacing between ``s. +It is important to note that `spacing` is a factor of the theme's spacing. + +{{"demo": "pages/components/masonry/FixedSpacing.js", "bg": true}} + +`spacing` accepts responsive values: + +{{"demo": "pages/components/masonry/ResponsiveSpacing.js", "bg": true}} + +## Column spanning + +This example demonstrates the use of the `columnSpan` to configure the number of columns taken up by each ``. + +{{"demo": "pages/components/masonry/DiffColSizeMasonry.js", "bg": true}} + +However, you have to choose the value of `columnSpan` for each item carefully or fine-tune heights of items so that your masonry does not break. + +{{"demo": "pages/components/masonry/DiffColSizeMasonryBroken.js", "bg": true}} + +## Server-side rendering + +This example demonstrates the use of the `defaultHeight` to configure a fixed height of each ``. This is used for server-side rendering. +By default, `height: 100%` will be set to the content of ``. If you change this, there can be unwanted gap between `` and the content that you pass to it. + +{{"demo": "pages/components/masonry/SSRMasonry.js", "bg": true}} diff --git a/docs/src/pagesApi.js b/docs/src/pagesApi.js index 3fa8b63a0ca4db..8f4cd760d371c2 100644 --- a/docs/src/pagesApi.js +++ b/docs/src/pagesApi.js @@ -83,6 +83,8 @@ module.exports = [ { pathname: '/api-docs/list-item-text' }, { pathname: '/api-docs/list-subheader' }, { pathname: '/api-docs/loading-button' }, + { pathname: '/api-docs/masonry' }, + { pathname: '/api-docs/masonry-item' }, { pathname: '/api-docs/menu' }, { pathname: '/api-docs/menu-item' }, { pathname: '/api-docs/menu-list' }, diff --git a/docs/translations/api-docs/masonry-item/masonry-item-de.json b/docs/translations/api-docs/masonry-item/masonry-item-de.json new file mode 100644 index 00000000000000..535abe02463228 --- /dev/null +++ b/docs/translations/api-docs/masonry-item/masonry-item-de.json @@ -0,0 +1,12 @@ +{ + "componentDescription": "", + "propDescriptions": { + "children": "The content of the component, normally an <img /> or a <div />. It should be only one element.", + "classes": "Override or extend the styles applied to the component. See CSS API below for more details.", + "columnSpan": "The number of columns taken up by the component", + "component": "The component used for the root node. Either a string to use a HTML element or a component.", + "defaultHeight": "The initial height of the component in px. This is provided for server-side rendering.", + "sx": "Allows defining system overrides as well as additional CSS styles. See the `sx` page for more details." + }, + "classDescriptions": { "root": { "description": "Styles applied to the root element." } } +} diff --git a/docs/translations/api-docs/masonry-item/masonry-item-es.json b/docs/translations/api-docs/masonry-item/masonry-item-es.json new file mode 100644 index 00000000000000..535abe02463228 --- /dev/null +++ b/docs/translations/api-docs/masonry-item/masonry-item-es.json @@ -0,0 +1,12 @@ +{ + "componentDescription": "", + "propDescriptions": { + "children": "The content of the component, normally an <img /> or a <div />. It should be only one element.", + "classes": "Override or extend the styles applied to the component. See CSS API below for more details.", + "columnSpan": "The number of columns taken up by the component", + "component": "The component used for the root node. Either a string to use a HTML element or a component.", + "defaultHeight": "The initial height of the component in px. This is provided for server-side rendering.", + "sx": "Allows defining system overrides as well as additional CSS styles. See the `sx` page for more details." + }, + "classDescriptions": { "root": { "description": "Styles applied to the root element." } } +} diff --git a/docs/translations/api-docs/masonry-item/masonry-item-fr.json b/docs/translations/api-docs/masonry-item/masonry-item-fr.json new file mode 100644 index 00000000000000..535abe02463228 --- /dev/null +++ b/docs/translations/api-docs/masonry-item/masonry-item-fr.json @@ -0,0 +1,12 @@ +{ + "componentDescription": "", + "propDescriptions": { + "children": "The content of the component, normally an <img /> or a <div />. It should be only one element.", + "classes": "Override or extend the styles applied to the component. See CSS API below for more details.", + "columnSpan": "The number of columns taken up by the component", + "component": "The component used for the root node. Either a string to use a HTML element or a component.", + "defaultHeight": "The initial height of the component in px. This is provided for server-side rendering.", + "sx": "Allows defining system overrides as well as additional CSS styles. See the `sx` page for more details." + }, + "classDescriptions": { "root": { "description": "Styles applied to the root element." } } +} diff --git a/docs/translations/api-docs/masonry-item/masonry-item-ja.json b/docs/translations/api-docs/masonry-item/masonry-item-ja.json new file mode 100644 index 00000000000000..535abe02463228 --- /dev/null +++ b/docs/translations/api-docs/masonry-item/masonry-item-ja.json @@ -0,0 +1,12 @@ +{ + "componentDescription": "", + "propDescriptions": { + "children": "The content of the component, normally an <img /> or a <div />. It should be only one element.", + "classes": "Override or extend the styles applied to the component. See CSS API below for more details.", + "columnSpan": "The number of columns taken up by the component", + "component": "The component used for the root node. Either a string to use a HTML element or a component.", + "defaultHeight": "The initial height of the component in px. This is provided for server-side rendering.", + "sx": "Allows defining system overrides as well as additional CSS styles. See the `sx` page for more details." + }, + "classDescriptions": { "root": { "description": "Styles applied to the root element." } } +} diff --git a/docs/translations/api-docs/masonry-item/masonry-item-pt.json b/docs/translations/api-docs/masonry-item/masonry-item-pt.json new file mode 100644 index 00000000000000..535abe02463228 --- /dev/null +++ b/docs/translations/api-docs/masonry-item/masonry-item-pt.json @@ -0,0 +1,12 @@ +{ + "componentDescription": "", + "propDescriptions": { + "children": "The content of the component, normally an <img /> or a <div />. It should be only one element.", + "classes": "Override or extend the styles applied to the component. See CSS API below for more details.", + "columnSpan": "The number of columns taken up by the component", + "component": "The component used for the root node. Either a string to use a HTML element or a component.", + "defaultHeight": "The initial height of the component in px. This is provided for server-side rendering.", + "sx": "Allows defining system overrides as well as additional CSS styles. See the `sx` page for more details." + }, + "classDescriptions": { "root": { "description": "Styles applied to the root element." } } +} diff --git a/docs/translations/api-docs/masonry-item/masonry-item-ru.json b/docs/translations/api-docs/masonry-item/masonry-item-ru.json new file mode 100644 index 00000000000000..535abe02463228 --- /dev/null +++ b/docs/translations/api-docs/masonry-item/masonry-item-ru.json @@ -0,0 +1,12 @@ +{ + "componentDescription": "", + "propDescriptions": { + "children": "The content of the component, normally an <img /> or a <div />. It should be only one element.", + "classes": "Override or extend the styles applied to the component. See CSS API below for more details.", + "columnSpan": "The number of columns taken up by the component", + "component": "The component used for the root node. Either a string to use a HTML element or a component.", + "defaultHeight": "The initial height of the component in px. This is provided for server-side rendering.", + "sx": "Allows defining system overrides as well as additional CSS styles. See the `sx` page for more details." + }, + "classDescriptions": { "root": { "description": "Styles applied to the root element." } } +} diff --git a/docs/translations/api-docs/masonry-item/masonry-item-zh.json b/docs/translations/api-docs/masonry-item/masonry-item-zh.json new file mode 100644 index 00000000000000..535abe02463228 --- /dev/null +++ b/docs/translations/api-docs/masonry-item/masonry-item-zh.json @@ -0,0 +1,12 @@ +{ + "componentDescription": "", + "propDescriptions": { + "children": "The content of the component, normally an <img /> or a <div />. It should be only one element.", + "classes": "Override or extend the styles applied to the component. See CSS API below for more details.", + "columnSpan": "The number of columns taken up by the component", + "component": "The component used for the root node. Either a string to use a HTML element or a component.", + "defaultHeight": "The initial height of the component in px. This is provided for server-side rendering.", + "sx": "Allows defining system overrides as well as additional CSS styles. See the `sx` page for more details." + }, + "classDescriptions": { "root": { "description": "Styles applied to the root element." } } +} diff --git a/docs/translations/api-docs/masonry-item/masonry-item.json b/docs/translations/api-docs/masonry-item/masonry-item.json new file mode 100644 index 00000000000000..535abe02463228 --- /dev/null +++ b/docs/translations/api-docs/masonry-item/masonry-item.json @@ -0,0 +1,12 @@ +{ + "componentDescription": "", + "propDescriptions": { + "children": "The content of the component, normally an <img /> or a <div />. It should be only one element.", + "classes": "Override or extend the styles applied to the component. See CSS API below for more details.", + "columnSpan": "The number of columns taken up by the component", + "component": "The component used for the root node. Either a string to use a HTML element or a component.", + "defaultHeight": "The initial height of the component in px. This is provided for server-side rendering.", + "sx": "Allows defining system overrides as well as additional CSS styles. See the `sx` page for more details." + }, + "classDescriptions": { "root": { "description": "Styles applied to the root element." } } +} diff --git a/docs/translations/api-docs/masonry/masonry-de.json b/docs/translations/api-docs/masonry/masonry-de.json new file mode 100644 index 00000000000000..548f75a86fdaa2 --- /dev/null +++ b/docs/translations/api-docs/masonry/masonry-de.json @@ -0,0 +1,12 @@ +{ + "componentDescription": "", + "propDescriptions": { + "children": "The content of the component. It's recommended to be <MasonryItem />s.", + "classes": "Override or extend the styles applied to the component. See CSS API below for more details.", + "columns": "Number of columns.", + "component": "The component used for the root node. Either a string to use a HTML element or a component.", + "spacing": "Defines the space between children. It is a factor of the theme's spacing.", + "sx": "Allows defining system overrides as well as additional CSS styles. See the `sx` page for more details." + }, + "classDescriptions": { "root": { "description": "Styles applied to the root element." } } +} diff --git a/docs/translations/api-docs/masonry/masonry-es.json b/docs/translations/api-docs/masonry/masonry-es.json new file mode 100644 index 00000000000000..548f75a86fdaa2 --- /dev/null +++ b/docs/translations/api-docs/masonry/masonry-es.json @@ -0,0 +1,12 @@ +{ + "componentDescription": "", + "propDescriptions": { + "children": "The content of the component. It's recommended to be <MasonryItem />s.", + "classes": "Override or extend the styles applied to the component. See CSS API below for more details.", + "columns": "Number of columns.", + "component": "The component used for the root node. Either a string to use a HTML element or a component.", + "spacing": "Defines the space between children. It is a factor of the theme's spacing.", + "sx": "Allows defining system overrides as well as additional CSS styles. See the `sx` page for more details." + }, + "classDescriptions": { "root": { "description": "Styles applied to the root element." } } +} diff --git a/docs/translations/api-docs/masonry/masonry-fr.json b/docs/translations/api-docs/masonry/masonry-fr.json new file mode 100644 index 00000000000000..548f75a86fdaa2 --- /dev/null +++ b/docs/translations/api-docs/masonry/masonry-fr.json @@ -0,0 +1,12 @@ +{ + "componentDescription": "", + "propDescriptions": { + "children": "The content of the component. It's recommended to be <MasonryItem />s.", + "classes": "Override or extend the styles applied to the component. See CSS API below for more details.", + "columns": "Number of columns.", + "component": "The component used for the root node. Either a string to use a HTML element or a component.", + "spacing": "Defines the space between children. It is a factor of the theme's spacing.", + "sx": "Allows defining system overrides as well as additional CSS styles. See the `sx` page for more details." + }, + "classDescriptions": { "root": { "description": "Styles applied to the root element." } } +} diff --git a/docs/translations/api-docs/masonry/masonry-ja.json b/docs/translations/api-docs/masonry/masonry-ja.json new file mode 100644 index 00000000000000..548f75a86fdaa2 --- /dev/null +++ b/docs/translations/api-docs/masonry/masonry-ja.json @@ -0,0 +1,12 @@ +{ + "componentDescription": "", + "propDescriptions": { + "children": "The content of the component. It's recommended to be <MasonryItem />s.", + "classes": "Override or extend the styles applied to the component. See CSS API below for more details.", + "columns": "Number of columns.", + "component": "The component used for the root node. Either a string to use a HTML element or a component.", + "spacing": "Defines the space between children. It is a factor of the theme's spacing.", + "sx": "Allows defining system overrides as well as additional CSS styles. See the `sx` page for more details." + }, + "classDescriptions": { "root": { "description": "Styles applied to the root element." } } +} diff --git a/docs/translations/api-docs/masonry/masonry-pt.json b/docs/translations/api-docs/masonry/masonry-pt.json new file mode 100644 index 00000000000000..548f75a86fdaa2 --- /dev/null +++ b/docs/translations/api-docs/masonry/masonry-pt.json @@ -0,0 +1,12 @@ +{ + "componentDescription": "", + "propDescriptions": { + "children": "The content of the component. It's recommended to be <MasonryItem />s.", + "classes": "Override or extend the styles applied to the component. See CSS API below for more details.", + "columns": "Number of columns.", + "component": "The component used for the root node. Either a string to use a HTML element or a component.", + "spacing": "Defines the space between children. It is a factor of the theme's spacing.", + "sx": "Allows defining system overrides as well as additional CSS styles. See the `sx` page for more details." + }, + "classDescriptions": { "root": { "description": "Styles applied to the root element." } } +} diff --git a/docs/translations/api-docs/masonry/masonry-ru.json b/docs/translations/api-docs/masonry/masonry-ru.json new file mode 100644 index 00000000000000..548f75a86fdaa2 --- /dev/null +++ b/docs/translations/api-docs/masonry/masonry-ru.json @@ -0,0 +1,12 @@ +{ + "componentDescription": "", + "propDescriptions": { + "children": "The content of the component. It's recommended to be <MasonryItem />s.", + "classes": "Override or extend the styles applied to the component. See CSS API below for more details.", + "columns": "Number of columns.", + "component": "The component used for the root node. Either a string to use a HTML element or a component.", + "spacing": "Defines the space between children. It is a factor of the theme's spacing.", + "sx": "Allows defining system overrides as well as additional CSS styles. See the `sx` page for more details." + }, + "classDescriptions": { "root": { "description": "Styles applied to the root element." } } +} diff --git a/docs/translations/api-docs/masonry/masonry-zh.json b/docs/translations/api-docs/masonry/masonry-zh.json new file mode 100644 index 00000000000000..548f75a86fdaa2 --- /dev/null +++ b/docs/translations/api-docs/masonry/masonry-zh.json @@ -0,0 +1,12 @@ +{ + "componentDescription": "", + "propDescriptions": { + "children": "The content of the component. It's recommended to be <MasonryItem />s.", + "classes": "Override or extend the styles applied to the component. See CSS API below for more details.", + "columns": "Number of columns.", + "component": "The component used for the root node. Either a string to use a HTML element or a component.", + "spacing": "Defines the space between children. It is a factor of the theme's spacing.", + "sx": "Allows defining system overrides as well as additional CSS styles. See the `sx` page for more details." + }, + "classDescriptions": { "root": { "description": "Styles applied to the root element." } } +} diff --git a/docs/translations/api-docs/masonry/masonry.json b/docs/translations/api-docs/masonry/masonry.json new file mode 100644 index 00000000000000..548f75a86fdaa2 --- /dev/null +++ b/docs/translations/api-docs/masonry/masonry.json @@ -0,0 +1,12 @@ +{ + "componentDescription": "", + "propDescriptions": { + "children": "The content of the component. It's recommended to be <MasonryItem />s.", + "classes": "Override or extend the styles applied to the component. See CSS API below for more details.", + "columns": "Number of columns.", + "component": "The component used for the root node. Either a string to use a HTML element or a component.", + "spacing": "Defines the space between children. It is a factor of the theme's spacing.", + "sx": "Allows defining system overrides as well as additional CSS styles. See the `sx` page for more details." + }, + "classDescriptions": { "root": { "description": "Styles applied to the root element." } } +} diff --git a/docs/translations/translations.json b/docs/translations/translations.json index 840063aa745e2a..7b5ba99ae91880 100644 --- a/docs/translations/translations.json +++ b/docs/translations/translations.json @@ -260,6 +260,7 @@ "/components/date-range-picker": "Date Range Picker ⚡️", "/components/date-time-picker": "Date Time Picker", "/components/time-picker": "Time Picker", + "/components/masonry": "Masonry", "/components/timeline": "Timeline", "/components/trap-focus": "Trap Focus", "/components/tree-view": "Tree View", diff --git a/packages/material-ui-lab/src/Masonry/Masonry.d.ts b/packages/material-ui-lab/src/Masonry/Masonry.d.ts new file mode 100644 index 00000000000000..90a5f588fa6956 --- /dev/null +++ b/packages/material-ui-lab/src/Masonry/Masonry.d.ts @@ -0,0 +1,50 @@ +import { ResponsiveStyleValue, SxProps } from '@material-ui/system'; +import { OverridableComponent, OverrideProps } from '@material-ui/core/OverridableComponent'; +import { Theme } from '@material-ui/core/styles'; +import { MasonryClasses } from './masonryClasses'; + +export interface MasonryTypeMap

{ + props: P & { + /** + * The content of the component. It's recommended to be ``s. + */ + children: NonNullable; + /** + * Override or extend the styles applied to the component. + */ + classes?: Partial; + /** + * Number of columns. + * @default 4 + */ + columns?: ResponsiveStyleValue; + /** + * Defines the space between children. It is a factor of the theme's spacing. + * @default 1 + */ + spacing?: ResponsiveStyleValue; + /** + * Allows defining system overrides as well as additional CSS styles. + */ + sx?: SxProps; + }; + defaultComponent: D; +} +/** + * + * Demos: + * + * - [Masonry](https://material-ui.com/components/masonry/) + * + * API: + * + * - [Masonry API](https://material-ui.com/api/masonry/) + */ +declare const Masonry: OverridableComponent; + +export type MasonryProps< + D extends React.ElementType = MasonryTypeMap['defaultComponent'], + P = {}, +> = OverrideProps, D>; + +export default Masonry; diff --git a/packages/material-ui-lab/src/Masonry/Masonry.js b/packages/material-ui-lab/src/Masonry/Masonry.js new file mode 100644 index 00000000000000..656aa2ffb3b63d --- /dev/null +++ b/packages/material-ui-lab/src/Masonry/Masonry.js @@ -0,0 +1,177 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import clsx from 'clsx'; +import { + createUnarySpacing, + getValue, + handleBreakpoints, + unstable_resolveBreakpointValues as resolveBreakpointValues, +} from '@material-ui/system'; +import { deepmerge, unstable_useForkRef as useForkRef } from '@material-ui/utils'; +import { unstable_composeClasses as composeClasses } from '@material-ui/unstyled'; +import { styled, useThemeProps } from '@material-ui/core/styles'; +import { getMasonryUtilityClass } from './masonryClasses'; +import MasonryContext from './MasonryContext'; + +const useUtilityClasses = (ownerState) => { + const { classes } = ownerState; + + const slots = { + root: ['root'], + }; + + return composeClasses(slots, getMasonryUtilityClass, classes); +}; + +export const style = ({ ownerState, theme }) => { + let styles = { + display: 'grid', + gridAutoRows: 0, + padding: 0, + overflow: 'auto', + width: '100%', + rowGap: 2, + boxSizing: 'border-box', + }; + + const base = {}; + Object.keys(theme.breakpoints.values).forEach((breakpoint) => { + if (ownerState.spacing[breakpoint] != null) { + base[breakpoint] = true; + } + }); + + const spacingValues = resolveBreakpointValues({ values: ownerState.spacing, base }); + const transformer = createUnarySpacing(theme); + const spacingStyleFromPropValue = (propValue) => { + return { + columnGap: getValue(transformer, propValue), + }; + }; + + styles = { + ...styles, + ...handleBreakpoints({ theme }, spacingValues, spacingStyleFromPropValue), + }; + + const columnValues = resolveBreakpointValues({ values: ownerState.columns, base }); + const columnStyleFromPropValue = (propValue) => { + return { + gridTemplateColumns: `repeat(${propValue}, 1fr)`, + }; + }; + + styles = deepmerge(styles, handleBreakpoints({ theme }, columnValues, columnStyleFromPropValue)); + + return styles; +}; + +const MasonryRoot = styled('div', { + name: 'MuiMasonry', + slot: 'Root', + overridesResolver: (props, styles) => { + return [styles.root]; + }, +})(style); + +const Masonry = React.forwardRef(function Masonry(inProps, ref) { + const props = useThemeProps({ + props: inProps, + name: 'MuiMasonry', + }); + + const masonryRef = React.useRef(); + const { children, className, component = 'div', columns = 4, spacing = 1, ...other } = props; + const ownerState = { ...props, spacing, columns }; + const classes = useUtilityClasses(ownerState); + + const contextValue = React.useMemo(() => ({ spacing }), [spacing]); + let didWarn = false; + React.useEffect(() => { + // scroller always appears when masonry's height goes beyond 2,000px on Chrome + const handleScroll = () => { + if (masonryRef.current.clientHeight === 1998 && !didWarn) { + console.warn( + [ + 'Material-UI: The Masonry can have the maximum height of 2,000px on Chrome browser.', + 'Items that go beyond this height fail to be rendered on Chrome browser.', + 'You can find more in this open issue: https://github.com/mui-org/material-ui/issues/27934', + ].join('\n'), + ); + + // eslint-disable-next-line react-hooks/exhaustive-deps + didWarn = true; + } + }; + const container = masonryRef.current; + container.addEventListener('scroll', handleScroll); + return () => { + container.removeEventListener('scroll', handleScroll); + }; + }, []); + + const handleRef = useForkRef(ref, masonryRef); + return ( + + + {children} + + + ); +}); + +Masonry.propTypes /* remove-proptypes */ = { + // ----------------------------- Warning -------------------------------- + // | These PropTypes are generated from the TypeScript type definitions | + // | To update them edit the d.ts file and run "yarn proptypes" | + // ---------------------------------------------------------------------- + /** + * The content of the component. It's recommended to be ``s. + */ + children: PropTypes /* @typescript-to-proptypes-ignore */.node.isRequired, + /** + * Override or extend the styles applied to the component. + */ + classes: PropTypes.object, + /** + * @ignore + */ + className: PropTypes.string, + /** + * Number of columns. + * @default 4 + */ + columns: PropTypes.oneOfType([ + PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.number, PropTypes.string])), + PropTypes.number, + PropTypes.object, + PropTypes.string, + ]), + /** + * The component used for the root node. + * Either a string to use a HTML element or a component. + */ + component: PropTypes.elementType, + /** + * Defines the space between children. It is a factor of the theme's spacing. + * @default 1 + */ + spacing: PropTypes.oneOfType([ + PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.number, PropTypes.string])), + PropTypes.number, + PropTypes.object, + PropTypes.string, + ]), + /** + * Allows defining system overrides as well as additional CSS styles. + */ + sx: PropTypes.object, +}; + +export default Masonry; diff --git a/packages/material-ui-lab/src/Masonry/Masonry.test.js b/packages/material-ui-lab/src/Masonry/Masonry.test.js new file mode 100644 index 00000000000000..48d1ab18104dab --- /dev/null +++ b/packages/material-ui-lab/src/Masonry/Masonry.test.js @@ -0,0 +1,114 @@ +import { expect } from 'chai'; +import * as React from 'react'; +import { createClientRender, describeConformance } from 'test/utils'; +import Masonry, { masonryClasses as classes } from '@material-ui/lab/Masonry'; +import { createTheme } from '@material-ui/core/styles'; +import defaultTheme from '@material-ui/core/styles/defaultTheme'; +import { style } from './Masonry'; + +describe('', () => { + const render = createClientRender(); + + describeConformance( + +

+ , + () => ({ + classes, + inheritComponent: 'div', + render, + refInstanceof: window.HTMLDivElement, + testComponentPropWith: 'span', + muiName: 'MuiMasonry', + skip: ['componentsProp', 'themeVariants'], + }), + ); + + const theme = createTheme({ spacing: 8 }); + + describe('style attribute:', () => { + it('should render with correct default styles', () => { + expect( + style({ + ownerState: { + columns: 4, + spacing: 1, + }, + theme, + }), + ).to.deep.equal({ + display: 'grid', + gridAutoRows: 0, + padding: 0, + overflow: 'auto', + width: '100%', + rowGap: 2, + columnGap: theme.spacing(1), + gridTemplateColumns: 'repeat(4, 1fr)', + boxSizing: 'border-box', + }); + }); + + it('should render with column gap responsive to breakpoints', () => { + expect( + style({ + ownerState: { + columns: 4, + spacing: { xs: 1, sm: 2, md: 3 }, + }, + theme, + }), + ).to.deep.equal({ + '@media (min-width:0px)': { + columnGap: theme.spacing(1), + gridTemplateColumns: 'repeat(4, 1fr)', + }, + [`@media (min-width:${defaultTheme.breakpoints.values.sm}px)`]: { + columnGap: theme.spacing(2), + gridTemplateColumns: 'repeat(4, 1fr)', + }, + [`@media (min-width:${defaultTheme.breakpoints.values.md}px)`]: { + columnGap: theme.spacing(3), + gridTemplateColumns: 'repeat(4, 1fr)', + }, + display: 'grid', + gridAutoRows: 0, + padding: 0, + overflow: 'auto', + width: '100%', + rowGap: 2, + boxSizing: 'border-box', + }); + }); + + it('should render with grid-template-columns responsive to breakpoints', () => { + expect( + style({ + ownerState: { + columns: { xs: 3, sm: 5, md: 7 }, + spacing: 1, + }, + theme, + }), + ).to.deep.equal({ + '@media (min-width:0px)': { + gridTemplateColumns: 'repeat(3, 1fr)', + }, + [`@media (min-width:${defaultTheme.breakpoints.values.sm}px)`]: { + gridTemplateColumns: 'repeat(5, 1fr)', + }, + [`@media (min-width:${defaultTheme.breakpoints.values.md}px)`]: { + gridTemplateColumns: 'repeat(7, 1fr)', + }, + display: 'grid', + gridAutoRows: 0, + padding: 0, + overflow: 'auto', + width: '100%', + columnGap: theme.spacing(1), + rowGap: 2, + boxSizing: 'border-box', + }); + }); + }); +}); diff --git a/packages/material-ui-lab/src/Masonry/MasonryContext.js b/packages/material-ui-lab/src/Masonry/MasonryContext.js new file mode 100644 index 00000000000000..54f687f5a0d81f --- /dev/null +++ b/packages/material-ui-lab/src/Masonry/MasonryContext.js @@ -0,0 +1,12 @@ +import * as React from 'react'; + +/** + * @ignore - internal component. + */ +const MasonryContext = React.createContext({}); + +if (process.env.NODE_ENV !== 'production') { + MasonryContext.displayName = 'MasonryContext'; +} + +export default MasonryContext; diff --git a/packages/material-ui-lab/src/Masonry/index.d.ts b/packages/material-ui-lab/src/Masonry/index.d.ts new file mode 100644 index 00000000000000..f5e1cb096abdf2 --- /dev/null +++ b/packages/material-ui-lab/src/Masonry/index.d.ts @@ -0,0 +1,5 @@ +export * from './Masonry'; +export { default } from './Masonry'; + +export * from './masonryClasses'; +export { default as masonryClasses } from './masonryClasses'; diff --git a/packages/material-ui-lab/src/Masonry/index.js b/packages/material-ui-lab/src/Masonry/index.js new file mode 100644 index 00000000000000..055003e228425e --- /dev/null +++ b/packages/material-ui-lab/src/Masonry/index.js @@ -0,0 +1,4 @@ +export { default } from './Masonry'; + +export * from './masonryClasses'; +export { default as masonryClasses } from './masonryClasses'; diff --git a/packages/material-ui-lab/src/Masonry/masonryClasses.ts b/packages/material-ui-lab/src/Masonry/masonryClasses.ts new file mode 100644 index 00000000000000..bcef28f0b67017 --- /dev/null +++ b/packages/material-ui-lab/src/Masonry/masonryClasses.ts @@ -0,0 +1,16 @@ +import { generateUtilityClass, generateUtilityClasses } from '@material-ui/unstyled'; + +export interface MasonryClasses { + /** Styles applied to the root element. */ + root: string; +} + +export type MasonryClassKey = keyof MasonryClasses; + +export function getMasonryUtilityClass(slot: string): string { + return generateUtilityClass('MuiMasonry', slot); +} + +const masonryClasses: MasonryClasses = generateUtilityClasses('MuiMasonry', ['root']); + +export default masonryClasses; diff --git a/packages/material-ui-lab/src/MasonryItem/MasonryItem.d.ts b/packages/material-ui-lab/src/MasonryItem/MasonryItem.d.ts new file mode 100644 index 00000000000000..b7a773daf4e0ca --- /dev/null +++ b/packages/material-ui-lab/src/MasonryItem/MasonryItem.d.ts @@ -0,0 +1,49 @@ +import { SxProps } from '@material-ui/system'; +import { OverridableComponent, OverrideProps } from '@material-ui/core/OverridableComponent'; +import { Theme } from '@material-ui/core/styles'; +import { MasonryItemClasses } from './masonryItemClasses'; + +export interface MasonryItemTypeMap

{ + props: P & { + /** + * The content of the component, normally an `` or a `

`. It should be only one element. + */ + children: NonNullable; + /** + * Override or extend the styles applied to the component. + */ + classes?: Partial; + /** + * The initial height of the component in px. This is provided for server-side rendering. + */ + defaultHeight?: number; + /** + * The number of columns taken up by the component + * @default 1 + */ + columnSpan?: number; + /** + * Allows defining system overrides as well as additional CSS styles. + */ + sx?: SxProps; + }; + defaultComponent: D; +} +/** + * + * Demos: + * + * - [Masonry](https://material-ui.com/components/masonry/) + * + * API: + * + * - [MasonryItem API](https://material-ui.com/api/masonry-item/) + */ +declare const MasonryItem: OverridableComponent; + +export type MasonryItemProps< + D extends React.ElementType = MasonryItemTypeMap['defaultComponent'], + P = {}, +> = OverrideProps, D>; + +export default MasonryItem; diff --git a/packages/material-ui-lab/src/MasonryItem/MasonryItem.js b/packages/material-ui-lab/src/MasonryItem/MasonryItem.js new file mode 100644 index 00000000000000..d69ac0eacce3f9 --- /dev/null +++ b/packages/material-ui-lab/src/MasonryItem/MasonryItem.js @@ -0,0 +1,188 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import clsx from 'clsx'; +import { + createUnarySpacing, + getValue, + handleBreakpoints, + unstable_resolveBreakpointValues as resolveBreakpointValues, +} from '@material-ui/system'; +import { unstable_useForkRef as useForkRef } from '@material-ui/utils'; +import { unstable_composeClasses as composeClasses } from '@material-ui/unstyled'; +import { styled, useThemeProps, useTheme } from '@material-ui/core/styles'; +import { getMasonryItemUtilityClass } from './masonryItemClasses'; +import MasonryContext from '../Masonry/MasonryContext'; + +// dummy resize observer used to prevent crash for old browsers that do not support ResizeObserver API(e.g., 11IE) +const MockResizeObserver = () => { + return { + observe: () => {}, + unobserve: () => {}, + disconnect: () => {}, + }; +}; + +const useUtilityClasses = (ownerState) => { + const { classes } = ownerState; + + const slots = { + root: ['root'], + }; + + return composeClasses(slots, getMasonryItemUtilityClass, classes); +}; + +export const style = ({ ownerState, theme }) => { + let styles = { + width: '100%', + '& > *': { + // all contents should have a width of 100% + width: '100%', + boxSizing: 'inherit', + ...(ownerState.isSSR && { height: '100%' }), + }, + visibility: ownerState.height ? 'visible' : 'hidden', + gridColumnEnd: `span ${ownerState.columnSpan}`, + boxSizing: 'inherit', + }; + + if (Array.isArray(ownerState.spacing) || typeof ownerState.spacing === 'object') { + const base = {}; + Object.keys(theme.breakpoints.values).forEach((breakpoint) => { + if (ownerState.spacing[breakpoint] != null) { + base[breakpoint] = true; + } + }); + const spacingValues = resolveBreakpointValues({ values: ownerState.spacing, base }); + const transformer = createUnarySpacing(theme); + const styleFromPropValue = (propValue) => { + const gap = ownerState.height + ? Number(getValue(transformer, propValue).replace('px', '')) + : 0; + // For lazy-loaded images to load properly, masonry item should take up space greater than 1px. + // Taking into account a row gap of 2px, rowSpan should at least be 2. + const rowSpan = ownerState.height ? Math.ceil((ownerState.height + gap) / 2) : 2; + return { + gridRowEnd: `span ${rowSpan}`, + paddingBottom: gap === 0 ? 0 : gap - 2, + }; + }; + styles = { ...styles, ...handleBreakpoints({ theme }, spacingValues, styleFromPropValue) }; + } + return styles; +}; + +const MasonryItemRoot = styled('div', { + name: 'MuiMasonryItem', + slot: 'Root', + overridesResolver: (props, styles) => { + return [styles.root]; + }, +})(style); + +const MasonryItem = React.forwardRef(function MasonryItem(inProps, ref) { + const props = useThemeProps({ + props: inProps, + name: 'MuiMasonryItem', + }); + + const masonryItemRef = React.useRef(null); + + const { spacing = 1 } = React.useContext(MasonryContext); + const { children, className, component = 'div', columnSpan = 1, defaultHeight, ...other } = props; + const isSSR = defaultHeight !== undefined; + + const [height, setHeight] = React.useState(defaultHeight); + + const ownerState = { + ...props, + isSSR, + spacing, + columnSpan, + height: height < 0 ? 0 : height, // MasonryItems to which negative or zero height is passed will be hidden + }; + + const classes = useUtilityClasses(ownerState); + const resizeObserver = React.useRef(null); + React.useEffect(() => { + // do not create a resize observer in case of SSR masonry + if (isSSR) { + return () => {}; + } + try { + resizeObserver.current = new ResizeObserver(([item]) => { + setHeight(item.contentRect.height); + }); + } catch (err) { + resizeObserver.current = MockResizeObserver(); + } + const item = masonryItemRef.current.firstChild; + resizeObserver.current.observe(item); + return () => { + resizeObserver.current.unobserve(item); + }; + }, [isSSR]); + + const handleRef = useForkRef(ref, masonryItemRef); + + const theme = useTheme(); + const styleProp = {}; + if (!Array.isArray(spacing) && typeof spacing !== 'object') { + const gap = height ? Number(theme.spacing(spacing).replace('px', '')) : 0; + const rowSpan = height ? Math.ceil((height + gap) / 2) : 2; + styleProp.gridRowEnd = `span ${rowSpan}`; + styleProp.paddingBottom = gap === 0 ? 0 : gap - 2; + } + + return ( + + {React.Children.only(children)} + + ); +}); + +MasonryItem.propTypes /* remove-proptypes */ = { + // ----------------------------- Warning -------------------------------- + // | These PropTypes are generated from the TypeScript type definitions | + // | To update them edit the d.ts file and run "yarn proptypes" | + // ---------------------------------------------------------------------- + /** + * The content of the component, normally an `` or a `
`. It should be only one element. + */ + children: PropTypes.element.isRequired, + /** + * Override or extend the styles applied to the component. + */ + classes: PropTypes.object, + /** + * @ignore + */ + className: PropTypes.string, + /** + * The number of columns taken up by the component + * @default 1 + */ + columnSpan: PropTypes.number, + /** + * The component used for the root node. + * Either a string to use a HTML element or a component. + */ + component: PropTypes.elementType, + /** + * The initial height of the component in px. This is provided for server-side rendering. + */ + defaultHeight: PropTypes.number, + /** + * Allows defining system overrides as well as additional CSS styles. + */ + sx: PropTypes.object, +}; + +export default MasonryItem; diff --git a/packages/material-ui-lab/src/MasonryItem/MasonryItem.test.js b/packages/material-ui-lab/src/MasonryItem/MasonryItem.test.js new file mode 100644 index 00000000000000..c0173c121f63d6 --- /dev/null +++ b/packages/material-ui-lab/src/MasonryItem/MasonryItem.test.js @@ -0,0 +1,111 @@ +import * as React from 'react'; +import { createClientRender, describeConformance } from 'test/utils'; +import MasonryItem, { masonryItemClasses as classes } from '@material-ui/lab/MasonryItem'; +import { expect } from 'chai'; +import { createTheme } from '@material-ui/core/styles'; +import defaultTheme from '@material-ui/core/styles/defaultTheme'; +import { style } from './MasonryItem'; + +describe('', () => { + const render = createClientRender(); + + describeConformance( + +
+ , + () => ({ + classes, + inheritComponent: 'div', + render, + refInstanceof: window.HTMLDivElement, + testComponentPropWith: 'span', + muiName: 'MuiMasonryItem', + skip: [ + 'componentsProp', + 'themeVariants', + // reactTestRenderer fails due to this error: `TypeError: parameter 1 is not of type "Element"` + 'reactTestRenderer', + ], + }), + ); + + const children =
; + const theme = createTheme({ + spacing: 8, + }); + + it('should render children by default', () => { + const { getByTestId } = render({children}); + expect(getByTestId('test-children')).not.to.equal(null); + }); + + describe('style attribute:', () => { + it('should render with padding bottom and grid-row-end responsive to breakpoints', () => { + expect( + style({ + ownerState: { + height: 100, + columnSpan: 1, + spacing: { xs: 1, sm: 2, md: 3 }, + }, + theme, + }), + ).to.deep.equal({ + '@media (min-width:0px)': { + gridRowEnd: `span ${Math.ceil((100 + Number(theme.spacing(1).replace('px', ''))) / 2)}`, + paddingBottom: Number(theme.spacing(1).replace('px', '')) - 2, + }, + [`@media (min-width:${defaultTheme.breakpoints.values.sm}px)`]: { + gridRowEnd: `span ${Math.ceil((100 + Number(theme.spacing(2).replace('px', ''))) / 2)}`, + paddingBottom: Number(theme.spacing(2).replace('px', '')) - 2, + }, + [`@media (min-width:${defaultTheme.breakpoints.values.md}px)`]: { + gridRowEnd: `span ${Math.ceil((100 + Number(theme.spacing(3).replace('px', ''))) / 2)}`, + paddingBottom: Number(theme.spacing(3).replace('px', '')) - 2, + }, + width: '100%', + [`& > *`]: { + width: '100%', + boxSizing: 'inherit', + }, + visibility: 'visible', + gridColumnEnd: 'span 1', + boxSizing: 'inherit', + }); + }); + + it('should render with given column span', () => { + expect( + style({ + ownerState: { + height: 100, + columnSpan: 2, + spacing: 1, + }, + theme, + }), + ).to.deep.equal({ + width: '100%', + [`& > *`]: { + width: '100%', + boxSizing: 'inherit', + }, + visibility: 'visible', + gridColumnEnd: 'span 2', + boxSizing: 'inherit', + }); + }); + + it('should compute grid-row-end based on given height', () => { + const { getByTestId } = render( + + {children} + , + ); + const computedStyle = getComputedStyle(getByTestId('test-root')); + expect(computedStyle['grid-row-end']).to.equal( + `span ${Math.ceil((150 + Number(theme.spacing(1).replace('px', ''))) / 2)}`, + ); + }); + }); +}); diff --git a/packages/material-ui-lab/src/MasonryItem/index.d.ts b/packages/material-ui-lab/src/MasonryItem/index.d.ts new file mode 100644 index 00000000000000..5f7ad58b1ae710 --- /dev/null +++ b/packages/material-ui-lab/src/MasonryItem/index.d.ts @@ -0,0 +1,5 @@ +export * from './MasonryItem'; +export { default } from './MasonryItem'; + +export * from './masonryItemClasses'; +export { default as masonryItemClasses } from './masonryItemClasses'; diff --git a/packages/material-ui-lab/src/MasonryItem/index.js b/packages/material-ui-lab/src/MasonryItem/index.js new file mode 100644 index 00000000000000..126c43ca7ee4c2 --- /dev/null +++ b/packages/material-ui-lab/src/MasonryItem/index.js @@ -0,0 +1,4 @@ +export { default } from './MasonryItem'; + +export * from './masonryItemClasses'; +export { default as masonryItemClasses } from './masonryItemClasses'; diff --git a/packages/material-ui-lab/src/MasonryItem/masonryItemClasses.ts b/packages/material-ui-lab/src/MasonryItem/masonryItemClasses.ts new file mode 100644 index 00000000000000..38e9be4fa595e8 --- /dev/null +++ b/packages/material-ui-lab/src/MasonryItem/masonryItemClasses.ts @@ -0,0 +1,16 @@ +import { generateUtilityClass, generateUtilityClasses } from '@material-ui/unstyled'; + +export interface MasonryItemClasses { + /** Styles applied to the root element. */ + root: string; +} + +export type MasonryItemClassKey = keyof MasonryItemClasses; + +export function getMasonryItemUtilityClass(slot: string): string { + return generateUtilityClass('MuiMasonryItem', slot); +} + +const masonryItemClasses: MasonryItemClasses = generateUtilityClasses('MuiMasonryItem', ['root']); + +export default masonryItemClasses; diff --git a/packages/material-ui-system/src/breakpoints.js b/packages/material-ui-system/src/breakpoints.js index 084339319d2404..b070c42e9f8f9f 100644 --- a/packages/material-ui-system/src/breakpoints.js +++ b/packages/material-ui-system/src/breakpoints.js @@ -113,4 +113,27 @@ export function mergeBreakpointsInOrder(breakpointsInput, ...styles) { return removeUnusedBreakpoints(Object.keys(emptyBreakpoints), mergedOutput); } +export function resolveBreakpointValues({ values: breakpointValues, base }) { + const keys = Object.keys(base); + + if (keys.length === 0) { + return breakpointValues; + } + + let previous; + + return keys.reduce((acc, breakpoint) => { + if (typeof breakpointValues === 'object') { + acc[breakpoint] = + breakpointValues[breakpoint] != null + ? breakpointValues[breakpoint] + : breakpointValues[previous]; + } else { + acc[breakpoint] = breakpointValues; + } + previous = breakpoint; + return acc; + }, {}); +} + export default breakpoints; diff --git a/packages/material-ui-system/src/index.js b/packages/material-ui-system/src/index.js index c70da053d26e83..8ee5f188fe3ce7 100644 --- a/packages/material-ui-system/src/index.js +++ b/packages/material-ui-system/src/index.js @@ -2,7 +2,11 @@ export { css, keyframes, GlobalStyles, StyledEngineProvider } from '@material-ui export { default as borders } from './borders'; export * from './borders'; export { default as breakpoints } from './breakpoints'; -export { handleBreakpoints, mergeBreakpointsInOrder } from './breakpoints'; +export { + handleBreakpoints, + mergeBreakpointsInOrder, + resolveBreakpointValues as unstable_resolveBreakpointValues, +} from './breakpoints'; export { default as compose } from './compose'; export { default as display } from './display'; export { default as flexbox } from './flexbox'; diff --git a/packages/material-ui/src/Grid/Grid.js b/packages/material-ui/src/Grid/Grid.js index f864f5bce7a1de..c8ca466ae313a6 100644 --- a/packages/material-ui/src/Grid/Grid.js +++ b/packages/material-ui/src/Grid/Grid.js @@ -12,7 +12,11 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import clsx from 'clsx'; -import { unstable_extendSxProp as extendSxProp, handleBreakpoints } from '@material-ui/system'; +import { + unstable_extendSxProp as extendSxProp, + handleBreakpoints, + unstable_resolveBreakpointValues as resolveBreakpointValues, +} from '@material-ui/system'; import { unstable_composeClasses as composeClasses } from '@material-ui/unstyled'; import requirePropFactory from '../utils/requirePropFactory'; import styled from '../styles/styled'; @@ -25,27 +29,6 @@ function getOffset(val) { return `${parse}${String(val).replace(String(parse), '') || 'px'}`; } -// Duplicated with Stack.js -function resolveBreakpointValues({ values, base }) { - const keys = Object.keys(base); - - if (keys.length === 0) { - return values; - } - - let previous; - - return keys.reduce((acc, breakpoint) => { - if (typeof values === 'object') { - acc[breakpoint] = values[breakpoint] != null ? values[breakpoint] : values[previous]; - } else { - acc[breakpoint] = values; - } - previous = breakpoint; - return acc; - }, {}); -} - function generateGrid(globalStyles, theme, breakpoint, ownerState) { const size = ownerState[breakpoint]; diff --git a/packages/material-ui/src/Stack/Stack.js b/packages/material-ui/src/Stack/Stack.js index d951873c43ba34..d4f1d6a9624e9f 100644 --- a/packages/material-ui/src/Stack/Stack.js +++ b/packages/material-ui/src/Stack/Stack.js @@ -5,6 +5,7 @@ import { getValue, handleBreakpoints, unstable_extendSxProp as extendSxProp, + unstable_resolveBreakpointValues as resolveBreakpointValues, } from '@material-ui/system'; import { deepmerge } from '@material-ui/utils'; import styled from '../styles/styled'; @@ -31,27 +32,6 @@ function joinChildren(children, separator) { }, []); } -// Duplicated with Grid.js -function resolveBreakpointValues({ values, base }) { - const keys = Object.keys(base); - - if (keys.length === 0) { - return values; - } - - let previous; - - return keys.reduce((acc, breakpoint) => { - if (typeof values === 'object') { - acc[breakpoint] = values[breakpoint] != null ? values[breakpoint] : values[previous]; - } else { - acc[breakpoint] = values; - } - previous = breakpoint; - return acc; - }, {}); -} - const getSideFromDirection = (direction) => { return { row: 'Left', diff --git a/test/regressions/index.js b/test/regressions/index.js index 5febba1071a708..6575785d8044e6 100644 --- a/test/regressions/index.js +++ b/test/regressions/index.js @@ -68,6 +68,7 @@ const blacklist = [ 'docs-components-hidden', // Need to dynamically resize to test 'docs-components-icons/FontAwesomeIconSize.png', // Relies on cascading network requests 'docs-components-image-list', // Image don't load + 'docs-components-masonry/ImageMasonry.png', // Image don't load 'docs-components-material-icons/synonyms.png', // No component 'docs-components-menus', // Need interaction 'docs-components-modal/KeepMountedModal.png', // Needs interaction