Reputation: 453
I have a Card
div that is supposed to show the scroll if the content exceeds it's height. I've used overflow-y: auto
to do that. I'm trying to use a Select
inside it, and the select menu is supposed to show in front of the card. The menu position is absolute
.
The problem is, even with position: absolute
, the menu is taking space inside the card. making it scrollable.
If I remove the overflow from the card, it works fine, but the content exceeds it. I've created a sandbox for it:
https://codesandbox.io/s/position-absolute-inside-overflow-y-9kppcy?file=/src/App.js
Show the SelectMenu
inside a portal.
Remove the overflow from the card, add it to a CardBody
element, and keep the select outside it.
This might be a common question, but I could not find a solution.
Upvotes: 7
Views: 419
Reputation: 84
Edit: The original Code Sandbox link was wrong, it should be working now.
Here's a solution using only CSS, preserving as much functionality of the Card
class as I could.
And here's the CSS:
.App {
font-family: sans-serif;
text-align: center;
}
.Card {
display: flex;
flex-direction: column;
align-items: center;
border: 1px solid #bebebe;
background-color: white;
border-radius: 0.5rem;
padding: 2rem 1rem;
margin: 4rem;
overflow-y: auto;
height: 124px;
}
.Select {
width: 60vw;
}
.SelectButton {
all: unset;
background-color: royalblue;
color: white;
padding: 1rem;
box-sizing: border-box;
cursor: pointer;
width: 100%;
}
.SelectMenu {
position: absolute;
z-index: 99;
width: 60vw;
border: 1px solid #bebebe;
background-color: white;
border-radius: 0.5rem;
padding: 2rem 1rem;
box-sizing: border-box;
margin-top: 10px;
}
A note on using position: absolute
from this post Using position:absolute, the nearest positioned ancestor is having no effect:
From the post:
- An absolutely positioned element will be positioned within its containing block, which is defined by the nearest positioned ancestor. However, if there is no positioned ancestor, the containing block is the initial containing block (i.e., the viewport).
- When you apply position: absolute to an element you remove it from the normal flow. That's it. The element will still be positioned as though it were in the normal flow. It isn't until you apply the CSS offset properties (left, right, top, bottom) that you actually position the element.
So, since in this case there isn't any positioned ancestor, the containing block is the viewport. This allows the absolute
ly positioned menu to keep its position in the viewport, but if there was more content in the div that caused it to overflow, the menu would not scroll with the div. You can see this behavior by uncommenting some code in App.js
in the Code Sandbox fork.
I also changed some of the widths to make things look nice, but from a functionality perspective that doesn't make a difference.
Upvotes: 0
Reputation: 615
If you don't need the Menu
to be inside the Card
you can just add an external wrapper and position it relative to that wrapper.
Upvotes: 0
Reputation: 136
You can overwrite the overflow for that card only with a modifier class. That should give you your desired result.
Upvotes: 0
Reputation: 66133
I think your solution of using a React Portal actually makes a lot of sense, but you just need a little more trick to position the element.
{showMenu &&
createPortal(
<div className="SelectMenu" style={menuStyle}>
I'm the menu.
</div>,
PORTAL_HOST_NODE
)}
The menuStyle
is simply a computed value returned by useMemo
, which is simply the CSS styles we need to apply to position your menu. What you want is the menu to use position: fixed
, and then simply use the top
, left
, width
and height
attributes of the trigger element (in this case, your "Open Select" button) to position the element.
In order to ensure that the position is updated when the viewport is scrolled, you will also need to track the window.scrollY
value:
const [scrollY, setScrollY] = useState(window.scrollY);
useEffect(() => {
const onScroll = () => setScrollY(window.scrollY);
window.addEventListener("scroll", onScroll);
return () => {
window.removeEventListener("scroll", onScroll);
};
}, []);
I see that you have a manual 0.5rem
offset you want for the select menu to be positioned away from its trigger element. We can store in as a CSS variable:
.SelectMenu {
--top-offset: 0.5rem;
position: fixed;
top: 0;
left: 0;
z-index: 99;
border: 1px solid #bebebe;
background-color: white;
border-radius: 0.5rem;
padding: 2rem 1rem;
box-sizing: border-box;
}
Then, it is matter of setting the other code up as mentioned before. Some notes:
const [menuTriggerElementRect, setMenuTriggerElementRect] = useState(null);
const toggleMenu = useCallback((e) => {
setShowMenu((old) => !old);
setMenuTriggerElementRect(e.currentTarget.getBoundingClientRect());
}, []);
const menuStyle = useMemo(() => {
if (menuTriggerElementRect === null) return {};
const { top, left, width, height } = menuTriggerElementRect;
return {
transform: `translate(${left}px, calc(${
top + height - scrollY
}px + var(--top-offset)))`,
width: `${width}px`
};
}, [menuTriggerElementRect, scrollY]);
See your updated CodeSandbox example here:
Upvotes: 1