diff --git a/.changeset/soft-bags-shave.md b/.changeset/soft-bags-shave.md new file mode 100644 index 0000000000..19a19a4d25 --- /dev/null +++ b/.changeset/soft-bags-shave.md @@ -0,0 +1,5 @@ +--- +'react-select': minor +--- + +Auto-update menu position when using menu portalling diff --git a/packages/react-select/package.json b/packages/react-select/package.json index 390dc8fa55..80fcd56b9a 100644 --- a/packages/react-select/package.json +++ b/packages/react-select/package.json @@ -13,10 +13,12 @@ "@babel/runtime": "^7.12.0", "@emotion/cache": "^11.4.0", "@emotion/react": "^11.8.1", + "@floating-ui/dom": "^1.0.1", "@types/react-transition-group": "^4.4.0", "memoize-one": "^5.0.0", "prop-types": "^15.6.0", - "react-transition-group": "^4.3.0" + "react-transition-group": "^4.3.0", + "use-isomorphic-layout-effect": "^1.1.2" }, "devDependencies": { "@types/jest-in-case": "^1.0.3", diff --git a/packages/react-select/src/components/Menu.tsx b/packages/react-select/src/components/Menu.tsx index 8ce7891c8e..ac98727a93 100644 --- a/packages/react-select/src/components/Menu.tsx +++ b/packages/react-select/src/components/Menu.tsx @@ -5,9 +5,14 @@ import { ReactNode, RefCallback, ContextType, + useState, + useCallback, + useRef, } from 'react'; import { jsx } from '@emotion/react'; import { createPortal } from 'react-dom'; +import { autoUpdate } from '@floating-ui/dom'; +import useLayoutEffect from 'use-isomorphic-layout-effect'; import { animatedScrollTo, @@ -15,7 +20,6 @@ import { getScrollParent, getScrollTop, normalizedHeight, - RectType, scrollTo, } from '../utils'; import { @@ -36,8 +40,8 @@ import { // Get Menu Placement // ------------------------------ -interface MenuState { - placement: CoercedMenuPlacement | null; +interface CalculatedMenuPlacementAndHeight { + placement: CoercedMenuPlacement; maxHeight: number; } interface PlacementArgs { @@ -58,10 +62,13 @@ export function getMenuPlacement({ shouldScroll, isFixedPosition, theme, -}: PlacementArgs): MenuState { +}: PlacementArgs): CalculatedMenuPlacementAndHeight { const { spacing } = theme; const scrollParent = getScrollParent(menuEl!); - const defaultState: MenuState = { placement: 'bottom', maxHeight }; + const defaultState: CalculatedMenuPlacementAndHeight = { + placement: 'bottom', + maxHeight, + }; // something went wrong, return default state if (!menuEl || !menuEl.offsetParent) return defaultState; @@ -288,9 +295,16 @@ export const menuCSS = < }); const PortalPlacementContext = createContext<{ - getPortalPlacement: ((menuState: MenuState) => void) | null; + getPortalPlacement: + | ((menuState: CalculatedMenuPlacementAndHeight) => void) + | null; }>({ getPortalPlacement: null }); +interface MenuState { + placement: CoercedMenuPlacement | null; + maxHeight: number; +} + // NOTE: internal only export class MenuPlacer< Option, @@ -539,14 +553,10 @@ export interface MenuPortalProps< menuPosition: MenuPosition; } -interface MenuPortalState { - placement: 'bottom' | 'top' | null; -} - export interface PortalStyleArgs { offset: number; position: MenuPosition; - rect: RectType; + rect: { left: number; width: number }; } export const menuPortalCSS = ({ @@ -561,69 +571,126 @@ export const menuPortalCSS = ({ zIndex: 1, }); -export class MenuPortal< +interface ComputedPosition { + offset: number; + rect: { left: number; width: number }; +} + +export const MenuPortal = < Option, IsMulti extends boolean, Group extends GroupBase