Improve ARIA labels for buttons and links on the Account setting page (#25499)

* Improve announced button and link labels for screen reader users

* Improve labels for integration widgets and update test

* Make integration widget IDs to be required

GitOrigin-RevId: 1e0124ef63a91fb63dffd79881c60794bccb9d27
This commit is contained in:
Rebeka Dekany
2025-05-12 18:10:00 +02:00
committed by Copybot
parent 3cb7ef05d9
commit ac51878186
13 changed files with 102 additions and 44 deletions
+2 -2
View File
@@ -93,7 +93,7 @@ describe('git-bridge', function () {
cy.get('code').contains(`git clone ${gitURL(id.toString())}`)
})
cy.findByRole('button', {
name: 'Generate token',
name: /generate token/i,
}).click()
cy.get('code').contains(/olp_[a-zA-Z0-9]{16}/)
})
@@ -196,7 +196,7 @@ describe('git-bridge', function () {
cy.get('code').contains(`git clone ${gitURL(projectId.toString())}`)
})
cy.findByRole('button', {
name: 'Generate token',
name: /generate token/i,
}).click()
cy.get('code')
.contains(/olp_[a-zA-Z0-9]{16}/)
@@ -895,10 +895,12 @@
"layout_options": "",
"layout_processing": "",
"learn_more": "",
"learn_more_about": "",
"learn_more_about_account": "",
"learn_more_about_compile_timeouts": "",
"learn_more_about_link_sharing": "",
"learn_more_about_managed_users": "",
"learn_more_about_managing_email": "",
"learn_more_about_other_causes_of_compile_timeouts": "",
"leave": "",
"leave_any_group_subscriptions": "",
@@ -1144,6 +1146,7 @@
"other_causes_of_compile_timeouts": "",
"other_logs_and_files": "",
"other_output_files": "",
"our_help_page": "",
"our_team_will_get_back_to_you_shortly": "",
"our_values": "",
"out_of_sync": "",
@@ -70,7 +70,7 @@ function AccountInfoSection() {
return (
<>
<h3>{t('update_account_info')}</h3>
<h3 id="update-account-info">{t('update_account_info')}</h3>
<form id="account-info-form" onSubmit={handleSubmit}>
{hasAffiliationsFeature ? null : (
<ReadOrWriteFormGroup
@@ -128,6 +128,7 @@ function AccountInfoSection() {
disabled={!isFormValid}
isLoading={isLoading}
loadingLabel={t('saving') + '…'}
aria-labelledby={isLoading ? undefined : 'update-account-info'}
>
{t('update')}
</OLButton>
@@ -40,6 +40,7 @@ function EmailsSectionContent() {
<a
href="/learn/how-to/Managing_your_Overleaf_emails"
target="_blank"
aria-label={t('learn_more_about_managing_email')}
/>,
]}
/>
@@ -20,6 +20,7 @@ function trackLinkingClick(integration: string) {
}
type IntegrationLinkingWidgetProps = {
id: string
logo: ReactNode
title: string
description: string
@@ -35,6 +36,7 @@ type IntegrationLinkingWidgetProps = {
}
export function IntegrationLinkingWidget({
id,
logo,
title,
description,
@@ -65,12 +67,17 @@ export function IntegrationLinkingWidget({
<div>{logo}</div>
<div className="description-container">
<div className="title-row">
<h4>{title}</h4>
<h4 id={id}>{title}</h4>
{!hasFeature && <OLBadge bg="info">{t('premium_feature')}</OLBadge>}
</div>
<p className="small">
{description}{' '}
<a href={helpPath} target="_blank" rel="noreferrer">
<a
href={helpPath}
target="_blank"
rel="noreferrer"
aria-label={t('learn_more_about', { integrationName: title })}
>
{t('learn_more')}
</a>
</p>
@@ -78,6 +85,7 @@ export function IntegrationLinkingWidget({
</div>
<div>
<ActionButton
titleId={id}
integration={title}
hasFeature={hasFeature}
linked={linked}
@@ -105,6 +113,7 @@ type ActionButtonProps = {
handleUnlinkClick: () => void
linkPath: string
disabled?: boolean
titleId: string
}
function ActionButton({
@@ -114,16 +123,22 @@ function ActionButton({
linkPath,
disabled,
integration,
titleId,
}: ActionButtonProps) {
const { t } = useTranslation()
const linkTextId = `${titleId}-link`
if (!hasFeature) {
return (
<OLButton
variant="primary"
href="/user/subscription/plans"
onClick={() => trackUpgradeClick(integration)}
aria-labelledby={`${titleId} ${linkTextId}`}
>
<span className="text-capitalize">{t('upgrade')}</span>
<span id={linkTextId} className="text-capitalize">
{t('upgrade')}
</span>
</OLButton>
)
} else if (linked) {
@@ -69,12 +69,17 @@ export function SSOLinkingWidget({
<div>{providerLogos[providerId]}</div>
<div className="description-container">
<div className="title-row">
<h4>{title}</h4>
<h4 id={providerId}>{title}</h4>
</div>
<p className="small">
{description?.replace(/<[^>]+>/g, '')}{' '}
{helpPath ? (
<a href={helpPath} target="_blank" rel="noreferrer">
<a
href={helpPath}
target="_blank"
rel="noreferrer"
aria-label={t('learn_more_about', { integrationName: title })}
>
{t('learn_more')}
</a>
) : null}
@@ -85,6 +90,7 @@ export function SSOLinkingWidget({
</div>
<div>
<ActionButton
titleId={providerId}
unlinkRequestInflight={unlinkRequestInflight}
accountIsLinked={linked}
linkPath={`${linkPath}?intent=link`}
@@ -106,6 +112,7 @@ type ActionButtonProps = {
accountIsLinked?: boolean
linkPath: string
onUnlinkClick: () => void
titleId: string
}
function ActionButton({
@@ -113,8 +120,11 @@ function ActionButton({
accountIsLinked,
linkPath,
onUnlinkClick,
titleId,
}: ActionButtonProps) {
const { t } = useTranslation()
const linkTextId = `${titleId}-link`
if (unlinkRequestInflight) {
return (
<OLButton variant="danger-ghost" disabled>
@@ -123,13 +133,24 @@ function ActionButton({
)
} else if (accountIsLinked) {
return (
<OLButton variant="danger-ghost" onClick={onUnlinkClick}>
<OLButton
variant="danger-ghost"
onClick={onUnlinkClick}
aria-labelledby={`${linkTextId} ${titleId}`}
id={linkTextId}
>
{t('unlink')}
</OLButton>
)
} else {
return (
<OLButton variant="secondary" href={linkPath} className="text-capitalize">
<OLButton
variant="secondary"
href={linkPath}
className="text-capitalize"
aria-labelledby={`${linkTextId} ${titleId}`}
id={linkTextId}
>
{t('link')}
</OLButton>
)
+3
View File
@@ -1174,11 +1174,13 @@
"ldap_create_admin_instructions": "Choose an email address for the first __appName__ admin account. This should correspond to an account in the LDAP system. You will then be asked to log in with this account.",
"learn": "Learn",
"learn_more": "Learn more",
"learn_more_about": "Learn more about __integrationName__",
"learn_more_about_account": "<0>Learn more</0> about managing your __appName__ account.",
"learn_more_about_compile_timeouts": "<0>Learn more</0> about compile timeouts.",
"learn_more_about_emails": "<0>Learn more</0> about managing your __appName__ emails.",
"learn_more_about_link_sharing": "Learn more about Link Sharing",
"learn_more_about_managed_users": "Learn more about Managed Users.",
"learn_more_about_managing_email": "Learn more about managing your __appName__ emails.",
"learn_more_about_other_causes_of_compile_timeouts": "<0>Learn more</0> about other causes of compile timeouts and how to fix them.",
"leave": "Leave",
"leave_any_group_subscriptions": "Leave any group subscriptions other than the one that will be managing your account. <0>Leave them from the Subscription page.</0>",
@@ -1511,6 +1513,7 @@
"other_output_files": "Download other output files",
"other_sessions": "Other Sessions",
"other_ways_to_log_in": "Other ways to log in",
"our_help_page": "Our help page",
"our_team_will_get_back_to_you_shortly": "Our team will get back to you shortly.",
"our_values": "Our values",
"out_of_sync": "Out of sync",
@@ -47,7 +47,7 @@ describe('<AccountInfoSection />', function () {
})
fireEvent.click(
screen.getByRole('button', {
name: 'Update',
name: /update/i,
})
)
expect(updateMock.callHistory.called()).to.be.true
@@ -68,7 +68,7 @@ describe('<AccountInfoSection />', function () {
target: { value: 'john' },
})
const button = screen.getByRole('button', {
name: 'Update',
name: /update/i,
}) as HTMLButtonElement
expect(button.disabled).to.be.true
@@ -87,14 +87,14 @@ describe('<AccountInfoSection />', function () {
fireEvent.click(
screen.getByRole('button', {
name: 'Update',
name: /update/i,
})
)
await screen.findByRole('button', { name: /saving/i })
finishUpdateCall(200)
await screen.findByRole('button', {
name: 'Update',
name: /update/i,
})
screen.getByText('Thanks, your settings have been updated.')
})
@@ -105,7 +105,7 @@ describe('<AccountInfoSection />', function () {
fireEvent.click(
screen.getByRole('button', {
name: 'Update',
name: /update/i,
})
)
await screen.findByText('Something went wrong. Please try again.')
@@ -117,7 +117,7 @@ describe('<AccountInfoSection />', function () {
fireEvent.click(
screen.getByRole('button', {
name: 'Update',
name: /update/i,
})
)
await screen.findByText(
@@ -136,7 +136,7 @@ describe('<AccountInfoSection />', function () {
fireEvent.click(
screen.getByRole('button', {
name: 'Update',
name: /update/i,
})
)
await screen.findByText('This email is already registered')
@@ -153,7 +153,7 @@ describe('<AccountInfoSection />', function () {
fireEvent.click(
screen.getByRole('button', {
name: 'Update',
name: /update/i,
})
)
expect(
@@ -184,7 +184,7 @@ describe('<AccountInfoSection />', function () {
fireEvent.click(
screen.getByRole('button', {
name: 'Update',
name: /update account info/i,
})
)
expect(
@@ -212,7 +212,7 @@ describe('<AccountInfoSection />', function () {
fireEvent.click(
screen.getByRole('button', {
name: 'Update',
name: /update/i,
})
)
expect(
@@ -106,7 +106,7 @@ describe('<EmailsSection />', function () {
})
fireEvent.click(button)
await screen.findByLabelText(/email/i)
await screen.findByLabelText(/email/i, { selector: 'input' })
})
it('renders "Start adding your address" until a valid email is typed', async function () {
@@ -121,7 +121,7 @@ describe('<EmailsSection />', function () {
})
fireEvent.click(button)
const input = screen.getByLabelText(/email/i)
const input = screen.getByLabelText(/email/i, { selector: 'input' })
// initially the text is displayed and the "add email" button disabled
screen.getByText('Start by adding your email address.')
@@ -200,7 +200,7 @@ describe('<EmailsSection />', function () {
.post('/user/emails/confirm-secondary', 200)
fireEvent.click(addAnotherEmailBtn)
const input = screen.getByLabelText(/email/i)
const input = screen.getByLabelText(/email/i, { selector: 'input' })
fireEvent.change(input, {
target: { value: userEmailData.email },
@@ -242,7 +242,7 @@ describe('<EmailsSection />', function () {
.post('/user/emails/secondary', 400)
fireEvent.click(addAnotherEmailBtn)
const input = screen.getByLabelText(/email/i)
const input = screen.getByLabelText(/email/i, { selector: 'input' })
fireEvent.change(input, {
target: { value: userEmailData.email },
@@ -279,7 +279,7 @@ describe('<EmailsSection />', function () {
await userEvent.click(button)
const input = screen.getByLabelText(/email/i)
const input = screen.getByLabelText(/email/i, { selector: 'input' })
fireEvent.change(input, {
target: { value: 'user@autocomplete.edu' },
})
@@ -302,7 +302,10 @@ describe('<EmailsSection />', function () {
await userEvent.click(button)
await userEvent.type(screen.getByLabelText(/email/i), userEmailData.email)
await userEvent.type(
screen.getByLabelText(/email/i, { selector: 'input' }),
userEmailData.email
)
await userEvent.click(screen.getByRole('button', { name: /let us know/i }))
@@ -415,7 +418,10 @@ describe('<EmailsSection />', function () {
// open "add new email" section and click "let us know" to open the Country/University form
await userEvent.click(button)
await userEvent.type(screen.getByLabelText(/email/i), userEmailData.email)
await userEvent.type(
screen.getByLabelText(/email/i, { selector: 'input' }),
userEmailData.email
)
await userEvent.click(screen.getByRole('button', { name: /let us know/i }))
// select a country
@@ -457,7 +463,10 @@ describe('<EmailsSection />', function () {
await userEvent.click(button)
await userEvent.type(screen.getByLabelText(/email/i), userEmailData.email)
await userEvent.type(
screen.getByLabelText(/email/i, { selector: 'input' }),
userEmailData.email
)
await userEvent.click(screen.getByRole('button', { name: /let us know/i }))
@@ -574,7 +583,7 @@ describe('<EmailsSection />', function () {
await userEvent.click(button)
await userEvent.type(
screen.getByLabelText(/email/i),
screen.getByLabelText(/email/i, { selector: 'input' }),
`user@${hostnameFirstChar}`
)
@@ -647,7 +656,7 @@ describe('<EmailsSection />', function () {
await userEvent.click(button)
await userEvent.type(
screen.getByLabelText(/email/i),
screen.getByLabelText(/email/i, { selector: 'input' }),
`user@${hostnameFirstChar}`
)
@@ -40,6 +40,7 @@ describe('<EmailsSection />', function () {
screen.getByText(/add additional email addresses/i)
screen.getByText(/to change your primary email/i)
screen.getByLabelText('Learn more about managing your Overleaf emails.')
})
it('renders a loading message when loading', async function () {
@@ -69,16 +69,19 @@ describe('<LinkingSection />', function () {
screen.getByText('linked accounts')
screen.getByText('Google')
screen.getByRole('button', { name: /link google/i })
screen.getByText('Log in with Google.')
screen.getByRole('button', { name: 'Unlink' })
screen.getByRole('button', { name: /unlink/i })
screen.getByText('ORCID')
screen.getByText(
/Securely establish your identity by linking your ORCID iD/
)
const helpLink = screen.getByRole('link', { name: 'Learn more' })
const helpLink = screen.getByRole('link', {
name: /learn more about orcid/i,
})
expect(helpLink.getAttribute('href')).to.equal('/blog/434')
const linkButton = screen.getByRole('button', { name: 'Link' })
const linkButton = screen.getByRole('button', { name: /link orcid/i })
expect(linkButton.getAttribute('href')).to.equal('/auth/orcid?intent=link')
})
@@ -6,6 +6,7 @@ import * as eventTracking from '@/infrastructure/event-tracking'
describe('<IntegrationLinkingWidgetTest/>', function () {
const defaultProps = {
id: 'integration-widget-id',
logo: <div />,
title: 'Integration',
description: 'paragraph1',
@@ -32,7 +33,7 @@ describe('<IntegrationLinkingWidgetTest/>', function () {
})
it('should render an upgrade link and track clicks', function () {
const upgradeLink = screen.getByRole('button', { name: 'Upgrade' })
const upgradeLink = screen.getByRole('button', { name: /upgrade/i })
expect(upgradeLink.getAttribute('href')).to.equal(
'/user/subscription/plans'
)
@@ -25,7 +25,7 @@ describe('<SSOLinkingWidget />', function () {
screen.getByText('integration')
screen.getByText('integration description')
expect(
screen.getByRole('link', { name: 'Learn more' }).getAttribute('href')
screen.getByRole('link', { name: /learn more/i }).getAttribute('href')
).to.equal('/help/integration')
})
@@ -33,7 +33,7 @@ describe('<SSOLinkingWidget />', function () {
it('should render a link to `linkPath`', function () {
render(<SSOLinkingWidget {...defaultProps} linked={false} />)
expect(
screen.getByRole('button', { name: 'Link' }).getAttribute('href')
screen.getByRole('button', { name: /link/i }).getAttribute('href')
).to.equal('/integration/link?intent=link')
})
})
@@ -49,11 +49,11 @@ describe('<SSOLinkingWidget />', function () {
})
it('should display an `unlink` button', function () {
screen.getByRole('button', { name: 'Unlink' })
screen.getByRole('button', { name: /unlink/i })
})
it('should open a modal to confirm integration unlinking', function () {
fireEvent.click(screen.getByRole('button', { name: 'Unlink' }))
fireEvent.click(screen.getByRole('button', { name: /unlink/i }))
screen.getByText('Unlink integration Account')
screen.getByText(
'Warning: When you unlink your account from integration you will not be able to sign in using integration anymore.'
@@ -61,7 +61,7 @@ describe('<SSOLinkingWidget />', function () {
})
it('should cancel unlinking when clicking cancel in the confirmation modal', async function () {
fireEvent.click(screen.getByRole('button', { name: 'Unlink' }))
fireEvent.click(screen.getByRole('button', { name: /unlink/i }))
const cancelBtn = screen.getByRole('button', {
name: 'Cancel',
hidden: false,
@@ -80,9 +80,9 @@ describe('<SSOLinkingWidget />', function () {
render(
<SSOLinkingWidget {...defaultProps} linked onUnlink={unlinkFunction} />
)
fireEvent.click(screen.getByRole('button', { name: 'Unlink' }))
fireEvent.click(screen.getByRole('button', { name: /unlink/i }))
confirmBtn = within(screen.getByRole('dialog')).getByRole('button', {
name: 'Unlink',
name: /unlink/i,
hidden: false,
})
})
@@ -114,11 +114,11 @@ describe('<SSOLinkingWidget />', function () {
render(
<SSOLinkingWidget {...defaultProps} linked onUnlink={unlinkFunction} />
)
fireEvent.click(screen.getByRole('button', { name: 'Unlink' }))
fireEvent.click(screen.getByRole('button', { name: /unlink/i }))
const confirmBtn = within(screen.getByRole('dialog')).getByRole(
'button',
{
name: 'Unlink',
name: /unlink/i,
hidden: false,
}
)
@@ -130,7 +130,7 @@ describe('<SSOLinkingWidget />', function () {
})
it('should display the unlink button ', async function () {
await screen.findByRole('button', { name: 'Unlink' })
await screen.findByRole('button', { name: /unlink/i })
})
})
})