In my react app I'm doing some kind of animations to make it nicer. I want to make a div appear by sliding from above disappear by sliding it back.
I'm using bootstrap 5.1.0 and react-bootstrap 1.6.1 in my stack.
I came up with a solution that is to wrap the content inside a div and toggle its maxHeight between window.innerHeight and 0 as needed. I added also a custom css class
.transition-height{
transition: max-height 0.5s ease-in-out;
}
Problem is that obviosly, when I close the div, the animation starts from a very hight value of maxHeight and so there's a delay between the close trigger event and the moment when you see the div actually closing. I used window.innerHeight as I don't have a known height target value to use, it depends from the length of div content.
I uploaded the code to codesandbox, so you can see its behaviour.
How can I avoid this?
In your case just need to get the height of the div with useRef and when will show = true set the current height. codesandbox exapmle
ApperDiv
const AppearDiv = (props) => {
let { show = false, className = "", style = {} } = props;
const ref = useRef(null);
const getPaperBoxHeight = ref.current?.scrollHeight || 0;
const filteredProps = Object.entries(props)
.filter(([k, _]) => !["className", "show", "style"].includes(k))
.reduce((p, [k, v]) => ({ ...p, [k]: v }), {});
style = show ? { height: getPaperBoxHeight } : { height: 0 };
return (
<div
ref={ref}
className={
"overflow-hidden transition-height" + (className && ` ${className}`)
}
style={style}
{...filteredProps}
/>
);
};
Related
Working on an Angular 14 application, I want all context menu pop-ups to be only 80% of their size, as the default size is too large and clunky in the context of the data presented in the application. This is working fine to accomplish this:
.cdk-overlay-pane .mat-menu-panel {
transform: scale(0.8);
transform-origin: top left;
}
However, the problem is that the context menu appears at full size for a moment, and then the transform takes effect and it "snaps" to the desired size. I don't want it to appear until the transform is complete. Anybody know how to accomplish this?
I was able to do this by defaulting mat-menu-panel to visibility: hidden, then showing it a fraction of a second after the menu is opened. (I don't like using javascript like this within the context of an Angular app, but I don't know any other way.)
Default CSS:
.mat-menu-panel {
visibility:hidden;
}
Showing after menu is opened:
public onContextMenu(event: MouseEvent, item: any) {
event.preventDefault();
this.contextMenuPosition.x = event.clientX + 'px';
this.contextMenuPosition.y = event.clientY + 'px';
this.matMenuTrigger.menuData = { 'item': item };
this.matMenuTrigger.menuOpened.subscribe(() => {
setTimeout(() => {
const overlayPanes = document.getElementsByClassName('mat-menu-panel') as HTMLCollectionOf<HTMLElement>;
Array.from(overlayPanes).forEach((el) => {
el.style.visibility = 'visible';
});
}, 200);
});
this.matMenuTrigger.openMenu();
}
On this sandbox, I've recreated the classic sliding-puzzle game.
On my GameBlock component, I'm using a combination of css transform: translate(x,y) and transition: transform in order to animate the sliding game-pieces:
const StyledGameBlock = styled.div<{
index: number;
isNextToSpace: boolean;
backgroundColor: string;
}>`
position: absolute;
display: flex;
justify-content: center;
align-items: center;
width: ${BLOCK_SIZE}px;
height: ${BLOCK_SIZE}px;
background-color: ${({ backgroundColor }) => backgroundColor};
${({ isNextToSpace }) => isNextToSpace && "cursor: pointer"};
${({ index }) => css`
transform: translate(
${getX(index) * BLOCK_SIZE}px,
${getY(index) * BLOCK_SIZE}px
);
`}
transition: transform 400ms;
`;
Basically, I'm using the block's current index on the board in order to calculate it's x and y values which change the transform: translate value of the block when it's being moved.
While this does manage to trigger a smooth transition when sliding the block to the top, to the right and to the left - for some reason, sliding the block from top to bottom doesn't transition smoothly.
Any ideas what's causing this exception?
React, lists and keys
What you're seeing is the result of a mount/unmount of the <GameBlock /> components.
Although you're passing a key prop to the component, React is unsure that you're still rendering the same element.
If I have to guess why react is uncertain, I would put the culprit at:
Changing the array sort with:
const previousSpace = gameBlocks[spaceIndex];
gameBlocks[spaceIndex] = gameBlocks[index];
gameBlocks[index] = previousSpace;
having different virtual DOM results using the conditional on isSpace:
({ correctIndex, currentIndex, isSpace, isNextToSpace }) => isSpace ? null : ( <GameBlock ....
Usually in applications, we don't mind a re-mount since it's pretty fast. When we attach an animation, we don't want any re-mounts since they mess with the css-transitions.
in order for react to be certain it's the same node and no re-mount is needed. we should take care that; between renders; the virtual dom stays mostly the same.
we can achieve that not doing anything fancy in the render of the list, and passing down the same keys between renders.
Pass isSpace down
Instead of changing the the rendered DOM nodes, we want the list render to always return an equal amount of nodes, with the exact same keys for each Node, in the same order.
simply passing 'isSpace' down and styling as display:none; should do the trick.
<GameBlock
...
isSpace={isSpace}
...
>
const StyledGameBlock = styled.div<{ ....}>`
...
display: ${({isSpace})=> isSpace? 'none':'flex'};
...
`;
Making sure to not change the arraysort
React considers the gameBlocks array to be modified, the keys are in a different order. Thus triggering unmount/mount of the rendered <GameBlock/> components.
We can make sure that react considers this array to be unmodified, by only changing the properties of the items in the list and not the sort itself.
in your case, we can leave all properties as is, only changing the currentIndex for the blocks that are moved/swapped with each other.
const onMove = useCallback(
(index) => {
const newSpaceIndex = gameBlocks[index].currentIndex; // the space will get the current index of the clicked block.
const movedBlockNewIndex = gameBlocks[spaceIndex].currentIndex; // the clicked block will get the index of the space.
setState({
spaceIndex: spaceIndex, // the space will always have the same index in the array.
gameBlocks: gameBlocks.map((block) => {
const isMovingBlock = index === block.correctIndex; // check if this block is the one that was clicked
const isSpaceBlock =
gameBlocks[spaceIndex].currentIndex === block.currentIndex; // check if this block is the space block.
let newCurrentIndex = block.currentIndex; // most blocks will stay in their spot.
if (isMovingBlock) {
newCurrentIndex = movedBlockNewIndex; // the moving block will swap with the space.
}
if (isSpaceBlock) {
newCurrentIndex = newSpaceIndex; // the space will swap with the moving block
}
return {
...block,
currentIndex: newCurrentIndex,
isNextToSpace: getIsNextToSpace(newCurrentIndex, newSpaceIndex)
};
})
});
},
[gameBlocks, spaceIndex]
);
...
// we have to be sure to call onMove the with the index of the clicked block.
() => onMove(correctIndex)
The only things we've changed are is the currentIndex of the clicked block and the space.
sandbox:
sandbox example based on your provided sandbox.
closing thoughts: I think your code was easy to read and understand, good job on that!
Additionally to the excellent answer and explanations #Lars provided, I wanted to share visual proof that certain <GameBlock /> components are indeed unmounted or changed in order, causing the hiccup in the CSS animation.
As you can see, when focussing one of the blocks and sliding down, the element changes its position in the DOM.
If data in container A collapses(minimised), Component B should increase vertically in size and appear on full page. Similarly if Component B is collapsed,component A should increase.By default,both the components have equal screen space.
there are tons of ways to do this, you can check how flexbox in CSS works. it should not bee very react specific, All you need to do from react is to know which component is collapsed and which is to expanded.
In the parent component, you'll want to track which component is maximised. Then, pass a maximised prop to component A and component B, and let them set their CSS classes based on it. You could hide most of the content if you just want a mini version of the component.
Assuming you're using function components with hooks, it would look somewhat like this:
const Container = () => {
// Either "A", "B" or null (equal sizes)
const [componentMaximised, setComponentMaximised] = useState(null);
return (
<div className="container">
<A maximised={componentMaximised === "A"}/>
<B maximised={componentMaximised === "B"}/>
</div>
);
};
const A = props => {
return (
<div className={props.maximised ? "component component-maximised" : "component"}>
// ...
</div>
);
};
const B = props => {
return (
<div className={props.maximised ? "component component-maximised" : "component"}>
// ...
</div>
);
};
You'll also want to pass the setComponentMaximised function to each component as a prop if you want them to be able to have a button to maximise and minimise themselves.
For your CSS, use display: flex in combination with flex-grow to set how the items share the space:
.container {
height: 100vh; /* approx full height */
display: flex;
flex-flow: column nowrap;
}
.component {
flex-grow: 1;
overflow: hidden; /* prevent contents from spilling out of component */
}
.component-maximised {
flex-grow: 3;
}
Quick demo of this technique (try changing the classes manually in HTML)
https://codepen.io/gh102003/pen/MWKOQqE
You can use flex-grow: 0 if you just want the component to take up the space it needs.
I have a React application. I am using React Spring for overall animations. I am not able to animate 2 things -
The animation I am experimenting with is a simple opacity animation.
import { useSpring, animated } from "react-spring";
/***
Some code
***/
const styleProps = useSpring({
to: { opacity: 1 },
from: { opacity: 0 }
});
1) Is conditional elements. Please refer code below -
<section>
{!flag ? (
<animated.div style={styleProps}>
Some random text
</animated.div>
) : (
<animated.div style={styleProps}>
To appear with animation
</animated.div>
)
}
</section>
The issue is that the animated.div of react-spring does not animate the same. What is the right way? Is there a way to animate the same without react-spring?
2) I have a conditional bootstrap className attached based on a flag. I want to animate the same
<animated.div style={styleProps} className={classnames({
"col-lg-6": !flag,
"col-lg-12": flag
})}
>
Random Content
</animated.div>
For this also, the issue is that it is not animating. What is the right way?
Yo have a lot of question. I can answer part of it and maybe you will understand it better.
Your example of useSpring animation is triggered only once. And when you switch between components with the conditional render it will no longer animate.
But you can re-trigger the animation in useSpring, if you change the 'to' parameter conditionally (and leave the render to react-spring).
const styleProps1 = useSpring({
to: { opacity: flag ? 1 : 0 },
from: { opacity: 0 }
});
const styleProps2 = useSpring({
to: { opacity: flag ? 0 : 1 },
from: { opacity: 0 }
});
<section>
<>
<animated.div style={styleProps1}>
Some random text
</animated.div>
<animated.div style={styleProps2}>
To appear with animation
</animated.div>
</>
</section>
You have to use absolute positioning if you want the element to appear in the same place.
You can achieve similar effect with useTranstion also with absolute positioning. In this case the element dismounted at the end of animation. So if you have mouse click problems with the useSpring method you can try to switch to useTransition.
Maybe it also answer your second questiona as well. I am not familiar with bootstrap.
I'm trying to create an App with a global dictionary; so that when a word that appears in the dictionary is hovered than a small box appears next to it with a definition.
The problem is that the text in the dictionary can appear any where on the screen, and I need to align the floating box so that it will not be displayed out side of the screen
Similar to this
only that I need to be able to style the floating box, like this
Note that the box display outside of the screen:
I tired to use ui material ToolTip
but it throws
TypeError
Cannot read property 'className' of undefined
I solved a similar problem before with jQuery, where I dynamically calculated the position of the box, relative to the screen and the current element.
but I don't know how to do it in react, mainly since I don't know how to get the position of the current element dynamical.
Please help
To give an idea where to start, have a look at useCallback and refs for React. With the given information from node.getBoundingClientRect(), you could calculate if your tooltip is outside the visible area of the browser.
// edit: useCallback won't work in this case, because the visibility is triggered by a css hover and the dimensions are not yet available for the hidden tooltip. Here is a possible solution with useRef and use useEffect though:
function ToolTip({ word, description }) {
const [left, setLeft] = useState(0);
const [hover, setHover] = useState(false);
const ref = useRef(null);
useEffect(() => {
if (ref.current) {
const { right } = ref.current.getBoundingClientRect();
if (window.innerWidth < right) {
setLeft(window.innerWidth - right);
}
}
}, [hover]);
return (
<span
desc={description}
className="dashed"
onMouseEnter={() => setHover(true)}
onMouseLeave={() => {
setHover(false);
setLeft(0);
}}
>
{word}
<div ref={ref} className="explain" style={{ left }}>
<h2>{word}</h2>
{description}
</div>
</span>
);
}
Codepen example: https://codesandbox.io/s/lk60yj307
I was able to do it with
https://www.npmjs.com/package/#syncfusion/ej2-react-popups
But I still wonder what is the correct way to do it in code.