Skip to content

Commit

Permalink
feat: close Wallet dropdown when clicking outside the component conta…
Browse files Browse the repository at this point in the history
…iner (#925)
  • Loading branch information
cpcramer authored Aug 5, 2024
1 parent 922b308 commit b89f300
Show file tree
Hide file tree
Showing 5 changed files with 136 additions and 42 deletions.
1 change: 1 addition & 0 deletions .changeset/thin-zebras-dream.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
---

-**chore**: Organize const variables and update imports for the Transaction component. By @cpcramer #961
-**feat**: Add close wallet dropdown when clicking outside of the component's container. By @cpcramer #925
121 changes: 100 additions & 21 deletions src/wallet/components/Wallet.test.tsx
Original file line number Diff line number Diff line change
@@ -1,40 +1,119 @@
import '@testing-library/jest-dom';
import { render, screen, waitFor } from '@testing-library/react';
import { useAccount, useConnect, useDisconnect } from 'wagmi';
import { fireEvent, render, screen } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { ConnectWallet } from './ConnectWallet';
import { Wallet } from './Wallet';
import { WalletDropdown } from './WalletDropdown';
import { useWalletContext } from './WalletProvider';

vi.mock('wagmi', () => ({
useAccount: vi.fn(),
useConnect: vi.fn(),
useDisconnect: vi.fn(),
vi.mock('./WalletProvider', () => ({
useWalletContext: vi.fn(),
WalletProvider: ({ children }) => <>{children}</>,
}));

vi.mock('./ConnectWallet', () => ({
ConnectWallet: () => <div data-testid="connect-wallet">Connect Wallet</div>,
}));

vi.mock('./WalletDropdown', () => ({
WalletDropdown: () => (
<div data-testid="wallet-dropdown">Wallet Dropdown</div>
),
}));

describe('Wallet Component', () => {
let mockSetIsOpen: ReturnType<typeof vi.fn>;

beforeEach(() => {
vi.clearAllMocks();
(useAccount as vi.Mock).mockReturnValue({ status: 'disconnected' });
(useConnect as vi.Mock).mockReturnValue({
connectors: [{ name: 'injected' }],
connect: vi.fn(),
mockSetIsOpen = vi.fn();
(useWalletContext as ReturnType<typeof vi.fn>).mockReturnValue({
isOpen: false,
setIsOpen: mockSetIsOpen,
});
(useDisconnect as vi.Mock).mockReturnValue({ disconnect: vi.fn() });
});

it('should render the Wallet component with ConnectWallet', async () => {
it('should render the Wallet component with ConnectWallet', () => {
render(
<Wallet>
<ConnectWallet />
<WalletDropdown>
<div />
</WalletDropdown>
<WalletDropdown />
</Wallet>,
);
await waitFor(() => {
expect(
screen.getByTestId('ockConnectWallet_Container'),
).toBeInTheDocument();

expect(screen.getByTestId('connect-wallet')).toBeDefined();
expect(screen.queryByTestId('wallet-dropdown')).toBeNull();
});

it('should close the wallet when clicking outside', () => {
(useWalletContext as ReturnType<typeof vi.fn>).mockReturnValue({
isOpen: true,
setIsOpen: mockSetIsOpen,
});

render(
<Wallet>
<ConnectWallet />
<WalletDropdown />
</Wallet>,
);

expect(screen.getByTestId('wallet-dropdown')).toBeDefined();

fireEvent.click(document.body);

expect(mockSetIsOpen).toHaveBeenCalledWith(false);
});

it('should not close the wallet when clicking inside', () => {
(useWalletContext as ReturnType<typeof vi.fn>).mockReturnValue({
isOpen: true,
setIsOpen: mockSetIsOpen,
});

render(
<Wallet>
<ConnectWallet />
<WalletDropdown />
</Wallet>,
);

const walletDropdown = screen.getByTestId('wallet-dropdown');
expect(walletDropdown).toBeDefined();

fireEvent.click(walletDropdown);

expect(mockSetIsOpen).not.toHaveBeenCalled();
});

it('should not trigger click handler when wallet is closed', () => {
render(
<Wallet>
<ConnectWallet />
<WalletDropdown />
</Wallet>,
);

expect(screen.queryByTestId('wallet-dropdown')).toBeNull();

fireEvent.click(document.body);

expect(mockSetIsOpen).not.toHaveBeenCalled();
});

it('should remove event listener on unmount', () => {
const removeEventListenerSpy = vi.spyOn(document, 'removeEventListener');

const { unmount } = render(
<Wallet>
<ConnectWallet />
<WalletDropdown />
</Wallet>,
);

unmount();

expect(removeEventListenerSpy).toHaveBeenCalledWith(
'click',
expect.any(Function),
);
});
});
42 changes: 34 additions & 8 deletions src/wallet/components/Wallet.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { Children, useMemo } from 'react';
import { Children, useEffect, useMemo, useRef } from 'react';
import type { WalletReact } from '../types';
import { ConnectWallet } from './ConnectWallet';
import { WalletDropdown } from './WalletDropdown';
import { WalletProvider } from './WalletProvider';
import { WalletProvider, useWalletContext } from './WalletProvider';

const WalletContent = ({ children }: WalletReact) => {
const { isOpen, setIsOpen } = useWalletContext();
const walletContainerRef = useRef<HTMLDivElement>(null);

export function Wallet({ children }: WalletReact) {
const { connect, dropdown } = useMemo(() => {
const childrenArray = Children.toArray(children);
return {
Expand All @@ -15,12 +18,35 @@ export function Wallet({ children }: WalletReact) {
};
}, [children]);

// Handle clicking outside the wallet component to close the dropdown.
useEffect(() => {
const handleClickOutsideComponent = (event: MouseEvent) => {
if (
walletContainerRef.current &&
!walletContainerRef.current.contains(event.target as Node) &&
isOpen
) {
setIsOpen(false);
}
};

document.addEventListener('click', handleClickOutsideComponent);
return () =>
document.removeEventListener('click', handleClickOutsideComponent);
}, [isOpen, setIsOpen]);

return (
<div ref={walletContainerRef} className="relative w-fit shrink-0">
{connect}
{isOpen && dropdown}
</div>
);
};

export const Wallet = ({ children }: WalletReact) => {
return (
<WalletProvider>
<div className="relative w-fit shrink-0">
{connect}
{dropdown}
</div>
<WalletContent>{children}</WalletContent>
</WalletProvider>
);
}
};
9 changes: 0 additions & 9 deletions src/wallet/components/WalletDropdown.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,6 @@ const useWalletContextMock = useWalletContext as vi.Mock;
const useAccountMock = useAccount as vi.Mock;

describe('WalletDropdown', () => {
it('renders null when isOpen is false', () => {
useWalletContextMock.mockReturnValue({ isOpen: false });
useAccountMock.mockReturnValue({ address: '0x123' });

render(<WalletDropdown>Test Children</WalletDropdown>);

expect(screen.queryByText('Test Children')).not.toBeInTheDocument();
});

it('renders null when address is not provided', () => {
useWalletContextMock.mockReturnValue({ isOpen: true });
useAccountMock.mockReturnValue({ address: null });
Expand Down
5 changes: 1 addition & 4 deletions src/wallet/components/WalletDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,8 @@ import { useAccount } from 'wagmi';
import { Identity } from '../../identity/components/Identity';
import { background, cn } from '../../styles/theme';
import type { WalletDropdownReact } from '../types';
import { useWalletContext } from './WalletProvider';

export function WalletDropdown({ children, className }: WalletDropdownReact) {
const { isOpen } = useWalletContext();

const { address } = useAccount();

const childrenArray = useMemo(() => {
Expand All @@ -20,7 +17,7 @@ export function WalletDropdown({ children, className }: WalletDropdownReact) {
});
}, [children, address]);

if (!isOpen || !address) {
if (!address) {
return null;
}

Expand Down

0 comments on commit b89f300

Please sign in to comment.