Skip to content

Commit c3c24b2

Browse files
vasharma05-brunoarpit-bruno
authored andcommitted
fix: Allow matching the menu's width to the trigger (#46)
* Add a new prop to allow matching the menu's width with the trigger width, if the trigger is larger * Review comments * Add e2e test for the env switcher dropdown
1 parent cbee32f commit c3c24b2

4 files changed

Lines changed: 67 additions & 2 deletions

File tree

packages/bruno-api-docs/e2e/components/layout/env-switcher.component.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export class EnvSwitcherComponent extends BaseComponent {
1414
readonly showVarsToggle = this.page.getByTestId('show-vars-toggle');
1515
readonly trigger: Locator;
1616
readonly menu: Locator;
17+
readonly surface: Locator;
1718
readonly emptyOption: Locator;
1819

1920
constructor(
@@ -23,6 +24,7 @@ export class EnvSwitcherComponent extends BaseComponent {
2324
super(page, page.getByTestId(`${base}-root`));
2425
this.trigger = this.root.getByTestId(base);
2526
this.menu = this.page.getByTestId(`${base}-dropdown`);
27+
this.surface = this.menu.locator('xpath=ancestor::div[@data-tippy-root]');
2628
this.emptyOption = this.option('no-environments');
2729
}
2830

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { test, expect } from '../../playwright';
2+
3+
/**
4+
* The environment switcher passes `matchTriggerWidth` to its MenuDropdown, so
5+
* the menu is floored at the trigger's width and never renders narrower than the
6+
* button that opened it. The env names here are short, so the menu's natural
7+
* content sits at the dropdown's 10rem floor; we widen the trigger past that
8+
* floor to prove the popover grows to match a wider trigger.
9+
*/
10+
11+
const DESKTOP = { width: 1280, height: 900 };
12+
13+
test.describe('env switcher menu width', () => {
14+
test.use({ viewport: DESKTOP });
15+
16+
test('the menu is never narrower than the trigger that opened it', async ({ overviewPage, envSwitcher }) => {
17+
await overviewPage.goto('/');
18+
19+
await envSwitcher.trigger.evaluate((el) => {
20+
(el as HTMLElement).style.minWidth = '300px';
21+
});
22+
23+
await envSwitcher.open();
24+
await expect(envSwitcher.menu).toBeVisible();
25+
26+
const trigger = await envSwitcher.trigger.boundingBox();
27+
const surface = await envSwitcher.surface.boundingBox();
28+
if (trigger === null || surface === null) throw new Error('env switcher has no bounding box');
29+
30+
expect(surface.width).toBeGreaterThanOrEqual(trigger.width - 1);
31+
});
32+
});

packages/bruno-api-docs/src/components/EnvSwitcher/EnvSwitcher.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ const EnvSwitcher: React.FC<EnvSwitcherProps> = ({ testId = 'env-switcher' }) =>
6464
selectedItemId={hasEnvironments ? activeEnv?.name : undefined}
6565
showTickMark={false}
6666
placement="bottom-end"
67+
matchTriggerWidth
6768
testId={testId}
6869
>
6970
<button

packages/bruno-api-docs/src/ui/MenuDropdown/Dropdown.tsx

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ export interface DropdownProps extends Omit<TippyProps, 'render' | 'children' |
1111
/** Mouse handlers applied to the popover surface (used for hover-driven submenus). */
1212
onMouseEnter?: React.MouseEventHandler<HTMLDivElement>;
1313
onMouseLeave?: React.MouseEventHandler<HTMLDivElement>;
14+
/**
15+
* The popover width is set to the maximum of the content width and the trigger width,
16+
* it never renders narrower than the trigger, but still grows for wider content.
17+
*/
18+
matchTriggerWidth?: boolean;
1419
}
1520

1621
/**
@@ -29,6 +34,7 @@ export const Dropdown: React.FC<DropdownProps> = ({
2934
onMouseEnter,
3035
onMouseLeave,
3136
className,
37+
matchTriggerWidth,
3238
...props
3339
}) => {
3440
// Default to portaling the popover to <body> so it escapes any `overflow:
@@ -37,11 +43,35 @@ export const Dropdown: React.FC<DropdownProps> = ({
3743
// `appendTo` is itself the portal — no separate createPortal is needed.
3844
const resolvedAppendTo = appendTo ?? (() => document.body);
3945

46+
// Popper modifier that floors the popover's min-width at the trigger's width.
47+
// `effect` sets it before popper first measures the popover (so the initial
48+
// placement uses the widened box); `fn` keeps it in sync on later updates.
49+
const resolvedPopperOptions: TippyProps['popperOptions'] = matchTriggerWidth
50+
? {
51+
...props.popperOptions,
52+
modifiers: [
53+
...(props.popperOptions?.modifiers ?? []),
54+
{
55+
name: 'matchTriggerWidth',
56+
enabled: true,
57+
phase: 'beforeWrite',
58+
requires: ['computeStyles'],
59+
fn: ({ state }) => {
60+
state.styles.popper.minWidth = `${state.rects.reference.width}px`;
61+
},
62+
effect: ({ state }) => {
63+
state.elements.popper.style.minWidth = `${(state.elements.reference as HTMLElement).offsetWidth}px`;
64+
}
65+
}
66+
]
67+
}
68+
: props.popperOptions;
69+
4070
// When controlled (visible provided) Tippy must not also manage a trigger.
4171
const tippyProps: Partial<TippyProps>
4272
= visible !== undefined
43-
? { ...props, visible, interactive: true, appendTo: resolvedAppendTo }
44-
: { ...props, trigger: 'click', interactive: true, appendTo: resolvedAppendTo };
73+
? { ...props, visible, interactive: true, appendTo: resolvedAppendTo, popperOptions: resolvedPopperOptions }
74+
: { ...props, trigger: 'click', interactive: true, appendTo: resolvedAppendTo, popperOptions: resolvedPopperOptions };
4575

4676
return (
4777
<Tippy

0 commit comments

Comments
 (0)