]> source.dussan.org Git - sonarqube.git/commitdiff
[NO-JIRA] Improve a11y in facet lists
authorvikvorona <viktor.vorona@sonarsource.com>
Thu, 27 Apr 2023 07:48:28 +0000 (09:48 +0200)
committersonartech <sonartech@sonarsource.com>
Thu, 27 Apr 2023 20:03:07 +0000 (20:03 +0000)
27 files changed:
server/sonar-web/src/main/js/api/mocks/CodingRulesMock.ts
server/sonar-web/src/main/js/api/mocks/IssuesServiceMock.ts
server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts
server/sonar-web/src/main/js/apps/coding-rules/components/AvailableSinceFacet.tsx
server/sonar-web/src/main/js/apps/coding-rules/components/CodingRulesApp.tsx
server/sonar-web/src/main/js/apps/coding-rules/components/Facet.tsx
server/sonar-web/src/main/js/apps/coding-rules/components/ProfileFacet.tsx
server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/CodingRulesApp-test.tsx.snap
server/sonar-web/src/main/js/apps/component-measures/sidebar/DomainFacet.tsx
server/sonar-web/src/main/js/apps/component-measures/sidebar/ProjectOverviewFacet.tsx
server/sonar-web/src/main/js/apps/component-measures/sidebar/__tests__/__snapshots__/DomainFacet-test.tsx.snap
server/sonar-web/src/main/js/apps/issues/__tests__/IssuesApp-it.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/CharacteristicFacet.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/CreationDateFacet.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/PeriodFilter.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/ResolutionFacet.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/ScopeFacet.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/SeverityFacet.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/StandardFacet.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/StatusFacet.tsx
server/sonar-web/src/main/js/apps/issues/sidebar/TypeFacet.tsx
server/sonar-web/src/main/js/apps/issues/test-utils.tsx
server/sonar-web/src/main/js/components/facet/FacetHeader.tsx
server/sonar-web/src/main/js/components/facet/FacetItemsList.tsx
server/sonar-web/src/main/js/components/facet/ListStyleFacet.tsx
server/sonar-web/src/main/js/components/facet/__tests__/Facet-it.tsx
server/sonar-web/src/main/js/components/facet/__tests__/__snapshots__/ListStyleFacet-test.tsx.snap

index f26cbbe35251883dfd933343ffb3fff821c842d8..689828eba6191d54c7682d7c3d4bbfe2fd89b5c6 100644 (file)
@@ -32,18 +32,19 @@ import { Rule, RuleActivation, RuleDetails, RulesUpdateRequest } from '../../typ
 import { NoticeType } from '../../types/users';
 import { getFacet } from '../issues';
 import {
-  bulkActivateRules,
-  bulkDeactivateRules,
   Profile,
-  searchQualityProfiles,
   SearchQualityProfilesParameters,
   SearchQualityProfilesResponse,
+  bulkActivateRules,
+  bulkDeactivateRules,
+  searchQualityProfiles,
 } from '../quality-profiles';
 import { getRuleDetails, getRulesApp, searchRules, updateRule } from '../rules';
 import { dismissNotice, getCurrentUser } from '../users';
 
 interface FacetFilter {
   languages?: string;
+  available_since?: string;
 }
 
 const FACET_RULE_MAP: { [key: string]: keyof Rule } = {
@@ -158,6 +159,7 @@ export default class CodingRulesMock {
         ],
       }),
       mockRuleDetails({
+        createdAt: '2022-12-16T17:26:54+0100',
         key: 'rule8',
         type: 'VULNERABILITY',
         lang: 'py',
@@ -188,8 +190,8 @@ export default class CodingRulesMock {
     this.rules = cloneDeep(this.defaultRules);
   }
 
-  getRuleWithoutDetails() {
-    return this.rules.map((r) =>
+  getRulesWithoutDetails(rules: RuleDetails[]) {
+    return rules.map((r) =>
       pick(r, [
         'isTemplate',
         'key',
@@ -206,12 +208,17 @@ export default class CodingRulesMock {
     );
   }
 
-  filterFacet({ languages }: FacetFilter) {
-    let filteredRules = this.getRuleWithoutDetails();
+  filterFacet({ languages, available_since }: FacetFilter) {
+    let filteredRules = this.rules;
     if (languages) {
       filteredRules = filteredRules.filter((r) => r.lang && languages.includes(r.lang));
     }
-    return filteredRules;
+    if (available_since) {
+      filteredRules = filteredRules.filter(
+        (r) => r.createdAt && new Date(r.createdAt) > new Date(available_since)
+      );
+    }
+    return this.getRulesWithoutDetails(filteredRules);
   }
 
   setIsAdmin() {
@@ -314,7 +321,14 @@ export default class CodingRulesMock {
     return this.reply(rule);
   };
 
-  handleSearchRules = ({ facets, languages, p, ps, rule_key }: SearchRulesQuery) => {
+  handleSearchRules = ({
+    facets,
+    languages,
+    p,
+    ps,
+    available_since,
+    rule_key,
+  }: SearchRulesQuery) => {
     const countFacet = (facets || '').split(',').map((facet: keyof Rule) => {
       const facetCount = countBy(
         this.rules.map((r) => r[FACET_RULE_MAP[facet] || facet] as string)
@@ -328,9 +342,9 @@ export default class CodingRulesMock {
     const currentP = p || 1;
     let filteredRules: Rule[] = [];
     if (rule_key) {
-      filteredRules = this.getRuleWithoutDetails().filter((r) => r.key === rule_key);
+      filteredRules = this.getRulesWithoutDetails(this.rules).filter((r) => r.key === rule_key);
     } else {
-      filteredRules = this.filterFacet({ languages });
+      filteredRules = this.filterFacet({ languages, available_since });
     }
     const responseRules = filteredRules.slice((currentP - 1) * currentPs, currentP * currentPs);
     return this.reply({
index 3304e1afd01862cda92d2bc93f1f228bf17233cc..f2fc1dbfc74fea2b6de3ec9ba925ea72e12e0b16 100644 (file)
@@ -181,6 +181,7 @@ export default class IssuesServiceMock {
         issue: mockRawIssue(false, {
           key: 'issue11',
           component: 'foo:test1.js',
+          creationDate: '2022-01-01T09:36:01+0100',
           message: 'FlowIssue',
           characteristic: IssueCharacteristic.Clear,
           type: IssueType.CodeSmell,
@@ -760,6 +761,10 @@ export default class IssuesServiceMock {
       .filter((item) => !query.rules || query.rules.split(',').includes(item.issue.rule))
       .filter(
         (item) => !query.resolutions || query.resolutions.split(',').includes(item.issue.resolution)
+      )
+      .filter(
+        (item) =>
+          !query.inNewCodePeriod || new Date(item.issue.creationDate) > new Date('2023-01-10')
       );
 
     // Splice list items according to paging using a fixed page size
index 9750dfc676269c397d9f240136c1860141665422..487606f756035207ea4997ca5b412aac7d20f8aa 100644 (file)
@@ -19,7 +19,7 @@
  */
 import { fireEvent, screen, waitFor, within } from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
-import { byRole } from 'testing-library-selector';
+import { byPlaceholderText, byRole } from 'testing-library-selector';
 import CodingRulesMock from '../../../api/mocks/CodingRulesMock';
 import { mockCurrentUser, mockLoggedInUser } from '../../../helpers/testMocks';
 import { renderAppRoutes } from '../../../helpers/testReactTestingUtils';
@@ -32,8 +32,11 @@ jest.mock('../../../api/users');
 jest.mock('../../../api/quality-profiles');
 
 const ui = {
+  rulesList: byRole('list', { name: 'list_of_rules' }),
   activateInSelectOption: byRole('combobox', { name: 'coding_rules.activate_in' }),
   deactivateInSelectOption: byRole('combobox', { name: 'coding_rules.deactivate_in' }),
+  availableSinceFacet: byRole('button', { name: 'coding_rules.facet.available_since' }),
+  availableSinceDateField: byPlaceholderText('date'),
 };
 
 let handler: CodingRulesMock;
@@ -506,6 +509,24 @@ it('should not show notification for anonymous users', async () => {
   ).not.toBeInTheDocument();
 });
 
+it('should filter correctly', async () => {
+  const user = userEvent.setup();
+  renderCodingRulesApp(mockCurrentUser());
+
+  expect(await within(await ui.rulesList.find()).findAllByRole('listitem')).toHaveLength(8);
+  await user.click(await ui.availableSinceFacet.find());
+  await user.click(await ui.availableSinceDateField.find());
+  await userEvent.selectOptions(
+    await screen.findByRole('combobox', { name: 'Month:' }),
+    'November'
+  );
+  await userEvent.selectOptions(screen.getByRole('combobox', { name: 'Year:' }), '2022');
+  await user.click(screen.getByRole('gridcell', { name: '1' }));
+  expect(ui.availableSinceDateField.get()).toHaveDisplayValue('Nov 1, 2022');
+  // eslint-disable-next-line jest-dom/prefer-in-document
+  expect(within(ui.rulesList.get()).getAllByRole('listitem')).toHaveLength(1);
+});
+
 function renderCodingRulesApp(currentUser?: CurrentUser, navigateTo?: string) {
   renderAppRoutes('coding_rules', routes, {
     navigateTo,
index 410764f83cd7962060f7fe849dea788752143a68..110322782bde218d530b8504da04c0dc34cca850 100644 (file)
@@ -34,8 +34,10 @@ interface Props {
 }
 
 class AvailableSinceFacet extends React.PureComponent<Props & WrappedComponentProps> {
+  property: keyof Query = 'availableSince';
+
   handleHeaderClick = () => {
-    this.props.onToggle('availableSince');
+    this.props.onToggle(this.property);
   };
 
   handleClear = () => {
@@ -53,10 +55,12 @@ class AvailableSinceFacet extends React.PureComponent<Props & WrappedComponentPr
 
   render() {
     const { open, value } = this.props;
+    const headerId = `facet_${this.property}`;
 
     return (
-      <FacetBox property="availableSince">
+      <FacetBox property={this.property}>
         <FacetHeader
+          id={headerId}
           name={translate('coding_rules.facet.available_since')}
           onClear={this.handleClear}
           onClick={this.handleHeaderClick}
index 9ee6237e2b00b9783fe7ba31747c1bbe3ccd5aa4..94202b9a1ef9a4e969e7662bc2b19ed284e1b9cd 100644 (file)
@@ -45,25 +45,25 @@ import { SecurityStandard } from '../../../types/security';
 import { Dict, Paging, RawQuery, Rule, RuleActivation } from '../../../types/types';
 import { CurrentUser, isLoggedIn } from '../../../types/users';
 import {
+  STANDARDS,
   shouldOpenSonarSourceSecurityFacet,
   shouldOpenStandardsChildFacet,
   shouldOpenStandardsFacet,
-  STANDARDS,
 } from '../../issues/utils';
 import {
   Activation,
   Actives,
-  areQueriesEqual,
   FacetKey,
   Facets,
+  OpenFacets,
+  Query,
+  areQueriesEqual,
   getAppFacet,
   getOpen,
   getSelected,
   getServerFacet,
   hasRuleKey,
-  OpenFacets,
   parseQuery,
-  Query,
   serializeQuery,
   shouldRequestFacet,
 } from '../query';
@@ -662,8 +662,7 @@ export class CodingRulesApp extends React.PureComponent<Props, State> {
                 />
               ) : (
                 <>
-                  <h2 className="a11y-hidden">{translate('list_of_rules')}</h2>
-                  <ul>
+                  <ul aria-label={translate('list_of_rules')}>
                     {rules.map((rule) => (
                       <RuleListItem
                         activation={this.getRuleActivation(rule.key)}
index 6b8fd8e1956da969fc98776239eb12518ff8562c..6045fb3de9c99a17666097b24fcff454ce6ad1ef 100644 (file)
@@ -111,6 +111,7 @@ export default class Facet extends React.PureComponent<Props> {
           (key) => -stats[key],
           (key) => renderTextName(key).toLowerCase()
         ));
+    const headerId = `facet_${property}`;
 
     return (
       <FacetBox
@@ -118,6 +119,7 @@ export default class Facet extends React.PureComponent<Props> {
         property={property}
       >
         <FacetHeader
+          id={headerId}
           name={translate('coding_rules.facet', property)}
           disabled={disabled}
           disabledHelper={disabledHelper}
@@ -130,7 +132,7 @@ export default class Facet extends React.PureComponent<Props> {
         </FacetHeader>
 
         {open && items !== undefined && (
-          <FacetItemsList label={property}>{items.map(this.renderItem)}</FacetItemsList>
+          <FacetItemsList labelledby={headerId}>{items.map(this.renderItem)}</FacetItemsList>
         )}
 
         {open && this.props.renderFooter !== undefined && this.props.renderFooter()}
index 9fbac37e959104ec437cf9acd6beecfb54cf4834..8c9c32473623a4ff68db68321847f29a7df25e7b 100644 (file)
@@ -165,10 +165,12 @@ export default class ProfileFacet extends React.PureComponent<Props> {
     );
 
     const property = 'profile';
+    const headerId = `facet_${property}`;
 
     return (
       <FacetBox property={property}>
         <FacetHeader
+          id={headerId}
           name={translate('coding_rules.facet.qprofile')}
           onClear={this.handleClear}
           onClick={this.handleHeaderClick}
@@ -187,7 +189,9 @@ export default class ProfileFacet extends React.PureComponent<Props> {
           />
         </FacetHeader>
 
-        {open && <FacetItemsList label={property}>{profiles.map(this.renderItem)}</FacetItemsList>}
+        {open && (
+          <FacetItemsList labelledby={headerId}>{profiles.map(this.renderItem)}</FacetItemsList>
+        )}
       </FacetBox>
     );
   }
index 78e53adfec650b482f58e47da78647a861f799ab..9a9f3631fce4de375d936a0a65df39bdd1e060cf 100644 (file)
@@ -263,12 +263,9 @@ exports[`should render correctly: loaded 1`] = `
       <div
         className="layout-page-main-inner"
       >
-        <h2
-          className="a11y-hidden"
+        <ul
+          aria-label="list_of_rules"
         >
-          list_of_rules
-        </h2>
-        <ul>
           <RuleListItem
             isLoggedIn={true}
             key="javascript:S1067"
@@ -388,12 +385,9 @@ exports[`should render correctly: loading 1`] = `
       <div
         className="layout-page-main-inner"
       >
-        <h2
-          className="a11y-hidden"
-        >
-          list_of_rules
-        </h2>
-        <ul />
+        <ul
+          aria-label="list_of_rules"
+        />
       </div>
     </main>
   </div>
index d38472c3173ea8fdbff470cffba4d2c07995dc3e..638040d7232cac4f62964c49f0617e67b70e1d09 100644 (file)
@@ -149,10 +149,12 @@ export default class DomainFacet extends React.PureComponent<Props> {
     const { domain, open } = this.props;
     const helperMessageKey = `component_measures.domain_facets.${domain.name}.help`;
     const helper = hasMessage(helperMessageKey) ? translate(helperMessageKey) : undefined;
+    const headerId = `facet_${domain.name}`;
     return (
       <FacetBox property={domain.name}>
         <FacetHeader
           helper={helper}
+          id={headerId}
           name={getLocalizedMetricDomain(domain.name)}
           onClick={this.handleHeaderClick}
           open={open}
@@ -160,7 +162,7 @@ export default class DomainFacet extends React.PureComponent<Props> {
         />
 
         {open && (
-          <FacetItemsList label={domain.name}>
+          <FacetItemsList labelledby={headerId}>
             {this.renderOverviewFacet()}
             {this.renderItemsFacet()}
           </FacetItemsList>
index 741f09af76dc4dbc63d13048018df8309ae7c20e..bb33f4419ea88565de388517085815417f5fb1fb 100644 (file)
@@ -33,7 +33,7 @@ export default function ProjectOverviewFacet({ value, selected, onChange }: Prop
   const facetName = translate('component_measures.overview', value, 'facet');
   return (
     <FacetBox property={value}>
-      <FacetItemsList label={value}>
+      <FacetItemsList label={facetName}>
         <FacetItem
           active={value === selected}
           key={value}
index 604e785ec6816d26896402525a528ed45e76f823..5f59d3f3a948bb63c7fcdba0d42a49e4641d26fc 100644 (file)
@@ -6,13 +6,14 @@ exports[`should display facet item list 1`] = `
 >
   <FacetHeader
     helper="component_measures.domain_facets.Reliability.help"
+    id="facet_Reliability"
     name="Reliability"
     onClick={[Function]}
     open={true}
     values={[]}
   />
   <FacetItemsList
-    label="Reliability"
+    labelledby="facet_Reliability"
   >
     <FacetItem
       active={false}
@@ -142,6 +143,7 @@ exports[`should display facet item list with bugs selected 1`] = `
 >
   <FacetHeader
     helper="component_measures.domain_facets.Reliability.help"
+    id="facet_Reliability"
     name="Reliability"
     onClick={[Function]}
     open={true}
@@ -152,7 +154,7 @@ exports[`should display facet item list with bugs selected 1`] = `
     }
   />
   <FacetItemsList
-    label="Reliability"
+    labelledby="facet_Reliability"
   >
     <FacetItem
       active={false}
@@ -282,13 +284,14 @@ exports[`should not display subtitles of new measures if there is none 1`] = `
 >
   <FacetHeader
     helper="component_measures.domain_facets.Reliability.help"
+    id="facet_Reliability"
     name="Reliability"
     onClick={[Function]}
     open={true}
     values={[]}
   />
   <FacetItemsList
-    label="Reliability"
+    labelledby="facet_Reliability"
   >
     <FacetItem
       active={false}
@@ -365,13 +368,14 @@ exports[`should not display subtitles of new measures if there is none, even on
 >
   <FacetHeader
     helper="component_measures.domain_facets.Reliability.help"
+    id="facet_Reliability"
     name="Reliability"
     onClick={[Function]}
     open={true}
     values={[]}
   />
   <FacetItemsList
-    label="Reliability"
+    labelledby="facet_Reliability"
   >
     <FacetItem
       active={false}
index 78b6be118e14a0814994ac79abe8ef3622e362df..99da2310af8f348077df1a7ba1ba76420c445274 100644 (file)
@@ -545,6 +545,16 @@ describe('issues app', () => {
       ).toHaveTextContent('ts674');
     });
   });
+
+  it('should show the new code issues only', async () => {
+    const user = userEvent.setup();
+
+    renderProjectIssuesApp('project/issues?id=myproject');
+
+    expect(await ui.issueItems.findAll()).toHaveLength(7);
+    await user.click(await ui.inNewCodeFilter.find());
+    expect(await ui.issueItems.findAll()).toHaveLength(6);
+  });
 });
 
 describe('issues item', () => {
index eb0b153027ea6dd5744cfca673e323cabc69ab46..94efb0f82cdd4d0c460081eb2fb82509021fafc6 100644 (file)
@@ -141,10 +141,13 @@ export default class CharacteristicFacet extends React.PureComponent<Props> {
       .filter(([, value]) => value === fitFor)
       .map(([key]) => key as IssueCharacteristic);
 
+    const headerId = `facet_${this.property}_${fitFor}`;
+
     return (
       <FacetBox property={this.property}>
         <FacetHeader
           fetching={this.props.fetching}
+          id={headerId}
           name={translate('issues.facet.characteristics', fitFor)}
           onClear={this.handleClear}
           onClick={this.handleHeaderClick}
@@ -154,7 +157,7 @@ export default class CharacteristicFacet extends React.PureComponent<Props> {
 
         {this.props.open && (
           <>
-            <FacetItemsList label={this.property}>
+            <FacetItemsList labelledby={headerId}>
               {availableCharacteristics.map(this.renderItem)}
             </FacetItemsList>
             <MultipleSelectionHint
index fd37e3fb43ceb369c3c593a9be1eba190862d967..1a80490f5d1261aea01875269e060afc57c1a35a 100644 (file)
@@ -290,6 +290,7 @@ export class CreationDateFacet extends React.PureComponent<Props & WrappedCompon
   render() {
     const { forceShow, open, fetching } = this.props;
     const values = this.getValues();
+    const headerId = `facet_${this.property}`;
 
     if (values.length < 1 && !forceShow) {
       return null;
@@ -299,6 +300,7 @@ export class CreationDateFacet extends React.PureComponent<Props & WrappedCompon
       <FacetBox property={this.property}>
         <FacetHeader
           fetching={fetching}
+          id={headerId}
           name={translate('issues.facet', this.property)}
           onClear={this.handleClear}
           onClick={this.handleHeaderClick}
index 7bc7718e116c857afbe8ad10cbf3f40a3f90891c..c8f0745e2646327995f6e633a7e0ecbe24b30838 100644 (file)
@@ -55,7 +55,7 @@ export default function PeriodFilter(props: PeriodFilterProps) {
 
   return (
     <FacetBox property={PROPERTY}>
-      <FacetItemsList label={PROPERTY}>
+      <FacetItemsList label={translate('issues.facet', PROPERTY)}>
         <FacetItem
           active={newCodeSelected}
           loading={fetching}
index c437f10efc4c017d13b7a1ffb60f21e398a3cd38..54bc9f3823825db8d1cab9eb226ba17abde456dc 100644 (file)
@@ -118,6 +118,7 @@ export default class ResolutionFacet extends React.PureComponent<Props> {
   render() {
     const { resolutions, stats = {}, forceShow, fetching, open } = this.props;
     const values = resolutions.map((resolution) => this.getFacetItemName(resolution));
+    const headerId = `facet_${this.property}`;
 
     if (values.length < 1 && !forceShow) {
       return null;
@@ -127,6 +128,7 @@ export default class ResolutionFacet extends React.PureComponent<Props> {
       <FacetBox property={this.property}>
         <FacetHeader
           fetching={fetching}
+          id={headerId}
           name={translate('issues.facet', this.property)}
           onClear={this.handleClear}
           onClick={this.handleHeaderClick}
@@ -136,7 +138,7 @@ export default class ResolutionFacet extends React.PureComponent<Props> {
 
         {open && (
           <>
-            <FacetItemsList label={this.property}>
+            <FacetItemsList labelledby={headerId}>
               {RESOLUTIONS.map(this.renderItem)}
             </FacetItemsList>
             <MultipleSelectionHint
index 73320c777627a279d2f1cd217e8ae09ea48bd747..b474458e71a8313eb6b1ac537b6506d391ee469c 100644 (file)
@@ -45,6 +45,7 @@ export default function ScopeFacet(props: ScopeFacetProps) {
   const values = scopes.map((scope) => translate('issue.scope', scope));
 
   const property = 'scopes';
+  const headerId = `facet_${property}`;
   if (values.length < 1 && !forceShow) {
     return null;
   }
@@ -53,6 +54,7 @@ export default function ScopeFacet(props: ScopeFacetProps) {
     <FacetBox property={property}>
       <FacetHeader
         fetching={fetching}
+        id={headerId}
         name={translate('issues.facet.scopes')}
         onClear={() => props.onChange({ scopes: [] })}
         onClick={() => props.onToggle('scopes')}
@@ -62,7 +64,7 @@ export default function ScopeFacet(props: ScopeFacetProps) {
 
       {open && (
         <>
-          <FacetItemsList label={property}>
+          <FacetItemsList labelledby={headerId}>
             {SOURCE_SCOPES.map(({ scope, qualifier }) => {
               const active = scopes.includes(scope);
               const stat = stats[scope];
index f587a50b359e37fe18fbe9dd3c0484f90e29f0ff..6fe70022f66b30b0d117b8c94af23b4b4ea0d750 100644 (file)
@@ -95,11 +95,13 @@ export default class SeverityFacet extends React.PureComponent<Props> {
   render() {
     const { fetching, open, severities, stats = {} } = this.props;
     const values = severities.map((severity) => translate('severity', severity));
+    const headerId = `facet_${this.property}`;
 
     return (
       <FacetBox property={this.property}>
         <FacetHeader
           fetching={fetching}
+          id={headerId}
           name={translate('issues.facet', this.property)}
           onClear={this.handleClear}
           onClick={this.handleHeaderClick}
@@ -109,7 +111,7 @@ export default class SeverityFacet extends React.PureComponent<Props> {
 
         {open && (
           <>
-            <FacetItemsList label={this.property}>{SEVERITIES.map(this.renderItem)}</FacetItemsList>
+            <FacetItemsList labelledby={headerId}>{SEVERITIES.map(this.renderItem)}</FacetItemsList>
             <MultipleSelectionHint options={Object.keys(stats).length} values={severities.length} />
           </>
         )}
index c5b9c80cdf08d05760eff442d130e84587b35d0b..68bcefe5b7aa33a2cb71c72c358c5e5ad735dfe7 100644 (file)
@@ -157,6 +157,10 @@ export default class StandardFacet extends React.PureComponent<Props, State> {
     ];
   };
 
+  getFacetHeaderId = (property: string) => {
+    return `facet_${property}`;
+  };
+
   handleHeaderClick = () => {
     this.props.onToggle(this.property);
   };
@@ -265,7 +269,7 @@ export default class StandardFacet extends React.PureComponent<Props, State> {
     stats: any,
     values: string[],
     categories: string[],
-    listLabel: ValuesProp,
+    listKey: ValuesProp,
     renderName: (standards: Standards, category: string) => React.ReactNode,
     renderTooltip: (standards: Standards, category: string) => string,
     onClick: (x: string, multiple?: boolean) => void
@@ -283,7 +287,7 @@ export default class StandardFacet extends React.PureComponent<Props, State> {
     };
 
     return (
-      <FacetItemsList label={listLabel}>
+      <FacetItemsList labelledby={this.getFacetHeaderId(listKey)}>
         {categories.map((category) => (
           <FacetItem
             active={values.includes(category)}
@@ -349,7 +353,7 @@ export default class StandardFacet extends React.PureComponent<Props, State> {
     const allItemShown = limitedList.length + selectedBelowLimit.length === sortedItems.length;
     return (
       <>
-        <FacetItemsList label={SecurityStandard.SONARSOURCE}>
+        <FacetItemsList labelledby={this.getFacetHeaderId(SecurityStandard.SONARSOURCE)}>
           {limitedList.map((item) => (
             <FacetItem
               active={values.includes(item)}
@@ -365,7 +369,7 @@ export default class StandardFacet extends React.PureComponent<Props, State> {
         {selectedBelowLimit.length > 0 && (
           <>
             {!allItemShown && <div className="note spacer-bottom text-center">⋯</div>}
-            <FacetItemsList label={SecurityStandard.SONARSOURCE}>
+            <FacetItemsList labelledby={this.getFacetHeaderId(SecurityStandard.SONARSOURCE)}>
               {selectedBelowLimit.map((item) => (
                 <FacetItem
                   active={true}
@@ -428,6 +432,7 @@ export default class StandardFacet extends React.PureComponent<Props, State> {
         <FacetBox className="is-inner" property={SecurityStandard.SONARSOURCE}>
           <FacetHeader
             fetching={fetchingSonarSourceSecurity}
+            id={this.getFacetHeaderId(SecurityStandard.SONARSOURCE)}
             name={translate('issues.facet.sonarsourceSecurity')}
             onClick={this.handleSonarSourceSecurityHeaderClick}
             open={sonarsourceSecurityOpen}
@@ -445,6 +450,7 @@ export default class StandardFacet extends React.PureComponent<Props, State> {
         <FacetBox className="is-inner" property={SecurityStandard.OWASP_TOP10_2021}>
           <FacetHeader
             fetching={fetchingOwaspTop102021}
+            id={this.getFacetHeaderId(SecurityStandard.OWASP_TOP10_2021)}
             name={translate('issues.facet.owaspTop10_2021')}
             onClick={this.handleOwaspTop102021HeaderClick}
             open={owaspTop102021Open}
@@ -460,6 +466,7 @@ export default class StandardFacet extends React.PureComponent<Props, State> {
         <FacetBox className="is-inner" property={SecurityStandard.OWASP_TOP10}>
           <FacetHeader
             fetching={fetchingOwaspTop10}
+            id={this.getFacetHeaderId(SecurityStandard.OWASP_TOP10)}
             name={translate('issues.facet.owaspTop10')}
             onClick={this.handleOwaspTop10HeaderClick}
             open={owaspTop10Open}
@@ -508,6 +515,7 @@ export default class StandardFacet extends React.PureComponent<Props, State> {
     return (
       <FacetBox property={this.property}>
         <FacetHeader
+          id={this.getFacetHeaderId(this.property)}
           name={translate('issues.facet', this.property)}
           onClear={this.handleClear}
           onClick={this.handleHeaderClick}
index c2130d2a157d70f2b5b755e855f0872e15f99798..5adf79a35625683c569c7b63bd7124108e5ae648 100644 (file)
@@ -95,6 +95,7 @@ export default class StatusFacet extends React.PureComponent<Props> {
   render() {
     const { statuses, stats = {}, forceShow, fetching, open } = this.props;
     const values = statuses.map((status) => translate('issue.status', status));
+    const headerId = `facet_${this.property}`;
 
     if (values.length < 1 && !forceShow) {
       return null;
@@ -104,6 +105,7 @@ export default class StatusFacet extends React.PureComponent<Props> {
       <FacetBox property={this.property}>
         <FacetHeader
           fetching={fetching}
+          id={headerId}
           name={translate('issues.facet', this.property)}
           onClear={this.handleClear}
           onClick={this.handleHeaderClick}
@@ -113,7 +115,7 @@ export default class StatusFacet extends React.PureComponent<Props> {
 
         {open && (
           <>
-            <FacetItemsList label={this.property}>{STATUSES.map(this.renderItem)}</FacetItemsList>
+            <FacetItemsList labelledby={headerId}>{STATUSES.map(this.renderItem)}</FacetItemsList>
             <MultipleSelectionHint options={Object.keys(stats).length} values={statuses.length} />
           </>
         )}
index 51ea8f58f4bc0d4b6d939e60c98830558dac4d10..ee224035d0077c42e39b642635f867ea0b530acd 100644 (file)
@@ -102,6 +102,7 @@ export default class TypeFacet extends React.PureComponent<Props> {
   render() {
     const { types, stats = {}, forceShow, open, fetching } = this.props;
     const values = types.map((type) => translate('issue.type', type));
+    const typeFacetHeaderId = `facet_${this.property}`;
 
     if (values.length < 1 && !forceShow) {
       return null;
@@ -111,6 +112,7 @@ export default class TypeFacet extends React.PureComponent<Props> {
       <FacetBox property={this.property}>
         <FacetHeader
           fetching={fetching}
+          id={typeFacetHeaderId}
           name={translate('issues.facet', this.property)}
           onClear={this.handleClear}
           onClick={this.handleHeaderClick}
@@ -120,7 +122,7 @@ export default class TypeFacet extends React.PureComponent<Props> {
 
         {open && (
           <>
-            <FacetItemsList label={this.property}>
+            <FacetItemsList labelledby={typeFacetHeaderId}>
               {ISSUE_TYPES.filter((t) => t !== 'SECURITY_HOTSPOT').map(this.renderItem)}
             </FacetItemsList>
             <MultipleSelectionHint options={Object.keys(stats).length} values={types.length} />
index d52b7ac367cffd7053c860e56f986370ee29987f..8a6ff37de2114c3c40f474311415a1e60a48fe1b 100644 (file)
@@ -92,9 +92,10 @@ export const ui = {
   showFiltersButton: (showMore = true) =>
     byRole('button', { name: `issues.show_${showMore ? 'more' : 'less'}_filters` }),
 
-  ruleFacetList: byRole('list', { name: 'rules' }),
-  languageFacetList: byRole('list', { name: 'languages' }),
+  ruleFacetList: byRole('list', { name: 'issues.facet.rules' }),
+  languageFacetList: byRole('list', { name: 'issues.facet.languages' }),
   ruleFacetSearch: byRole('searchbox', { name: 'search.search_for_rules' }),
+  inNewCodeFilter: byRole('checkbox', { name: 'issues.new_code' }),
 };
 
 export async function waitOnDataLoaded() {
index 512b3e59f6ff643def4d2d831125ec8f04106988..9f319649f35c6c8a170f472bfb6cb19f98573cfa 100644 (file)
@@ -19,8 +19,8 @@
  */
 import classNames from 'classnames';
 import * as React from 'react';
-import { Button, ButtonLink } from '../../components/controls/buttons';
 import HelpTooltip from '../../components/controls/HelpTooltip';
+import { Button, ButtonLink } from '../../components/controls/buttons';
 import OpenCloseIcon from '../../components/icons/OpenCloseIcon';
 import DeferredSpinner from '../../components/ui/DeferredSpinner';
 import { translate, translateWithParameters } from '../../helpers/l10n';
@@ -33,6 +33,7 @@ interface Props {
   disabled?: boolean;
   disabledHelper?: string;
   name: string;
+  id: string;
   onClear?: () => void;
   onClick?: () => void;
   open: boolean;
@@ -70,7 +71,7 @@ export default class FacetHeader extends React.PureComponent<Props> {
   }
 
   render() {
-    const { disabled, values, disabledHelper, name, open, children, fetching } = this.props;
+    const { disabled, values, disabledHelper, name, open, children, fetching, id } = this.props;
     const showClearButton = values != null && values.length > 0 && this.props.onClear != null;
     const header = disabled ? (
       <Tooltip overlay={disabledHelper}>
@@ -99,6 +100,7 @@ export default class FacetHeader extends React.PureComponent<Props> {
               onClick={this.handleClick}
               aria-expanded={open}
               tabIndex={0}
+              id={id}
             >
               <OpenCloseIcon className="little-spacer-right" open={open} />
               {header}
@@ -106,7 +108,7 @@ export default class FacetHeader extends React.PureComponent<Props> {
             {this.renderHelper()}
           </span>
         ) : (
-          <span className="search-navigator-facet-header display-flex-center">
+          <span className="search-navigator-facet-header display-flex-center" id={id}>
             {header}
             {this.renderHelper()}
           </span>
index e86348de14298d5945cae2f6a0d72133651b72ae..980b66e1580f26dc16150c5682842eede0576517 100644 (file)
  */
 import * as React from 'react';
 
-export interface FacetItemsListProps {
-  children?: React.ReactNode;
-  label: string;
-}
+export type FacetItemsListProps =
+  | {
+      children?: React.ReactNode;
+      labelledby: string;
+      label?: never;
+    }
+  | {
+      children?: React.ReactNode;
+      labelledby?: never;
+      label: string;
+    };
 
-export default function FacetItemsList({ children, label }: FacetItemsListProps) {
+export default function FacetItemsList({ children, labelledby, label }: FacetItemsListProps) {
+  const props = labelledby ? { 'aria-labelledby': labelledby } : { 'aria-label': label };
   return (
-    <div className="search-navigator-facet-list" role="list" aria-label={label}>
+    <div className="search-navigator-facet-list" role="list" {...props}>
       {children}
     </div>
   );
index b49859ee10f1d50ded891ccd610103faadfd5319..2953bdf44e622a2d8c477a64be37847eafd7fd1c 100644 (file)
@@ -231,6 +231,10 @@ export default class ListStyleFacet<S> extends React.Component<Props<S>, State<S
     return stats && stats[item] !== undefined ? stats && stats[item] : undefined;
   }
 
+  getFacetHeaderId = (property: string) => {
+    return `facet_${property}`;
+  };
+
   showFullList = () => {
     this.setState({ showFullList: true });
   };
@@ -275,7 +279,7 @@ export default class ListStyleFacet<S> extends React.Component<Props<S>, State<S
 
     return (
       <>
-        <FacetItemsList label={property}>
+        <FacetItemsList labelledby={this.getFacetHeaderId(property)}>
           {limitedList.map((item) => (
             <FacetItem
               active={this.props.values.includes(item)}
@@ -291,7 +295,7 @@ export default class ListStyleFacet<S> extends React.Component<Props<S>, State<S
         {selectedBelowLimit.length > 0 && (
           <>
             <div className="note spacer-bottom text-center">⋯</div>
-            <FacetItemsList label={property}>
+            <FacetItemsList labelledby={this.getFacetHeaderId(property)}>
               {selectedBelowLimit.map((item) => (
                 <FacetItem
                   active={true}
@@ -352,7 +356,7 @@ export default class ListStyleFacet<S> extends React.Component<Props<S>, State<S
 
     return (
       <>
-        <FacetItemsList label={property}>
+        <FacetItemsList labelledby={this.getFacetHeaderId(property)}>
           {searchResults.map((result) => this.renderSearchResult(result))}
         </FacetItemsList>
         {searchMaxResults && (
@@ -419,6 +423,7 @@ export default class ListStyleFacet<S> extends React.Component<Props<S>, State<S
           fetching={fetching}
           name={facetHeader}
           disabled={disabled}
+          id={this.getFacetHeaderId(property)}
           disabledHelper={disabledHelper}
           onClear={this.handleClear}
           onClick={disabled ? undefined : this.handleHeaderClick}
index dd13d2969b5966ee7ba52be85b5fa18144f4b310..8526a32551a52f236c9b7569aad52f4864a02550 100644 (file)
@@ -24,11 +24,11 @@ import { findTooltipWithContent, renderComponent } from '../../../helpers/testRe
 import FacetBox, { FacetBoxProps } from '../FacetBox';
 import FacetHeader from '../FacetHeader';
 import FacetItem from '../FacetItem';
-import FacetItemsList, { FacetItemsListProps } from '../FacetItemsList';
+import FacetItemsList from '../FacetItemsList';
 
 it('should render and function correctly', () => {
   const onFacetClick = jest.fn();
-  renderFacet(undefined, undefined, undefined, { onClick: onFacetClick });
+  renderFacet(undefined, undefined, { onClick: onFacetClick });
 
   // Start closed.
   let facetHeader = screen.getByRole('button', { name: 'foo', expanded: false });
@@ -78,7 +78,6 @@ it('should correctly render a disabled header', () => {
 function renderFacet(
   facetBoxProps: Partial<FacetBoxProps> = {},
   facetHeaderProps: Partial<FacetHeader['props']> = {},
-  facetItemListProps: Partial<FacetItemsListProps> = {},
   facetItemProps: Partial<FacetItem['props']> = {}
 ) {
   function Facet() {
@@ -86,10 +85,12 @@ function renderFacet(
     const [values, setValues] = React.useState(facetHeaderProps.values ?? undefined);
 
     const property = 'foo';
+    const headerId = `facet_${property}`;
 
     return (
       <FacetBox property={property} {...facetBoxProps}>
         <FacetHeader
+          id={headerId}
           name="foo"
           onClick={() => setOpen(!open)}
           onClear={() => setValues(undefined)}
@@ -97,7 +98,7 @@ function renderFacet(
         />
 
         {open && (
-          <FacetItemsList label={property} {...facetItemListProps}>
+          <FacetItemsList labelledby={headerId}>
             <FacetItem
               active={true}
               name="Foo/Bar"
index 497402e96f61a9a88f47de9640576a12953b7832..97ef4850aa8e7a28720babeeb4879d2ea16f0c95 100644 (file)
@@ -9,6 +9,7 @@ exports[`should be disabled 1`] = `
     disabled={true}
     disabledHelper="Disabled helper description"
     fetching={false}
+    id="facet_foo"
     name="facet header"
     onClear={[Function]}
     open={false}
@@ -23,6 +24,7 @@ exports[`should display all selected items 1`] = `
 >
   <FacetHeader
     fetching={false}
+    id="facet_foo"
     name="facet header"
     onClear={[Function]}
     onClick={[Function]}
@@ -45,7 +47,7 @@ exports[`should display all selected items 1`] = `
     value=""
   />
   <FacetItemsList
-    label="foo"
+    labelledby="facet_foo"
   >
     <FacetItem
       active={true}
@@ -76,7 +78,7 @@ exports[`should display all selected items 1`] = `
     ⋯
   </div>
   <FacetItemsList
-    label="foo"
+    labelledby="facet_foo"
   >
     <FacetItem
       active={true}
@@ -108,6 +110,7 @@ exports[`should render 1`] = `
 >
   <FacetHeader
     fetching={false}
+    id="facet_foo"
     name="facet header"
     onClear={[Function]}
     onClick={[Function]}
@@ -124,7 +127,7 @@ exports[`should render 1`] = `
     value=""
   />
   <FacetItemsList
-    label="foo"
+    labelledby="facet_foo"
   >
     <FacetItem
       active={false}
@@ -178,6 +181,7 @@ exports[`should search 1`] = `
 >
   <FacetHeader
     fetching={false}
+    id="facet_foo"
     name="facet header"
     onClear={[Function]}
     onClick={[Function]}
@@ -194,7 +198,7 @@ exports[`should search 1`] = `
     value="query"
   />
   <FacetItemsList
-    label="foo"
+    labelledby="facet_foo"
   >
     <FacetItem
       active={false}
@@ -239,6 +243,7 @@ exports[`should search 2`] = `
 >
   <FacetHeader
     fetching={false}
+    id="facet_foo"
     name="facet header"
     onClear={[Function]}
     onClick={[Function]}
@@ -255,7 +260,7 @@ exports[`should search 2`] = `
     value="query"
   />
   <FacetItemsList
-    label="foo"
+    labelledby="facet_foo"
   >
     <FacetItem
       active={false}
@@ -311,6 +316,7 @@ exports[`should search 3`] = `
 >
   <FacetHeader
     fetching={false}
+    id="facet_foo"
     name="facet header"
     onClear={[Function]}
     onClick={[Function]}
@@ -327,7 +333,7 @@ exports[`should search 3`] = `
     value=""
   />
   <FacetItemsList
-    label="foo"
+    labelledby="facet_foo"
   >
     <FacetItem
       active={false}
@@ -381,6 +387,7 @@ exports[`should search 4`] = `
 >
   <FacetHeader
     fetching={false}
+    id="facet_foo"
     name="facet header"
     onClear={[Function]}
     onClick={[Function]}
@@ -414,6 +421,7 @@ exports[`should search 5`] = `
 >
   <FacetHeader
     fetching={false}
+    id="facet_foo"
     name="facet header"
     onClear={[Function]}
     onClick={[Function]}