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}
>