diff options
author | Wouter Admiraal <wouter.admiraal@sonarsource.com> | 2022-08-26 15:03:06 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2022-08-30 20:03:14 +0000 |
commit | 3d50c83a073845113f2621ce92695a9bc297d7bf (patch) | |
tree | becfba4591bdb0014492dbd90419343372bf6e17 /server | |
parent | a74e990dbbddbfac7c608a537304180d6869e406 (diff) | |
download | sonarqube-3d50c83a073845113f2621ce92695a9bc297d7bf.tar.gz sonarqube-3d50c83a073845113f2621ce92695a9bc297d7bf.zip |
SONAR-16855 [893325] Information or relationship only presented visually
Diffstat (limited to 'server')
17 files changed, 369 insertions, 229 deletions
diff --git a/server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts b/server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts index 32221bf8792..af7cf936e55 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts +++ b/server/sonar-web/src/main/js/apps/coding-rules/__tests__/CodingRules-it.ts @@ -87,23 +87,23 @@ it('should show hotspot rule section', async () => { expect(await screen.findByRole('heading', { level: 3, name: 'Hot hotspot' })).toBeInTheDocument(); expect(screen.getByText('Introduction to this rule')).toBeInTheDocument(); expect( - screen.getByRole('button', { + screen.getByRole('tab', { name: 'coding_rules.description_section.title.root_cause.SECURITY_HOTSPOT' }) ).toBeInTheDocument(); expect( - screen.getByRole('button', { + screen.getByRole('tab', { name: 'coding_rules.description_section.title.assess_the_problem' }) ).toBeInTheDocument(); expect( - screen.getByRole('button', { + screen.getByRole('tab', { name: 'coding_rules.description_section.title.more_info' }) ).toBeInTheDocument(); // Check that we render plain html await user.click( - screen.getByRole('button', { + screen.getByRole('tab', { name: 'coding_rules.description_section.title.more_info' }) ); @@ -118,18 +118,18 @@ it('should show rule advanced section', async () => { ).toBeInTheDocument(); expect(screen.getByText('Introduction to this rule')).toBeInTheDocument(); expect( - screen.getByRole('button', { + screen.getByRole('tab', { name: 'coding_rules.description_section.title.how_to_fix' }) ).toBeInTheDocument(); expect( - screen.getByRole('button', { + screen.getByRole('tab', { name: 'coding_rules.description_section.title.more_info' }) ).toBeInTheDocument(); // Check that we render plain html await user.click( - screen.getByRole('button', { + screen.getByRole('tab', { name: 'coding_rules.description_section.title.more_info' }) ); @@ -143,13 +143,13 @@ it('should show rule advanced section with context', async () => { await screen.findByRole('heading', { level: 3, name: 'Python rule with context' }) ).toBeInTheDocument(); expect( - screen.getByRole('button', { + screen.getByRole('tab', { name: 'coding_rules.description_section.title.how_to_fix' }) ).toBeInTheDocument(); await user.click( - screen.getByRole('button', { + screen.getByRole('tab', { name: 'coding_rules.description_section.title.how_to_fix' }) ); @@ -384,13 +384,13 @@ it('should show notification for rule advanced section and remove it after user name: 'Awesome Python rule with education principles' }); expect( - screen.getByRole('button', { + screen.getByRole('tab', { name: 'coding_rules.description_section.title.more_info' }) ).toBeInTheDocument(); await user.click( - screen.getByRole('button', { + screen.getByRole('tab', { name: 'coding_rules.description_section.title.more_info' }) ); @@ -408,12 +408,12 @@ it('should show notification for rule advanced section and remove it after user ); // navigate away and come back await user.click( - screen.getByRole('button', { + screen.getByRole('tab', { name: 'coding_rules.description_section.title.how_to_fix' }) ); await user.click( - screen.getByRole('button', { + screen.getByRole('tab', { name: 'coding_rules.description_section.title.more_info' }) ); @@ -429,31 +429,31 @@ it('should show notification for rule advanced section and removes it when user name: 'Awesome Python rule with education principles' }); expect( - screen.getByRole('button', { + screen.getByRole('tab', { name: 'coding_rules.description_section.title.more_info' }) ).toBeInTheDocument(); // navigate away and come back await user.click( - screen.getByRole('button', { + screen.getByRole('tab', { name: 'coding_rules.description_section.title.how_to_fix' }) ); await user.click( - screen.getByRole('button', { + screen.getByRole('tab', { name: 'coding_rules.description_section.title.more_info' }) ); expect( - screen.getByRole('button', { + screen.getByRole('tab', { name: 'coding_rules.description_section.title.more_info' }) ).toBeInTheDocument(); await user.click( - screen.getByRole('button', { + screen.getByRole('tab', { name: 'coding_rules.description_section.title.more_info' }) ); @@ -469,12 +469,12 @@ it('should show notification for rule advanced section and removes it when user // navigate away and come back await user.click( - screen.getByRole('button', { + screen.getByRole('tab', { name: 'coding_rules.description_section.title.how_to_fix' }) ); await user.click( - screen.getByRole('button', { + screen.getByRole('tab', { name: 'coding_rules.description_section.title.more_info' }) ); @@ -486,7 +486,7 @@ it('should not show notification for anonymous users', async () => { renderCodingRulesApp(mockCurrentUser(), 'coding_rules?open=rule8'); await user.click( - await screen.findByRole('button', { + await screen.findByRole('tab', { name: 'coding_rules.description_section.title.more_info' }) ); diff --git a/server/sonar-web/src/main/js/apps/issues/__tests__/IssueApp-it.tsx b/server/sonar-web/src/main/js/apps/issues/__tests__/IssueApp-it.tsx index 0d3a43a18b8..2774e105574 100644 --- a/server/sonar-web/src/main/js/apps/issues/__tests__/IssueApp-it.tsx +++ b/server/sonar-web/src/main/js/apps/issues/__tests__/IssueApp-it.tsx @@ -46,7 +46,7 @@ it('should show education principles', async () => { const user = userEvent.setup(); renderProjectIssuesApp('project/issues?issues=issue2&open=issue2&id=myproject'); await user.click( - await screen.findByRole('button', { name: `coding_rules.description_section.title.more_info` }) + await screen.findByRole('tab', { name: `coding_rules.description_section.title.more_info` }) ); expect(screen.getByRole('heading', { name: 'Defense-In-Depth', level: 3 })).toBeInTheDocument(); }); @@ -59,7 +59,7 @@ it('should open issue and navigate', async () => { // Select an issue with an advanced rule expect(await screen.findByRole('region', { name: 'Fix that' })).toBeInTheDocument(); await user.click(screen.getByRole('region', { name: 'Fix that' })); - expect(screen.getByRole('button', { name: 'issue.tabs.code' })).toBeInTheDocument(); + expect(screen.getByRole('tab', { name: 'issue.tabs.code' })).toBeInTheDocument(); // Are rule headers present? expect(screen.getByRole('heading', { level: 1, name: 'Fix that' })).toBeInTheDocument(); @@ -67,19 +67,19 @@ it('should open issue and navigate', async () => { // Select the "why is this an issue" tab and check its content expect( - screen.getByRole('button', { name: `coding_rules.description_section.title.root_cause` }) + screen.getByRole('tab', { name: `coding_rules.description_section.title.root_cause` }) ).toBeInTheDocument(); await user.click( - screen.getByRole('button', { name: `coding_rules.description_section.title.root_cause` }) + screen.getByRole('tab', { name: `coding_rules.description_section.title.root_cause` }) ); expect(screen.getByRole('heading', { name: 'Because' })).toBeInTheDocument(); // Select the "how to fix it" tab expect( - screen.getByRole('button', { name: `coding_rules.description_section.title.how_to_fix` }) + screen.getByRole('tab', { name: `coding_rules.description_section.title.how_to_fix` }) ).toBeInTheDocument(); await user.click( - screen.getByRole('button', { name: `coding_rules.description_section.title.how_to_fix` }) + screen.getByRole('tab', { name: `coding_rules.description_section.title.how_to_fix` }) ); // Is the context selector present with the expected values and default selection? @@ -103,10 +103,10 @@ it('should open issue and navigate', async () => { // Select the main info tab and check its content expect( - screen.getByRole('button', { name: `coding_rules.description_section.title.more_info` }) + screen.getByRole('tab', { name: `coding_rules.description_section.title.more_info` }) ).toBeInTheDocument(); await user.click( - screen.getByRole('button', { name: `coding_rules.description_section.title.more_info` }) + screen.getByRole('tab', { name: `coding_rules.description_section.title.more_info` }) ); expect(screen.getByRole('heading', { name: 'Link' })).toBeInTheDocument(); @@ -123,10 +123,10 @@ it('should open issue and navigate', async () => { // Select the "why is this an issue tab" and check its content expect( - screen.getByRole('button', { name: `coding_rules.description_section.title.root_cause` }) + screen.getByRole('tab', { name: `coding_rules.description_section.title.root_cause` }) ).toBeInTheDocument(); await user.click( - screen.getByRole('button', { name: `coding_rules.description_section.title.root_cause` }) + screen.getByRole('tab', { name: `coding_rules.description_section.title.root_cause` }) ); expect(screen.getByRole('heading', { name: 'Default' })).toBeInTheDocument(); @@ -413,7 +413,7 @@ it('should show code tabs when any secondary location is selected', async () => // Select the "why is this an issue" tab await user.click( - screen.getByRole('button', { name: 'coding_rules.description_section.title.root_cause' }) + screen.getByRole('tab', { name: 'coding_rules.description_section.title.root_cause' }) ); expect( screen.queryByRole('row', { @@ -430,7 +430,7 @@ it('should show code tabs when any secondary location is selected', async () => // selecting the same selected hotspot location should also navigate back to code page await user.click( - screen.getByRole('button', { name: 'coding_rules.description_section.title.root_cause' }) + screen.getByRole('tab', { name: 'coding_rules.description_section.title.root_cause' }) ); expect( screen.queryByRole('row', { diff --git a/server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanel.tsx b/server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanel.tsx index 0f1e50315e5..e3be38745b2 100644 --- a/server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanel.tsx +++ b/server/sonar-web/src/main/js/apps/overview/branches/MeasuresPanel.tsx @@ -19,7 +19,7 @@ */ import * as React from 'react'; import { rawSizes } from '../../../app/theme'; -import BoxedTabs from '../../../components/controls/BoxedTabs'; +import BoxedTabs, { getTabId, getTabPanelId } from '../../../components/controls/BoxedTabs'; import ComponentReportActions from '../../../components/controls/ComponentReportActions'; import { Location, withRouter } from '../../../components/hoc/withRouter'; import DeferredSpinner from '../../../components/ui/DeferredSpinner'; @@ -50,8 +50,8 @@ export interface MeasuresPanelProps { } export enum MeasuresPanelTabs { - New, - Overall + New = 'new', + Overall = 'overall' } export function MeasuresPanel(props: MeasuresPanelProps) { @@ -115,9 +115,13 @@ export function MeasuresPanel(props: MeasuresPanelProps) { </div> ) : ( <> - <BoxedTabs onSelect={selectTab} selected={tab} tabs={tabs} /> + <BoxedTabs onSelect={key => selectTab(key)} selected={tab} tabs={tabs} /> - <div className="overview-panel-content flex-1 bordered"> + <div + className="overview-panel-content flex-1 bordered" + role="tabpanel" + id={getTabPanelId(tab)} + aria-labelledby={getTabId(tab)}> {!hasDiffMeasures && isNewCodeTab ? ( <MeasuresPanelNoNewCode branch={branch} component={component} period={period} /> ) : ( diff --git a/server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/MeasuresPanel-test.tsx.snap b/server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/MeasuresPanel-test.tsx.snap index 97d53169603..6936ea45634 100644 --- a/server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/MeasuresPanel-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/MeasuresPanel-test.tsx.snap @@ -48,11 +48,11 @@ exports[`should render correctly for applications: default 1`] = ` </div> <BoxedTabs onSelect={[Function]} - selected={0} + selected="new" tabs={ Array [ Object { - "key": 0, + "key": "new", "label": <div className="text-left overview-measures-tab" > @@ -64,7 +64,7 @@ exports[`should render correctly for applications: default 1`] = ` </div>, }, Object { - "key": 1, + "key": "overall", "label": <div className="text-left overview-measures-tab" > @@ -85,7 +85,10 @@ exports[`should render correctly for applications: default 1`] = ` } /> <div + aria-labelledby="tab-new" className="overview-panel-content flex-1 bordered" + id="tabpanel-new" + role="tabpanel" > <MeasuresPanelIssueMeasureRow branchLike={ @@ -770,11 +773,11 @@ exports[`should render correctly for applications: overall 1`] = ` </div> <BoxedTabs onSelect={[Function]} - selected={1} + selected="overall" tabs={ Array [ Object { - "key": 0, + "key": "new", "label": <div className="text-left overview-measures-tab" > @@ -786,7 +789,7 @@ exports[`should render correctly for applications: overall 1`] = ` </div>, }, Object { - "key": 1, + "key": "overall", "label": <div className="text-left overview-measures-tab" > @@ -807,7 +810,10 @@ exports[`should render correctly for applications: overall 1`] = ` } /> <div + aria-labelledby="tab-overall" className="overview-panel-content flex-1 bordered" + id="tabpanel-overall" + role="tabpanel" > <MeasuresPanelIssueMeasureRow branchLike={ @@ -1702,11 +1708,11 @@ exports[`should render correctly for projects: default 1`] = ` </div> <BoxedTabs onSelect={[Function]} - selected={0} + selected="new" tabs={ Array [ Object { - "key": 0, + "key": "new", "label": <div className="text-left overview-measures-tab" > @@ -1718,7 +1724,7 @@ exports[`should render correctly for projects: default 1`] = ` </div>, }, Object { - "key": 1, + "key": "overall", "label": <div className="text-left overview-measures-tab" > @@ -1739,7 +1745,10 @@ exports[`should render correctly for projects: default 1`] = ` } /> <div + aria-labelledby="tab-new" className="overview-panel-content flex-1 bordered" + id="tabpanel-new" + role="tabpanel" > <MeasuresPanelIssueMeasureRow branchLike={ @@ -2424,11 +2433,11 @@ exports[`should render correctly for projects: overall 1`] = ` </div> <BoxedTabs onSelect={[Function]} - selected={1} + selected="overall" tabs={ Array [ Object { - "key": 0, + "key": "new", "label": <div className="text-left overview-measures-tab" > @@ -2440,7 +2449,7 @@ exports[`should render correctly for projects: overall 1`] = ` </div>, }, Object { - "key": 1, + "key": "overall", "label": <div className="text-left overview-measures-tab" > @@ -2461,7 +2470,10 @@ exports[`should render correctly for projects: overall 1`] = ` } /> <div + aria-labelledby="tab-overall" className="overview-panel-content flex-1 bordered" + id="tabpanel-overall" + role="tabpanel" > <MeasuresPanelIssueMeasureRow branchLike={ @@ -3356,11 +3368,11 @@ exports[`should render correctly if branch is misconfigured: hide settings 1`] = </div> <BoxedTabs onSelect={[Function]} - selected={0} + selected="new" tabs={ Array [ Object { - "key": 0, + "key": "new", "label": <div className="text-left overview-measures-tab" > @@ -3382,7 +3394,7 @@ exports[`should render correctly if branch is misconfigured: hide settings 1`] = </div>, }, Object { - "key": 1, + "key": "overall", "label": <div className="text-left overview-measures-tab" > @@ -3403,7 +3415,10 @@ exports[`should render correctly if branch is misconfigured: hide settings 1`] = } /> <div + aria-labelledby="tab-new" className="overview-panel-content flex-1 bordered" + id="tabpanel-new" + role="tabpanel" > <MeasuresPanelNoNewCode branch={ @@ -3500,11 +3515,11 @@ exports[`should render correctly if branch is misconfigured: show settings 1`] = </div> <BoxedTabs onSelect={[Function]} - selected={0} + selected="new" tabs={ Array [ Object { - "key": 0, + "key": "new", "label": <div className="text-left overview-measures-tab" > @@ -3526,7 +3541,7 @@ exports[`should render correctly if branch is misconfigured: show settings 1`] = </div>, }, Object { - "key": 1, + "key": "overall", "label": <div className="text-left overview-measures-tab" > @@ -3547,7 +3562,10 @@ exports[`should render correctly if branch is misconfigured: show settings 1`] = } /> <div + aria-labelledby="tab-new" className="overview-panel-content flex-1 bordered" + id="tabpanel-new" + role="tabpanel" > <MeasuresPanelNoNewCode branch={ @@ -3700,11 +3718,11 @@ exports[`should render correctly if there is no coverage 1`] = ` </div> <BoxedTabs onSelect={[Function]} - selected={0} + selected="new" tabs={ Array [ Object { - "key": 0, + "key": "new", "label": <div className="text-left overview-measures-tab" > @@ -3716,7 +3734,7 @@ exports[`should render correctly if there is no coverage 1`] = ` </div>, }, Object { - "key": 1, + "key": "overall", "label": <div className="text-left overview-measures-tab" > @@ -3737,7 +3755,10 @@ exports[`should render correctly if there is no coverage 1`] = ` } /> <div + aria-labelledby="tab-new" className="overview-panel-content flex-1 bordered" + id="tabpanel-new" + role="tabpanel" > <MeasuresPanelIssueMeasureRow branchLike={ @@ -4154,11 +4175,11 @@ exports[`should render correctly if there is no new code measures 1`] = ` </div> <BoxedTabs onSelect={[Function]} - selected={0} + selected="new" tabs={ Array [ Object { - "key": 0, + "key": "new", "label": <div className="text-left overview-measures-tab" > @@ -4170,7 +4191,7 @@ exports[`should render correctly if there is no new code measures 1`] = ` </div>, }, Object { - "key": 1, + "key": "overall", "label": <div className="text-left overview-measures-tab" > @@ -4191,7 +4212,10 @@ exports[`should render correctly if there is no new code measures 1`] = ` } /> <div + aria-labelledby="tab-new" className="overview-panel-content flex-1 bordered" + id="tabpanel-new" + role="tabpanel" > <MeasuresPanelNoNewCode branch={ @@ -4277,11 +4301,11 @@ exports[`should render correctly when code scope is new code 1`] = ` </div> <BoxedTabs onSelect={[Function]} - selected={0} + selected="new" tabs={ Array [ Object { - "key": 0, + "key": "new", "label": <div className="text-left overview-measures-tab" > @@ -4293,7 +4317,7 @@ exports[`should render correctly when code scope is new code 1`] = ` </div>, }, Object { - "key": 1, + "key": "overall", "label": <div className="text-left overview-measures-tab" > @@ -4314,7 +4338,10 @@ exports[`should render correctly when code scope is new code 1`] = ` } /> <div + aria-labelledby="tab-new" className="overview-panel-content flex-1 bordered" + id="tabpanel-new" + role="tabpanel" > <MeasuresPanelIssueMeasureRow branchLike={ @@ -4999,11 +5026,11 @@ exports[`should render correctly when code scope is overall code 1`] = ` </div> <BoxedTabs onSelect={[Function]} - selected={1} + selected="overall" tabs={ Array [ Object { - "key": 0, + "key": "new", "label": <div className="text-left overview-measures-tab" > @@ -5015,7 +5042,7 @@ exports[`should render correctly when code scope is overall code 1`] = ` </div>, }, Object { - "key": 1, + "key": "overall", "label": <div className="text-left overview-measures-tab" > @@ -5036,7 +5063,10 @@ exports[`should render correctly when code scope is overall code 1`] = ` } /> <div + aria-labelledby="tab-overall" className="overview-panel-content flex-1 bordered" + id="tabpanel-overall" + role="tabpanel" > <MeasuresPanelIssueMeasureRow branchLike={ diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/BranchLikeTabs.tsx b/server/sonar-web/src/main/js/apps/projectBranches/components/BranchLikeTabs.tsx index 02ea904266f..77732e65287 100644 --- a/server/sonar-web/src/main/js/apps/projectBranches/components/BranchLikeTabs.tsx +++ b/server/sonar-web/src/main/js/apps/projectBranches/components/BranchLikeTabs.tsx @@ -18,7 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as React from 'react'; -import BoxedTabs from '../../../components/controls/BoxedTabs'; +import BoxedTabs, { getTabId, getTabPanelId } from '../../../components/controls/BoxedTabs'; import BranchIcon from '../../../components/icons/BranchIcon'; import PullRequestIcon from '../../../components/icons/PullRequestIcon'; import { @@ -128,15 +128,17 @@ export default class BranchLikeTabs extends React.PureComponent<Props, State> { tabs={TABS} /> - <BranchLikeTable - branchLikes={branchLikesToDisplay} - component={component} - displayPurgeSetting={isBranchMode} - onDelete={this.handleDeleteBranchLike} - onRename={this.handleRenameBranchLike} - onUpdatePurgeSetting={this.handleUpdatePurgeSetting} - title={title} - /> + <div role="tabpanel" id={getTabPanelId(currentTab)} aria-labelledby={getTabId(currentTab)}> + <BranchLikeTable + branchLikes={branchLikesToDisplay} + component={component} + displayPurgeSetting={isBranchMode} + onDelete={this.handleDeleteBranchLike} + onRename={this.handleRenameBranchLike} + onUpdatePurgeSetting={this.handleUpdatePurgeSetting} + title={title} + /> + </div> {deleting && ( <DeleteBranchModal diff --git a/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/BranchLikeTabs-test.tsx.snap b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/BranchLikeTabs-test.tsx.snap index 5ad71478b28..68626aa084a 100644 --- a/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/BranchLikeTabs-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/projectBranches/components/__tests__/__snapshots__/BranchLikeTabs-test.tsx.snap @@ -33,75 +33,81 @@ exports[`should render all tabs correctly 1`] = ` ] } /> - <Memo(BranchLikeTable) - branchLikes={ - Array [ - Object { - "analysisDate": "2018-01-01", - "excludedFromPurge": true, - "isMain": true, - "name": "master", - }, - Object { - "analysisDate": "2018-01-01", - "excludedFromPurge": true, - "isMain": false, - "name": "branch-1", - }, - Object { - "analysisDate": "2018-01-01", - "excludedFromPurge": true, - "isMain": false, - "name": "branch-11", - }, - Object { - "analysisDate": "2018-01-01", - "excludedFromPurge": true, - "isMain": false, - "name": "branch-12", - }, - Object { - "analysisDate": "2018-01-01", - "excludedFromPurge": true, - "isMain": false, - "name": "branch-2", - }, - Object { - "analysisDate": "2018-01-01", - "excludedFromPurge": true, - "isMain": false, - "name": "branch-3", - }, - ] - } - component={ - Object { - "breadcrumbs": Array [], - "key": "my-project", - "name": "MyProject", - "qualifier": "TRK", - "qualityGate": Object { - "isDefault": true, - "key": "30", - "name": "Sonar way", - }, - "qualityProfiles": Array [ + <div + aria-labelledby="tab-0" + id="tabpanel-0" + role="tabpanel" + > + <Memo(BranchLikeTable) + branchLikes={ + Array [ + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": true, + "name": "master", + }, + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": false, + "name": "branch-1", + }, + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": false, + "name": "branch-11", + }, + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": false, + "name": "branch-12", + }, + Object { + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": false, + "name": "branch-2", + }, Object { - "deleted": false, - "key": "my-qp", - "language": "ts", + "analysisDate": "2018-01-01", + "excludedFromPurge": true, + "isMain": false, + "name": "branch-3", + }, + ] + } + component={ + Object { + "breadcrumbs": Array [], + "key": "my-project", + "name": "MyProject", + "qualifier": "TRK", + "qualityGate": Object { + "isDefault": true, + "key": "30", "name": "Sonar way", }, - ], - "tags": Array [], + "qualityProfiles": Array [ + Object { + "deleted": false, + "key": "my-qp", + "language": "ts", + "name": "Sonar way", + }, + ], + "tags": Array [], + } } - } - displayPurgeSetting={true} - onDelete={[Function]} - onRename={[Function]} - onUpdatePurgeSetting={[Function]} - title="project_branch_pull_request.table.branch" - /> + displayPurgeSetting={true} + onDelete={[Function]} + onRename={[Function]} + onUpdatePurgeSetting={[Function]} + title="project_branch_pull_request.table.branch" + /> + </div> </Fragment> `; @@ -138,64 +144,70 @@ exports[`should render all tabs correctly 2`] = ` ] } /> - <Memo(BranchLikeTable) - branchLikes={ - Array [ - Object { - "analysisDate": "2018-01-01", - "base": "master", - "branch": "feature/foo/bar", - "key": "1", - "target": "master", - "title": "PR-1", - }, - Object { - "analysisDate": "2018-01-01", - "base": "master", - "branch": "feature/foo/bar", - "key": "2", - "target": "master", - "title": "PR-2", - }, - Object { - "analysisDate": "2018-01-01", - "base": "master", - "branch": "feature/foo/bar", - "isOrphan": true, - "key": "2", - "target": "llb-100", - "title": "PR-2", - }, - ] - } - component={ - Object { - "breadcrumbs": Array [], - "key": "my-project", - "name": "MyProject", - "qualifier": "TRK", - "qualityGate": Object { - "isDefault": true, - "key": "30", - "name": "Sonar way", - }, - "qualityProfiles": Array [ + <div + aria-labelledby="tab-1" + id="tabpanel-1" + role="tabpanel" + > + <Memo(BranchLikeTable) + branchLikes={ + Array [ Object { - "deleted": false, - "key": "my-qp", - "language": "ts", + "analysisDate": "2018-01-01", + "base": "master", + "branch": "feature/foo/bar", + "key": "1", + "target": "master", + "title": "PR-1", + }, + Object { + "analysisDate": "2018-01-01", + "base": "master", + "branch": "feature/foo/bar", + "key": "2", + "target": "master", + "title": "PR-2", + }, + Object { + "analysisDate": "2018-01-01", + "base": "master", + "branch": "feature/foo/bar", + "isOrphan": true, + "key": "2", + "target": "llb-100", + "title": "PR-2", + }, + ] + } + component={ + Object { + "breadcrumbs": Array [], + "key": "my-project", + "name": "MyProject", + "qualifier": "TRK", + "qualityGate": Object { + "isDefault": true, + "key": "30", "name": "Sonar way", }, - ], - "tags": Array [], + "qualityProfiles": Array [ + Object { + "deleted": false, + "key": "my-qp", + "language": "ts", + "name": "Sonar way", + }, + ], + "tags": Array [], + } } - } - displayPurgeSetting={false} - onDelete={[Function]} - onRename={[Function]} - onUpdatePurgeSetting={[Function]} - title="project_branch_pull_request.table.pull_request" - /> + displayPurgeSetting={false} + onDelete={[Function]} + onRename={[Function]} + onUpdatePurgeSetting={[Function]} + title="project_branch_pull_request.table.pull_request" + /> + </div> </Fragment> `; diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerTabs.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerTabs.tsx index 294e89be5c9..5b0bd74fb42 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerTabs.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotViewerTabs.tsx @@ -19,7 +19,7 @@ */ import { groupBy } from 'lodash'; import * as React from 'react'; -import BoxedTabs from '../../../components/controls/BoxedTabs'; +import BoxedTabs, { getTabId, getTabPanelId } from '../../../components/controls/BoxedTabs'; import RuleDescription from '../../../components/rules/RuleDescription'; import { isInput, isShortcut } from '../../../helpers/keyboardEventHelpers'; import { KeyboardKeys } from '../../../helpers/keycodes'; @@ -187,7 +187,13 @@ export default class HotspotViewerTabs extends React.PureComponent<Props, State> return ( <> <BoxedTabs onSelect={this.handleSelectTabs} selected={currentTab.key} tabs={tabs} /> - <div className="bordered huge-spacer-bottom">{currentTab.content}</div> + <div + className="bordered huge-spacer-bottom" + role="tabpanel" + aria-labelledby={getTabId(currentTab.key)} + id={getTabPanelId(currentTab.key)}> + {currentTab.content} + </div> </> ); } diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotViewerTabs-test.tsx.snap b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotViewerTabs-test.tsx.snap index 90ef112adf5..8f26bcf4641 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotViewerTabs-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotViewerTabs-test.tsx.snap @@ -70,7 +70,10 @@ exports[`should render correctly: fix 1`] = ` } /> <div + aria-labelledby="tab-fix" className="bordered huge-spacer-bottom" + id="tabpanel-fix" + role="tabpanel" > <RuleDescription className="big-padded" @@ -158,7 +161,10 @@ exports[`should render correctly: risk 1`] = ` } /> <div + aria-labelledby="tab-code" className="bordered huge-spacer-bottom" + id="tabpanel-code" + role="tabpanel" > <div className="padded" @@ -241,7 +247,10 @@ exports[`should render correctly: vulnerability 1`] = ` } /> <div + aria-labelledby="tab-vulnerability" className="bordered huge-spacer-bottom" + id="tabpanel-vulnerability" + role="tabpanel" > <RuleDescription className="big-padded" @@ -329,7 +338,10 @@ exports[`should render correctly: with comments or changelog element 1`] = ` } /> <div + aria-labelledby="tab-code" className="bordered huge-spacer-bottom" + id="tabpanel-code" + role="tabpanel" > <div className="padded" diff --git a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmTabRenderer.tsx b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmTabRenderer.tsx index 102dead85c2..d2f72177858 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmTabRenderer.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/AlmTabRenderer.tsx @@ -20,6 +20,7 @@ import * as React from 'react'; import { FormattedMessage } from 'react-intl'; import Link from '../../../../components/common/Link'; +import { getTabId, getTabPanelId } from '../../../../components/controls/BoxedTabs'; import { Button } from '../../../../components/controls/buttons'; import { Alert } from '../../../../components/ui/Alert'; import DeferredSpinner from '../../../../components/ui/DeferredSpinner'; @@ -77,7 +78,11 @@ export default function AlmTabRenderer(props: AlmTabRendererProps) { const preventCreation = loadingProjectCount || (!multipleAlmEnabled && definitions.length > 0); return ( - <div className="bordered"> + <div + className="bordered" + role="tabpanel" + id={getTabPanelId(almTab)} + aria-labelledby={getTabId(almTab)}> <div className="big-padded"> <DeferredSpinner loading={loadingAlmDefinitions}> {definitions.length === 0 && ( diff --git a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/AlmTabRenderer-test.tsx.snap b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/AlmTabRenderer-test.tsx.snap index f6ea40b3c1c..895747d0970 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/AlmTabRenderer-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/settings/components/almIntegration/__tests__/__snapshots__/AlmTabRenderer-test.tsx.snap @@ -2,7 +2,10 @@ exports[`should render correctly for multi-ALM binding: editing a definition 1`] = ` <div + aria-labelledby="tab-azure" className="bordered" + id="tabpanel-azure" + role="tabpanel" > <div className="big-padded" @@ -48,7 +51,10 @@ exports[`should render correctly for multi-ALM binding: editing a definition 1`] exports[`should render correctly for multi-ALM binding: loaded 1`] = ` <div + aria-labelledby="tab-azure" className="bordered" + id="tabpanel-azure" + role="tabpanel" > <div className="big-padded" @@ -94,7 +100,10 @@ exports[`should render correctly for multi-ALM binding: loaded 1`] = ` exports[`should render correctly for multi-ALM binding: loading ALM definitions 1`] = ` <div + aria-labelledby="tab-azure" className="bordered" + id="tabpanel-azure" + role="tabpanel" > <div className="big-padded" @@ -140,7 +149,10 @@ exports[`should render correctly for multi-ALM binding: loading ALM definitions exports[`should render correctly for multi-ALM binding: loading project count 1`] = ` <div + aria-labelledby="tab-azure" className="bordered" + id="tabpanel-azure" + role="tabpanel" > <div className="big-padded" @@ -186,7 +198,10 @@ exports[`should render correctly for multi-ALM binding: loading project count 1` exports[`should render correctly for single-ALM binding 1`] = ` <div + aria-labelledby="tab-azure" className="bordered" + id="tabpanel-azure" + role="tabpanel" > <div className="big-padded" @@ -232,7 +247,10 @@ exports[`should render correctly for single-ALM binding 1`] = ` exports[`should render correctly for single-ALM binding 2`] = ` <div + aria-labelledby="tab-azure" className="bordered" + id="tabpanel-azure" + role="tabpanel" > <div className="big-padded" @@ -278,7 +296,10 @@ exports[`should render correctly for single-ALM binding 2`] = ` exports[`should render correctly for single-ALM binding 3`] = ` <div + aria-labelledby="tab-azure" className="bordered" + id="tabpanel-azure" + role="tabpanel" > <div className="big-padded" @@ -324,7 +345,10 @@ exports[`should render correctly for single-ALM binding 3`] = ` exports[`should render correctly with validation: create a first 1`] = ` <div + aria-labelledby="tab-azure" className="bordered" + id="tabpanel-azure" + role="tabpanel" > <div className="big-padded" @@ -360,7 +384,10 @@ exports[`should render correctly with validation: create a first 1`] = ` exports[`should render correctly with validation: create a second 1`] = ` <div + aria-labelledby="tab-azure" className="bordered" + id="tabpanel-azure" + role="tabpanel" > <div className="big-padded" @@ -410,7 +437,10 @@ exports[`should render correctly with validation: create a second 1`] = ` exports[`should render correctly with validation: default 1`] = ` <div + aria-labelledby="tab-azure" className="bordered" + id="tabpanel-azure" + role="tabpanel" > <div className="big-padded" @@ -460,7 +490,10 @@ exports[`should render correctly with validation: default 1`] = ` exports[`should render correctly with validation: empty 1`] = ` <div + aria-labelledby="tab-azure" className="bordered" + id="tabpanel-azure" + role="tabpanel" > <div className="big-padded" @@ -496,7 +529,10 @@ exports[`should render correctly with validation: empty 1`] = ` exports[`should render correctly with validation: pass the correct key for bitbucket cloud 1`] = ` <div + aria-labelledby="tab-bitbucket" className="bordered" + id="tabpanel-bitbucket" + role="tabpanel" > <div className="big-padded" diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/Authentication.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/Authentication.tsx index 88dd2eb57fe..0aeb3c0f341 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/authentication/Authentication.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/Authentication.tsx @@ -22,7 +22,7 @@ import { FormattedMessage } from 'react-intl'; import { useSearchParams } from 'react-router-dom'; import Link from '../../../../components/common/Link'; import ScreenPositionHelper from '../../../../components/common/ScreenPositionHelper'; -import BoxedTabs from '../../../../components/controls/BoxedTabs'; +import BoxedTabs, { getTabId, getTabPanelId } from '../../../../components/controls/BoxedTabs'; import { Alert } from '../../../../components/ui/Alert'; import { translate } from '../../../../helpers/l10n'; import { getBaseUrl } from '../../../../helpers/system'; @@ -130,7 +130,10 @@ export default function Authentication(props: Props) { maxHeight: `calc(100vh - ${top + HEIGHT_ADJUSTMENT}px)` }} className="bordered overflow-y-auto tabbed-definitions" - key={currentTab}> + key={currentTab} + role="tabpanel" + aria-labelledby={getTabId(currentTab)} + id={getTabPanelId(currentTab)}> <div className="big-padded"> <Alert variant="info"> <FormattedMessage diff --git a/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-test.tsx b/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-test.tsx index 954ef528460..c429e9c33c0 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-test.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/authentication/__tests__/Authentication-test.tsx @@ -27,15 +27,15 @@ it('should render tabs and allow navigation', async () => { const user = userEvent.setup(); renderAuthentication(); - expect(screen.getAllByRole('button')).toHaveLength(4); + expect(screen.getAllByRole('tab')).toHaveLength(4); - expect(screen.getByRole('button', { name: 'SAML' })).toHaveAttribute('aria-current', 'true'); + expect(screen.getByRole('tab', { name: 'SAML' })).toHaveAttribute('aria-selected', 'true'); - await user.click(screen.getByRole('button', { name: 'github GitHub' })); + await user.click(screen.getByRole('tab', { name: 'github GitHub' })); - expect(screen.getByRole('button', { name: 'SAML' })).toHaveAttribute('aria-current', 'false'); - expect(screen.getByRole('button', { name: 'github GitHub' })).toHaveAttribute( - 'aria-current', + expect(screen.getByRole('tab', { name: 'SAML' })).toHaveAttribute('aria-selected', 'false'); + expect(screen.getByRole('tab', { name: 'github GitHub' })).toHaveAttribute( + 'aria-selected', 'true' ); }); diff --git a/server/sonar-web/src/main/js/components/controls/BoxedTabs.tsx b/server/sonar-web/src/main/js/components/controls/BoxedTabs.tsx index 53682deec15..1c5a028558c 100644 --- a/server/sonar-web/src/main/js/components/controls/BoxedTabs.tsx +++ b/server/sonar-web/src/main/js/components/controls/BoxedTabs.tsx @@ -21,7 +21,7 @@ import styled from '@emotion/styled'; import * as React from 'react'; import { colors, sizes } from '../../app/theme'; -export interface BoxedTabsProps<K> { +export interface BoxedTabsProps<K extends string | number> { className?: string; onSelect: (key: K) => void; selected?: K; @@ -76,19 +76,21 @@ const ActiveBorder = styled.div<{ active: boolean }>` top: -1px; `; -export default function BoxedTabs<K>(props: BoxedTabsProps<K>) { +export default function BoxedTabs<K extends string | number>(props: BoxedTabsProps<K>) { const { className, tabs, selected } = props; return ( - <TabContainer className={className}> + <TabContainer className={className} role="tablist"> {tabs.map(({ key, label }, i) => ( <StyledTab + id={getTabId(key)} active={selected === key} - aria-current={selected === key} + aria-selected={selected === key} + aria-controls={getTabPanelId(key)} // eslint-disable-next-line react/no-array-index-key key={i} onClick={() => selected !== key && props.onSelect(key)} - type="button"> + role="tab"> <ActiveBorder active={selected === key} /> {label} </StyledTab> @@ -96,3 +98,11 @@ export default function BoxedTabs<K>(props: BoxedTabsProps<K>) { </TabContainer> ); } + +export function getTabPanelId(key: string | number) { + return `tabpanel-${key}`; +} + +export function getTabId(key: string | number) { + return `tab-${key}`; +} diff --git a/server/sonar-web/src/main/js/components/controls/Tooltip.tsx b/server/sonar-web/src/main/js/components/controls/Tooltip.tsx index 57452118cb6..3103862c93c 100644 --- a/server/sonar-web/src/main/js/components/controls/Tooltip.tsx +++ b/server/sonar-web/src/main/js/components/controls/Tooltip.tsx @@ -286,7 +286,10 @@ export class TooltipInner extends React.Component<TooltipProps, State> { }; handleBlur = () => { - this.setState({ visible: false }); + if (this.mounted) { + this.setState({ visible: false }); + } + if (this.props.onHide) { this.props.onHide(); } diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/BoxedTabs-test.tsx b/server/sonar-web/src/main/js/components/controls/__tests__/BoxedTabs-test.tsx index 746aeb63098..a2cab636bb4 100644 --- a/server/sonar-web/src/main/js/components/controls/__tests__/BoxedTabs-test.tsx +++ b/server/sonar-web/src/main/js/components/controls/__tests__/BoxedTabs-test.tsx @@ -45,7 +45,7 @@ function mountRender(overrides: Partial<BoxedTabsProps<string>> = {}) { return mount(dom(overrides)); } -function dom<K>(overrides: Partial<BoxedTabsProps<K>>) { +function dom(overrides: Partial<BoxedTabsProps<string>>) { return ( <BoxedTabs className="boxed-tabs" diff --git a/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/BoxedTabs-test.tsx.snap b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/BoxedTabs-test.tsx.snap index e5c5524e456..daec2475906 100644 --- a/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/BoxedTabs-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/controls/__tests__/__snapshots__/BoxedTabs-test.tsx.snap @@ -108,6 +108,7 @@ exports[`should render correctly 1`] = ` > <Styled(div) className="boxed-tabs" + role="tablist" > <Insertion cache={ @@ -296,13 +297,16 @@ exports[`should render correctly 1`] = ` /> <div className="boxed-tabs emotion-6" + role="tablist" > <Styled(button) active={true} - aria-current={true} + aria-controls="tabpanel-a" + aria-selected={true} + id="tab-a" key="0" onClick={[Function]} - type="button" + role="tab" > <Insertion cache={ @@ -510,10 +514,12 @@ exports[`should render correctly 1`] = ` } /> <button - aria-current={true} + aria-controls="tabpanel-a" + aria-selected={true} className="emotion-1" + id="tab-a" onClick={[Function]} - type="button" + role="tab" > <Styled(div) active={true} @@ -717,10 +723,12 @@ exports[`should render correctly 1`] = ` </Styled(button)> <Styled(button) active={false} - aria-current={false} + aria-controls="tabpanel-b" + aria-selected={false} + id="tab-b" key="1" onClick={[Function]} - type="button" + role="tab" > <Insertion cache={ @@ -932,10 +940,12 @@ exports[`should render correctly 1`] = ` } /> <button - aria-current={false} + aria-controls="tabpanel-b" + aria-selected={false} className="emotion-3" + id="tab-b" onClick={[Function]} - type="button" + role="tab" > <Styled(div) active={false} @@ -1139,10 +1149,12 @@ exports[`should render correctly 1`] = ` </Styled(button)> <Styled(button) active={false} - aria-current={false} + aria-controls="tabpanel-c" + aria-selected={false} + id="tab-c" key="2" onClick={[Function]} - type="button" + role="tab" > <Insertion cache={ @@ -1354,10 +1366,12 @@ exports[`should render correctly 1`] = ` } /> <button - aria-current={false} + aria-controls="tabpanel-c" + aria-selected={false} className="emotion-3" + id="tab-c" onClick={[Function]} - type="button" + role="tab" > <Styled(div) active={false} diff --git a/server/sonar-web/src/main/js/components/rules/RuleTabViewer.tsx b/server/sonar-web/src/main/js/components/rules/RuleTabViewer.tsx index 4adc421d9e3..35bfee5a846 100644 --- a/server/sonar-web/src/main/js/components/rules/RuleTabViewer.tsx +++ b/server/sonar-web/src/main/js/components/rules/RuleTabViewer.tsx @@ -27,7 +27,7 @@ import { translate } from '../../helpers/l10n'; import { RuleDetails } from '../../types/types'; import { NoticeType } from '../../types/users'; import ScreenPositionHelper from '../common/ScreenPositionHelper'; -import BoxedTabs from '../controls/BoxedTabs'; +import BoxedTabs, { getTabId, getTabPanelId } from '../controls/BoxedTabs'; import MoreInfoRuleDescription from './MoreInfoRuleDescription'; import RuleDescription from './RuleDescription'; import './style.css'; @@ -312,7 +312,10 @@ export class RuleTabViewer extends React.PureComponent<RuleTabViewerProps, State // We substract the footer height with padding (80) and the main layout padding (20) maxHeight: scrollInTab ? `calc(100vh - ${top + 100}px)` : 'initial' }} - className="bordered display-flex-column"> + className="bordered display-flex-column" + role="tabpanel" + aria-labelledby={getTabId(selectedTab.key)} + id={getTabPanelId(selectedTab.key)}> {/* Adding a key to force re-rendering of the tab container, so that it resets the scroll position */} <div className="overflow-y-auto spacer" key={selectedTab.key}> {tabContent} |