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;
+}