Show/hide element at specific index with react js - javascript

I am displaying cards from an array of objects with map, and I would like to show only onMouse enter some icons, but only on the card where mouse is hover. Right now, icons show on all card
const onMousseEnter = (index) => {
if (index === interestsList[index].id) {
setVisibility(true);
}
};
{interestsList
.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0))
.map((el, index) => (
<InterestCard
key={index}
onMouseEnter={() => onMousseEnter(index)}
onMouseLeave={() => onMouseLeave(index)}>
<CategoryLeftCol onClick={openCatPage(index)}>
<SurfingRoundedIcon sx={{ fontSize: 22 }} />
<Typography variant="h6" fontWeight="bold">
{el.name}
</Typography>
</CategoryLeftCol>
{visible && (
<CategoryRightCol>
<IconButton onClick={() => setOpen(true)}>
<Edit sx={{ fontSize: 14 }} />
</IconButton>
<IconButton onClick={() => setOpenAlert(true)}>
<Delete sx={{ fontSize: 14 }} />
</IconButton>
</CategoryRightCol>
)}
I can get the index of the card where mouse is hover but problem persist...

className="interestCard" // for <InterestCard/>
...
className="interestCard_icons" // for <CategoryRightCol/> i guess
css:
.interestCard_icons {
opacity: 0;
pointer-events: none;
transition: opacity 0.3s;
}
.interestCard:hover .interestCard_icons {
opacity: 1;
pointer-events: auto;
}

try to create a new Component for using on each index of interestsList :
import { useState } from "react";
{interestsList
.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0))
.map((el, index) => (
<ChildComponent
index={index}
el={el}
key={index}
/>
)}
function ChildComponent({el,index}){
const [visible,setVisibility] = useState(false);
const onMousseEnter = (index) => {
if (index === interestsList[index].id) {
setVisibility(true);
}
};
return (<InterestCard
key={index}
onMouseEnter={() => onMousseEnter(index)}
onMouseLeave={() => onMouseLeave(index)}>
<CategoryLeftCol onClick={openCatPage(index)}>
<SurfingRoundedIcon sx={{ fontSize: 22 }} />
<Typography variant="h6" fontWeight="bold">
{el.name}
</Typography>
</CategoryLeftCol>
{visible && (
<CategoryRightCol>
<IconButton onClick={() => setOpen(true)}>
<Edit sx={{ fontSize: 14 }} />
</IconButton>
<IconButton onClick={() => setOpenAlert(true)}>
<Delete sx={{ fontSize: 14 }} />
</IconButton>
</CategoryRightCol>)
}

Related

HTMLAudioElement pause not triggering in ReactJS

I am able to fetch and display audio information, and also to trigger playback but could not pause the media item by running the pause function. At least, it is not receiving an event in "onpause" and the audio keeps running.
Using ref should work as expected. The tested audio is served by https://arweave.net/, if that makes a difference.
Can you spot the issue?
export const Player = (): JSX.Element => {
const [playingAudio, setPlayingAudio] = useState<boolean>(false);
const { fetchMetadata, metadata } = useContract();
const theme: Theme = useTheme();
const audio = useRef<HTMLAudioElement | null>(null);
useEffect(() => {
const updateMetadata = async () => await fetchMetadata();
updateMetadata();
if (metadata) {
audio.current = new Audio(metadata.animation_url);
if (audio.current) {
audio.current.onpause = () => setPlayingAudio(false);
audio.current.onended = () => setPlayingAudio(false);
audio.current.onplay = () => setPlayingAudio(true);
}
}
}, [fetchMetadata, metadata]);
const playAudio = async (): Promise<void> => await audio?.current?.play();
const pauseAudio = (): void => audio?.current?.pause();
return (
<Card sx={{ width: '100%', maxWidth: '600px', display: 'flex', justifyContent: 'space-between', marginY: 3 }}>
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
<CardContent sx={{ flex: '1 0 auto' }}>
<Typography component="div" variant="h5">
{metadata?.name}
</Typography>
<Typography variant="subtitle1" color="text.secondary" component="div">
{metadata?.description}
</Typography>
</CardContent>
<Box sx={{ display: 'flex', alignItems: 'center', pl: 1, pb: 1 }}>
<IconButton aria-label="previous">
{theme.direction === 'rtl' ? <SkipNextIcon /> : <SkipPreviousIcon />}
</IconButton>
{playingAudio ? (
<IconButton aria-label="play/pause" onClick={pauseAudio}>
<PauseCircle sx={{ height: 38, width: 38 }} />
</IconButton>
) : (
<IconButton aria-label="play/pause" onClick={playAudio}>
<PlayArrowIcon sx={{ height: 38, width: 38 }} />
</IconButton>
)}
<IconButton aria-label="next">
{theme.direction === 'rtl' ? <SkipPreviousIcon /> : <SkipNextIcon />}
</IconButton>
</Box>
</Box>
<CardMedia
component="img"
sx={{ width: 200 }}
image={metadata?.image}
alt={metadata?.name}
/>
</Card>
);
};
As #morganney hinted, I should check the presence of the metadata, before updating it again (as it was in a loop), so now no unnecessary re-rendering is happening, and I can pause the audio.
useEffect(() => {
const updateMetadata = async () => await fetchMetadata();
if (!metadata) updateMetadata();
if (metadata) {
audio.current = new Audio(metadata.animation_url);
if (audio.current) {
audio.current.onpause = () => setPlayingAudio(false);
audio.current.onended = () => setPlayingAudio(false);
audio.current.onplay = () => setPlayingAudio(true);
}
}
}, [fetchMetadata, metadata]);
(the rest of the code stays the same)

How to implement drag'n'drop for nested lists in React?

I am creating my sidebar component which is divided by sections. What I want to do, is to implement drag'n'drop functionality. But here is the problem, here is how those sections look like in code:
const sidebarSections = [
{
name: 'General',
routePrefix: '/general',
elements: [
{
name: "Home",
icon: <HomeIcon/>,
href: "/home",
show: true
},
{
name: "Clients",
icon: <SupervisedUserCircleIcon/>,
href: "/clients",
show: true
},
{
name: "ACL",
icon: <ManageAccountsIcon/>,
href: "/acl",
show: true
},
{
name: "Modify roles",
icon: <AddModeratorIcon/>,
href: "/roles",
show: true
},
]
},
...
Every section - in this case General - has its elements. As you probably guess, I want to implement drag'n'drop only for those elements and not for whole sidebarSections.
Here is how this sidebar list looks like in code:
const [sidebar, setSidebar] = React.useState(sidebarSections)
const list = () => (
<Box
sx={{
width: anchor === 'top' || anchor === 'bottom' ? 'auto' : 250,
}}
role="presentation"
onKeyDown={toggleDrawer(anchor, false)}
>
<List>
{sidebarSections.map((section, key) => (
<Box
key={key}
sx={{
marginTop: 2,
}}
>
{section.name ? (
<ListItemText
sx={{
px: 2,
color: 'primary.light',
}}
>
{section.name}
</ListItemText>
) : null}
<List>
{section.elements.map((subsection) =>
subsection.show || settings ? (
<ListItem key={subsection.name}>
{settings ? (
<Checkbox
checked={subsection.show}
onChange={() => {
subsection.show = !subsection.show;
setSidebar((prev) => [...prev]);
}}
/>
) : null}
<Link href={section.routePrefix + subsection.href} passHref>
<Button
sx={{
color: router.pathname.includes(
`${section.routePrefix + subsection.href}`
)
? "secondary.main"
: "primary.main",
width: "100%",
justifyContent: "start"
}}
startIcon={subsection.icon}
>
{subsection.name}
</Button>
</Link>
</ListItem>
) : null
)}
</List>
<Divider
sx={{color: 'primary.light'}}
/>
</Box>
))}
</List>
</Box>
)
I was using this guide while was trying to do this, but, because of nested list I wasn't able to do that.
This is how I solved this problem. Instead of {subsection.name} you can pass your component (Button in my case).
DragDropContext has to be only on list where you want have drag'n'drop enabled. Also I have changed handleDrop function to make it parse nested list and render it more time to show new order of elements.
const handleDrop = (droppedItem) => {
if (!droppedItem.destination) return;
const updatedSidebar = [...sidebar];
updatedSidebar.forEach(section => {
section.elements.forEach(subsection => {
if (droppedItem.draggableId === subsection.name) {
const [reorderedItem] = section.elements.splice(droppedItem.source.index, 1);
section.elements.splice(droppedItem.destination.index, 0, reorderedItem);
}
})
})
setSidebar(updatedSidebar);
};
const list2 = () => (
<Box
sx={{
width: anchor === 'top' || anchor === 'bottom' ? 'auto' : 250,
}}
role="presentation"
onKeyDown={toggleDrawer(anchor, false)}
>
<List>
{sidebarSections.map((section, key) => (
<Box
key={key}
sx={{
marginTop: 2,
}}
>
{section.name ? (
<ListItemText
sx={{
px: 2,
color: 'primary.light',
}}
>
{section.name}
</ListItemText>
) : null}
<List>
<DragDropContext onDragEnd={handleDrop}>
<Droppable droppableId="list-container">
{(provided) => (
<div
className="list-container"
{...provided.droppableProps}
ref={provided.innerRef}
>
{section.elements.map((subsection, index) =>
subsection.show || settings ? (
<ListItem key={subsection.name}>
{settings ? (
<Box>
<Draggable key={subsection.name} draggableId={subsection.name} index={index}>
{(provided) => (
<div
className="item-container"
ref={provided.innerRef}
{...provided.dragHandleProps}
{...provided.draggableProps}
>
{subsection.name}
</div>
)}
</Draggable>
{provided.placeholder}
</Box>
) : null}
{!settings ? (
<Link href={section.routePrefix + subsection.href} passHref>
<Button
sx={{
color: router.pathname.includes(
`${section.routePrefix + subsection.href}`
)
? "secondary.main"
: "primary.main",
width: "100%",
justifyContent: "start"
}}
startIcon={subsection.icon}
>
{subsection.name}
</Button>
</Link>
) : null}
</ListItem>
) : null
)}
</div>
)}
</Droppable>
</DragDropContext>
</List>
<Divider
sx={{color: 'primary.light'}}
/>
</Box>
))}
</List>
</Box>
);

Calling onClick inside map function

I have a custom Input Box and when I type inside a custom input component It'll re-render the typed input inside.
import {
Badge,
Box,
CloseButton,
Grid,
GridItem,
Input,
Text
} from "#chakra-ui/react";
import React, { useEffect, useState } from "react";
function InputTag(props) {
const [tags, setTags] = useState(props.values);
const removeTag = (index) => {
setTags(tags.filter((_, i) => i !== index));
};
const addTag = (event) => {
if (event.target.value !== "") {
setTags([...tags, event.target.value]);
props.setFieldValue("tags", [...tags, event.target.value]);
event.target.value = "";
}
};
useEffect(() => {
props.show === false && setTags([]);
}, [props.show]);
//update values based on click suggestions
useEffect(() => {
setTags([props.values, props.suggTag]);
}, [props.suggTag, props.values]);
return (
<Box
display={"flex"}
border="1px"
borderColor={"gray.200"}
borderRadius={"md"}
padding={2}
>
<Grid templateColumns="repeat(3, 1fr)" gap={2} overflow="visible">
{tags &&
tags.map((tag, index) => (
<GridItem key={index}>
<Badge
variant="solid"
colorScheme={"purple"}
display={"flex"}
borderRadius="full"
justifyContent="space-between"
alignItems="center"
gap={2}
>
<Text>{tag}</Text>
<CloseButton onClick={() => removeTag(index)} />
</Badge>
</GridItem>
))}
</Grid>
<Input
type="text"
name="tags"
id="tags"
variant={"unstyled"}
placeholder="Add Tag"
_placeholder={{ fontsize: "md" }}
onChange={props.handleChange}
onBlur={props.handleBlur}
onError={props.errors}
onKeyUp={(event) =>
event.key === "Enter" ? addTag(event) && event.preventDefault() : null
}
/>
</Box>
);
}
export default InputTag;
Here, when I hit enter It'll render them inside the custom Input Box
I Inserted a custom array of strings as "ex_Tag" inside Previewer.js so that when I click on the word in array, it'll also get rendered inside custom input as well.
function NewUploader({ isOpen, onClose }) {
const cancelRef = useRef();
const ex_tags = ["Design", "Strategy", "Human Centered Design"];
const [show, Setshow] = useState(true);
const [suggTag, setSuggTag] = useState();
const initialValues = {
files: null,
tags: []
};
const validationSchema = yup.object({
files: yup.mixed().required("File is Required"),
tags: yup.mixed().required("tags required")
});
const onSubmit = (values, actions) => {
const formData = new FormData();
formData.append("files", values.files[0]);
formData.append("tags", values.tags);
for (var pair of formData.entries()) {
console.log(pair[0] + ", " + pair[1]);
}
actions.setSubmitting(false);
actions.resetForm();
Setshow(!show);
onClose();
};
const handlethis = (e) => {
e.preventDefault();
};
//insert suggested word to useState so i can pass it to custom input
const handleClick = (tag) => {
setSuggTag(tag);
};
return (
<Modal isOpen={isOpen} onClose={onClose} isCentered>
{/* update code on model here */}
<ModalOverlay />
<ModalContent>
<ModalHeader>
<Text fontWeight={"bold"} color="gray.900">
Upload Buddy
</Text>
</ModalHeader>
<ModalCloseButton />
<ModalBody>
<Flex direction="column" gap={3}>
<Box>
<Text fontWeight={"normal"} color="gray.700">
This learning contentwill not be summarised. to summarize your
content, use{" "}
<Link color={"purple.400"}>Create Knowledge Nugget</Link> option
instead.
</Text>
</Box>
<Box>
<Formik
initialValues={initialValues}
onSubmit={onSubmit}
validationSchema={validationSchema}
>
{(formik) => (
<Form
onSubmit={handlethis}
autoComplete="off"
encType="multipart/form-data"
>
<FormLabel htmlFor="file">
<Text
fontSize="sm"
fontWeight="normal"
color="gray.900"
fontFamily={"body"}
>
Upload files
</Text>
</FormLabel>
{/* drag droop sec */}
{formik.isSubmitting ? (
<>
<Grid
templateColumns="repeat(3, 1fr)"
gap={2}
overflow="hidden"
>
{formik.values.files &&
formik.values.files.map((file, index) => (
<GridItem key={index}>
<Badge
variant="solid"
borderRadius="xl"
colorScheme={"gray"}
w={file.name.length * 4}
h="8"
display="flex"
justifyContent="center"
alignItems="center"
my={2}
>
<Text fontFamily={"body"}>{file.name}</Text>
<CloseButton colorScheme={"blackAlpha"} />
</Badge>
</GridItem>
))}
</Grid>
<Progress colorScheme={"yellow"} isIndeterminate />
</>
) : (
<>
<Dragdrop setFieldValue={formik.setFieldValue} />
<Grid
templateColumns="repeat(3, 1fr)"
gap={2}
overflow="hidden"
>
{formik.values.files &&
formik.values.files.map((file, index) => (
<GridItem key={index}>
<Badge
variant="solid"
borderRadius="xl"
colorScheme={"gray"}
w={file.name.length * 4}
h="8"
display="flex"
justifyContent="space-between"
alignItems="center"
my={2}
>
<Text fontFamily={"body"}>{file.name}</Text>
<CloseButton colorScheme={"blackAlpha"} />
</Badge>
</GridItem>
))}
</Grid>
{formik.errors.files && formik.touched.files && (
<Text fontFamily={"body"} color="red">
{formik.errors.files}
</Text>
)}
</>
)}
<FormErrorMessage>
<ErrorMessage name="file" />
</FormErrorMessage>
<FormLabel htmlFor="tags">
<Text
fontSize="sm"
fontWeight="normal"
color="gray.900"
fontFamily={"body"}
>
Tags
</Text>
</FormLabel>
<InputTag
setFieldValue={formik.setFieldValue}
handleChange={formik.handleChange}
handleBlur={formik.handleBlur.call}
values={formik.values.tags}
show={show}
suggTag={suggTag}
/>
{formik.errors.tags && formik.touched.tags && (
<Text fontFamily={"body"} color="red">
{formik.errors.tags}
</Text>
)}
<FormErrorMessage>
<ErrorMessage name="tags" />
</FormErrorMessage>
<Box
aria-invalid="true"
display={"flex"}
flexDir="row"
gap={2}
my={2}
>
<Text fontFamily={"body"}>Suggested</Text>
<Grid
templateColumns="repeat(3, 1fr)"
gap={2}
overflow="hidden"
>
{ex_tags.map(
(tag, index) => (
<GridItem key={index}>
//I inserted on click call here
<Box onClick={handleClick(tag)}>
<Badge
variant={"subtle"}
borderRadius="lg"
colorScheme={"gray"}
_hover={{
cursor: "pointer",
bgColor: "gray.200"
}}
>
<Text fontFamily={"body"}>{tag}</Text>
</Badge>
</Box>
</GridItem>
),
this
)}
</Grid>
</Box>
<Box display={"flex"} justifyContent="center" my={3}>
<Button
type="button"
ref={cancelRef}
colorScheme="yellow"
isLoading={formik.isSubmitting}
onClick={formik.handleSubmit}
>
<Text
fontWeight="bold"
fontSize="18px"
color="gray.900"
fontFamily={"body"}
>
Submit
</Text>
</Button>
</Box>
</Form>
)}
</Formik>
</Box>
</Flex>
</ModalBody>
</ModalContent>
</Modal>
);
}
export default NewUploader;
but It seems when I render them to the screen it will come out as I triggered the onClick even though I didn't.
For now I commented out the useEffect func inside input component
I have uploaded it to code sandbox Bellow.
https://codesandbox.io/s/amazing-heyrovsky-9kr0ex?file=/src/Previewer.js

Option added to the bottom of options in MUI autocomplete

I am building a category adder and previously I was only allowing the "add category chip" to show for the nooptions render, but I realized that if there was a category "software ngineer" and someone wanted to add "Software" you couldn't do it.
Ultimately I want to add the Add chip (which is different then the normal chips) to the end of all of the options with the search term in it. I tried adding it to the render option, but then it showed up after every option and I tried adding it to filter options but then I can't customize that chip.
Any suggestions on solving this?
My code:
<Autocomplete
id="Categories"
// set it to the left of the cell
style={{ width: '100%', marginTop: '1rem' }}
key={autocompleteKey}
options={
user.categories.length > 0
? user.categories.filter((c) => {
return !category.includes(c || c.charAt(0).toUpperCase() + c.slice(1));
})
: []
}
onChange={(event, value) => (handleAddCategoryToCompany(value))}
onInputChange={(event, value) => setNewCategory(value)}
popupIcon=""
filterOptions={(options) => {
console.log(options);
const result = [...options];
if (search.length > 0) result.push(search);
return result;
}}
renderOption={(event, value: string) => (
<>
<Chip
variant="outlined"
sx={{
m: '2px',
}}
onClick={() => handleAddCategoryToCompany(value)}
key={Math.random()}
label={value.charAt(0).toUpperCase() + value.slice(1)}
onDelete={() => removeFromGlobal(value)}
/>
{/* {search.length > 0
? (
<Chip
variant="outlined"
onClick={handleAddCategory}
key={Math.random()}
sx={{
m: '2px',
}}
label={search}
icon={<AddCircleIcon />}
/>
) : null} */}
</>
)}
renderInput={(par) => (
// Textfield where the options have a delete icon
<TextField
{...par}
fullWidth
label="Add Tag(s)"
margin="normal"
size="small"
name="Name"
onChange={(event) => (setSearch(event.target.value))}
key={autocompleteKey}
type="company"
value={search}
variant="outlined"
/>
)}
noOptionsText={search.length > 0
? (
<Chip
variant="outlined"
onClick={handleAddCategory}
key={Math.random()}
sx={{
m: '2px',
}}
label={search}
icon={<AddCircleIcon />}
/>
) : null}
/>
I figured it out by adding a modifier to the search option in the filter options and then had a conditional statement in render to look for this option. Seems to be working.
<Autocomplete
id="Categories"
// set it to the left of the cell
style={{ width: '100%', marginTop: '1rem' }}
key={autocompleteKey}
options={
user.categories.length > 0
? user.categories.filter((c) => {
return !category.includes(c || c.charAt(0).toUpperCase() + c.slice(1));
})
: []
}
onChange={(event, value) => (handleAddCategoryToCompany(value))}
onInputChange={(event, value) => setNewCategory(value)}
popupIcon=""
filterOptions={(options) => {
console.log(options);
const result = [...options];
if (search.length > 0) result.push(`${search} (new)`);
return result;
}}
renderOption={(event, value: string) => (
<>
{value.indexOf(' (new)') > -1
? (
<Chip
variant="outlined"
onClick={handleAddCategory}
key={Math.random()}
sx={{
m: '2px',
}}
label={search}
icon={<AddCircleIcon />}
/>
)
: (
<Chip
variant="outlined"
sx={{
m: '2px',
}}
onClick={() => handleAddCategoryToCompany(value)}
key={Math.random()}
label={value.charAt(0).toUpperCase() + value.slice(1)}
onDelete={() => removeFromGlobal(value)}
/>
)}
</>
)}
renderInput={(par) => (
// Textfield where the options have a delete icon
<TextField
{...par}
fullWidth
label="Add Tag(s)"
margin="normal"
size="small"
name="Name"
onChange={(event) => (setSearch(event.target.value))}
key={autocompleteKey}
type="company"
value={search}
variant="outlined"
/>
)}
noOptionsText={search.length > 0
? (
<Chip
variant="outlined"
onClick={handleAddCategory}
key={Math.random()}
sx={{
m: '2px',
}}
label={search}
icon={<AddCircleIcon />}
/>
) : null}
/>

Clear icon displaying on all elements in react

I'm trying to display a remove icon on a grid display that the users mouse is hovering over.
this.state = {
action: [],
}
<div>
{this.state.action.map((value, index) => {
return (
<div key={index} onMouseEnter={this.removeElementIcon} onMouseLeave={this.hideRemoveElementIcon} className={classes.gridClass}>
<Grid className={classes.marginGrid}>
<Paper className={classes.paddingPaper}>
<Typography variant={"h5"}>{value}</Typography>
<Typography component={"p"}>{value}</Typography>
</Paper>
</Grid>
{this.state.removeElementIcon ?
<IconButton className={classes.removeElement} color={"secondary"} arial-label={"remove element"} onClick={()=> this.removeElement(value)}>
<ClearIcon color={"error"} />
</IconButton>
: null}
</div>
I've tried just returning some jsx from the method itself.
removeElementIcon = () => {
return ({
this.state.removeElementIcon ?
<IconButton className={classes.removeElement} color={"secondary"}
arial-label={"remove element"} onClick={() => this.removeElement(value)}>
<ClearIcon color={"error"}/>
</IconButton> :
null
});
Instead of:
removeElementIcon = () => {
this.setState({removeElementIcon: true});
};
hideRemoveElementIcon = () => {
this.setState({removeElementIcon: false});
};
Instead of just displaying the clear icon on one element it displays on all elements.
You need to maintain item index in state,
this.state = {
action: [],
hoverIndex: '',
}
Pass the index to your removeElementIcon function,
<div
key={index}
onMouseEnter={() => this.removeElementIcon(index)}
onMouseLeave={hideRemoveElementIcon}
className={classes.gridClass}
>
...
</div>
In your function's set the hoverIndex,
removeElementIcon = (index) => {
this.setState({removeElementIcon: true, hoverIndex: index});
};
hideRemoveElementIcon = () => {
this.setState({removeElementIcon: false, hoverIndex:''});
};
And finally apply the condition,
{this.state.removeElementIcon && this.state.hoverIndex === index ?
<IconButton className={classes.removeElement} color={"secondary"} arial-label={"remove element"} onClick={() => this.removeElement(value)}>
<ClearIcon color={"error"}/>
</IconButton>
: null
}
or even short way
{this.state.removeElementIcon && this.state.hoverIndex === index &&
<IconButton className={classes.removeElement} color={"secondary"} arial-label={"remove element"} onClick={() => this.removeElement(value)}>
<ClearIcon color={"error"}/>
</IconButton>
}
Demo with simple button.

Categories