diff --git a/.changeset/connect-button-explicit-chain-status.md b/.changeset/connect-button-explicit-chain-status.md new file mode 100644 index 0000000000..f7d2f2e325 --- /dev/null +++ b/.changeset/connect-button-explicit-chain-status.md @@ -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). diff --git a/packages/rainbowkit/src/components/ConnectButton/ConnectButton.test.tsx b/packages/rainbowkit/src/components/ConnectButton/ConnectButton.test.tsx index 4139014c8c..5905c7bf0d 100644 --- a/packages/rainbowkit/src/components/ConnectButton/ConnectButton.test.tsx +++ b/packages/rainbowkit/src/components/ConnectButton/ConnectButton.test.tsx @@ -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('', () => { const renderTextButton = (locale?: Locale) => { @@ -41,4 +43,61 @@ describe('', () => { 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 ( + + + + + ); + }; + + const renderConnected = async (props: ConnectButtonProps = {}) => { + const result = renderWithProviders(, { + 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/); + }); + }); }); diff --git a/packages/rainbowkit/src/components/ConnectButton/ConnectButton.tsx b/packages/rainbowkit/src/components/ConnectButton/ConnectButton.tsx index 672f788f00..636ed659fe 100644 --- a/packages/rainbowkit/src/components/ConnectButton/ConnectButton.tsx +++ b/packages/rainbowkit/src/components/ConnectButton/ConnectButton.tsx @@ -35,7 +35,7 @@ const defaultProps = { export function ConnectButton({ accountStatus = defaultProps.accountStatus, - chainStatus = defaultProps.chainStatus, + chainStatus: chainStatusProp, label = defaultProps.label, showBalance = defaultProps.showBalance, }: ConnectButtonProps) { @@ -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. @@ -80,95 +87,105 @@ export function ConnectButton({ > {ready && account && connectionStatus === 'connected' ? ( <> - {chain && (chains.length > 1 || unsupportedChain) && ( - - 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 ? ( - - {i18n.t('connect_wallet.wrong_network.label')} - - ) : ( - - {chain.hasIcon ? ( + {chain && + (chains.length > 1 || + unsupportedChain || + chainStatusExplicit) && ( + + 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 ? ( + + {i18n.t('connect_wallet.wrong_network.label')} + + ) : ( + + {chain.hasIcon ? ( + + value === 'full' || value === 'icon' + ? 'block' + : 'none', + )} + height="24" + width="24" + > + + + ) : null} - 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" > - + {chain.name ?? chain.id} - ) : null} - { - 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} - - )} - - - )} + )} + + + )} {!unsupportedChain && ( ``` +> 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.