Fix a11y for remote clusters managment form validation errors (#39656)

Co-authored-by: Filipp Baranovskii <filipp_baranovskii@epam.com>
Co-authored-by: Michail Yasonik <michail.yasonik@elastic.co>
Co-authored-by: Alexey Antonov <alexwizp@gmail.com>
This commit is contained in:
Philipp B 2019-07-18 18:03:20 +03:00 committed by Michail Yasonik
parent 48e007aa07
commit 9a5afa06b2
3 changed files with 97 additions and 54 deletions

View file

@ -17,12 +17,12 @@ Array [
<div
class="euiFlexItem"
>
<h4
<h2
class="euiTitle euiTitle--small euiTitle euiTitle--xsmall euiDescribedFormGroup__title"
id="mockId-title"
>
Name
</h4>
</h2>
<div
class="euiText euiText--small euiDescribedFormGroup__description"
id="mockId"
@ -89,12 +89,12 @@ Array [
<div
class="euiFlexItem"
>
<h4
<h2
class="euiTitle euiTitle--small euiTitle euiTitle--xsmall euiDescribedFormGroup__title"
id="mockId-title"
>
Seed nodes for cluster discovery
</h4>
</h2>
<div
class="euiText euiText--small euiDescribedFormGroup__description"
id="mockId"
@ -102,9 +102,7 @@ Array [
<div
class="euiTextColor euiTextColor--subdued"
>
<p>
A list of remote cluster nodes to query for the cluster state. Specify multiple seed nodes so discovery doesn't fail if a node is unavailable.
</p>
A list of remote cluster nodes to query for the cluster state. Specify multiple seed nodes so discovery doesn't fail if a node is unavailable.
</div>
</div>
</div>
@ -197,12 +195,12 @@ Array [
<div
class="euiFlexItem"
>
<h4
<h2
class="euiTitle euiTitle--small euiTitle euiTitle--xsmall euiDescribedFormGroup__title"
id="mockId-title"
>
Make remote cluster optional
</h4>
</h2>
<div
class="euiText euiText--small euiDescribedFormGroup__description"
id="mockId"
@ -292,6 +290,7 @@ Array [
class="euiFlexItem euiFlexItem--flexGrowZero"
>
<button
aria-describedby="staticGenerator_removeClustersErrorTitle staticGenerator_removeClustersErrorList"
class="euiButton euiButton--secondary euiButton--fill"
data-test-subj="remoteClusterFormSaveButton"
type="button"
@ -494,27 +493,8 @@ Array [
</div>
</div>,
<div
class="euiCallOut euiCallOut--danger"
class="euiSpacer euiSpacer--m"
data-test-subj="remoteClusterFormGlobalError"
>
<div
class="euiCallOutHeader"
>
<svg
aria-hidden="true"
class="euiIcon euiIcon--medium euiIcon-isLoading euiCallOutHeader__icon"
focusable="false"
height="16"
viewBox="0 0 16 16"
width="16"
xmlns="http://www.w3.org/2000/svg"
/>
<span
class="euiCallOutHeader__title"
>
Fix errors before continuing.
</span>
</div>
</div>,
/>,
]
`;

View file

@ -9,7 +9,6 @@ import PropTypes from 'prop-types';
import { merge } from 'lodash';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import {
EuiButton,
EuiButtonEmpty,
@ -29,6 +28,9 @@ import {
EuiSwitch,
EuiText,
EuiTitle,
EuiDelayRender,
EuiScreenReaderOnly,
htmlIdGenerator,
} from '@elastic/eui';
import {
@ -44,6 +46,9 @@ const defaultFields = {
skipUnavailable: false,
};
const ERROR_TITLE_ID = 'removeClustersErrorTitle';
const ERROR_LIST_ID = 'removeClustersErrorList';
export class RemoteClusterForm extends Component {
static propTypes = {
save: PropTypes.func.isRequired,
@ -65,6 +70,7 @@ export class RemoteClusterForm extends Component {
const { fields, disabledFields } = props;
const fieldsState = merge({}, defaultFields, fields);
this.generateId = htmlIdGenerator();
this.state = {
localSeedErrors: [],
seedInput: '',
@ -232,24 +238,20 @@ export class RemoteClusterForm extends Component {
<EuiDescribedFormGroup
title={(
<EuiTitle size="s">
<h4>
<h2>
<FormattedMessage
id="xpack.remoteClusters.remoteClusterForm.sectionSeedsTitle"
defaultMessage="Seed nodes for cluster discovery"
/>
</h4>
</h2>
</EuiTitle>
)}
description={(
<Fragment>
<p>
<FormattedMessage
id="xpack.remoteClusters.remoteClusterForm.sectionSeedsDescription1"
defaultMessage="A list of remote cluster nodes to query for the cluster state.
Specify multiple seed nodes so discovery doesn't fail if a node is unavailable."
/>
</p>
</Fragment>
<FormattedMessage
id="xpack.remoteClusters.remoteClusterForm.sectionSeedsDescription1"
defaultMessage="A list of remote cluster nodes to query for the cluster state.
Specify multiple seed nodes so discovery doesn't fail if a node is unavailable."
/>
)}
fullWidth
>
@ -312,12 +314,12 @@ export class RemoteClusterForm extends Component {
<EuiDescribedFormGroup
title={(
<EuiTitle size="s">
<h4>
<h2>
<FormattedMessage
id="xpack.remoteClusters.remoteClusterForm.sectionSkipUnavailableTitle"
defaultMessage="Make remote cluster optional"
/>
</h4>
</h2>
</EuiTitle>
)}
description={(
@ -434,6 +436,7 @@ export class RemoteClusterForm extends Component {
onClick={this.save}
fill
disabled={isSaveDisabled}
aria-describedby={`${this.generateId(ERROR_TITLE_ID)} ${this.generateId(ERROR_LIST_ID)}`}
>
<FormattedMessage
id="xpack.remoteClusters.remoteClusterForm.saveButtonLabel"
@ -486,7 +489,7 @@ export class RemoteClusterForm extends Component {
<EuiCallOut
title={message}
icon="cross"
color="danger"
color="warning"
>
{errorBody}
</EuiCallOut>
@ -500,27 +503,84 @@ export class RemoteClusterForm extends Component {
}
renderErrors = () => {
const { areErrorsVisible } = this.state;
const {
areErrorsVisible,
fieldsErrors: {
name: errorClusterName,
seeds: errorsSeeds,
},
localSeedErrors,
} = this.state;
const hasErrors = this.hasErrors();
if (!areErrorsVisible || !hasErrors) {
return null;
}
const errorExplanations = [];
if (errorClusterName) {
errorExplanations.push({
key: 'nameExplanation',
field: i18n.translate('xpack.remoteClusters.remoteClusterForm.inputNameErrorMessage', {
defaultMessage: 'The "Name" field is invalid.',
}),
error: errorClusterName
});
}
if (errorsSeeds) {
errorExplanations.push({
key: 'seedsExplanation',
field: i18n.translate('xpack.remoteClusters.remoteClusterForm.inputSeedsErrorMessage', {
defaultMessage: 'The "Seed nodes" field is invalid.',
}),
error: errorsSeeds
});
}
if (localSeedErrors && localSeedErrors.length) {
errorExplanations.push({
key: 'localSeedExplanation',
field: i18n.translate('xpack.remoteClusters.remoteClusterForm.inputLocalSeedErrorMessage', {
defaultMessage: 'The "Seed nodes" field is invalid.',
}),
error: localSeedErrors.join(' '),
});
}
const messagesToBeRendered = errorExplanations.length && (
<EuiScreenReaderOnly>
<dl id={this.generateId(ERROR_LIST_ID)} aria-labelledby={this.generateId(ERROR_TITLE_ID)}>
{errorExplanations.map(({ key, field, error }) => (
<div key={key}>
<dt>{field}</dt>
<dd>{error}</dd>
</div>
))}
</dl>
</EuiScreenReaderOnly>
);
return (
<Fragment>
<EuiSpacer size="m" />
<EuiSpacer size="m" data-test-subj="remoteClusterFormGlobalError" />
<EuiCallOut
data-test-subj="remoteClusterFormGlobalError"
title={(
<FormattedMessage
id="xpack.remoteClusters.remoteClusterForm.errorTitle"
defaultMessage="Fix errors before continuing."
/>
<h3 id={this.generateId(ERROR_TITLE_ID)}>
<FormattedMessage
id="xpack.remoteClusters.remoteClusterForm.errorTitle"
defaultMessage="Fix errors before continuing."
/>
</h3>
)}
color="danger"
iconType="cross"
/>
<EuiDelayRender>
{messagesToBeRendered}
</EuiDelayRender>
</Fragment>
);
}
@ -550,12 +610,12 @@ export class RemoteClusterForm extends Component {
<EuiDescribedFormGroup
title={(
<EuiTitle size="s">
<h4>
<h2>
<FormattedMessage
id="xpack.remoteClusters.remoteClusterForm.sectionNameTitle"
defaultMessage="Name"
/>
</h4>
</h2>
</EuiTitle>
)}
description={(

View file

@ -11,6 +11,9 @@ import { RemoteClusterForm } from './remote_cluster_form';
// Make sure we have deterministic aria IDs.
jest.mock('@elastic/eui/lib/components/form/form_row/make_id', () => () => 'mockId');
jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({
htmlIdGenerator: (prefix = 'staticGenerator') => (suffix = 'staticId') => `${prefix}_${suffix}`
}));
describe('RemoteClusterForm', () => {
test(`renders untouched state`, () => {