Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: close Wallet dropdown when clicking outside the component container #925

Merged
merged 5 commits into from
Aug 5, 2024
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
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', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Niiice

(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');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

neat!!!


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