I am using React Native Typescript for my. In My app there are three elements one is Map, bottom-sheet (which position is absolute) and One button. I have created one custom bottom sheet. Bottom-sheet is animated Scrollable. Inside the Bottom-sheet I used Custom search bar and under the search bar there is Flat-list where I am rendering the data. In IOS simulator I can see the FlatList card under the Button. which is really bad from UI (IOS-Image) but in Android it looks perfect Android-simulator. I want to hide the flat-List data which is coming under the button. I tried background color white and tried with height, width, bottom.But none of of work. I want to make efficient css position instead of hard code position. But I don't know how to fixed it.
I share my code in Expo-snacks
This is my custom Bottom-sheet
import React from "react";
import {
SafeAreaView,
StyleSheet,
Text,
useWindowDimensions,
TouchableOpacity,
View,
} from "react-native";
import Animated, {
Extrapolate,
interpolate,
useAnimatedGestureHandler,
useAnimatedStyle,
withTiming,
} from "react-native-reanimated";
import { PanGestureHandler } from "react-native-gesture-handler";
import styled from "styled-components/native";
interface Props {
panY: number;
children: React.ReactNode;
}
export default function BottomSheet({ panY, children }: Props) {
const { height } = useWindowDimensions();
const gestureHandler = useAnimatedGestureHandler(
{
onStart(_, context) {
context.startY = panY.value;
},
onActive(event, context) {
panY.value = context.startY + event.translationY;
},
onEnd() {
if (panY.value < -height * 0.4) {
panY.value = withTiming(-(height * 0.6));
} else {
panY.value = withTiming(0);
}
},
},
[height]
);
const animatedStyle = useAnimatedStyle(() => {
return {
transform: [
{
translateY: interpolate(panY.value, [-1, 0], [-1, 0], {
extrapolateLeft: Extrapolate.EXTEND,
extrapolateRight: Extrapolate.CLAMP,
}),
},
],
};
});
return (
<PanGestureHandler onGestureEvent={gestureHandler}>
<Animated.View
style={[styles.container, { top: height * 0.7 }, animatedStyle]}
>
{children}
</Animated.View>
</PanGestureHandler>
);
}
const styles = StyleSheet.create({
container: {
position: "absolute",
top: 0,
left: 0,
right: 0,
backgroundColor: "white",
shadowColor: "black",
shadowOffset: {
height: -6,
width: 0,
},
shadowOpacity: 0.1,
shadowRadius: 5,
borderTopEndRadius: 15,
borderTopLeftRadius: 15,
},
});
This is My App component
import React, { useState } from "react";
import {
StyleSheet,
Text,
View,
Dimensions,
useWindowDimensions,
SafeAreaView,
StatusBar,
RefreshControl,
} from "react-native";
import styled from "styled-components/native";
import MapView from "react-native-maps";
import { useSharedValue } from "react-native-reanimated";
import { FlatList } from "react-native-gesture-handler";
import BottomSheet from "./ActionSheet";
import BottomButton from "./BottomButton";
import SafeAreaWrapper from "./SafeAreaWrapper";
import SearchBar from "./SearchBar";
const { width, height } = Dimensions.get("screen");
const api =
"http://open-api.myhelsinki.fi/v1/events/?distance_filter=60.1699%2C24.9384%2C10&language_filter=en&limit=100";
const initialRegion = {
latitudeDelta: 15,
longitudeDelta: 15,
latitude: 60.1098678,
longitude: 24.7385084,
};
export default function App() {
const { width, height } = useWindowDimensions();
const [loading, setLoading] = useState(false);
const [data, setData] = React.useState([]);
const updateState = async () => {
try {
const response = await fetch(api);
const data = await response.json();
setData(data.data);
} catch (error) {
console.log("failed to catch", error);
}
};
React.useEffect(() => {
updateState();
}, []);
const y = useSharedValue(0);
return (
<SafeAreaWrapper>
<Container>
<MapContent>
<MapView style={styles.mapStyle} initialRegion={initialRegion} />
</MapContent>
<BottomSheet panY={y}>
<Content>
<SearchContainer>
<SearchBar placeholder={"write some thing"} />
</SearchContainer>
{data && data === undefined ? (
<Text>loading</Text>
) : (
<HeroFlatList
data={data}
showsVerticalScrollIndicator={false}
refreshControl={
<RefreshControl
enabled={true}
refreshing={loading}
onRefresh={updateState}
/>
}
keyExtractor={(_, index) => index.toString()}
renderItem={({ item, index }) => {
const image = item?.description.images.map((img) => img.url);
const startDate = item?.event_dates?.starting_day;
return (
<EventContainer key={index}>
<EventImage
source={{
uri:
image[0] ||
"https://res.cloudinary.com/drewzxzgc/image/upload/v1631085536/zma1beozwbdc8zqwfhdu.jpg",
}}
/>
<DescriptionContainer>
<Title ellipsizeMode="tail" numberOfLines={1}>
{item?.name?.en}
</Title>
<DescriptionText>
{item?.description?.intro ||
"No description available"}
</DescriptionText>
<DateText>{startDate}</DateText>
</DescriptionContainer>
</EventContainer>
);
}}
/>
)}
</Content>
</BottomSheet>
<ButtonContainer>
<BottomButton
title={"Save"}
onPress={() => {
console.log("jjj");
}}
/>
</ButtonContainer>
</Container>
</SafeAreaWrapper>
);
}
const styles = StyleSheet.create({
mapStyle: {
width: width,
height: height, // I DON'T KNOW HOW TO CONVERT THEM INTO STYLED COMPONENT
},
});
const HeroFlatList = styled(FlatList).attrs({
contentContainerStyle: {
paddingTop: 20,
paddingBottom: 2000, // BAD PRACTICE
flexGrow: 1, //SEEMS LIKE IT DOES NOT WORK
},
})`
height: 2000px; // BAD PRACTICE
`;
const MapContent = styled.View`
flex: 1;
`;
const Content = styled.View`
flex: 1;
padding: 20px;
`;
const Container = styled.View`
flex: 1;
`;
const ButtonContainer = styled.View`
background-color: #fdfbfb; // My Button container
`;
const TextName = styled.Text`
color: orange;
font-weight: bold;
`;
const SearchContainer = styled.View`
margin-bottom: 5px;
`;
const Title = styled.Text`
font-size: 16px;
font-weight: 700;
margin-bottom: 5px;
`;
const DescriptionText = styled(Title)`
font-size: 14px;
opacity: 0.7;
`;
const DateText = styled(Title)`
font-size: 14px;
opacity: 0.8;
color: #0099cc;
`;
const EventImage = styled.Image`
width: 70px;
height: 70px;
border-radius: 70px;
margin-right: 20px;
`;
const DescriptionContainer = styled.View`
width: 200px;
`;
const EventContainer = styled.View`
flex-direction: row;
padding: 20px;
margin-bottom: 10px;
border-radius: 20px;
background-color: rgba(0, 0, 0, 0.8);
`;
Related
I have a dynamically sized collection of objects being passed into a Nav component that are being mapped and rendered as buttons. I want to apply a CSS animation to each button so that they slide in from off screen one at a time when the Nav component mounts. I figured that I would set up a loop through each one that updates a boolean value inside of a corresponding state object which applies the CSS class to the button to animate it, but each time that state object is updated, all of the buttons rerender which in turn starts all of the animations over. How can I prevent these rerenders?
// Nav.jsx
import React, { useState, useEffect } from 'react';
import { Button } from '../../../components';
import './Nav.scss';
const Nav = ({ actions }) => {
const [renderStates, setRenderStates] = useState(actions.reduce((accum, val) => {
return {...accum, [val.id]: false};
}, {}));
useEffect(() => {
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
const updateStates = async () => {
for (let i = 0; i < actions.length; i++) {
if (i > 0) {
await delay(75);
}
setRenderStates((prev) => ({
...prev,
[i]: true,
}));
};
};
updateStates();
}, [actions.length]);
return (
<div className='Nav'>
{actions.map((act) => (
<div className={`Nav__Button ${renderStates[act.id] ? 'Animate' : ''}`} key={act.id}>
<Button icon={act.icon} onClick={act.onClick} />
</div>
))}
</div>
);
};
export default Nav;
/* Nav.scss */
.Nav {
height: 100%;
width: fit-content;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
align-self: center;
padding: 1rem;
}
.Nav > * {
margin: 20% 0,
}
.Nav__Button {
margin-left: -5rem;
}
.Animate {
animation: slideInFromLeft .4s ease;
}
#keyframes slideInFromLeft {
0% {
margin-left: -5rem;
}
75% {
margin-left: .5rem;
}
100% {
margin-left: 0;
}
}
Here's a codesandbox that illustrates the problem (refresh the embedded browser to see the issue):
https://codesandbox.io/s/react-css-animations-on-timer-8mxnsz
Any help would be appreciated. Thanks.
You will need to create a from the elements inside actions.map and render a memoized version of it so that if the props do not change it will not re-render.
import { useState, useEffect, memo } from "react";
import "./styles.css";
const Test = ({ animate, label }) => {
return (
<div className={`Nav__Button ${animate ? "Animate" : ""}`}>
<button>{label}</button>
</div>
);
};
const TestMemo = memo(Test);
export default function App() {
const actions = [
{
id: 0,
label: "button 0"
},
{
id: 1,
label: "button 1"
},
{
id: 2,
label: "button 2"
},
{
id: 3,
label: "button 3"
}
];
const [renderStates, setRenderStates] = useState(
actions.reduce((accum, val) => {
return { ...accum, [val.id]: false };
}, {})
);
useEffect(() => {
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const updateStates = async () => {
for (let i = 0; i < actions.length; i++) {
if (i > 0) {
await delay(2000);
}
setRenderStates((prev) => ({
...prev,
[i]: true
}));
}
};
updateStates();
}, [actions.length]);
return (
<div className="App">
{actions.map((act) => (
<TestMemo animate={renderStates[act.id]} label={act.label} />
))}
</div>
);
}
I've almost got this working in a codesandbox, but the drop accuracy still isn't quite right. I'm at a loss for what else to try.
Here is my container:
import update from "immutability-helper";
import { useCallback, useState } from "react";
import { useDrop } from "react-dnd";
import { DraggableBox } from "./DraggableBox.js";
import { ItemTypes } from "./ItemTypes.js";
import { snapToGrid as doSnapToGrid } from "./snapToGrid.js";
const styles = {
width: 300,
height: 300,
border: "1px solid black",
position: "relative",
transform: "scale(.8)"
};
export const Container = ({ snapToGrid }) => {
const [boxes, setBoxes] = useState({
a: { top: 20, left: 80, title: "Drag me around" },
b: { top: 180, left: 20, title: "Drag me too" }
});
const moveBox = useCallback(
(id, left, top) => {
setBoxes(
update(boxes, {
[id]: {
$merge: { left, top }
}
})
);
},
[boxes]
);
const [, drop] = useDrop(
() => ({
accept: ItemTypes.BOX,
drop(item, monitor) {
const delta = monitor.getDifferenceFromInitialOffset();
let left = item.left + delta.x;
let top = item.top + delta.y;
if (snapToGrid) {
[left, top] = doSnapToGrid(left, top);
}
moveBox(item.id, left, top);
return undefined;
}
}),
[moveBox]
);
return (
<div ref={drop} style={styles}>
{Object.keys(boxes).map((key) => (
<DraggableBox key={key} id={key} {...boxes[key]} />
))}
</div>
);
};
Here is my draggable box:
import { memo, useEffect } from 'react'
import { useDrag } from 'react-dnd'
import { getEmptyImage } from 'react-dnd-html5-backend'
import { Box } from './Box.js'
import { ItemTypes } from './ItemTypes.js'
function getStyles(left, top, isDragging) {
const transform = `translate3d(${left }px, ${top }px, 0)`
return {
position: 'absolute',
transform,
WebkitTransform: transform,
// IE fallback: hide the real node using CSS when dragging
// because IE will ignore our custom "empty image" drag preview.
opacity: isDragging ? 0 : 1,
height: isDragging ? 0 : '',
}
}
export const DraggableBox = memo(function DraggableBox(props) {
const { id, title, left, top } = props
const [{ isDragging }, drag, preview] = useDrag(
() => ({
type: ItemTypes.BOX,
item: { id, left, top, title },
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
}),
[id, left, top, title],
)
useEffect(() => {
preview(getEmptyImage(), { captureDraggingState: true })
}, [])
return (
<div
ref={drag}
style={getStyles(left, top, isDragging)}
role="DraggableBox"
>
<Box title={title} />
</div>
)
})
Here is my custom drag layer:
import { useDragLayer } from "react-dnd";
import { BoxDragPreview } from "./BoxDragPreview.js";
import { ItemTypes } from "./ItemTypes.js";
import { snapToGrid } from "./snapToGrid.js";
const layerStyles = {
position: "fixed",
pointerEvents: "none",
zIndex: 100,
left: 0 ,
top: 0 ,
width: "100%",
height: "100%",
};
function getItemStyles(initialOffset, currentOffset, isSnapToGrid) {
if (!initialOffset || !currentOffset) {
return {
display: 'none',
}
}
let { x, y } = currentOffset
if (isSnapToGrid) {
x -= initialOffset.x
y -= initialOffset.y
;[x, y] = snapToGrid(x, y)
x += initialOffset.x
y += initialOffset.y
}
const transform = ` translate(${x }px, ${y }px)`
return {
transform,
WebkitTransform: transform,
}
}
export const CustomDragLayer = (props) => {
const {
itemType,
isDragging,
item,
initialOffset,
currentOffset,
delta
} = useDragLayer((monitor) => ({
item: monitor.getItem(),
itemType: monitor.getItemType(),
initialOffset: monitor.getInitialSourceClientOffset(),
currentOffset: monitor.getSourceClientOffset(),
delta: monitor.getDifferenceFromInitialOffset(),
isDragging: monitor.isDragging()
}));
function renderItem() {
switch (itemType) {
case ItemTypes.BOX:
return <BoxDragPreview title={item.title} />;
default:
return null;
}
}
if (!isDragging) {
return null;
}
return (
<div style={layerStyles}>
<div style={getItemStyles(initialOffset, currentOffset, props.snapToGrid)}>{renderItem()}</div>
</div>
);
};
And here is my box drag preview:
import { memo, useEffect, useState } from "react";
import { Box } from "./Box.js";
const styles = {
display: "inline-block",
transform: "scale(.8)",
transformOrigin: "top left"
};
export const BoxDragPreview = memo(function BoxDragPreview({ title }) {
const [tickTock, setTickTock] = useState(false);
useEffect(
function subscribeToIntervalTick() {
const interval = setInterval(() => setTickTock(!tickTock), 500);
return () => clearInterval(interval);
},
[tickTock]
);
return (
<div style={styles}>
<Box title={title} yellow={tickTock} preview />
</div>
);
});
I tried item.left + delta.x / scaleValue, but that was even less accurate than this example. Here is the url to the codesandbox.
https://codesandbox.io/s/dazzling-wozniak-2fvs81?file=/src/BoxDragPreview.js:0-616
For the below line of code, how can I dispatch notificationsStateUpdate without onClick? I want this action to be dispatched if notificationClicked is true, so I currently have a ternary expression set up.
However, I can't seem to get the syntax to work. Is it possible to dispatch in this scenario?
{notificationClicked ?
<NotificationList
notifications={newNotifications} />
dispatch(notificationsStateUpdate({newNotifications}))
: null}
Full code for context
import React, { useState, useEffect, useRef } from 'react';
import { useSelector, useDispatch, connect } from 'react-redux';
import _ from 'lodash';
import {makeStyles, useTheme} from '#material-ui/core/styles';
import usePrevious from '../hooks/usePrevious';
import NotificationList from './NotificationList';
import { notificationsStateUpdate } from '../actions';
export default function Notifications(props) {
const [newNotifications, setNewNotifications] = useState([]);
const users = useSelector(state => state.users);
const notificationClicked = useSelector(state => state.notificationClicked)
const prevUsers = usePrevious(users);
const dispatch = useDispatch();
console.log('inside', users);
const isEqual = _.isEqual(prevUsers, users);
const timestamp = !isEqual ? new Date().getTime() : new Date("1991-09-24").getTime();
useEffect(() => {
const notifications = [];
console.log('users', users);
users.forEach((user) => {
if (user.uid === props.uid && user.posts) {
user.posts.forEach((postContent) => {
const likes = postContent.like ? Object.values(postContent.like) : null
const comments = postContent.comments_text ? Object.values(postContent.comments_text) : null
if (likes){
let filtererdLikes = likes.filter(post => {
return post.like_notification === false
})
notifications.push(filtererdLikes)
}
if (comments){
let letfilteredComments = comments.filter(post => {
return post.comment_notification === false
})
notifications.push(letfilteredComments)
}
})
}
});
const notificationsDataClean = notifications.flat(Infinity)
setNewNotifications(notificationsDataClean);
}, [timestamp]);
const useStyles = makeStyles((theme) => ({
body: {
margin: '25',
background: '#3f51b5'
},
iconButton: {
position: 'relative',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: 50,
height: 50,
color: '#333333',
background: '#dddddd',
border: 'none',
outline: 'none',
borderRadius: '50%',
'&:hover': {
cursor: 'pointer'
},
'&:active': {
background: '#cccccc'
}
},
iconButton__badge: {
position: 'absolute',
top: -10,
right: -10,
width: 25,
height: 25,
background: 'red',
color: '#ffffff',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
borderRadius: '50%'
}
}
));
const classes = useStyles();
const theme = useTheme();
return (
<div>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Icon Button Notification Badge</title>
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" />
</head>
<body className={classes.body}>
<button type="button" className={classes.iconButton}>
<span class="material-icons">notifications</span>
<span className={classes.iconButton__badge}>{newNotifications.length}</span>
</button>
</body>
{notificationClicked ? <NotificationList notifications={newNotifications} /> dispatch(notificationsStateUpdate({newNotifications})) : null}
</div>
);
}
If I am understanding correctly, this should work
<NotificationList
notificationClicked, // pass in as prop instead of a ternary
notifications={newNotifications} />
then call your dispatch in useEffect in NotificationList
<NotificationList>
/////
useEffect =(() => {
//Whatever else
if (notificationClicked) {
dispatch(notificationsStateUpdate({newNotifications}))
}
},[notificationClicked])
I am trying to override the default styles in a Mui component using the classes prop and passing the css into it. So far I have been unsuccessful and I'm not sure what I have wrong.
The motivation for not using makeStyles is to keep our files cleaner and Emotion is faster.
I am able to do it successfully using makeStyles like this:
import { css } from '#emotion/react';
import HelpIcon from '#material-ui/icons/Help';
import { Tooltip } from '#material-ui/core';
import ClickAwayListener from '#material-ui/core/ClickAwayListener';
import { useState } from 'react';
import { cbNeutral } from '../../src/theme/palette';
import { makeStyles } from '#material-ui/core/styles';
import PropTypes from 'prop-types';
const useStyles = makeStyles(() => ({
arrow: {
'&:before': {
border: `1px solid ${cbNeutral[900]}`
},
color: cbNeutral[1000]
},
tooltip: {
backgroundColor: cbNeutral[1000],
border: `1px solid ${cbNeutral[900]}`,
color: cbNeutral[400],
boxShadow: '0px 8px 16px 0px rgba(14, 13, 38, 0.12)',
maxWidth: '385px',
fontSize: '1.125rem',
padding: '18px 25px',
fontWeight: 400
},
root: {
fill: cbNeutral[700],
'&:hover': {
fill: cbNeutral[400],
cursor: 'pointer'
}
}
}));
const ToolTip = ({ text }) => {
const styles = useStyles();
const [open, setOpen] = useState(false);
return (
<ClickAwayListener onClickAway={() => setOpen(false)}>
<Tooltip
arrow
title={text}
placement="right-start"
open={open}
classes={{
arrow: styles.arrow,
tooltip: styles.tooltip
}}
>
<HelpIcon onClick={() => setOpen(true)} classes={{ root: styles.root }} />
</Tooltip>
</ClickAwayListener>
);
};
export default ToolTip;
ToolTip.propTypes = {
text: PropTypes.string.isRequired
};
I am trying to do it with Emotion like this:
import { css } from '#emotion/react';
import HelpIcon from '#material-ui/icons/Help';
import { Tooltip } from '#material-ui/core';
import ClickAwayListener from '#material-ui/core/ClickAwayListener';
import { useState } from 'react';
import { cbNeutral } from '../../src/theme/palette';
import { makeStyles } from '#material-ui/core/styles';
import PropTypes from 'prop-types';
const toolTipCss = css`
.arrow {
color: ${cbNeutral[1000]};
&:before {
border: 1px solid ${cbNeutral[900]};
}
}
.tooltip {
background-color: ${cbNeutral[1000]};
border: 1px solid ${cbNeutral[900]};
color: cbNeutral[400];
box-shadow: 0px 8px 16px 0px rgba(14, 13, 38, 0.12);
max-width: 385px;
font-size: 1.125rem;
padding: 18px 25px;
font-weight: 400;
}
.help-icon {
fill: ${cbNeutral[700]};
&:hover {
fill: ${cbNeutral[400]};
cursor: pointer;
}
}
`;
const ToolTip = ({ text }) => {
const [open, setOpen] = useState(false);
return (
<div css={toolTipCss}>
<ClickAwayListener onClickAway={() => setOpen(false)}>
<Tooltip
arrow
title={text}
placement="right-start"
open={open}
classes={{
arrow: 'arrow',
tooltip: 'tooltip'
}}
>
<HelpIcon onClick={() => setOpen(true)} className="help-icon" />
</Tooltip>
</ClickAwayListener>
</div>
);
};
export default ToolTip;
ToolTip.propTypes = {
text: PropTypes.string.isRequired
};
In 👆 that version where I use className="help-icon", it works as expected, but classes={{arrow: 'arrow', tooltip: 'tooltip'}} does not change the default MUI styles.
I am using these versions of MUI and Emotion
"#emotion/react": "^11.4.0",
"#material-ui/core": "^4.11.4",
I know we have it working like this in another app using older versions of MUI and emotion. I don't know if something has changed in the mean time or of I just have something wrong with my syntax. Any help would be appreciated.
I use Material UI in ReactJS and have the following component to render an SVG:
import React, { FunctionComponent } from "react";
import SvgIconMUI from "#material-ui/core/SvgIcon";
import { SvgIconProps } from "./types";
import { ReactComponent as HelloWorld } from "Icon/Sections/icon-flat.svg";
const SvgIcon: FunctionComponent<SvgIconProps> = ({
slug,
classes,
svgIconViewBox = "0 0 96 96",
}) => {
const SpecificSpartenIcon = HelloWorld;
return (
SpecificSpartenIcon && (
<div className={classes.svgContainer}>
<SvgIconMUI viewBox={svgIconViewBox} className={classes.svgIcon}>
<SpecificSpartenIcon />
</SvgIconMUI>
</div>
)
);
};
export default SvgIcon;
What I want to achieve: set another color for the SVG.
What I have tried to change the color:
import { makeStyles, Theme } from "#material-ui/core";
interface Props {
colors: ExpertColor;
}
const svgIconViewBox = "0 0 96 96";
const selectedIconStyles = { fontSize: 30 };
const useStyles = makeStyles<Theme, Props>((theme: Theme) => ({
svgContainer: {
display: "flex",
justifyContent: "center",
position: "relative",
height: "100%",
width: "100%",
padding: 0,
fill: "red !important;",
color: "red !important;",
},
svgIcon: {
position: "absolute",
height: "55px",
width: "55px",
top: 0,
fill: "red !important;",
color: "red !important;",
},
}));
export default { useStyles, svgIconViewBox, selectedIconStyles };
It does not work. Do you have an idea ?