How to do React-horizontal scroll using mouse wheel - reactjs

I have used tailwind-CSS on react js I want to scroll horizontally using mouse wheel when user hover over the card section so for pc users can scroll horizontally by using mouse wheel instead of both shift and mouse wheel.
Live Anywhere
<div className="flex space-x-3 overflow-y-scroll scrollbar-hide p-3 -ml-3">
{cardsDate?.map(({ img, title }) => (
<MediumCard key={img} img={img} title={title} />
))}
</div>
</section>

You can use a custom scroll function as a ref to your div.
export function useHorizontalScroll() {
const elRef = useRef();
useEffect(() => {
const el = elRef.current;
if (el) {
const onWheel = e => {
if (e.deltaY == 0) return;
e.preventDefault();
el.scrollTo({
left: el.scrollLeft + e.deltaY,
behavior: "smooth"
});
};
el.addEventListener("wheel", onWheel);
return () => el.removeEventListener("wheel", onWheel);
}
}, []);
return elRef;
}
The above function can be imported and used as follows:
<div className="App" ref={scrollRef} style={{ overflow: "auto" }}>
<div style={{ whiteSpace: "nowrap" }}>
<Picture />
</div>
</div>
I created a codesandbox as an example.

try with scrollBy working well
const element = document.querySelector("#container");
element.addEventListener('wheel', (event) => {
event.preventDefault();
element.scrollBy({
left: event.deltaY < 0 ? -30 : 30,
});
});
container with his children elements

You can do this inline without the use of refs or hooks with the following code:
<div
style={{ scrollbarColor="transparent", overflowX="auto" }}
onWheel={(e) => {
// here im handling the horizontal scroll inline, without the use of hooks
const strength = Math.abs(e.deltaY);
if (e.deltaY === 0) return;
const el = e.currentTarget;
if (
!(el.scrollLeft === 0 && e.deltaY < 0) &&
!(
el.scrollWidth -
el.clientWidth -
Math.round(el.scrollLeft) ===
0 && e.deltaY > 0
)
) {
e.preventDefault();
}
el.scrollTo({
left: el.scrollLeft + e.deltaY,
// large scrolls with smooth animation behavior will lag, so switch to auto
behavior: strength > 70 ? "auto" : "smooth",
});
}}
>
// ...
</div>

Related

React gallery App. I want Add tags to an image individually but the tag is being added to all images. How can I solve this?

**> This is my Gallery Component **
import React, {useState} from 'react';
import useFirestore from '../hooks/useFirestore';
import { motion } from 'framer-motion';
const Gallery = ({ setSelectedImg }) => {
const { docs } = useFirestore('images');
here im setting the state as a Tags array
const [tags, setTags] = useState([""]);
const addTag = (e) => {
if (e.key === "Enter") {
if (e.target.value.length > 0) {
setTags([...tags, e.target.value]);
e.target.value = "";
}
}
};
functions for adding and removing Tags
const removeTag = (removedTag) => {
const newTags = tags.filter((tag) => tag !== removedTag);
setTags(newTags);
};
return (
<>
<div className="img-grid">
{docs && docs.map(doc => (
< motion.div className="img-wrap" key={doc.id}
layout
whileHover={{ opacity: 1 }}s
onClick={() => setSelectedImg(doc.url)}
>
here Im adding the Tag input to each Image...the problem is that when adding a Tag is added to all the pictures. I want to add the tags for the image that I´m selecting.
<div className="tag-container">
{tags.map((tag, ) => {
return (
<div key={doc.id} className="tag">
{tag} <span onClick={() => removeTag(tag)}>x</span>
</div>
);
})}
<input onKeyDown={addTag} />
</div>
<motion.img src={doc.url} alt="uploaded pic"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 1 }}
>
</motion.img>
</motion.div>
))}
</div>
</>
)
}
export default Gallery;
The tags array that you are using to store values entered by the user are not unique with respect to each image item. Meaning, every image item in your program is using the same instance of the tags array, what you need to do is
Either create an object that stores an array of tags for each image:
const [tagsObj, setTagsObj] = {}, then while adding a new tag for say image_1, you can simply do setTagsObj(prevObj => {...prevObj, image_1: [...prevObj?.image_1, newTagValue]},
Or create an Image Component which would then handle tags for a single image:
Gallery Component:
{
imageList.map(imageEl =>
<ImageItem key={imageEl} image={imageEl} />
)
}
ImageItem Component:
import {useState} from 'react';
export default function ImageItem({image}) {
const [tags, setTags] = useState([]);
const addTag = (e) => {
if (e.key === "Enter") {
const newVal = e.target.value;
if (newVal.length > 0) {
setTags(prevTags => [...prevTags, newVal]);
e.target.value = '';
}
}
};
const removeTag = (removedTag) => {
setTags(prevTags => prevTags.filter((tag) => tag !== removedTag));
}
return (
<div style={{margin: '12px', padding: '12px', width: '100px', height:'100px', display:'flex', flexDirection: 'column', alignItems:'center'}}>
<span>{image}</span>
{tags.map((tag, index) => {
return (
<div key={tag+index}>
{tag} <span onClick={() => removeTag(tag)}>x</span>
</div>
);
})}
<input onKeyDown={addTag} />
</div>
);
}
Refer this sandbox for ease, if available Gallery unique image tags sandbox
I suggest using the second method, as it is easy to understand and debug later on.
I hope this helps, please accept the answer if it does!

How to get the index of items visible in the viewport?

folks, I am building a chat app. Currently, I am working on a feature in which when the user scrolls a sticky date is shown to the top, based on the date of the messages visible in the viewport area. But I don't know how to get the index of the last item visible in the viewport.
Here is the codesandbox
Here is the code
const MessageComponent = () => {
const { id } = useParams();
const {
data: messageData,
isSuccess,
isError,
isLoading,
isFetching,
hasMoreData,
firstData,
isUninitialized,
} = useGetData(id);
const { loadMore } = useGetScrollData(messageData, id);
const user = User();
const chatId =
messageData[0]?.type == "date"
? messageData[1]?.chat?._id
: messageData[0]?.chat?._id;
const islastItemChatId: boolean = messageData.length > 0 && chatId != id;
const scrollRef = useScrollRef(islastItemChatId, id);
const scrollFunc = (e: any) => {
// let m = e.target.scrollHeight + e.target.scrollTop;
// let i = e.target.scrollHeight - m;
// console.log({ i, e });
if (!scrollRef.current) return;
const containerMiddle =
scrollRef.current.scrollTop +
scrollRef.current.getBoundingClientRect().height / 2;
const infiniteScrollItems = scrollRef.current.children[0].children;
console.log({ containerMiddle, infiniteScrollItems, e: e.target });
};
return (
<>
{messageData.length > 0 && (
<div
id="scrollableDiv"
style={{
height: "80%",
overflow: "auto",
display: "flex",
flexDirection: "column-reverse",
padding: "10px 0px",
}}
ref={scrollRef}
>
<InfiniteScroll
dataLength={messageData.length}
hasMore={hasMoreData}
onScroll={scrollFunc}
loader={
<div className="loading-container">
<div className="lds-ring">
<div></div>
</div>
</div>
}
endMessage={
<div className="message-container date">
<div className={`text-container sender large-margin`}>
<span>You have seen all the messages</span>
</div>
</div>
}
style={{ display: "flex", flexDirection: "column-reverse" }}
next={loadMore}
inverse={true}
scrollableTarget="scrollableDiv"
>
{messageData.map((item, index: number) => {
const isUserChat = item?.sender?._id === user._id;
const className =
item?.type == "date"
? "date"
: isUserChat
? "user-message"
: "sender-message";
const prevItem: IMessageData | null =
index < messageData?.length ? messageData[index - 1] : null;
const nextItem: IMessageData | null =
index < messageData?.length ? messageData[index + 1] : null;
return (
<Message
key={item._id}
item={item}
prevItem={prevItem}
className={className}
isUserChat={isUserChat}
index={index}
nextItem={nextItem}
/>
);
})}
</InfiniteScroll>
</div>
)}
</>
);
};
The recommended way to check the visibility of an element is using the Intersection Observer API.
The lib react-intersection-observer can help you to observe all the elements and map the indexes with some states.
However, for your use case, you can minimize the element to observe by only observing the visibility of where a new date starts and where the last message is for that date and setting the stickiness of the date label accordingly.
I implemented the solution here https://codesandbox.io/s/sticky-date-chat-ykmlx2?file=/src/App.js
I built an UI with this structure
------
marker -> observed element that signals when the block of a date starts going up
------
date label -> a label to show the date which has conditional stickiness (position: sticky)
------
date messages -> the block of messages for the same date
------
last date message -> observe the last message to know if a block of dates is still visible
------
This logic is contained in the MessagesBlock component
const MessagesBlock = ({ block }) => {
const [refDateMarker, inViewDateMarker] = useInView();
const [refLastMessage, inViewLastMessage, entryLastMessage] = useInView();
let styles = {};
// make the date sticky when the marker has already gone up
// and the last message is either visible or hasn't reached the viewport yet
if (
!inViewDateMarker &&
(inViewLastMessage ||
(!inViewLastMessage && entryLastMessage?.boundingClientRect?.y > 0))
) {
styles = { position: "sticky", top: 0 };
}
return (
<>
<div ref={refDateMarker} style={{ height: 1 }} />
<div
style={{
...styles,
// some more styles omitted for brevity
}}
>
{dayjs(block[0].createdAt).format("DD/MM/YYYY")}
</div>
{block.map((item, index) => {
...
return (
<Message
ref={index === block.length - 1 ? refLastMessage : null}
key={item._id}
item={item}
className={className}
isUserChat={isUserChat}
index={index}
/>
);
})}
</>
);
};
Of course, to achieve this, I had to change the data a little. I used the groupby function provided by lodash
import groupBy from "lodash.groupby";
...
const byDate = groupBy(data, (item) =>
dayjs(item.createdAt).format("DD/MM/YYYY")
);
And render the blocks in the scroll container
<InfiniteScroll
...
>
{Object.entries(byDate).map(([date, block]) => (
<MessagesBlock block={block} />
))}
</InfiniteScroll>
I had to make some style changes and omit some details from your version, but I hope you can add them to my provided solution.

Is there any other way to hide dropdown menu when clicked outside?

So, I am creating a dropdown menu in React and if I click outside the dropdown menu, it should close. For that, I am currently using click eventListeners. Is there any other way that can be used instead of using eventListeners? I tried with onFocus and onBlur, but that doesn't seem to work.
Here's the code snippet:
const [showMenu, setShowMenu] = useState(false);
const dropdownRef = useRef(null);
useEffect(() => {
//hiding the dropdown if clicked outside
const pageClickEvent = (e: { target: unknown }) => {
if (dropdownRef.current !== null && !dropdownRef.current.contains(e.target)) {
setShowMenu(!showMenu);
}
};
//if the dropdown is active then listens for click
if (showMenu) {
document.addEventListener("click", pageClickEvent);
}
//unsetting the listener
return () => {
document.removeEventListener("click", pageClickEvent);
};
}, [showMenu]);
<Button onClick = {() => setShowMenu(!showMenu)} />
{showMenu ? (
<div className="dropdown-content" ref={dropdownRef} >
<a>
...
<a>
</div>
) : null}
Yes there is. Use an overlay under the menu.
function MyComponent() {
const [menuVisible, setMenuVisible] = useState(false);
return (
<div>
<button className='dropdown-button' onClick={() => setMenuVisible(true)}>Click me</button>
{menuVisible ? (
<ul className='dropdown-menu'>
{/* items go here */ }
</ul>
) : null}
{/* now the important part */}
{menuVisible ? (<div className='overlay' onClick={() => setMenuVisible(false)} />) : null}
</div>
)
}
CSS
.overlay {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
background: rgba(0, 0, 0, 0.01);
}

How to make mapbox resize when changing the browser width

How can I adjust the code below so that map resizes when I resize the browser/screen? (First time using Mapbox without a senior developers help).
From my own research, there are code libraries that can add an event listener on the map container and listen for that to resize, then I can then call map.resize() programmatically. However, I would like to know what best practice are as that way feels a bit hacky.
Code
const applyToArray = (func, array) => func.apply(Math, array)
const getBoundsForPoints = (points) => {
console.log('Points:', points)
// Calculate corner values of bounds
const pointsLong = points.map(point => point.geometry.coordinates[0])
const pointsLat = points.map(point => point.geometry.coordinates[1])
const cornersLongLat = [
[applyToArray(Math.min, pointsLong), applyToArray(Math.min, pointsLat)],
[applyToArray(Math.max, pointsLong), applyToArray(Math.max, pointsLat)]
]
// Use WebMercatorViewport to get center longitude/latitude and zoom
const viewport = new WebMercatorViewport({ width: 600, height: 600 })
// #ts-ignore
.fitBounds(cornersLongLat, { padding: {top:20, bottom: 90, left:20, right:20} })
const { longitude, latitude, zoom } = viewport
return { longitude, latitude, zoom }
}
const myMap = () => {
const bounds = getBoundsForPoints(parkData.features);
const [viewport, setViewport] = useState({
width: "100%",
height: "600px",
...bounds
});
const [selectedPark, setSelectedPark] = useState(null);
return (
<div>
<ReactMapGL
{...viewport}
mapboxApiAccessToken="pk.eyJ1IjoiYmVubmtpbmd5IiwiYSI6ImNrY2ozMnJ5dzBrZ28ycnA1b2Vqb2I0bXgifQ.ZOaVtzsDQOrAovh9Orh13Q"
mapStyle="mapbox://styles/mapbox/streets-v11"
onViewportChange={viewport => {
setViewport(viewport);
}}
>
{parkData.features.map(park => (
<Marker
key={park.properties.ID}
latitude={park.geometry.coordinates[1]}
longitude={park.geometry.coordinates[0]}
>
<button
className="marker-btn"
onClick={e => {
e.preventDefault();
setSelectedPark(park);
}}
>
<img src={mapIcon} alt="Map Pointer Icon" />
</button>
</Marker>
))}
</ReactMapGL>
{selectedPark ? (
<div className={ styles.officeInfo }>
<div className={ styles.officeInfoImage }>
<img src={selectedPark.properties.IMAGE} />
</div>
<div className={ styles.officeInfoContent }>
<h3>{selectedPark.properties.NAME}</h3>
<p>{selectedPark.properties.DESCRIPTION}</p>
<button onClick={e => {
e.preventDefault();
setSelectedPark(null);
}}>X</button>
</div>
</div>
) : null}
</div>
);
}
Code demo
https://codesandbox.io/s/immutable-glitter-hokq6?file=/src/App.js
I will legit buy you a coffee or two for helping <3
Having worked with mapbox pretty extensively, I can tell you that map.resize() will have to be called at some point, either by you, or by the react-map-gl bindings internally.
So, you need to first create a resize-handler, I prefer to do this in a hook:
const useResize = (handler) => {
useEffect(() => {
window.addEventListener("resize", handler);
return () => {
window.removeEventListener("resize", handler);
};
}, [handler]);
};
Then you can use this in your component as such:
const onResize = useCallback(() => {
setViewport({ ...viewport });
}, []);
useResize(onResize);
Since you update the viewport object, react-map-gl will recognize this as a state-change and update the viewport accordingly, probably by calling map.resize() internally.
Here is a sandbox: https://codesandbox.io/s/upbeat-tdd-o8vn3?file=/src/App.js
The other (arguably better) solution is to simply call map.resize() yourself. To do this, simply capture the map object on load:
<ReactMapGL
{...viewport}
onLoad={({ map }) => setMap(map)}
>
Then call map.resize() in your resize-handler:
const onResize = useCallback(() => {
if (map) {
map.resize();
}
}, [map]);
This solution is going to have better performance, since it is closer to the metal and doesn't require cloning the current viewport object.
Here's a sandbox for this solution: https://codesandbox.io/s/optimistic-kalam-8p8jo?file=/src/App.js

React Spring translate animation not working and clicking on a list item seems to be delayed

I have a simple list of options in a menu like so:
Option 1
Option 2
Option 3
When the user clicks on an option, there should be a highlighted bar that shows which one they selected. And when the user clicks on different options, the highlighted bar should slide up and down depending on what they chose. I'm trying to use react-spring, but I can't seem to get the animation and clicking behavior to happen properly.
With my current code, the highlighted bar does not slide up and down; it just shows and hides upon user selection. And clicking on an option once does not put the highlighted bar on it, instead, I have to click twice for it to show up correctly on the selected option.
Help is appreciated! This is my first time using react-spring so I'm a bit lost on this.
Below is the code snippet for the animations and rendering the component:
const [currentIndex, setCurrentIndex] = useState<number>(0);
const [previousIndex, setPreviousIndex] = useState<number>(0);
const onClick = (name: string, index: number) => {
setPreviousIndex(currentIndex);
setCurrentIndex(index);
setSpring(fn());
};
// Spring animation code
const fn = () => (
{
transform: `translateY(${currentIndex * 52}px)`,
from: {
transform: `translateY(${previousIndex * 52}px)`,
},
}
);
const [spring, setSpring] = useState<any>(useSpring(fn()));
// Rendering component
return (
<div>
{options.map((option, index) => (
<>
{currentIndex === index && <animated.div style={{...spring, ...{ background: 'orange', height: 52, width: '100%', position: 'absolute', left: 0, zIndex: 1}}}></animated.div>}
<div onClick={() => onClick(option.name, index)}>
<TextWithIcon icon={currentIndex === index ? option.filledIcon : option.outlineIcon} text={option.name} />
</div>
</>
))}
</div>
);
And here is the custom component, TextWithIcon:
// Interfaces
interface TextWithIconProps {
containerStyle?: Record<any, any>;
icon: ReactElement;
text: string;
textStyle?: Record<any, any>;
}
// TextWithIcon component
const TextWithIcon: React.FC<TextWithIconProps> = ({ containerStyle, icon, text, textStyle}) => {
return (
<div id='menu-items' style={{...styles.container, ...containerStyle}}>
{icon}
<Text style={{...styles.text, ...textStyle}}>{text}</Text>
</div>
)
};
You have to set the duration property of the config property inside the useSpring's parameter to a value. Try the following:
const [currentIndex, setCurrentIndex] = useState<number>(0);
const [previousIndex, setPreviousIndex] = useState<number>(0);
const onClick = (name: string, index: number) => {
setPreviousIndex(currentIndex);
setCurrentIndex(index);
setSpring(fn());
};
// Spring animation code
const fn = () => (
{
transform: `translateY(${currentIndex * 52}px)`,
from: {
transform: `translateY(${previousIndex * 52}px)`,
},
config: {
duration: 1250 //this can be a different number
}
}
);
const [spring, setSpring] = useState<any>(useSpring(fn()));
// Rendering component
return (
<div>
{options.map((option, index) => (
<>
{currentIndex === index && <animated.div style={{...spring, ...{ background: 'orange', height: 52, width: '100%', position: 'absolute', left: 0, zIndex: 1}}}></animated.div>}
<div onClick={() => onClick(option.name, index)}>
<TextWithIcon icon={currentIndex === index ? option.filledIcon : option.outlineIcon} text={option.name} />
</div>
</>
))}
</div>
);
References:
StackOverflow. Animation duration in React Spring.https://stackoverflow.com/a/54076843/8121551. (Accessed August 23, 2021).

Resources