From 494faf4536795a1c13541b365cedc4efbcb04c86 Mon Sep 17 00:00:00 2001 From: Andrei Zhaleznichenka Date: Thu, 7 Aug 2025 12:09:34 +0200 Subject: [PATCH] chore: Wizard with custom steps poc --- .../common/steps-components.tsx | 48 +++++ pages/0_wizard_and_steps/styles.scss | 41 +++++ pages/0_wizard_and_steps/variant-1.page.tsx | 168 ++++++++++++++++++ pages/app/index.tsx | 1 + .../__snapshots__/documenter.test.ts.snap | 10 ++ src/steps/index.tsx | 1 - src/steps/interfaces.ts | 2 + src/steps/internal.tsx | 5 +- src/wizard/interfaces.ts | 10 ++ src/wizard/internal.tsx | 31 ++-- src/wizard/styles.scss | 6 + src/wizard/wizard-form.tsx | 14 +- 12 files changed, 320 insertions(+), 17 deletions(-) create mode 100644 pages/0_wizard_and_steps/common/steps-components.tsx create mode 100644 pages/0_wizard_and_steps/styles.scss create mode 100644 pages/0_wizard_and_steps/variant-1.page.tsx diff --git a/pages/0_wizard_and_steps/common/steps-components.tsx b/pages/0_wizard_and_steps/common/steps-components.tsx new file mode 100644 index 0000000000..06010c9f85 --- /dev/null +++ b/pages/0_wizard_and_steps/common/steps-components.tsx @@ -0,0 +1,48 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import React from 'react'; +import clsx from 'clsx'; + +import { Badge, Box, SpaceBetween } from '~components'; + +import styles from '../styles.scss'; + +export function StepHeader({ + visited, + active, + isNew, + onClick, + children, +}: { + children: React.ReactNode; + visited: boolean; + isNew: boolean; + active: boolean; + onClick: () => void; +}) { + return ( + + ); +} + +export function StepDescription({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} diff --git a/pages/0_wizard_and_steps/styles.scss b/pages/0_wizard_and_steps/styles.scss new file mode 100644 index 0000000000..ee326b31ab --- /dev/null +++ b/pages/0_wizard_and_steps/styles.scss @@ -0,0 +1,41 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + SPDX-License-Identifier: Apache-2.0 +*/ + +@use '~design-tokens' as awsui; + +.steps-button { + border-block: none; + border-inline: none; + border-start-start-radius: 0; + border-start-end-radius: 0; + border-end-start-radius: 0; + border-end-end-radius: 0; + padding-block: 0; + padding-inline: 0; + background: none; + cursor: pointer; + color: awsui.$color-text-status-inactive; + + &.steps-button-visited { + text-decoration: underline; + color: awsui.$color-text-link-default; + } + + &-active { + font-weight: bold; + color: awsui.$color-text-interactive-active; + cursor: text; + user-select: text; + + &.steps-button-visited { + text-decoration: none; + color: awsui.$color-text-interactive-active; + } + } + + &:focus { + outline-color: awsui.$color-border-item-focused; + } +} diff --git a/pages/0_wizard_and_steps/variant-1.page.tsx b/pages/0_wizard_and_steps/variant-1.page.tsx new file mode 100644 index 0000000000..57f4773d10 --- /dev/null +++ b/pages/0_wizard_and_steps/variant-1.page.tsx @@ -0,0 +1,168 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import React, { useState } from 'react'; + +import { + AppLayout, + Box, + Checkbox, + Container, + Drawer, + FormField, + Header, + Select, + SpaceBetween, + StatusIndicatorProps, + Steps, +} from '~components'; +import Button from '~components/button'; +import I18nProvider from '~components/i18n'; +import messages from '~components/i18n/messages/all.en'; +import Wizard from '~components/wizard'; + +import { i18nStrings as wizardI18nStrings } from '../wizard/common'; +import { StepDescription, StepHeader } from './common/steps-components'; + +const stepsMetadata = [ + { title: 'Step 1: Workflow overview', subtitle: 'Review step progress' }, + { title: 'Step 2: Select EC2 instance', subtitle: 'Create or choose target EC2 instance' }, + { title: 'Step 3: Select S3 bucket', subtitle: 'Pick S3 bucket access' }, + { title: 'Step 4: Configure IAM role', subtitle: 'Set permissions and policies' }, + { title: 'Step 5: Updated S3 bucket policy', subtitle: 'Configure bucket access rules' }, + { title: 'Step 6: Updated KMS key policy', subtitle: 'Set encryption permissions based on the S3 bucket selection' }, +]; + +const statusOptions = [{ value: 'success' }, { value: 'in-progress' }, { value: 'pending' }, { value: 'error' }]; + +interface StateSettings { + status: StatusIndicatorProps.Type; + isNew: boolean; +} + +export default function WizardPage() { + const [activeStepIndex, setActiveStepIndex] = useState(3); + const [stepStates, setStepStates] = useState([ + { status: 'success', isNew: false }, + { status: 'error', isNew: false }, + { status: 'success', isNew: false }, + { status: 'in-progress', isNew: false }, + { status: 'pending', isNew: false }, + { status: 'pending', isNew: true }, + ]); + const changeStepSettings = (index: number, settings: (prev: StateSettings) => StateSettings) => { + setStepStates(prev => { + const copy = [...prev]; + copy[index] = settings(copy[index]); + return copy; + }); + }; + const changeStepStatus = (index: number, status: StatusIndicatorProps.Type) => + changeStepSettings(index, prev => ({ ...prev, status })); + const changeStepNew = (index: number, isNew: boolean) => changeStepSettings(index, prev => ({ ...prev, isNew })); + const getStatusProps = (status: StatusIndicatorProps.Type) => { + switch (status) { + case 'success': + return { status: 'success', statusIconAriaLabel: 'success' } as const; + case 'in-progress': + return { status: 'in-progress', statusIconAriaLabel: 'in progress', statusColorOverride: 'blue' } as const; + case 'pending': + return { status: 'pending', statusIconAriaLabel: 'pending' } as const; + case 'error': + default: + return { status: 'error', statusIconAriaLabel: 'error' } as const; + } + }; + const steps = stepsMetadata.map((_, index) => { + const { title, subtitle } = stepsMetadata[index]; + const { status, isNew } = stepStates[index]; + const statusProps = getStatusProps(status); + return { + title, + ...statusProps, + header: ( + setActiveStepIndex(index)} + isNew={isNew} + > + {title} + + ), + details: {subtitle}, + content: ( + + +
Content {index + 1}
+
+
+ ), + }; + }); + + const [activeDrawerId, setActiveDrawerId] = useState('settings'); + return ( + + setActiveDrawerId(detail.activeDrawerId)} + drawers={[ + { + id: 'settings', + content: ( + Steps settings}> + + {stepsMetadata.map(({ title }, index) => ( + + + + + changeStepNew(index, detail.checked)} + > + New + + + + ))} + + + ), + trigger: { iconName: 'settings' }, + ariaLabels: { + drawerName: 'Steps settings', + triggerButton: 'Open steps settings', + closeButton: 'Close steps settings', + }, + }, + ]} + contentType="wizard" + content={ + setActiveStepIndex(e.detail.requestedStepIndex)} + secondaryActions={activeStepIndex === 2 ? : null} + customNavigationSide={ + + + + } + customNavigationTop={Custom nav top!} + /> + } + /> + + ); +} diff --git a/pages/app/index.tsx b/pages/app/index.tsx index 5678ad8cb3..ecd4fa8330 100644 --- a/pages/app/index.tsx +++ b/pages/app/index.tsx @@ -42,6 +42,7 @@ function isAppLayoutPage(pageId?: string) { 'prompt-input/simple', 'funnel-analytics/static-single-page-flow', 'funnel-analytics/static-multi-page-flow', + '0_wizard_and_steps', ]; return pageId !== undefined && appLayoutPages.some(match => pageId.includes(match)); } diff --git a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap index 88e3d348ea..36b0f4eb4c 100644 --- a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap +++ b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap @@ -26259,6 +26259,16 @@ Use this if you need to wait for a response from the server before the user can }, ], "regions": [ + { + "description": "Overrides wizard steps navigation with a custom one when it is rendered on the side.", + "isDefault": false, + "name": "customNavigationSide", + }, + { + "description": "Overrides wizard steps navigation with a custom one when it is rendered on the top.", + "isDefault": false, + "name": "customNavigationTop", + }, { "description": "Specifies left-aligned secondary actions for the wizard. Use a button dropdown if multiple actions are required.", "isDefault": false, diff --git a/src/steps/index.tsx b/src/steps/index.tsx index 6a6b4fc0d7..f52656a91d 100644 --- a/src/steps/index.tsx +++ b/src/steps/index.tsx @@ -16,7 +16,6 @@ const Steps = ({ steps, ...props }: StepsProps) => { const baseProps = getBaseProps(props); const baseComponentProps = useBaseComponent('Steps'); const externalProps = getExternalProps(props); - return ; }; diff --git a/src/steps/interfaces.ts b/src/steps/interfaces.ts index d328fee253..e4b4b6f868 100644 --- a/src/steps/interfaces.ts +++ b/src/steps/interfaces.ts @@ -33,10 +33,12 @@ export interface StepsProps extends BaseComponentProps { export namespace StepsProps { export type Status = StatusIndicatorProps.Type; + export type Color = StatusIndicatorProps.Color; export interface Step { status: Status; statusIconAriaLabel?: string; + statusColorOverride?: Color; header: React.ReactNode; details?: React.ReactNode; } diff --git a/src/steps/internal.tsx b/src/steps/internal.tsx index 892198aac4..01a6c90f8c 100644 --- a/src/steps/internal.tsx +++ b/src/steps/internal.tsx @@ -12,11 +12,11 @@ import styles from './styles.css.js'; type InternalStepsProps = SomeRequired & InternalBaseComponentProps; -const InternalStep = ({ status, statusIconAriaLabel, header, details }: StepsProps.Step) => { +const InternalStep = ({ status, statusIconAriaLabel, statusColorOverride, header, details }: StepsProps.Step) => { return (
  • - + {header}
    @@ -47,6 +47,7 @@ const InternalSteps = ({ key={index} status={step.status} statusIconAriaLabel={step.statusIconAriaLabel} + statusColorOverride={step.statusColorOverride} header={step.header} details={step.details} /> diff --git a/src/wizard/interfaces.ts b/src/wizard/interfaces.ts index 7a44e30478..5440977916 100644 --- a/src/wizard/interfaces.ts +++ b/src/wizard/interfaces.ts @@ -129,6 +129,16 @@ export interface WizardProps extends BaseComponentProps { * or `skip` (when navigated using navigation pane or the *skip-to* button to the previously unvisited step). */ onNavigate?: NonCancelableEventHandler; + + /** + * Overrides wizard steps navigation with a custom one when it is rendered on the side. + */ + customNavigationSide?: React.ReactNode; + + /** + * Overrides wizard steps navigation with a custom one when it is rendered on the top. + */ + customNavigationTop?: React.ReactNode; } export namespace WizardProps { diff --git a/src/wizard/internal.tsx b/src/wizard/internal.tsx index fb82f3b807..0298ebc4bb 100644 --- a/src/wizard/internal.tsx +++ b/src/wizard/internal.tsx @@ -45,6 +45,8 @@ export default function InternalWizard({ onCancel, onSubmit, onNavigate, + customNavigationSide, + customNavigationTop, __internalRootRef, __injectAnalyticsComponentMetadata = false, ...rest @@ -179,23 +181,30 @@ export default function InternalWizard({
    -