alternativeLabel in Stepper component using material UI - reactjs

I'm learning Material UI. I am struggling with one problem.
I am using Stepper component and for mobile devices I want add props alternativeLabel which will make the labels will be under the circle.
I got lost. Could someone show me how to achieve it ?
Thank you for any help ! ❤️
export default function ProgressBarStepper({ step }) {
const classes = useStyles();
const [activeStep, setActiveStep] = React.useState(step);
const [skipped, setSkipped] = React.useState(new Set());
const isStepSkipped = (step) => {
return skipped.has(step);
};
// how to have alternativeLabel only for mobile ??
//connector={false}
return (
<Box sx={{ width: "100%" }}>
<Stepper activeStep={activeStep} connector={<QontoConnector />} >
{steps.map((label, index) => {
const stepProps = {};
const labelProps = {};
if (isStepSkipped(index)) {
stepProps.completed = false;
}
return (
<Step
sx={{
"& .MuiStepLabel-root .Mui-completed": {
fontWeight: "700",
},
}}
key={label}
{...stepProps}
>
<StepLabel
className={classes.boolspac}
sx={{ fontWeight: "700", padding: "0" }}
{...labelProps}
>
{label}
</StepLabel>
</Step>
);
})}
</Stepper>
</Box>
);
}

Related

Path aware side navigation using collapsible nested lists in React/NextJS

I've coded a collapsible nested navigation for my website. A menu object is iterated over to create the sidebar as it appears in the image. Items with children can be collapsed and this is also performing flawlessly. I am having trouble with implementing a feature which would automatically collapse all parents of an item if a user navigates to it. So in the image I added, if a user navigates to "/nastava/studiranje" from the main meenu or the "menu" on the right side of the creen (or receives a direct link to that location) "Nastava" and "Studiranje" lists should be collapsed.
I thought the right approach is to get the current route by using pathname method on the router object, passing the route to MultiLevel component and checking if it matches to the current passed item link. But it just doesn't seem to work that way.
Here is the code so far.
const SideMenu = () => {
const router = useRouter();
//menu is imported and sorted in createDataTree function
const [menu, setMenu] = useState([]);
const [openItems, setOpenItems] = useState();
const routerPathname = router.pathname;
useEffect(() => {
const pathnames = routerPathname.split('/').filter((x) => x);
setOpenItems(pathnames);
const theMenu = createDataTree(mainMenu.nodes);
setMenu(theMenu.filter((item) => item.url === '/' + pathnames[0]));
}, [routerPathname]);
return (
<Box>
//menu[0] to get from main menu object into "Nastava" child
{menu[0]?.childNodes.map((item, key) => (
<MenuItem key={key} item={item} openItems={openItems} />
))}
</Box>
);
};
export default SideMenu;
MenuItem.jsx
import MultiLevel from './MultiLevel';
import SingleLevel from './SingleLevel';
const hasChildren = (item) => {
const { childNodes: children } = item;
if (children === undefined) {
return false;
}
if (children.constructor !== Array) {
return false;
}
if (children.length === 0) {
return false;
}
return true;
};
const MenuItem = ({ item }) => {
const Component = hasChildren(item) ? MultiLevel : SingleLevel;
return <Component item={item} />;
};
export default MenuItem;
SingleLevel.jsx
import { ListItem } from '#mui/material';
import HeaderLink from '../../Elements/HeaderLink/HeaderLink';
const SingleLevel = ({ item, active }) => {
return (
<ListItem sx={{ listStyle: 'none', lineHeight: '0.5rem', borderColor: 'text.default' }}>
<HeaderLink href={item.url} sx={{ color: 'text.primary', '&:hover': { paddingLeft: '5px' }, fontWeight: '600' }}>
{item.label}
</HeaderLink>
</ListItem>
);
};
export default SingleLevel;
MultiLevel.jsx
import { Box, Collapse, List, ListItem } from '#mui/material';
import HeaderLink from '../../Elements/HeaderLink/HeaderLink';
import ExpandLess from '#mui/icons-material/ExpandLess';
import ExpandMore from '#mui/icons-material/ExpandMore';
import MenuItem from './MenuItem';
import { useState } from 'react';
const MultiLevel = ({ item }) => {
const { childNodes: children } = item;
const [open, setOpen] = useState(false);
const handleClick = () => {
setOpen((prev) => !prev);
};
return (
<>
<ListItem button sx={{ listStyle: 'none', lineHeight: '0.5rem', borderColor: 'text.default' }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', maxWidth: '200px', alignItems: 'center' }}>
<HeaderLink href={item.url} sx={{ color: 'text.primary', '&:hover': { paddingLeft: '5px' }, fontWeight: '600' }}>
{item.label}
</HeaderLink>
{open ? <ExpandLess onClick={handleClick} /> : <ExpandMore onClick={handleClick} />}
</Box>
</ListItem>
<Collapse in={open} timeout="auto" unmountOnExit>
<List component="div" disablePadding>
{children.map((child, key) => (
<MenuItem key={key} item={child} />
))}
</List>
</Collapse>
</>
);
};
export default MultiLevel;
This is an approximation of the menu object.
const menu = [
{
label: 'Nastava',
url:'/nastava',
childNodes: [
{
label:'Studiranje',
url:'/nastava/studiranje'
childNodes: [
{
label: 'Raspored',
url: '/nastava/studiranje/raspored'
},
{
label: 'Konzultacije',
url: '/nastava/studiranje/konzultacije'
},
......
],
{
label:'Upisi'
url:'/nastava/upisi'
},
{
label:'Studiji',
url:'/nastava/studiji',
childNodes:[
{
label:'Preddiplomski studiji',
url:'/nastava/studiji/preddiplomski-studiji'
},
{
label:'Diplomski studiji',
url:'/nastava/studiji/diplomski-studiji'
}
]
}
},
]
}

Overriding MUI Stepper

I need to change a Mui Stepper ( which the code works perfetly )
but what I need is a bit different ,
Instead of having this :
I want to get the text under the icon and instead of having a line between tow steps I prefer to have a '<'
Here is the code :
import Box from '#mui/material/Box';
import Stepper from '#mui/material/Stepper';
import Step from '#mui/material/Step';
import StepButton from '#mui/material/StepButton';
import Button from '#mui/material/Button';
import Typography from '#mui/material/Typography';
const steps = ['Select campaign settings', 'Create an ad group', 'Create an ad'];
export default function HorizontalNonLinearStepper() {
const [activeStep, setActiveStep] = React.useState(0);
const [completed, setCompleted] = React.useState<{
[k: number]: boolean;
}>({});
const totalSteps = () => {
return steps.length;
};
const completedSteps = () => {
return Object.keys(completed).length;
};
const isLastStep = () => {
return activeStep === totalSteps() - 1;
};
const allStepsCompleted = () => {
return completedSteps() === totalSteps();
};
const handleNext = () => {
const newActiveStep =
isLastStep() && !allStepsCompleted()
? // It's the last step, but not all steps have been completed,
// find the first step that has been completed
steps.findIndex((step, i) => !(i in completed))
: activeStep + 1;
setActiveStep(newActiveStep);
};
const handleBack = () => {
setActiveStep((prevActiveStep) => prevActiveStep - 1);
};
const handleStep = (step: number) => () => {
setActiveStep(step);
};
const handleComplete = () => {
const newCompleted = completed;
newCompleted[activeStep] = true;
setCompleted(newCompleted);
handleNext();
};
const handleReset = () => {
setActiveStep(0);
setCompleted({});
};
return (
<Box sx={{ width: '100%' }}>
<Stepper nonLinear activeStep={activeStep}>
{steps.map((label, index) => (
<Step key={label} completed={completed[index]}>
<StepButton color="inherit" onClick={handleStep(index)}>
{label}
</StepButton>
</Step>
))}
</Stepper>
<div>
{allStepsCompleted() ? (
<React.Fragment>
<Typography sx={{ mt: 2, mb: 1 }}>
All steps completed - you&apos;re finished
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'row', pt: 2 }}>
<Box sx={{ flex: '1 1 auto' }} />
<Button onClick={handleReset}>Reset</Button>
</Box>
</React.Fragment>
) : (
<React.Fragment>
<Typography sx={{ mt: 2, mb: 1 }}>Step {activeStep + 1}</Typography>
<Box sx={{ display: 'flex', flexDirection: 'row', pt: 2 }}>
<Button
color="inherit"
disabled={activeStep === 0}
onClick={handleBack}
sx={{ mr: 1 }}
>
Back
</Button>
<Box sx={{ flex: '1 1 auto' }} />
<Button onClick={handleNext} sx={{ mr: 1 }}>
Next
</Button>
{activeStep !== steps.length &&
(completed[activeStep] ? (
<Typography variant="caption" sx={{ display: 'inline-block' }}>
Step {activeStep + 1} already completed
</Typography>
) : (
<Button onClick={handleComplete}>
{completedSteps() === totalSteps() - 1
? 'Finish'
: 'Complete Step'}
</Button>
))}
</Box>
</React.Fragment>
)}
</div>
</Box>
);
}
Is there a way to override the MUI Stepper styles ?
Thank you in advance
Okay so basically it was 2 steps. The first one was to make the labels appear below the icons which was relatively easy.
I had to add alternativeLabel as a prop to the <Stepper />.
The next step was to remove the lines and replace them with < which wasn't straightforward. I did that by styling the .MuiStepConnector class, replacing its content and removing the border.
<Stepper
nonLinear
alternativeLabel
activeStep={activeStep}
sx={{
".MuiStepConnector-root": {
top: 0
},
".MuiStepConnector-root span": {
borderColor: "transparent"
},
".MuiStepConnector-root span::before": {
display: "flex",
justifyContent: "center",
content: '"<"'
}
}}
>
This is the result:

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

ClickAwayListener component not working with DragDropContext

I made a dropdown using Button and a Popper using Material UI components where you can click on the button and get a list of subjects to choose from. To make the popper disappear either we can click on the button again or use a <ClickAwayListener> component which listens to click event and closes the Popper. Now I've to make the list capable of drag and drop feature so I use the react-beautiful-dnd npm package.But the <ClickAwayListener> doesn't seem to work when I include <DragDropContext>, <Droppable> and <Draggable> components.Can anyone help me figure it out?
Here's the code without drag and drop feature. CodeSandbox link https://codesandbox.io/s/gallant-newton-mfmhd?file=/demo.js
const subjectsFromBackend = [
{ name: "Physics", selected: false },
{ name: "Chemistry", selected: false },
{ name: "Biology", selected: false },
{ name: "Mathematics", selected: false },
{ name: "Computer Science", selected: false },
];
const useStyles = makeStyles((theme) => ({
root: {
display: "flex"
},
paper: {
marginRight: theme.spacing(2)
}
}));
export default function MenuListComposition() {
const classes = useStyles();
const [open, setOpen] = React.useState(false);
const anchorRef = React.useRef(null);
const [subjects, setSubjects] = React.useState(subjectsFromBackend);
const handleToggle = () => {
setOpen(!open);
};
const handleClose = () => {
setOpen(false);
};
const ColumnItem = ({ subjectName, selected }) => {
return (
<>
<Grid container>
<Grid item>
<Checkbox checked={selected} />
</Grid>
<Grid item>{subjectName}</Grid>
</Grid>
</>
);
};
return (
<div className={classes.root}>
<div>
<Button
ref={anchorRef}
onClick={handleToggle}
style={{ width: 385, justifyContent: "left", textTransform: "none" }}
>
{`Subjects Selected`}
</Button>
<Popper
open={open}
anchorEl={anchorRef.current}
role={undefined}
transition
disablePortal
>
{({ TransitionProps, placement }) => (
<Grow
{...TransitionProps}
style={{
transformOrigin:
placement === "bottom" ? "center top" : "center bottom"
}}
>
<Paper style={{ maxHeight: 200, overflow: "auto", width: 385 }}>
<ClickAwayListener onClickAway={handleClose}>
<List>
{subjects.map((col, index) => (
<ListItem>
<ColumnItem
subjectName={col.name}
selected={col.selected}
/>
</ListItem>
))}
</List>
</ClickAwayListener>
</Paper>
</Grow>
)}
</Popper>
</div>
</div>
);
}
Here's the same code using drag and drop. CodeSandbox Link https://codesandbox.io/s/material-demo-forked-ertti
const subjectsFromBackend = [
{ name: "Physics", selected: false },
{ name: "Chemistry", selected: false },
{ name: "Biology", selected: false },
{ name: "Mathematics", selected: false },
{ name: "Computer Science", selected: false },
];
const useStyles = makeStyles((theme) => ({
root: {
display: "flex"
},
paper: {
marginRight: theme.spacing(2)
}
}));
export default function MenuListComposition() {
const classes = useStyles();
const [open, setOpen] = React.useState(false);
const anchorRef = React.useRef(null);
const [subjects, setSubjects] = React.useState(subjectsFromBackend);
const handleToggle = () => {
setOpen(!open);
};
const handleClose = () => {
setOpen(false);
};
const ColumnItem = ({ subjectName, selected }) => {
return (
<>
<Grid container>
<Grid item>
<Checkbox checked={selected} />
</Grid>
<Grid item>{subjectName}</Grid>
</Grid>
</>
);
};
const onDragEnd = (result, subjects, setSubjects) => {
const { source, destination } = result;
if (!destination) return;
if (source.droppableId !== destination.droppableId) return;
const copiedItems = [...subjects];
const [removed] = copiedItems.splice(source.index, 1);
copiedItems.splice(destination.index, 0, removed);
setSubjects(copiedItems);
};
return (
<div className={classes.root}>
<div>
<Button
ref={anchorRef}
onClick={handleToggle}
style={{ width: 385, justifyContent: "left", textTransform: "none" }}
>
{`Subjects Selected`}
</Button>
<Popper
open={open}
anchorEl={anchorRef.current}
role={undefined}
transition
disablePortal
>
{({ TransitionProps, placement }) => (
<Grow
{...TransitionProps}
style={{
transformOrigin:
placement === "bottom" ? "center top" : "center bottom"
}}
>
<DragDropContext
onDragEnd={(res) => onDragEnd(res, subjects, setSubjects)}
>
<Paper style={{ maxHeight: 200, overflow: "auto", width: 385 }}>
<ClickAwayListener onClickAway={handleClose}>
<Droppable droppableId={"subjectsColumn"}>
{(provided, snapshot) => (
<div
{...provided.droppableProps}
ref={provided.innerRef}
>
<List>
{subjects.map((col, index) => (
<Draggable
key={col.name}
draggableId={col.name}
index={index}
>
{(provided, snapshot) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
>
<ListItem>
<ColumnItem
subjectName={col.name}
selected={col.selected}
/>
</ListItem>
</div>
)}
</Draggable>
))}
</List>
{provided.placeholder}
</div>
)}
</Droppable>
</ClickAwayListener>
</Paper>
</DragDropContext>
</Grow>
)}
</Popper>
</div>
</div>
);
}
I found out that the ClickAwayListener's child component should be wrapped around a div so that the click event could be triggered properly.
You need to have your ClickAwayListener at the top when you are using the Drag and Drop.
return (
<ClickAwayListener
onClickAway={() => {
console.log("i got called");
handleClose();
}}
>
.....
</ClickAwayListener>
Here is the working codesandbox - https://codesandbox.io/s/material-demo-forked-h1o1s?file=/demo.js:4877-4897

Undefined is not an object in React Native project but console.log shows value

I want to make transition view in my React Native project. In Picture component I always get error: undefined is not an object (evaluating 'info.images[0]'). When I console.log info.images[0] or info.images[0].image I got normal link for picture not undefined.
My FavouritePlaces.tsx component:
const FavouritePlaces = ({navigation}: HomeNavigationProps<"FavouritePlaces">) => {
const[markers, setMarkers] = useState([]);
useEffect(() => {
const getFavourites = async () => {
let keys = []
try {
keys = await AsyncStorage.getAllKeys()
} catch (e) {
// read key error
}
let values
try {
values = await AsyncStorage.multiGet(keys)
setMarkers(values)
console.log('ValuesFromAsyncStorage', values)
} catch(e) {
// read error
}
}
getFavourites();
}, [])
const transition = (
<Transition.Together>
<Transition.Out type='fade' />
<Transition.In type='fade' />
</Transition.Together>
);
const list = useRef<TransitioningView>(null);
const theme = useTheme()
const width = (wWidth - theme.spacing.m * 3) / 2;
const [footerHeight, setFooterHeight] = useState(0);
return (
<Box flex={1} backgroundColor="background">
<Header
title="Избранные места"
left={{icon: 'menu', onPress: () => navigation.openDrawer()}}
right={{icon: 'shopping-bag', onPress: () => true}}
/>
<Box flex={1}>
<ScrollView contentContainerStyle={{
paddingHorizontal: theme.spacing.m,
paddingBottom: footerHeight
}}>
<Transitioning.View ref={list} transition={transition}>
{markers ?
<Box flexDirection='row'>
<Box marginRight='s'>
{markers
.filter((_, i) => i % 2 !== 0).map((currentMarker) => <Picture key={currentMarker}
place={currentMarker}
width={width}/>)}
</Box>
<Box>
{markers
.filter((_, i) => i % 2 === 0).map((currentMarker) => <Picture key={currentMarker}
place={currentMarker}
width={width}/>)}
</Box>
</Box> : undefined}
</Transitioning.View>
</ScrollView>
<TopCurve footerHeight={footerHeight}/>
<Box position='absolute' bottom={0} left={0} right={0} onLayout={({
nativeEvent: {
layout: {height},
}
}) => setFooterHeight(height)}>
<Footer label="Удалить из избранного" onPress={() => {
list.current?.animateNextTransition();
// setPlaces(places.filter((place => !place.selected)))
// console.log(defaultPictures)
}}/>
</Box>
</Box>
</Box>
)
}
export default FavouritePlaces
Picture component:
const Picture = ({
place,
width
}: PictureProps) => {
const info = JSON.parse(place[1])
console.log('currentInfo', info.images[0].image)
const [selected, setSelected] = useState(false);
return (
<BorderlessTap onPress={() => {
setSelected(prev => !prev);
place.selected = !place.selected;
}}>
{/*<Text>{place.description}</Text>*/}
{info.images[0].image ?
<ImageBackground
style={{backgroundColor: 'black', borderRadius: '3%', marginBottom: '3%', alignItems: 'flex-end', padding: '3%', width, height: 80}}
source={{ uri: info.images[0].image }}>
{selected && (
<RoundedIcon
backgroundColor="primary"
color="background"
size={24} name="check"
/>
)}
</ImageBackground> : undefined}
</BorderlessTap>
)
}
export default Picture
You are rendering the Picture component before getFavourites has populated markers.
Your logic markers ? ... : undefined is always going to be truthy because [] evaluates to true. You should do something like markers.length ? ... : undefined.

Resources