fix: dropdown menu portals (#2740)

This commit is contained in:
Matthew Russell 2023-01-26 00:52:49 -08:00 committed by GitHub
parent 1ba0ad234b
commit 3893b26d30
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 144 additions and 59 deletions

View File

@ -139,7 +139,8 @@ const AccountHistoryManager = ({
<div className="h-full w-full flex flex-col gap-8"> <div className="h-full w-full flex flex-col gap-8">
<div className="w-full flex flex-col-reverse lg:flex-row items-start lg:items-center justify-between gap-4 px-2"> <div className="w-full flex flex-col-reverse lg:flex-row items-start lg:items-center justify-between gap-4 px-2">
<div className="flex items-center gap-4 shrink-0"> <div className="flex items-center gap-4 shrink-0">
<DropdownMenu> <DropdownMenu
trigger={
<DropdownMenuTrigger> <DropdownMenuTrigger>
{accountType {accountType
? `${ ? `${
@ -149,6 +150,8 @@ const AccountHistoryManager = ({
} Account` } Account`
: t('Select account type')} : t('Select account type')}
</DropdownMenuTrigger> </DropdownMenuTrigger>
}
>
<DropdownMenuContent> <DropdownMenuContent>
{[ {[
Schema.AccountType.ACCOUNT_TYPE_GENERAL, Schema.AccountType.ACCOUNT_TYPE_GENERAL,
@ -164,10 +167,13 @@ const AccountHistoryManager = ({
))} ))}
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
<DropdownMenu> <DropdownMenu
trigger={
<DropdownMenuTrigger> <DropdownMenuTrigger>
{asset ? asset.symbol : t('Select asset')} {asset ? asset.symbol : t('Select asset')}
</DropdownMenuTrigger> </DropdownMenuTrigger>
}
>
<DropdownMenuContent> <DropdownMenuContent>
{assets.map((a) => ( {assets.map((a) => (
<DropdownMenuItem key={a.id} onClick={() => setAsset(a)}> <DropdownMenuItem key={a.id} onClick={() => setAsset(a)}>

View File

@ -3,6 +3,7 @@ import { VegaWalletContext } from '@vegaprotocol/wallet';
import type { VegaWalletContextShape } from '@vegaprotocol/wallet'; import type { VegaWalletContextShape } from '@vegaprotocol/wallet';
import { VegaWalletConnectButton } from './vega-wallet-connect-button'; import { VegaWalletConnectButton } from './vega-wallet-connect-button';
import { truncateByChars } from '@vegaprotocol/react-helpers'; import { truncateByChars } from '@vegaprotocol/react-helpers';
import userEvent from '@testing-library/user-event';
const mockUpdateDialogOpen = jest.fn(); const mockUpdateDialogOpen = jest.fn();
jest.mock('@vegaprotocol/wallet', () => ({ jest.mock('@vegaprotocol/wallet', () => ({
@ -31,7 +32,7 @@ it('Not connected', () => {
expect(mockUpdateDialogOpen).toHaveBeenCalled(); expect(mockUpdateDialogOpen).toHaveBeenCalled();
}); });
it('Connected', () => { it('Connected', async () => {
const pubKey = { publicKey: '123456__123456', name: 'test' }; const pubKey = { publicKey: '123456__123456', name: 'test' };
render( render(
generateJsx({ generateJsx({
@ -42,6 +43,6 @@ it('Connected', () => {
const button = screen.getByTestId('manage-vega-wallet'); const button = screen.getByTestId('manage-vega-wallet');
expect(button).toHaveTextContent(truncateByChars(pubKey.publicKey)); expect(button).toHaveTextContent(truncateByChars(pubKey.publicKey));
fireEvent.click(button); userEvent.click(button);
expect(mockUpdateDialogOpen).not.toHaveBeenCalled(); expect(mockUpdateDialogOpen).not.toHaveBeenCalled();
}); });

View File

@ -142,15 +142,21 @@ export const VegaWalletConnectButton = () => {
return ( return (
<> <>
<div className="hidden lg:block"> <div className="hidden lg:block">
<DropdownMenu open={dropdownOpen}> <DropdownMenu
open={dropdownOpen}
trigger={
<DropdownMenuTrigger <DropdownMenuTrigger
data-testid="manage-vega-wallet" data-testid="manage-vega-wallet"
onClick={() => setDropdownOpen((curr) => !curr)} onClick={() => setDropdownOpen((curr) => !curr)}
> >
{activeKey && <span className="uppercase">{activeKey.name}</span>} {activeKey && (
<span className="uppercase">{activeKey.name}</span>
)}
{': '} {': '}
{truncateByChars(pubKey)} {truncateByChars(pubKey)}
</DropdownMenuTrigger> </DropdownMenuTrigger>
}
>
<DropdownMenuContent <DropdownMenuContent
onInteractOutside={() => setDropdownOpen(false)} onInteractOutside={() => setDropdownOpen(false)}
> >

View File

@ -60,10 +60,13 @@ export const CandlesChartContainer = ({
return ( return (
<div className="h-full flex flex-col"> <div className="h-full flex flex-col">
<div className="px-4 py-2 flex flex-row flex-wrap gap-4"> <div className="px-4 py-2 flex flex-row flex-wrap gap-4">
<DropdownMenu> <DropdownMenu
trigger={
<DropdownMenuTrigger> <DropdownMenuTrigger>
{t(`Interval: ${intervalLabels[interval]}`)} {t(`Interval: ${intervalLabels[interval]}`)}
</DropdownMenuTrigger> </DropdownMenuTrigger>
}
>
<DropdownMenuContent> <DropdownMenuContent>
<DropdownMenuRadioGroup <DropdownMenuRadioGroup
value={interval} value={interval}
@ -84,10 +87,13 @@ export const CandlesChartContainer = ({
</DropdownMenuRadioGroup> </DropdownMenuRadioGroup>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
<DropdownMenu> <DropdownMenu
trigger={
<DropdownMenuTrigger> <DropdownMenuTrigger>
<Icon name={chartTypeIcon.get(chartType) as IconName} /> <Icon name={chartTypeIcon.get(chartType) as IconName} />
</DropdownMenuTrigger> </DropdownMenuTrigger>
}
>
<DropdownMenuContent> <DropdownMenuContent>
<DropdownMenuRadioGroup <DropdownMenuRadioGroup
value={chartType} value={chartType}
@ -104,8 +110,9 @@ export const CandlesChartContainer = ({
</DropdownMenuRadioGroup> </DropdownMenuRadioGroup>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
<DropdownMenu> <DropdownMenu
<DropdownMenuTrigger>{t('Overlays')}</DropdownMenuTrigger> trigger={<DropdownMenuTrigger>{t('Overlays')}</DropdownMenuTrigger>}
>
<DropdownMenuContent> <DropdownMenuContent>
{Object.values(Overlay).map((overlay) => ( {Object.values(Overlay).map((overlay) => (
<DropdownMenuCheckboxItem <DropdownMenuCheckboxItem
@ -128,8 +135,9 @@ export const CandlesChartContainer = ({
))} ))}
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
<DropdownMenu> <DropdownMenu
<DropdownMenuTrigger>{t('Studies')}</DropdownMenuTrigger> trigger={<DropdownMenuTrigger>{t('Studies')}</DropdownMenuTrigger>}
>
<DropdownMenuContent> <DropdownMenuContent>
{Object.values(Study).map((study) => ( {Object.values(Study).map((study) => (
<DropdownMenuCheckboxItem <DropdownMenuCheckboxItem

View File

@ -96,13 +96,18 @@ export const NetworkSwitcher = () => {
const menuRef = useRef<HTMLButtonElement | null>(null); const menuRef = useRef<HTMLButtonElement | null>(null);
return ( return (
<DropdownMenu open={isOpen} onOpenChange={handleOpen}> <DropdownMenu
open={isOpen}
onOpenChange={handleOpen}
trigger={
<DropdownMenuTrigger <DropdownMenuTrigger
ref={menuRef} ref={menuRef}
className="flex justify-between items-center" className="flex justify-between items-center"
> >
{envTriggerMapping[VEGA_ENV]} {envTriggerMapping[VEGA_ENV]}
</DropdownMenuTrigger> </DropdownMenuTrigger>
}
>
<DropdownMenuContent <DropdownMenuContent
align="start" align="start"
style={{ minWidth: `${menuRef.current?.offsetWidth || 290}px` }} style={{ minWidth: `${menuRef.current?.offsetWidth || 290}px` }}

View File

@ -0,0 +1,34 @@
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
} from './dropdown-menu';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
describe('DropdownMenu', () => {
const text = 'Dropdown menu content';
// Upgrade from @radix-ui/react-dropdown-menu 0.1.6 to 2.0.2 renders
// dropdowns inline (rather than portals). Currently not using a portal
// will break the UI due to z-index issues
it('renders using a portal', async () => {
render(
<div className="test-wrapper">
<DropdownMenu
trigger={<DropdownMenuTrigger>Trigger</DropdownMenuTrigger>}
>
<DropdownMenuContent>
<p>{text}</p>
</DropdownMenuContent>
</DropdownMenu>
</div>
);
userEvent.click(screen.getByText(/trigger/i));
const contentElement = await screen.findByText(text);
expect(contentElement).toBeInTheDocument();
// if content is within .test-wrapper then its not been rendered in a portal
expect(contentElement.closest('.test-wrapper')).toBe(null);
});
});

View File

@ -29,16 +29,21 @@ export const CheckboxItems = () => {
console.log(checkboxItems); console.log(checkboxItems);
return ( return (
<DropdownMenu> <DropdownMenu
trigger={
<DropdownMenuTrigger> <DropdownMenuTrigger>
<span>Select many things</span> <span>Select many things</span>
</DropdownMenuTrigger> </DropdownMenuTrigger>
}
>
<DropdownMenuContent> <DropdownMenuContent>
{checkboxItems.map(({ label, state: [checked, setChecked] }) => ( {checkboxItems.map(({ label, state: [checked, setChecked] }) => (
<DropdownMenuCheckboxItem <DropdownMenuCheckboxItem
key={label} key={label}
checked={checked} checked={checked}
onCheckedChange={setChecked} onCheckedChange={(checked) =>
setChecked(typeof checked === 'boolean' ? checked : false)
}
> >
{label} {label}
<DropdownMenuItemIndicator /> <DropdownMenuItemIndicator />
@ -55,10 +60,13 @@ export const RadioItems = () => {
return ( return (
<div style={{ textAlign: 'center', padding: 50 }}> <div style={{ textAlign: 'center', padding: 50 }}>
<DropdownMenu> <DropdownMenu
trigger={
<DropdownMenuTrigger> <DropdownMenuTrigger>
<span>Open</span> <span>Open</span>
</DropdownMenuTrigger> </DropdownMenuTrigger>
}
>
<DropdownMenuContent> <DropdownMenuContent>
<DropdownMenuItem onSelect={() => console.log('minimize')}> <DropdownMenuItem onSelect={() => console.log('minimize')}>
Minimize window Minimize window
@ -92,10 +100,13 @@ export const IconMenu = () => {
return ( return (
<div style={{ textAlign: 'center', padding: 50 }}> <div style={{ textAlign: 'center', padding: 50 }}>
<DropdownMenu> <DropdownMenu
trigger={
<DropdownMenuTrigger> <DropdownMenuTrigger>
<Icon name="cog" /> <Icon name="cog" />
</DropdownMenuTrigger> </DropdownMenuTrigger>
}
>
<DropdownMenuContent> <DropdownMenuContent>
{iconMenuItems.map(({ label }) => ( {iconMenuItems.map(({ label }) => (
<DropdownMenuItem key={label}>{label}</DropdownMenuItem> <DropdownMenuItem key={label}>{label}</DropdownMenuItem>

View File

@ -1,5 +1,6 @@
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'; import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
import classNames from 'classnames'; import classNames from 'classnames';
import type { ReactNode } from 'react';
import { forwardRef } from 'react'; import { forwardRef } from 'react';
import { Icon } from '../icon'; import { Icon } from '../icon';
@ -12,11 +13,24 @@ const itemClass = classNames(
'whitespace-nowrap' 'whitespace-nowrap'
); );
type DropdownMenuProps = DropdownMenuPrimitive.DropdownMenuProps & {
trigger: ReactNode;
};
/** /**
* Contains all the parts of a dropdown menu. * Contains all the parts of a dropdown menu.
*/ */
export const DropdownMenu = DropdownMenuPrimitive.Root; export const DropdownMenu = ({
children,
trigger,
...props
}: DropdownMenuProps) => {
return (
<DropdownMenuPrimitive.Root {...props}>
{trigger}
<DropdownMenuPrimitive.Portal>{children}</DropdownMenuPrimitive.Portal>
</DropdownMenuPrimitive.Root>
);
};
/** /**
* The button that toggles the dropdown menu. * The button that toggles the dropdown menu.
* By default, the {@link DropdownMenuContent} will position itself against the trigger. * By default, the {@link DropdownMenuContent} will position itself against the trigger.