Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export class EnvSwitcherComponent extends BaseComponent {
readonly showVarsToggle = this.page.getByTestId('show-vars-toggle');
readonly trigger: Locator;
readonly menu: Locator;
readonly surface: Locator;
readonly emptyOption: Locator;

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

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { test, expect } from '../../playwright';

/**
* The environment switcher passes `matchTriggerWidth` to its MenuDropdown, so
* the menu is floored at the trigger's width and never renders narrower than the
* button that opened it. The env names here are short, so the menu's natural
* content sits at the dropdown's 10rem floor; we widen the trigger past that
* floor to prove the popover grows to match a wider trigger.
*/

const DESKTOP = { width: 1280, height: 900 };

test.describe('env switcher menu width', () => {
test.use({ viewport: DESKTOP });

test('the menu is never narrower than the trigger that opened it', async ({ overviewPage, envSwitcher }) => {
await overviewPage.goto('/');

await envSwitcher.trigger.evaluate((el) => {
(el as HTMLElement).style.minWidth = '300px';
});

await envSwitcher.open();
await expect(envSwitcher.menu).toBeVisible();

const trigger = await envSwitcher.trigger.boundingBox();
const surface = await envSwitcher.surface.boundingBox();
if (trigger === null || surface === null) throw new Error('env switcher has no bounding box');

expect(surface.width).toBeGreaterThanOrEqual(trigger.width - 1);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ const EnvSwitcher: React.FC<EnvSwitcherProps> = ({ testId = 'env-switcher' }) =>
selectedItemId={hasEnvironments ? activeEnv?.name : undefined}
showTickMark={false}
placement="bottom-end"
matchTriggerWidth
testId={testId}
>
<button
Expand Down
34 changes: 32 additions & 2 deletions packages/bruno-api-docs/src/ui/MenuDropdown/Dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ export interface DropdownProps extends Omit<TippyProps, 'render' | 'children' |
/** Mouse handlers applied to the popover surface (used for hover-driven submenus). */
onMouseEnter?: React.MouseEventHandler<HTMLDivElement>;
onMouseLeave?: React.MouseEventHandler<HTMLDivElement>;
/**
* The popover width is set to the maximum of the content width and the trigger width,
* it never renders narrower than the trigger, but still grows for wider content.
*/
Comment thread
arpit-bruno marked this conversation as resolved.
matchTriggerWidth?: boolean;
}

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

// Popper modifier that floors the popover's min-width at the trigger's width.
// `effect` sets it before popper first measures the popover (so the initial
// placement uses the widened box); `fn` keeps it in sync on later updates.
const resolvedPopperOptions: TippyProps['popperOptions'] = matchTriggerWidth
? {
...props.popperOptions,
modifiers: [
...(props.popperOptions?.modifiers ?? []),
{
name: 'matchTriggerWidth',
enabled: true,
phase: 'beforeWrite',
requires: ['computeStyles'],
fn: ({ state }) => {
state.styles.popper.minWidth = `${state.rects.reference.width}px`;
},
effect: ({ state }) => {
state.elements.popper.style.minWidth = `${(state.elements.reference as HTMLElement).offsetWidth}px`;
}
}
]
}
: props.popperOptions;

// When controlled (visible provided) Tippy must not also manage a trigger.
const tippyProps: Partial<TippyProps>
= visible !== undefined
? { ...props, visible, interactive: true, appendTo: resolvedAppendTo }
: { ...props, trigger: 'click', interactive: true, appendTo: resolvedAppendTo };
? { ...props, visible, interactive: true, appendTo: resolvedAppendTo, popperOptions: resolvedPopperOptions }
: { ...props, trigger: 'click', interactive: true, appendTo: resolvedAppendTo, popperOptions: resolvedPopperOptions };

return (
<Tippy
Expand Down
Loading