From d5c5cf7ff15d1b090a8e335c3afb14306137cc06 Mon Sep 17 00:00:00 2001 From: Simon Brebeck Date: Thu, 30 Oct 2025 17:28:12 +0100 Subject: [PATCH 1/8] feat: add custom primary actions for wizard component --- pages/wizard/custom-primary-actions.page.tsx | 97 ++++++++++++++++++++ src/wizard/__tests__/wizard.test.tsx | 77 ++++++++++++++++ src/wizard/interfaces.ts | 5 + src/wizard/internal.tsx | 2 + src/wizard/wizard-form.tsx | 44 +++++---- 5 files changed, 206 insertions(+), 19 deletions(-) create mode 100644 pages/wizard/custom-primary-actions.page.tsx diff --git a/pages/wizard/custom-primary-actions.page.tsx b/pages/wizard/custom-primary-actions.page.tsx new file mode 100644 index 0000000000..25fe190bc8 --- /dev/null +++ b/pages/wizard/custom-primary-actions.page.tsx @@ -0,0 +1,97 @@ +// 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/wizard/__tests__/wizard.test.tsx b/src/wizard/__tests__/wizard.test.tsx index 5f7e43a54b..57c25dcba4 100644 --- a/src/wizard/__tests__/wizard.test.tsx +++ b/src/wizard/__tests__/wizard.test.tsx @@ -515,6 +515,83 @@ 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 all steps', () => { + const onCustomClick = jest.fn(); + const customActions = ( + + ); + + const [wrapper, rerender] = renderDefaultWizard({ + customPrimaryActions: customActions, + activeStepIndex: 0, + }); + + DEFAULT_STEPS.forEach((_, index) => { + const customActionButtonWrapper = wrapper.findActions()!.findButton('[data-testid="custom-action"]'); + expect(customActionButtonWrapper).not.toBeNull(); + customActionButtonWrapper?.click(); + expect(onCustomClick).toHaveBeenCalledTimes(index + 1); + + // Navigate to next step for next iteration + console.log(wrapper.findContent()?.getElement().innerHTML); + if (index < DEFAULT_STEPS.length - 1) { + rerender({ + customPrimaryActions: customActions, + activeStepIndex: index + 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..133927130f 100644 --- a/src/wizard/interfaces.ts +++ b/src/wizard/interfaces.ts @@ -103,6 +103,11 @@ export interface WizardProps extends BaseComponentProps { */ allowSkipTo?: boolean; + /** + * Specifies right-aligned custom primary actions for the wizard. Overwrites existing buttons (e.g. Cancel, Next, Finish, ...). + */ + 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} From 4ef11ff9ece21883019407c9dbbc0579dbc6460c Mon Sep 17 00:00:00 2001 From: Simon Brebeck Date: Fri, 31 Oct 2025 09:50:46 +0100 Subject: [PATCH 2/8] fix: restrict wizard custom primary actions to core-only --- src/wizard/interfaces.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/wizard/interfaces.ts b/src/wizard/interfaces.ts index 133927130f..71a5e07f34 100644 --- a/src/wizard/interfaces.ts +++ b/src/wizard/interfaces.ts @@ -105,6 +105,8 @@ export interface WizardProps extends BaseComponentProps { /** * Specifies right-aligned custom primary actions for the wizard. Overwrites existing buttons (e.g. Cancel, Next, Finish, ...). + * + * @awsuiSystem core */ customPrimaryActions?: React.ReactNode; From ccba078e68dc4085399ed9a09a8277f903d9edf5 Mon Sep 17 00:00:00 2001 From: Simon Brebeck Date: Fri, 31 Oct 2025 14:10:39 +0100 Subject: [PATCH 3/8] fix: update test snapshots --- .../__snapshots__/documenter.test.ts.snap | 372 ++++++------------ 1 file changed, 130 insertions(+), 242 deletions(-) diff --git a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap index f4a10fff93..afd5372280 100644 --- a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap +++ b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap @@ -3363,13 +3363,6 @@ Note: Manual filtering doesn't disable match highlighting.", "optional": true, "type": "string", }, - { - "defaultValue": "false", - "description": "Defines whether entered text option is shown as the first option in the dropdown when value is non-empty.", - "name": "hideEnteredTextOption", - "optional": true, - "type": "boolean", - }, { "deprecatedTag": "The usage of the \`id\` attribute is reserved for internal use cases. For testing and other use cases, use [data attributes](https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Use_data_attributes). If you must @@ -22289,7 +22282,6 @@ Links are rendered as \`\` tags. The anchor also has the attributes \`target="_blank"\` and \`rel="noopener"\`. Additionally, the \`activeHref\` property won't be modified when a user chooses the link. - \`externalIconAriaLabel\` (string) - Adds an aria-label to the external icon. -Note: Deprecated, use i18nStrings.externalIconAriaLabel instead. - \`info\` (ReactNode) - Enables you to display content next to the link. Although it is technically possible to insert any content, our UX guidelines allow only to add a Badge and/or a "New" label. @@ -23099,48 +23091,6 @@ use the \`id\` attribute, consider setting it on a parent element instead.", "optional": true, "type": "string", }, - { - "description": "The visual orientation of the steps (vertical or horizontal). -By default the orientation is vertical.", - "inlineType": { - "name": "StepsProps.Orientation", - "type": "union", - "values": [ - "horizontal", - "vertical", - ], - }, - "name": "orientation", - "optional": true, - "systemTags": [ - "core", - ], - "type": "string", - }, - { - "description": "Render a step. This overrides the default icon, header, and details provided by the component. -The function is called for each step and should return an object with the following keys: -* \`header\` (React.ReactNode) - Summary corresponding to the step. -* \`details\` (React.ReactNode) - (Optional) Additional information corresponding to the step. -* \`icon\` (React.ReactNode) - (Optional) Replaces the standard step icon from the status indicator.", - "inlineType": { - "name": "(step: StepsProps.Step) => { header: React.ReactNode; details?: React.ReactNode; icon?: React.ReactNode; }", - "parameters": [ - { - "name": "step", - "type": "StepsProps.Step", - }, - ], - "returnType": "{ header: React.ReactNode; details?: React.ReactNode; icon?: React.ReactNode; }", - "type": "function", - }, - "name": "renderStep", - "optional": true, - "systemTags": [ - "core", - ], - "type": "((step: StepsProps.Step) => { header: React.ReactNode; details?: React.ReactNode; icon?: React.ReactNode; })", - }, { "description": "An array of individual steps @@ -35437,37 +35387,6 @@ wrapper.clickSelectAll(); ], }, }, - { - "description": "Returns an inline token.", - "name": "findInlineToken", - "parameters": [ - { - "description": "1-based index of the inline token to return", - "flags": { - "isOptional": false, - }, - "name": "tokenIndex", - "typeName": "number", - }, - ], - "returnType": { - "isNullable": true, - "name": "TokenWrapper", - }, - }, - { - "name": "findInlineTokens", - "parameters": [], - "returnType": { - "isNullable": false, - "name": "Array", - "typeArguments": [ - { - "name": "TokenWrapper", - }, - ], - }, - }, { "name": "findPlaceholder", "parameters": [], @@ -35701,81 +35620,6 @@ wrapper.selectOptionByValue('option_1'); ], "name": "TokenGroupItemWrapper", }, - { - "methods": [ - { - "description": "Returns the token description.", - "name": "findDescription", - "parameters": [], - "returnType": { - "isNullable": true, - "name": "ElementWrapper", - "typeArguments": [ - { - "name": "HTMLElement", - }, - ], - }, - }, - { - "description": "Returns the token dismiss button.", - "name": "findDismiss", - "parameters": [], - "returnType": { - "isNullable": true, - "name": "ElementWrapper", - "typeArguments": [ - { - "name": "HTMLElement", - }, - ], - }, - }, - { - "description": "Returns the token label.", - "name": "findLabel", - "parameters": [], - "returnType": { - "isNullable": false, - "name": "ElementWrapper", - "typeArguments": [ - { - "name": "HTMLElement", - }, - ], - }, - }, - { - "description": "Returns the token label tag.", - "name": "findLabelTag", - "parameters": [], - "returnType": { - "isNullable": true, - "name": "ElementWrapper", - "typeArguments": [ - { - "name": "HTMLElement", - }, - ], - }, - }, - { - "description": "Returns the token tags.", - "name": "findTags", - "parameters": [], - "returnType": { - "isNullable": true, - "name": "Array", - "typeArguments": [ - { - "name": "ElementWrapper", - }, - ], - }, - }, - ], - "name": "TokenWrapper", - }, { "methods": [ { @@ -38528,6 +38372,81 @@ Returns the current value of the input.", ], "name": "TimeInputWrapper", }, + { + "methods": [ + { + "description": "Returns the token description.", + "name": "findDescription", + "parameters": [], + "returnType": { + "isNullable": true, + "name": "ElementWrapper", + "typeArguments": [ + { + "name": "HTMLElement", + }, + ], + }, + }, + { + "description": "Returns the token dismiss button.", + "name": "findDismiss", + "parameters": [], + "returnType": { + "isNullable": true, + "name": "ElementWrapper", + "typeArguments": [ + { + "name": "HTMLElement", + }, + ], + }, + }, + { + "description": "Returns the token label.", + "name": "findLabel", + "parameters": [], + "returnType": { + "isNullable": false, + "name": "ElementWrapper", + "typeArguments": [ + { + "name": "HTMLElement", + }, + ], + }, + }, + { + "description": "Returns the token label tag.", + "name": "findLabelTag", + "parameters": [], + "returnType": { + "isNullable": true, + "name": "ElementWrapper", + "typeArguments": [ + { + "name": "HTMLElement", + }, + ], + }, + }, + { + "description": "Returns the token tags.", + "name": "findTags", + "parameters": [], + "returnType": { + "isNullable": true, + "name": "Array", + "typeArguments": [ + { + "name": "ElementWrapper", + }, + ], + }, + }, + ], + "name": "TokenWrapper", + }, { "methods": [ { @@ -44756,37 +44675,6 @@ The dismiss button is only rendered when the \`dismissible\` property is set to "name": "ElementWrapper", }, }, - { - "description": "Returns an inline token.", - "name": "findInlineToken", - "parameters": [ - { - "description": "1-based index of the inline token to return", - "flags": { - "isOptional": false, - }, - "name": "tokenIndex", - "typeName": "number", - }, - ], - "returnType": { - "isNullable": false, - "name": "TokenWrapper", - }, - }, - { - "name": "findInlineTokens", - "parameters": [], - "returnType": { - "isNullable": false, - "name": "MultiElementWrapper", - "typeArguments": [ - { - "name": "TokenWrapper", - }, - ], - }, - }, { "name": "findPlaceholder", "parameters": [], @@ -44895,61 +44783,6 @@ The dismiss button is only rendered when the \`dismissible\` property is set to ], "name": "TokenGroupItemWrapper", }, - { - "methods": [ - { - "description": "Returns the token description.", - "name": "findDescription", - "parameters": [], - "returnType": { - "isNullable": false, - "name": "ElementWrapper", - }, - }, - { - "description": "Returns the token dismiss button.", - "name": "findDismiss", - "parameters": [], - "returnType": { - "isNullable": false, - "name": "ElementWrapper", - }, - }, - { - "description": "Returns the token label.", - "name": "findLabel", - "parameters": [], - "returnType": { - "isNullable": false, - "name": "ElementWrapper", - }, - }, - { - "description": "Returns the token label tag.", - "name": "findLabelTag", - "parameters": [], - "returnType": { - "isNullable": false, - "name": "ElementWrapper", - }, - }, - { - "description": "Returns the token tags.", - "name": "findTags", - "parameters": [], - "returnType": { - "isNullable": false, - "name": "MultiElementWrapper", - "typeArguments": [ - { - "name": "ElementWrapper", - }, - ], - }, - }, - ], - "name": "TokenWrapper", - }, { "methods": [ { @@ -46886,6 +46719,61 @@ To find a specific row use the \`findRow(n)\` function as chaining \`findRows(). ], "name": "TimeInputWrapper", }, + { + "methods": [ + { + "description": "Returns the token description.", + "name": "findDescription", + "parameters": [], + "returnType": { + "isNullable": false, + "name": "ElementWrapper", + }, + }, + { + "description": "Returns the token dismiss button.", + "name": "findDismiss", + "parameters": [], + "returnType": { + "isNullable": false, + "name": "ElementWrapper", + }, + }, + { + "description": "Returns the token label.", + "name": "findLabel", + "parameters": [], + "returnType": { + "isNullable": false, + "name": "ElementWrapper", + }, + }, + { + "description": "Returns the token label tag.", + "name": "findLabelTag", + "parameters": [], + "returnType": { + "isNullable": false, + "name": "ElementWrapper", + }, + }, + { + "description": "Returns the token tags.", + "name": "findTags", + "parameters": [], + "returnType": { + "isNullable": false, + "name": "MultiElementWrapper", + "typeArguments": [ + { + "name": "ElementWrapper", + }, + ], + }, + }, + ], + "name": "TokenWrapper", + }, { "methods": [ { From 08633b00d667754f5971c58e81160112c0780190 Mon Sep 17 00:00:00 2001 From: Simon Brebeck Date: Fri, 31 Oct 2025 15:21:42 +0100 Subject: [PATCH 4/8] fix: update test snapshots --- .../__snapshots__/documenter.test.ts.snap | 380 ++++++++++++------ 1 file changed, 250 insertions(+), 130 deletions(-) diff --git a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap index afd5372280..fadab2f1cb 100644 --- a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap +++ b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap @@ -3363,6 +3363,13 @@ Note: Manual filtering doesn't disable match highlighting.", "optional": true, "type": "string", }, + { + "defaultValue": "false", + "description": "Defines whether entered text option is shown as the first option in the dropdown when value is non-empty.", + "name": "hideEnteredTextOption", + "optional": true, + "type": "boolean", + }, { "deprecatedTag": "The usage of the \`id\` attribute is reserved for internal use cases. For testing and other use cases, use [data attributes](https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Use_data_attributes). If you must @@ -22282,6 +22289,7 @@ Links are rendered as \`\` tags. The anchor also has the attributes \`target="_blank"\` and \`rel="noopener"\`. Additionally, the \`activeHref\` property won't be modified when a user chooses the link. - \`externalIconAriaLabel\` (string) - Adds an aria-label to the external icon. +Note: Deprecated, use i18nStrings.externalIconAriaLabel instead. - \`info\` (ReactNode) - Enables you to display content next to the link. Although it is technically possible to insert any content, our UX guidelines allow only to add a Badge and/or a "New" label. @@ -23091,6 +23099,48 @@ use the \`id\` attribute, consider setting it on a parent element instead.", "optional": true, "type": "string", }, + { + "description": "The visual orientation of the steps (vertical or horizontal). +By default the orientation is vertical.", + "inlineType": { + "name": "StepsProps.Orientation", + "type": "union", + "values": [ + "horizontal", + "vertical", + ], + }, + "name": "orientation", + "optional": true, + "systemTags": [ + "core", + ], + "type": "string", + }, + { + "description": "Render a step. This overrides the default icon, header, and details provided by the component. +The function is called for each step and should return an object with the following keys: +* \`header\` (React.ReactNode) - Summary corresponding to the step. +* \`details\` (React.ReactNode) - (Optional) Additional information corresponding to the step. +* \`icon\` (React.ReactNode) - (Optional) Replaces the standard step icon from the status indicator.", + "inlineType": { + "name": "(step: StepsProps.Step) => { header: React.ReactNode; details?: React.ReactNode; icon?: React.ReactNode; }", + "parameters": [ + { + "name": "step", + "type": "StepsProps.Step", + }, + ], + "returnType": "{ header: React.ReactNode; details?: React.ReactNode; icon?: React.ReactNode; }", + "type": "function", + }, + "name": "renderStep", + "optional": true, + "systemTags": [ + "core", + ], + "type": "((step: StepsProps.Step) => { header: React.ReactNode; details?: React.ReactNode; icon?: React.ReactNode; })", + }, { "description": "An array of individual steps @@ -27977,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, @@ -35387,6 +35445,37 @@ wrapper.clickSelectAll(); ], }, }, + { + "description": "Returns an inline token.", + "name": "findInlineToken", + "parameters": [ + { + "description": "1-based index of the inline token to return", + "flags": { + "isOptional": false, + }, + "name": "tokenIndex", + "typeName": "number", + }, + ], + "returnType": { + "isNullable": true, + "name": "TokenWrapper", + }, + }, + { + "name": "findInlineTokens", + "parameters": [], + "returnType": { + "isNullable": false, + "name": "Array", + "typeArguments": [ + { + "name": "TokenWrapper", + }, + ], + }, + }, { "name": "findPlaceholder", "parameters": [], @@ -35620,6 +35709,81 @@ wrapper.selectOptionByValue('option_1'); ], "name": "TokenGroupItemWrapper", }, + { + "methods": [ + { + "description": "Returns the token description.", + "name": "findDescription", + "parameters": [], + "returnType": { + "isNullable": true, + "name": "ElementWrapper", + "typeArguments": [ + { + "name": "HTMLElement", + }, + ], + }, + }, + { + "description": "Returns the token dismiss button.", + "name": "findDismiss", + "parameters": [], + "returnType": { + "isNullable": true, + "name": "ElementWrapper", + "typeArguments": [ + { + "name": "HTMLElement", + }, + ], + }, + }, + { + "description": "Returns the token label.", + "name": "findLabel", + "parameters": [], + "returnType": { + "isNullable": false, + "name": "ElementWrapper", + "typeArguments": [ + { + "name": "HTMLElement", + }, + ], + }, + }, + { + "description": "Returns the token label tag.", + "name": "findLabelTag", + "parameters": [], + "returnType": { + "isNullable": true, + "name": "ElementWrapper", + "typeArguments": [ + { + "name": "HTMLElement", + }, + ], + }, + }, + { + "description": "Returns the token tags.", + "name": "findTags", + "parameters": [], + "returnType": { + "isNullable": true, + "name": "Array", + "typeArguments": [ + { + "name": "ElementWrapper", + }, + ], + }, + }, + ], + "name": "TokenWrapper", + }, { "methods": [ { @@ -38372,81 +38536,6 @@ Returns the current value of the input.", ], "name": "TimeInputWrapper", }, - { - "methods": [ - { - "description": "Returns the token description.", - "name": "findDescription", - "parameters": [], - "returnType": { - "isNullable": true, - "name": "ElementWrapper", - "typeArguments": [ - { - "name": "HTMLElement", - }, - ], - }, - }, - { - "description": "Returns the token dismiss button.", - "name": "findDismiss", - "parameters": [], - "returnType": { - "isNullable": true, - "name": "ElementWrapper", - "typeArguments": [ - { - "name": "HTMLElement", - }, - ], - }, - }, - { - "description": "Returns the token label.", - "name": "findLabel", - "parameters": [], - "returnType": { - "isNullable": false, - "name": "ElementWrapper", - "typeArguments": [ - { - "name": "HTMLElement", - }, - ], - }, - }, - { - "description": "Returns the token label tag.", - "name": "findLabelTag", - "parameters": [], - "returnType": { - "isNullable": true, - "name": "ElementWrapper", - "typeArguments": [ - { - "name": "HTMLElement", - }, - ], - }, - }, - { - "description": "Returns the token tags.", - "name": "findTags", - "parameters": [], - "returnType": { - "isNullable": true, - "name": "Array", - "typeArguments": [ - { - "name": "ElementWrapper", - }, - ], - }, - }, - ], - "name": "TokenWrapper", - }, { "methods": [ { @@ -44675,6 +44764,37 @@ The dismiss button is only rendered when the \`dismissible\` property is set to "name": "ElementWrapper", }, }, + { + "description": "Returns an inline token.", + "name": "findInlineToken", + "parameters": [ + { + "description": "1-based index of the inline token to return", + "flags": { + "isOptional": false, + }, + "name": "tokenIndex", + "typeName": "number", + }, + ], + "returnType": { + "isNullable": false, + "name": "TokenWrapper", + }, + }, + { + "name": "findInlineTokens", + "parameters": [], + "returnType": { + "isNullable": false, + "name": "MultiElementWrapper", + "typeArguments": [ + { + "name": "TokenWrapper", + }, + ], + }, + }, { "name": "findPlaceholder", "parameters": [], @@ -44783,6 +44903,61 @@ The dismiss button is only rendered when the \`dismissible\` property is set to ], "name": "TokenGroupItemWrapper", }, + { + "methods": [ + { + "description": "Returns the token description.", + "name": "findDescription", + "parameters": [], + "returnType": { + "isNullable": false, + "name": "ElementWrapper", + }, + }, + { + "description": "Returns the token dismiss button.", + "name": "findDismiss", + "parameters": [], + "returnType": { + "isNullable": false, + "name": "ElementWrapper", + }, + }, + { + "description": "Returns the token label.", + "name": "findLabel", + "parameters": [], + "returnType": { + "isNullable": false, + "name": "ElementWrapper", + }, + }, + { + "description": "Returns the token label tag.", + "name": "findLabelTag", + "parameters": [], + "returnType": { + "isNullable": false, + "name": "ElementWrapper", + }, + }, + { + "description": "Returns the token tags.", + "name": "findTags", + "parameters": [], + "returnType": { + "isNullable": false, + "name": "MultiElementWrapper", + "typeArguments": [ + { + "name": "ElementWrapper", + }, + ], + }, + }, + ], + "name": "TokenWrapper", + }, { "methods": [ { @@ -46719,61 +46894,6 @@ To find a specific row use the \`findRow(n)\` function as chaining \`findRows(). ], "name": "TimeInputWrapper", }, - { - "methods": [ - { - "description": "Returns the token description.", - "name": "findDescription", - "parameters": [], - "returnType": { - "isNullable": false, - "name": "ElementWrapper", - }, - }, - { - "description": "Returns the token dismiss button.", - "name": "findDismiss", - "parameters": [], - "returnType": { - "isNullable": false, - "name": "ElementWrapper", - }, - }, - { - "description": "Returns the token label.", - "name": "findLabel", - "parameters": [], - "returnType": { - "isNullable": false, - "name": "ElementWrapper", - }, - }, - { - "description": "Returns the token label tag.", - "name": "findLabelTag", - "parameters": [], - "returnType": { - "isNullable": false, - "name": "ElementWrapper", - }, - }, - { - "description": "Returns the token tags.", - "name": "findTags", - "parameters": [], - "returnType": { - "isNullable": false, - "name": "MultiElementWrapper", - "typeArguments": [ - { - "name": "ElementWrapper", - }, - ], - }, - }, - ], - "name": "TokenWrapper", - }, { "methods": [ { From 274f5282c5b28cc211bba3735c004b3d9909336b Mon Sep 17 00:00:00 2001 From: Simon Brebeck Date: Fri, 31 Oct 2025 16:49:55 +0100 Subject: [PATCH 5/8] fix: address PR feedback on wizard component - Change SpaceBetween size from 'm' to 'xs' in custom primary actions - Remove unnecessary allowSkipTo prop - Replace original chaining with non-null assertion in tests - Remove debug.console.log statement --- pages/wizard/custom-primary-actions.page.tsx | 3 +-- src/wizard/__tests__/wizard.test.tsx | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/pages/wizard/custom-primary-actions.page.tsx b/pages/wizard/custom-primary-actions.page.tsx index 25fe190bc8..01810643fb 100644 --- a/pages/wizard/custom-primary-actions.page.tsx +++ b/pages/wizard/custom-primary-actions.page.tsx @@ -63,7 +63,7 @@ export default function WizardPage() { }; const customPrimaryActions = ( - + {activeStepIndex > 0 && ( : null} diff --git a/src/wizard/__tests__/wizard.test.tsx b/src/wizard/__tests__/wizard.test.tsx index 57c25dcba4..203e7a555e 100644 --- a/src/wizard/__tests__/wizard.test.tsx +++ b/src/wizard/__tests__/wizard.test.tsx @@ -550,11 +550,10 @@ describe('Custom primary actions', () => { DEFAULT_STEPS.forEach((_, index) => { const customActionButtonWrapper = wrapper.findActions()!.findButton('[data-testid="custom-action"]'); expect(customActionButtonWrapper).not.toBeNull(); - customActionButtonWrapper?.click(); + customActionButtonWrapper!.click(); expect(onCustomClick).toHaveBeenCalledTimes(index + 1); // Navigate to next step for next iteration - console.log(wrapper.findContent()?.getElement().innerHTML); if (index < DEFAULT_STEPS.length - 1) { rerender({ customPrimaryActions: customActions, From 77cb7fccd6270c1a5540be368a70a907f3b45024 Mon Sep 17 00:00:00 2001 From: Simon Brebeck Date: Fri, 31 Oct 2025 16:55:11 +0100 Subject: [PATCH 6/8] chore: change property description (remove '...') --- src/wizard/interfaces.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wizard/interfaces.ts b/src/wizard/interfaces.ts index 71a5e07f34..97fb647e38 100644 --- a/src/wizard/interfaces.ts +++ b/src/wizard/interfaces.ts @@ -104,7 +104,7 @@ export interface WizardProps extends BaseComponentProps { allowSkipTo?: boolean; /** - * Specifies right-aligned custom primary actions for the wizard. Overwrites existing buttons (e.g. Cancel, Next, Finish, ...). + * Specifies right-aligned custom primary actions for the wizard. Overwrites existing buttons (e.g. Cancel, Next, Finish). * * @awsuiSystem core */ From 6e40eacbc5691dee16294012fdfef9f02d48e411 Mon Sep 17 00:00:00 2001 From: Simon Brebeck Date: Fri, 31 Oct 2025 17:09:50 +0100 Subject: [PATCH 7/8] test: split custom primary actions validation into 3 different steps --- src/wizard/__tests__/wizard.test.tsx | 56 +++++++++++++++++++++------- 1 file changed, 42 insertions(+), 14 deletions(-) diff --git a/src/wizard/__tests__/wizard.test.tsx b/src/wizard/__tests__/wizard.test.tsx index 203e7a555e..2e2c7ddbbc 100644 --- a/src/wizard/__tests__/wizard.test.tsx +++ b/src/wizard/__tests__/wizard.test.tsx @@ -534,7 +534,7 @@ describe('Custom primary actions', () => { expect(wrapper.findActions()!.findButton('[data-testid="custom-next"]')).not.toBeNull(); }); - test('custom primary actions work on all steps', () => { + test('custom primary actions work on first step', () => { const onCustomClick = jest.fn(); const customActions = ( ); - const [wrapper, rerender] = renderDefaultWizard({ + const [wrapper] = renderDefaultWizard({ customPrimaryActions: customActions, activeStepIndex: 0, }); - DEFAULT_STEPS.forEach((_, index) => { - const customActionButtonWrapper = wrapper.findActions()!.findButton('[data-testid="custom-action"]'); - expect(customActionButtonWrapper).not.toBeNull(); - customActionButtonWrapper!.click(); - expect(onCustomClick).toHaveBeenCalledTimes(index + 1); + const customActionButtonWrapper = wrapper.findActions()!.findButton('[data-testid="custom-action"]'); + expect(customActionButtonWrapper).not.toBeNull(); + customActionButtonWrapper!.click(); + expect(onCustomClick).toHaveBeenCalledTimes(1); + }); - // Navigate to next step for next iteration - if (index < DEFAULT_STEPS.length - 1) { - rerender({ - customPrimaryActions: customActions, - activeStepIndex: index + 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', () => { From bdd2365ec7a077bac63adffa613e446b7ddeb9bf Mon Sep 17 00:00:00 2001 From: Simon Brebeck Date: Fri, 31 Oct 2025 17:13:59 +0100 Subject: [PATCH 8/8] fix: update test snapshots --- .../snapshot-tests/__snapshots__/documenter.test.ts.snap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap index fadab2f1cb..93e3d26acd 100644 --- a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap +++ b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap @@ -28028,7 +28028,7 @@ 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, ...).", + "description": "Specifies right-aligned custom primary actions for the wizard. Overwrites existing buttons (e.g. Cancel, Next, Finish).", "isDefault": false, "name": "customPrimaryActions", "systemTags": [