- {status === Statuses.Visited || status === Statuses.Next ? (
+ {status === StepStatusValues.Visited || status === StepStatusValues.Next ? (
{
evt.preventDefault();
- if (status === Statuses.Visited) {
+ if (status === StepStatusValues.Visited) {
onStepClick(index);
} else {
onSkipToClick(index);
}
}}
variant="primary"
+ nativeAttributes={step.isOptional ? { 'aria-describedby': optionalDescriptionId } : undefined}
>
{step.title}
) : (
{step.title}
diff --git a/src/wizard/wizard-step-list.tsx b/src/wizard/wizard-step-list.tsx
new file mode 100644
index 0000000000..ccde68bf17
--- /dev/null
+++ b/src/wizard/wizard-step-list.tsx
@@ -0,0 +1,209 @@
+// 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 { useUniqueId } from '@cloudscape-design/component-toolkit/internal';
+
+import { getNavigationActionDetail } from './analytics-metadata/utils';
+import { WizardProps } from './interfaces';
+
+import analyticsSelectors from './analytics-metadata/styles.css.js';
+import styles from './styles.css.js';
+
+export const StepStatusValues = {
+ Active: 'active',
+ Visited: 'visited',
+ Unvisited: 'unvisited',
+ Next: 'next',
+} as const;
+
+export type StepStatus = (typeof StepStatusValues)[keyof typeof StepStatusValues];
+
+export interface WizardStepListProps {
+ activeStepIndex: number;
+ farthestStepIndex: number;
+ allowSkipTo: boolean;
+ i18nStrings: WizardProps.I18nStrings;
+ isLoadingNextStep: boolean;
+ onStepClick: (stepIndex: number) => void;
+ onSkipToClick: (stepIndex: number) => void;
+ steps: ReadonlyArray
;
+}
+
+export function getStepStatus(
+ index: number,
+ activeStepIndex: number,
+ farthestStepIndex: number,
+ isLoadingNextStep: boolean,
+ allowSkipTo: boolean,
+ steps: ReadonlyArray<{ isOptional?: boolean }>
+): StepStatus {
+ if (activeStepIndex === index) {
+ return StepStatusValues.Active;
+ }
+ if (isLoadingNextStep) {
+ return StepStatusValues.Unvisited;
+ }
+ if (farthestStepIndex >= index) {
+ return StepStatusValues.Visited;
+ }
+ if (allowSkipTo && index > activeStepIndex) {
+ // Can we skip to this step? (all steps between current and this one are optional)
+ if (canSkip(activeStepIndex + 1, index, steps)) {
+ return StepStatusValues.Next;
+ }
+ // Immediate next step is also navigable if it's optional
+ if (index === activeStepIndex + 1 && steps[index]?.isOptional) {
+ return StepStatusValues.Next;
+ }
+ }
+ return StepStatusValues.Unvisited;
+}
+
+export function canSkip(fromIndex: number, toIndex: number, steps: ReadonlyArray<{ isOptional?: boolean }>): boolean {
+ // Can't skip if there are no steps to skip over
+ if (fromIndex >= toIndex) {
+ return false;
+ }
+ for (let i = fromIndex; i < toIndex; i++) {
+ if (!steps[i].isOptional) {
+ return false;
+ }
+ }
+ return true;
+}
+
+export function handleStepNavigation(
+ stepIndex: number,
+ status: StepStatus,
+ onStepClick: (index: number) => void,
+ onSkipToClick: (index: number) => void
+): void {
+ if (status === StepStatusValues.Visited) {
+ onStepClick(stepIndex);
+ } else if (status === StepStatusValues.Next) {
+ onSkipToClick(stepIndex);
+ }
+}
+
+export default function WizardStepList({
+ activeStepIndex,
+ farthestStepIndex,
+ allowSkipTo,
+ i18nStrings,
+ isLoadingNextStep,
+ onStepClick,
+ onSkipToClick,
+ steps,
+}: WizardStepListProps) {
+ return (
+
+ {steps.map((step, index) => (
+
+ ))}
+
+ );
+}
+
+interface WizardStepListItemProps {
+ index: number;
+ step: WizardProps.Step;
+ activeStepIndex: number;
+ farthestStepIndex: number;
+ allowSkipTo: boolean;
+ i18nStrings: WizardProps.I18nStrings;
+ isLoadingNextStep: boolean;
+ onStepClick: (stepIndex: number) => void;
+ onSkipToClick: (stepIndex: number) => void;
+ steps: ReadonlyArray;
+}
+
+function WizardStepListItem({
+ index,
+ step,
+ activeStepIndex,
+ farthestStepIndex,
+ allowSkipTo,
+ i18nStrings,
+ isLoadingNextStep,
+ onStepClick,
+ onSkipToClick,
+ steps,
+}: WizardStepListItemProps) {
+ const status = getStepStatus(index, activeStepIndex, farthestStepIndex, isLoadingNextStep, allowSkipTo, steps);
+ const isClickable = status === StepStatusValues.Visited || status === StepStatusValues.Next;
+ const stepLabel = i18nStrings.stepNumberLabel?.(index + 1);
+ const fullStepLabel = `${stepLabel}: ${step.title}`;
+ const optionalDescriptionId = useUniqueId('wizard-step-optional-');
+ const state = {
+ active: 'active',
+ unvisited: 'disabled',
+ visited: 'enabled',
+ next: 'enabled',
+ }[status];
+
+ const handleInteraction = () => handleStepNavigation(index, status, onStepClick, onSkipToClick);
+
+ return (
+
+
+
+
+ {i18nStrings.stepNumberLabel?.(index + 1)}
+ {step.isOptional && {` - ${i18nStrings.optional}`}}
+
+
+ {
+ event.preventDefault();
+ handleInteraction();
+ }}
+ onKeyDown={event => {
+ if (event.key === ' ' || event.key === 'Enter') {
+ event.preventDefault();
+ }
+ // Enter activates the button on key down instead of key up.
+ if (event.key === 'Enter') {
+ handleInteraction();
+ }
+ }}
+ onKeyUp={event => {
+ // Emulate button behavior, which also fires on space.
+ if (event.key === ' ') {
+ handleInteraction();
+ }
+ }}
+ role="button"
+ tabIndex={isClickable ? 0 : undefined}
+ aria-label={fullStepLabel}
+ aria-describedby={step.isOptional ? optionalDescriptionId : undefined}
+ {...(status === StepStatusValues.Unvisited
+ ? {}
+ : getNavigationActionDetail(index, 'step', true, `.${analyticsSelectors['step-title']}`))}
+ >
+
+
+ {step.title}
+
+
+ );
+}
diff --git a/src/wizard/wizard-step-navigation-expandable.tsx b/src/wizard/wizard-step-navigation-expandable.tsx
new file mode 100644
index 0000000000..0c80807308
--- /dev/null
+++ b/src/wizard/wizard-step-navigation-expandable.tsx
@@ -0,0 +1,67 @@
+// 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 InternalExpandableSection from '../expandable-section/internal';
+import { WizardProps } from './interfaces';
+import WizardStepList from './wizard-step-list';
+
+import styles from './styles.css.js';
+
+interface WizardStepNavigationExpandableProps {
+ activeStepIndex: number;
+ farthestStepIndex: number;
+ allowSkipTo: boolean;
+ i18nStrings: WizardProps.I18nStrings;
+ isLoadingNextStep: boolean;
+ onStepClick: (stepIndex: number) => void;
+ onSkipToClick: (stepIndex: number) => void;
+ steps: ReadonlyArray;
+ expanded: boolean;
+ onExpandChange: (expanded: boolean) => void;
+}
+
+export default function WizardStepNavigationExpandable({
+ activeStepIndex,
+ farthestStepIndex,
+ allowSkipTo,
+ i18nStrings,
+ isLoadingNextStep,
+ onStepClick,
+ onSkipToClick,
+ steps,
+ expanded,
+ onExpandChange,
+}: WizardStepNavigationExpandableProps) {
+ const collapsedStepsLabel = i18nStrings.collapsedStepsLabel?.(activeStepIndex + 1, steps.length);
+ const headerAriaLabel = collapsedStepsLabel
+ ? `${collapsedStepsLabel} - ${i18nStrings.navigationAriaLabel ?? 'Steps'}`
+ : undefined;
+
+ return (
+ onExpandChange(detail.expanded)}
+ >
+
+
+ );
+}