React MUI - Masonry mobile responsive - reactjs

I'm new to react
I'm trying to figure out how I can make a Masonry image grid - mobile resonsive.
The idea is that I want the images to stack on top of each other on mobile view.
The Masonry grid should still act as a Masonry on desktop screens.
I just cannot figure out how to do that.
I appreciate the help.
Here is my code, view demo here
import * as React from 'react';
// material-ui
import { useTheme, styled } from '#mui/material/styles';
import { Box, Paper, Button, Container, Grid, Typography } from '#mui/material';
import Masonry from '#mui/lab/Masonry';
const Label = styled(Paper)(({ theme }) => ({
backgroundColor: theme.palette.mode === 'dark' ? '#1A2027' : '#fff',
...theme.typography.body2,
padding: theme.spacing(0.5),
textAlign: 'center',
color: theme.palette.text.secondary,
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
}));
const MasonryPage = () => (
<Box>
<Masonry columns={3} spacing={3}>
{itemData.map((item, index) => (
<div key={index}>
<Label>{index + 1}</Label>
<img
src={`${item.img}?w=162&auto=format`}
srcSet={`${item.img}?w=162&auto=format&dpr=2 2x`}
alt={item.title}
loading="lazy"
style={{
borderBottomLeftRadius: 4,
borderBottomRightRadius: 4,
display: 'block',
width: '100%',
}}
/>
</div>
))}
</Masonry>
</Box>
);
export default MasonryPage;
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',
},
];

As the previous poster suggested you can use MUI's breakpoints to set the number of columns based on viewport width. For example:
import React from 'react';
import Masonry from '#mui/lab/Masonry';
import Box from '#mui/material/Box';
import Typography from '#mui/material/Typography';
const CollectionList = (props) => {
return (
<Box sx={{ pt: 4 }}>
<Typography variant="h2">Collections</Typography>
<Masonry columns={{ xs: 1, sm: 2, md: 3 }} spacing={2}>
<div>One</div>
<div>Two</div>
<div>Three</div>
</Masonry>
</Box>
);
};
export default CollectionList;
The value will be set implicitly for each breakpoint you specify as well as any up to the next one set. So in the example above, any viewport with medium (md) or higher will have 3 columns.
columns={{ xs: 1, md: 3, xl: 4 }}
would set 1 column for anything smaller than medium (md), 3 columns for anything larger or equal to medium up to extra large. From there on, it will be 4 columns.

you might want to try columns={{lg:3, md:6}} like this

Related

Make material ui SwipeableDrawer draggable even on desktop

I added swipeable drawer based on the documentation:
Swipeable You can make the drawer swipeable with the SwipeableDrawer
component.
This component comes with a 2 kB gzipped payload overhead. Some
low-end mobile devices won't be able to follow the fingers at 60 FPS.
You can use the disableBackdropTransition prop to help.
{(['left', 'right', 'top', 'bottom'] as const).map((anchor) => (
<React.Fragment key={anchor}>
<Button onClick={toggleDrawer(anchor, true)}>{anchor}</Button>
<SwipeableDrawer
anchor={anchor}
open={state[anchor]}
onClose={toggleDrawer(anchor, false)}
onOpen={toggleDrawer(anchor, true)}
>
{list(anchor)}
</SwipeableDrawer>
</React.Fragment>
))}
The problem with this is that it's only draggable on mobile device. Is it possible to make it draggable on desktop?
Like the user clicks on the edge with the cursor and drags the drawer to close or open it.
Especially, by adding an edge that the user can click on with the cursor:
Swipeable edge You can configure the SwipeableDrawer to have a visible
edge when closed.
If you are on a desktop, you can toggle the drawer with the "OPEN"
button. If you are on mobile, you can open the demo in CodeSandbox
("edit" icon) and swipe.
import * as React from 'react';
import PropTypes from 'prop-types';
import { Global } from '#emotion/react';
import { styled } from '#mui/material/styles';
import CssBaseline from '#mui/material/CssBaseline';
import { grey } from '#mui/material/colors';
import Button from '#mui/material/Button';
import Box from '#mui/material/Box';
import Skeleton from '#mui/material/Skeleton';
import Typography from '#mui/material/Typography';
import SwipeableDrawer from '#mui/material/SwipeableDrawer';
const drawerBleeding = 56;
const Root = styled('div')(({ theme }) => ({
height: '100%',
backgroundColor:
theme.palette.mode === 'light' ? grey[100] : theme.palette.background.default,
}));
const StyledBox = styled(Box)(({ theme }) => ({
backgroundColor: theme.palette.mode === 'light' ? '#fff' : grey[800],
}));
const Puller = styled(Box)(({ theme }) => ({
width: 30,
height: 6,
backgroundColor: theme.palette.mode === 'light' ? grey[300] : grey[900],
borderRadius: 3,
position: 'absolute',
top: 8,
left: 'calc(50% - 15px)',
}));
function SwipeableEdgeDrawer(props) {
const { window } = props;
const [open, setOpen] = React.useState(false);
const toggleDrawer = (newOpen) => () => {
setOpen(newOpen);
};
// This is used only for the example
const container = window !== undefined ? () => window().document.body : undefined;
return (
<Root>
<CssBaseline />
<Global
styles={{
'.MuiDrawer-root > .MuiPaper-root': {
height: `calc(50% - ${drawerBleeding}px)`,
overflow: 'visible',
},
}}
/>
<Box sx={{ textAlign: 'center', pt: 1 }}>
<Button onClick={toggleDrawer(true)}>Open</Button>
</Box>
<SwipeableDrawer
container={container}
anchor="bottom"
open={open}
onClose={toggleDrawer(false)}
onOpen={toggleDrawer(true)}
swipeAreaWidth={drawerBleeding}
disableSwipeToOpen={false}
ModalProps={{
keepMounted: true,
}}
>
<StyledBox
sx={{
position: 'absolute',
top: -drawerBleeding,
borderTopLeftRadius: 8,
borderTopRightRadius: 8,
visibility: 'visible',
right: 0,
left: 0,
}}
>
<Puller />
<Typography sx={{ p: 2, color: 'text.secondary' }}>51 results</Typography>
</StyledBox>
<StyledBox
sx={{
px: 2,
pb: 2,
height: '100%',
overflow: 'auto',
}}
>
<Skeleton variant="rectangular" height="100%" />
</StyledBox>
</SwipeableDrawer>
</Root>
);
}
SwipeableEdgeDrawer.propTypes = {
/**
* Injected by the documentation to work in an iframe.
* You won't need it on your project.
*/
window: PropTypes.func,
};
export default SwipeableEdgeDrawer;

Dynamically adjust ImageList columns based on screen size?

I've tried using ImageList component instead of Grid as I just need a grid of photos with titles and it seems to be the whole point of ImageList. My issue is that unlike with Grid I cannot pass breakpoint props for different screen sizes (which I find weird as this would seem logical) so that I can get different count of columns on different screens. What would be the best approach to adjust number of columns based on screen size?
ImageList uses CSS grid and needs the col prop to set the grid-template-columns but without any responsive API baked in. You can swap the ImageList with a Box component with the display set to grid, and uses the sx prop to declare the column template value depend on the screen size, but first let define some breakpoints:
const theme = createTheme({
breakpoints: {
values: {
mobile: 0,
bigMobile: 350,
tablet: 650,
desktop: 900
}
}
});
Then in the component, you can start using it like this:
import ImageListItem, { imageListItemClasses } from "#mui/material/ImageListItem";
<ThemeProvider theme={theme}>
<Box
sx={{
display: "grid",
gridTemplateColumns: {
mobile: "repeat(1, 1fr)",
bigMobile: "repeat(2, 1fr)",
tablet: "repeat(3, 1fr)",
desktop: "repeat(4, 1fr)"
}
// standard variant from here:
// https://github.com/mui-org/material-ui/blob/3e679ac9e368aeb170d564d206d59913ceca7062/packages/mui-material/src/ImageListItem/ImageListItem.js#L42-L43
[`& .${imageListItemClasses.root}`]: {
display: "flex",
flexDirection: "column"
}
}}
>
{itemData.map((item) => <ImageListItem {...}/>)}
</Box>
</ThemeProvider>
Live Demo
References
Media queries in MUI components
https://mui.com/customization/breakpoints/#main-content
I used the useMediaQuery hook to get the cols props for the ImageList component.
import { ImageList, ImageListItem, useMediaQuery } from '#mui/material';
function Gallery() {
const matches = useMediaQuery('(min-width:600px)');
return (
<ImageList cols={matches ? 3 : 2} variant="masonry">
<ImageListItem>
...
</ImageListItem>
</ImageList>
);
}
I spotted a similar problem. The ImageList renders a <ul> tag in DOM. Hence I created my own ImageList styled <ul> component which works fine with ImageListItem. Here as per the gridTemplateColumns attribute for screens with display size sm will show 2 images, display size md will show 4 images
and display size lg will show 5 images.
import * as React from 'react';
import ImageListItem from '#mui/material/ImageListItem';
import { styled } from '#mui/material/styles';
const ImageGalleryList = styled('ul')(({ theme }) => ({
display: 'grid',
padding: 0,
margin: theme.spacing(0, 4),
gap: 8,
[theme.breakpoints.up('sm')]: {
gridTemplateColumns: 'repeat(2, 1fr)'
},
[theme.breakpoints.up('md')]: {
gridTemplateColumns: 'repeat(4, 1fr)'
},
[theme.breakpoints.up('lg')]: {
gridTemplateColumns: 'repeat(5, 1fr)'
},
}));
export default function ImageGallery({imageData}) {
return (
<ImageGalleryList>
{itemData.map((item) => (
<ImageListItem key={item.img}>
// Replace this with your ImageListItem
</ImageListItem>
))}
</ImageGalleryList>
);
}
This solution I came up with works, but seems like a lot of lines for something that Grid handles out of the box. Doesn't ImageList have some built in responsive design implementation?
export function Example(props) {
// not sure if there is a way to get something like this dictionary from React?
const breakpoints = {
xs: 0,
sm: 600,
md: 960,
lg: 1280,
xl: 1920
}
const getColumns = (width) => {
if (width < breakpoints.sm) {
return 2
} else if (width < breakpoints.md) {
return 3
} else if (width < breakpoints.lg) {
return 6
} else if (width < breakpoints.xl) {
return 7
} else {
return 8
}
}
const [columns, setColumns] = useState(getColumns(window.innerWidth))
const updateDimensions = () => {
setColumns(getColumns(window.innerWidth))
}
useEffect(() => {
window.addEventListener("resize", updateDimensions);
return () => window.removeEventListener("resize", updateDimensions);
}, []);
return (
<ImageList cols={columns}>
{/* list items ... */}
</ImageList>
)
}
Instead of using ImageList I used "Image masonry" seems to work.
https://mui.com/material-ui/react-masonry/#main-content
<Masonry columns={{ xs: 1, sm: 2, md: 3, lg: 4, xl: 5 }} spacing={{ xs: 1, sm: 2 }}>
Image masonry
This example demonstrates the use of Masonry for images. Masonry orders its children by row. If you'd like to order images by column, check out ImageList.
I resolved this by overriding column-count property on ImageList (root component)
So, you can add breakpoints using sx props
<ImageList
sx={{
columnCount: {
xs: '1 !important',
sm: '2 !important',
md: '3 !important',
lg: '4 !important',
xl: '5 !important',
},
}}
>
{/* list items ... */}
</ImageList>

Material UI date picker behaving differently on mobile device

I'm using Material UI datepicker in a React project. I've created a calendar + time selection (code below). For some reason when I test this on Chrome desktop using the device toolbar to emulate iphone 8 it works fine, but when I go to the site on my actual iphone 8, the UI is broken (images below). Any thoughts?
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { Button, Typography } from '#material-ui/core';
import { makeStyles } from '#material-ui/core/styles';
import moment from 'moment';
import { MuiPickersUtilsProvider, DatePicker } from '#material-ui/pickers';
import DateFnsUtils from '#date-io/date-fns';
const useStyles = makeStyles(theme => ({
root: {
display: 'flex',
flexDirection: 'row',
padding: theme.spacing(3),
[theme.breakpoints.down('sm')]: {
padding: 0,
},
},
picker: {
display: 'flex',
flexDirection: 'row',
[theme.breakpoints.down('sm')]: {
flexDirection: 'column',
},
},
timeContainer: {
display: 'flex',
flexDirection: 'column',
marginLeft: theme.spacing(2),
overflowY: 'scroll',
[theme.breakpoints.down('sm')]: {
alignItems: 'center',
marginLeft: 0,
marginBottom: theme.spacing(3),
},
},
timeButton: { width: 226, height: 50 },
}));
export default function Timesgrid(props) {
const classes = useStyles();
const [selectedDate, setSeletedDate] = useState(moment());
const disableDay = date => {
const formattedDate = moment(date).format('YYYY-MM-DD');
return formattedDate in props.availability &&
props.availability[formattedDate].length > 0
? false
: true;
};
const handleDateChange = date => {
setSeletedDate(moment(date));
};
const renderTimes = () => {
const dateStr = moment(selectedDate).format('YYYY-MM-DD');
const availability = props.availability[dateStr];
return availability && availability.length ? (
<div className={classes.timeContainer}>
{availability.map((time, i) => (
<Button
style={{ marginTop: i === 0 ? 0 : 10 }}
key={time}
className={classes.timeButton}
variant="outlined"
color="primary"
onClick={() => props.onDatetimeSelected(moment(time))}
>
<b>{moment(time).format('LT')}</b>
</Button>
))}
</div>
) : (
<Typography variant="h6" className={classes.timeContainer}>
No availability
</Typography>
);
};
const renderPicker = () => {
return (
<div className={classes.picker}>
<MuiPickersUtilsProvider utils={DateFnsUtils}>
<DatePicker
disablePast
shouldDisableDate={disableDay}
variant="static"
defaultValue={selectedDate}
value={selectedDate}
onChange={handleDateChange}
/>
</MuiPickersUtilsProvider>
{renderTimes()}
</div>
);
};
return <div className={classes.root}>{renderPicker()}</div>;
}
Timesgrid.propTypes = {
availability: PropTypes.object.isRequired,
onDatetimeSelected: PropTypes.func.isRequired,
};
The answer to your question as we discussed in comments was to add the following CSS styling.
overflow: scroll
height: 100%
Justification:
The reason I suggested this solution was due to the comparisons in your images. The popup screen for the component was mushed together on the native browser popup but, not on the Chrome DevTools popup. This indicated that the native browser container was not allowing for overflow to occur.
Setting the height of the component to 100% ensures that it takes up the full height of it's container and allowing overflow ensures that if your component is greater than 100% of the containers height that it will render as shown in the devtools view.
Furthermore, In your reply you said your solution was
overflowY: scroll,
height: 100%
and overflow: scroll allows for overflow on both axes. Since the x-axis doesn't require overflow, this is a better solution.
Glad to see it worked.

How to make a material ui chips array scroll left and right using arrows?

I am trying to make material ui Chips scroll left and right in a single line.
Here is a code sandbox I am working on.
CodeSandbox
If I understood your question correctly...
What you want is basically a single line of material-ui chips with scrolling when the container size is not enough to show everything.
Demo code: Chips + overflow auto + maxWidth + flexWrap nowrap
It is somewhat easy to achieve this...
We can add overflow:auto and maxWidth:400px to the chips container element.
Basically, this tells the browser to show the scroll if the content requires more than 400px of horizontal space (width).
And to make the chips flow horizontally we just add to the container flexWrap: nowrap to instruct the render to do not wrap elements.
Sources:
Github: material-ui - docs/src/pages/components/chips/ChipsArray.js
You can certainly check out react-material-ui-caraousel.
Just like:
import React from "react";
import { makeStyles } from "#material-ui/core/styles";
import Chip from "#material-ui/core/Chip";
import Paper from "#material-ui/core/Paper";
import Carousel from 'react-material-ui-carousel'
const useStyles = makeStyles((theme) => ({
root: {
display: "flex",
justifyContent: "center",
flexWrap: "wrap",
listStyle: "none",
padding: theme.spacing(0.5),
margin: 0,
maxWidth: "1000px"
},
chip: {
margin: theme.spacing(0.5)
}
}));
export default function ChipsArray() {
const classes = useStyles();
const [chipData, setChipData] = React.useState([
{ key: 0, label: "Angular" },
{ key: 1, label: "jQuery" },
{ key: 2, label: "Polymer" },
{ key: 3, label: "React" },
{ key: 4, label: "Php" },
{ key: 4, label: "Golang" },
{ key: 4, label: "More" }
]);
const handleDelete = (chipToDelete) => () => {
setChipData((chips) =>
chips.filter((chip) => chip.key !== chipToDelete.key)
);
};
return (
<Paper component="ul" className={classes.root}>
<Carousel
autoPlay={false}
interval={0}
animation="slide"
timeout={100}
>
{
chipData.map( (item, i) => (
<li key={item.key}>
<Chip
label={item.label}
onDelete={handleDelete(item)}
className={classes.chip}
/>
</li>
) )
}
</Carousel>
</Paper>
);
}

Trying to change the colour of SVG Image Icon upon selection Material UI

I've created a vertical tab where when clicked the text changes color. I'm trying to change the icon color as well with a separate color. Can anyone help, please
I've tried naming it on the tab style sheet and individually but no result. The code below will show my attempt where I tried creating a class, but an error of Cannot read property 'icon' of undefined appears. If the solution to color the icon and separately can be done with classes then I guess we just have to resolve this error. If not please ignore the error and if there are any other solutions please suggest. thank you
imported files:
import React from "react";
import PropTypes from "prop-types";
import ReactDOM from "react-dom";
import Tabs from "#material-ui/core/Tabs";
import Tab from "#material-ui/core/Tab";
import Typography from "#material-ui/core/Typography";
import { withStyles } from "#material-ui/core/styles";
import Home from './Screens/Home'
import home from './home.svg';
Class content:
export default class ProfileTabs extends React.PureComponent {
state = { activeIndex: 0 };
handleChange = (_, activeIndex) => this.setState({ activeIndex });
render() {
const { classes } = this.props;
const { activeIndex } = this.state;
return (
<nav className= "side-drawer">
<div style={{letterSpacing: 0.7, left: 70, position: "absolute", marginTop: 40 ,}}>
<VerticalTabs className={classes.icon} variant="fullWidth" value={activeIndex} onChange={this.handleChange}>
<MyTab icon ={<img className= "home" src={home} alt="home" style={{height: 45, left:20, top:20, position: "absolute"}}/*Pay FlatIcon or replace by design *//>}
label={<p style={{ textTransform:"capitalize", position:"absolute", left:120, top:27.5,}}>
Home
</p>}
</VerticalTabs>
{activeIndex === 0 && <TabContainer><Home/></TabContainer>}
</div>
</nav>
);
}
}
Styles and other:
const VerticalTabs = withStyles(theme => ({
flexContainer: {
flexDirection: "column"
},
indicator: {
display: "none"
},
root:{
position:"absolute",
left:-70,
top:-40,
}
}))(Tabs);
const MyTab = withStyles(theme => ({
selected: {
color: "White",
borderRight: "none",
},
root: {
minWidth: 221,
margin:0,
paddingBottom:99
},
}))(Tab);
const styles = theme => ({
icon: {
color:"red"
}
});
function TabContainer(props) {
return (
<Typography component="div" style={{ padding: 9 * 3 }}>
{props.children}
</Typography>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<ProfileTabs />, rootElement);
ProfileTabs.propTypes = {
classes: PropTypes.object.isRequired
};
Current Error:
TypeError: Cannot read property 'icon' of undefined
28 | return (
29 | <nav className= "side-drawer">
30 | <div style={{letterSpacing: 0.7, left: 70, position: "absolute", marginTop: 40 ,}}>
> 31 | <VerticalTabs className={classes.icon} variant="fullWidth" value={activeIndex} onChange={this.handleChange}>
| ^ 32 | <MyTab icon ={<img className= "home" src={home} alt="home" style={{height: 45, left:20, top:20, position: "absolute"}}/*Pay FlatIcon or replace by design *//>}
33 | label={<p style={{ textTransform:"capitalize", position:"absolute", left:120, top:27.5,}}>
34 | Home
I hope this helps. I've refactored your code to separate all the styles, and then used the MUI classes objects to pass your specific style overrides down to their elements. The styles and classes may still need some tweaking - this is a best-bet given the supplied code:
import Icon from "#material-ui/core/Icon"; // <== only if you want to use the MUI 'home'
const styles = {
iconOnlyStyle: {
color: 'blue'
},
myDivStyles: {
letterSpacing: 0.7,
left: 70,
position: "absolute",
marginTop: 40
},
myIconStyles: {
color:"red", // <== this will change both label and icon
height: 45,
left:20,
top:20,
position: "absolute"
},
myLabelStyles: {
textTransform: "capitalize",
position: "absolute",
left: 120,
top: 27.5
},
sideDrawer: {
// ...styles
},
tabRoot: {
minWidth: 221,
margin:0,
paddingBottom:99
},
tabSelected: {
color: "White",
borderRight: "none"
},
tabsRoot: {
position:"absolute",
left:-70,
top:-40
},
tabsFlexContainer: {
flexDirection: "column"
},
tabsIndicator: {
display: "none"
}
}
class ProfileTabs extends React.PureComponent {
state = { activeIndex: 0 };
handleChange = (_, activeIndex) => this.setState({ activeIndex });
render() {
const { classes } = this.props;
return (
<nav className={classes.sideDrawer}>
<div className={classes.myDivStyles}>
<Tabs
variant="fullWidth"
value={activeIndex}
onChange={this.handleChange}
classes={{
root: classes.tabsRoot,
flexContainer: classes.tabsFlexContainer,
indicator: classes.tabsIndicator
}}
>
<Tab
label="Home"
icon={home}
icon={<Icon classes={{ root: classes.iconOnlyStyle }}>home</Icon>}
classes={{
root: classes.tabRoot,
labelIcon: classes.myIconStyles,
label: classes.myLabelStyles,
selected: classes.tabSelected
}}
/>
</Tabs>
</div>
</nav>
)
}
}
export default withStyles(styles)(ProfileTabs)
Let me know if this doesn't make sense or if you need any explanations. The classes prop in the MUI library took me a while to get the gist of when I first used it, but hopefully this helps clear-up how it can work.

Resources