From f1f8aa0a89921f442b1983cec15804df2670a927 Mon Sep 17 00:00:00 2001 From: Peter Makowski Date: Fri, 5 Jan 2024 14:07:42 +0100 Subject: [PATCH] feat: adjust contextual dropdown vert position --- .../ContextualMenu/ContextualMenu.stories.mdx | 3 +- .../ContextualMenu/ContextualMenu.tsx | 44 ++++++++++++++++++- .../ContextualMenuDropdown.tsx | 26 +++++++++-- 3 files changed, 67 insertions(+), 6 deletions(-) diff --git a/src/components/ContextualMenu/ContextualMenu.stories.mdx b/src/components/ContextualMenu/ContextualMenu.stories.mdx index 690f580d0..8c036aea5 100644 --- a/src/components/ContextualMenu/ContextualMenu.stories.mdx +++ b/src/components/ContextualMenu/ContextualMenu.stories.mdx @@ -1,5 +1,5 @@ import { ArgsTable, Canvas, Meta, Story } from "@storybook/addon-docs"; - +import Button from "../Button"; import ContextualMenu from "./ContextualMenu"; ( voluptas odit aspernatur alias molestias facere.

)} + ); diff --git a/src/components/ContextualMenu/ContextualMenu.tsx b/src/components/ContextualMenu/ContextualMenu.tsx index 536c17862..b1f80ce90 100644 --- a/src/components/ContextualMenu/ContextualMenu.tsx +++ b/src/components/ContextualMenu/ContextualMenu.tsx @@ -110,6 +110,23 @@ export type Props = PropsWithSpread< HTMLProps >; +const getClosestScrollableParent = ( + node: HTMLElement | null +): HTMLElement | null => { + let currentNode = node; + while (currentNode && currentNode !== document.body) { + const { overflowY, overflowX } = window.getComputedStyle(currentNode); + if ( + ["auto", "scroll", "overlay"].includes(overflowY) && + ["auto", "scroll", "overlay"].includes(overflowX) + ) { + return currentNode; + } + currentNode = currentNode.parentElement; + } + return document.body; +}; + /** * Get the node to use for positioning the menu. * @param wrapper - The component's wrapping element. @@ -191,11 +208,35 @@ const ContextualMenu = ({ const wrapper = useRef(null); const [positionCoords, setPositionCoords] = useState(); const [adjustedPosition, setAdjustedPosition] = useState(position); + const [verticalPosition, setVerticalPosition] = useState<"top" | "bottom">( + "bottom" + ); const hasToggle = hasToggleIcon || toggleLabel; + const updateVerticalPosition = useCallback(() => { + const parent = getPositionNode(wrapper.current, positionNode); + if (!parent) { + return null; + } + const rect = parent.getBoundingClientRect(); + const scrollableParent = getClosestScrollableParent(parent); + if (!scrollableParent) { + return null; + } + const scrollableParentRect = scrollableParent.getBoundingClientRect(); + const spaceBelow = scrollableParentRect.height - rect.bottom; + const spaceAbove = rect.top; + const dropdownHeight = rect.height; + + setVerticalPosition( + spaceBelow >= dropdownHeight || spaceBelow > spaceAbove ? "bottom" : "top" + ); + }, [wrapper, positionNode]); + useEffect(() => { setAdjustedPosition(position); - }, [position, autoAdjust]); + updateVerticalPosition(); + }, [position, autoAdjust, updateVerticalPosition]); // Update the coordinates of the position node. const updatePositionCoords = useCallback(() => { @@ -338,6 +379,7 @@ const ContextualMenu = ({ adjustedPosition={adjustedPosition} + verticalPosition={verticalPosition} autoAdjust={autoAdjust} handleClose={closePortal} constrainPanelWidth={constrainPanelWidth} diff --git a/src/components/ContextualMenu/ContextualMenuDropdown/ContextualMenuDropdown.tsx b/src/components/ContextualMenu/ContextualMenuDropdown/ContextualMenuDropdown.tsx index 5c71191f3..202fd28ec 100644 --- a/src/components/ContextualMenu/ContextualMenuDropdown/ContextualMenuDropdown.tsx +++ b/src/components/ContextualMenu/ContextualMenuDropdown/ContextualMenuDropdown.tsx @@ -24,6 +24,7 @@ export enum Label { export type MenuLink = string | ButtonProps | ButtonProps[]; export type Position = "left" | "center" | "right"; +type VerticalPosition = "top" | "bottom"; /** * The props for the ContextualMenuDropdown component. @@ -31,6 +32,7 @@ export type Position = "left" | "center" | "right"; */ export type Props = { adjustedPosition?: Position; + verticalPosition: VerticalPosition; autoAdjust?: boolean; handleClose?: (evt?: MouseEvent) => void; constrainPanelWidth?: boolean; @@ -55,6 +57,7 @@ export type Props = { */ const getPositionStyle = ( position: Position, + verticalPosition: VerticalPosition, positionCoords: Props["positionCoords"], constrainPanelWidth: Props["constrainPanelWidth"] ): React.CSSProperties => { @@ -62,7 +65,10 @@ const getPositionStyle = ( return null; } const { height, left, top, width } = positionCoords; - const topPos = top + height + (window.scrollY || 0); + const topPos = + verticalPosition === "bottom" + ? top + height + (window.scrollY || 0) + : top + (window.scrollY || 0); let leftPos = left; switch (position) { @@ -173,6 +179,7 @@ const ContextualMenuDropdown = ({ links, position, positionCoords, + verticalPosition, positionNode, scrollOverflow, setAdjustedPosition, @@ -182,16 +189,26 @@ const ContextualMenuDropdown = ({ const dropdown = useRef(); const [positionStyle, setPositionStyle] = useState( - getPositionStyle(adjustedPosition, positionCoords, constrainPanelWidth) + getPositionStyle( + adjustedPosition, + verticalPosition, + positionCoords, + constrainPanelWidth + ) ); const [maxHeight, setMaxHeight] = useState(); // Update the styles to position the menu. const updatePositionStyle = useCallback(() => { setPositionStyle( - getPositionStyle(adjustedPosition, positionCoords, constrainPanelWidth) + getPositionStyle( + adjustedPosition, + verticalPosition, + positionCoords, + constrainPanelWidth + ) ); - }, [adjustedPosition, positionCoords, constrainPanelWidth]); + }, [adjustedPosition, positionCoords, verticalPosition, constrainPanelWidth]); // Update the position when the window fitment info changes. const onUpdateWindowFitment = useCallback( @@ -237,6 +254,7 @@ const ContextualMenuDropdown = ({ ...(scrollOverflow ? { maxHeight, minHeight: "2rem", overflowX: "auto" } : {}), + ...(verticalPosition === "top" ? { bottom: "0" } : {}), }} {...props} >