diff --git a/.changeset/fair-horses-sneeze.md b/.changeset/fair-horses-sneeze.md
new file mode 100644
index 00000000000..a334c15d9b9
--- /dev/null
+++ b/.changeset/fair-horses-sneeze.md
@@ -0,0 +1,10 @@
+---
+'braid-design-system': minor
+---
+
+---
+updated:
+ - MenuRenderer
+---
+
+**MenuRenderer** Ensure menu is visible, even when it's trigger element is inside a container with overflow hidden.
diff --git a/packages/braid-design-system/src/lib/components/MenuRenderer/MenuRenderer.css.ts b/packages/braid-design-system/src/lib/components/MenuRenderer/MenuRenderer.css.ts
index 8bbbdd32072..d42fd6b57c0 100644
--- a/packages/braid-design-system/src/lib/components/MenuRenderer/MenuRenderer.css.ts
+++ b/packages/braid-design-system/src/lib/components/MenuRenderer/MenuRenderer.css.ts
@@ -7,6 +7,21 @@ export const backdrop = style({
height: '100vh',
});
+export const triggerVars = {
+ top: createVar(),
+ left: createVar(),
+ bottom: createVar(),
+ right: createVar(),
+};
+
+// Top and bottom reversed to allow for a more natural API
+export const menuPosition = style({
+ top: triggerVars.bottom,
+ bottom: triggerVars.top,
+ left: triggerVars.left,
+ right: triggerVars.right,
+});
+
export const menuIsClosed = style({
transform: `translateY(${calc(vars.grid).negate().multiply(2)})`,
visibility: 'hidden',
@@ -23,10 +38,6 @@ export const width = styleVariants({ small, medium, large }, (w) => [
{ vars: { [widthVar]: w } },
]);
-export const placementBottom = style({
- bottom: '100%',
-});
-
export const menuYPadding = 'xxsmall';
export const menuHeightLimit = style({
diff --git a/packages/braid-design-system/src/lib/components/MenuRenderer/MenuRenderer.tsx b/packages/braid-design-system/src/lib/components/MenuRenderer/MenuRenderer.tsx
index ce3874e9d5c..dcfe81bf712 100644
--- a/packages/braid-design-system/src/lib/components/MenuRenderer/MenuRenderer.tsx
+++ b/packages/braid-design-system/src/lib/components/MenuRenderer/MenuRenderer.tsx
@@ -24,6 +24,9 @@ import buildDataAttributes, {
type DataAttributeMap,
} from '../private/buildDataAttributes';
import * as styles from './MenuRenderer.css';
+import { triggerVars } from './MenuRenderer.css';
+import { BraidPortal } from '../BraidPortal/BraidPortal';
+import { assignInlineVars } from '@vanilla-extract/dynamic';
interface TriggerProps {
'aria-haspopup': boolean;
@@ -77,10 +80,29 @@ const {
BACKDROP_CLICK,
} = actionTypes;
+type position = { top: number; bottom: number; left: number; right: number };
+
+const getPosition = (element: HTMLElement | null): position | undefined => {
+ if (!element) {
+ return undefined;
+ }
+
+ const { top, bottom, left, right } = element.getBoundingClientRect();
+ const { scrollX, scrollY, innerWidth, innerHeight } = window;
+
+ return {
+ top: innerHeight - top - scrollY,
+ bottom: bottom + scrollY,
+ left: left + scrollX,
+ right: innerWidth - right - scrollX,
+ };
+};
+
interface State {
open: boolean;
highlightIndex: number;
closeReason: CloseReason;
+ triggerPosition?: position;
}
const CLOSED_INDEX = -1;
@@ -89,6 +111,7 @@ const initialState: State = {
open: false,
highlightIndex: CLOSED_INDEX,
closeReason: CLOSE_REASON_EXIT,
+ triggerPosition: undefined,
};
export const MenuRenderer = ({
@@ -120,8 +143,8 @@ export const MenuRenderer = ({
'All child nodes within a menu component must be a MenuItem, MenuItemLink, MenuItemCheckbox or MenuItemDivider: https://seek-oss.github.io/braid-design-system/components/MenuRenderer',
);
- const [{ open, highlightIndex, closeReason }, dispatch] = useReducer(
- (state: State, action: Action): State => {
+ const [{ open, highlightIndex, closeReason, triggerPosition }, dispatch] =
+ useReducer((state: State, action: Action): State => {
switch (action.type) {
case MENU_TRIGGER_UP:
case MENU_ITEM_UP: {
@@ -130,6 +153,7 @@ export const MenuRenderer = ({
open: true,
closeReason: CLOSE_REASON_EXIT,
highlightIndex: getNextIndex(-1, state.highlightIndex, itemCount),
+ triggerPosition: buttonRef && getPosition(buttonRef.current),
};
}
case MENU_TRIGGER_DOWN:
@@ -139,6 +163,7 @@ export const MenuRenderer = ({
open: true,
closeReason: CLOSE_REASON_EXIT,
highlightIndex: getNextIndex(1, state.highlightIndex, itemCount),
+ triggerPosition: buttonRef && getPosition(buttonRef.current),
};
}
case BACKDROP_CLICK:
@@ -183,22 +208,23 @@ export const MenuRenderer = ({
open: nextOpen,
closeReason: CLOSE_REASON_EXIT,
highlightIndex: nextOpen ? 0 : CLOSED_INDEX,
+ triggerPosition: buttonRef && getPosition(buttonRef.current),
};
}
case MENU_TRIGGER_CLICK: {
const nextOpen = !state.open;
+
return {
...state,
open: nextOpen,
closeReason: CLOSE_REASON_EXIT,
+ triggerPosition: buttonRef && getPosition(buttonRef.current),
};
}
default:
return state;
}
- },
- initialState,
- );
+ }, initialState);
useEffect(() => {
if (lastOpen.current === open) {
@@ -300,6 +326,7 @@ export const MenuRenderer = ({
reserveIconSpace={reserveIconSpace}
focusTrigger={focusTrigger}
dispatch={dispatch}
+ triggerPosition={triggerPosition}
>
{items}
@@ -342,8 +369,9 @@ interface MenuProps {
highlightIndex: number;
open: boolean;
children: ReactChild[];
- position?: 'absolute' | 'relative';
+ triggerPosition?: position;
}
+
export function Menu({
offsetSpace,
align,
@@ -355,61 +383,73 @@ export function Menu({
focusTrigger,
highlightIndex,
reserveIconSpace,
- position = 'absolute',
+ triggerPosition,
}: MenuProps) {
let dividerCount = 0;
+ const inlineVars =
+ triggerPosition &&
+ assignInlineVars({
+ [triggerVars[placement]]: `${triggerPosition[placement]}px`,
+ [triggerVars[align]]: `${triggerPosition[align]}px`,
+ });
+
return (
-
-
-
- {Children.map(children, (item, i) => {
- if (isDivider(item)) {
- dividerCount++;
- return item;
- }
-
- const menuItemIndex = i - dividerCount;
-
- return (
-
- {item}
-
- );
- })}
-
-
-
-
+ triggerPosition && (
+
+
+
+
+ {Children.map(children, (item, i) => {
+ if (isDivider(item)) {
+ dividerCount++;
+ return item;
+ }
+ const menuItemIndex = i - dividerCount;
+ return (
+
+ {item}
+
+ );
+ })}
+
+
+
+
+
+ )
);
}