aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main/js
diff options
context:
space:
mode:
authorstanislavh <stanislav.honcharov@sonarsource.com>2024-11-26 15:06:59 +0100
committersonartech <sonartech@sonarsource.com>2024-11-28 20:02:58 +0000
commit94747daf9743ed21903f14b045fb01f2798d4507 (patch)
tree0d623ed21baccb996441ee8a192b403d15313dd6 /server/sonar-web/src/main/js
parentfaa2dd6c0e597a9250b82898870119aafdbe8fc9 (diff)
downloadsonarqube-94747daf9743ed21903f14b045fb01f2798d4507.tar.gz
sonarqube-94747daf9743ed21903f14b045fb01f2798d4507.zip
SONAR-22310 Fix a11y issue on project information page
Diffstat (limited to 'server/sonar-web/src/main/js')
-rw-r--r--server/sonar-web/src/main/js/apps/projectInformation/ProjectInformationApp.tsx13
-rw-r--r--server/sonar-web/src/main/js/apps/projectInformation/__tests__/ProjectInformationApp-it.tsx29
-rw-r--r--server/sonar-web/src/main/js/apps/projectInformation/about/AboutProject.tsx27
-rw-r--r--server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaDescription.tsx11
-rw-r--r--server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaKey.tsx50
-rw-r--r--server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaLinks.tsx6
-rw-r--r--server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaQualityGate.tsx34
-rw-r--r--server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaQualityProfiles.tsx82
-rw-r--r--server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaSize.tsx29
-rw-r--r--server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaTags.tsx8
-rw-r--r--server/sonar-web/src/main/js/apps/projectInformation/about/components/MetaVisibility.tsx6
-rw-r--r--server/sonar-web/src/main/js/apps/projectInformation/about/components/__tests__/MetaQualityProfiles-test.tsx8
-rw-r--r--server/sonar-web/src/main/js/apps/projectInformation/badges/ProjectBadges.tsx12
-rw-r--r--server/sonar-web/src/main/js/apps/projectInformation/badges/__tests__/ProjectBadges-it.tsx8
-rw-r--r--server/sonar-web/src/main/js/design-system/components/CodeSnippet.tsx22
-rw-r--r--server/sonar-web/src/main/js/design-system/components/IlllustredSelectionCard.tsx11
-rw-r--r--server/sonar-web/src/main/js/design-system/components/clipboard.tsx3
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}