From d4de8052eaba77beeeeb71d11f045a8cd56a7914 Mon Sep 17 00:00:00 2001 From: Samiul Monir <150824886+Samiul-TheSoccerFan@users.noreply.github.com> Date: Mon, 16 Dec 2024 10:49:37 -0500 Subject: [PATCH] [Search][A11Y] Playground -> Open AI form (#202071) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary refers https://github.com/elastic/kibana/issues/195048#issuecomment-2393788073 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 (cherry picked from commit 07c64de7ef08ece1b2710fa54fbeb29460ec809d) --- .../create_connector_flyout/index.test.tsx | 68 ++++++++++++++++++- .../create_connector_flyout/index.tsx | 24 +++++++ .../edit_connector_flyout/index.test.tsx | 38 ++++++++++- .../edit_connector_flyout/index.tsx | 44 ++++++++++-- 4 files changed, 163 insertions(+), 11 deletions(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/index.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/index.test.tsx index df52b1729bb8d..1c5008d4e1dde 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/index.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/index.test.tsx @@ -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'; @@ -426,7 +426,7 @@ describe('CreateConnectorFlyout', () => { describe('Submitting', () => { it('creates a connector correctly', async () => { - const { getByTestId } = appMockRenderer.render( + const { getByTestId, queryByTestId } = appMockRenderer.render( { name: 'My test', secrets: {}, }); + expect(queryByTestId('connector-form-header-error-label')).not.toBeInTheDocument(); + }); + + it('show error message in the form header', async () => { + appMockRenderer.render( + + ); + + 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( + + ); + + 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 () => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/index.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/index.tsx index b93d2815bf197..c341f861b5ead 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/index.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/create_connector_flyout/index.tsx @@ -9,6 +9,7 @@ import React, { memo, ReactNode, useCallback, useEffect, useRef, useState } from import { EuiButton, EuiButtonGroup, + EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiFlyout, @@ -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, @@ -60,6 +62,7 @@ const CreateConnectorFlyoutComponent: React.FC = ({ const [actionType, setActionType] = useState(null); const [hasActionsUpgradeableByTrial, setHasActionsUpgradeableByTrial] = useState(false); const canSave = hasSaveActionsCapability(capabilities); + const [showFormErrors, setShowFormErrors] = useState(false); const [preSubmitValidationErrorMessage, setPreSubmitValidationErrorMessage] = useState(null); @@ -106,6 +109,7 @@ const CreateConnectorFlyoutComponent: React.FC = ({ const setResetForm = (reset: ResetForm) => { resetConnectorForm.current = reset; + setShowFormErrors(false); }; const onChangeGroupAction = (id: string) => { @@ -127,6 +131,7 @@ const CreateConnectorFlyoutComponent: React.FC = ({ const validateAndCreateConnector = useCallback(async () => { setPreSubmitValidationErrorMessage(null); + setShowFormErrors(false); const { isValid, data } = await submit(); if (!isMounted.current) { @@ -159,6 +164,8 @@ const CreateConnectorFlyoutComponent: React.FC = ({ const createdConnector = await createConnector(validConnector); return createdConnector; + } else { + setShowFormErrors(true); } }, [submit, preSubmitValidator, createConnector]); @@ -228,6 +235,23 @@ const CreateConnectorFlyoutComponent: React.FC = ({ )} + {showFormErrors && ( + <> + + + + )} { describe('Submitting', () => { it('updates the connector correctly', async () => { - const { getByTestId } = appMockRenderer.render( + const { getByTestId, queryByTestId } = appMockRenderer.render( { name: 'My test', secrets: {}, }); + expect(queryByTestId('connector-form-header-error-label')).not.toBeInTheDocument(); }); it('updates connector form field with latest value', async () => { @@ -555,6 +556,39 @@ describe('EditConnectorFlyout', () => { }); }); + it('show error message in the form header', async () => { + appMockRenderer.render( + + ); + + 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( + + ); + + 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')), diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/edit_connector_flyout/index.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/edit_connector_flyout/index.tsx index e787f3eac42bf..1188f06a87d56 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/edit_connector_flyout/index.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/edit_connector_flyout/index.tsx @@ -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'; @@ -62,6 +69,7 @@ const EditConnectorFlyoutComponent: React.FC = ({ const canSave = hasSaveActionsCapability(capabilities); const { isLoading: isUpdatingConnector, updateConnector } = useUpdateConnector(); const { isLoading: isExecutingConnector, executeConnector } = useExecuteConnector(); + const [showFormErrors, setShowFormErrors] = useState(false); const [preSubmitValidationErrorMessage, setPreSubmitValidationErrorMessage] = useState(null); @@ -90,6 +98,7 @@ const EditConnectorFlyoutComponent: React.FC = ({ if (nextPage === EditConnectorTabs.Configuration && testExecutionResult !== none) { setTestExecutionResult(none); } + setShowFormErrors(false); setTab(nextPage); }, [testExecutionResult, setTestExecutionResult] @@ -146,6 +155,7 @@ const EditConnectorFlyoutComponent: React.FC = ({ const onClickSave = useCallback(async () => { setPreSubmitValidationErrorMessage(null); + setShowFormErrors(false); const { isValid, data } = await submit(); if (!isMounted.current) { @@ -194,6 +204,8 @@ const EditConnectorFlyoutComponent: React.FC = ({ } return updatedConnector; + } else { + setShowFormErrors(true); } }, [ onConnectorUpdated, @@ -218,6 +230,23 @@ const EditConnectorFlyoutComponent: React.FC = ({ <> {isEdit && ( <> + {showFormErrors && ( + <> + + + + )} = ({ ); }, [ 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(() => {