I am implementing drop-down list using styled-component in react. In the process, I have two questions.
First, when dropDownVisible changes from true to false, why doesn't the animation effect apply and it disappears immediately? How can I improve the animation effect? Like when this list goes down, I want to make it gradually when it goes up.
Second, when StyledDropdown is dropped down, I want it to drop down behind the StyledHead, so I set the z-index property like that. I want the StyledHead to be always on top, so I'm curious why the StyledHead is hidden as the StyledDropdown drops down, even though I gave the z-index property bigger.
The source code is roughly structured like this:
// AApage.jsx
import { useEffect, useState, useRef } from 'react';
import { MdArrowDropDown, MdArrowDropUp } from 'react-icons/md';
import styled, { keyframes } from 'styled-components';
const dropAnimation = keyframes`
0% {
transform : translateY(-300px);
display : none;
}
100% {
transform : translateY(0);
}
`;
const StyledHead = styled.div`
width: 100px;
height: 100px;
background-color: red;
z-index: 11;
`;
const StyledDropdown = styled.div`
width: 100px;
height: 300px;
background-color: #d9d9d9;
border-radius: 0px 0px 10px 10px;
z-index: 3;
animation: ${dropAnimation} 1s alternate;
`;
const AApage = () => {
const [dropDownVisible, setDropDownVisible] = useState<boolean>(false);
const toggleDropDownVisible = () => {
setDropDownVisible((prev) => !prev);
};
return (
<>
<StyledHead>
<div>Dropdown</div>
<span>{`${dropDownVisible}`}</span>
{dropDownVisible ? (
<MdArrowDropUp
onClick={() => {
toggleDropDownVisible();
}}
></MdArrowDropUp>
) : (
<MdArrowDropDown
onClick={() => {
toggleDropDownVisible();
}}
></MdArrowDropDown>
)}
</StyledHead>
{dropDownVisible ? (
<StyledDropdown>
<div>temp data</div>
<div>temp data</div>
<div>temp data</div>
</StyledDropdown>
) : (
<></>
)}
</>
);
};
export default AApage;
I am using react-beautiful-dnd. I have it working, except when I drag and drag an item into one of my lists, the item is positioned incorrrectly, has a short delay, then jumps to the correct position.
Here is what it looks like: Link to issue
As you can see, after the item is dropped in a list, the item readjusts itself to fit within the div.
Here is the code for the item:
import React, { useState } from "react";
import styled from "styled-components";
import { Draggable } from "react-beautiful-dnd";
const Container = styled.div`
margin: 0 0 8px 0;
background-color: rgba(140, 240, 255);
`;
const Title = styled.div`
font-size: 1.5rem;
`;
const Gradient = styled.div`
background: black;
height: 2px;
margin: 0.5rem;
`;
const Description = styled.div`
font-size: 1rem;
`;
const Ticket = ({ ticket, setCategories, id, index }) => {
const [isDeleted, setIsDeleted] = useState(false);
const handleDelete = (e) => {
e.preventDefault();
fetch(`/tickets/${ticket.id}`, {
method: "DELETE",
}).then(
fetch("/categories")
.then((r) => r.json())
.then(setCategories)
);
setIsDeleted(true);
};
return (
<Draggable draggableId={id.toString()} index={index}>
{(provided, snapshot) =>
isDeleted ? null : (
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
>
<Container
style={{
backgroundColor: snapshot.isDragging
? "aquamarine"
: "rgba(140, 240, 255)",
}}
>
<Title>{ticket.title}</Title>
<Gradient></Gradient>
<Description>{ticket.description}</Description>
<button onClick={handleDelete}>Delete</button>
</Container>
</div>
)
}
</Draggable>
);
};
export default Ticket;
And here is for the list:
import React, { useState } from "react";
import styled from "styled-components";
import Ticket from "./Ticket";
import { Droppable } from "react-beautiful-dnd";
import { transformData } from "./Categories";
const Container = styled.div`
background-color: rgba(255, 255, 255, 0.8);
border-radius: 0.25em;
box-shadow: 0 0 0.25em rgba(0, 0, 0, 0.25);
text-align: center;
width: 20rem;
font-size: 1.5rem;
padding: 4px;
margin: 1rem;
`;
const Gradient = styled.div`
background: black;
height: 2px;
margin: 1rem;
`;
const FormContainer = styled.div`
margin: 1rem;
border: 1px solid black;
backgroundColor: rgba(140, 240, 255);
`;
const Button = styled.button`
margin-left: 1rem;
`;
const DropDiv = styled.div`
min-height: 50vh;
padding: 4px;
`;
const Category = ({ category, user, setCategories, id }) => {
const [isClicked, setIsClicked] = useState(false);
const [title, setTitle] = useState("");
const [description, setDescription] = useState("");
const handleSubmit = (e) => {
e.preventDefault();
const newTicket = {
title: title,
description: description,
user_id: user.id,
category_id: id,
};
fetch("/tickets", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(newTicket),
}).then(
fetch("/categories")
.then((r) => r.json())
.then(transformData)
.then(setCategories)
);
setIsClicked(false);
};
return (
<Container>
{category.title}
<Button onClick={() => setIsClicked(!isClicked)}>Add</Button>
<Gradient></Gradient>
{isClicked ? (
<FormContainer>
<form onSubmit={handleSubmit}>
<label>Title</label>
<input onChange={(e) => setTitle(e.target.value)}></input>
<label>Description</label>
<input onChange={(e) => setDescription(e.target.value)}></input>
<button type="submit">Submit</button>
</form>
</FormContainer>
) : null}
<Droppable droppableId={id.toString()}>
{(provided, snapshot) => (
<DropDiv
{...provided.droppableProps}
ref={provided.innerRef}
style={{
background: snapshot.isDraggingOver ? "lightblue" : "",
}}
>
{category.tickets.map((ticket, index) => {
return (
<Ticket
ticket={ticket}
key={ticket.id}
setCategories={setCategories}
id={ticket.id}
index={index}
/>
);
})}
{provided.placeholder}
</DropDiv>
)}
</Droppable>
</Container>
);
};
export default Category;
I have tried flexbox styling and messed with margin and padding. If i remove the margin and padding it seems to go away, but in beautiful-dnd examples they all have space between items and theres no delay like this. Does anyone have any ideas?
It looks like the placeholder might not have the 8px bottom margin that the rest of the Draggables have.
You'll notice that when you pick up something (without changing it's position) from somewhere other than the end of the list, the list will shift up a bit right away, and when you drop something at the end of a list, you don't see the issue.
The placeholder gets its margins from the item being dragged. You can see this happening by looking at the inline styles on the placeholder element that appears at the end of the droppable while you are dragging.
So, you might want to try putting the provided innerRef, draggableProps, and dragHandleProps on the Ticket Container itself instead of a parent div, as it's possible that because they are on a different element, react-beautiful-dnd isn't taking the margins in to account.
The delay could be caused because your list is changing size while dragging an element, and this provokes the library to recalculate and animate slower.
The solution is to avoid this size change while dragging.
The causes of the issue could be:
---- Cause A ----
The {provided.placeholder} auto inserted in the DOM while dragging doesn't have the same margins/padding that the other Draggable elements
Solution
Be sure to add the styles that separate the elements (margin/padding) to the element that you are applying the provided innerRef, draggableProps, and dragHandleProps because in this way the {provided.placeholder} will inherit those styles.
---- Cause B ----
You are using flexbox or css-grid with a gap property to separate the elements.
Solution
Stop using gap and just add margin to the element with provided innerRef, draggableProps, and dragHandleProps (do not use inline styles but css classes)
Extra: To confirm that size change is the cause
Drag some elements from somewhere other than the end of the list and notice how another element will move up a bit.
Also, when you drop something at the end of a list, you don't see the problem.
I was having this same issue- but as seen in the docs, inline styling with provided props did the trick for me:
const ListItem = ({ item, index }) => {
return (
<Draggable draggableId={item.id} className="draggableItem" index={index}>
{(provided, snapshot) => {
return (
<div
ref={provided.innerRef}
snapshot={snapshot}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={{
userSelect: "none",
padding: 12,
margin: "0 0 8px 0",
minHeight: "50px",
borderRadius: "4px",
backgroundColor: snapshot.isDragging
? "rgb(247, 247, 247)"
: "#fff",
...provided.draggableProps.style,
}}
>
<div className="cardHeader">Header</div>
<span>Content</span>
<div className="cardFooter">
<span>{item.content}</span>
<div className="author">
{item.id}
<img className="avatar" />
</div>
</div>
</div>
);
}}
</Draggable>
);
};
I am new to React and being held back by a seemingly simple task.
I've got a Header component nested within which is a HamburgerButton component. Clicking the latter should make a sidenav appear but for now I would like the icon to change from the 'hamburger' to the big 'X'.
Here is my parent component:
import { MyMoviesLogo } from 'components/Icons';
import HamburgerButton from 'components/HamburgerButton/HamburgerButton';
import styles from './Header.module.css';
const Header = (): JSX.Element => {
const [isActive, setIsActive] = useState(false);
return (
<header className={styles.header}>
<MyMoviesLogo className={styles.headerIcon} />
<HamburgerButton
isActive={false}
/>
</header>
);
};
export default Header;
And here is the HamburgerButton
import styles from './HamburgerButton.module.css';
type HamburgerButtonProps = {
isActive: boolean;
onClick?: () => void;
};
const addMultipleClassNames = (classNames: string[]): string => classNames.join(' ');
const HamburgerButton = ({ isActive, onClick }: HamburgerButtonProps): JSX.Element => {
return (
<div className={isActive ? addMultipleClassNames([styles.hamburger, styles.active]) : styles.hamburger} onClick={onClick}>
<div className={styles.bar}></div>
<div className={styles.bar}></div>
<div className={styles.bar}></div>
</div>
);
}
export default HamburgerButton;
Here's my HamburgerButton.module.css file:
.hamburger {
cursor: pointer;
display: block;
width: 25px;
}
.bar {
background-color: var(--hamburger-button-global);
display: block;
height: 3px;
margin: 5px auto;
transition: all 0.3s ease-in-out;
width: 25px;
}
.hamburger.active .bar:nth-child(2) {
opacity: 0;
}
.hamburger.active .bar:nth-child(1) {
transform: translateY(8px) rotate(45deg);
}
.hamburger.active .bar:nth-child(3) {
transform: translateY(-8px) rotate(-45deg);
}
Manually changing the isActive prop to false verifies that the styling is applied as required.
My question is, how could I make it so when I click the icon its state gets toggled? I am familiar with React hooks like useState but can't quite put something together.
Any help would be greatly appreciated.
Thank you.
P.S.: It's probably obvious but I am using TypeScript.
You should use your onClick prop from your <HamburgerButton /> to change the parent state.
<HamburgerButton isActive={isActive} onClick={() => { setIsActive(oldState => !oldState) } />
When I click the button the transition happens instantly. How can I add a smooth in-and-out transition? I want the whole to slide nice and smooth when buttons are in focus.
const [margin, setMargin] = useState("-100vw");
const setStyle = (margin) => {
setMargin(margin);
};
const Box = styled.span`
display: block;
width: 150vw;
margin-top: 0;
height: 0;
margin-left: ${margin};
transition: all 0.8s 0.2s ease-in-out;
`;
return (
<Box>
<Wrapper>
{children}
<p>page contents</p>
<button onMouseEnter={() => setStyle("-100vw")}>Change</button>
</Wrapper>
<TriangleLeft>
<Closer>
<button onMouseEnter={() => setStyle("0")}>Change</button>
</Closer>
</TriangleLeft>
</Box>
);
};
I am not sure if this is an issue with css or how i am handling the hooks...
I take a look in your code and you just need to do some changes:
Remove all styled components out of Field component, like this:
const TriangleLeft = styled.span`
....
`;
const Box = styled.span`
....
`;
const Wrapper = styled.span`
....
`;
const Closer = styled.span`
....
`;
const Field = ({ children }) => {
const [margin, setMargin] = useState("-100vw");
const setStyle = (margin) => {
setMargin(margin);
};
return .....
}
Also, in your Box style, your margin-left should be:
/* Completed Style: */
const Box = styled.span<BoxProps>`
display: block;
width: 150vw;
margin-top: 0;
height: 0;
/* Here you are getting the prop margin and setting it to margin-left */
margin-left: ${props => props.margin};
transition: all 0.8s 0.2s ease-in-out;
`;
And finally in your Box tag:
return (
// Here you are saying that Box has a custom prop named margin and setting it with margin state.
<Box margin={margin}>
<Wrapper>
{children}
.....
You can check it here.
I have a sidebar and some main content. The main content takes up the majority of the screen whilst the sidebar should only take a small portion. I've got a parent container that is a flexbox. The two children (sidebar and main content) are both div elements.
The sidebar is closed by default
The Issue: Toggling the sidebar does not expand the sidebar as expected
Things I've Checked:
Flex values in the sidebar css are being updated correctly
Sidebar events are being fired and the isOpen hook is being updated correctly
// main.ts
import styled from 'styled-components';
export const Content = styled.div`
flex: 4;
margin-top: 1em;
margin-bottom: 2em;
height: 100vh;
`;
export const Container = styled.div`
display: flex;
`;
// sidebar/styles.ts
import styled from 'styled-components';
interface RootProps {
isOpen: boolean;
}
export const Root = styled.div<RootProps>`
padding: 1em;
background-color: ${DARK};
flex: ${({ isOpen }: RootProps) => (isOpen ? 1 : 0)};
`;
// sidebar/index.tsx
export const Sidebar: React.FC = () => {
const dispatch = useDispatch();
const isOpen = useSidebar();
const handleOpen = () => dispatch(toggleSidebar());
return (
<Root isOpen={isOpen}>
<Button onClick={handleOpen}>
CLICK ME
</Button>
</Root>
);
};
// Usage
// Sidebar styles mirror what's in the sidebar styled component file
<Container>
<Sidebar />
<Content />
</Container>
Expected outcome is that the sidebar expands and collapses when toggling the button. No error messages are displayed and the sidebar flex values are being updated correctly.
JSFiddle with just HTML / CSS but essentially the desired effect: https://jsfiddle.net/5dLk9ex3/3/
The problem with your code is somewhere in the reducer scope (your question is incomplete), your Sidebar doesn't rerender after you dispatching an action.
Working example:
import React, { useState } from 'react';
import ReactDOM from 'react-dom';
import styled from 'styled-components';
const Container = styled.div`
display: flex;
`;
const First = styled.div`
flex: ${({ isOpen }) => (isOpen ? 1 : 0)};
background-color: red;
height: 20px;
`;
const Second = styled.div`
flex: 4;
background-color: green;
height: 20px;
`;
const DEFAULT_INITIAL = false;
const App = () => {
const [isOpen, setIsOpen] = useState(DEFAULT_INITIAL);
const onClick = () => {
console.log('Toggle Sidebar');
setIsOpen(p => !p);
};
return (
<>
<Container>
<First isOpen={isOpen}>FIRST</First>
<Second>SECOND</Second>
</Container>
<button onClick={onClick}>OpenSider</button>
</>
);
};
Please refer to the first comment of this answer
I'm unsure as to why but setting the main content div to min-width: 0 resolved the issue. See this for more: Flex items not shrinking when window gets smaller