Can we animate a CSS change with a State? - css

I have a Sidebar on left of my App, and it is set on 300px wide. When I click on a button, the Sidebar goes from 300px to 50px.
I change this width with a state :
const [isMinimized, setIsMinimized] = useState(false);
[...]
<div className={`${isMinimized ? 'w-16' : '2xl:w-80 w-56'} relative bg-white`}>
I'd like to set an animation when the sidebar minimize. I tried to add an animation on my div, but does not work.
Is it possible ?

You can specify a className when isMinimized is true ( for Example isMinimzed ? 'animatedScrollbar w-16' : '2xl:w-80 w-56') and add your transition as css using the className of the minimized state .

You need to add transition-width so tailwind adds transition-property:width to the element's style properties.
You can do this all within JSX with tailwind classes. No need for other code.
Check below
function MyApp() {
const [isMinimized, setIsMinimized] = React.useState(false);
return (
<div >
<div
className = {
`${isMinimized ? 'w-16' : '2xl:w-80 w-56'} relative bg-black duration-1000 transition-width ease-in-out`
} >
MY DIV
</div>
<button
onClick = {() => setIsMinimized(!isMinimized)}
>
CLICK
</button>
</div>
)
}
ReactDOM.createRoot(
document.getElementById("root")
).render( <
MyApp / >
);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>
<script src="https://cdn.tailwindcss.com"></script>
Or see jsFiddle
If you want to add custom transition durations or behaviors you would need to change tailwind config. Read more here tailwind transition

Use Web Animation API
First create a ref object of the element you change its width.
const ref = useRef(null);
And use it:
<div ref={ref} className={relative bg-white`}>
Call this function on click.
const onClick = () => {
const { current } = ref;
if (current) {
const keyframe = [{ width: "300px" }, { width: "50px" }];
const options = {
duration: 1000,
fill: "forwards",
delay: 0,
easing: "ease-in",
};
const newKeyframeEffect = new KeyframeEffect(current, keyframe, options);
const animation = new Animation(newKeyframeEffect, document.timeline);
animation.play();
animation.onfinish = () => {
}
}
};
Do whatever you need in onfinish functoin.
If you need to change the width of the element back to 300px, do this:
Track if the element is expanded.
const [isExpended, setIsExpended] = useState(false)
If expanded, change its width from 300px to 50px. Else vise versa.
const keyframe = isExpended ? [{ width: "50px" }, { width: "300px" }]: [{ width: "300px" }, { width: "50px" }];
When the animation finishes, change the isExpended state.
setIsExpended(!isExpended)
Example code
const App = () => {
const ref = useRef(null)
const [isExpended, setIsExpended] = useState(false)
const onClick = () => {
const { current } = ref;
if (current) {
const keyframe = isExpended ? [{ width: "50px" }, { width: "300px" }]: [{ width: "300px" }, { width: "50px" }];
const options = {
duration: 1000,
fill: "forwards",
delay: 0,
easing: "ease-in",
};
const newKeyframeEffect = new KeyframeEffect(current, keyframe, options);
const animation = new Animation(newKeyframeEffect, document.timeline);
animation.play();
animation.onfinish = () => setIsExpended(!isExpended);
}
};
return <div ref={ref} style={{width: "300px", background: "red"}}>
<button onClick={onClick} type="button">Click</button>
</div>
}
Try this
https://stackblitz.com/edit/react-17rkkx?file=src%2FApp.js
Reference
useRef - https://reactjs.org/docs/hooks-reference.html#useref
Web Animation API - https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API

Related

crop image with react - customize react-easy-crop styling

I'm trying to make a very simple react component that would crop images with react-easy-crop. Apparently it is possible to customize the style of react-easy-crop module with style prop that takes 3 objects: containerStyle, mediaStyle and cropAreaStyle.
This is the default layout:
I want to expand cropArea to full width of its container and to fit media in it by height (so that we don't see the part of the original image outside of cropArea) but can't figure out how to do it. The cropAreaStyle object doesn't seem to affect width or height since it is calculated and injected in the module file (even after setting disableAutomaticStylesInjection to true).
import React from 'react'
import ReactDOM from 'react-dom'
import Cropper from 'react-easy-crop'
import './styles.css'
class App extends React.Component {
state = {
imageSrc:
'https://img.huffingtonpost.com/asset/5ab4d4ac2000007d06eb2c56.jpeg?cache=sih0jwle4e&ops=1910_1000',
crop: { x: 0, y: 0 },
zoom: 1,
aspect: 1 / 1,
style: { containerStyle: { position: "absolute", top: "0", width: "calc(100% - 2px)", height: window.innerWidth, overflow: "hidden", border: "1px solid black" },
mediaStyle: { height: "100%", display: "block" },
cropAreaStyle: {position: "absolute", top: "0", border: "1px solid black", width: "100%", height: "100%" }}
}
onCropChange = (crop) => {
this.setState({ crop })
}
onCropComplete = (croppedArea, croppedAreaPixels) => {
console.log(croppedArea, croppedAreaPixels)
}
onZoomChange = (zoom) => {
this.setState({ zoom })
}
render() {
return (
<div className="App">
<div className="crop-container">
<Cropper
image={this.state.imageSrc}
crop={this.state.crop}
zoom={this.state.zoom}
aspect={this.state.aspect}
onCropChange={this.onCropChange}
onCropComplete={this.onCropComplete}
onZoomChange={this.onZoomChange}
style={this.state.style}
disableAutomaticStylesInjection={true}
/>
</div>
</div>
)
}
}
const rootElement = document.getElementById('root')
ReactDOM.render(<App />, rootElement)
This is what I'm trying to achieve:
The black square is cropArea that I can't resize...
I want cropArea to remain square.
Is there an easy way to do this, without changing the module file?
The solution with another module is acceptable also
Thanks in advance
I tried to use the object cropAreaStyle but it's not working, instead use the prop cropSize and don't pass the prop aspect.
In order to get the height of the media pass the prop onMediaLoaded:
onMediaLoad = (mediaSize) => {
this.setState({
cropHeight: mediaSize.height,
});
};
App.js
import React from 'react';
import ReactDOM from 'react-dom';
import Cropper from 'react-easy-crop';
import './style.css';
class App extends React.Component {
state = {
imageSrc:
'https://img.huffingtonpost.com/asset/5ab4d4ac2000007d06eb2c56.jpeg?cache=sih0jwle4e&ops=1910_1000',
crop: { x: 0, y: 0 },
zoom: 1,
cropHeight: 0,
};
onCropChange = (crop) => {
this.setState({ crop });
};
onCropComplete = (croppedArea, croppedAreaPixels) => {
console.log(croppedArea, croppedAreaPixels);
};
onZoomChange = (zoom) => {
this.setState({ zoom });
};
onMediaLoad = (mediaSize) => {
this.setState({
cropHeight: mediaSize.height,
});
};
render() {
const cropSize = {
height: `${this.state.cropHeight}px`,
width: '100%',
};
return (
<div className="App">
<div className="crop-container">
<Cropper
image={this.state.imageSrc}
crop={this.state.crop}
zoom={this.state.zoom}
onCropChange={this.onCropChange}
onCropComplete={this.onCropComplete}
onZoomChange={this.onZoomChange}
onMediaLoaded={this.onMediaLoad}
cropSize={cropSize}
/>
</div>
</div>
);
}
}
export default App;
Demo: https://stackblitz.com/edit/react-4zmgud
It seems that what you need is the objectFit property set to vertical-cover.
See this demo: https://codesandbox.io/s/crazy-liskov-04u7m0

Next.js not re-rendering UI on state change

On my Next js project I am looping through an array of amenities that displays a div that when clicked toggles an active prop. The addAmenity function handles the logic that loops through the amenities array and toggles the specific array item's active property. If the active prop of the div is true the .amenities-active class is supposed to be applied to it and the background of the div should turn green but it does not. Is there any idea as to what I am doing wrong. The console.log(tempList) confirms a change to true when clicked on a false amenity but the UI does not change to have a green background color.
//Next js
const amenitiesDefaultArr = [
{
data: "bathroom",
text: "Private Bathroom",
icon: <WcIcon fontSize="large" />,
active: false,
},
{
data: "dining",
text: "Dining Hall",
icon: <FastfoodIcon fontSize="large" />,
active: false,
},
{
data: "wifi",
text: "Wifi",
icon: <WifiIcon fontSize="large" />,
active: false,
}
]
const addAmenity = (e) => {
let dataItem = e.currentTarget.dataset.amenity
let tempList = amenitiesList
tempList.map(el => {
if (el.data === dataItem) el.active = !el.active
return el
})
console.log(tempList)
setAmenitiesList(tempList)
}
const AddDorm = () => {
const [amenitiesList, setAmenitiesList] = useState(amenitiesDefaultArr)
return (
<>
{
amenitiesList.map(el => {
const {data, text, icon } = el
let { active } = el
return (
<div
className={`amenity ${active && `amenity-active`}`}
key={data}
data-amenity={data}
onClick={(e) => addAmenity(e)}
>
<p>{text}</p>
{icon}
</div>
)
</>
})
)
/* CSS */
.amenity {
padding: 0.5rem 1rem;
display: flex;
align-items: center;
border-radius: 50px;
box-shadow: 5px 5px 10px #919191,
-5px -5px 10px #ffffff;
z-index: 4;
cursor: pointer;
}
.amenity-active {
background-color: var(--green);
}
The main problem is you are trying to pass data in a JSX element as you would do in html.
data-amenity={data}
React does work this way.So, e.currentTarget.dataset.amenity is always undeined.Instead, React uses refs to access dom elements. You can learn more about refs in the official React documentation. But in your case, you don't even need any ref as you can send data directly to any function. Check:
<div
className={`amenity ${active && `amenity-active`}`}
key={data}
// data-amenity={data}
onClick={() => addAmenity(data)}
>
<p>{text}</p>
</div>
and in addAmenity just receive it
const addAmenity = (incoming) => {
let dataItem = incoming
...
}
Below I provide the version of your code I fixed for you which is working perfect. Please let me know if this was helpful.
//Next js
import { useState } from 'react'
const amenitiesDefaultArr = [
{
data: "bathroom",
text: "Private Bathroom",
icon: <WcIcon fontSize="large" />,
active: false,
},
{
data: "dining",
text: "Dining Hall",
icon: <FastfoodIcon fontSize="large" />,
active: false,
},
{
data: "wifi",
text: "Wifi",
icon: <WifiIcon fontSize="large" />,
active: false,
}
]
const AddDorm = () => {
const [amenitiesList, setAmenitiesList] = useState(amenitiesDefaultArr)
const addAmenity = (incoming) => {
let dataItem = incoming
const tempList = amenitiesList.map(el => {
if (el.data === dataItem) el.active = !el.active
return el
})
console.log(tempList)
setAmenitiesList(tempList)
}
return (
<>
{
amenitiesList.map(el => {
const {data, text, icon} = el
let { active } = el
return (
<div
className={`amenity ${active && `amenity-active`}`}
key={data}
// data-amenity={data}
onClick={() => addAmenity(data)}
>
<p>{text}</p>
{icon}
</div>
)})}
</>
)
}
export default AddDorm

Why clientHeight or offsetHeight not available on ref element on toggled div in React component with useRef hook

I have a React component which show and hide some content.
Here is working example.
const Collapse = ({ children }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const [height, setHeight] = useState<number | undefined>(0);
const toggledHeight = isOpen ? height : 0;
const myRef = useRef<HTMLDivElement>(null);
const toggle = () => setIsOpen(!isOpen);
useEffect(() => {
setHeight(myRef.current?.clientHeight);
}, [myRef]);
// useLayoutEffect(() => {
// setHeight(myRef.current?.clientHeight);
// }, []);
return (
<React.Fragment>
<div
ref={myRef}
className={`content ${isOpen ? "isOpen" : ""}`}
style={{ height: `${toggledHeight}px` }}
>
{children}
{console.log(height)}
{console.log(myRef)}
</div>
<button type="button" onClick={toggle}>
<span>Toggle content</span>
</button>
</React.Fragment>
);
};
export default Collapse;
I want to create a css height transition when showing and hiding the content.
Why is myRef.current?.offsetHeight or myRef.current?.clientHeight not available and always 0?
In the Style you are passing the dynamic height. I think it's not getting in the pixels. So you need to change from
style={{ height: ${height+"px"} }}
Looks like you are telling it from the first moment to be 0 :)
No need to look for exact height number, just animate max-height
I've managed to animate you code like this:
const Collapse = ({ children }: Props) => {
const [isOpen, setIsOpen] = useState(false);
const toggle = () => {
setIsOpen(!isOpen);
};
return (
<>
<div
style={{
maxHeight: isOpen ? '1000px' : '0px',
transition: 'max-height 1s ease-in-out',
}}
>
{children}
</div>
<button type="button" onClick={toggle}>
<span>Toggle content</span>
</button>
</>
);
};

styling material-table (specifically height)

I'm using material table from material UI react from here and I can't figure out how to style the table properly, especially the height of rows.
I've tried all the ways explained in doc, alternatively and altogether and nothing makes it.
But there seems to be something somewhere that takes precedence over my css. Some of these CSS prop do have effects (like textAlign and vertical align), but most don't.
What am I missing here ?
I also overrode some css directly, without any effect
tr {
height: 100px;
}
tr td {
height: auto !important;
overflow:scroll;
}
MuiTableRow-root.{
height:50px;
max-height:50px;
}
here's my code:
let columns = [];
//console.log(this.props.data.matrix[0][0])
for(var i=0;i<this.props.headers.length;i++){
let filtering = false;
let obj = {'title':this.props.headers[i].display,'field':this.props.headers[i].accessor,headerStyle: {
backgroundColor: '#315481',
color: '#FFF',
},cellStyle:{maxHeight:50,height:50,overflow:'scroll',padding:1}}
columns.push(obj)
}
return(
<div>
<a className="anchor" id={this.props.id}></a>
<div className="centeringDiv"><h3>{this.props.title}</h3></div>
{this.props.infoText && <SectionInfo infoText = {this.props.infoText} />}
<div style={{ maxWidth: '100%',marginTop:15}}>
<MaterialTable
columns={columns}
data={this.props.dataToDisplay}
title={''}
onRowClick={((evt, selectedRow) => this.setState({ selectedRow }))}
options={{
pageSize:pageSizeCalc,
maxBodyHeight:400,
paging:true,
filtering:this.props.filtering,
pageSizeOptions: pageSizeOptionsCalc,
//fixedColumns: {left: 1},
rowStyle: rowData => ({backgroundColor: (this.state.selectedRow && this.state.selectedRow.tableData.id === rowData.tableData.id) ? '#EEE' : '#FFF',maxHeight:100}),
headerStyle:{height:60,maxHeight:60,padding:0,overflow:'none',lineHeight:1,textAlign:'center',fontSize:'2.5vh'},
cellStyle:{height:100,overflowY:'scroll',verticalAlign:'top',fontSize:'2vh',overflow:'scroll',padding:1}
}}
/>
finally the computed css
Try to style cell with cellStyle property like:
const cellStyle = {
padding: '0 14px',
};
const [state, setState] = React.useState<TableState>({
columns: [
{ title: 'ID', field: 'id', cellStyle }...
]
...
})

React style width based on length of props

I have a react component:
const TeamRow = props => {
const items = props.team.map((item, index) => {
const con_width = 100 / props.team.length;
const Style = {
team_img: {
width: "50px",
height: "auto"
},
member_container: {
float: "left",
width: `{con_width}%`
}
};
return (
<div style={Style.member_container}>
<img style={Style.team_img} src={logo} />
<p>{item.name}</p>
<p>{item.bio}</p>
</div>
);
});
return <div>{items}</div>;
};
The idea is to have a set of divs next to one another horizontally. The width of each div should equal 100% divided by the number of divs.
{con_width} is calculated correctly, but in the web inspector, there is no 'width' style at all. It just gets ignored by react. What am I doing wrong?
Change
width: `{con_width}%`
To
width: `${con_width}%`
You need to use ${} in template literals to print the value

Resources