From 91e181464d46427863a6a0ce973fa566ac1b0ca1 Mon Sep 17 00:00:00 2001 From: Peter Makowski Date: Wed, 24 Jan 2024 16:12:25 +0100 Subject: [PATCH] feat: adjust contextual dropdown vertical position (#1009) --- .../ContextualMenu/ContextualMenu.stories.mdx | 1 + .../ContextualMenuDropdown.tsx | 85 +++++++++++++++++-- 2 files changed, 79 insertions(+), 7 deletions(-) diff --git a/src/components/ContextualMenu/ContextualMenu.stories.mdx b/src/components/ContextualMenu/ContextualMenu.stories.mdx index a8914a11..3b32eed6 100644 --- a/src/components/ContextualMenu/ContextualMenu.stories.mdx +++ b/src/components/ContextualMenu/ContextualMenu.stories.mdx @@ -38,6 +38,7 @@ export const ScrollTemplate = (args) => ( voluptas odit aspernatur alias molestias facere.

)} + ); diff --git a/src/components/ContextualMenu/ContextualMenuDropdown/ContextualMenuDropdown.tsx b/src/components/ContextualMenu/ContextualMenuDropdown/ContextualMenuDropdown.tsx index 5c71191f..5e6420be 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. @@ -55,6 +56,7 @@ export type Props = { */ const getPositionStyle = ( position: Position, + verticalPosition: VerticalPosition, positionCoords: Props["positionCoords"], constrainPanelWidth: Props["constrainPanelWidth"] ): React.CSSProperties => { @@ -62,7 +64,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) { @@ -161,6 +166,23 @@ const generateLink = ( ); }; +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; +}; + const ContextualMenuDropdown = ({ adjustedPosition, autoAdjust, @@ -180,30 +202,74 @@ const ContextualMenuDropdown = ({ ...props }: Props): JSX.Element => { const dropdown = useRef(); - + const [verticalPosition, setVerticalPosition] = + useState("bottom"); 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]); + + const updateVerticalPosition = useCallback(() => { + if (!positionNode) { + return null; + } + const scrollableParent = getClosestScrollableParent(positionNode); + if (!scrollableParent) { + return null; + } + const scrollableParentRect = scrollableParent.getBoundingClientRect(); + const rect = positionNode.getBoundingClientRect(); + + // Calculate the rect in relation to the scrollableParent + const relativeRect = { + top: rect.top - scrollableParentRect.top, + bottom: rect.bottom - scrollableParentRect.top, + height: rect.height, + }; + + const spaceBelow = scrollableParentRect.height - relativeRect.bottom; + const spaceAbove = relativeRect.top; + const dropdownHeight = relativeRect.height; + + setVerticalPosition( + spaceBelow >= dropdownHeight || spaceBelow > spaceAbove ? "bottom" : "top" + ); + }, [positionNode]); // Update the position when the window fitment info changes. const onUpdateWindowFitment = useCallback( (fitsWindow: WindowFitment) => { if (autoAdjust) { setAdjustedPosition(adjustForWindow(position, fitsWindow)); + updateVerticalPosition(); } if (scrollOverflow) { setMaxHeight(fitsWindow.fromBottom.spaceBelow - 16); } }, - [autoAdjust, position, scrollOverflow, setAdjustedPosition] + [ + autoAdjust, + position, + scrollOverflow, + setAdjustedPosition, + updateVerticalPosition, + ] ); // Handle adjusting the horizontal position and scrolling of the dropdown so that it remains on screen. @@ -220,6 +286,10 @@ const ContextualMenuDropdown = ({ updatePositionStyle(); }, [adjustedPosition, updatePositionStyle]); + useEffect(() => { + updateVerticalPosition(); + }, [updateVerticalPosition]); + return ( // Vanilla Framework uses .p-contextual-menu parent modifier classnames to determine the correct position of the .p-contextual-menu__dropdown dropdown (left, center, right). // Extra span wrapper is required as the dropdown is rendered in a portal. @@ -237,6 +307,7 @@ const ContextualMenuDropdown = ({ ...(scrollOverflow ? { maxHeight, minHeight: "2rem", overflowX: "auto" } : {}), + ...(verticalPosition === "top" ? { bottom: "0" } : {}), }} {...props} >