diff --git a/.changeset/afraid-bobcats-mate.md b/.changeset/afraid-bobcats-mate.md new file mode 100644 index 0000000000..c13b79576f --- /dev/null +++ b/.changeset/afraid-bobcats-mate.md @@ -0,0 +1,5 @@ +--- +'@clerk/clerk-js': patch +--- + +Improve accessibility of `` and `` by using `aria-*` attributes (where appropriate) and roles like `menu` and `menuitem`. diff --git a/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcher.tsx b/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcher.tsx index f2dc5c28a2..34843fd80d 100644 --- a/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcher.tsx +++ b/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcher.tsx @@ -1,3 +1,5 @@ +import { useId } from 'react'; + import { withOrganizationsEnabledGuard } from '../../common'; import { withCoreUserGuard } from '../../contexts'; import { Flow } from '../../customizables'; @@ -12,12 +14,15 @@ const _OrganizationSwitcher = withFloatingTree(() => { offset: 8, }); + const switcherButtonMenuId = useId(); + return ( { isOpen={isOpen} > @@ -145,7 +147,7 @@ export const OrganizationSwitcherPopover = React.forwardRef t => ({ padding: `0 ${theme.space.$6}`, marginBottom: t.space.$2 })} /> - + {manageOrganizationButton} {__unstable_manageBillingUrl && billingOrganizationButton} diff --git a/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcherTrigger.tsx b/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcherTrigger.tsx index d37adce2a3..8575795b24 100644 --- a/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcherTrigger.tsx +++ b/packages/clerk-js/src/ui/components/OrganizationSwitcher/OrganizationSwitcherTrigger.tsx @@ -33,6 +33,9 @@ export const OrganizationSwitcherTrigger = withAvatarShimmer( colorScheme='neutral' sx={[t => ({ minHeight: 0, padding: `0 ${t.space.$2} 0 0`, position: 'relative' }), sx]} ref={ref} + aria-label={`${props.isOpen ? 'Close' : 'Open'} organization switcher`} + aria-expanded={props.isOpen} + aria-haspopup='dialog' {...rest} > {organization && ( diff --git a/packages/clerk-js/src/ui/components/OrganizationSwitcher/OtherOrganizationActions.tsx b/packages/clerk-js/src/ui/components/OrganizationSwitcher/OtherOrganizationActions.tsx index 712ea0f118..f2becc256e 100644 --- a/packages/clerk-js/src/ui/components/OrganizationSwitcher/OtherOrganizationActions.tsx +++ b/packages/clerk-js/src/ui/components/OrganizationSwitcher/OtherOrganizationActions.tsx @@ -44,7 +44,10 @@ export const OrganizationActionList = (props: OrganizationActionListProps) => { return ( <> - + diff --git a/packages/clerk-js/src/ui/components/OrganizationSwitcher/UserInvitationSuggestionList.tsx b/packages/clerk-js/src/ui/components/OrganizationSwitcher/UserInvitationSuggestionList.tsx index 1d5eadcbc9..fac892d69c 100644 --- a/packages/clerk-js/src/ui/components/OrganizationSwitcher/UserInvitationSuggestionList.tsx +++ b/packages/clerk-js/src/ui/components/OrganizationSwitcher/UserInvitationSuggestionList.tsx @@ -139,6 +139,7 @@ const SwitcherInvitationActions = (props: PropsOfComponent & { show sx={t => ({ borderTop: showBorder ? `${t.borders.$normal} ${t.colors.$blackAlpha200}` : 'none', })} + role='menu' {...restProps} /> ); diff --git a/packages/clerk-js/src/ui/components/OrganizationSwitcher/UserMembershipList.tsx b/packages/clerk-js/src/ui/components/OrganizationSwitcher/UserMembershipList.tsx index d84b2f9291..e37d18a6f5 100644 --- a/packages/clerk-js/src/ui/components/OrganizationSwitcher/UserMembershipList.tsx +++ b/packages/clerk-js/src/ui/components/OrganizationSwitcher/UserMembershipList.tsx @@ -36,6 +36,8 @@ export const UserMembershipList = (props: UserMembershipListProps) => { overflowY: 'auto', ...common.unstyledScrollbar(t), })} + role='group' + aria-label={hidePersonal ? 'List of all organization memberships' : 'List of all accounts'} > {currentOrg && !hidePersonal && ( { icon={SwitchArrows} sx={{ borderRadius: 0 }} onClick={onPersonalWorkspaceClick} + role='menuitem' > { icon={SwitchArrows} sx={{ borderRadius: 0 }} onClick={() => onOrganizationClick(organization)} + role='menuitem' > { f.withOrganizations(); f.withUser({ email_addresses: ['test@clerk.dev'] }); }); - const { queryByRole } = render(, { wrapper }); + const { queryByRole } = await act(() => render(, { wrapper })); expect(queryByRole('button')).toBeDefined(); }); @@ -25,7 +25,7 @@ describe('OrganizationSwitcher', () => { f.withUser({ email_addresses: ['test@clerk.dev'] }); }); props.setProps({ hidePersonal: false }); - const { getByText } = render(, { wrapper }); + const { getByText } = await act(() => render(, { wrapper })); expect(getByText('Personal account')).toBeDefined(); }); @@ -168,7 +168,7 @@ describe('OrganizationSwitcher', () => { props.setProps({ hidePersonal: true }); const { getByRole, userEvent } = render(, { wrapper }); await userEvent.click(getByRole('button')); - await userEvent.click(getByRole('button', { name: 'Manage Organization' })); + await userEvent.click(getByRole('menuitem', { name: 'Manage Organization' })); expect(fixtures.clerk.openOrganizationProfile).toHaveBeenCalled(); }); @@ -183,8 +183,8 @@ describe('OrganizationSwitcher', () => { }); props.setProps({ hidePersonal: true }); const { getByRole, userEvent } = render(, { wrapper }); - await userEvent.click(getByRole('button')); - await userEvent.click(getByRole('button', { name: 'Create Organization' })); + await userEvent.click(getByRole('button', { name: 'Open organization switcher' })); + await userEvent.click(getByRole('menuitem', { name: 'Create Organization' })); expect(fixtures.clerk.openCreateOrganization).toHaveBeenCalled(); }); @@ -198,7 +198,7 @@ describe('OrganizationSwitcher', () => { }); }); props.setProps({ hidePersonal: true }); - const { queryByRole } = render(, { wrapper }); + const { queryByRole } = await act(() => render(, { wrapper })); expect(queryByRole('button', { name: 'Create Organization' })).not.toBeInTheDocument(); }); diff --git a/packages/clerk-js/src/ui/components/UserButton/UserButton.tsx b/packages/clerk-js/src/ui/components/UserButton/UserButton.tsx index 3b17659c6d..53e56058ee 100644 --- a/packages/clerk-js/src/ui/components/UserButton/UserButton.tsx +++ b/packages/clerk-js/src/ui/components/UserButton/UserButton.tsx @@ -1,3 +1,5 @@ +import { useId } from 'react'; + import { getFullName, getIdentifier } from '../../../utils/user'; import { useCoreUser, useUserButtonContext, withCoreUserGuard } from '../../contexts'; import { descriptors, Flex, Flow, Text } from '../../customizables'; @@ -14,6 +16,8 @@ const _UserButton = withFloatingTree(() => { offset: 8, }); + const userButtonMenuId = useId(); + return ( { ref={reference} onClick={toggle} isOpen={isOpen} + aria-controls={userButtonMenuId} /> { isOpen={isOpen} > 0 ? ( <> - + {otherSessions.map(session => ( ({ height: t.sizes.$14, borderRadius: 0 })} onClick={handleSessionClicked(session)} + role='menuitem' > - + ) : ( - {addAccountButton} + {addAccountButton} ); return ( @@ -69,6 +70,8 @@ export const UserButtonPopover = React.forwardRef @@ -77,7 +80,10 @@ export const UserButtonPopover = React.forwardRef ({ padding: `0 ${theme.space.$6}`, marginBottom: theme.space.$2 })} /> - + ({ borderRadius: theme.radii.$circle }), sx]} ref={ref} + aria-label={`${props.isOpen ? 'Close' : 'Open'} user button`} + aria-expanded={props.isOpen} + aria-haspopup='dialog' {...rest} > { }); }); const { getByText, getByRole, userEvent } = render(, { wrapper }); - await userEvent.click(getByRole('button', { name: 'FL' })); + await userEvent.click(getByRole('button', { name: 'Open user button' })); expect(getByText('Manage account')).not.toBeNull(); }); @@ -45,7 +45,7 @@ describe('UserButton', () => { }); }); const { getByText, getByRole, userEvent } = render(, { wrapper }); - await userEvent.click(getByRole('button', { name: 'FL' })); + await userEvent.click(getByRole('button', { name: 'Open user button' })); await userEvent.click(getByText('Manage account')); expect(fixtures.clerk.openUserProfile).toHaveBeenCalled(); }); @@ -60,7 +60,7 @@ describe('UserButton', () => { }); }); const { getByText, getByRole, userEvent } = render(, { wrapper }); - await userEvent.click(getByRole('button', { name: 'FL' })); + await userEvent.click(getByRole('button', { name: 'Open user button' })); await userEvent.click(getByText('Sign out')); expect(fixtures.clerk.signOut).toHaveBeenCalled(); }); @@ -96,7 +96,7 @@ describe('UserButton', () => { it('renders all sessions', async () => { const { wrapper } = await createFixtures(initConfig); const { getByText, getByRole, userEvent } = render(, { wrapper }); - await userEvent.click(getByRole('button', { name: 'FL' })); + await userEvent.click(getByRole('button', { name: 'Open user button' })); expect(getByText('First1 Last1')).toBeDefined(); expect(getByText('First2 Last2')).toBeDefined(); expect(getByText('First3 Last3')).toBeDefined(); @@ -106,7 +106,7 @@ describe('UserButton', () => { const { wrapper, fixtures } = await createFixtures(initConfig); fixtures.clerk.setActive.mockReturnValueOnce(Promise.resolve()); const { getByText, getByRole, userEvent } = render(, { wrapper }); - await userEvent.click(getByRole('button', { name: 'FL' })); + await userEvent.click(getByRole('button', { name: 'Open user button' })); await userEvent.click(getByText('First3 Last3')); expect(fixtures.clerk.setActive).toHaveBeenCalledWith( expect.objectContaining({ session: expect.objectContaining({ user: expect.objectContaining({ id: '3' }) }) }), @@ -117,7 +117,7 @@ describe('UserButton', () => { const { wrapper, fixtures } = await createFixtures(initConfig); fixtures.clerk.signOut.mockReturnValueOnce(Promise.resolve()); const { getByText, getByRole, userEvent } = render(, { wrapper }); - await userEvent.click(getByRole('button', { name: 'FL' })); + await userEvent.click(getByRole('button', { name: 'Open user button' })); await userEvent.click(getByText('Sign out')); expect(fixtures.clerk.signOut).toHaveBeenCalledWith(expect.any(Function), { sessionId: '0' }); }); diff --git a/packages/clerk-js/src/ui/elements/Actions.tsx b/packages/clerk-js/src/ui/elements/Actions.tsx index fcb2d339f6..434b61f4fa 100644 --- a/packages/clerk-js/src/ui/elements/Actions.tsx +++ b/packages/clerk-js/src/ui/elements/Actions.tsx @@ -86,6 +86,7 @@ export const Action = (props: ActionProps) => { ]} isDisabled={card.isLoading} onClick={onClick} + role='menuitem' {...rest} > { '&:hover': { color: 'inherit' }, }} isExternal + aria-label='Clerk logo' >