diff --git a/.changeset/fix-account-modal-accessible-name.md b/.changeset/fix-account-modal-accessible-name.md new file mode 100644 index 0000000000..683703baf8 --- /dev/null +++ b/.changeset/fix-account-modal-accessible-name.md @@ -0,0 +1,15 @@ +--- +"@rainbow-me/rainbowkit": patch +--- + +fix: give the account modal an accessible name + +`AccountModal` set `aria-labelledby="rk_account_modal_title"` on its dialog, +but `ProfileDetails` rendered its heading with its own hardcoded id, +`rk_profile_title`, and put that same id on the balance heading too. The +reference never resolved, so screen readers announced the modal as an +unnamed dialog, and the duplicated id was invalid on its own. + +`ProfileDetails` now takes the `titleId` from `AccountModal` and renders it +on the account name only, so the dialog is announced by the connected +address or ENS name, the way the chain and connect modals already are. diff --git a/packages/rainbowkit/src/components/AccountModal/AccountModal.test.tsx b/packages/rainbowkit/src/components/AccountModal/AccountModal.test.tsx new file mode 100644 index 0000000000..5383f59abf --- /dev/null +++ b/packages/rainbowkit/src/components/AccountModal/AccountModal.test.tsx @@ -0,0 +1,43 @@ +import user from '@testing-library/user-event'; +import React, { Fragment } from 'react'; +import { describe, expect, it } from 'vitest'; +import { useConnect } from 'wagmi'; +import { renderWithProviders } from '../../../test/'; +import { mockedAccounts } from '../../../test/mockWallet'; +import { formatAddress } from '../ConnectButton/formatAddress'; +import { AccountModal } from './AccountModal'; + +const AccountModalWithConnectButton = () => { + const { connect, connectors } = useConnect(); + + return ( + + {}} open /> + + + ); +}; + +describe('', () => { + it('names the dialog after the connected account', async () => { + const modal = renderWithProviders(); + + await user.click(await modal.findByTestId('rk-connect-btn')); + + const dialog = await modal.findByRole('dialog'); + const titleId = dialog.getAttribute('aria-labelledby'); + expect(titleId).toBeTruthy(); + + // The id the dialog points at must exist exactly once, on the account name + const titles = document.querySelectorAll(`[id="${titleId}"]`); + expect(titles).toHaveLength(1); + expect(titles[0]).toHaveTextContent(formatAddress(mockedAccounts[0])); + expect(dialog).toHaveAccessibleName(formatAddress(mockedAccounts[0])); + }); +}); diff --git a/packages/rainbowkit/src/components/AccountModal/AccountModal.tsx b/packages/rainbowkit/src/components/AccountModal/AccountModal.tsx index fa0e3babf2..75d1e37e0d 100644 --- a/packages/rainbowkit/src/components/AccountModal/AccountModal.tsx +++ b/packages/rainbowkit/src/components/AccountModal/AccountModal.tsx @@ -36,6 +36,7 @@ export function AccountModal({ onClose, open }: AccountModalProps) { balance={balance} onClose={onClose} onDisconnect={disconnect} + titleId={titleId} /> diff --git a/packages/rainbowkit/src/components/ProfileDetails/ProfileDetails.tsx b/packages/rainbowkit/src/components/ProfileDetails/ProfileDetails.tsx index e908d77f73..4847a300a1 100644 --- a/packages/rainbowkit/src/components/ProfileDetails/ProfileDetails.tsx +++ b/packages/rainbowkit/src/components/ProfileDetails/ProfileDetails.tsx @@ -26,6 +26,7 @@ interface ProfileDetailsProps { balance: ReturnType['balance']; onClose: () => void; onDisconnect: () => void; + titleId: string; } export function ProfileDetails({ @@ -35,6 +36,7 @@ export function ProfileDetails({ balance, onClose, onDisconnect, + titleId, }: ProfileDetailsProps) { const showRecentTransactions = useContext(ShowRecentTransactionsContext); const { i18n } = useContext(I18nContext); @@ -65,7 +67,6 @@ export function ProfileDetails({ const displayBalance = ethBalance ? abbreviateETHBalance(Number.parseFloat(ethBalance)) : undefined; - const titleId = 'rk_profile_title'; const mobile = isMobile(); return ( @@ -120,7 +121,6 @@ export function ProfileDetails({