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

feat: adjust contextual dropdown vertical position #1009

Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions src/components/ContextualMenu/ContextualMenu.stories.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export const ScrollTemplate = (args) => (
voluptas odit aspernatur alias molestias facere.
</p>
)}
<ContextualMenu {...args} />
</div>
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export enum Label {
export type MenuLink<L = null> = string | ButtonProps<L> | ButtonProps<L>[];

export type Position = "left" | "center" | "right";
type VerticalPosition = "top" | "bottom";

/**
* The props for the ContextualMenuDropdown component.
Expand Down Expand Up @@ -55,14 +56,18 @@ export type Props<L = null> = {
*/
const getPositionStyle = (
position: Position,
verticalPosition: VerticalPosition,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In Tooltip we have combined positions of btm-left, top-left, left, etc. Maybe we should stick to the combined way of positioning? To stay backward compatible left can be the same as btm-left.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did think about it, but decided against it to keep things simple. I'll try and give that a go.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having looked at it I'd rather treat this as a future improvement, I don't want to break the existing logic and it's already quite complex. To do this right it would need major refactoring.

As is, vertical position will be automatically adjusted and displayed at the bottom (which is as it works currently).

positionCoords: Props["positionCoords"],
constrainPanelWidth: Props["constrainPanelWidth"]
): React.CSSProperties => {
if (!positionCoords) {
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) {
Expand Down Expand Up @@ -161,6 +166,23 @@ const generateLink = <L,>(
);
};

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 = <L,>({
adjustedPosition,
autoAdjust,
Expand All @@ -180,30 +202,74 @@ const ContextualMenuDropdown = <L,>({
...props
}: Props<L>): JSX.Element => {
const dropdown = useRef();

const [verticalPosition, setVerticalPosition] =
useState<VerticalPosition>("bottom");
const [positionStyle, setPositionStyle] = useState(
getPositionStyle(adjustedPosition, positionCoords, constrainPanelWidth)
getPositionStyle(
adjustedPosition,
verticalPosition,
positionCoords,
constrainPanelWidth
)
);
const [maxHeight, setMaxHeight] = useState<number>();

// 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.
Expand All @@ -220,6 +286,10 @@ const ContextualMenuDropdown = <L,>({
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.
Expand All @@ -237,6 +307,7 @@ const ContextualMenuDropdown = <L,>({
...(scrollOverflow
? { maxHeight, minHeight: "2rem", overflowX: "auto" }
: {}),
...(verticalPosition === "top" ? { bottom: "0" } : {}),
}}
{...props}
>
Expand Down
Loading