I'm using React and have a table with some actions (delete, edit, etc.) in the cell. And I need to put a tooltip on each action. I'm not using the jquery and don't plan to, and not title props (I will need to upgrade this tooltip to some specific data or even another component).
So the problem is I can't position the tooltip correctly (for example in the middle of the top or bottom). Witch params should my component receive and how to do it with css?
const Tooltip = ({position = 'top', display, style, children}) => {
let displayClass = display ? `fade ${position} in` : `tooltip-${position}`
return (
<div className={`tooltip ${displayClass} `} role='tooltip'>
<div className='tooltip-arrow' />
<div className='tooltip-inner'>
{children}
</div>
</div>
)
}
const ActionLinkItem = ({page, action, data, onMouseEnter, onMouseLeave, display, tooltipText, id}) => {
const {buttonClass, icon} = actionsStyles[action]
return (
<Link to={`/${page}/${action.toLowerCase()}/${data.id}`}>
<a
className={`btn btn-xs ${buttonClass}`}
id={id}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
><i className={`fa fa-${icon}`} />
<Tooltip display={display} action={action}>{tooltipText}</Tooltip>
</a>
</Link>
)
}
export default class Actions extends Component {
constructor (props) {
super(props)
this.state = {
tooltipActive: ''
}
}
handleHover (event) {
this.setState({
tooltipActive: event.target.id
})
}
handleBlur (event) {
this.setState({
tooltipActive: ''
})
}
getActionsTemplate () {
const {actions, data, page} = this.props
return actions.map(action => {
let display = this.state.tooltipActive === `${action.action}-${data.id}`
let id = `${action.action}-${data.id}`
let tooltip = tooltipText[action.action].replace(/{type}/g, page).replace(/{item}/g, data.name
return <ActionLinkItem
key={`${data.id}-${action.action}`}
page={page}
action={action.action}
data={data}
id={id}
tooltipText={tooltip}
display={display}
onMouseEnter={(e) => this.handleHover(e)}
onMouseLeave={(e) => this.handleBlur(e)}
/>
})
}
render () {
return (
<div className='row'>
{this.getActionsTemplate()}
</div>
)
}
}
At its simplest, tooltip should be absolutely positioned within a positioned element.
So, add a style of position: relative to the in ActionLinkItem
and add a style of `position: absolute to the outer in Tooltip.
For added credit, you will want to set a width on your tooltip, and position or center it within the of ActionLinkItem using styles like bottom: 100%.
You can also do some calculations to ensure that your tooltip does not run off the page by moving it left and right if the containing is on the right or left respectively.
Related
So, ive been stuck on this for awhile now and i've tried several solutions from StackOverflow to make this happen but nothing seems to work. It should be as simple as using the useRef hook and scrolling to that component, but it doesnt work, i've tried targeting the DOM after the component finishes loading, i've tried even scrolling into view the bottom of the container after i add a new message.
so, im creating a chat application, and like most chats i want the most recent chat at the bottom of the container, and i want the scroll bar to scroll to that most recent chat. the way i have the container setup is 3 seperate columns where the middle column holds all the information on the page, and the left most column is the navbar. the chats are in the middle column, and extend to the bottom of the viewport, and from there anything that overfills the container is hidden, and has a scroll bar.
I should be doing this inside the useEffect hook, but it doesnt work there either.
can anyone see something i cannot see?
const ChatRoom = () => {
const dispatch = useDispatch();
const { id } = useParams();
const {
selectedChat: { chat, loading, messages },
} = useSelector((state) => state.chat);
const { user } = useSelector((state) => state.auth);
const [message, setMessage] = React.useState("");
useEffect(() => {
// check if the chat is already in the store if not dispatch getChat
if (!chat || chat.id !== id) {
dispatch(getChat(id));
}
scrollToBottom(false);
}, [dispatch, id]);
const updateChatName = () => {
// displays a prompt which gathers the chat name the user wants to change to
// dispatches to update chat action to update the chat name
const newChatName = prompt("Enter new chat name");
if (newChatName) {
dispatch(updateChat({ id, chatName: newChatName }));
window.location.reload();
} else {
dispatch(setAlert("Please enter a chat name", "info"));
}
};
const sendMsg = (e, message) => {
// prevents the page from refreshing when the user presses enter
e.preventDefault();
// trim the message to remove whitespace
const trimmedMessage = message.trim();
// if the message is not empty
if (trimmedMessage) {
// dispatch the send message action
dispatch(sendMessage(trimmedMessage, chat._id));
// get the textarea element and clear it
const textarea = document.getElementsByClassName("messageInput")[0];
textarea.value = "";
// scroll to the bottom of the chat
const c = document.getElementsByClassName("chatContainer")[0];
c.scrollTop({
scrollTop: c.scrollHeight,
animate: true,
duration: "slow",
easing: "easeInOutQuad",
});
// focus the textarea
textarea.focus();
// reset the message
setMessage("");
} else {
// if the message is empty
dispatch(setAlert("Please enter a message", "info"));
}
};
return (
<Container fluid className="chatPageContainer">
<Meta
title={
chat
? chat.chatName
? `Chat | ${chat.chatName}`
: `Chat | ${chat.users[0].firstName}, ${
chat.users[1].firstName
} ${
chat.users.length > 2
? `+ ${chat.users.length - 2} other users`
: ""
}`
: ``
}
/>
<Row>
<span className="titleContainer">
<h1>Chat</h1>
</span>
</Row>
<div className="chatTitleBarContainer">
{chat && (
<>
<TitleBar users={chat.users} user={user} />
<span id="chatName" onClick={updateChatName}>
{chat.chatName
? chat.chatName
: `${chat.users[0].firstName}, ${chat.users[1].firstName} ${
chat.users.length > 3
? `+ ${chat.users.length - 2} other users`
: ""
}`}
</span>
</>
)}
</div>
<div className="mainContentContainer">
<div className="chatContainer">
{loading ? (
<Loader />
) : (
<>
<div className="chatMessages">
{messages &&
messages.map((message, indx) => {
// get the last messasge in the chat, and set the lastSenderId to that message.sender._id
const lastMessage = messages[indx - 1];
let lastSenderId = lastMessage
? lastMessage.sender._id
: null;
return (
<MessageItem
key={message._id}
message={message}
user={user}
nextMessage={messages[indx + 1]}
lastSenderId={lastSenderId}
/>
);
})}
{/* dummy div to scroll to bottom of container */}
</div>
<div id="bottomChatContainer"></div>
</>
)}
<div className="footer">
<textarea
name="message"
className="messageInput"
onKeyDown={(e) => {
if (e.key === "Enter" && !e.shiftKey) {
sendMsg(e, message);
}
}}
onChange={(e) => setMessage(e.target.value)}
placeholder="Type a message..."
></textarea>
<FiSend
className="sendMessageButton"
onClick={(e) => sendMsg(e, message)}
/>
</div>
</div>
</div>
</Container>
);
};
async function scrollToBottom(animate) {
const c = document.getElementById("bottomChatContainer");
if (animate) {
c.animate({ scrollTop: c.scrollHeight }, "slow");
} else {
c.scrollTop = c.scrollHeight;
}
}
export default ChatRoom;
---Edit: Revised Solution---
In the map function of the array of messages that returns a MessageItem component, one can make use of a temporary variable(boolean) that tracks whether or not the current element of the messages array is the last one, at which point the bool becomes true. This bool value should be passed as prop to the messageItem component that renders each message element. The code should look something like this:
...
<div>{ // Make sure this has necessary CSS to allow it to scroll in the y-direction
messagesArray.map(message, index) => {
let tempBool = false;
if(last element) tempBool = true
return(
<MessageItem {...props} scrollIntoViewBool={tempBool} />
)
}
}</div>
....
In the MessageItem component, one can assign a ref to each container that renders message details, and using the combination of the ref and the scrollIntoViewBool prop, use the .scrollIntoView method on the current value of the ref whenever the prop is true. But this should be done in a useEffect so that the scrolling happens after the messages have been loaded. The code will look like this:
...
const messageRef = useRef(null);
useEffect(() => {
if(!scrollIntoViewBool) return;
const lastMessage = messageRef.current;
lastMessage.scrollIntoView();
}, [messageRef, scrollIntoViewBool]);
...
return...
<div ref={messageRef}>Message Contents</div>
For more details you can check out this and this GitHub link to the chat component I made for my app
one thing you could do is instead of scrolling down to the bottom of the enclosing div, add a sibling div to the mapped-messages and then scroll it into view every time a new message is posted/fetched. The code will look something like this:
<div className="chatMessages">
{messages.map().....}
<div ref={dummyDiv}></div> /* scroll this into view */
</div>
The common problem of having a position: fixed header and applying consistent padding to the content below it so that nothing is covered. If the content inside of the header is dynamically subject to changing and the styling needs to stay consistent regardless of what is added or removed.
I implemented this hook:
export const useElemHeight = () => {
const elementRef = useRef(null);
const [heightOfEl, setHeightOfEl] = useState(null);
function handleCheckElHeightOnScreenResize() {
setHeightOfEl(elementRef.current.clientHeight);
}
useEffect(() => {
if (elementRef.current) {
setHeightOfEl(elementRef.current.clientHeight);
window.addEventListener("resize", handleCheckElHeightOnScreenResize);
return () => {
window.removeEventListener("resize", handleCheckElHeightOnScreenResize);
};
}
}, [elementRef]);
return [elementRef, heightOfEl];
};
Application
export default function App() {
const [elementRef, heightOfEl] = useElemHeight();
return (
<div className="App">
<Header ref={elementRef} />
<Content height={heightOfEl} />
</div>
);
}
const Content = ({ height }) => {
const ADDITIONAL_UNITS = 20;
return (
<div
className="content"
style={{ paddingTop: `${height + ADDITIONAL_UNITS}px` }}
>
CONTENT
</div>
);
};
export default Content;
regardless of what content is added or removed from the header the padding will always stay consistent via all screen sizes, but are there any real performance costs to implementing something like this? Working demo here.
Sandbox
I've made a CSS grid displaying a <p>{name}</p> element on each grid item. When an onMouseOver hover events I want to display a new div (contained within the react fragment), but in the same position. Instead of using CSS, I've used JavaScript to code the logic for this - when a user hovers on the grid item, it flips a boolean (saved as state variable isMouseHovering) from false to true.
I have a conditional statement that displays the 'new' div, when this state value is truthy.
The problem is that this logic applies for grid item container element, and not for the elements inside the grid container. So when I over over grid, it does what it's supposed to do and displays the new div. However when I hover over the p-tag within the grid item container element, it flickers between states.
How do I structure this so that the div changes on hover, no matter where on the grid item the user hovers the mouse?
Project.js
import { useState } from 'react'
const Project = ({name,url,techStack,image,blurb}) => {
const [isMouseHovering, setMouseHovering] = useState(false);
const handleOnMouseOver = () => setMouseHovering((isMouseHovering) => !isMouseHovering);
const handleMouseOut = () => setMouseHovering(false);
return(
<div className = 'grid-item' onMouseOver={handleOnMouseOver} onMouseOut = {handleMouseOut}>
{
isMouseHovering ? (
// element I want to display when mouse hovering, in same position as <p>{name}</p> element
<>
<a href='https://xxxxxxxxxxxxxxxx'>
<p>website</p>
</a>
</>
)
:
<p>{name}</p>
}
</div>
)
}
export default Project
The issue is you're using onMouseOver and onMouseOut instead of onMouseEnter and onMouseLeave.
onMouseOut will trigger when you hover over a children of the element you are in causing the bug you're describing.
Also you're toggling the state in your handleMouseOver function but from what I understood you want it to be set to true when you hover the div.
import { useState } from 'react';
const Project = ({ name, url, techStack, image, blurb }) => {
const [isMouseHovering, setMouseHovering] = useState(false);
const handleMouseEnter = () => setMouseHovering(true);
const handleMouseLeave = () => setMouseHovering(false);
return (
<div
className="grid-item"
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
{isMouseHovering ? (
// element I want to display when mouse hovering, in same position as <p>{name}</p> element
<>
<a href="https://xxxxxxxxxxxxxxxx">
<p>website</p>
</a>
</>
) : (
<p>{name}</p>
)}
</div>
);
};
export default Project;
https://developer.mozilla.org/en-US/docs/Web/API/Element/mouseenter_event
Try this:
import { useState } from 'react'
const Project = ({name,url,techStack,image,blurb}) => {
const handleOnMouseOver = (dir) => {
const ele = document.getElementById('dev_span');
if (dir === 'in' && ele) {
ele.style.display = 'block';
} else if (ele) {
ele.style.display = 'none';
}
};
return(
<div
className="grid-item"
onMouseEnter={() => handleOnMouseOver('in')}
onMouseLeave={() => handleOnMouseOver('out')}
>
<span id="dev_span" style={{ display: 'none' }}>
<a href="https://xxxxxxxxxxxxxxxx">
<p>website</p>
</a>
</span>
<p>{name}</p>
</div>
)
}
You can check for the element that is trigger the event.
import { useState } from "react";
const Project = ({ name, url, techStack, image, blurb }) => {
const [isMouseHovering, setMouseHovering] = useState(false);
const handleOnMouseOver = (e) => {
if (e.target.className === "grid-item") {
setMouseHovering((isMouseHovering) => !isMouseHovering);
}
};
const handleMouseOut = () => setMouseHovering(false);
return (
<div
className="grid-item"
onMouseOver={(e) => handleOnMouseOver(e)}
onMouseOut={handleMouseOut}
>
{isMouseHovering ? (
// element I want to display when mouse hovering, in same position as <p>{name}</p> element
<>
<a href="https://xxxxxxxxxxxxxxxx">
<p>website</p>
</a>
</>
) : (
<p>{name}</p>
)}
</div>
);
};
export default Project;
I need to show tooltips for react-select container (not for a separate options) using react-tooltip library.
I made my own SelectContainer component based on the original one and added there data-tip and data-for HTML attributes. Tooltip shows up but when I change selected value it disappears and is not displayed any more.
Here is my code:
const getSelectContainer = options => props => {
return (
<components.SelectContainer
{...props}
innerProps={{
...props.innerProps, ...{
'data-tip': options.tooltipText,
'data-for': options.tooltipId,
}
}}
/>
)
}
const CustomSelect = (props) => {
const tooltipId='tooltip_id'
const tooltipText='tooltip'
const selectedOption = colourOptions.filter(option => option.value === props.value)
return (
<div>
<ReactTooltip effect="solid" html={true} place="bottom" id={tooltipId} />
<Select
defaultValue={colourOptions[4]}
value={selectedOption}
options={colourOptions}
classNamePrefix="react-select"
onChange={item => props.onChange(item.value)}
className="my-select"
components={{
SelectContainer: getSelectContainer({
tooltipText:tooltipText,
tooltipId:tooltipId
})
}}
/>
</div>
)
}
class Page extends Component {
constructor (props) {
super(props)
this.state = {
selectValue: 'red'
}
}
render () {
const onChange = (value)=> {
this.setState({selectValue: value})
//alert(value)
}
return (
<CustomSelect
value={this.state.selectValue}
onChange={onChange}>
</CustomSelect>
)
}
}
See full example here:
If I wrap Select with another <div> and assign tooltip HTML attributes to it everything works correctly but I don't want to add one more DOM element just for that.
What can I do to show tooltips after changing selection?
Try rebuilding the react-tooltip when the state changes.
useEffect(() => {
ReactTooltip.rebuild();
}, [state]);
i have created gird of elements (something like gallery) AllElements Component where i am mapping SingleElement Component
renderAllElements = () => (
this.state.myData.map(se => (
<SingleElement key={se.id} name={se.name} tagline={se.tagline} image_url={se.image_url}/>
)
)
)
And my SingleElement renders this, as below
render() {
return (
<div className="singleElement">
<img src={this.props.image_url} alt={this.props.name} />
<h4>{this.props.name}</h4>
<h5>{this.props.tagline}</h5>
</div>
)
}
To the point, what I want achieve? ---> After clicking on one of the elements (specyfied SingleElement) the details is shown in front of the screen (hovering over whole grid). Let's name this Component SingleElementDetails. What is the best way to achieve it? Should SingleElementDetails Component be sibling of SingleElement Component or it's child ?
You could use the AllElements state and an method to handle when/what to show.
Something like this:
class AllElements extends React.Component {
constructor(props) {
super(props);
this.state = {
myData: {},
viewingElement: null,
};
this.see = this.see.bind(this);
this.close = this.close.bind(this);
}
see(id) {
this.setState({
viewingElement: id,
});
}
close() {
this.setState({
viewingElement: null,
});
}
render() {
const { myData, viewingElement } = this.state;
return (
<div>
{myData.map(se => (
<SingleElement
key={se.id}
name={se.name}
tagline={se.tagline}
image_url={se.image_url}
see={this.see}
close={this.close}
/>
))}
{viewingElement && (
<SingleElementDetails element={myData[this.state]} />
)}
</div>
);
}
}
Then you need to fire this.props.see on the onClick event from SingleElement and use CSS to visually position SingleElementDetails over the rest of the contest.