Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions pages/wizard/custom-primary-actions.page.tsx
Original file line number Diff line number Diff line change
@@ -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: (
<>
<Container header={<Header>Step 1, substep one</Header>}></Container>
<Container header={<Header>Step 1, substep two</Header>}></Container>
</>
),
},
{
title: 'Step 2',
content: (
<>
<Container header={<Header>Step 2, substep one</Header>}></Container>
<Container header={<Header>Step 2, substep two</Header>}></Container>
</>
),
isOptional: true,
},
{
title: 'Step 3',
content: (
<>
<Container header={<Header>Step 3, substep one</Header>}></Container>
<Container header={<Header>Step 3, substep two</Header>}></Container>
</>
),
},
];

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 = (
<SpaceBetween size="xs" direction="horizontal">
{activeStepIndex > 0 && (
<Button variant="normal" onClick={onPrevious}>
Custom Previous
</Button>
)}
{activeStepIndex < steps.length - 1 && (
<Button variant="primary" onClick={onNext}>
Custom Next
</Button>
)}
{activeStepIndex === steps.length - 1 && (
<Button variant="primary" onClick={onFinish}>
Custom Finish
</Button>
)}
</SpaceBetween>
);

return (
<Wizard
id="wizard"
steps={steps}
i18nStrings={i18nStrings}
activeStepIndex={activeStepIndex}
onNavigate={e => setActiveStepIndex(e.detail.requestedStepIndex)}
secondaryActions={activeStepIndex === 2 ? <Button>Save as draft</Button> : null}
customPrimaryActions={customPrimaryActions}
/>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
104 changes: 104 additions & 0 deletions src/wizard/__tests__/wizard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,110 @@ describe('Custom actions', () => {
});
});

describe('Custom primary actions', () => {
test('renders custom primary actions instead of default buttons', () => {
const customActions = (
<>
<Button data-testid="custom-cancel">Custom Cancel</Button>
<Button data-testid="custom-next" variant="primary">
Custom Next
</Button>
</>
);
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 = (
<Button data-testid="custom-action" onClick={onCustomClick}>
Custom Action
</Button>
);

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 = (
<Button data-testid="custom-action" onClick={onCustomClick}>
Custom Action
</Button>
);

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 = (
<Button data-testid="custom-action" onClick={onCustomClick}>
Custom Action
</Button>
);

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 = <Button>Custom Only</Button>;
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(
Expand Down
7 changes: 7 additions & 0 deletions src/wizard/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
2 changes: 2 additions & 0 deletions src/wizard/internal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export default function InternalWizard({
submitButtonText,
isLoadingNextStep = false,
allowSkipTo = false,
customPrimaryActions,
secondaryActions,
onCancel,
onSubmit,
Expand Down Expand Up @@ -201,6 +202,7 @@ export default function InternalWizard({
activeStepIndex={actualActiveStepIndex}
isPrimaryLoading={isLoadingNextStep}
allowSkipTo={allowSkipTo}
customPrimaryActions={customPrimaryActions}
secondaryActions={secondaryActions}
onCancelClick={onCancelClick}
onPreviousClick={onPreviousClick}
Expand Down
44 changes: 25 additions & 19 deletions src/wizard/wizard-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ interface WizardFormProps extends InternalBaseComponentProps {
submitButtonText?: string;
isPrimaryLoading: boolean;
allowSkipTo: boolean;
customPrimaryActions?: React.ReactNode;
secondaryActions?: React.ReactNode;
onCancelClick: () => void;
onPreviousClick: () => void;
Expand Down Expand Up @@ -80,6 +81,7 @@ function WizardForm({
isPrimaryLoading,
allowSkipTo,
secondaryActions,
customPrimaryActions,
onCancelClick,
onPreviousClick,
onPrimaryClick,
Expand Down Expand Up @@ -151,25 +153,29 @@ function WizardForm({
__internalRootRef={ref}
className={styles['form-component']}
actions={
<WizardActions
cancelButtonText={i18nStrings.cancelButton}
primaryButtonText={isLastStep ? (submitButtonText ?? i18nStrings.submitButton) : i18nStrings.nextButton}
primaryButtonLoadingText={
isLastStep ? i18nStrings.submitButtonLoadingAnnouncement : i18nStrings.nextButtonLoadingAnnouncement
}
previousButtonText={i18nStrings.previousButton}
onCancelClick={onCancelClick}
onPreviousClick={onPreviousClick}
onPrimaryClick={onPrimaryClick}
onSkipToClick={() => onSkipToClick(skipToTargetIndex)}
showPrevious={activeStepIndex !== 0}
isPrimaryLoading={isPrimaryLoading}
showSkipTo={showSkipTo}
skipToButtonText={skipToButtonText}
isLastStep={isLastStep}
activeStepIndex={activeStepIndex}
skipToStepIndex={skipToTargetIndex}
/>
customPrimaryActions ? (
customPrimaryActions
) : (
<WizardActions
cancelButtonText={i18nStrings.cancelButton}
primaryButtonText={isLastStep ? (submitButtonText ?? i18nStrings.submitButton) : i18nStrings.nextButton}
primaryButtonLoadingText={
isLastStep ? i18nStrings.submitButtonLoadingAnnouncement : i18nStrings.nextButtonLoadingAnnouncement
}
previousButtonText={i18nStrings.previousButton}
onCancelClick={onCancelClick}
onPreviousClick={onPreviousClick}
onPrimaryClick={onPrimaryClick}
onSkipToClick={() => onSkipToClick(skipToTargetIndex)}
showPrevious={activeStepIndex !== 0}
isPrimaryLoading={isPrimaryLoading}
showSkipTo={showSkipTo}
skipToButtonText={skipToButtonText}
isLastStep={isLastStep}
activeStepIndex={activeStepIndex}
skipToStepIndex={skipToTargetIndex}
/>
)
}
secondaryActions={secondaryActions}
errorText={errorText}
Expand Down
Loading