Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed menu position based on scrollable parent #3830

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .changeset/unlucky-lemons-grow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
'react-select': major
---

Fixed menu position based on scrollable parent.

WHY the change was made -
Earlier the position of the menu was based on the height of the `window` element which caused the menu to
get truncated when it was near the bottom of the window but was placed inside some other scrollable div.

WHAT the breaking change is -
With this change, the menu's position will be based on the dimensions of its nearest scrollable parent, having
a position other than 'static'. This helps in constraning/shifting the menu height/position so that it fits
inside the parent and not just the window element.
29 changes: 18 additions & 11 deletions packages/react-select/src/components/Menu.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,26 +63,29 @@ export function getMenuPlacement({
// something went wrong, return default state
if (!menuEl || !menuEl.offsetParent) return defaultState;

// we can't trust `scrollParent.scrollHeight` --> it may increase when
// the menu is rendered
const { height: scrollHeight } = scrollParent.getBoundingClientRect();
const {
top: scrollParentTop,
bottom: scrollParentBottom
} = scrollParent.getBoundingClientRect();

const {
bottom: menuBottom,
height: menuHeight,
top: menuTop,
} = menuEl.getBoundingClientRect();

const { top: containerTop } = menuEl.offsetParent.getBoundingClientRect();
const viewHeight = window.innerHeight;
// using the height of the scrollable parent and not the window height.
const viewHeight = scrollParent.clientHeight;
const scrollTop = getScrollTop(scrollParent);

const marginBottom = parseInt(getComputedStyle(menuEl).marginBottom, 10);
const marginTop = parseInt(getComputedStyle(menuEl).marginTop, 10);
const viewSpaceAbove = containerTop - marginTop;
const viewSpaceAbove = menuTop - scrollParentTop - scrollTop;
const viewSpaceBelow = viewHeight - menuTop;
const scrollSpaceAbove = viewSpaceAbove + scrollTop;
const scrollSpaceBelow = scrollHeight - scrollTop - menuTop;

// scroll space left in the parent not relative to the menu
const scrollSpaceBelow = scrollParent.scrollHeight - scrollTop - viewHeight;
const scrollSpaceBelowMenu = scrollParentBottom - menuTop;
const scrollDown = menuBottom - viewHeight + scrollTop + marginBottom;
const scrollUp = scrollTop + menuTop - marginTop;
const scrollDuration = 160;
Expand All @@ -96,7 +99,7 @@ export function getMenuPlacement({
}

// 2: the menu will fit, if scrolled
if (scrollSpaceBelow >= menuHeight && !isFixedPosition) {
if (scrollSpaceBelow - marginTop - marginBottom >= menuHeight && !isFixedPosition) {
if (shouldScroll) {
animatedScrollTo(scrollParent, scrollDown, scrollDuration);
}
Expand All @@ -105,8 +108,11 @@ export function getMenuPlacement({
}

// 3: the menu will fit, if constrained
// change in `scrollParent.scrollHeight` will not have any effect on the positioning
// of the menu and the menu will be contained in the previous dimensions.
if (
(!isFixedPosition && scrollSpaceBelow >= minHeight) ||
(!isFixedPosition && scrollSpaceBelowMenu >= minHeight
&& viewHeight + scrollSpaceBelow >= scrollSpaceBelowMenu) ||
(isFixedPosition && viewSpaceBelow >= minHeight)
) {
if (shouldScroll) {
Expand All @@ -115,9 +121,10 @@ export function getMenuPlacement({

// we want to provide as much of the menu as possible to the user,
// so give them whatever is available below rather than the minHeight.
// not greater than the `maxHeight`.
const constrainedHeight = isFixedPosition
? viewSpaceBelow - marginBottom
: scrollSpaceBelow - marginBottom;
: Math.min(scrollSpaceBelowMenu - marginBottom, maxHeight);

return {
placement: 'bottom',
Expand Down
3 changes: 2 additions & 1 deletion packages/react-select/src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,14 +127,15 @@ export function scrollTo(el: Element, top: number): void {

export function getScrollParent(element: ElementRef<*>): Element {
let style = getComputedStyle(element);
const excludeStaticParent = style.position === 'absolute';
let excludeStaticParent = style.position === 'absolute';
const overflowRx = /(auto|scroll)/;
const docEl = ((document.documentElement: any): Element); // suck it, flow...

if (style.position === 'fixed') return docEl;

for (let parent = element; (parent = parent.parentElement); ) {
style = getComputedStyle(parent);
excludeStaticParent = style.position !== 'static' ? false : excludeStaticParent;
if (excludeStaticParent && style.position === 'static') {
continue;
}
Expand Down