aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorWouter Admiraal <wouter.admiraal@sonarsource.com>2022-08-09 14:23:21 +0200
committersonartech <sonartech@sonarsource.com>2022-08-11 20:03:48 +0000
commit8fb3f5912ee9b1e87431c54982e268eb5bbfc2ce (patch)
treec836f8efea2ac201b10fab46126e6f2b941d0222
parentabb4ac07656f900846fc2c62cdc893fe11126ab5 (diff)
downloadsonarqube-8fb3f5912ee9b1e87431c54982e268eb5bbfc2ce.tar.gz
sonarqube-8fb3f5912ee9b1e87431c54982e268eb5bbfc2ce.zip
SONAR-16782 [893363] Status message not automatically announced
-rw-r--r--server/sonar-web/config/indexHtmlTemplate.js2
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/BulkChangeModal.tsx7
-rw-r--r--server/sonar-web/src/main/js/apps/issues/components/IssuesApp.tsx2
-rw-r--r--server/sonar-web/src/main/js/components/ui/DeferredSpinner.tsx21
-rw-r--r--server/sonar-web/src/main/js/components/ui/__tests__/DeferredSpinner-test.tsx74
-rw-r--r--server/sonar-web/src/main/js/components/ui/__tests__/__snapshots__/DeferredSpinner-test.tsx.snap87
-rw-r--r--sonar-core/src/main/resources/org/sonar/l10n/core.properties1
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