React.js + MUI: Modal closes when clicking on Select component - reactjs

for MUI learning purposes I'm creating a simple CRUD app with a modal. That modal contains a simple form with a few TextField and one Select components. THe issue is, that when clicking on the Select component, the modal closes.
Modal:
<ClickAwayListener
onClickAway={handleClickAway}
>
<Box sx={{ marginTop: '80px' }}>
<Button
sx={{
borderRadius: '8px',
backgroundColor: '#fff',
color: '#091fbb',
border: '1px solid #091fbb'
}}
onClick={handleOpen}
>
Add new
</Button>
<Modal
hideBackdrop
open={open}
onClose={handleClose}
sx={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
backgroundColor: '#fff',
border: '1px solid #b9c2ff',
borderRadius: '8px',
height: 'fit-content',
width: 400,
boxShadow: 2,
}}
>
<form
onSubmit={handleSubmit}
style={{
display: 'flex',
flexDirection: 'column',
paddingTop: '12px',
paddingLeft: '18px',
paddingRight: '18px',
paddingBottom: '30px',
}}
>
<Typography variant='h6' sx={{ my: 2, textAlign: 'center' }}>ADD NEW PARTICIPANT</Typography>
<FormControl sx={{ my: 1 }}>
<Typography variant='body2'>Fullname</Typography>
<TextField
variant='standard'
value={fullname}
onChange={(e) => setFullname(e.target.value)}
/>
</FormControl>
<FormControl sx={{ my: 1 }}>
<Typography variant='body2'>Gender</Typography>
<Select
variant='standard'
value={gender}
MenuProps={{
onClick: e => {
e.preventDefault();
}
}}
onChange={(e) => setGender(e.target.value)}
>
<MenuItem value="None"><em>None</em></MenuItem>
<MenuItem value='Male'>Male</MenuItem>
<MenuItem value='Female'>Female</MenuItem>
<MenuItem value='Other'>Other</MenuItem>
</Select>
</FormControl>
<FormControl sx={{ my: 1 }}>
<Typography variant='body2'>Email</Typography>
<TextField
variant='standard'
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</FormControl>
<FormControl sx={{ my: 1 }}>
<Typography variant='body2'>Phone nr</Typography>
<TextField
variant='standard'
value={phone}
onChange={(e) => setPhone(e.target.value)}
/>
</FormControl>
<FormControl sx={{ my: 1 }}>
<Typography variant='body2'>Description</Typography>
<TextField
variant='standard'
value={description}
onChange={(e) => setDescription(e.target.value)}
multiline
rows={3}
/>
</FormControl>
{ !isLoading && <Button
variant='contained'
type='submit'
sx={{
backgroundColor: '#091fbb'
}}>
Add participant
</Button>}
{ isLoading && <Button
variant='contained'
type='submit'
disabled
sx={{
backgroundColor: '#091fbb'
}}>
Adding participant...
</Button>}
</form>
</Modal>
</Box>
</ClickAwayListener>
Handler functions and states for Modal:
const [open, setOpen] = useState(false);
const [fullname, setFullname] = useState('');
const [gender, setGender] = useState('None');
const [email, setEmail] = useState('');
const [phone, setPhone] = useState('');
const [description, setDescription] = useState('');
const [isLoading, setIsLoading] = useState(false);
const handleOpen = () => {
setOpen(!open);
};
const handleClose = () => {
setFullname('');
setGender('None');
setEmail('');
setPhone('');
setDescription('');
setOpen(false);
};
const handleClickAway = (e) => {
if (!e.target.classList.contains('MuiMenuItem-root')) {
setFullname('');
setGender('None');
setEmail('');
setPhone('');
setDescription('');
setOpen(false);
}
};
const handleSubmit = (e) => {
e.preventDefault();
const newParticipant = { fullname, gender, email, phone, description };
const requestOptions = {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newParticipant)
};
setIsLoading(true);
fetch('http://localhost:8000/participants', requestOptions)
.then(() => {
setFullname('');
setGender('None');
setEmail('');
setPhone('');
setDescription('');
setIsLoading(false);
setOpen(!open);
})
};
Could anyone advise on how to solve this? Adding MenuProps to prevent default behavior on the Select component and the if statement in handleClickAway function didnt help in my case, even though that helped other who were facing the same issue.

Assuming that the goal is to have Select work in Modal without closing it, perhaps the default behavior of Modal could be enough and use of ClickAwayListener may be not be necessary.
Instead of styling Modal directly with the sx prop, try wrap the modal content in a Box and style this container. This preserves the default behavior of Modal, so that clicking on Select would not trigger the closing of it.
Since Modal internally detect click on the backdrop to close itself, consider to style the backdrop with a transparent background instead of disabling it, so that the use of ClickAwayListener could also be omitted.
Demo of simplified example on: stackblitz (excluded all data handling)
<Modal
open={open}
onClose={handleClose}
// 👇 Style the backdrop to be transparent
slotProps={{ backdrop: { sx: { background: "transparent" } } }}
>
<Box
// 👇 Style the container Box for modal content
sx={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
backgroundColor: "#fff",
border: "1px solid #b9c2ff",
borderRadius: "8px",
height: "fit-content",
width: 400,
boxShadow: 2,
}}
>
{/* Modal content here */}
</Box>
</Modal>

This happens because the menu is by default mounted in the DOM outside of the modal HTML hierarchy; the Select component uses a Menu component which in turn uses a Popper component. Looking at the API documentation for Popper:
The children will be under the DOM hierarchy of the parent component.
disablePortal:bool = false
A simple solution is to override this default in MenuProps, which will cause the component to be rendered as a child to the Modal and will no longer trigger the ClickAwayListener callback. I'm not aware of any downsides to this approach.
<Select
variant='standard'
value={gender}
MenuProps={{
disablePortal: true, // <--- HERE
onClick: e => {
e.preventDefault();
}
}}
onChange={(e) => setGender(e.target.value)}
> . . . </Select>

Related

React MUI Collapse component ignores timeout and acts as if timeout is 0

I'm using React, Typescript with MUI and I wanted to make a burger menu where each clicking on an option smoothly opens a <Box> component, which contains additional options. What happens is that it ignores the timeout={2000} prop and just displays it as if it's display is none and then gets turned into display:block for example.
const [badgeMenuOpen, setBadgeMenuOpen] = useState<boolean>(false);
const [badgeMenuOpen1, setBadgeMenuOpen1] = useState<boolean>(false);
const [badgeMenuOpen2, setBadgeMenuOpen2] = useState<boolean\>(false);
const handleMenuClick = () => {
if (badgeMenuOpen) {
setBadgeMenuOpen(false);
} else {
setBadgeMenuOpen(true);
}
};
const handleMenu1Click = () => {
if (badgeMenuOpen1) {
setBadgeMenuOpen1(false);
} else {
setBadgeMenuOpen1(true);
}
};
const handleMenu2Click = () => {
if (badgeMenuOpen2) {
setBadgeMenuOpen2(false);
} else {
setBadgeMenuOpen2(true);
}
};
<IconButton
size="medium"
edge="start"
sx={{ color: "#0073d1" }}
aria-label="sidebar-logo"
onClick={handleSideMenuClick}
/>
<Typography>menu\</Typography>
<MenuIcon\>
<IconButton\>
<StyledDrawer
open={sideMenuOpen}
anchor="right"
hideBackdrop={true}
aria-controls={sideMenuOpen ? "sidebar-logo" : undefined}
aria-expanded={sideMenuOpen ? true : false}
sx={{ marginTop: "60px", backgroundColor: "#f8f8f" }}
onClose={() =\> {
setSideMenuOpen(false);
}}
id="MUI-drawer"
\>
<Box minWidth={"100%"} role="presentation"\>
<List
disablePadding={true}
sx={{ minWidth: "100%", display: "flex", flexDirection: "column" }}
\>
<ListItem sx={{ display: "flex", justifyContent: "center" }}>
<RouterLink
className="header__phone-menu__item__submenu__item"
to="/home"
\>
{t("exampletext")}
</RouterLink\>
<Divider /\>
</ListItem\>
<Divider /\>
<ListItem
disablePadding={true}
sx={{
display: "flex",
flexDirection: "column",
backgroundColor: badgeMenuOpen1 ? "#fff" : "#f8f8f",
}}
\>
<StyledBadge
onClick={handleMenu1Click}
id={"badgeMenu"}
color="error"
invisible={false}
badgeContent={"!"}
sx={{
display: "flex",
flexDirection: "column",
}}
className="header__phone-menu__item__submenu-toggle"
\>
{t("exampletext")}
</StyledBadge>
<Box
sx={{ height: badgeMenuOpen1 ? "100%" : "0px" }}
id="badgeId"
className="header__phone-menu__item__submenu"
\>
<RouterLink
className="header__phone-menu__item__submenu__item"
to="/cards"
\>
{t("exampletext")}
</RouterLink\>
I tried using <Fade> component, which actually worked, but it just changed it's opacity to 0 so the space was still occupied and that doesn't quite work for me.
Are there any specific parents that the `<Collapse>` component has to be inside for it to work?
--EDIT-- Ignore the \ inside the code, stackoverflow formated it somehow

Disable button on Form Submit in React

For some reason, my Sign In button does not become disabled when the form is submitted. Is there something obviously incorrect?
It is supposed to be disabled when processing a user's sign in API request.
This is also not a result of a fast API, because even if I set the network speed to very slow, it does not become disabled at any point.
import React, { useRef, useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import {
Alert, Box, Card, TextField, Typography,
} from '#mui/material';
import LoadingButton from '#mui/lab/LoadingButton';
import { useAuth } from '../context/AuthContext';
import '../../pages/bg.css';
export default function SignInAuth() {
const emailRef = useRef();
const passwordRef = useRef();
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const navigate = useNavigate();
const auth = useAuth();
async function handleSubmit(e) {
e.preventDefault();
if (!emailRef.current.value) {
return setError('Please enter an Email Address');
}
if (!passwordRef.current.value) {
return setError('Please enter a password');
}
setLoading(true);
setError('');
fetch(
`${process.env.REACT_APP_API_URL}auth/login`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
Email: emailRef.current.value,
Password: passwordRef.current.value,
}),
},
).then((response) => {
if (response.ok) {
auth.signin(emailRef.current.value);
return navigate('/Dashboard');
}
// Response must be handled in a promise.
return response.json().then((errorText) => { throw new Error(errorText.message); });
}).then(setLoading(false)).catch((err) => {
setError(err.message); // This comes from the returned error promise.
// System throws error, and then error is set for the text.
});
return null;
}
const onSubmit = (e) => {
handleSubmit(e);
};
const textFieldStyle = {
width: '300px',
};
return (
<Box sx={{
zIndex: 1,
minWidth: '200px',
width: '450px',
}}
>
<Card sx={{ p: '20px', borderRadius: '15px' }}>
<Typography variant="h4" sx={{ fontWeight: 600, textAlign: 'center', mb: '8px' }}>Sign In</Typography>
<form onSubmit={(e) => onSubmit(e)}>
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
{error && <Alert severity="error" sx={{ ...textFieldStyle, my: '10px' }}>{error}</Alert>}
<TextField autoFocus margin="normal" inputRef={emailRef} type="email" id="email" label="Email" sx={textFieldStyle} />
<TextField margin="normal" inputRef={passwordRef} type="password" id="password" label="Password" sx={textFieldStyle} />
<LoadingButton
sx={{ mt: '16px', mb: '4px', width: '150px' }}
loading={loading}
variant="contained"
type="submit"
>
Sign In
</LoadingButton>
</Box>
</form>
<Typography variant="subtitle1" align="center" sx={{ mt: '10px' }}>
<Link to="/forgot-password">Forgot Password?</Link>
</Typography>
</Card>
<Box>
<Typography variant="subtitle1" align="center" sx={{ mt: '10px' }}>
Need an account?
{' '}
<Link to="/SignUp">Sign Up</Link>
</Typography>
</Box>
</Box>
);
}
According to the documentation, you must pass in a disabled prop to the LoadingButton component. Set it to be the same as your loading state.
<LoadingButton
sx={{ mt: '16px', mb: '4px', width: '150px' }}
loading={loading}
disabled={loading}
variant="contained"
type="submit"
>
Sign In
</LoadingButton>

Get All TextField values from loop in Next.js when I press Submit button

first of all look at these below screenshots:
There are two tasks which I want to achieve:
There are two questions shown on the page using the array map method, by default I'm showing only one question, and when I press the next part button the second question will appear with the same question and a TextField (multiline). Now I've implemented a word counter in TextField but when I type something in 1st question the counter works properly. But when I go to the next question the here counter shows the previous question's word counter value, I want them to separately work for both questions.
When I click on the next part and again when I click on the previous part then the values from TextField are removed automatically. I want the values there if I navigate to the previous and next part questions. Also, I want to get both TextField values for a form submission when I press the Submit Test button.
Below are my codes for this page. I'm using Next.js and MUI
import { Grid, Typography, Box, NoSsr, TextField } from '#mui/material';
import PersonIcon from '#mui/icons-material/Person';
import Timer from '../../../components/timer';
import Button from '#mui/material/Button';
import { useState } from 'react';
import ArrowForwardIosIcon from '#mui/icons-material/ArrowForwardIos';
import { Scrollbars } from 'react-custom-scrollbars';
import AppBar from '#mui/material/AppBar';
import Toolbar from '#mui/material/Toolbar';
import ArrowBackIosIcon from '#mui/icons-material/ArrowBackIos';
import axios from '../../../lib/axios';
import { decode } from 'html-entities';
import { blueGrey } from '#mui/material/colors';
export default function Writing({ questions }) {
const [show, setShow] = useState(false);
const [show1, setShow1] = useState(true);
const [showQuestionCounter, setShowQuestionCounter] = useState(0);
const [wordsCount, setWordsCount] = useState(0);
return (
<>
<Box sx={{ flexGrow: 1 }}>
<AppBar position="fixed" style={{ background: blueGrey[900] }}>
<Toolbar>
<Grid container spacing={2} alignItems="center">
<Grid item xs={4} display="flex" alignItems="center">
<PersonIcon
sx={{ background: '#f2f2f2', borderRadius: '50px' }}
/>
<Typography variant="h6" color="#f2f2f2" ml={1}>
xxxxx xxxxx-1234
</Typography>
</Grid>
<Grid item xs={4} container justifyContent="center">
<Timer timeValue={2400} />
</Grid>
<Grid item xs={4} container justifyContent={'right'}>
<Button
variant="contained"
style={{ background: 'white', color: 'black' }}
size="small">
Settings
</Button>
<Button
variant="contained"
style={{
background: 'white',
color: 'black',
margin: '0px 10px',
}}
size="small">
Hide
</Button>
<Button
variant="contained"
style={{ background: 'white', color: 'black' }}
size="small">
Help
</Button>
</Grid>
</Grid>
</Toolbar>
</AppBar>
</Box>
<Box
sx={{
background: blueGrey[50],
height: '100%',
width: '100%',
position: 'absolute',
}}
pt={{ xs: 13, sm: 11, md: 10, lg: 11, xl: 11 }}>
{questions.map((question, index) =>
index === showQuestionCounter ? (
<Box
key={question.id}
px={3}
sx={{ background: '#f2f2f2', pb: 4 }}
position={{
xs: 'sticky',
sm: 'sticky',
lg: 'initial',
md: 'initial',
xl: 'initial',
}}>
<Box
style={{ background: '#f7fcff', borderRadius: '4px' }}
py={1}
px={2}>
<Box>
<Typography variant="h6" component="h6" ml={1}>
Part {question.id}
</Typography>
<Typography variant="subtitle2" component="div" ml={1} mt={1}>
<NoSsr>
<div
dangerouslySetInnerHTML={{
__html: decode(question.questions[0].question, {
level: 'html5',
}),
}}></div>
</NoSsr>
</Typography>
</Box>
</Box>
<Box
style={{
background: '#f7fcff',
borderRadius: '4px',
marginBottom: '75px',
}}
pt={1}
px={3}
mt={{ xs: 2, sm: 2, md: 2, lg: 0, xl: 3 }}>
<Grid container spacing={2}>
<Grid item xs={12} sm={12} lg={6} md={6} xl={6}>
<Box
py={{ lg: 1, md: 1, xl: 1 }}
style={{ height: '50vh' }}>
<Scrollbars universal>
<Typography
variant="body1"
component="div"
style={{ textAlign: 'justify' }}
mr={2}>
<NoSsr>
<div
dangerouslySetInnerHTML={{
__html: decode(question.question_text, {
level: 'html5',
}),
}}></div>
</NoSsr>
</Typography>
</Scrollbars>
</Box>
</Grid>
<Grid
item
xs={12}
sm={12}
lg={6}
md={6}
xl={6}
mt={{ md: 4, lg: 4, xl: 4 }}>
<TextField
id={`${question.id}`}
label="Type your answer here"
multiline
name={`answer_${question.id}`}
rows={12}
variant="outlined"
fullWidth
helperText={`Words Count: ${wordsCount}`}
onChange={(e) => {
setWordsCount(
e.target.value.trim().split(/\s+/).length
);
}}
/>
</Grid>
</Grid>
</Box>
</Box>
) : null
)}
<Box sx={{ position: 'fixed', width: '100%', left: 0, bottom: 0 }}>
<Grid
container
style={{ background: blueGrey[300], display: 'flex' }}
py={2}
px={3}>
<Grid
item
xs={3}
sm={3}
lg={6}
md={6}
xl={6}
container
justifyContent={'start'}>
<Button
variant="contained"
style={{ background: 'white', color: 'black' }}
size="small">
Save Draft
</Button>
</Grid>
<Grid
item
xs={9}
sm={9}
lg={6}
md={6}
xl={6}
container
justifyContent={'end'}>
<Button
variant="contained"
size="small"
style={{
background: 'white',
color: 'black',
visibility: show1 ? 'visible' : 'hidden',
}}
endIcon={<ArrowForwardIosIcon />}
onClick={() => {
setShow((prev) => !prev);
setShowQuestionCounter(showQuestionCounter + 1);
setShow1((s) => !s);
}}>
Next Part
</Button>
{show && (
<>
<Box>
<Button
variant="contained"
style={{
background: 'white',
color: 'black',
margin: '0 10px',
visibility: show ? 'visible' : 'hidden',
}}
startIcon={<ArrowBackIosIcon />}
size="small"
onClick={() => {
setShow1((s) => !s);
setShowQuestionCounter(showQuestionCounter - 1);
setShow((prev) => !prev);
}}>
previous Part
</Button>
<Button variant="contained" color="success">
Submit Test
</Button>
</Box>
</>
)}
</Grid>
</Grid>
</Box>
</Box>
</>
);
}
export async function getServerSideProps(context) {
const { id } = context.query;
const token = context.req.cookies.token;
if (!token) {
context.res.writeHead(302, {
Location: '/',
});
context.res.end();
}
const res = await axios.get(`test/${id}/questions`, {
headers: {
Authorization: `Bearer ${token}`,
},
});
if (res.data.success) {
return {
props: {
questions: res.data.data.questions,
},
};
}
}
The wordsCount state is shared between both questions, which means that when you go to the next question the state remains unchanged and shows the wordsCount from the first question. To solve it, each question needs to have it's own state which you can do by creating a Question component and mapping over it:
export default function Question({ question }) {
const [wordsCount, setWordsCount] = useState(0)
return (
<Box
// ...
>
{/* ... */}
<TextField
// ...
helperText={`${wordsCount} words`}
onChange={(e) => {
setWordsCount(e.target.value.trim().split(/\s+/).length)
}}
/>
{/* ... */}
</Box>
)
}
Then map over it:
{questions.map((question, index) =>
index === showQuestionCounter ? (
<Question key={question.id} question={question} />
) : null
)}
Currently, the value of TextField gets reset you the component is unmounted (i.e. when you go to the next question). You need to make the TextField component a controlled component, meaning that you store the value of the field in useState. And if you need to submit the value of TextField later, then you probably need to store the values in the parent component:
export default function Writing({ questions }) {
// ...
const [answers, setAnswers] = useState([])
function handleChange(id, answer) {
// copy the current answers
let newAnswers = [...answers]
// find the index of the id of the answer in the current answers
const index = newAnswers.findIndex((item) => item.id === id)
// if the answer does exist, replace the previous answer with the new one, else add the new answer
if (index) {
newAnswers[index] = { id, answer }
setAnswers(newAnswers)
} else {
setAnswers([...answers, { id, answer }])
}
}
return (
<>
{/* ... */}
{questions.map((question, index) =>
index === showQuestionCounter ? (
<Question
key={question.id}
question={question}
value={answers.find((item) => item.id === question.id)?.answer || ''}
handleChange={handleChange}
/>
) : null
)}
</>
}
In the Question component, add the handler.
export default function Question({ question, value, handleInputChange }) {
const [wordsCount, setWordsCount] = useState(0)
return (
<Box>
{/* ... */}
<TextField
helperText={`${wordsCount} words`}
value={value}
onChange={(e) => {
handleInputChange(question.id, e.target.value)
setWordsCount(e.target.value.trim().split(/\s+/).length)
}}
/>
{/* ... */}
</Box>
)
}
In the parent component (Writing) you should be able to use the values for form submission.

Automatically Changing State on a React App

I'm looking to change the way I manage state in my app.
Currently, I am using a mapped component which when selected, will set the index of the card and then use this index to colour the component background blue.
This is great! And it works, however to change from e.g card 1 to 2, I need to tap on card 1 again to set index to 0, then select card 2. I do not know how to change the function so if selected outside the container, set index =0, then set index=1, per a conventional app.
I am managing as such:
const [isSelected, setIsSelected] = useState("");
function handleParamChange(e) {
e.preventDefault();
const param = e.target.name //name may be desc
const value = e.target.value
setParams(prevParams => {
return { ...prevParams, [param]: value}
})
}
With a mapped component of:
{
jobs.length > 0 &&
jobs.map(
(job, index) =>
<JobCard
key={job.id}
job={job}
index={index + 1}
isSelected={isSelected}
setIsSelected={setIsSelected}
/>)
}
const JobCard = ({ setIsSelected, isSelected, index, job }) => {
const [open, setOpen] = useState(false)
const [isActive, setIsActive] = useState(false);
console.log(isSelected)
return (
<CardContainer>
{/* BELOW WORKING */}
{/* <CardPrimary onClick={() => setIsSelected(true)} className={isSelected ? "css-class-to-highlight-div" : undefined}> */}
<CardPrimary
onClick={() => {
if (!isSelected) {
setIsSelected(index);
setIsActive(true);
} else if (isSelected === index) {
setIsSelected("");
setIsActive(false);
}
}}
style={{
backgroundColor: isActive ? "#0062ff" : "inherit",
display: "flex",
height: "90%",
width: "95%",
borderRadius: "10px",
justifyContent: "center",
flexDirection: "column",
boxShadow: "0px 4px 10px rgba(0, 0, 0, 0.25)"
}}
>
<CardHeader>
<CardHeaderTopRow>
<Typography variant = "cardheader1" color={isActive ? "white" : "inherit"}>
{job.title}
</Typography>
<HeartDiv>
<IconButton color={open ? "error" : "buttoncol"} sx={{ boxShadow: 3}} fontSize ="2px" size="small"
onClick={()=> setOpen(prevOpen => !prevOpen)}>
<FavoriteIcon fontSize="inherit"
/>
</IconButton>
</HeartDiv>
</CardHeaderTopRow>
<Typography variant = "subtitle4" color={isActive ? "#d6d6d6" : "text.secondary"}>
{job.company.display_name}
</Typography>
</CardHeader>
</CardContainer>
)
}
export default JobCard
This all works.
And I have no issues, I just want to improve it. So How would you implement react hooks and a ref to automatically assign the state and change the isSelected index based on clicks?
If i understand correct, you only want one card to be selected and also the isActive inside the JobCard should be in sync with the isSelected you pass.
If so, you only need one state variable and to be safe, it should really be the job.id and not the index.
So, instead of const [isSelected, setIsSelected] = useState(""); you should rename it to something like const [selectedJob, setSelectedJob] = useState();
and also handle the toggle click here
const toggleJob = useCallback((jobId)=>{
setSelectedJob( (currentJobId) => currentJobId === jobId ? null : jobId );
},[]);
Then
{
jobs.length > 0 &&
jobs.map(
(job, index) =>
<JobCard
key={job.id}
job={job}
isSelected={job.id === selectedJob}
toggleJob={toggleJob}
/>)
}
and finally
const JobCard = ({ toggleJob, isSelected, job }) => {
const [open, setOpen] = useState(false)
const handleCardClick = useCallback(()=>{
toggleJob(job.id);
},[toggleJob, job])
return (
<CardContainer>
<CardPrimary
onClick={handleCardClick}
style={{
backgroundColor: isSelected ? "#0062ff" : "inherit",
display: "flex",
height: "90%",
width: "95%",
borderRadius: "10px",
justifyContent: "center",
flexDirection: "column",
boxShadow: "0px 4px 10px rgba(0, 0, 0, 0.25)"
}}
>
<CardHeader>
<CardHeaderTopRow>
<Typography variant = "cardheader1" color={isSelected ? "white" : "inherit"}>
{job.title}
</Typography>
<HeartDiv>
<IconButton color={open ? "error" : "buttoncol"} sx={{ boxShadow: 3}} fontSize ="2px" size="small"
onClick={()=> setOpen(prevOpen => !prevOpen)}>
<FavoriteIcon fontSize="inherit"
/>
</IconButton>
</HeartDiv>
</CardHeaderTopRow>
<Typography variant = "subtitle4" color={isSelected ? "#d6d6d6" : "text.secondary"}>
{job.company.display_name}
</Typography>
</CardHeader>
</CardContainer>
)
}
export default JobCard

How to change the position of material-ui's dialog?

Using material-ui in my react app, is there a way I can change the position when the dialog is opened? now it's always centered.
Thanks in advance!
You can create styles and pass it through classes prop. Here is an example of how you could do that.
import React from 'react';
import { makeStyles, Dialog } from '#material-ui/core';
const useStyles = makeStyles({
dialog: {
position: 'absolute',
left: 10,
top: 50
}
});
function Example() {
const classes = useStyles();
return (
<Dialog
classes={{
paper: classes.dialog
}}
/* rest of the props */
>
{/* content of the dialog */}
</Dialog>
);
}
I would say don't use position: absolute, it could break the scrolling behavior. The position was control differently with scroll='paper' or scroll='body'
You can use the following code to always align your dialog to the top of the page with two custom classes.
Demo: codesandbox.io
See the original article with explanation
const useStyles = makeStyles({
topScrollPaper: {
alignItems: 'flex-start',
},
topPaperScrollBody: {
verticalAlign: 'top',
},
})
function SimpleDialog(props: SimpleDialogProps) {
const classes = useStyles()
return (
<Dialog
onClose={handleClose}
aria-labelledby="simple-dialog-title"
open={open}
scroll="paper"
classes={{
scrollPaper: classes.topScrollPaper,
paperScrollBody: classes.topPaperScrollBody,
}}
></Dialog>
)
}
For MUI 5, we can use both SxProps and styled() utility:
Via SxProps:
// flex-start: to position it at the top
// flex-end: to position it at the bottom
// center: to position it at the center
const sx: SxProps = {
"& .MuiDialog-container": {
alignItems: "flex-start"
}
};
<Dialog
open={infoModalOpen}
onClose={() => setInfoModalOpen(false)}
sx={sx}
scroll="paper"
>
....
</Dialog>
Via styled components:
const StyledDialog = styled(Dialog)(({theme}) => ({
"& .MuiDialog-container": {
alignItems: "flex-start"
}
}));
<StyledDialog
open={infoModalOpen}
onClose={() => setInfoModalOpen(false)}
sx={sx}
scroll="paper"
>
....
</StyledDialog>
#radovix's and #Sumit Wadhwa's answers are correct. In case you don't want to use makeStyles, Here is how I resolved it.
When scroll="body";
<Dialog
fullWidth
onClose={() => setOpen(false)}
open={true}
scroll="body"
PaperProps={{ sx: { mt: "50px", verticalAlign: "top" } }}
>
{/* Dialog Content here */}
</Dialog>;
When scroll="paper";
<Dialog
fullWidth
onClose={() => setOpen(false)}
open={true}
scroll="paper"
sx={{
"& .MuiDialog-container": {
alignItems: "flex-start",
},
}}
PaperProps={{ sx: { mt: "50px" } }}
>
{/* Dialog Content here */}
</Dialog>;
You can adjust the margin-top however you like

Resources