diff --git a/pages/wizard/custom-primary-actions.page.tsx b/pages/wizard/custom-primary-actions.page.tsx new file mode 100644 index 0000000000..01810643fb --- /dev/null +++ b/pages/wizard/custom-primary-actions.page.tsx @@ -0,0 +1,96 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React, { useState } from 'react'; + +import { SpaceBetween } from '~components'; +import Button from '~components/button'; +import Container from '~components/container'; +import Header from '~components/header'; +import Wizard, { WizardProps } from '~components/wizard'; + +import { i18nStrings } from './common'; + +const steps: WizardProps.Step[] = [ + { + title: 'Step 1', + content: ( + <> + Step 1, substep one}> + Step 1, substep two}> + + ), + }, + { + title: 'Step 2', + content: ( + <> + Step 2, substep one}> + Step 2, substep two}> + + ), + isOptional: true, + }, + { + title: 'Step 3', + content: ( + <> + Step 3, substep one}> + Step 3, substep two}> + + ), + }, +]; + +export default function WizardPage() { + const [activeStepIndex, setActiveStepIndex] = useState(0); + + const onNext = () => { + if (activeStepIndex >= steps.length) { + return; + } + setActiveStepIndex(activeStepIndex + 1); + }; + + const onPrevious = () => { + if (activeStepIndex <= 0) { + return; + } + setActiveStepIndex(activeStepIndex - 1); + }; + + const onFinish = () => { + alert('Finish'); + }; + + const customPrimaryActions = ( + + {activeStepIndex > 0 && ( + + )} + {activeStepIndex < steps.length - 1 && ( + + )} + {activeStepIndex === steps.length - 1 && ( + + )} + + ); + + return ( + setActiveStepIndex(e.detail.requestedStepIndex)} + secondaryActions={activeStepIndex === 2 ? : null} + customPrimaryActions={customPrimaryActions} + /> + ); +} diff --git a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap index f4a10fff93..93e3d26acd 100644 --- a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap +++ b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap @@ -28027,6 +28027,14 @@ Use this if you need to wait for a response from the server before the user can }, ], "regions": [ + { + "description": "Specifies right-aligned custom primary actions for the wizard. Overwrites existing buttons (e.g. Cancel, Next, Finish).", + "isDefault": false, + "name": "customPrimaryActions", + "systemTags": [ + "core", + ], + }, { "description": "Specifies left-aligned secondary actions for the wizard. Use a button dropdown if multiple actions are required.", "isDefault": false, diff --git a/src/wizard/__tests__/wizard.test.tsx b/src/wizard/__tests__/wizard.test.tsx index 5f7e43a54b..2e2c7ddbbc 100644 --- a/src/wizard/__tests__/wizard.test.tsx +++ b/src/wizard/__tests__/wizard.test.tsx @@ -515,6 +515,110 @@ describe('Custom actions', () => { }); }); +describe('Custom primary actions', () => { + test('renders custom primary actions instead of default buttons', () => { + const customActions = ( + <> + + + + ); + const [wrapper] = renderDefaultWizard({ customPrimaryActions: customActions }); + + expect(wrapper.findPrimaryButton()).toBeNull(); + expect(wrapper.findCancelButton()).toBeNull(); + expect(wrapper.findPreviousButton()).toBeNull(); + expect(wrapper.findActions()!.findButton('[data-testid="custom-cancel"]')).not.toBeNull(); + expect(wrapper.findActions()!.findButton('[data-testid="custom-next"]')).not.toBeNull(); + }); + + test('custom primary actions work on first step', () => { + const onCustomClick = jest.fn(); + const customActions = ( + + ); + + const [wrapper] = renderDefaultWizard({ + customPrimaryActions: customActions, + activeStepIndex: 0, + }); + + const customActionButtonWrapper = wrapper.findActions()!.findButton('[data-testid="custom-action"]'); + expect(customActionButtonWrapper).not.toBeNull(); + customActionButtonWrapper!.click(); + expect(onCustomClick).toHaveBeenCalledTimes(1); + }); + + test('custom primary actions work on middle step', () => { + const onCustomClick = jest.fn(); + const customActions = ( + + ); + + const [wrapper] = renderDefaultWizard({ + customPrimaryActions: customActions, + activeStepIndex: 1, + }); + + const customActionButtonWrapper = wrapper.findActions()!.findButton('[data-testid="custom-action"]'); + expect(customActionButtonWrapper).not.toBeNull(); + customActionButtonWrapper!.click(); + expect(onCustomClick).toHaveBeenCalledTimes(1); + }); + + test('custom primary actions work on last step', () => { + const onCustomClick = jest.fn(); + const customActions = ( + + ); + + const [wrapper] = renderDefaultWizard({ + customPrimaryActions: customActions, + activeStepIndex: DEFAULT_STEPS.length - 1, + }); + + const customActionButtonWrapper = wrapper.findActions()!.findButton('[data-testid="custom-action"]'); + expect(customActionButtonWrapper).not.toBeNull(); + customActionButtonWrapper!.click(); + expect(onCustomClick).toHaveBeenCalledTimes(1); + }); + + test('custom primary actions override skip-to button', () => { + const customActions = ; + const [wrapper] = renderDefaultWizard({ + customPrimaryActions: customActions, + allowSkipTo: true, + }); + + expect(wrapper.findSkipToButton()).toBeNull(); + expect(wrapper.findActions()!.findButton()!.getElement()).toHaveTextContent('Custom Only'); + }); + + test('falls back to default actions when customPrimaryActions is null', () => { + const [wrapper] = renderDefaultWizard({ customPrimaryActions: null }); + + expect(wrapper.findPrimaryButton()).not.toBeNull(); + expect(wrapper.findCancelButton()).not.toBeNull(); + expect(wrapper.findPrimaryButton().getElement()).toHaveTextContent(DEFAULT_I18N_SETS[0].nextButton!); + }); + + test('falls back to default actions when customPrimaryActions is undefined', () => { + const [wrapper] = renderDefaultWizard({ customPrimaryActions: undefined }); + + expect(wrapper.findPrimaryButton()).not.toBeNull(); + expect(wrapper.findCancelButton()).not.toBeNull(); + expect(wrapper.findPrimaryButton().getElement()).toHaveTextContent(DEFAULT_I18N_SETS[0].nextButton!); + }); +}); + describe('i18n', () => { test('supports rendering static strings using i18n provider', () => { const { container } = render( diff --git a/src/wizard/interfaces.ts b/src/wizard/interfaces.ts index 7a44e30478..97fb647e38 100644 --- a/src/wizard/interfaces.ts +++ b/src/wizard/interfaces.ts @@ -103,6 +103,13 @@ export interface WizardProps extends BaseComponentProps { */ allowSkipTo?: boolean; + /** + * Specifies right-aligned custom primary actions for the wizard. Overwrites existing buttons (e.g. Cancel, Next, Finish). + * + * @awsuiSystem core + */ + customPrimaryActions?: React.ReactNode; + /** * Specifies left-aligned secondary actions for the wizard. Use a button dropdown if multiple actions are required. */ diff --git a/src/wizard/internal.tsx b/src/wizard/internal.tsx index fb82f3b807..1bbf37377b 100644 --- a/src/wizard/internal.tsx +++ b/src/wizard/internal.tsx @@ -41,6 +41,7 @@ export default function InternalWizard({ submitButtonText, isLoadingNextStep = false, allowSkipTo = false, + customPrimaryActions, secondaryActions, onCancel, onSubmit, @@ -201,6 +202,7 @@ export default function InternalWizard({ activeStepIndex={actualActiveStepIndex} isPrimaryLoading={isLoadingNextStep} allowSkipTo={allowSkipTo} + customPrimaryActions={customPrimaryActions} secondaryActions={secondaryActions} onCancelClick={onCancelClick} onPreviousClick={onPreviousClick} diff --git a/src/wizard/wizard-form.tsx b/src/wizard/wizard-form.tsx index ef31e7b864..1db4b301e5 100644 --- a/src/wizard/wizard-form.tsx +++ b/src/wizard/wizard-form.tsx @@ -36,6 +36,7 @@ interface WizardFormProps extends InternalBaseComponentProps { submitButtonText?: string; isPrimaryLoading: boolean; allowSkipTo: boolean; + customPrimaryActions?: React.ReactNode; secondaryActions?: React.ReactNode; onCancelClick: () => void; onPreviousClick: () => void; @@ -80,6 +81,7 @@ function WizardForm({ isPrimaryLoading, allowSkipTo, secondaryActions, + customPrimaryActions, onCancelClick, onPreviousClick, onPrimaryClick, @@ -151,25 +153,29 @@ function WizardForm({ __internalRootRef={ref} className={styles['form-component']} actions={ - onSkipToClick(skipToTargetIndex)} - showPrevious={activeStepIndex !== 0} - isPrimaryLoading={isPrimaryLoading} - showSkipTo={showSkipTo} - skipToButtonText={skipToButtonText} - isLastStep={isLastStep} - activeStepIndex={activeStepIndex} - skipToStepIndex={skipToTargetIndex} - /> + customPrimaryActions ? ( + customPrimaryActions + ) : ( + onSkipToClick(skipToTargetIndex)} + showPrevious={activeStepIndex !== 0} + isPrimaryLoading={isPrimaryLoading} + showSkipTo={showSkipTo} + skipToButtonText={skipToButtonText} + isLastStep={isLastStep} + activeStepIndex={activeStepIndex} + skipToStepIndex={skipToTargetIndex} + /> + ) } secondaryActions={secondaryActions} errorText={errorText}