Skip to content

Commit

Permalink
[Search][A11Y] Playground -> Open AI form (#202071)
Browse files Browse the repository at this point in the history
## Summary

refers
#195048 (comment)

The goal is to display a high-level error message at the top of the
form. We don’t need to show specific details but can provide a general
hint, such as `There are some errors in the form`. When using a screen
reader, this message will indicate that the form submission failed due
to errors.

Each individual field will then be responsible for displaying its own
specific error messages.

This approach ensures compliance with A11Y standards.

### PR Screen Record

#### Add


https://github.com/user-attachments/assets/2e6c3304-0ad1-4d84-acc0-f9413e1fd73f

#### Edit


https://github.com/user-attachments/assets/7f54114c-4b95-40cd-bdec-f506d7151674



### Checklist

Check the PR satisfies following conditions. 

Reviewers should verify this PR satisfies this list as well.

- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [X] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

---------

Co-authored-by: Elastic Machine <[email protected]>
  • Loading branch information
Samiul-TheSoccerFan and elasticmachine authored Dec 16, 2024
1 parent d18a44c commit 07c64de
Show file tree
Hide file tree
Showing 4 changed files with 163 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import React, { lazy } from 'react';

import { actionTypeRegistryMock } from '../../../action_type_registry.mock';
import userEvent from '@testing-library/user-event';
import { waitFor, act } from '@testing-library/react';
import { waitFor, act, screen } from '@testing-library/react';
import CreateConnectorFlyout from '.';
import { AppMockRenderer, createAppMockRenderer } from '../../test_utils';
import { TECH_PREVIEW_LABEL } from '../../translations';
Expand Down Expand Up @@ -426,7 +426,7 @@ describe('CreateConnectorFlyout', () => {

describe('Submitting', () => {
it('creates a connector correctly', async () => {
const { getByTestId } = appMockRenderer.render(
const { getByTestId, queryByTestId } = appMockRenderer.render(
<CreateConnectorFlyout
actionTypeRegistry={actionTypeRegistry}
onClose={onClose}
Expand Down Expand Up @@ -469,6 +469,70 @@ describe('CreateConnectorFlyout', () => {
name: 'My test',
secrets: {},
});
expect(queryByTestId('connector-form-header-error-label')).not.toBeInTheDocument();
});

it('show error message in the form header', async () => {
appMockRenderer.render(
<CreateConnectorFlyout
actionTypeRegistry={actionTypeRegistry}
onClose={onClose}
onConnectorCreated={onConnectorCreated}
onTestConnector={onTestConnector}
/>
);

await userEvent.click(await screen.findByTestId(`${actionTypeModel.id}-card`));
expect(await screen.findByTestId('test-connector-text-field')).toBeInTheDocument();

await userEvent.type(
await screen.findByTestId('test-connector-text-field'),
'My text field',
{
delay: 100,
}
);

await userEvent.click(await screen.findByTestId('create-connector-flyout-save-btn'));
expect(onClose).not.toHaveBeenCalled();
expect(onConnectorCreated).not.toHaveBeenCalled();
expect(await screen.findByTestId('connector-form-header-error-label')).toBeInTheDocument();
});

it('removes error message from the form header', async () => {
appMockRenderer.render(
<CreateConnectorFlyout
actionTypeRegistry={actionTypeRegistry}
onClose={onClose}
onConnectorCreated={onConnectorCreated}
onTestConnector={onTestConnector}
/>
);

await userEvent.click(await screen.findByTestId(`${actionTypeModel.id}-card`));
expect(await screen.findByTestId('test-connector-text-field')).toBeInTheDocument();

await userEvent.type(
await screen.findByTestId('test-connector-text-field'),
'My text field',
{
delay: 100,
}
);

await userEvent.click(await screen.findByTestId('create-connector-flyout-save-btn'));
expect(onClose).not.toHaveBeenCalled();
expect(onConnectorCreated).not.toHaveBeenCalled();
expect(await screen.findByTestId('connector-form-header-error-label')).toBeInTheDocument();

await userEvent.type(await screen.findByTestId('nameInput'), 'My test', {
delay: 100,
});

await userEvent.click(await screen.findByTestId('create-connector-flyout-save-btn'));
expect(onClose).toHaveBeenCalled();
expect(onConnectorCreated).toHaveBeenCalled();
expect(screen.queryByTestId('connector-form-header-error-label')).not.toBeInTheDocument();
});

it('runs pre submit validator correctly', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import React, { memo, ReactNode, useCallback, useEffect, useRef, useState } from
import {
EuiButton,
EuiButtonGroup,
EuiCallOut,
EuiFlexGroup,
EuiFlexItem,
EuiFlyout,
Expand All @@ -18,6 +19,7 @@ import {

import { getConnectorCompatibility } from '@kbn/actions-plugin/common';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import {
ActionConnector,
ActionType,
Expand Down Expand Up @@ -60,6 +62,7 @@ const CreateConnectorFlyoutComponent: React.FC<CreateConnectorFlyoutProps> = ({
const [actionType, setActionType] = useState<ActionType | null>(null);
const [hasActionsUpgradeableByTrial, setHasActionsUpgradeableByTrial] = useState<boolean>(false);
const canSave = hasSaveActionsCapability(capabilities);
const [showFormErrors, setShowFormErrors] = useState<boolean>(false);

const [preSubmitValidationErrorMessage, setPreSubmitValidationErrorMessage] =
useState<ReactNode>(null);
Expand Down Expand Up @@ -106,6 +109,7 @@ const CreateConnectorFlyoutComponent: React.FC<CreateConnectorFlyoutProps> = ({

const setResetForm = (reset: ResetForm) => {
resetConnectorForm.current = reset;
setShowFormErrors(false);
};

const onChangeGroupAction = (id: string) => {
Expand All @@ -127,6 +131,7 @@ const CreateConnectorFlyoutComponent: React.FC<CreateConnectorFlyoutProps> = ({

const validateAndCreateConnector = useCallback(async () => {
setPreSubmitValidationErrorMessage(null);
setShowFormErrors(false);

const { isValid, data } = await submit();
if (!isMounted.current) {
Expand Down Expand Up @@ -159,6 +164,8 @@ const CreateConnectorFlyoutComponent: React.FC<CreateConnectorFlyoutProps> = ({

const createdConnector = await createConnector(validConnector);
return createdConnector;
} else {
setShowFormErrors(true);
}
}, [submit, preSubmitValidator, createConnector]);

Expand Down Expand Up @@ -228,6 +235,23 @@ const CreateConnectorFlyoutComponent: React.FC<CreateConnectorFlyoutProps> = ({
<EuiSpacer size="xs" />
</>
)}
{showFormErrors && (
<>
<EuiCallOut
size="s"
color="danger"
iconType="warning"
data-test-subj="connector-form-header-error-label"
title={i18n.translate(
'xpack.triggersActionsUI.sections.actionConnectorAdd.headerFormLabel',
{
defaultMessage: 'There are errors in the form',
}
)}
/>
<EuiSpacer size="m" />
</>
)}
<ConnectorForm
actionTypeModel={actionTypeModel}
connector={initialConnector}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import React, { lazy } from 'react';

import { actionTypeRegistryMock } from '../../../action_type_registry.mock';
import userEvent from '@testing-library/user-event';
import { waitFor, act } from '@testing-library/react';
import { waitFor, act, screen } from '@testing-library/react';
import EditConnectorFlyout from '.';
import { ActionConnector, EditConnectorTabs, GenericValidationResult } from '../../../../types';
import { AppMockRenderer, createAppMockRenderer } from '../../test_utils';
Expand Down Expand Up @@ -415,7 +415,7 @@ describe('EditConnectorFlyout', () => {

describe('Submitting', () => {
it('updates the connector correctly', async () => {
const { getByTestId } = appMockRenderer.render(
const { getByTestId, queryByTestId } = appMockRenderer.render(
<EditConnectorFlyout
actionTypeRegistry={actionTypeRegistry}
onClose={onClose}
Expand Down Expand Up @@ -459,6 +459,7 @@ describe('EditConnectorFlyout', () => {
name: 'My test',
secrets: {},
});
expect(queryByTestId('connector-form-header-error-label')).not.toBeInTheDocument();
});

it('updates connector form field with latest value', async () => {
Expand Down Expand Up @@ -555,6 +556,39 @@ describe('EditConnectorFlyout', () => {
});
});

it('show error message in the form header', async () => {
appMockRenderer.render(
<EditConnectorFlyout
actionTypeRegistry={actionTypeRegistry}
onClose={onClose}
connector={connector}
onConnectorUpdated={onConnectorUpdated}
/>
);

expect(await screen.findByTestId('test-connector-text-field')).toBeInTheDocument();
await userEvent.clear(screen.getByTestId('nameInput'));
await userEvent.click(screen.getByTestId('edit-connector-flyout-save-btn'));
expect(await screen.findByTestId('connector-form-header-error-label')).toBeInTheDocument();
});

it('removes error message from the form header', async () => {
appMockRenderer.render(
<EditConnectorFlyout
actionTypeRegistry={actionTypeRegistry}
onClose={onClose}
connector={connector}
onConnectorUpdated={onConnectorUpdated}
/>
);

await userEvent.clear(screen.getByTestId('nameInput'));
await userEvent.type(screen.getByTestId('nameInput'), 'My new name');
await userEvent.type(screen.getByTestId('test-connector-secret-text-field'), 'password');
await userEvent.click(screen.getByTestId('edit-connector-flyout-save-btn'));
expect(screen.queryByTestId('connector-form-header-error-label')).not.toBeInTheDocument();
});

it('runs pre submit validator correctly', async () => {
const errorActionTypeModel = actionTypeRegistryMock.createMockActionTypeModel({
actionConnectorFields: lazy(() => import('../connector_error_mock')),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,14 @@
*/

import React, { memo, ReactNode, useCallback, useEffect, useRef, useState } from 'react';
import { EuiFlyout, EuiFlyoutBody, EuiButton, EuiConfirmModal } from '@elastic/eui';
import {
EuiFlyout,
EuiFlyoutBody,
EuiButton,
EuiConfirmModal,
EuiCallOut,
EuiSpacer,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import { ActionTypeExecutorResult, isActionTypeExecutorResult } from '@kbn/actions-plugin/common';
Expand Down Expand Up @@ -62,6 +69,7 @@ const EditConnectorFlyoutComponent: React.FC<EditConnectorFlyoutProps> = ({
const canSave = hasSaveActionsCapability(capabilities);
const { isLoading: isUpdatingConnector, updateConnector } = useUpdateConnector();
const { isLoading: isExecutingConnector, executeConnector } = useExecuteConnector();
const [showFormErrors, setShowFormErrors] = useState<boolean>(false);

const [preSubmitValidationErrorMessage, setPreSubmitValidationErrorMessage] =
useState<ReactNode>(null);
Expand Down Expand Up @@ -90,6 +98,7 @@ const EditConnectorFlyoutComponent: React.FC<EditConnectorFlyoutProps> = ({
if (nextPage === EditConnectorTabs.Configuration && testExecutionResult !== none) {
setTestExecutionResult(none);
}
setShowFormErrors(false);
setTab(nextPage);
},
[testExecutionResult, setTestExecutionResult]
Expand Down Expand Up @@ -146,6 +155,7 @@ const EditConnectorFlyoutComponent: React.FC<EditConnectorFlyoutProps> = ({

const onClickSave = useCallback(async () => {
setPreSubmitValidationErrorMessage(null);
setShowFormErrors(false);

const { isValid, data } = await submit();
if (!isMounted.current) {
Expand Down Expand Up @@ -194,6 +204,8 @@ const EditConnectorFlyoutComponent: React.FC<EditConnectorFlyoutProps> = ({
}

return updatedConnector;
} else {
setShowFormErrors(true);
}
}, [
onConnectorUpdated,
Expand All @@ -218,6 +230,23 @@ const EditConnectorFlyoutComponent: React.FC<EditConnectorFlyoutProps> = ({
<>
{isEdit && (
<>
{showFormErrors && (
<>
<EuiCallOut
size="s"
color="danger"
iconType="warning"
data-test-subj="connector-form-header-error-label"
title={i18n.translate(
'xpack.triggersActionsUI.sections.editConnectorForm.headerFormLabel',
{
defaultMessage: 'There are errors in the form',
}
)}
/>
<EuiSpacer size="m" />
</>
)}
<ConnectorForm
actionTypeModel={actionTypeModel}
connector={getConnectorWithoutSecrets(connector)}
Expand Down Expand Up @@ -265,17 +294,18 @@ const EditConnectorFlyoutComponent: React.FC<EditConnectorFlyoutProps> = ({
);
}, [
connector,
docLinks.links.alerting.preconfiguredConnectors,
actionTypeModel,
isEdit,
docLinks.links.alerting.preconfiguredConnectors,
hasErrors,
isFormModified,
isSaved,
isSaving,
showFormErrors,
onFormModifiedChange,
preSubmitValidationErrorMessage,
showButtons,
isSaved,
isSaving,
onClickSave,
onFormModifiedChange,
isFormModified,
hasErrors,
]);

const renderTestTab = useCallback(() => {
Expand Down

0 comments on commit 07c64de

Please sign in to comment.