css transform: translate transition behaving strangely - css

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.

Related

change global css element

i've made this banner like screen that appears when my site is loaded, but here's the thing, i don't want no scrollbar while this opening animation it's happening, i only want to show the other components (the scrollbar and the whole site) once the gsap animation finishes, how could i proceed? thanks! (i tried to create a function to control those global elements, is it a way?)
So if I understand correctly you need the Banner to be displayed until the site is loaded. Maybe you are making some API calls or in general, you are planning to show the banner for let's say 3 sec and post that you want your actual components to be displayed.
You can try below approch:
export const APP = (): JSX.Element => {
const [isAnimationInProgress, SetAnimationState] = React.useState(true);
React.useEffect(() => {
// You can have your page load API calls done here
// Or wait for 'X' seconds
// Post that set the AnimationState to false to render actual components
setAnimationState(false);
})
return (
{
isAnimationInProgress && <Banner />
}
{
!isAnimationInProgress && <ActualComponent />
}
)
}
Regarding scrollbars, including overflow: hidden; in style for the banner should do the work if you are getting scrollbars for the Banner component.

filter icon in material-ui's v5 DataGrid

The default behavior for the new DataGrid is to hide a filter icon unless you hover over the column header (and have a filter applied). In the previous version the icon remained visible.
Codesandbox https://codesandbox.io/s/mui-datagrid-filter-icon-7rbrk
When a filter is applied it adds a new iconButtonContainer div. The classes are: MuiDataGrid-iconButtonContainer css-ltf0zy-MuiDataGrid-iconButtonContainer
Is there a way to override this behavior? All I'd like to do is set visibility to always be visible when that div is generated by the library.
The answer here was to create a separate styled component of the data grid and use the global classnames imported from mui to reference the correct one for the style you wish to override. In my case it was something like:
const MyStyledGrid = styled(DataGrid, () => ({
[`& .${gridClasses.iconButtonContainer}`] : {
visibility: "visible",
width: "auto"
}
}))
function MyComponent() {
return (
<MyStyledDataGrid {...props} />
)
}

How to style a component "from the outside" with scoped css

I'm using scoped CSS with https://github.com/gaoxiaoliangz/react-scoped-css and am trying to follow the following rules (besides others):
Scoped component CSS should only include styles that manipulate the "inside" of the component. E.g. manipulating padding, background-color etc. is fine whilst I try to stay away from manipulating stuff like margin, width, flex etc. from within the component CSS
Manipulating the "outside" of a component (margin, width, flex etc.) should only be done by "consuming" or parent components
This is rule is somewhat derived from some of the ideas behind BEM (and probably other CSS methodologies as well) and allows for a rather modular system where components can be used without "touching their outside" but letting the parent decide how their internal layouts etc. works.
Whilst this is all fine in theory, I don't really know how to best manipulate the "outside styles" of a component from the consuming code which is best shown with an example:
search-field.scoped.css (the component)
.input-field {
background: lightcoral;
}
search-field.tsx (the component)
import './search-field.scoped.css';
type SearchFieldProps = {
className: string;
};
export const SearchField = (props: SearchFieldProps) => {
return <input className={`input-field ${props.className}`} placeholder="Search text" />;
};
sidebar.scoped.css (the consumer)
.sidebar-search-field {
margin: 16px;
}
sidebar.tsx (the consumer)
import './sidebar.scoped.css';
// ...
export const Sidebar = () => {
return (
<SearchField className="sidebar-search-field" />
(/* ... */)
);
};
In the above example, the CSS from the class sidebar-search-field in sidebar.scoped.css is not applied because the class passed to SearchField is scoped to the Sidebar and the final selector .sidebar-search-field[data-sidebarhash] simply doesn't match as the input element of the SearchField (obviously) doesn't have the data attribute data-sidebarhash but data-searchfieldhash.
ATM, I tend to create wrapper elements in situations like this which works but is rather cumbersome & clutters the markdown unnecessarily:
// ...
export const Sidebar = () => {
return (
<div className="sidebar-search-field">
<SearchField />
</div>
(/* ... */)
);
};
Question
Is there any way to "style scoped CSS component from the outside"?
Ps.: I'm not sure if all the above also applies to scoped styles in Vue. If not, please let me know how it works there so that I can create a feature request in https://github.com/gaoxiaoliangz/react-scoped-css.

position absolute - float within screen limits (React)

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.

CSS Transitions don't survive React Component changing inner DOM structure

I have illustrated the problem in this CodePen
const Component = ({ structure }) => {
switch (structure) {
case 'nested':
return (
<div>
<AnimatedComponent />
</div>
);
case 'flat':
return
<AnimatedComponent />
;
}
};
There's some logic in AnimatedComponent that changes the styling of the Component in an animated fashion, e.g. change the background color from black to red over a duration of 1 second. The animation is started by changing a color class on AnimatedComponent. There is CSS to handle the animation given the changed class.
When changing the DOM structure from nested to flat, the HTML element is destroyed and recreated, the transition starting state is lost (aka the browser doesn't know which class was set before because the element was newly created).
What I want React to do is to change the DOM structure with moving elements in new positions, not destroying and recreating them.
Is this possible?
I tried to use the key props on <AnimatedComponent />, but it only fixes the flash of DOM change. Animation is skipped. See Codepen. Thanks Thomas Rooney for this suggestion.
Can I tell React to apply the class changes just one tick after the position of the DOM element was changed?
Can I tell React to apply the class changes just one tick after the position of the DOM element was changed?
Yes, this is precisely what the setTimeout function is for. Copying your second example, where you fixed the flickering, wrapping your color action dispatch with setTimeout (with no time value, which defaults to 0), seems to fix your issue.
onColorClick: () => {
setTimeout(() => {
dispatch({type: 'TOGGLE_COLOR'})
})
},
codepen
Update: I've noticed it's a bit more reliable to add some time before the color change (second argument in setTimeout, (fn, ms). I believe this is because setState is also happening asynchronously.
onColorClick: () => {
setTimeout(() => {
dispatch({type: 'TOGGLE_COLOR'})
}, 100) <-- play around with this value
},

Resources