From: Wouter Admiraal Date: Thu, 4 Aug 2022 14:36:31 +0000 (+0200) Subject: SONAR-16731 SONAR-16885 [891615] [892423] X-Git-Tag: 9.6.0.59041~72 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=98f9feb78ba9e384649ba3953c8154c50c05e3b7;p=sonarqube.git SONAR-16731 SONAR-16885 [891615] [892423] * [891615] Keyboard focus is lost or misplaced due to user interaction or content update * [892423] Keyboard focus is lost or misplaced due to user interaction or content update --- 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 81ad6fa1d73..71914d5b442 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 @@ -239,7 +239,7 @@ it('should have all type facet', async () => { 'issue.type.VULNERABILITY', 'issue.type.CODE_SMELL', 'issue.type.SECURITY_HOTSPOT' - ].forEach(name => expect(screen.getByRole('link', { name })).toBeInTheDocument()); + ].forEach(name => expect(screen.getByRole('button', { name })).toBeInTheDocument()); }); }); @@ -249,7 +249,7 @@ it('select the correct quality profile for bulk change base on language search', renderCodingRulesApp(mockLoggedInUser()); const selectQP = handler.allQualityProfile('js')[0]; - await user.click(await screen.findByRole('link', { name: 'JavaScript' })); + await user.click(await screen.findByRole('button', { name: 'JavaScript' })); await user.click(await screen.findByRole('button', { name: 'bulk_change' })); await user.click(await screen.findByRole('link', { name: 'coding_rules.activate_in…' })); const dialog = screen.getByRole('dialog', { @@ -266,7 +266,7 @@ it('no quality profile for bulk cahnge base on language search', async () => { handler.setIsAdmin(); renderCodingRulesApp(mockLoggedInUser()); - await user.click(await screen.findByRole('link', { name: 'C' })); + await user.click(await screen.findByRole('button', { name: 'C' })); await user.click(await screen.findByRole('button', { name: 'bulk_change' })); await user.click(await screen.findByRole('link', { name: 'coding_rules.activate_in…' })); const dialog = screen.getByRole('dialog', { diff --git a/server/sonar-web/src/main/js/apps/component-measures/style.css b/server/sonar-web/src/main/js/apps/component-measures/style.css index b4e0cdd5d32..2e7b5b50693 100644 --- a/server/sonar-web/src/main/js/apps/component-measures/style.css +++ b/server/sonar-web/src/main/js/apps/component-measures/style.css @@ -30,6 +30,10 @@ margin-right: -4px; } +button.search-navigator-facet { + text-align: start; +} + .search-navigator-facet .leak-box { height: var(--controlHeight); line-height: var(--controlHeight); 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 4514bf73532..9e4d644cd42 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 @@ -158,8 +158,8 @@ it('should be able to navigate to other issue located in the same file', async ( it('should support OWASP Top 10 version 2021', async () => { const user = userEvent.setup(); renderIssueApp(); - await user.click(await screen.findByRole('link', { name: 'issues.facet.standards' })); - const owaspTop102021 = screen.getByRole('link', { name: 'issues.facet.owaspTop10_2021' }); + await user.click(await screen.findByRole('button', { name: 'issues.facet.standards' })); + const owaspTop102021 = screen.getByRole('button', { name: 'issues.facet.owaspTop10_2021' }); expect(owaspTop102021).toBeInTheDocument(); await user.click(owaspTop102021); @@ -168,7 +168,7 @@ it('should support OWASP Top 10 version 2021', async () => { const standard = await handler.getStandards(); /* eslint-disable-next-line testing-library/render-result-naming-convention */ const linkName = renderOwaspTop102021Category(standard, val); - expect(await screen.findByRole('link', { name: linkName })).toBeInTheDocument(); + expect(await screen.findByRole('button', { name: linkName })).toBeInTheDocument(); }) ); }); @@ -415,7 +415,7 @@ describe('redirects', () => { ); expect( - await screen.findByRole('link', { name: `issue.type.${IssueType.CodeSmell}` }) + await screen.findByRole('button', { name: `issue.type.${IssueType.CodeSmell}` }) ).toBeInTheDocument(); }); }); diff --git a/server/sonar-web/src/main/js/apps/projects/filters/Filter.tsx b/server/sonar-web/src/main/js/apps/projects/filters/Filter.tsx index 1693fd2494f..8ed53ef69e8 100644 --- a/server/sonar-web/src/main/js/apps/projects/filters/Filter.tsx +++ b/server/sonar-web/src/main/js/apps/projects/filters/Filter.tsx @@ -66,25 +66,36 @@ export default class Filter extends React.PureComponent { ); } - handleClick = (event: React.MouseEvent) => { + getUrlOptionForSingleValue = (option: string) => { + return this.isSelected(option) ? null : option; + }; + + getUrlOptionForMultiValue = ( + event: React.MouseEvent, + option: string, + value: Option[] + ) => { + if (event.ctrlKey || event.metaKey) { + if (this.isSelected(option)) { + return value.length > 1 ? value.filter(val => val !== option).join(',') : null; + } + + return value.concat(option).join(','); + } + + return this.isSelected(option) && value.length < 2 ? null : option; + }; + + handleClick = (event: React.MouseEvent) => { event.preventDefault(); - event.currentTarget.blur(); const { property, value } = this.props; const { key: option } = event.currentTarget.dataset; - let urlOption; if (option) { - if (Array.isArray(value) && (event.ctrlKey || event.metaKey)) { - if (this.isSelected(option)) { - urlOption = value.length > 1 ? value.filter(val => val !== option).join(',') : null; - } else { - urlOption = value.concat(option).join(','); - } - } else { - urlOption = - this.isSelected(option) && (!Array.isArray(value) || value.length < 2) ? null : option; - } + const urlOption = Array.isArray(value) + ? this.getUrlOptionForMultiValue(event, option, value) + : this.getUrlOptionForSingleValue(option); this.props.onQueryChange({ [property]: urlOption }); } @@ -110,6 +121,7 @@ export default class Filter extends React.PureComponent { 'facet', 'search-navigator-facet', 'projects-facet', + 'button-link', { active: this.isSelected(option), 'search-navigator-facet-half': this.props.halfWidth @@ -127,11 +139,12 @@ export default class Filter extends React.PureComponent { option > value; return ( - @@ -143,7 +156,7 @@ export default class Filter extends React.PureComponent { {this.renderOptionBar(facetValue)} )} - + ); } diff --git a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/Filter-test.tsx b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/Filter-test.tsx index 14b9a2c55e3..c2f55eed29f 100644 --- a/server/sonar-web/src/main/js/apps/projects/filters/__tests__/Filter-test.tsx +++ b/server/sonar-web/src/main/js/apps/projects/filters/__tests__/Filter-test.tsx @@ -19,6 +19,7 @@ */ import { shallow } from 'enzyme'; import * as React from 'react'; +import { mockEvent } from '../../../../helpers/testUtils'; import Filter from '../Filter'; it('renders', () => { @@ -60,6 +61,69 @@ it('renders facet bar chart', () => { ).toMatchSnapshot(); }); +it('should handle click when value is single', () => { + const onQueryChange = jest.fn(); + const wrapper = shallowRender({ onQueryChange, value: 'option1' }); + + // select + wrapper.instance().handleClick(mockEvent({ currentTarget: { dataset: { key: 'option2' } } })); + expect(onQueryChange).toBeCalledWith({ foo: 'option2' }); + + onQueryChange.mockClear(); + + // deselect + wrapper.instance().handleClick(mockEvent({ currentTarget: { dataset: { key: 'option1' } } })); + expect(onQueryChange).toBeCalledWith({ foo: null }); +}); + +it('should handle click when value is array', () => { + const onQueryChange = jest.fn(); + const wrapper = shallowRender({ onQueryChange, value: ['option1', 'option2'] }); + + // select one + wrapper.instance().handleClick(mockEvent({ currentTarget: { dataset: { key: 'option2' } } })); + expect(onQueryChange).toBeCalledWith({ foo: 'option2' }); + + onQueryChange.mockClear(); + + // select other + wrapper.instance().handleClick(mockEvent({ currentTarget: { dataset: { key: 'option3' } } })); + expect(onQueryChange).toBeCalledWith({ foo: 'option3' }); + + onQueryChange.mockClear(); + + // select additional + wrapper + .instance() + .handleClick(mockEvent({ ctrlKey: true, currentTarget: { dataset: { key: 'option3' } } })); + expect(onQueryChange).toBeCalledWith({ foo: 'option1,option2,option3' }); + + onQueryChange.mockClear(); + + // deselect one + wrapper + .instance() + .handleClick(mockEvent({ metaKey: true, currentTarget: { dataset: { key: 'option2' } } })); + expect(onQueryChange).toBeCalledWith({ foo: 'option1' }); +}); + +it('should handle click when value is array with one value', () => { + const onQueryChange = jest.fn(); + const wrapper = shallowRender({ onQueryChange, value: ['option1'] }); + + // deselect one + wrapper + .instance() + .handleClick(mockEvent({ ctrlKey: true, currentTarget: { dataset: { key: 'option1' } } })); + expect(onQueryChange).toBeCalledWith({ foo: null }); + + onQueryChange.mockClear(); + + // deselect one + wrapper.instance().handleClick(mockEvent({ currentTarget: { dataset: { key: 'option1' } } })); + expect(onQueryChange).toBeCalledWith({ foo: null }); +}); + function shallowRender(overrides: Partial = {}) { return shallow( - 1 - + @@ -66,51 +69,54 @@ exports[`hightlights under selected 1`] = ` @@ -124,48 +130,51 @@ exports[`renders 1`] = ` `; @@ -178,13 +187,14 @@ exports[`renders facet bar chart 1`] = `
-
- - + `; @@ -284,48 +296,51 @@ exports[`renders header and footer 1`] = `