diff options
author | stanislavh <stanislav.honcharov@sonarsource.com> | 2024-11-26 15:06:59 +0100 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2024-11-28 20:02:58 +0000 |
commit | 94747daf9743ed21903f14b045fb01f2798d4507 (patch) | |
tree | 0d623ed21baccb996441ee8a192b403d15313dd6 /server/sonar-web/src/main/js | |
parent | faa2dd6c0e597a9250b82898870119aafdbe8fc9 (diff) | |
download | sonarqube-94747daf9743ed21903f14b045fb01f2798d4507.tar.gz sonarqube-94747daf9743ed21903f14b045fb01f2798d4507.zip |
SONAR-22310 Fix a11y issue on project information page
Diffstat (limited to 'server/sonar-web/src/main/js')
17 files changed, 220 insertions, 139 deletions
diff --git a/server/sonar-web/src/main/js/apps/projectInformation/ProjectInformationApp.tsx b/server/sonar-web/src/main/js/apps/projectInformation/ProjectInformationApp.tsx index bcc3d3d4268..e808094109a 100644 --- a/server/sonar-web/src/main/js/apps/projectInformation/ProjectInformationApp.tsx +++ b/server/sonar-web/src/main/js/apps/projectInformation/ProjectInformationApp.tsx @@ -18,8 +18,10 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { Heading } from '@sonarsource/echoes-react'; import { useEffect, useState } from 'react'; -import { Card, LargeCenteredLayout, PageContentFontWrapper, Title } from '~design-system'; +import { Helmet } from 'react-helmet-async'; +import { Card, LargeCenteredLayout, PageContentFontWrapper } from '~design-system'; import { MetricKey } from '~sonar-aligned/types/metrics'; import { getMeasures } from '../../api/measures'; import withAvailableFeatures, { @@ -71,14 +73,17 @@ function ProjectInformationApp(props: Props) { const regulatoryReportFeatureEnabled = props.hasFeature(Feature.RegulatoryReport); const isApp = isApplication(component.qualifier); + const title = translate(isApp ? 'application' : 'project', 'info.title'); + return ( <main> + <Helmet defer={false} title={title} /> <LargeCenteredLayout> <PageContentFontWrapper> <div className="overview sw-my-6 sw-typo-default"> - <Title className="sw-mb-12"> - {translate(isApp ? 'application' : 'project', 'info.title')} - </Title> + <Heading as="h1" className="sw-mb-12"> + {title} + </Heading> <div className="sw-grid sw-grid-cols-[488px_minmax(0,_2fr)] sw-gap-x-12 sw-gap-y-3 sw-auto-rows-min"> <div className="sw-row-span-3"> <Card> diff --git a/server/sonar-web/src/main/js/apps/projectInformation/__tests__/ProjectInformationApp-it.tsx b/server/sonar-web/src/main/js/apps/projectInformation/__tests__/ProjectInformationApp-it.tsx index 867f9f660ba..9996a39d69b 100644 --- a/server/sonar-web/src/main/js/apps/projectInformation/__tests__/ProjectInformationApp-it.tsx +++ b/server/sonar-web/src/main/js/apps/projectInformation/__tests__/ProjectInformationApp-it.tsx @@ -62,10 +62,9 @@ const modeHandler = new ModeServiceMock(); const ui = { projectPageTitle: byRole('heading', { name: 'project.info.title' }), applicationPageTitle: byRole('heading', { name: 'application.info.title' }), - qualityGateList: byRole('list', { name: 'project.info.quality_gate' }), - qualityProfilesList: byRole('list', { name: 'overview.quality_profiles' }), - externalLinksList: byRole('list', { name: 'overview.external_links' }), - link: byRole('link'), + qualityGateHeader: byRole('heading', { name: 'project.info.quality_gate' }), + qualityProfilesHeader: byRole('heading', { name: 'overview.quality_profiles' }), + externalLinksHeader: byRole('heading', { name: 'overview.external_links' }), tags: byRole('generic', { name: /tags:/ }), size: byRole('link', { name: /project.info.see_more_info_on_x_locs/ }), newKeyInput: byRole('textbox'), @@ -99,10 +98,10 @@ it('should show fields for project', async () => { { currentUser: mockLoggedInUser(), featureList: [Feature.AiCodeAssurance] }, ); expect(await ui.projectPageTitle.find()).toBeInTheDocument(); - expect(ui.qualityGateList.get()).toBeInTheDocument(); - expect(ui.link.getAll(ui.qualityGateList.get())).toHaveLength(1); - expect(ui.link.getAll(ui.qualityProfilesList.get())).toHaveLength(1); - expect(ui.link.getAll(ui.externalLinksList.get())).toHaveLength(1); + expect(ui.qualityGateHeader.get()).toBeInTheDocument(); + expect(byRole('link', { name: /project.info.quality_gate.link_label/ }).getAll()).toHaveLength(1); + expect(byRole('link', { name: /overview.link_to_x_profile_y/ }).getAll()).toHaveLength(1); + expect(byRole('link', { name: 'test' }).getAll()).toHaveLength(1); expect(screen.getByText('project.info.ai_code_assurance.title')).toBeInTheDocument(); expect(screen.getByText('Test description')).toBeInTheDocument(); expect(screen.getByText('my-project')).toBeInTheDocument(); @@ -128,9 +127,9 @@ it('should show application fields', async () => { { currentUser: mockLoggedInUser() }, ); expect(await ui.applicationPageTitle.find()).toBeInTheDocument(); - expect(ui.qualityGateList.query()).not.toBeInTheDocument(); - expect(ui.qualityProfilesList.query()).not.toBeInTheDocument(); - expect(ui.externalLinksList.query()).not.toBeInTheDocument(); + expect(ui.qualityGateHeader.query()).not.toBeInTheDocument(); + expect(ui.qualityProfilesHeader.query()).not.toBeInTheDocument(); + expect(ui.externalLinksHeader.query()).not.toBeInTheDocument(); expect(screen.getByText('Test description')).toBeInTheDocument(); expect(screen.getByText('my-project')).toBeInTheDocument(); expect(screen.getByText('visibility.private')).toBeInTheDocument(); @@ -188,8 +187,8 @@ it('should not show field that is not configured', async () => { qualityProfiles: [], }); expect(await ui.projectPageTitle.find()).toBeInTheDocument(); - expect(ui.qualityGateList.query()).not.toBeInTheDocument(); - expect(ui.qualityProfilesList.query()).not.toBeInTheDocument(); + expect(ui.qualityGateHeader.query()).not.toBeInTheDocument(); + expect(ui.qualityProfilesHeader.query()).not.toBeInTheDocument(); expect(screen.getByText('visibility.public')).toBeInTheDocument(); expect(ui.tags.get()).toHaveTextContent('no_tags'); expect(screen.getByText('project.info.empty_description')).toBeInTheDocument(); @@ -202,8 +201,8 @@ it('should hide visibility if public', async () => { qualityProfiles: [], }); expect(await ui.projectPageTitle.find()).toBeInTheDocument(); - expect(ui.qualityGateList.query()).not.toBeInTheDocument(); - expect(ui.qualityProfilesList.query()).not.toBeInTheDocument(); + expect(ui.qualityGateHeader.query()).not.toBeInTheDocument(); + expect(ui.qualityProfilesHeader.query()).not.toBeInTheDocument(); expect(screen.getByText('visibility.public')).toBeInTheDocument(); expect(ui.tags.get()).toHaveTextContent('no_tags'); expect(screen.getByText('project.info.empty_description')).toBeInTheDocument(); diff --git a/server/sonar-web/src/main/js/apps/projectInformation/about/AboutProject.tsx b/server/sonar-web/src/main/js/apps/projectInformation/about/AboutProject.tsx index 27dd155c0fb..c16707829f7 100644 --- a/server/sonar-web/src/main/js/apps/projectInformation/about/AboutProject.tsx +++ b/server/sonar-web/src/main/js/apps/projectInformation/about/AboutProject.tsx @@ -18,10 +18,11 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { Heading } from '@sonarsource/echoes-react'; import classNames from 'classnames'; import { PropsWithChildren, useEffect, useState } from 'react'; import { FormattedMessage } from 'react-intl'; -import { BasicSeparator, SubHeading, SubTitle } from '~design-system'; +import { BasicSeparator } from '~design-system'; import { ComponentQualifier, Visibility } from '~sonar-aligned/types/component'; import { getProjectLinks } from '../../../api/projectLinks'; import { useAvailableFeatures } from '../../../app/components/available-features/withAvailableFeatures'; @@ -68,9 +69,9 @@ export default function AboutProject(props: AboutProjectProps) { return ( <> - <div> - <SubTitle>{translate(isApp ? 'application' : 'project', 'about.title')}</SubTitle> - </div> + <Heading className="sw-mb-4" as="h2"> + {translate(isApp ? 'application' : 'project', 'about.title')} + </Heading> {!isApp && (component.qualityGate || @@ -88,19 +89,19 @@ export default function AboutProject(props: AboutProjectProps) { {isAiAssured === true && ( <ProjectInformationSection> - <SubHeading>{translate('project.info.ai_code_assurance.title')}</SubHeading> - <span> - <FormattedMessage id="projects.ai_code.content" /> - </span> + <Heading className="sw-mb-2" as="h3"> + {translate('project.info.ai_code_assurance.title')} + </Heading> + <FormattedMessage id="projects.ai_code.content" /> </ProjectInformationSection> )} {component.isAiCodeFixEnabled === true && ( <ProjectInformationSection> - <SubHeading>{translate('project.info.ai_code_fix.title')}</SubHeading> - <span> - <FormattedMessage id="project.info.ai_code_fix.message" /> - </span> + <Heading className="sw-mb-2" as="h3"> + {translate('project.info.ai_code_fix.title')} + </Heading> + <FormattedMessage id="project.info.ai_code_fix.message" /> </ProjectInformationSection> )} @@ -145,7 +146,7 @@ function ProjectInformationSection(props: PropsWithChildren<ProjectInformationSe const { children, className, last = false } = props; return ( <> - <div className={classNames('sw-py-4', className)}>{children}</div> + <section className={classNames('sw-py-4', className)}>{children}</section> {!last && <BasicSeparator />} </> ); diff --git a/server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaDescription.tsx b/server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaDescription.tsx index 1f8ee2be88d..1fceb061a5c 100644 --- a/server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaDescription.tsx +++ b/server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaDescription.tsx @@ -18,7 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { SubHeading, TextMuted } from '~design-system'; +import { Heading, Text } from '@sonarsource/echoes-react'; import { translate } from '../../../../helpers/l10n'; interface Props { @@ -29,11 +29,10 @@ interface Props { export default function MetaDescription({ description, isApp }: Props) { return ( <> - <SubHeading>{translate('project.info.description')}</SubHeading> - <TextMuted - className="it__project-description" - text={description ?? translate(isApp ? 'application' : 'project', 'info.empty_description')} - /> + <Heading as="h3">{translate('project.info.description')}</Heading> + <Text as="p" isSubdued className="it__project-description sw-mt-2"> + {description ?? translate(isApp ? 'application' : 'project', 'info.empty_description')} + </Text> </> ); } diff --git a/server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaKey.tsx b/server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaKey.tsx index 57d72f61f32..59605f5d494 100644 --- a/server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaKey.tsx +++ b/server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaKey.tsx @@ -18,8 +18,15 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { ClipboardIconButton, CodeSnippet, HelperHintIcon, SubHeading } from '~design-system'; -import HelpTooltip from '~sonar-aligned/components/controls/HelpTooltip'; +import { + Button, + ButtonVariety, + Heading, + IconQuestionMark, + Popover, +} from '@sonarsource/echoes-react'; +import { useIntl } from 'react-intl'; +import { ClipboardIconButton, CodeSnippet } from '~design-system'; import { translate } from '../../../../helpers/l10n'; interface MetaKeyProps { @@ -28,26 +35,35 @@ interface MetaKeyProps { } export default function MetaKey({ componentKey, qualifier }: MetaKeyProps) { + const intl = useIntl(); return ( <> <div className="sw-flex sw-items-baseline"> - <SubHeading>{translate('overview.project_key', qualifier)}</SubHeading> - <HelpTooltip - className="sw-ml-1" - overlay={ - <p className="sw-max-w-abs-250"> - {translate('overview.project_key.tooltip', qualifier)} - </p> - } + <Heading as="h3">{translate('overview.project_key', qualifier)}</Heading> + <Popover + title={intl.formatMessage( + { id: 'about_x' }, + { x: translate('overview.project_key', qualifier) }, + )} + description={translate('overview.project_key.tooltip', qualifier)} > - <HelperHintIcon /> - </HelpTooltip> + <Button + className="sw-ml-1 sw-p-0 sw-h-fit sw-min-h-fit" + aria-label={intl.formatMessage({ id: 'help' })} + variety={ButtonVariety.DefaultGhost} + > + <IconQuestionMark color="echoes-color-icon-subdued" /> + </Button> + </Popover> </div> - <div className="sw-w-full"> - <div className="sw-flex sw-gap-2 sw-items-center sw-min-w-0"> - <CodeSnippet className="sw-min-w-0 sw-px-1" isOneLine noCopy snippet={componentKey} /> - <ClipboardIconButton copyValue={componentKey} /> - </div> + <div className="sw-mt-2 sw-w-full sw-flex sw-gap-2 sw-items-center sw-break-words sw-min-w-0"> + <CodeSnippet + className="sw-min-w-0 sw-px-1 sw-max-w-10/12" + noCopy + isOneLine + snippet={componentKey} + /> + <ClipboardIconButton copyValue={componentKey} /> </div> </> ); diff --git a/server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaLinks.tsx b/server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaLinks.tsx index 41d8dbd8e6d..46a49d35429 100644 --- a/server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaLinks.tsx +++ b/server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaLinks.tsx @@ -18,7 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { SubHeading } from '~design-system'; +import { Heading } from '@sonarsource/echoes-react'; import MetaLink from '../../../../components/common/MetaLink'; import { translate } from '../../../../helpers/l10n'; import { orderLinks } from '../../../../helpers/projectLinks'; @@ -33,8 +33,8 @@ export default function MetaLinks({ links }: Readonly<Props>) { return ( <> - <SubHeading id="external-links">{translate('overview.external_links')}</SubHeading> - <ul className="sw-flex sw-flex-col sw-gap-2" aria-labelledby="external-links"> + <Heading as="h3">{translate('overview.external_links')}</Heading> + <ul className="sw-mt-2 sw-flex sw-flex-col sw-gap-3"> {orderedLinks.map((link) => ( <MetaLink key={link.id} link={link} /> ))} diff --git a/server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaQualityGate.tsx b/server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaQualityGate.tsx index 58c5d34997e..f06675f8ddf 100644 --- a/server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaQualityGate.tsx +++ b/server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaQualityGate.tsx @@ -18,8 +18,8 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { FormattedMessage } from 'react-intl'; -import { Link, Note, StyledMutedText, SubHeading } from '~design-system'; +import { Heading, LinkStandalone, Text } from '@sonarsource/echoes-react'; +import { FormattedMessage, useIntl } from 'react-intl'; import { translate } from '../../../../helpers/l10n'; import { getQualityGateUrl } from '../../../../helpers/urls'; @@ -29,21 +29,33 @@ interface Props { } export default function MetaQualityGate({ qualityGate, isAiAssured }: Props) { + const intl = useIntl(); return ( - <div> - <SubHeading id="quality-gate-header">{translate('project.info.quality_gate')}</SubHeading> - - <ul className="sw-flex sw-flex-col sw-gap-2" aria-labelledby="quality-gate-header"> + <section> + <Heading as="h3">{translate('project.info.quality_gate')}</Heading> + <ul className="sw-mt-2 sw-flex sw-flex-col sw-gap-3"> <li> - {qualityGate.isDefault && <Note className="sw-mr-2">({translate('default')})</Note>} - <Link to={getQualityGateUrl(qualityGate.name)}>{qualityGate.name}</Link> + {qualityGate.isDefault && ( + <Text isSubdued className="sw-mr-2"> + ({translate('default')}) + </Text> + )} + <LinkStandalone + aria-label={intl.formatMessage( + { id: 'project.info.quality_gate.link_label' }, + { gate: qualityGate.name }, + )} + to={getQualityGateUrl(qualityGate.name)} + > + {qualityGate.name} + </LinkStandalone> </li> </ul> {isAiAssured === true && ( - <StyledMutedText className="sw-text-wrap sw-mt-2"> + <Text as="p" isSubdued className="sw-mt-2"> <FormattedMessage id="project.info.quality_gate.ai_code_assurance.description" /> - </StyledMutedText> + </Text> )} - </div> + </section> ); } diff --git a/server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaQualityProfiles.tsx b/server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaQualityProfiles.tsx index b7fb2514ee1..c06a665a363 100644 --- a/server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaQualityProfiles.tsx +++ b/server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaQualityProfiles.tsx @@ -18,12 +18,12 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { Heading, LinkStandalone, Tooltip } from '@sonarsource/echoes-react'; import React, { useContext, useEffect } from 'react'; -import { Badge, Link, SubHeading } from '~design-system'; +import { Badge } from '~design-system'; import { ComponentQualityProfile } from '~sonar-aligned/types/component'; import { searchRules } from '../../../../api/rules'; import { LanguagesContext } from '../../../../app/components/languages/LanguagesContext'; -import Tooltip from '../../../../components/controls/Tooltip'; import { translate, translateWithParameters } from '../../../../helpers/l10n'; import { getQualityProfileUrl } from '../../../../helpers/urls'; import { Languages } from '../../../../types/languages'; @@ -62,10 +62,9 @@ export function MetaQualityProfiles({ profiles }: Readonly<Props>) { }, [profiles]); return ( - <div> - <SubHeading id="quality-profiles-list">{translate('overview.quality_profiles')}</SubHeading> - - <ul className="sw-flex sw-flex-col sw-gap-2" aria-labelledby="quality-profiles-list"> + <section> + <Heading as="h3">{translate('overview.quality_profiles')}</Heading> + <ul className="sw-mt-2 sw-flex sw-flex-col sw-gap-3"> {profiles.map((profile) => ( <ProfileItem key={profile.key} @@ -75,7 +74,7 @@ export function MetaQualityProfiles({ profiles }: Readonly<Props>) { /> ))} </ul> - </div> + </section> ); } @@ -93,43 +92,42 @@ function ProfileItem({ const count = deprecatedByKey[profile.key] || 0; return ( - <li> - <div className="sw-grid sw-grid-cols-[1fr_3fr]"> - <span>{languageName}</span> - <div> - {profile.deleted ? ( - <Tooltip - key={profile.key} - content={translateWithParameters('overview.deleted_profile', profile.name)} + <li className="sw-grid sw-grid-cols-[1fr_3fr]"> + <span>{languageName}</span> + <div> + {profile.deleted ? ( + <Tooltip + key={profile.key} + content={translateWithParameters('overview.deleted_profile', profile.name)} + > + <div className="project-info-deleted-profile">{profile.name}</div> + </Tooltip> + ) : ( + <> + <LinkStandalone + aria-label={translateWithParameters( + 'overview.link_to_x_profile_y', + languageName, + profile.name, + )} + to={getQualityProfileUrl(profile.name, profile.language)} > - <div className="project-info-deleted-profile">{profile.name}</div> - </Tooltip> - ) : ( - <> - <Link to={getQualityProfileUrl(profile.name, profile.language)}> - <span - aria-label={translateWithParameters( - 'overview.link_to_x_profile_y', - languageName, - profile.name, - )} - > - {profile.name} + {profile.name} + </LinkStandalone> + {count > 0 && ( + <Tooltip + key={profile.key} + content={translateWithParameters('overview.deprecated_profile', count)} + > + <span> + <Badge className="sw-ml-6" variant="deleted"> + {translate('deprecated')} + </Badge> </span> - </Link> - {count > 0 && ( - <Tooltip - key={profile.key} - content={translateWithParameters('overview.deprecated_profile', count)} - > - <span className="sw-ml-6"> - <Badge variant="deleted">{translate('deprecated')}</Badge> - </span> - </Tooltip> - )} - </> - )} - </div> + </Tooltip> + )} + </> + )} </div> </li> ); diff --git a/server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaSize.tsx b/server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaSize.tsx index 63913b08057..be601b713bc 100644 --- a/server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaSize.tsx +++ b/server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaSize.tsx @@ -18,7 +18,8 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { DrilldownLink, Note, SizeIndicator, SubHeading } from '~design-system'; +import { Heading, Link, LinkHighlight, Text, TextSize } from '@sonarsource/echoes-react'; +import { SizeIndicator } from '~design-system'; import { formatMeasure } from '~sonar-aligned/helpers/measures'; import { ComponentQualifier } from '~sonar-aligned/types/component'; import { MetricKey, MetricType } from '~sonar-aligned/types/metrics'; @@ -46,23 +47,25 @@ export default function MetaSize({ component, measures }: MetaSizeProps) { return ( <> - <div className="sw-flex sw-items-baseline"> - <SubHeading>{localizeMetric(MetricKey.ncloc)}</SubHeading> + <div className="sw-mb-2 sw-flex sw-items-baseline"> + <Heading as="h3">{localizeMetric(MetricKey.ncloc)}</Heading> <span className="sw-ml-1">({translate('project.info.main_branch')})</span> </div> <div className="sw-flex sw-items-center"> {ncloc && ncloc.value ? ( <> - <DrilldownLink to={url}> - <span + <Text size={TextSize.Large}> + <Link aria-label={translateWithParameters( 'project.info.see_more_info_on_x_locs', ncloc.value, )} + highlight={LinkHighlight.Default} + to={url} > {formatMeasure(ncloc.value, MetricType.ShortInteger)} - </span> - </DrilldownLink> + </Link> + </Text> <span className="sw-ml-2"> <SizeIndicator value={Number(ncloc.value)} size="xs" /> @@ -75,13 +78,17 @@ export default function MetaSize({ component, measures }: MetaSizeProps) { {isApp && ( <span className="sw-inline-flex sw-items-center sw-ml-10"> {projects ? ( - <DrilldownLink to={url}> - <span>{formatMeasure(projects.value, MetricType.ShortInteger)}</span> - </DrilldownLink> + <Text size={TextSize.Large}> + <Link highlight={LinkHighlight.Default} to={url}> + {formatMeasure(projects.value, MetricType.ShortInteger)} + </Link> + </Text> ) : ( <span>0</span> )} - <Note className="sw-ml-1">{translate('metric.projects.name')}</Note> + <Text isSubdued className="sw-ml-1"> + {translate('metric.projects.name')} + </Text> </span> )} </div> diff --git a/server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaTags.tsx b/server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaTags.tsx index 15267e94c9f..96a753c2dee 100644 --- a/server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaTags.tsx +++ b/server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaTags.tsx @@ -18,10 +18,10 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { Spinner } from '@sonarsource/echoes-react'; +import { Heading, Spinner } from '@sonarsource/echoes-react'; import { difference, without } from 'lodash'; import { useEffect, useState } from 'react'; -import { MultiSelector, SubHeading, Tags } from '~design-system'; +import { MultiSelector, Tags } from '~design-system'; import { ComponentQualifier } from '~sonar-aligned/types/component'; import { searchProjectTags, setApplicationTags, setProjectTags } from '../../../../api/components'; import Tooltip from '../../../../components/controls/Tooltip'; @@ -74,7 +74,9 @@ export default function MetaTags(props: Props) { return ( <> - <SubHeading>{translate('tags')}</SubHeading> + <Heading as="h3" className="sw-mb-2"> + {translate('tags')} + </Heading> <Tags allowUpdate={canUpdateTags()} ariaTagsListLabel={translate('tags')} diff --git a/server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaVisibility.tsx b/server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaVisibility.tsx index 6e1a9e86605..d42dd5fdc73 100644 --- a/server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaVisibility.tsx +++ b/server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaVisibility.tsx @@ -18,7 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { SubHeading } from '~design-system'; +import { Heading } from '@sonarsource/echoes-react'; import { Visibility } from '~sonar-aligned/types/component'; import PrivacyBadgeContainer from '../../../../components/common/PrivacyBadgeContainer'; import { translate } from '../../../../helpers/l10n'; @@ -31,7 +31,9 @@ interface Props { export default function MetaVisibility({ qualifier, visibility }: Props) { return ( <> - <SubHeading>{translate('visibility')}</SubHeading> + <Heading className="sw-mb-2" as="h3"> + {translate('visibility')} + </Heading> <PrivacyBadgeContainer qualifier={qualifier} visibility={visibility} /> </> ); diff --git a/server/sonar-web/src/main/js/apps/projectInformation/about/components/__tests__/MetaQualityProfiles-test.tsx b/server/sonar-web/src/main/js/apps/projectInformation/about/components/__tests__/MetaQualityProfiles-test.tsx index e9a4852c8e4..dbfa94dbe8a 100644 --- a/server/sonar-web/src/main/js/apps/projectInformation/about/components/__tests__/MetaQualityProfiles-test.tsx +++ b/server/sonar-web/src/main/js/apps/projectInformation/about/components/__tests__/MetaQualityProfiles-test.tsx @@ -54,8 +54,12 @@ it('should render correctly', async () => { renderMetaQualityprofiles(); - expect(await screen.findByText('overview.deleted_profile.javascript')).toBeInTheDocument(); - expect(await screen.findByText('overview.deprecated_profile.10')).toBeInTheDocument(); + await expect(await screen.findByText('javascript')).toHaveATooltipWithContent( + 'overview.deleted_profile.javascript', + ); + await expect(await screen.findByText('deprecated')).toHaveATooltipWithContent( + 'overview.deprecated_profile.10', + ); }); function renderMetaQualityprofiles( diff --git a/server/sonar-web/src/main/js/apps/projectInformation/badges/ProjectBadges.tsx b/server/sonar-web/src/main/js/apps/projectInformation/badges/ProjectBadges.tsx index 28cdc20e5d8..551aa7caf8b 100644 --- a/server/sonar-web/src/main/js/apps/projectInformation/badges/ProjectBadges.tsx +++ b/server/sonar-web/src/main/js/apps/projectInformation/badges/ProjectBadges.tsx @@ -21,6 +21,7 @@ import { Spinner } from '@sonarsource/echoes-react'; import { isEmpty } from 'lodash'; import { useState } from 'react'; +import { useIntl } from 'react-intl'; import { BasicSeparator, ButtonSecondary, @@ -58,6 +59,7 @@ export default function ProjectBadges(props: ProjectBadgesProps) { branchLike, component: { key: project, qualifier, configuration }, } = props; + const intl = useIntl(); const [selectedType, setSelectedType] = useState(BadgeType.measure); const [selectedMetric, setSelectedMetric] = useState(MetricKey.alert_status); const [selectedFormat, setSelectedFormat] = useState<BadgeFormats>('md'); @@ -94,6 +96,8 @@ export default function ProjectBadges(props: ProjectBadgesProps) { }; const canRenew = configuration?.showSettings; + const selectedMetricOption = metricOptions.find((m) => m.value === selectedMetric); + return ( <div> <SubTitle>{translate('overview.badges.get_badge')}</SubTitle> @@ -107,7 +111,10 @@ export default function ProjectBadges(props: ProjectBadgesProps) { selected={BadgeType.measure === selectedType} image={ <Image - alt={translate('overview.badges', BadgeType.measure, 'alt')} + alt={intl.formatMessage( + { id: `overview.badges.${BadgeType.measure}.alt` }, + { metric: selectedMetricOption?.label }, + )} src={getBadgeUrl(BadgeType.measure, fullBadgeOptions, token, true)} /> } @@ -164,7 +171,7 @@ export default function ProjectBadges(props: ProjectBadgesProps) { setSelectedMetric(option.value); } }} - value={metricOptions.find((m) => m.value === selectedMetric)} + value={selectedMetricOption} /> </FormField> )} @@ -189,6 +196,7 @@ export default function ProjectBadges(props: ProjectBadgesProps) { <Spinner className="sw-my-2" isLoading={isFetchingToken || isRenewing}> {!isLoading && ( <CodeSnippet + copyAriaLabel={translate('overview.badges.copy_snippet')} language="plaintext" className="sw-p-6 it__code-snippet" snippet={getBadgeSnippet(selectedType, fullBadgeOptions, token)} diff --git a/server/sonar-web/src/main/js/apps/projectInformation/badges/__tests__/ProjectBadges-it.tsx b/server/sonar-web/src/main/js/apps/projectInformation/badges/__tests__/ProjectBadges-it.tsx index 9a1759b9ac6..2fcba449749 100644 --- a/server/sonar-web/src/main/js/apps/projectInformation/badges/__tests__/ProjectBadges-it.tsx +++ b/server/sonar-web/src/main/js/apps/projectInformation/badges/__tests__/ProjectBadges-it.tsx @@ -57,7 +57,9 @@ it('should renew token', async () => { renderProjectBadges(); await ui.appLoaded(); - expect(screen.getByAltText(`overview.badges.${BadgeType.measure}.alt`)).toHaveAttribute( + expect( + screen.getByAltText(`overview.badges.${BadgeType.measure}.alt.metric.alert_status.name`), + ).toHaveAttribute( 'src', expect.stringContaining( `host/api/project_badges/measure?branch=branch-6.7&project=my-project&metric=${MetricKey.alert_status}&token=foo`, @@ -75,7 +77,9 @@ it('should renew token', async () => { ), ); - expect(screen.getByAltText(`overview.badges.${BadgeType.measure}.alt`)).toHaveAttribute( + expect( + screen.getByAltText(`overview.badges.${BadgeType.measure}.alt.metric.alert_status.name`), + ).toHaveAttribute( 'src', expect.stringContaining( `host/api/project_badges/measure?branch=branch-6.7&project=my-project&metric=${MetricKey.alert_status}&token=bar`, diff --git a/server/sonar-web/src/main/js/design-system/components/CodeSnippet.tsx b/server/sonar-web/src/main/js/design-system/components/CodeSnippet.tsx index 931031b855a..7adce4394c4 100644 --- a/server/sonar-web/src/main/js/design-system/components/CodeSnippet.tsx +++ b/server/sonar-web/src/main/js/design-system/components/CodeSnippet.tsx @@ -29,6 +29,7 @@ import { ClipboardButton } from './clipboard'; interface Props { className?: string; + copyAriaLabel?: string; isOneLine?: boolean; join?: string; language?: string; @@ -43,16 +44,26 @@ interface Props { const s = ' \\' + '\n '; export function CodeSnippet(props: Readonly<Props>) { - const { className, isOneLine, join = s, language, noCopy, render, snippet, wrap = false } = props; + const { + copyAriaLabel, + className, + isOneLine, + join = s, + language, + noCopy, + render, + snippet, + wrap = false, + } = props; const snippetArray = Array.isArray(snippet) ? snippet.filter(isDefined) : [snippet]; const finalSnippet = isOneLine ? snippetArray.join(' ') : snippetArray.join(join); const isSimpleOneLine = isOneLine && noCopy; const copyButton = isOneLine ? ( - <StyledSingleLineClipboardButton copyValue={finalSnippet} /> + <StyledSingleLineClipboardButton copyValue={finalSnippet} ariaLabel={copyAriaLabel} /> ) : ( - <StyledClipboardButton copyValue={finalSnippet} /> + <StyledClipboardButton ariaLabel={copyAriaLabel} copyValue={finalSnippet} /> ); const renderSnippet = @@ -77,7 +88,10 @@ export function CodeSnippet(props: Readonly<Props>) { > {!noCopy && copyButton} <CodeSyntaxHighlighter - className={classNames('sw-overflow-auto', { 'sw-pr-24': !noCopy, 'sw-flex': !noCopy })} + className={classNames('sw-overflow-auto', { + 'sw-pr-24': !noCopy, + 'sw-flex': !noCopy, + })} htmlAsString={renderSnippet} language={language} wrap={wrap} diff --git a/server/sonar-web/src/main/js/design-system/components/IlllustredSelectionCard.tsx b/server/sonar-web/src/main/js/design-system/components/IlllustredSelectionCard.tsx index 4030d6e7639..b254f97c0b1 100644 --- a/server/sonar-web/src/main/js/design-system/components/IlllustredSelectionCard.tsx +++ b/server/sonar-web/src/main/js/design-system/components/IlllustredSelectionCard.tsx @@ -38,7 +38,11 @@ export function IllustratedSelectionCard(props: Props) { const { className, description, image, onClick, selected } = props; return ( - <StyledSelectionCard className={classNames(className, { selected })} onClick={onClick}> + <StyledSelectionCard + className={classNames(className, { selected })} + aria-pressed={selected} + onClick={onClick} + > <ImageContainer>{image}</ImageContainer> <DescriptionContainer> <Note>{description}</Note> @@ -73,11 +77,14 @@ export const StyledSelectionCard = styled(BareButton)` transition: border 0.3s ease; &:hover, - &:focus, &:active { border: ${themeBorder('default', 'primary')}; } + &:focus { + outline: ${themeBorder('focus', 'primary')}; + } + &.selected { border: ${themeBorder('default', 'primary')}; } diff --git a/server/sonar-web/src/main/js/design-system/components/clipboard.tsx b/server/sonar-web/src/main/js/design-system/components/clipboard.tsx index bb7f6bbd0f2..45a6b778cd3 100644 --- a/server/sonar-web/src/main/js/design-system/components/clipboard.tsx +++ b/server/sonar-web/src/main/js/design-system/components/clipboard.tsx @@ -34,6 +34,7 @@ import React, { ComponentProps, useCallback, useState } from 'react'; const COPY_SUCCESS_NOTIFICATION_LIFESPAN = 1000; interface ButtonProps { + ariaLabel?: string; children?: React.ReactNode; className?: string; copiedLabel?: string; @@ -48,6 +49,7 @@ export function ClipboardButton(props: ButtonProps) { className, children, copyValue, + ariaLabel, copiedLabel = 'Copied', copyLabel = 'Copy', } = props; @@ -58,6 +60,7 @@ export function ClipboardButton(props: ButtonProps) { {/* TODO ^ Remove TooltipProvider after design-system is reintegrated into sonar-web */} <Tooltip content={copiedLabel} isOpen={copySuccess}> <Button + aria-label={ariaLabel} className={classNames('sw-select-none', className)} onClick={handleCopy} prefix={icon} |