diff options
author | Wouter Admiraal <wouter.admiraal@sonarsource.com> | 2022-08-09 14:23:21 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2022-08-11 20:03:48 +0000 |
commit | 8fb3f5912ee9b1e87431c54982e268eb5bbfc2ce (patch) | |
tree | c836f8efea2ac201b10fab46126e6f2b941d0222 | |
parent | abb4ac07656f900846fc2c62cdc893fe11126ab5 (diff) | |
download | sonarqube-8fb3f5912ee9b1e87431c54982e268eb5bbfc2ce.tar.gz sonarqube-8fb3f5912ee9b1e87431c54982e268eb5bbfc2ce.zip |
SONAR-16782 [893363] Status message not automatically announced
7 files changed, 61 insertions, 133 deletions
diff --git a/server/sonar-web/config/indexHtmlTemplate.js b/server/sonar-web/config/indexHtmlTemplate.js index b212ffc5a23..3c1b6fee989 100644 --- a/server/sonar-web/config/indexHtmlTemplate.js +++ b/server/sonar-web/config/indexHtmlTemplate.js @@ -47,7 +47,7 @@ module.exports = (cssHash, jsHash) => ` <div id="content"> <div class="global-loading"> <i class="spinner global-loading-spinner"></i> - <span class="global-loading-text">Loading...</span> + <span aria-live="polite" class="global-loading-text">Loading...</span> </div> </div> diff --git a/server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.tsx b/server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.tsx index 132f6028c01..a16277dee4b 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.tsx +++ b/server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.tsx @@ -37,6 +37,7 @@ import Tooltip from '../../../components/controls/Tooltip'; import IssueTypeIcon from '../../../components/icons/IssueTypeIcon'; import SeverityHelper from '../../../components/shared/SeverityHelper'; import { Alert } from '../../../components/ui/Alert'; +import DeferredSpinner from '../../../components/ui/DeferredSpinner'; import { throwGlobalError } from '../../../helpers/error'; import { translate, translateWithParameters } from '../../../helpers/l10n'; import { Component, Dict, Issue, IssueType, Paging } from '../../../types/types'; @@ -241,7 +242,11 @@ export default class BulkChangeModal extends React.PureComponent<Props, State> { </div> <div className="modal-body"> <div className="text-center"> - <i className="spinner spacer" /> + <DeferredSpinner + timeout={0} + className="spacer" + ariaLabel={translate('issues.loading_issues')} + /> </div> </div> <div className="modal-foot"> diff --git a/server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx b/server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx index b3c2305d7ad..7f902a2dd3b 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx +++ b/server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx @@ -1124,7 +1124,7 @@ export class App extends React.PureComponent<Props, State> { onIssueChange={this.handleIssueChange} /> ) : ( - <DeferredSpinner loading={loading}> + <DeferredSpinner loading={loading} ariaLabel={translate('issues.loading_issues')}> {checkAll && paging && paging.total > MAX_PAGE_SIZE && ( <Alert className="big-spacer-bottom" variant="warning"> <FormattedMessage diff --git a/server/sonar-web/src/main/js/components/ui/DeferredSpinner.tsx b/server/sonar-web/src/main/js/components/ui/DeferredSpinner.tsx index 4ac68a3137f..6a0cae1c478 100644 --- a/server/sonar-web/src/main/js/components/ui/DeferredSpinner.tsx +++ b/server/sonar-web/src/main/js/components/ui/DeferredSpinner.tsx @@ -22,6 +22,7 @@ import * as React from 'react'; import './DeferredSpinner.css'; interface Props { + ariaLabel?: string; children?: React.ReactNode; className?: string; customSpinner?: JSX.Element; @@ -74,17 +75,27 @@ export default class DeferredSpinner extends React.PureComponent<Props, State> { }; render() { + const { ariaLabel, children, className, customSpinner, placeholder } = this.props; if (this.state.showSpinner) { return ( - this.props.customSpinner || ( - <i className={classNames('deferred-spinner', this.props.className)} /> + customSpinner || ( + <i + className={classNames('deferred-spinner', className)} + aria-live={ariaLabel ? 'polite' : undefined} + aria-label={ariaLabel} + /> ) ); } return ( - this.props.children || - (this.props.placeholder ? ( - <i className={classNames('deferred-spinner-placeholder', this.props.className)} /> + children || + (placeholder ? ( + <i + data-testid="deferred-spinner-placeholder" + className={classNames('deferred-spinner-placeholder', className)} + aria-live={ariaLabel ? 'polite' : undefined} + aria-label={ariaLabel} + /> ) : null) ); } diff --git a/server/sonar-web/src/main/js/components/ui/__tests__/DeferredSpinner-test.tsx b/server/sonar-web/src/main/js/components/ui/__tests__/DeferredSpinner-test.tsx index 0b9f3f64f73..c27f9d1b397 100644 --- a/server/sonar-web/src/main/js/components/ui/__tests__/DeferredSpinner-test.tsx +++ b/server/sonar-web/src/main/js/components/ui/__tests__/DeferredSpinner-test.tsx @@ -17,7 +17,7 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { mount } from 'enzyme'; +import { render, screen } from '@testing-library/react'; import * as React from 'react'; import DeferredSpinner from '../DeferredSpinner'; @@ -25,56 +25,54 @@ beforeAll(() => { jest.useFakeTimers(); }); -afterAll(() => { +afterEach(() => { jest.runOnlyPendingTimers(); +}); + +afterAll(() => { jest.useRealTimers(); }); -it('renders spinner after timeout', () => { - const spinner = mount(<DeferredSpinner />); - expect(spinner).toMatchSnapshot(); +it('renders children before timeout', () => { + renderDeferredSpinner({ children: <a href="#">foo</a> }); + expect(screen.getByRole('link')).toBeInTheDocument(); jest.runAllTimers(); - spinner.update(); - expect(spinner).toMatchSnapshot(); + expect(screen.queryByRole('link')).not.toBeInTheDocument(); }); -it('add custom className', () => { - const spinner = mount(<DeferredSpinner className="foo" />); +it('renders spinner after timeout', () => { + renderDeferredSpinner(); + expect(screen.queryByLabelText('loading')).not.toBeInTheDocument(); jest.runAllTimers(); - spinner.update(); - expect(spinner).toMatchSnapshot(); + expect(screen.getByLabelText('loading')).toBeInTheDocument(); }); -it('renders children before timeout', () => { - const spinner = mount( - <DeferredSpinner> - <div>foo</div> - </DeferredSpinner> - ); - expect(spinner).toMatchSnapshot(); - jest.runAllTimers(); - spinner.update(); - expect(spinner).toMatchSnapshot(); +it('renders a placeholder while waiting', () => { + renderDeferredSpinner({ placeholder: true }); + expect(screen.getByTestId('deferred-spinner-placeholder')).toBeInTheDocument(); }); -it('is controlled by loading prop', () => { - const spinner = mount( - <DeferredSpinner loading={false}> - <div>foo</div> - </DeferredSpinner> - ); - expect(spinner).toMatchSnapshot(); - spinner.setProps({ loading: true }); - expect(spinner).toMatchSnapshot(); +it('allows setting a custom class name', () => { + renderDeferredSpinner({ className: 'foo' }); jest.runAllTimers(); - spinner.update(); - expect(spinner).toMatchSnapshot(); - spinner.setProps({ loading: false }); - spinner.update(); - expect(spinner).toMatchSnapshot(); + expect(screen.getByLabelText('loading')).toHaveClass('foo'); }); -it('renders a placeholder while waiting', () => { - const spinner = mount(<DeferredSpinner placeholder={true} />); - expect(spinner).toMatchSnapshot(); +it('can be controlled by the loading prop', () => { + const { rerender } = renderDeferredSpinner({ loading: true }); + jest.runAllTimers(); + expect(screen.getByLabelText('loading')).toBeInTheDocument(); + + rerender(prepareDeferredSpinner({ loading: false })); + expect(screen.queryByLabelText('loading')).not.toBeInTheDocument(); }); + +function renderDeferredSpinner(props: Partial<DeferredSpinner['props']> = {}) { + // We don't use our renderComponent() helper here, as we have some tests that + // require changes in props. + return render(prepareDeferredSpinner(props)); +} + +function prepareDeferredSpinner(props: Partial<DeferredSpinner['props']> = {}) { + return <DeferredSpinner ariaLabel="loading" {...props} />; +} diff --git a/server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/DeferredSpinner-test.tsx.snap b/server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/DeferredSpinner-test.tsx.snap deleted file mode 100644 index 6822674e7d2..00000000000 --- a/server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/DeferredSpinner-test.tsx.snap +++ /dev/null @@ -1,87 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`add custom className 1`] = ` -<DeferredSpinner - className="foo" -> - <i - className="deferred-spinner foo" - /> -</DeferredSpinner> -`; - -exports[`is controlled by loading prop 1`] = ` -<DeferredSpinner - loading={false} -> - <div> - foo - </div> -</DeferredSpinner> -`; - -exports[`is controlled by loading prop 2`] = ` -<DeferredSpinner - loading={true} -> - <div> - foo - </div> -</DeferredSpinner> -`; - -exports[`is controlled by loading prop 3`] = ` -<DeferredSpinner - loading={true} -> - <i - className="deferred-spinner" - /> -</DeferredSpinner> -`; - -exports[`is controlled by loading prop 4`] = ` -<DeferredSpinner - loading={false} -> - <div> - foo - </div> -</DeferredSpinner> -`; - -exports[`renders a placeholder while waiting 1`] = ` -<DeferredSpinner - placeholder={true} -> - <i - className="deferred-spinner-placeholder" - /> -</DeferredSpinner> -`; - -exports[`renders children before timeout 1`] = ` -<DeferredSpinner> - <div> - foo - </div> -</DeferredSpinner> -`; - -exports[`renders children before timeout 2`] = ` -<DeferredSpinner> - <i - className="deferred-spinner" - /> -</DeferredSpinner> -`; - -exports[`renders spinner after timeout 1`] = `<DeferredSpinner />`; - -exports[`renders spinner after timeout 2`] = ` -<DeferredSpinner> - <i - className="deferred-spinner" - /> -</DeferredSpinner> -`; diff --git a/sonar-core/src/main/resources/org/sonar/l10n/core.properties b/sonar-core/src/main/resources/org/sonar/l10n/core.properties index fbf1f85bb05..b5b4b70602c 100644 --- a/sonar-core/src/main/resources/org/sonar/l10n/core.properties +++ b/sonar-core/src/main/resources/org/sonar/l10n/core.properties @@ -909,6 +909,7 @@ issue.this_issue_involves_x_code_locations=This issue involves {0} code location issue.from_external_rule_engine=Issue detected by an external rule engine: {0} issue.external_issue_description=This is external rule {0}. No details are available. issues.cannot_open_issue_max_initial_X_fetched=Cannot open selected issue, as it's not part of the initial {0} loaded issues. +issues.loading_issues=Loading issues issues.return_to_list=Return to List issues.bulk_change_X_issues=Bulk Change {0} Issue(s) issues.select_all_issues=Select all Issues |