Skip to content
Open
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
17 changes: 17 additions & 0 deletions .changeset/connect-button-explicit-chain-status.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
"@rainbow-me/rainbowkit": patch
---

fix: respect explicit `chainStatus` on `ConnectButton` for single-chain dApps

Previously, the chain selector on `ConnectButton` was hidden whenever exactly
one chain was configured, even when the consumer explicitly set the
`chainStatus` prop. This made it impossible to keep the active network
visible on the default button without dropping down to `ConnectButton.Custom`.

The default behavior is unchanged (the selector is still hidden for
single-chain dApps that do not pass `chainStatus`). Passing any value for
`chainStatus` (`"full"`, `"icon"`, `"name"`, or `"none"`) is now treated as
an explicit opt-in: the selector renders for `full`/`icon`/`name`, and stays
hidden for `none`. This resolves
[#1418](https://github.com/rainbow-me/rainbowkit/issues/1418).
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { screen, waitFor } from '@testing-library/react';
import React from 'react';
import user from '@testing-library/user-event';
import React, { Fragment } from 'react';
import { describe, expect, it } from 'vitest';
import { useConnect } from 'wagmi';
import { mainnet } from 'wagmi/chains';
import { renderWithProviders } from '../../../test';
import type { Locale } from '../../locales';
import { ConnectButton } from './ConnectButton';
import { ConnectButton, type ConnectButtonProps } from './ConnectButton';

describe('<ConnectButton />', () => {
const renderTextButton = (locale?: Locale) => {
Expand Down Expand Up @@ -41,4 +43,61 @@ describe('<ConnectButton />', () => {
const button = renderTextButton('ru-RU');
await waitFor(() => expect(button.textContent).toBe('Подключить кошелек'));
});

describe('chain selector visibility with a single chain', () => {
const ConnectAndButton = (props: ConnectButtonProps) => {
const { connect, connectors } = useConnect();

return (
<Fragment>
<ConnectButton {...props} />
<button
data-testid="rk-test-connect"
onClick={() => connect({ connector: connectors[0] })}
type="button"
>
connect
</button>
</Fragment>
);
};

const renderConnected = async (props: ConnectButtonProps = {}) => {
const result = renderWithProviders(<ConnectAndButton {...props} />, {
chains: [mainnet],
});

const trigger = await result.findByTestId('rk-test-connect');
await user.click(trigger);

return result;
};

it('Hides the chain selector by default when only one chain is configured', async () => {
const result = await renderConnected();

await result.findByTestId('rk-account-button');
expect(result.queryByTestId('rk-chain-button')).toBeNull();
});

it('Renders the chain selector when `chainStatus` is set explicitly', async () => {
const result = await renderConnected({ chainStatus: 'full' });

const chainButton = await result.findByTestId('rk-chain-button');
expect(chainButton).toBeInTheDocument();
});

it('Hides the chain selector via display sprinkle when `chainStatus="none"` is set explicitly', async () => {
const result = await renderConnected({ chainStatus: 'none' });

await result.findByTestId('rk-account-button');
// The chain button is mounted but suppressed via vanilla-extract
// `display: none` sprinkles. jsdom does not compute styles from those
// CSS modules, so we assert the suppressing class is applied instead
// of `toBeVisible()`.
const chainButton = result.queryByTestId('rk-chain-button');
expect(chainButton).not.toBeNull();
expect(chainButton?.className).toMatch(/display_none/);
});
});
});
187 changes: 102 additions & 85 deletions packages/rainbowkit/src/components/ConnectButton/ConnectButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const defaultProps = {

export function ConnectButton({
accountStatus = defaultProps.accountStatus,
chainStatus = defaultProps.chainStatus,
chainStatus: chainStatusProp,
label = defaultProps.label,
showBalance = defaultProps.showBalance,
}: ConnectButtonProps) {
Expand All @@ -44,6 +44,13 @@ export function ConnectButton({
const { setShowBalance } = useShowBalance();
const [ready, setReady] = useState(false);

// When the consumer explicitly sets `chainStatus`, the chain selector is
// rendered even for single-chain dApps so the active network is always
// visible (Issue #1418). Without an explicit value we keep the historical
// behavior of hiding the selector unless multiple chains are configured.
const chainStatusExplicit = chainStatusProp !== undefined;
const chainStatus = chainStatusProp ?? defaultProps.chainStatus;

const { i18n } = useContext(I18nContext);

// biome-ignore lint/correctness/useExhaustiveDependencies: Preserve existing ready guard behavior.
Expand Down Expand Up @@ -80,95 +87,105 @@ export function ConnectButton({
>
{ready && account && connectionStatus === 'connected' ? (
<>
{chain && (chains.length > 1 || unsupportedChain) && (
<Box
alignItems="center"
aria-label="Chain Selector"
as="button"
background={
unsupportedChain
? 'connectButtonBackgroundError'
: 'connectButtonBackground'
}
borderRadius="connectButton"
boxShadow="connectButton"
className={touchableStyles({
active: 'shrink',
hover: 'grow',
})}
color={
unsupportedChain
? 'connectButtonTextError'
: 'connectButtonText'
}
display={mapResponsiveValue(chainStatus, (value) =>
value === 'none' ? 'none' : 'flex',
)}
fontFamily="body"
fontWeight="bold"
gap="6"
key={
// Force re-mount to prevent CSS transition
unsupportedChain ? 'unsupported' : 'supported'
}
onClick={openChainModal}
paddingX="10"
paddingY="8"
testId={
unsupportedChain ? 'wrong-network-button' : 'chain-button'
}
transition="default"
type="button"
>
{unsupportedChain ? (
<Box
alignItems="center"
display="flex"
height="24"
paddingX="4"
>
{i18n.t('connect_wallet.wrong_network.label')}
</Box>
) : (
<Box alignItems="center" display="flex" gap="6">
{chain.hasIcon ? (
{chain &&
(chains.length > 1 ||
unsupportedChain ||
chainStatusExplicit) && (
<Box
alignItems="center"
aria-label="Chain Selector"
as="button"
background={
unsupportedChain
? 'connectButtonBackgroundError'
: 'connectButtonBackground'
}
borderRadius="connectButton"
boxShadow="connectButton"
className={touchableStyles({
active: 'shrink',
hover: 'grow',
})}
color={
unsupportedChain
? 'connectButtonTextError'
: 'connectButtonText'
}
display={mapResponsiveValue(chainStatus, (value) =>
value === 'none' ? 'none' : 'flex',
)}
fontFamily="body"
fontWeight="bold"
gap="6"
key={
// Force re-mount to prevent CSS transition
unsupportedChain ? 'unsupported' : 'supported'
}
onClick={openChainModal}
paddingX="10"
paddingY="8"
testId={
unsupportedChain
? 'wrong-network-button'
: 'chain-button'
}
transition="default"
type="button"
>
{unsupportedChain ? (
<Box
alignItems="center"
display="flex"
height="24"
paddingX="4"
>
{i18n.t('connect_wallet.wrong_network.label')}
</Box>
) : (
<Box alignItems="center" display="flex" gap="6">
{chain.hasIcon ? (
<Box
display={mapResponsiveValue(
chainStatus,
(value) =>
value === 'full' || value === 'icon'
? 'block'
: 'none',
)}
height="24"
width="24"
>
<AsyncImage
alt={chain.name ?? 'Chain icon'}
background={chain.iconBackground}
borderRadius="full"
height="24"
src={chain.iconUrl}
width="24"
/>
</Box>
) : null}
<Box
display={mapResponsiveValue(chainStatus, (value) =>
value === 'full' || value === 'icon'
? 'block'
: 'none',
display={mapResponsiveValue(
chainStatus,
(value) => {
if (value === 'icon' && !chain.iconUrl) {
return 'block'; // Show the chain name if there is no iconUrl
}

return value === 'full' || value === 'name'
? 'block'
: 'none';
},
)}
height="24"
width="24"
>
<AsyncImage
alt={chain.name ?? 'Chain icon'}
background={chain.iconBackground}
borderRadius="full"
height="24"
src={chain.iconUrl}
width="24"
/>
{chain.name ?? chain.id}
</Box>
) : null}
<Box
display={mapResponsiveValue(chainStatus, (value) => {
if (value === 'icon' && !chain.iconUrl) {
return 'block'; // Show the chain name if there is no iconUrl
}

return value === 'full' || value === 'name'
? 'block'
: 'none';
})}
>
{chain.name ?? chain.id}
</Box>
</Box>
)}
<DropdownIcon />
</Box>
)}
)}
<DropdownIcon />
</Box>
)}

{!unsupportedChain && (
<Box
Expand Down
5 changes: 5 additions & 0 deletions site/data/en-US/docs/connect-button.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,11 @@ Hide the chain UI entirely.
<ConnectButton chainStatus="__none__" />
```

> Note: when only a single chain is configured the chain selector is hidden by
> default. Pass any `chainStatus` value explicitly (for example `"full"`,
> `"icon"`, or `"name"`) to force the selector to render so the current
> network is always visible.

#### Show balance

Use the `showBalance` prop to hide/show the balance.
Expand Down