diff --git a/pages/wizard/native-form-submit.page.tsx b/pages/wizard/native-form-submit.page.tsx new file mode 100644 index 0000000000..bdfb3320a1 --- /dev/null +++ b/pages/wizard/native-form-submit.page.tsx @@ -0,0 +1,82 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React, { useState } from 'react'; + +import { Container, FormField, Header, Input, Wizard, WizardProps } from '~components'; + +import { i18nStrings } from './common'; + +export default function WizardPage() { + const [activeStepIndex, setActiveStepIndex] = useState(0); + const [value1, setValue1] = useState(''); + const [value2, setValue2] = useState(''); + const [attemptedToSubmit, setAttemptedToSubmit] = useState(false); + const [resultText, setResultText] = useState(''); + + const steps: WizardProps['steps'] = [ + { + title: 'Step 1', + content: ( + A container for step 1}> + + setValue1(e.detail.value)} /> + + + ), + }, + { + title: 'Step 2', + content: ( + A container for step 2}> + + setValue2(e.detail.value)} /> + + + ), + }, + ]; + + return ( + <> +
{resultText}
+ { + setAttemptedToSubmit(true); + if (value1.length < 1) { + return; + } + setAttemptedToSubmit(false); + setActiveStepIndex(e.detail.requestedStepIndex); + setResultText( + `Navigate action was called. Starting index: ${activeStepIndex}. Ending index: ${e.detail.requestedStepIndex}` + ); + }} + onSubmit={() => { + setAttemptedToSubmit(true); + if (value2.length < 1) { + return; + } + setResultText('Submit action was called.'); + }} + onCancel={() => { + setResultText('Cancel action was called.'); + }} + /> + + ); +} diff --git a/src/__tests__/functional-tests/outer-form-submit.test.tsx b/src/__tests__/functional-tests/outer-form-submit.test.tsx index 11d19f6d84..af4afe7833 100644 --- a/src/__tests__/functional-tests/outer-form-submit.test.tsx +++ b/src/__tests__/functional-tests/outer-form-submit.test.tsx @@ -19,7 +19,7 @@ afterEach(() => { clearVisualRefreshState(); }); -const skippedComponents = ['button']; +const skippedComponents = ['button', 'wizard']; describe('Check outer form submission', () => { getAllComponents() diff --git a/src/wizard/__integ__/wizard.test.ts b/src/wizard/__integ__/wizard.test.ts index ad8aaafabf..020bd26570 100644 --- a/src/wizard/__integ__/wizard.test.ts +++ b/src/wizard/__integ__/wizard.test.ts @@ -20,11 +20,23 @@ class WizardPageObject extends BasePageObject { toggleScrollableContainer() { return this.click(createWrapper().findToggle().findNativeInput().toSelector()); } + getInput(selector?: string) { + return wizardWrapper.findContent().findInput(selector); + } + getInputSelector(selector?: string) { + return wizardWrapper.findContent().findInput(selector).findNativeInput().toSelector(); + } + getFormFieldSelector(selector?: string) { + return wizardWrapper.findContent().findFormField(selector).toSelector(); + } + getFormFieldErrorSelector(selector?: string) { + return wizardWrapper.findContent().findFormField(selector).findError().toSelector(); + } } -function setupTest(testFn: (page: WizardPageObject) => Promise) { +function setupTest(testFn: (page: WizardPageObject) => Promise, url?: string) { return useBrowser(async browser => { - await browser.url('/#/light/wizard/simple?visualRefresh=false'); + await browser.url(url ?? '/#/light/wizard/simple?visualRefresh=false'); const page = new WizardPageObject(browser); await page.waitForVisible(wizardWrapper.findPrimaryButton().toSelector()); await testFn(page); @@ -32,6 +44,60 @@ function setupTest(testFn: (page: WizardPageObject) => Promise) { } describe('Wizard keyboard navigation', () => { + test( + 'calls user defined form validation from onNavigate on input element on Enter key', + setupTest(async page => { + const firstNameInput = page.getInputSelector('[data-testid="first-name-input"]'); + + await page.click(firstNameInput); + await page.keys(['Enter']); + + const errorText = page.getFormFieldErrorSelector('[data-testid="first-name-form-field"]'); + await expect(page.getText(errorText)).resolves.toContain('This field cannot be left blank.'); + }, '/#/light/wizard/native-form-submit') + ); + + test( + 'navigates to next step on non-last step on input element on Enter key', + setupTest(async page => { + const firstNameInput = page.getInputSelector('[data-testid="first-name-input"]'); + + await page.click(firstNameInput); + await page.keys(['MyFirstName']); + await page.keys(['Enter']); + + await expect(page.getText(`[data-testid="result-text"]`)).resolves.toContain( + 'Navigate action was called. Starting index: 0. Ending index: 1' + ); + }, '/#/light/wizard/native-form-submit') + ); + + test( + 'invokes submit action on last step on input element on Enter key w/ user validation', + setupTest(async page => { + const firstNameInput = page.getInputSelector('[data-testid="first-name-input"]'); + + await page.click(firstNameInput); + await page.keys(['MyFirstName']); + await page.keys(['Enter']); + + const lastNameInput = page.getInputSelector('[data-testid="last-name-input"]'); + await page.click(lastNameInput); + await page.keys(['Enter']); + + const errorText = page.getFormFieldErrorSelector('[data-testid="last-name-form-field"]'); + await expect(page.getText(errorText)).resolves.toContain('This field cannot be left blank.'); + + await expect(page.getText(`[data-testid="result-text"]`)).resolves.not.toContain('Submit action was called.'); + + await page.click(lastNameInput); + await page.keys(['MyLastName']); + await page.keys(['Enter']); + + await expect(page.getText(`[data-testid="result-text"]`)).resolves.toContain('Submit action was called.'); + }, '/#/light/wizard/native-form-submit') + ); + test( 'navigate to the first step from menu navigation link using the Enter key', setupTest(async page => { diff --git a/src/wizard/__tests__/wizard.test.tsx b/src/wizard/__tests__/wizard.test.tsx index 5f7e43a54b..66406329b1 100644 --- a/src/wizard/__tests__/wizard.test.tsx +++ b/src/wizard/__tests__/wizard.test.tsx @@ -1,10 +1,11 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 import * as React from 'react'; -import { render } from '@testing-library/react'; +import { fireEvent, render } from '@testing-library/react'; import Button from '../../../lib/components/button'; import TestI18nProvider from '../../../lib/components/i18n/testing'; +import Input from '../../../lib/components/input'; import createWrapper from '../../../lib/components/test-utils/dom'; import WizardWrapper from '../../../lib/components/test-utils/dom/wizard'; import Wizard, { WizardProps } from '../../../lib/components/wizard'; @@ -556,3 +557,54 @@ describe('i18n', () => { expect(wrapper.findPreviousButton()!.getElement()).toHaveTextContent('Custom previous'); }); }); + +describe('Native form submit button', () => { + const onChange = jest.fn(); + + const steps = [ + { + title: 'Step 1', + isOptional: false, + content: , + }, + { + title: 'Step 2', + isOptional: false, + content: , + }, + { + title: 'Step 3', + isOptional: false, + content: , + }, + ]; + + test('invokes onNavigate function on non-last step', () => { + const onNavigate = jest.fn(); + const onSubmit = jest.fn(); + + const [wrapper] = renderDefaultWizard({ activeStepIndex: 0, steps, allowSkipTo: false, onNavigate, onSubmit }); + const inputStep1 = wrapper.findContent()?.findInput()?.getElement() as HTMLElement; + + fireEvent.submit(inputStep1); + + expect(onNavigate).toHaveBeenCalledTimes(1); + expect(onNavigate).toHaveBeenCalledWith( + expect.objectContaining({ detail: { requestedStepIndex: 1, reason: 'next' } }) + ); + expect(onSubmit).toHaveBeenCalledTimes(0); + }); + + test('invokes onSubmit function on last step', () => { + const onNavigate = jest.fn(); + const onSubmit = jest.fn(); + + const [wrapper] = renderDefaultWizard({ activeStepIndex: 2, steps, allowSkipTo: false, onNavigate, onSubmit }); + const inputStep3 = wrapper.findContent()?.findInput()?.getElement() as HTMLElement; + + fireEvent.submit(inputStep3); + + expect(onNavigate).toHaveBeenCalledTimes(0); + expect(onSubmit).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/wizard/internal.tsx b/src/wizard/internal.tsx index fb82f3b807..9f3ce02107 100644 --- a/src/wizard/internal.tsx +++ b/src/wizard/internal.tsx @@ -1,6 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import React, { useRef } from 'react'; +import React, { FormEvent, useRef } from 'react'; import clsx from 'clsx'; import { useMergeRefs, warnOnce } from '@cloudscape-design/component-toolkit/internal'; @@ -166,6 +166,11 @@ export default function InternalWizard({ }, }; + const handleNativeFormOnSubmit = (event: FormEvent) => { + event.preventDefault(); + onPrimaryClick(); + }; + return (
- +
+ + +
diff --git a/src/wizard/styles.scss b/src/wizard/styles.scss index d5f2341412..ba7d641855 100644 --- a/src/wizard/styles.scss +++ b/src/wizard/styles.scss @@ -304,3 +304,7 @@ display: flex; justify-content: flex-end; } + +.wizard-hidden-form-submit-button { + display: none; +}