From ba66f1757791bee67cd68595a13198ae5e188877 Mon Sep 17 00:00:00 2001 From: Philipp Schneider Date: Thu, 22 Jan 2026 21:58:48 +0100 Subject: [PATCH 01/37] feat: first test for new wizard --- src/i18n/messages/all.en.json | 7 +- src/wizard/interfaces.ts | 3 + src/wizard/internal.tsx | 5 + src/wizard/styles.scss | 153 ++++++++++++++++- src/wizard/wizard-form.tsx | 16 +- src/wizard/wizard-step-navigation-modal.tsx | 175 ++++++++++++++++++++ 6 files changed, 354 insertions(+), 5 deletions(-) create mode 100644 src/wizard/wizard-step-navigation-modal.tsx diff --git a/src/i18n/messages/all.en.json b/src/i18n/messages/all.en.json index 7e88102e91..4131b2d35d 100644 --- a/src/i18n/messages/all.en.json +++ b/src/i18n/messages/all.en.json @@ -468,6 +468,9 @@ "i18nStrings.nextButton": "Next", "i18nStrings.optional": "optional", "i18nStrings.nextButtonLoadingAnnouncement": "Loading next step", - "i18nStrings.submitButtonLoadingAnnouncement": "Submitting form" + "i18nStrings.submitButtonLoadingAnnouncement": "Submitting form", + "i18nStrings.stepNavigationTitle": "Step", + "i18nStrings.stepNavigationDismissAriaLabel": "Close step navigation", + "i18nStrings.stepNavigationConfirmButton": "Ok" } -} \ No newline at end of file +} diff --git a/src/wizard/interfaces.ts b/src/wizard/interfaces.ts index 97fb647e38..741c0f01c6 100644 --- a/src/wizard/interfaces.ts +++ b/src/wizard/interfaces.ts @@ -170,6 +170,9 @@ export namespace WizardProps { optional?: string; nextButtonLoadingAnnouncement?: string; submitButtonLoadingAnnouncement?: string; + stepNavigationTitle?: string; + stepNavigationDismissAriaLabel?: string; + stepNavigationConfirmButton?: string; } export interface NavigateDetail { diff --git a/src/wizard/internal.tsx b/src/wizard/internal.tsx index ad0709ae55..039082e496 100644 --- a/src/wizard/internal.tsx +++ b/src/wizard/internal.tsx @@ -136,6 +136,9 @@ export default function InternalWizard({ previousButton: i18n('i18nStrings.previousButton', rest.i18nStrings?.previousButton), nextButton: i18n('i18nStrings.nextButton', rest.i18nStrings?.nextButton), optional: i18n('i18nStrings.optional', rest.i18nStrings?.optional), + stepNavigationTitle: rest.i18nStrings?.stepNavigationTitle, + stepNavigationDismissAriaLabel: rest.i18nStrings?.stepNavigationDismissAriaLabel, + stepNavigationConfirmButton: rest.i18nStrings?.stepNavigationConfirmButton, }; if (activeStepIndex && activeStepIndex >= steps.length) { @@ -201,12 +204,14 @@ export default function InternalWizard({ i18nStrings={i18nStrings} submitButtonText={submitButtonText} activeStepIndex={actualActiveStepIndex} + farthestStepIndex={farthestStepIndex.current} isPrimaryLoading={isLoadingNextStep} allowSkipTo={allowSkipTo} customPrimaryActions={customPrimaryActions} secondaryActions={secondaryActions} onCancelClick={onCancelClick} onPreviousClick={onPreviousClick} + onStepClick={onStepClick} onSkipToClick={onSkipToClick} onPrimaryClick={onPrimaryClick} /> diff --git a/src/wizard/styles.scss b/src/wizard/styles.scss index d5f2341412..e184a84a92 100644 --- a/src/wizard/styles.scss +++ b/src/wizard/styles.scss @@ -261,14 +261,163 @@ } .collapsed-steps { - color: awsui.$color-text-heading-secondary; - font-weight: styles.$font-weight-bold; padding-block-start: awsui.$space-scaled-xxs; &-hidden { display: none; } } +.collapsed-steps-navigation { + display: inline-block; +} + +.collapsed-steps-trigger { + @include styles.styles-reset; + display: inline-flex; + align-items: center; + gap: awsui.$space-xxs; + padding-block: 0; + padding-inline: 0; + border-block: none; + border-inline: none; + background: transparent; + color: awsui.$color-text-link-default; + font-weight: styles.$font-weight-bold; + font-size: inherit; + cursor: pointer; + + &:hover { + color: awsui.$color-text-link-hover; + } + + &:disabled { + color: awsui.$color-text-interactive-disabled; + cursor: default; + } + + @include focus-visible.when-visible { + @include styles.link-focus; + } +} + +.collapsed-steps-trigger-label { + /* used for test utils */ +} + +.step-navigation-list { + list-style: none; + margin-block: 0; + margin-inline: 0; + padding-block: 0; + padding-inline: 0; +} + +.step-navigation-item { + position: relative; + padding-inline-start: awsui.$space-l; + margin-block-end: awsui.$space-s; + + &:not(:last-child)::before { + content: ''; + position: absolute; + inset-inline-start: calc(#{awsui.$space-l} / 2 - 1px); + inset-block-start: calc(#{awsui.$space-m} + 6px); + block-size: calc(100% + #{awsui.$space-s} - 6px); + inline-size: 2px; + background-color: awsui.$color-border-divider-default; + } + + &:last-child { + margin-block-end: 0; + } +} + +.step-navigation-button { + @include styles.styles-reset; + display: flex; + align-items: flex-start; + gap: awsui.$space-xs; + padding-block: 0; + padding-inline: 0; + border-block: none; + border-inline: none; + background: transparent; + cursor: pointer; + text-align: start; + inline-size: 100%; + + &:disabled { + cursor: default; + } + + @include focus-visible.when-visible { + @include styles.link-focus; + } +} + +.step-navigation-button-disabled { + cursor: default; +} + +.step-navigation-circle { + flex-shrink: 0; + inline-size: 12px; + block-size: 12px; + margin-block-start: 4px; + border-start-start-radius: 50%; + border-start-end-radius: 50%; + border-end-start-radius: 50%; + border-end-end-radius: 50%; + border-block: 2px solid awsui.$color-text-interactive-disabled; + border-inline: 2px solid awsui.$color-text-interactive-disabled; + background-color: awsui.$color-background-container-content; + box-sizing: border-box; +} + +.step-navigation-circle-selected { + border-color: awsui.$color-background-control-checked; + background-color: awsui.$color-background-control-checked; + box-shadow: + 0 0 0 2px awsui.$color-background-container-content, + 0 0 0 4px awsui.$color-background-control-checked; +} + +.step-navigation-circle-visited { + border-color: awsui.$color-text-interactive-default; +} + +.step-navigation-circle-unvisited { + border-color: awsui.$color-text-interactive-disabled; +} + +.step-navigation-content { + display: flex; + flex-direction: column; +} + +.step-navigation-label { + font-size: awsui.$font-size-body-s; + color: awsui.$color-text-small; +} + +.step-navigation-title { + font-size: awsui.$font-size-body-m; + color: awsui.$color-text-body-default; +} + +.step-navigation-title-selected { + color: awsui.$color-background-control-checked; + font-weight: styles.$font-weight-bold; +} + +.step-navigation-title-visited { + color: awsui.$color-text-interactive-default; +} + +.step-navigation-title-unvisited { + color: awsui.$color-text-status-inactive; +} + .form-header-component { &-wrapper { outline: none; diff --git a/src/wizard/wizard-form.tsx b/src/wizard/wizard-form.tsx index 1db4b301e5..5475f26923 100644 --- a/src/wizard/wizard-form.tsx +++ b/src/wizard/wizard-form.tsx @@ -25,12 +25,14 @@ import { useEffectOnUpdate } from '../internal/hooks/use-effect-on-update'; import { WizardProps } from './interfaces'; import WizardActions from './wizard-actions'; import WizardFormHeader from './wizard-form-header'; +import WizardStepNavigationModal from './wizard-step-navigation-modal'; import styles from './styles.css.js'; interface WizardFormProps extends InternalBaseComponentProps { steps: ReadonlyArray; activeStepIndex: number; + farthestStepIndex: number; showCollapsedSteps: boolean; i18nStrings: WizardProps.I18nStrings; submitButtonText?: string; @@ -41,6 +43,7 @@ interface WizardFormProps extends InternalBaseComponentProps { onCancelClick: () => void; onPreviousClick: () => void; onPrimaryClick: () => void; + onStepClick: (stepIndex: number) => void; onSkipToClick: (stepIndex: number) => void; } @@ -75,6 +78,7 @@ function WizardForm({ stepHeaderRef, steps, activeStepIndex, + farthestStepIndex, showCollapsedSteps, i18nStrings, submitButtonText, @@ -85,6 +89,7 @@ function WizardForm({ onCancelClick, onPreviousClick, onPrimaryClick, + onStepClick, onSkipToClick, }: WizardFormProps & { stepHeaderRef: MutableRefObject }) { const rootRef = useRef(); @@ -132,7 +137,16 @@ function WizardForm({ <>
- {i18nStrings.collapsedStepsLabel?.(activeStepIndex + 1, steps.length)} +
void; + onSkipToClick: (stepIndex: number) => void; + steps: ReadonlyArray; +} + +export default function WizardStepNavigationModal({ + activeStepIndex, + farthestStepIndex, + allowSkipTo, + i18nStrings, + isLoadingNextStep, + onStepClick, + onSkipToClick, + steps, +}: WizardStepNavigationModalProps) { + const [isModalVisible, setIsModalVisible] = useState(false); + const [selectedStepIndex, setSelectedStepIndex] = useState(activeStepIndex); + + const collapsedStepsLabel = i18nStrings.collapsedStepsLabel?.(activeStepIndex + 1, steps.length); + + // A step is reachable if it's the current step, a previously visited step, or the immediate next step + const isStepReachable = (index: number): boolean => { + if (isLoadingNextStep) { + return false; + } + // Current step or any previously visited step + if (index <= farthestStepIndex) { + return true; + } + // Immediate next step is always reachable + if (index === activeStepIndex + 1) { + return true; + } + // Skip-to: all intermediate steps must be optional + if (allowSkipTo) { + for (let i = activeStepIndex + 1; i < index; i++) { + if (!steps[i].isOptional) { + return false; + } + } + return true; + } + return false; + }; + + const handleOpenModal = () => { + setSelectedStepIndex(activeStepIndex); + setIsModalVisible(true); + }; + + const handleCloseModal = () => { + setIsModalVisible(false); + }; + + const handleConfirm = () => { + if (selectedStepIndex !== activeStepIndex) { + // Use 'skip' for forward navigation to unvisited steps, 'step' otherwise + if (selectedStepIndex > farthestStepIndex) { + onSkipToClick(selectedStepIndex); + } else { + onStepClick(selectedStepIndex); + } + } + setIsModalVisible(false); + }; + + const handleStepSelect = (stepIndex: number) => { + if (isStepReachable(stepIndex)) { + setSelectedStepIndex(stepIndex); + } + }; + + return ( +
+ + + + + + {i18nStrings.cancelButton} + + + {i18nStrings.stepNavigationConfirmButton ?? 'Ok'} + + + + } + > +
    + {steps.map((step, index) => { + const isSelected = selectedStepIndex === index; + const isReachable = isStepReachable(index); + const stepLabel = i18nStrings.stepNumberLabel?.(index + 1) ?? `Step ${index + 1}`; + + // Simple: selected (highlighted) or reachable (clickable) or unreachable (disabled) + const displayStatus = isSelected ? 'selected' : isReachable ? 'visited' : 'unvisited'; + + return ( +
  • + +
  • + ); + })} +
+
+
+ ); +} From 37f59b73f0db6bed6ed21a2bc5e9a7ebedcffb72 Mon Sep 17 00:00:00 2001 From: Philipp Schneider Date: Mon, 26 Jan 2026 09:13:40 +0100 Subject: [PATCH 02/37] feat: Add example with popover for wizard --- pages/wizard/common.ts | 10 +- pages/wizard/step-navigation.page.tsx | 215 ++++++++++++++++++ src/wizard/styles.scss | 84 +++++++ src/wizard/wizard-form.tsx | 39 +++- .../wizard-step-navigation-dropdown.tsx | 174 ++++++++++++++ 5 files changed, 511 insertions(+), 11 deletions(-) create mode 100644 pages/wizard/step-navigation.page.tsx create mode 100644 src/wizard/wizard-step-navigation-dropdown.tsx diff --git a/pages/wizard/common.ts b/pages/wizard/common.ts index 4d4241e37d..da2e2d1f3a 100644 --- a/pages/wizard/common.ts +++ b/pages/wizard/common.ts @@ -17,4 +17,12 @@ const i18nStrings: WizardProps.I18nStrings = { submitButtonLoadingAnnouncement: 'Submitting form', }; -export { i18nStrings }; +// i18n strings with modal navigation support +const i18nStringsWithModal: WizardProps.I18nStrings = { + ...i18nStrings, + stepNavigationTitle: 'Go to step', + stepNavigationDismissAriaLabel: 'Close step navigation', + stepNavigationConfirmButton: 'Go', +}; + +export { i18nStrings, i18nStringsWithModal }; diff --git a/pages/wizard/step-navigation.page.tsx b/pages/wizard/step-navigation.page.tsx new file mode 100644 index 0000000000..d6e8416bdf --- /dev/null +++ b/pages/wizard/step-navigation.page.tsx @@ -0,0 +1,215 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React, { useState } from 'react'; + +import Box from '~components/box'; +import ColumnLayout from '~components/column-layout'; +import Container from '~components/container'; +import FormField from '~components/form-field'; +import Header from '~components/header'; +import Input from '~components/input'; +import Link from '~components/link'; +import RadioGroup from '~components/radio-group'; +import SpaceBetween from '~components/space-between'; +import Toggle from '~components/toggle'; +import Wizard, { WizardProps } from '~components/wizard'; + +import { i18nStrings, i18nStringsWithModal } from './common'; + +import styles from './styles.scss'; + +const steps: WizardProps.Step[] = [ + { + title: 'Choose distribution settings', + description: 'Configure the basic settings for your distribution.', + content: ( + Distribution settings}> + + + {}} /> + + + {}} /> + + + + ), + }, + { + title: 'Configure origin settings', + isOptional: true, + description: 'Specify where your content originates from.', + content: ( + Origin settings}> + + + {}} /> + + + {}} + /> + + + + ), + }, + { + title: 'Configure cache behavior', + isOptional: true, + description: 'Define how CloudFront caches your content.', + content: ( + Cache behavior settings}> + + + {}} /> + + + {}} + /> + + + + ), + }, + { + title: 'Configure SSL certificate', + isOptional: true, + content: ( + SSL certificate}> + + {}} + /> + + + ), + }, + { + title: 'Configure logging', + isOptional: true, + content: ( + Logging settings}> + + + {}}> + Log requests to S3 bucket + + + + + ), + }, + { + title: 'Review and create', + info: Info, + description: 'Review your configuration before creating the distribution.', + content: ( + + Distribution summary}> + +
+ Distribution name + my-distribution +
+
+ Origin domain + example.com +
+
+ Protocol policy + HTTPS only +
+
+ Cache behavior + Redirect HTTP to HTTPS +
+
+
+
+ ), + }, +]; + +export default function StepNavigationPage() { + const [activeStepIndex, setActiveStepIndex] = useState(0); + const [smallContainer, setSmallContainer] = useState(true); + const [useModalNavigation, setUseModalNavigation] = useState(false); + const [allowSkipTo, setAllowSkipTo] = useState(true); + const [isLoading, setIsLoading] = useState(false); + + const currentI18nStrings = useModalNavigation ? i18nStringsWithModal : i18nStrings; + + return ( + + + Step Navigation Demo Options}> + + setSmallContainer(detail.checked)}> + Small container (shows collapsed step navigation) + + setUseModalNavigation(detail.checked)}> + Use modal navigation (requires additional i18n strings) + + setAllowSkipTo(detail.checked)}> + Allow skip to (enables skipping optional steps) + + setIsLoading(detail.checked)}> + Loading state (disables navigation) + + + + + +
+ {useModalNavigation ? 'Modal Navigation Variant' : 'Dropdown Navigation Variant (Default)'} +
+ + {useModalNavigation + ? 'Using modal for step navigation. Requires stepNavigationTitle, stepNavigationConfirmButton, and cancelButton i18n strings.' + : 'Using dropdown for step navigation. No additional i18n strings required - works with basic i18n strings.'} + +
+ +
+ { + console.log('Navigate:', e.detail); + setActiveStepIndex(e.detail.requestedStepIndex); + }} + onCancel={() => console.log('Cancelled')} + onSubmit={() => console.log('Submitted')} + /> +
+
+
+ ); +} diff --git a/src/wizard/styles.scss b/src/wizard/styles.scss index e184a84a92..80fb15c33d 100644 --- a/src/wizard/styles.scss +++ b/src/wizard/styles.scss @@ -418,6 +418,90 @@ color: awsui.$color-text-status-inactive; } +/* Dropdown variant styles */ +.step-navigation-dropdown-list { + list-style: none; + margin-block: 0; + margin-inline: 0; + padding-block: awsui.$space-xs; + padding-inline: 0; + min-inline-size: 280px; + max-inline-size: 400px; +} + +.step-navigation-dropdown-item { + display: flex; + align-items: flex-start; + gap: awsui.$space-xs; + padding-block: awsui.$space-xs; + padding-inline: awsui.$space-m; + cursor: pointer; + + &:hover:not(.step-navigation-dropdown-item-disabled) { + background-color: awsui.$color-background-dropdown-item-hover; + } +} + +.step-navigation-dropdown-item-active { + background-color: awsui.$color-background-dropdown-item-selected; +} + +.step-navigation-dropdown-item-disabled { + cursor: default; +} + +.step-navigation-dropdown-circle { + flex-shrink: 0; + inline-size: 12px; + block-size: 12px; + margin-block-start: 4px; + border-start-start-radius: 50%; + border-start-end-radius: 50%; + border-end-start-radius: 50%; + border-end-end-radius: 50%; + border-block: 2px solid awsui.$color-text-interactive-disabled; + border-inline: 2px solid awsui.$color-text-interactive-disabled; + background-color: awsui.$color-background-container-content; + box-sizing: border-box; +} + +.step-navigation-dropdown-circle-active { + border-color: awsui.$color-background-control-checked; + background-color: awsui.$color-background-control-checked; + box-shadow: + 0 0 0 2px awsui.$color-background-container-content, + 0 0 0 4px awsui.$color-background-control-checked; +} + +.step-navigation-dropdown-circle-reachable { + border-color: awsui.$color-text-interactive-default; +} + +.step-navigation-dropdown-circle-disabled { + border-color: awsui.$color-text-interactive-disabled; +} + +.step-navigation-dropdown-content { + display: flex; + flex-direction: column; + min-inline-size: 0; +} + +.step-navigation-dropdown-title { + @include styles.text-wrapping; + font-size: awsui.$font-size-body-m; + color: awsui.$color-text-body-default; +} + +.step-navigation-dropdown-title-active { + color: awsui.$color-background-control-checked; + font-weight: styles.$font-weight-bold; +} + +.step-navigation-dropdown-title-disabled { + color: awsui.$color-text-status-inactive; +} + .form-header-component { &-wrapper { outline: none; diff --git a/src/wizard/wizard-form.tsx b/src/wizard/wizard-form.tsx index 5475f26923..1cbf20d040 100644 --- a/src/wizard/wizard-form.tsx +++ b/src/wizard/wizard-form.tsx @@ -25,10 +25,13 @@ import { useEffectOnUpdate } from '../internal/hooks/use-effect-on-update'; import { WizardProps } from './interfaces'; import WizardActions from './wizard-actions'; import WizardFormHeader from './wizard-form-header'; +import WizardStepNavigationDropdown from './wizard-step-navigation-dropdown'; import WizardStepNavigationModal from './wizard-step-navigation-modal'; import styles from './styles.css.js'; +export type StepNavigationVariant = 'modal' | 'dropdown' | 'auto'; + interface WizardFormProps extends InternalBaseComponentProps { steps: ReadonlyArray; activeStepIndex: number; @@ -45,6 +48,7 @@ interface WizardFormProps extends InternalBaseComponentProps { onPrimaryClick: () => void; onStepClick: (stepIndex: number) => void; onSkipToClick: (stepIndex: number) => void; + stepNavigationVariant?: StepNavigationVariant; } export const STEP_NAME_SELECTOR = `[${DATA_ATTR_FUNNEL_KEY}="${FUNNEL_KEY_STEP_NAME}"]`; @@ -91,6 +95,7 @@ function WizardForm({ onPrimaryClick, onStepClick, onSkipToClick, + stepNavigationVariant = 'auto', }: WizardFormProps & { stepHeaderRef: MutableRefObject }) { const rootRef = useRef(); const ref = useMergeRefs(rootRef, __internalRootRef); @@ -133,20 +138,34 @@ function WizardForm({ } }, [funnelInteractionId, funnelIdentifier, isLastStep, errorText, __internalRootRef, errorSlotId, funnelStepInfo]); + // Determine which navigation variant to use + // 'auto' mode: use modal if i18n strings for it are provided, otherwise use dropdown + const hasModalI18nStrings = Boolean( + i18nStrings.stepNavigationTitle && i18nStrings.stepNavigationConfirmButton && i18nStrings.cancelButton + ); + const effectiveVariant = + stepNavigationVariant === 'auto' ? (hasModalI18nStrings ? 'modal' : 'dropdown') : stepNavigationVariant; + + const stepNavigationProps = { + activeStepIndex, + farthestStepIndex, + allowSkipTo, + i18nStrings, + isLoadingNextStep: isPrimaryLoading, + onStepClick, + onSkipToClick, + steps, + }; + return ( <>
- + {effectiveVariant === 'modal' ? ( + + ) : ( + + )}
void; + onSkipToClick: (stepIndex: number) => void; + steps: ReadonlyArray; +} + +export default function WizardStepNavigationDropdown({ + activeStepIndex, + farthestStepIndex, + allowSkipTo, + i18nStrings, + isLoadingNextStep, + onStepClick, + onSkipToClick, + steps, +}: WizardStepNavigationDropdownProps) { + const [isOpen, setIsOpen] = useState(false); + + const collapsedStepsLabel = i18nStrings.collapsedStepsLabel?.(activeStepIndex + 1, steps.length); + + // A step is reachable if it's the current step, a previously visited step, or the immediate next step + const isStepReachable = (index: number): boolean => { + if (isLoadingNextStep) { + return false; + } + // Current step or any previously visited step + if (index <= farthestStepIndex) { + return true; + } + // Immediate next step is always reachable + if (index === activeStepIndex + 1) { + return true; + } + // Skip-to: all intermediate steps must be optional + if (allowSkipTo) { + for (let i = activeStepIndex + 1; i < index; i++) { + if (!steps[i].isOptional) { + return false; + } + } + return true; + } + return false; + }; + + const handleStepClick = (stepIndex: number) => { + if (!isStepReachable(stepIndex)) { + return; + } + + // Close dropdown first + setIsOpen(false); + + // Don't navigate if already on this step + if (stepIndex === activeStepIndex) { + return; + } + + // Use 'skip' for forward navigation to unvisited steps, 'step' otherwise + if (stepIndex > farthestStepIndex) { + onSkipToClick(stepIndex); + } else { + onStepClick(stepIndex); + } + }; + + const trigger = ( + + ); + + return ( +
+ setIsOpen(false)} + trigger={trigger} + stretchWidth={false} + expandToViewport={false} + > + +
    + {steps.map((step, index) => { + const isActive = activeStepIndex === index; + const isReachable = isStepReachable(index); + const stepLabel = i18nStrings.stepNumberLabel?.(index + 1) ?? `Step ${index + 1}`; + + return ( +
  • handleStepClick(index)} + > +
  • + ); + })} +
+
+
+
+ ); +} From de32932e3771ac3a779effa2cc5099c191895664 Mon Sep 17 00:00:00 2001 From: Philipp Schneider Date: Thu, 29 Jan 2026 11:08:54 +0100 Subject: [PATCH 03/37] feat: Add example for expandable section --- pages/wizard/with-expandable-section.page.tsx | 183 ++++++++++++++++++ .../with-expandable-section.styles.scss | 104 ++++++++++ 2 files changed, 287 insertions(+) create mode 100644 pages/wizard/with-expandable-section.page.tsx create mode 100644 pages/wizard/with-expandable-section.styles.scss diff --git a/pages/wizard/with-expandable-section.page.tsx b/pages/wizard/with-expandable-section.page.tsx new file mode 100644 index 0000000000..274439df8e --- /dev/null +++ b/pages/wizard/with-expandable-section.page.tsx @@ -0,0 +1,183 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React, { useState } from 'react'; + +import Box from '~components/box'; +import Button from '~components/button'; +import Container from '~components/container'; +import ExpandableSection from '~components/expandable-section'; +import Form from '~components/form'; +import Header from '~components/header'; +import Link from '~components/link'; +import SpaceBetween from '~components/space-between'; + +import localStyles from './with-expandable-section.styles.scss'; + +interface Step { + title: string; + isOptional?: boolean; +} + +const steps: Step[] = [ + { title: 'Choose instance type' }, + { title: 'Add storage', isOptional: true }, + { title: 'Review and create' }, +]; + +interface StepNavigationProps { + steps: Step[]; + activeStepIndex: number; + expanded: boolean; + onExpandedChange: (expanded: boolean) => void; + onStepClick: (index: number) => void; +} + +function StepNavigation({ steps, activeStepIndex, expanded, onExpandedChange, onStepClick }: StepNavigationProps) { + return ( + onExpandedChange(detail.expanded)} + > +
    + {steps.map((step, index) => { + const isActive = index === activeStepIndex; + const isVisited = index < activeStepIndex; + const status = isActive ? 'active' : isVisited ? 'visited' : 'unvisited'; + + return ( +
  • +
    +
    + {index < steps.length - 1 &&
    } +
    +
    + + Step {index + 1} + {step.isOptional && - optional} + + {isActive ? ( + + {step.title} + + ) : ( + onStepClick(index)}> + {step.title} + + )} +
    +
  • + ); + })} +
+
+ ); +} + +export default function WizardWithExpandableSectionPage() { + const [activeStepIndex, setActiveStepIndex] = useState(0); + const [navigationExpanded, setNavigationExpanded] = useState(true); + + const currentStep = steps[activeStepIndex]; + const isFirstStep = activeStepIndex === 0; + const isLastStep = activeStepIndex === steps.length - 1; + + const handlePrevious = () => { + if (!isFirstStep) { + setActiveStepIndex(activeStepIndex - 1); + } + }; + + const handleNext = () => { + if (!isLastStep) { + setActiveStepIndex(activeStepIndex + 1); + } + }; + + const renderStepContent = () => { + switch (activeStepIndex) { + case 0: + return ( + + + + Description + + SWAP WITH CONTENT + + + + + + Description + + SWAP WITH CONTENT + + + + + ); + case 1: + return ( + + Configure storage options for your instance. + + ); + case 2: + return ( + + Review your configuration and create the instance. + + ); + default: + return null; + } + }; + + return ( +
+
+ +
+
+
+ {currentStep.title} + {currentStep.isOptional && - optional} + + } + actions={ + + + {!isFirstStep && } + + + } + > + {renderStepContent()} +
+
+
+ ); +} diff --git a/pages/wizard/with-expandable-section.styles.scss b/pages/wizard/with-expandable-section.styles.scss new file mode 100644 index 0000000000..d411685fe6 --- /dev/null +++ b/pages/wizard/with-expandable-section.styles.scss @@ -0,0 +1,104 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + SPDX-License-Identifier: Apache-2.0 +*/ + +@use '~design-tokens' as awsui; + +.wizard-layout { + display: flex; + flex-direction: column; + gap: awsui.$space-static-l; + padding-block: awsui.$space-static-l; + padding-inline: awsui.$space-static-l; + + @media (min-width: 688px) { + flex-direction: row; + } +} + +.wizard-navigation { + flex-shrink: 0; + user-select: text; + + @media (min-width: 688px) { + inline-size: 240px; + } +} + +.wizard-content { + flex: 1; + min-inline-size: 0; +} + +.step-list { + list-style: none; + margin-block: 0; + margin-inline: 0; + padding-block: 0; + padding-inline: 0; + user-select: text; +} + +.step-item { + display: flex; + flex-direction: row; + gap: awsui.$space-static-s; + padding-block-end: awsui.$space-static-xs; +} + +.step-indicator { + display: flex; + flex-direction: column; + align-items: center; +} + +.step-circle { + inline-size: 20px; + block-size: 20px; + border-start-start-radius: 50%; + border-start-end-radius: 50%; + border-end-start-radius: 50%; + border-end-end-radius: 50%; + border-block: 2px solid; + border-inline: 2px solid; + flex-shrink: 0; + + &[data-status='active'] { + border-color: awsui.$color-background-control-checked; + background-color: awsui.$color-background-control-checked; + box-shadow: inset 0 0 0 3px awsui.$color-background-container-content; + } + + &[data-status='visited'] { + border-color: awsui.$color-text-interactive-default; + background-color: awsui.$color-text-interactive-default; + } + + &[data-status='unvisited'] { + border-color: awsui.$color-text-interactive-disabled; + background-color: transparent; + } +} + +.step-line { + inline-size: 2px; + flex: 1; + min-block-size: awsui.$space-static-l; + background-color: awsui.$color-border-divider-default; + margin-block: awsui.$space-static-xxs; +} + +.step-content { + padding-block-start: awsui.$space-static-xxs; + user-select: text; + cursor: default; +} + +.placeholder-content { + background-color: awsui.$color-background-layout-toggle-default; + border-start-start-radius: awsui.$border-radius-item; + border-start-end-radius: awsui.$border-radius-item; + border-end-start-radius: awsui.$border-radius-item; + border-end-end-radius: awsui.$border-radius-item; +} From 087e51dd98cdbc7eea8cd22218342a3f03c24f9f Mon Sep 17 00:00:00 2001 From: Philipp Schneider Date: Fri, 30 Jan 2026 07:50:23 +0100 Subject: [PATCH 04/37] feat: Wizard with expandable section --- src/wizard/internal.tsx | 7 +- src/wizard/styles.scss | 90 +++++++++++ src/wizard/wizard-form.tsx | 23 ++- .../wizard-step-navigation-expandable.tsx | 142 ++++++++++++++++++ 4 files changed, 253 insertions(+), 9 deletions(-) create mode 100644 src/wizard/wizard-step-navigation-expandable.tsx diff --git a/src/wizard/internal.tsx b/src/wizard/internal.tsx index 039082e496..6b08db78f9 100644 --- a/src/wizard/internal.tsx +++ b/src/wizard/internal.tsx @@ -1,6 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import React, { useRef } from 'react'; +import React, { useRef, useState } from 'react'; import clsx from 'clsx'; import { useMergeRefs, warnOnce } from '@cloudscape-design/component-toolkit/internal'; @@ -70,6 +70,9 @@ export default function InternalWizard({ const farthestStepIndex = useRef(actualActiveStepIndex); farthestStepIndex.current = Math.max(farthestStepIndex.current, actualActiveStepIndex); + // State for expandable step navigation (collapsed by default) + const [stepNavigationExpanded, setStepNavigationExpanded] = useState(false); + const isVisualRefresh = useVisualRefresh(); const isLastStep = actualActiveStepIndex >= steps.length - 1; @@ -214,6 +217,8 @@ export default function InternalWizard({ onStepClick={onStepClick} onSkipToClick={onSkipToClick} onPrimaryClick={onPrimaryClick} + stepNavigationExpanded={stepNavigationExpanded} + onStepNavigationExpandedChange={setStepNavigationExpanded} /> diff --git a/src/wizard/styles.scss b/src/wizard/styles.scss index 80fb15c33d..898f435f71 100644 --- a/src/wizard/styles.scss +++ b/src/wizard/styles.scss @@ -537,3 +537,93 @@ display: flex; justify-content: flex-end; } + +/* Expandable step navigation styles */ +.expandable-step-list { + list-style: none; + margin-block: 0; + margin-inline: 0; + padding-block: 0; + padding-inline: 0; +} + +.expandable-step-item { + display: grid; + grid-template-columns: awsui.$space-l 1fr; + column-gap: awsui.$space-xs; + padding-block: 0; + padding-inline: 0; +} + +.expandable-step-indicator { + display: flex; + flex-direction: column; + align-items: center; + grid-column: 1; + grid-row: 1 / span 2; +} + +.expandable-step-circle { + flex-shrink: 0; + inline-size: 12px; + block-size: 12px; + margin-block-start: 4px; + border-start-start-radius: 50%; + border-start-end-radius: 50%; + border-end-start-radius: 50%; + border-end-end-radius: 50%; + box-sizing: border-box; +} + +.expandable-step-circle-active { + background-color: awsui.$color-background-control-checked; + box-shadow: + 0 0 0 3px awsui.$color-background-container-content, + 0 0 0 5px awsui.$color-background-control-checked; +} + +.expandable-step-circle-visited { + background-color: awsui.$color-text-interactive-default; + border-block: 2px solid awsui.$color-text-interactive-default; + border-inline: 2px solid awsui.$color-text-interactive-default; +} + +.expandable-step-circle-next { + background-color: awsui.$color-text-interactive-default; + border-block: 2px solid awsui.$color-text-interactive-default; + border-inline: 2px solid awsui.$color-text-interactive-default; +} + +.expandable-step-circle-unvisited { + background-color: awsui.$color-background-container-content; + border-block: 2px solid awsui.$color-text-interactive-disabled; + border-inline: 2px solid awsui.$color-text-interactive-disabled; +} + +.expandable-step-line { + flex-grow: 1; + inline-size: 2px; + min-block-size: awsui.$space-m; + background-color: awsui.$color-border-divider-default; + margin-block: awsui.$space-xxs; +} + +.expandable-step-content { + display: flex; + flex-direction: column; + grid-column: 2; + padding-block-end: awsui.$space-xs; +} + +.expandable-step-title { + font-size: awsui.$font-size-body-m; +} + +.expandable-step-title-active { + color: awsui.$color-background-control-checked; + font-weight: styles.$font-weight-bold; +} + +.expandable-step-title-unvisited { + color: awsui.$color-text-status-inactive; +} diff --git a/src/wizard/wizard-form.tsx b/src/wizard/wizard-form.tsx index 1cbf20d040..6fd99b5876 100644 --- a/src/wizard/wizard-form.tsx +++ b/src/wizard/wizard-form.tsx @@ -26,11 +26,12 @@ import { WizardProps } from './interfaces'; import WizardActions from './wizard-actions'; import WizardFormHeader from './wizard-form-header'; import WizardStepNavigationDropdown from './wizard-step-navigation-dropdown'; +import WizardStepNavigationExpandable from './wizard-step-navigation-expandable'; import WizardStepNavigationModal from './wizard-step-navigation-modal'; import styles from './styles.css.js'; -export type StepNavigationVariant = 'modal' | 'dropdown' | 'auto'; +export type StepNavigationVariant = 'modal' | 'dropdown' | 'expandable' | 'auto'; interface WizardFormProps extends InternalBaseComponentProps { steps: ReadonlyArray; @@ -49,6 +50,8 @@ interface WizardFormProps extends InternalBaseComponentProps { onStepClick: (stepIndex: number) => void; onSkipToClick: (stepIndex: number) => void; stepNavigationVariant?: StepNavigationVariant; + stepNavigationExpanded?: boolean; + onStepNavigationExpandedChange?: (expanded: boolean) => void; } export const STEP_NAME_SELECTOR = `[${DATA_ATTR_FUNNEL_KEY}="${FUNNEL_KEY_STEP_NAME}"]`; @@ -96,6 +99,8 @@ function WizardForm({ onStepClick, onSkipToClick, stepNavigationVariant = 'auto', + stepNavigationExpanded = false, + onStepNavigationExpandedChange, }: WizardFormProps & { stepHeaderRef: MutableRefObject }) { const rootRef = useRef(); const ref = useMergeRefs(rootRef, __internalRootRef); @@ -139,12 +144,8 @@ function WizardForm({ }, [funnelInteractionId, funnelIdentifier, isLastStep, errorText, __internalRootRef, errorSlotId, funnelStepInfo]); // Determine which navigation variant to use - // 'auto' mode: use modal if i18n strings for it are provided, otherwise use dropdown - const hasModalI18nStrings = Boolean( - i18nStrings.stepNavigationTitle && i18nStrings.stepNavigationConfirmButton && i18nStrings.cancelButton - ); - const effectiveVariant = - stepNavigationVariant === 'auto' ? (hasModalI18nStrings ? 'modal' : 'dropdown') : stepNavigationVariant; + // 'auto' mode: default to expandable section for better accessibility + const effectiveVariant = stepNavigationVariant === 'auto' ? 'expandable' : stepNavigationVariant; const stepNavigationProps = { activeStepIndex, @@ -161,7 +162,13 @@ function WizardForm({ <>
- {effectiveVariant === 'modal' ? ( + {effectiveVariant === 'expandable' ? ( + {})} + /> + ) : effectiveVariant === 'modal' ? ( ) : ( diff --git a/src/wizard/wizard-step-navigation-expandable.tsx b/src/wizard/wizard-step-navigation-expandable.tsx new file mode 100644 index 0000000000..edeb560fb0 --- /dev/null +++ b/src/wizard/wizard-step-navigation-expandable.tsx @@ -0,0 +1,142 @@ +// 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 InternalBox from '../box/internal'; +import InternalExpandableSection from '../expandable-section/internal'; +import InternalLink from '../link/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'; + +interface WizardStepNavigationExpandableProps { + activeStepIndex: number; + farthestStepIndex: number; + allowSkipTo: boolean; + expanded: boolean; + onExpandedChange: (expanded: boolean) => void; + i18nStrings: WizardProps.I18nStrings; + isLoadingNextStep: boolean; + onStepClick: (stepIndex: number) => void; + onSkipToClick: (stepIndex: number) => void; + steps: ReadonlyArray; +} + +type StepStatus = 'active' | 'visited' | 'unvisited' | 'next'; + +export default function WizardStepNavigationExpandable({ + activeStepIndex, + farthestStepIndex, + allowSkipTo, + expanded, + onExpandedChange, + i18nStrings, + isLoadingNextStep, + onStepClick, + onSkipToClick, + steps, +}: WizardStepNavigationExpandableProps) { + const collapsedStepsLabel = i18nStrings.collapsedStepsLabel?.(activeStepIndex + 1, steps.length); + + const getStepStatus = (index: number): StepStatus => { + if (activeStepIndex === index) { + return 'active'; + } + if (isLoadingNextStep) { + return 'unvisited'; + } + if (farthestStepIndex >= index) { + return 'visited'; + } + if (allowSkipTo && canSkip(activeStepIndex + 1, index)) { + return 'next'; + } + return 'unvisited'; + }; + + const canSkip = (fromIndex: number, toIndex: number): boolean => { + let index = fromIndex; + while (index < toIndex) { + if (!steps[index].isOptional) { + return false; + } + index++; + } + return true; + }; + + const handleStepClick = (stepIndex: number, status: StepStatus) => { + if (status === 'visited') { + onStepClick(stepIndex); + } else if (status === 'next') { + onSkipToClick(stepIndex); + } + }; + + return ( + onExpandedChange(detail.expanded)} + > +
    + {steps.map((step, index) => { + const status = getStepStatus(index); + const isClickable = status === 'visited' || status === 'next'; + const stepLabel = i18nStrings.stepNumberLabel?.(index + 1) ?? `Step ${index + 1}`; + const optionalLabel = step.isOptional ? ` - ${i18nStrings.optional}` : ''; + + return ( +
  • +
    +
  • + ); + })} +
+
+ ); +} From bce987ce8d63304c348959720d0e4c3f3c65d080 Mon Sep 17 00:00:00 2001 From: Philipp Schneider Date: Fri, 30 Jan 2026 08:23:05 +0100 Subject: [PATCH 05/37] chore: Update snapshot tests --- .../__snapshots__/documenter.test.ts.snap | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap index 59280b81d2..af89750a73 100644 --- a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap +++ b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap @@ -30605,6 +30605,21 @@ Defaults to \`false\`.", "optional": true, "type": "((targetStep: WizardProps.Step, targetStepNumber: number) => string)", }, + { + "name": "stepNavigationConfirmButton", + "optional": true, + "type": "string", + }, + { + "name": "stepNavigationDismissAriaLabel", + "optional": true, + "type": "string", + }, + { + "name": "stepNavigationTitle", + "optional": true, + "type": "string", + }, { "inlineType": { "name": "(stepNumber: number) => string", From 916815dca446e050458d21cb2f9fa1797016262e Mon Sep 17 00:00:00 2001 From: Philipp Schneider Date: Mon, 2 Feb 2026 11:35:34 +0100 Subject: [PATCH 06/37] feat: Update tests and refactor files for wizard --- pages/wizard/common.ts | 1 + pages/wizard/with-expandable-section.page.tsx | 183 ------------------ .../with-expandable-section.styles.scss | 104 ---------- src/wizard/__integ__/wizard.test.ts | 60 ++++++ src/wizard/interfaces.ts | 1 + src/wizard/internal.tsx | 5 +- src/wizard/styles.scss | 29 ++- src/wizard/wizard-navigation.tsx | 85 +++----- src/wizard/wizard-step-list.tsx | 136 +++++++++++++ .../wizard-step-navigation-expandable.tsx | 113 ++--------- 10 files changed, 273 insertions(+), 444 deletions(-) delete mode 100644 pages/wizard/with-expandable-section.page.tsx delete mode 100644 pages/wizard/with-expandable-section.styles.scss create mode 100644 src/wizard/wizard-step-list.tsx diff --git a/pages/wizard/common.ts b/pages/wizard/common.ts index da2e2d1f3a..9537417185 100644 --- a/pages/wizard/common.ts +++ b/pages/wizard/common.ts @@ -15,6 +15,7 @@ const i18nStrings: WizardProps.I18nStrings = { optional: 'optional', nextButtonLoadingAnnouncement: 'Loading next step', submitButtonLoadingAnnouncement: 'Submitting form', + stepsNavigationLabel: 'Steps navigation', }; // i18n strings with modal navigation support diff --git a/pages/wizard/with-expandable-section.page.tsx b/pages/wizard/with-expandable-section.page.tsx deleted file mode 100644 index 274439df8e..0000000000 --- a/pages/wizard/with-expandable-section.page.tsx +++ /dev/null @@ -1,183 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -import React, { useState } from 'react'; - -import Box from '~components/box'; -import Button from '~components/button'; -import Container from '~components/container'; -import ExpandableSection from '~components/expandable-section'; -import Form from '~components/form'; -import Header from '~components/header'; -import Link from '~components/link'; -import SpaceBetween from '~components/space-between'; - -import localStyles from './with-expandable-section.styles.scss'; - -interface Step { - title: string; - isOptional?: boolean; -} - -const steps: Step[] = [ - { title: 'Choose instance type' }, - { title: 'Add storage', isOptional: true }, - { title: 'Review and create' }, -]; - -interface StepNavigationProps { - steps: Step[]; - activeStepIndex: number; - expanded: boolean; - onExpandedChange: (expanded: boolean) => void; - onStepClick: (index: number) => void; -} - -function StepNavigation({ steps, activeStepIndex, expanded, onExpandedChange, onStepClick }: StepNavigationProps) { - return ( - onExpandedChange(detail.expanded)} - > -
    - {steps.map((step, index) => { - const isActive = index === activeStepIndex; - const isVisited = index < activeStepIndex; - const status = isActive ? 'active' : isVisited ? 'visited' : 'unvisited'; - - return ( -
  • -
    -
    - {index < steps.length - 1 &&
    } -
    -
    - - Step {index + 1} - {step.isOptional && - optional} - - {isActive ? ( - - {step.title} - - ) : ( - onStepClick(index)}> - {step.title} - - )} -
    -
  • - ); - })} -
-
- ); -} - -export default function WizardWithExpandableSectionPage() { - const [activeStepIndex, setActiveStepIndex] = useState(0); - const [navigationExpanded, setNavigationExpanded] = useState(true); - - const currentStep = steps[activeStepIndex]; - const isFirstStep = activeStepIndex === 0; - const isLastStep = activeStepIndex === steps.length - 1; - - const handlePrevious = () => { - if (!isFirstStep) { - setActiveStepIndex(activeStepIndex - 1); - } - }; - - const handleNext = () => { - if (!isLastStep) { - setActiveStepIndex(activeStepIndex + 1); - } - }; - - const renderStepContent = () => { - switch (activeStepIndex) { - case 0: - return ( - - - - Description - - SWAP WITH CONTENT - - - - - - Description - - SWAP WITH CONTENT - - - - - ); - case 1: - return ( - - Configure storage options for your instance. - - ); - case 2: - return ( - - Review your configuration and create the instance. - - ); - default: - return null; - } - }; - - return ( -
-
- -
-
-
- {currentStep.title} - {currentStep.isOptional && - optional} - - } - actions={ - - - {!isFirstStep && } - - - } - > - {renderStepContent()} -
-
-
- ); -} diff --git a/pages/wizard/with-expandable-section.styles.scss b/pages/wizard/with-expandable-section.styles.scss deleted file mode 100644 index d411685fe6..0000000000 --- a/pages/wizard/with-expandable-section.styles.scss +++ /dev/null @@ -1,104 +0,0 @@ -/* - Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - SPDX-License-Identifier: Apache-2.0 -*/ - -@use '~design-tokens' as awsui; - -.wizard-layout { - display: flex; - flex-direction: column; - gap: awsui.$space-static-l; - padding-block: awsui.$space-static-l; - padding-inline: awsui.$space-static-l; - - @media (min-width: 688px) { - flex-direction: row; - } -} - -.wizard-navigation { - flex-shrink: 0; - user-select: text; - - @media (min-width: 688px) { - inline-size: 240px; - } -} - -.wizard-content { - flex: 1; - min-inline-size: 0; -} - -.step-list { - list-style: none; - margin-block: 0; - margin-inline: 0; - padding-block: 0; - padding-inline: 0; - user-select: text; -} - -.step-item { - display: flex; - flex-direction: row; - gap: awsui.$space-static-s; - padding-block-end: awsui.$space-static-xs; -} - -.step-indicator { - display: flex; - flex-direction: column; - align-items: center; -} - -.step-circle { - inline-size: 20px; - block-size: 20px; - border-start-start-radius: 50%; - border-start-end-radius: 50%; - border-end-start-radius: 50%; - border-end-end-radius: 50%; - border-block: 2px solid; - border-inline: 2px solid; - flex-shrink: 0; - - &[data-status='active'] { - border-color: awsui.$color-background-control-checked; - background-color: awsui.$color-background-control-checked; - box-shadow: inset 0 0 0 3px awsui.$color-background-container-content; - } - - &[data-status='visited'] { - border-color: awsui.$color-text-interactive-default; - background-color: awsui.$color-text-interactive-default; - } - - &[data-status='unvisited'] { - border-color: awsui.$color-text-interactive-disabled; - background-color: transparent; - } -} - -.step-line { - inline-size: 2px; - flex: 1; - min-block-size: awsui.$space-static-l; - background-color: awsui.$color-border-divider-default; - margin-block: awsui.$space-static-xxs; -} - -.step-content { - padding-block-start: awsui.$space-static-xxs; - user-select: text; - cursor: default; -} - -.placeholder-content { - background-color: awsui.$color-background-layout-toggle-default; - border-start-start-radius: awsui.$border-radius-item; - border-start-end-radius: awsui.$border-radius-item; - border-end-start-radius: awsui.$border-radius-item; - border-end-end-radius: awsui.$border-radius-item; -} diff --git a/src/wizard/__integ__/wizard.test.ts b/src/wizard/__integ__/wizard.test.ts index ad8aaafabf..340c5aa242 100644 --- a/src/wizard/__integ__/wizard.test.ts +++ b/src/wizard/__integ__/wizard.test.ts @@ -116,3 +116,63 @@ describe('Wizard scroll to top upon navigation', () => { }) ); }); + +// CSS selectors for reflow tests +const COLLAPSED_STEPS_SELECTOR = '[class*="collapsed-steps"]:not([class*="hidden"])'; +const EXPANDABLE_HEADER_SELECTOR = '[class*="collapsed-steps"] [class*="header"]'; + +describe('Wizard WCAG 1.4.10 Reflow', () => { + test( + 'shows expandable step navigation at narrow viewport', + useBrowser(async browser => { + // Using simple page - expandable navigation is now default behavior at narrow viewports + await browser.url('/#/light/wizard/simple'); + const page = new BasePageObject(browser); + + // Set viewport to 320px (WCAG 1.4.10 requirement - simulates 400% zoom on 1280px viewport) + await page.setWindowSize({ width: 320, height: 800 }); + + // Wait for the wizard to render + await page.waitForVisible(wizardWrapper.findPrimaryButton().toSelector()); + + // Verify expandable navigation is visible at narrow viewport + await expect(page.isDisplayed(COLLAPSED_STEPS_SELECTOR)).resolves.toBe(true); + }) + ); + + test( + 'expandable navigation shows current step information', + useBrowser(async browser => { + await browser.url('/#/light/wizard/simple'); + const page = new BasePageObject(browser); + + // Set viewport to narrow width + await page.setWindowSize({ width: 320, height: 800 }); + + // Wait for the wizard to render + await page.waitForVisible(wizardWrapper.findPrimaryButton().toSelector()); + + // Verify header shows step count (e.g., "Step 1 of 3") + const headerText = await page.getText(EXPANDABLE_HEADER_SELECTOR); + expect(headerText).toMatch(/Step \d+ of \d+/); + }) + ); + + test( + 'no horizontal scrolling at 320px viewport width', + useBrowser(async browser => { + await browser.url('/#/light/wizard/simple'); + const page = new BasePageObject(browser); + + // Set viewport to 320px (WCAG 1.4.10 requirement) + await page.setWindowSize({ width: 320, height: 800 }); + + // Wait for the wizard to render + await page.waitForVisible(wizardWrapper.findPrimaryButton().toSelector()); + + // Check that document doesn't cause horizontal scrolling + const documentWidth = await browser.execute(() => document.documentElement.scrollWidth); + expect(documentWidth).toBeLessThanOrEqual(320); + }) + ); +}); diff --git a/src/wizard/interfaces.ts b/src/wizard/interfaces.ts index 741c0f01c6..d1ad58ccf5 100644 --- a/src/wizard/interfaces.ts +++ b/src/wizard/interfaces.ts @@ -173,6 +173,7 @@ export namespace WizardProps { stepNavigationTitle?: string; stepNavigationDismissAriaLabel?: string; stepNavigationConfirmButton?: string; + stepsNavigationLabel?: string; } export interface NavigateDetail { diff --git a/src/wizard/internal.tsx b/src/wizard/internal.tsx index 6b08db78f9..1d33ed39b1 100644 --- a/src/wizard/internal.tsx +++ b/src/wizard/internal.tsx @@ -70,8 +70,8 @@ export default function InternalWizard({ const farthestStepIndex = useRef(actualActiveStepIndex); farthestStepIndex.current = Math.max(farthestStepIndex.current, actualActiveStepIndex); - // State for expandable step navigation (collapsed by default) - const [stepNavigationExpanded, setStepNavigationExpanded] = useState(false); + // State for expandable step navigation (expanded by default for accessibility) + const [stepNavigationExpanded, setStepNavigationExpanded] = useState(true); const isVisualRefresh = useVisualRefresh(); const isLastStep = actualActiveStepIndex >= steps.length - 1; @@ -142,6 +142,7 @@ export default function InternalWizard({ stepNavigationTitle: rest.i18nStrings?.stepNavigationTitle, stepNavigationDismissAriaLabel: rest.i18nStrings?.stepNavigationDismissAriaLabel, stepNavigationConfirmButton: rest.i18nStrings?.stepNavigationConfirmButton, + stepsNavigationLabel: rest.i18nStrings?.stepsNavigationLabel, }; if (activeStepIndex && activeStepIndex >= steps.length) { diff --git a/src/wizard/styles.scss b/src/wizard/styles.scss index 898f435f71..b82348244d 100644 --- a/src/wizard/styles.scss +++ b/src/wizard/styles.scss @@ -543,7 +543,8 @@ list-style: none; margin-block: 0; margin-inline: 0; - padding-block: 0; + padding-block-start: awsui.$space-s; + padding-block-end: 0; padding-inline: 0; } @@ -615,6 +616,11 @@ padding-block-end: awsui.$space-xs; } +.expandable-step-label { + font-size: awsui.$font-size-body-s; + color: awsui.$color-text-small; +} + .expandable-step-title { font-size: awsui.$font-size-body-m; } @@ -624,6 +630,27 @@ font-weight: styles.$font-weight-bold; } +.expandable-step-title-clickable { + @include styles.styles-reset; + background: transparent; + border-block: none; + border-inline: none; + padding-block: 0; + padding-inline: 0; + cursor: pointer; + text-align: start; + font-size: inherit; + color: awsui.$color-text-body-default; + + &:hover { + text-decoration: underline; + } + + @include focus-visible.when-visible { + @include styles.link-focus; + } +} + .expandable-step-title-unvisited { color: awsui.$color-text-status-inactive; } diff --git a/src/wizard/wizard-navigation.tsx b/src/wizard/wizard-navigation.tsx index 0af91cec33..c0e429e3a8 100644 --- a/src/wizard/wizard-navigation.tsx +++ b/src/wizard/wizard-navigation.tsx @@ -8,6 +8,7 @@ import { useVisualRefresh } from '../internal/hooks/use-visual-mode'; import InternalLink from '../link/internal'; import { getNavigationActionDetail } from './analytics-metadata/utils'; import { WizardProps } from './interfaces'; +import { getStepStatus, StepStatus } from './wizard-step-list'; import analyticsSelectors from './analytics-metadata/styles.css.js'; import styles from './styles.css.js'; @@ -29,17 +30,10 @@ interface NavigationStepProps { index: number; onStepClick: (stepIndex: number) => void; onSkipToClick: (stepIndex: number) => void; - status: string; + status: StepStatus; step: WizardProps.Step; } -enum Statuses { - Active = 'active', - Unvisited = 'unvisited', - Visited = 'visited', - Next = 'next', -} - export default function Navigation({ activeStepIndex, farthestStepIndex, @@ -58,15 +52,23 @@ export default function Navigation({ aria-label={i18nStrings.navigationAriaLabel} >
    - {steps.map((step, index: number) => - isVisualRefresh ? ( + {steps.map((step, index: number) => { + const status = getStepStatus( + index, + activeStepIndex, + farthestStepIndex, + isLoadingNextStep, + allowSkipTo, + steps + ); + return isVisualRefresh ? ( ) : ( @@ -76,41 +78,14 @@ export default function Navigation({ key={index} onStepClick={onStepClick} onSkipToClick={onSkipToClick} - status={getStatus(index)} + status={status} step={step} /> - ) - )} + ); + })}
); - - function getStatus(index: number) { - if (activeStepIndex === index) { - return Statuses.Active; - } - if (isLoadingNextStep) { - return Statuses.Unvisited; - } - if (farthestStepIndex >= index) { - return Statuses.Visited; - } - if (allowSkipTo && canSkip(activeStepIndex + 1, index)) { - return Statuses.Next; - } - return Statuses.Unvisited; - } - - function canSkip(fromIndex: number, toIndex: number) { - let index = fromIndex; - do { - if (!steps[index].isOptional) { - return false; - } - index++; - } while (index < toIndex); - return true; - } } function NavigationStepVisualRefresh({ @@ -122,10 +97,10 @@ function NavigationStepVisualRefresh({ step, }: NavigationStepProps) { function handleStepInteraction() { - if (status === Statuses.Visited) { + if (status === 'visited') { onStepClick(index); } - if (status === Statuses.Next) { + if (status === 'next') { onSkipToClick(index); } } @@ -138,8 +113,8 @@ function NavigationStepVisualRefresh({ }[status]; const linkClassName = clsx(styles['navigation-link'], { - [styles['navigation-link-active']]: status === Statuses.Active, - [styles['navigation-link-disabled']]: status === Statuses.Unvisited, + [styles['navigation-link-active']]: status === 'active', + [styles['navigation-link-disabled']]: status === 'unvisited', }); return ( @@ -153,8 +128,8 @@ function NavigationStepVisualRefresh({ { event.preventDefault(); handleStepInteraction(); @@ -175,8 +150,8 @@ function NavigationStepVisualRefresh({ } }} role="button" - tabIndex={status === Statuses.Visited || status === Statuses.Next ? 0 : undefined} - {...(status === Statuses.Unvisited + tabIndex={status === 'visited' || status === 'next' ? 0 : undefined} + {...(status === 'unvisited' ? {} : getNavigationActionDetail(index, 'step', true, `.${analyticsSelectors['step-title']}`))} > @@ -191,13 +166,13 @@ function NavigationStepVisualRefresh({ function NavigationStepClassic({ i18nStrings, index, onStepClick, onSkipToClick, status, step }: NavigationStepProps) { const spanClassName = clsx( styles['navigation-link'], - status === Statuses.Active ? styles['navigation-link-active'] : styles['navigation-link-disabled'] + status === 'active' ? styles['navigation-link-active'] : styles['navigation-link-disabled'] ); return (
  • @@ -211,12 +186,12 @@ function NavigationStepClassic({ i18nStrings, index, onStepClick, onSkipToClick, {step.isOptional && {` - ${i18nStrings.optional}`}}
    - {status === Statuses.Visited || status === Statuses.Next ? ( + {status === 'visited' || status === 'next' ? ( { evt.preventDefault(); - if (status === Statuses.Visited) { + if (status === 'visited') { onStepClick(index); } else { onSkipToClick(index); @@ -229,8 +204,8 @@ function NavigationStepClassic({ i18nStrings, index, onStepClick, onSkipToClick, ) : ( {step.title} diff --git a/src/wizard/wizard-step-list.tsx b/src/wizard/wizard-step-list.tsx new file mode 100644 index 0000000000..5bc6c85992 --- /dev/null +++ b/src/wizard/wizard-step-list.tsx @@ -0,0 +1,136 @@ +// 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 { getNavigationActionDetail } from './analytics-metadata/utils'; +import { WizardProps } from './interfaces'; + +import analyticsSelectors from './analytics-metadata/styles.css.js'; +import styles from './styles.css.js'; + +export type StepStatus = 'active' | 'visited' | 'unvisited' | 'next'; + +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 'active'; + } + if (isLoadingNextStep) { + return 'unvisited'; + } + if (farthestStepIndex >= index) { + return 'visited'; + } + if (allowSkipTo && canSkip(activeStepIndex + 1, index, steps)) { + return 'next'; + } + return 'unvisited'; +} + +export function canSkip(fromIndex: number, toIndex: number, steps: ReadonlyArray<{ isOptional?: boolean }>): boolean { + 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 === 'visited') { + onStepClick(stepIndex); + } else if (status === 'next') { + onSkipToClick(stepIndex); + } +} + +export default function WizardStepList({ + activeStepIndex, + farthestStepIndex, + allowSkipTo, + i18nStrings, + isLoadingNextStep, + onStepClick, + onSkipToClick, + steps, +}: WizardStepListProps) { + return ( +
      + {steps.map((step, index) => { + const status = getStepStatus(index, activeStepIndex, farthestStepIndex, isLoadingNextStep, allowSkipTo, steps); + const isClickable = status === 'visited' || status === 'next'; + const stepLabel = i18nStrings.stepNumberLabel?.(index + 1); + const optionalSuffix = step.isOptional ? ` - ${i18nStrings.optional}` : ''; + const fullStepLabel = `${stepLabel}${optionalSuffix}: ${step.title}`; + + return ( +
    • +
      +
    • + ); + })} +
    + ); +} diff --git a/src/wizard/wizard-step-navigation-expandable.tsx b/src/wizard/wizard-step-navigation-expandable.tsx index edeb560fb0..a534b2494f 100644 --- a/src/wizard/wizard-step-navigation-expandable.tsx +++ b/src/wizard/wizard-step-navigation-expandable.tsx @@ -1,16 +1,10 @@ // 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 InternalBox from '../box/internal'; import InternalExpandableSection from '../expandable-section/internal'; -import InternalLink from '../link/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'; +import WizardStepList from './wizard-step-list'; interface WizardStepNavigationExpandableProps { activeStepIndex: number; @@ -25,8 +19,6 @@ interface WizardStepNavigationExpandableProps { steps: ReadonlyArray; } -type StepStatus = 'active' | 'visited' | 'unvisited' | 'next'; - export default function WizardStepNavigationExpandable({ activeStepIndex, farthestStepIndex, @@ -41,102 +33,25 @@ export default function WizardStepNavigationExpandable({ }: WizardStepNavigationExpandableProps) { const collapsedStepsLabel = i18nStrings.collapsedStepsLabel?.(activeStepIndex + 1, steps.length); - const getStepStatus = (index: number): StepStatus => { - if (activeStepIndex === index) { - return 'active'; - } - if (isLoadingNextStep) { - return 'unvisited'; - } - if (farthestStepIndex >= index) { - return 'visited'; - } - if (allowSkipTo && canSkip(activeStepIndex + 1, index)) { - return 'next'; - } - return 'unvisited'; - }; - - const canSkip = (fromIndex: number, toIndex: number): boolean => { - let index = fromIndex; - while (index < toIndex) { - if (!steps[index].isOptional) { - return false; - } - index++; - } - return true; - }; - - const handleStepClick = (stepIndex: number, status: StepStatus) => { - if (status === 'visited') { - onStepClick(stepIndex); - } else if (status === 'next') { - onSkipToClick(stepIndex); - } - }; - return ( onExpandedChange(detail.expanded)} > -
      - {steps.map((step, index) => { - const status = getStepStatus(index); - const isClickable = status === 'visited' || status === 'next'; - const stepLabel = i18nStrings.stepNumberLabel?.(index + 1) ?? `Step ${index + 1}`; - const optionalLabel = step.isOptional ? ` - ${i18nStrings.optional}` : ''; - - return ( -
    • -
      -
    • - ); - })} -
    +
    ); } From db297cefc700c4ce65dad6b99dff7f768ebacebc Mon Sep 17 00:00:00 2001 From: Philipp Schneider Date: Mon, 2 Feb 2026 12:33:51 +0100 Subject: [PATCH 07/37] fix: Add can skip test --- src/wizard/wizard-step-list.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/wizard/wizard-step-list.tsx b/src/wizard/wizard-step-list.tsx index 5bc6c85992..f06f651932 100644 --- a/src/wizard/wizard-step-list.tsx +++ b/src/wizard/wizard-step-list.tsx @@ -46,6 +46,10 @@ export function getStepStatus( } 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; From d61db641ee4a4bfcb48a3b303cc98242b35fe164 Mon Sep 17 00:00:00 2001 From: Philipp Schneider Date: Mon, 2 Feb 2026 14:08:26 +0100 Subject: [PATCH 08/37] fix: Remove unnecessary files --- pages/wizard/common.ts | 10 +- pages/wizard/step-navigation.page.tsx | 215 ------------------ src/wizard/interfaces.ts | 3 - src/wizard/internal.tsx | 3 - src/wizard/styles.scss | 84 ------- src/wizard/wizard-form.tsx | 26 +-- .../wizard-step-navigation-dropdown.tsx | 174 -------------- src/wizard/wizard-step-navigation-modal.tsx | 175 -------------- 8 files changed, 6 insertions(+), 684 deletions(-) delete mode 100644 pages/wizard/step-navigation.page.tsx delete mode 100644 src/wizard/wizard-step-navigation-dropdown.tsx delete mode 100644 src/wizard/wizard-step-navigation-modal.tsx diff --git a/pages/wizard/common.ts b/pages/wizard/common.ts index 9537417185..c5895dc700 100644 --- a/pages/wizard/common.ts +++ b/pages/wizard/common.ts @@ -18,12 +18,4 @@ const i18nStrings: WizardProps.I18nStrings = { stepsNavigationLabel: 'Steps navigation', }; -// i18n strings with modal navigation support -const i18nStringsWithModal: WizardProps.I18nStrings = { - ...i18nStrings, - stepNavigationTitle: 'Go to step', - stepNavigationDismissAriaLabel: 'Close step navigation', - stepNavigationConfirmButton: 'Go', -}; - -export { i18nStrings, i18nStringsWithModal }; +export { i18nStrings }; diff --git a/pages/wizard/step-navigation.page.tsx b/pages/wizard/step-navigation.page.tsx deleted file mode 100644 index d6e8416bdf..0000000000 --- a/pages/wizard/step-navigation.page.tsx +++ /dev/null @@ -1,215 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -import React, { useState } from 'react'; - -import Box from '~components/box'; -import ColumnLayout from '~components/column-layout'; -import Container from '~components/container'; -import FormField from '~components/form-field'; -import Header from '~components/header'; -import Input from '~components/input'; -import Link from '~components/link'; -import RadioGroup from '~components/radio-group'; -import SpaceBetween from '~components/space-between'; -import Toggle from '~components/toggle'; -import Wizard, { WizardProps } from '~components/wizard'; - -import { i18nStrings, i18nStringsWithModal } from './common'; - -import styles from './styles.scss'; - -const steps: WizardProps.Step[] = [ - { - title: 'Choose distribution settings', - description: 'Configure the basic settings for your distribution.', - content: ( - Distribution settings}> - - - {}} /> - - - {}} /> - - - - ), - }, - { - title: 'Configure origin settings', - isOptional: true, - description: 'Specify where your content originates from.', - content: ( - Origin settings}> - - - {}} /> - - - {}} - /> - - - - ), - }, - { - title: 'Configure cache behavior', - isOptional: true, - description: 'Define how CloudFront caches your content.', - content: ( - Cache behavior settings}> - - - {}} /> - - - {}} - /> - - - - ), - }, - { - title: 'Configure SSL certificate', - isOptional: true, - content: ( - SSL certificate}> - - {}} - /> - - - ), - }, - { - title: 'Configure logging', - isOptional: true, - content: ( - Logging settings}> - - - {}}> - Log requests to S3 bucket - - - - - ), - }, - { - title: 'Review and create', - info: Info, - description: 'Review your configuration before creating the distribution.', - content: ( - - Distribution summary}> - -
    - Distribution name - my-distribution -
    -
    - Origin domain - example.com -
    -
    - Protocol policy - HTTPS only -
    -
    - Cache behavior - Redirect HTTP to HTTPS -
    -
    -
    -
    - ), - }, -]; - -export default function StepNavigationPage() { - const [activeStepIndex, setActiveStepIndex] = useState(0); - const [smallContainer, setSmallContainer] = useState(true); - const [useModalNavigation, setUseModalNavigation] = useState(false); - const [allowSkipTo, setAllowSkipTo] = useState(true); - const [isLoading, setIsLoading] = useState(false); - - const currentI18nStrings = useModalNavigation ? i18nStringsWithModal : i18nStrings; - - return ( - - - Step Navigation Demo Options}> - - setSmallContainer(detail.checked)}> - Small container (shows collapsed step navigation) - - setUseModalNavigation(detail.checked)}> - Use modal navigation (requires additional i18n strings) - - setAllowSkipTo(detail.checked)}> - Allow skip to (enables skipping optional steps) - - setIsLoading(detail.checked)}> - Loading state (disables navigation) - - - - - -
    - {useModalNavigation ? 'Modal Navigation Variant' : 'Dropdown Navigation Variant (Default)'} -
    - - {useModalNavigation - ? 'Using modal for step navigation. Requires stepNavigationTitle, stepNavigationConfirmButton, and cancelButton i18n strings.' - : 'Using dropdown for step navigation. No additional i18n strings required - works with basic i18n strings.'} - -
    - -
    - { - console.log('Navigate:', e.detail); - setActiveStepIndex(e.detail.requestedStepIndex); - }} - onCancel={() => console.log('Cancelled')} - onSubmit={() => console.log('Submitted')} - /> -
    -
    -
    - ); -} diff --git a/src/wizard/interfaces.ts b/src/wizard/interfaces.ts index d1ad58ccf5..7ec641724b 100644 --- a/src/wizard/interfaces.ts +++ b/src/wizard/interfaces.ts @@ -170,9 +170,6 @@ export namespace WizardProps { optional?: string; nextButtonLoadingAnnouncement?: string; submitButtonLoadingAnnouncement?: string; - stepNavigationTitle?: string; - stepNavigationDismissAriaLabel?: string; - stepNavigationConfirmButton?: string; stepsNavigationLabel?: string; } diff --git a/src/wizard/internal.tsx b/src/wizard/internal.tsx index 1d33ed39b1..b2981dbafb 100644 --- a/src/wizard/internal.tsx +++ b/src/wizard/internal.tsx @@ -139,9 +139,6 @@ export default function InternalWizard({ previousButton: i18n('i18nStrings.previousButton', rest.i18nStrings?.previousButton), nextButton: i18n('i18nStrings.nextButton', rest.i18nStrings?.nextButton), optional: i18n('i18nStrings.optional', rest.i18nStrings?.optional), - stepNavigationTitle: rest.i18nStrings?.stepNavigationTitle, - stepNavigationDismissAriaLabel: rest.i18nStrings?.stepNavigationDismissAriaLabel, - stepNavigationConfirmButton: rest.i18nStrings?.stepNavigationConfirmButton, stepsNavigationLabel: rest.i18nStrings?.stepsNavigationLabel, }; diff --git a/src/wizard/styles.scss b/src/wizard/styles.scss index b82348244d..3abf9f4000 100644 --- a/src/wizard/styles.scss +++ b/src/wizard/styles.scss @@ -418,90 +418,6 @@ color: awsui.$color-text-status-inactive; } -/* Dropdown variant styles */ -.step-navigation-dropdown-list { - list-style: none; - margin-block: 0; - margin-inline: 0; - padding-block: awsui.$space-xs; - padding-inline: 0; - min-inline-size: 280px; - max-inline-size: 400px; -} - -.step-navigation-dropdown-item { - display: flex; - align-items: flex-start; - gap: awsui.$space-xs; - padding-block: awsui.$space-xs; - padding-inline: awsui.$space-m; - cursor: pointer; - - &:hover:not(.step-navigation-dropdown-item-disabled) { - background-color: awsui.$color-background-dropdown-item-hover; - } -} - -.step-navigation-dropdown-item-active { - background-color: awsui.$color-background-dropdown-item-selected; -} - -.step-navigation-dropdown-item-disabled { - cursor: default; -} - -.step-navigation-dropdown-circle { - flex-shrink: 0; - inline-size: 12px; - block-size: 12px; - margin-block-start: 4px; - border-start-start-radius: 50%; - border-start-end-radius: 50%; - border-end-start-radius: 50%; - border-end-end-radius: 50%; - border-block: 2px solid awsui.$color-text-interactive-disabled; - border-inline: 2px solid awsui.$color-text-interactive-disabled; - background-color: awsui.$color-background-container-content; - box-sizing: border-box; -} - -.step-navigation-dropdown-circle-active { - border-color: awsui.$color-background-control-checked; - background-color: awsui.$color-background-control-checked; - box-shadow: - 0 0 0 2px awsui.$color-background-container-content, - 0 0 0 4px awsui.$color-background-control-checked; -} - -.step-navigation-dropdown-circle-reachable { - border-color: awsui.$color-text-interactive-default; -} - -.step-navigation-dropdown-circle-disabled { - border-color: awsui.$color-text-interactive-disabled; -} - -.step-navigation-dropdown-content { - display: flex; - flex-direction: column; - min-inline-size: 0; -} - -.step-navigation-dropdown-title { - @include styles.text-wrapping; - font-size: awsui.$font-size-body-m; - color: awsui.$color-text-body-default; -} - -.step-navigation-dropdown-title-active { - color: awsui.$color-background-control-checked; - font-weight: styles.$font-weight-bold; -} - -.step-navigation-dropdown-title-disabled { - color: awsui.$color-text-status-inactive; -} - .form-header-component { &-wrapper { outline: none; diff --git a/src/wizard/wizard-form.tsx b/src/wizard/wizard-form.tsx index 6fd99b5876..6f46187ed2 100644 --- a/src/wizard/wizard-form.tsx +++ b/src/wizard/wizard-form.tsx @@ -25,14 +25,10 @@ import { useEffectOnUpdate } from '../internal/hooks/use-effect-on-update'; import { WizardProps } from './interfaces'; import WizardActions from './wizard-actions'; import WizardFormHeader from './wizard-form-header'; -import WizardStepNavigationDropdown from './wizard-step-navigation-dropdown'; import WizardStepNavigationExpandable from './wizard-step-navigation-expandable'; -import WizardStepNavigationModal from './wizard-step-navigation-modal'; import styles from './styles.css.js'; -export type StepNavigationVariant = 'modal' | 'dropdown' | 'expandable' | 'auto'; - interface WizardFormProps extends InternalBaseComponentProps { steps: ReadonlyArray; activeStepIndex: number; @@ -49,7 +45,6 @@ interface WizardFormProps extends InternalBaseComponentProps { onPrimaryClick: () => void; onStepClick: (stepIndex: number) => void; onSkipToClick: (stepIndex: number) => void; - stepNavigationVariant?: StepNavigationVariant; stepNavigationExpanded?: boolean; onStepNavigationExpandedChange?: (expanded: boolean) => void; } @@ -98,7 +93,6 @@ function WizardForm({ onPrimaryClick, onStepClick, onSkipToClick, - stepNavigationVariant = 'auto', stepNavigationExpanded = false, onStepNavigationExpandedChange, }: WizardFormProps & { stepHeaderRef: MutableRefObject }) { @@ -143,10 +137,6 @@ function WizardForm({ } }, [funnelInteractionId, funnelIdentifier, isLastStep, errorText, __internalRootRef, errorSlotId, funnelStepInfo]); - // Determine which navigation variant to use - // 'auto' mode: default to expandable section for better accessibility - const effectiveVariant = stepNavigationVariant === 'auto' ? 'expandable' : stepNavigationVariant; - const stepNavigationProps = { activeStepIndex, farthestStepIndex, @@ -162,17 +152,11 @@ function WizardForm({ <>
    - {effectiveVariant === 'expandable' ? ( - {})} - /> - ) : effectiveVariant === 'modal' ? ( - - ) : ( - - )} + {})} + />
    void; - onSkipToClick: (stepIndex: number) => void; - steps: ReadonlyArray; -} - -export default function WizardStepNavigationDropdown({ - activeStepIndex, - farthestStepIndex, - allowSkipTo, - i18nStrings, - isLoadingNextStep, - onStepClick, - onSkipToClick, - steps, -}: WizardStepNavigationDropdownProps) { - const [isOpen, setIsOpen] = useState(false); - - const collapsedStepsLabel = i18nStrings.collapsedStepsLabel?.(activeStepIndex + 1, steps.length); - - // A step is reachable if it's the current step, a previously visited step, or the immediate next step - const isStepReachable = (index: number): boolean => { - if (isLoadingNextStep) { - return false; - } - // Current step or any previously visited step - if (index <= farthestStepIndex) { - return true; - } - // Immediate next step is always reachable - if (index === activeStepIndex + 1) { - return true; - } - // Skip-to: all intermediate steps must be optional - if (allowSkipTo) { - for (let i = activeStepIndex + 1; i < index; i++) { - if (!steps[i].isOptional) { - return false; - } - } - return true; - } - return false; - }; - - const handleStepClick = (stepIndex: number) => { - if (!isStepReachable(stepIndex)) { - return; - } - - // Close dropdown first - setIsOpen(false); - - // Don't navigate if already on this step - if (stepIndex === activeStepIndex) { - return; - } - - // Use 'skip' for forward navigation to unvisited steps, 'step' otherwise - if (stepIndex > farthestStepIndex) { - onSkipToClick(stepIndex); - } else { - onStepClick(stepIndex); - } - }; - - const trigger = ( - - ); - - return ( -
    - setIsOpen(false)} - trigger={trigger} - stretchWidth={false} - expandToViewport={false} - > - -
      - {steps.map((step, index) => { - const isActive = activeStepIndex === index; - const isReachable = isStepReachable(index); - const stepLabel = i18nStrings.stepNumberLabel?.(index + 1) ?? `Step ${index + 1}`; - - return ( -
    • handleStepClick(index)} - > -
    • - ); - })} -
    -
    -
    -
    - ); -} diff --git a/src/wizard/wizard-step-navigation-modal.tsx b/src/wizard/wizard-step-navigation-modal.tsx deleted file mode 100644 index f7546d7a16..0000000000 --- a/src/wizard/wizard-step-navigation-modal.tsx +++ /dev/null @@ -1,175 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -import React, { useState } from 'react'; -import clsx from 'clsx'; - -import InternalBox from '../box/internal'; -import { InternalButton } from '../button/internal'; -import InternalIcon from '../icon/internal'; -import InternalModal from '../modal/internal'; -import InternalSpaceBetween from '../space-between/internal'; -import { WizardProps } from './interfaces'; - -import styles from './styles.css.js'; - -interface WizardStepNavigationModalProps { - activeStepIndex: number; - farthestStepIndex: number; - allowSkipTo: boolean; - i18nStrings: WizardProps.I18nStrings; - isLoadingNextStep: boolean; - onStepClick: (stepIndex: number) => void; - onSkipToClick: (stepIndex: number) => void; - steps: ReadonlyArray; -} - -export default function WizardStepNavigationModal({ - activeStepIndex, - farthestStepIndex, - allowSkipTo, - i18nStrings, - isLoadingNextStep, - onStepClick, - onSkipToClick, - steps, -}: WizardStepNavigationModalProps) { - const [isModalVisible, setIsModalVisible] = useState(false); - const [selectedStepIndex, setSelectedStepIndex] = useState(activeStepIndex); - - const collapsedStepsLabel = i18nStrings.collapsedStepsLabel?.(activeStepIndex + 1, steps.length); - - // A step is reachable if it's the current step, a previously visited step, or the immediate next step - const isStepReachable = (index: number): boolean => { - if (isLoadingNextStep) { - return false; - } - // Current step or any previously visited step - if (index <= farthestStepIndex) { - return true; - } - // Immediate next step is always reachable - if (index === activeStepIndex + 1) { - return true; - } - // Skip-to: all intermediate steps must be optional - if (allowSkipTo) { - for (let i = activeStepIndex + 1; i < index; i++) { - if (!steps[i].isOptional) { - return false; - } - } - return true; - } - return false; - }; - - const handleOpenModal = () => { - setSelectedStepIndex(activeStepIndex); - setIsModalVisible(true); - }; - - const handleCloseModal = () => { - setIsModalVisible(false); - }; - - const handleConfirm = () => { - if (selectedStepIndex !== activeStepIndex) { - // Use 'skip' for forward navigation to unvisited steps, 'step' otherwise - if (selectedStepIndex > farthestStepIndex) { - onSkipToClick(selectedStepIndex); - } else { - onStepClick(selectedStepIndex); - } - } - setIsModalVisible(false); - }; - - const handleStepSelect = (stepIndex: number) => { - if (isStepReachable(stepIndex)) { - setSelectedStepIndex(stepIndex); - } - }; - - return ( -
    - - - - - - {i18nStrings.cancelButton} - - - {i18nStrings.stepNavigationConfirmButton ?? 'Ok'} - - - - } - > -
      - {steps.map((step, index) => { - const isSelected = selectedStepIndex === index; - const isReachable = isStepReachable(index); - const stepLabel = i18nStrings.stepNumberLabel?.(index + 1) ?? `Step ${index + 1}`; - - // Simple: selected (highlighted) or reachable (clickable) or unreachable (disabled) - const displayStatus = isSelected ? 'selected' : isReachable ? 'visited' : 'unvisited'; - - return ( -
    • - -
    • - ); - })} -
    -
    -
    - ); -} From f7e66ddf53541ad290eefd16e4679125083391c1 Mon Sep 17 00:00:00 2001 From: Philipp Schneider Date: Mon, 2 Feb 2026 14:42:48 +0100 Subject: [PATCH 09/37] clean up --- src/i18n/messages/all.en.json | 5 +--- src/wizard/wizard-navigation.tsx | 43 ++++++++++++++++---------------- src/wizard/wizard-step-list.tsx | 29 +++++++++++++-------- 3 files changed, 41 insertions(+), 36 deletions(-) diff --git a/src/i18n/messages/all.en.json b/src/i18n/messages/all.en.json index 4131b2d35d..72f5b868f8 100644 --- a/src/i18n/messages/all.en.json +++ b/src/i18n/messages/all.en.json @@ -468,9 +468,6 @@ "i18nStrings.nextButton": "Next", "i18nStrings.optional": "optional", "i18nStrings.nextButtonLoadingAnnouncement": "Loading next step", - "i18nStrings.submitButtonLoadingAnnouncement": "Submitting form", - "i18nStrings.stepNavigationTitle": "Step", - "i18nStrings.stepNavigationDismissAriaLabel": "Close step navigation", - "i18nStrings.stepNavigationConfirmButton": "Ok" + "i18nStrings.submitButtonLoadingAnnouncement": "Submitting form" } } diff --git a/src/wizard/wizard-navigation.tsx b/src/wizard/wizard-navigation.tsx index c0e429e3a8..49080ac72c 100644 --- a/src/wizard/wizard-navigation.tsx +++ b/src/wizard/wizard-navigation.tsx @@ -8,7 +8,7 @@ import { useVisualRefresh } from '../internal/hooks/use-visual-mode'; import InternalLink from '../link/internal'; import { getNavigationActionDetail } from './analytics-metadata/utils'; import { WizardProps } from './interfaces'; -import { getStepStatus, StepStatus } from './wizard-step-list'; +import { getStepStatus, StepStatus, StepStatusValues } from './wizard-step-list'; import analyticsSelectors from './analytics-metadata/styles.css.js'; import styles from './styles.css.js'; @@ -97,24 +97,25 @@ function NavigationStepVisualRefresh({ step, }: NavigationStepProps) { function handleStepInteraction() { - if (status === 'visited') { + if (status === StepStatusValues.Visited) { onStepClick(index); } - if (status === 'next') { + if (status === StepStatusValues.Next) { onSkipToClick(index); } } - const state = { - active: 'active', - unvisited: 'disabled', - visited: 'enabled', - next: 'enabled', - }[status]; + const stateMap: Record = { + [StepStatusValues.Active]: 'active', + [StepStatusValues.Unvisited]: 'disabled', + [StepStatusValues.Visited]: 'enabled', + [StepStatusValues.Next]: 'enabled', + }; + const state = stateMap[status]; const linkClassName = clsx(styles['navigation-link'], { - [styles['navigation-link-active']]: status === 'active', - [styles['navigation-link-disabled']]: status === 'unvisited', + [styles['navigation-link-active']]: status === StepStatusValues.Active, + [styles['navigation-link-disabled']]: status === StepStatusValues.Unvisited, }); return ( @@ -128,8 +129,8 @@ function NavigationStepVisualRefresh({
    { event.preventDefault(); handleStepInteraction(); @@ -150,8 +151,8 @@ function NavigationStepVisualRefresh({ } }} role="button" - tabIndex={status === 'visited' || status === 'next' ? 0 : undefined} - {...(status === 'unvisited' + tabIndex={status === StepStatusValues.Visited || status === StepStatusValues.Next ? 0 : undefined} + {...(status === StepStatusValues.Unvisited ? {} : getNavigationActionDetail(index, 'step', true, `.${analyticsSelectors['step-title']}`))} > @@ -166,13 +167,13 @@ function NavigationStepVisualRefresh({ function NavigationStepClassic({ i18nStrings, index, onStepClick, onSkipToClick, status, step }: NavigationStepProps) { const spanClassName = clsx( styles['navigation-link'], - status === 'active' ? styles['navigation-link-active'] : styles['navigation-link-disabled'] + status === StepStatusValues.Active ? styles['navigation-link-active'] : styles['navigation-link-disabled'] ); return (
  • @@ -186,12 +187,12 @@ function NavigationStepClassic({ i18nStrings, index, onStepClick, onSkipToClick, {step.isOptional && {` - ${i18nStrings.optional}`}}
    - {status === 'visited' || status === 'next' ? ( + {status === StepStatusValues.Visited || status === StepStatusValues.Next ? ( { evt.preventDefault(); - if (status === 'visited') { + if (status === StepStatusValues.Visited) { onStepClick(index); } else { onSkipToClick(index); @@ -204,8 +205,8 @@ function NavigationStepClassic({ i18nStrings, index, onStepClick, onSkipToClick, ) : ( {step.title} diff --git a/src/wizard/wizard-step-list.tsx b/src/wizard/wizard-step-list.tsx index f06f651932..6d69d38460 100644 --- a/src/wizard/wizard-step-list.tsx +++ b/src/wizard/wizard-step-list.tsx @@ -9,7 +9,14 @@ import { WizardProps } from './interfaces'; import analyticsSelectors from './analytics-metadata/styles.css.js'; import styles from './styles.css.js'; -export type StepStatus = 'active' | 'visited' | 'unvisited' | 'next'; +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; @@ -31,18 +38,18 @@ export function getStepStatus( steps: ReadonlyArray<{ isOptional?: boolean }> ): StepStatus { if (activeStepIndex === index) { - return 'active'; + return StepStatusValues.Active; } if (isLoadingNextStep) { - return 'unvisited'; + return StepStatusValues.Unvisited; } if (farthestStepIndex >= index) { - return 'visited'; + return StepStatusValues.Visited; } if (allowSkipTo && canSkip(activeStepIndex + 1, index, steps)) { - return 'next'; + return StepStatusValues.Next; } - return 'unvisited'; + return StepStatusValues.Unvisited; } export function canSkip(fromIndex: number, toIndex: number, steps: ReadonlyArray<{ isOptional?: boolean }>): boolean { @@ -64,9 +71,9 @@ export function handleStepNavigation( onStepClick: (index: number) => void, onSkipToClick: (index: number) => void ): void { - if (status === 'visited') { + if (status === StepStatusValues.Visited) { onStepClick(stepIndex); - } else if (status === 'next') { + } else if (status === StepStatusValues.Next) { onSkipToClick(stepIndex); } } @@ -85,13 +92,13 @@ export default function WizardStepList({
      {steps.map((step, index) => { const status = getStepStatus(index, activeStepIndex, farthestStepIndex, isLoadingNextStep, allowSkipTo, steps); - const isClickable = status === 'visited' || status === 'next'; + const isClickable = status === StepStatusValues.Visited || status === StepStatusValues.Next; const stepLabel = i18nStrings.stepNumberLabel?.(index + 1); const optionalSuffix = step.isOptional ? ` - ${i18nStrings.optional}` : ''; const fullStepLabel = `${stepLabel}${optionalSuffix}: ${step.title}`; return ( -
    • +
    • {` - ${i18nStrings.optional}`}} - {status === 'active' ? ( + {status === StepStatusValues.Active ? ( Date: Mon, 2 Feb 2026 14:47:19 +0100 Subject: [PATCH 10/37] clean up --- pages/wizard/common.ts | 1 - src/wizard/interfaces.ts | 1 - src/wizard/internal.tsx | 1 - 3 files changed, 3 deletions(-) diff --git a/pages/wizard/common.ts b/pages/wizard/common.ts index c5895dc700..4d4241e37d 100644 --- a/pages/wizard/common.ts +++ b/pages/wizard/common.ts @@ -15,7 +15,6 @@ const i18nStrings: WizardProps.I18nStrings = { optional: 'optional', nextButtonLoadingAnnouncement: 'Loading next step', submitButtonLoadingAnnouncement: 'Submitting form', - stepsNavigationLabel: 'Steps navigation', }; export { i18nStrings }; diff --git a/src/wizard/interfaces.ts b/src/wizard/interfaces.ts index 7ec641724b..97fb647e38 100644 --- a/src/wizard/interfaces.ts +++ b/src/wizard/interfaces.ts @@ -170,7 +170,6 @@ export namespace WizardProps { optional?: string; nextButtonLoadingAnnouncement?: string; submitButtonLoadingAnnouncement?: string; - stepsNavigationLabel?: string; } export interface NavigateDetail { diff --git a/src/wizard/internal.tsx b/src/wizard/internal.tsx index b2981dbafb..2a7758a068 100644 --- a/src/wizard/internal.tsx +++ b/src/wizard/internal.tsx @@ -139,7 +139,6 @@ export default function InternalWizard({ previousButton: i18n('i18nStrings.previousButton', rest.i18nStrings?.previousButton), nextButton: i18n('i18nStrings.nextButton', rest.i18nStrings?.nextButton), optional: i18n('i18nStrings.optional', rest.i18nStrings?.optional), - stepsNavigationLabel: rest.i18nStrings?.stepsNavigationLabel, }; if (activeStepIndex && activeStepIndex >= steps.length) { From f58bca1b10395a72d2199cc1e2e75a9aa6f79ebc Mon Sep 17 00:00:00 2001 From: Philipp Schneider Date: Mon, 2 Feb 2026 14:56:48 +0100 Subject: [PATCH 11/37] clean up --- src/i18n/messages/all.en.json | 2 +- src/wizard/internal.tsx | 7 +- src/wizard/styles.scss | 151 ------------------ src/wizard/wizard-form.tsx | 10 +- .../wizard-step-navigation-expandable.tsx | 9 +- 5 files changed, 6 insertions(+), 173 deletions(-) diff --git a/src/i18n/messages/all.en.json b/src/i18n/messages/all.en.json index 72f5b868f8..7e88102e91 100644 --- a/src/i18n/messages/all.en.json +++ b/src/i18n/messages/all.en.json @@ -470,4 +470,4 @@ "i18nStrings.nextButtonLoadingAnnouncement": "Loading next step", "i18nStrings.submitButtonLoadingAnnouncement": "Submitting form" } -} +} \ No newline at end of file diff --git a/src/wizard/internal.tsx b/src/wizard/internal.tsx index 2a7758a068..4836fbff6d 100644 --- a/src/wizard/internal.tsx +++ b/src/wizard/internal.tsx @@ -1,6 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -import React, { useRef, useState } from 'react'; +import React, { useRef } from 'react'; import clsx from 'clsx'; import { useMergeRefs, warnOnce } from '@cloudscape-design/component-toolkit/internal'; @@ -70,9 +70,6 @@ export default function InternalWizard({ const farthestStepIndex = useRef(actualActiveStepIndex); farthestStepIndex.current = Math.max(farthestStepIndex.current, actualActiveStepIndex); - // State for expandable step navigation (expanded by default for accessibility) - const [stepNavigationExpanded, setStepNavigationExpanded] = useState(true); - const isVisualRefresh = useVisualRefresh(); const isLastStep = actualActiveStepIndex >= steps.length - 1; @@ -214,8 +211,6 @@ export default function InternalWizard({ onStepClick={onStepClick} onSkipToClick={onSkipToClick} onPrimaryClick={onPrimaryClick} - stepNavigationExpanded={stepNavigationExpanded} - onStepNavigationExpandedChange={setStepNavigationExpanded} />
      diff --git a/src/wizard/styles.scss b/src/wizard/styles.scss index 3abf9f4000..19dca7c7e5 100644 --- a/src/wizard/styles.scss +++ b/src/wizard/styles.scss @@ -267,157 +267,6 @@ } } -.collapsed-steps-navigation { - display: inline-block; -} - -.collapsed-steps-trigger { - @include styles.styles-reset; - display: inline-flex; - align-items: center; - gap: awsui.$space-xxs; - padding-block: 0; - padding-inline: 0; - border-block: none; - border-inline: none; - background: transparent; - color: awsui.$color-text-link-default; - font-weight: styles.$font-weight-bold; - font-size: inherit; - cursor: pointer; - - &:hover { - color: awsui.$color-text-link-hover; - } - - &:disabled { - color: awsui.$color-text-interactive-disabled; - cursor: default; - } - - @include focus-visible.when-visible { - @include styles.link-focus; - } -} - -.collapsed-steps-trigger-label { - /* used for test utils */ -} - -.step-navigation-list { - list-style: none; - margin-block: 0; - margin-inline: 0; - padding-block: 0; - padding-inline: 0; -} - -.step-navigation-item { - position: relative; - padding-inline-start: awsui.$space-l; - margin-block-end: awsui.$space-s; - - &:not(:last-child)::before { - content: ''; - position: absolute; - inset-inline-start: calc(#{awsui.$space-l} / 2 - 1px); - inset-block-start: calc(#{awsui.$space-m} + 6px); - block-size: calc(100% + #{awsui.$space-s} - 6px); - inline-size: 2px; - background-color: awsui.$color-border-divider-default; - } - - &:last-child { - margin-block-end: 0; - } -} - -.step-navigation-button { - @include styles.styles-reset; - display: flex; - align-items: flex-start; - gap: awsui.$space-xs; - padding-block: 0; - padding-inline: 0; - border-block: none; - border-inline: none; - background: transparent; - cursor: pointer; - text-align: start; - inline-size: 100%; - - &:disabled { - cursor: default; - } - - @include focus-visible.when-visible { - @include styles.link-focus; - } -} - -.step-navigation-button-disabled { - cursor: default; -} - -.step-navigation-circle { - flex-shrink: 0; - inline-size: 12px; - block-size: 12px; - margin-block-start: 4px; - border-start-start-radius: 50%; - border-start-end-radius: 50%; - border-end-start-radius: 50%; - border-end-end-radius: 50%; - border-block: 2px solid awsui.$color-text-interactive-disabled; - border-inline: 2px solid awsui.$color-text-interactive-disabled; - background-color: awsui.$color-background-container-content; - box-sizing: border-box; -} - -.step-navigation-circle-selected { - border-color: awsui.$color-background-control-checked; - background-color: awsui.$color-background-control-checked; - box-shadow: - 0 0 0 2px awsui.$color-background-container-content, - 0 0 0 4px awsui.$color-background-control-checked; -} - -.step-navigation-circle-visited { - border-color: awsui.$color-text-interactive-default; -} - -.step-navigation-circle-unvisited { - border-color: awsui.$color-text-interactive-disabled; -} - -.step-navigation-content { - display: flex; - flex-direction: column; -} - -.step-navigation-label { - font-size: awsui.$font-size-body-s; - color: awsui.$color-text-small; -} - -.step-navigation-title { - font-size: awsui.$font-size-body-m; - color: awsui.$color-text-body-default; -} - -.step-navigation-title-selected { - color: awsui.$color-background-control-checked; - font-weight: styles.$font-weight-bold; -} - -.step-navigation-title-visited { - color: awsui.$color-text-interactive-default; -} - -.step-navigation-title-unvisited { - color: awsui.$color-text-status-inactive; -} - .form-header-component { &-wrapper { outline: none; diff --git a/src/wizard/wizard-form.tsx b/src/wizard/wizard-form.tsx index 6f46187ed2..eabf011791 100644 --- a/src/wizard/wizard-form.tsx +++ b/src/wizard/wizard-form.tsx @@ -45,8 +45,6 @@ interface WizardFormProps extends InternalBaseComponentProps { onPrimaryClick: () => void; onStepClick: (stepIndex: number) => void; onSkipToClick: (stepIndex: number) => void; - stepNavigationExpanded?: boolean; - onStepNavigationExpandedChange?: (expanded: boolean) => void; } export const STEP_NAME_SELECTOR = `[${DATA_ATTR_FUNNEL_KEY}="${FUNNEL_KEY_STEP_NAME}"]`; @@ -93,8 +91,6 @@ function WizardForm({ onPrimaryClick, onStepClick, onSkipToClick, - stepNavigationExpanded = false, - onStepNavigationExpandedChange, }: WizardFormProps & { stepHeaderRef: MutableRefObject }) { const rootRef = useRef(); const ref = useMergeRefs(rootRef, __internalRootRef); @@ -152,11 +148,7 @@ function WizardForm({ <>
      - {})} - /> +
      void; i18nStrings: WizardProps.I18nStrings; isLoadingNextStep: boolean; onStepClick: (stepIndex: number) => void; @@ -23,14 +21,13 @@ export default function WizardStepNavigationExpandable({ activeStepIndex, farthestStepIndex, allowSkipTo, - expanded, - onExpandedChange, i18nStrings, isLoadingNextStep, onStepClick, onSkipToClick, steps, }: WizardStepNavigationExpandableProps) { + const [expanded, setExpanded] = useState(true); const collapsedStepsLabel = i18nStrings.collapsedStepsLabel?.(activeStepIndex + 1, steps.length); return ( @@ -38,7 +35,7 @@ export default function WizardStepNavigationExpandable({ variant="footer" headerText={collapsedStepsLabel} expanded={expanded} - onChange={({ detail }) => onExpandedChange(detail.expanded)} + onChange={({ detail }) => setExpanded(detail.expanded)} >
  • diff --git a/src/wizard/styles.scss b/src/wizard/styles.scss index 19dca7c7e5..e2a830356e 100644 --- a/src/wizard/styles.scss +++ b/src/wizard/styles.scss @@ -303,90 +303,43 @@ justify-content: flex-end; } -/* Expandable step navigation styles */ -.expandable-step-list { - list-style: none; - margin-block: 0; - margin-inline: 0; - padding-block-start: awsui.$space-s; - padding-block-end: 0; - padding-inline: 0; -} - +/* Expandable step navigation styles - base classes first for stylelint no-descending-specificity */ .expandable-step-item { - display: grid; - grid-template-columns: awsui.$space-l 1fr; - column-gap: awsui.$space-xs; - padding-block: 0; - padding-inline: 0; -} - -.expandable-step-indicator { - display: flex; - flex-direction: column; - align-items: center; - grid-column: 1; - grid-row: 1 / span 2; -} - -.expandable-step-circle { - flex-shrink: 0; - inline-size: 12px; - block-size: 12px; - margin-block-start: 4px; - border-start-start-radius: 50%; - border-start-end-radius: 50%; - border-end-start-radius: 50%; - border-end-end-radius: 50%; - box-sizing: border-box; -} - -.expandable-step-circle-active { - background-color: awsui.$color-background-control-checked; - box-shadow: - 0 0 0 3px awsui.$color-background-container-content, - 0 0 0 5px awsui.$color-background-control-checked; -} - -.expandable-step-circle-visited { - background-color: awsui.$color-text-interactive-default; - border-block: 2px solid awsui.$color-text-interactive-default; - border-inline: 2px solid awsui.$color-text-interactive-default; -} - -.expandable-step-circle-next { - background-color: awsui.$color-text-interactive-default; - border-block: 2px solid awsui.$color-text-interactive-default; - border-inline: 2px solid awsui.$color-text-interactive-default; -} - -.expandable-step-circle-unvisited { - background-color: awsui.$color-background-container-content; - border-block: 2px solid awsui.$color-text-interactive-disabled; - border-inline: 2px solid awsui.$color-text-interactive-disabled; + /* used in test-utils */ } -.expandable-step-line { - flex-grow: 1; - inline-size: 2px; - min-block-size: awsui.$space-m; - background-color: awsui.$color-border-divider-default; - margin-block: awsui.$space-xxs; +.expandable-step-label { + font-size: awsui.$font-size-body-s; + color: awsui.$color-text-small; + grid-column: 2; + grid-row: 1; } -.expandable-step-content { - display: flex; - flex-direction: column; - grid-column: 2; - padding-block-end: awsui.$space-xs; +.expandable-step-link { + align-items: start; + column-gap: awsui.$space-xs; + display: grid; + font-size: awsui.$font-size-body-m; + grid-column: 1 / span 2; + grid-row: 2; + grid-template-columns: awsui.$space-l 1fr; } -.expandable-step-label { - font-size: awsui.$font-size-body-s; - color: awsui.$color-text-small; +.expandable-step-circle { + border-start-start-radius: 100%; + border-start-end-radius: 100%; + border-end-start-radius: 100%; + border-end-end-radius: 100%; + grid-column: 1; + block-size: 10px; + justify-self: center; + margin-block-start: 6px; + inline-size: 10px; } .expandable-step-title { + @include styles.text-wrapping; + grid-column: 2; font-size: awsui.$font-size-body-m; } @@ -405,10 +358,10 @@ cursor: pointer; text-align: start; font-size: inherit; - color: awsui.$color-text-body-default; + color: awsui.$color-text-interactive-default; &:hover { - text-decoration: underline; + color: awsui.$color-background-control-checked; } @include focus-visible.when-visible { @@ -419,3 +372,85 @@ .expandable-step-title-unvisited { color: awsui.$color-text-status-inactive; } + +/* Expandable step list - ul/li structure with state overrides */ +.expandable-step-list { + list-style: none; + position: relative; + margin-block: 0; + margin-inline: 0; + padding-block-start: awsui.$space-scaled-xxs; + padding-block-end: 0; + padding-inline: 0; + box-sizing: border-box; + + /* stylelint-disable selector-max-type, no-descending-specificity */ + > li { + display: grid; + column-gap: awsui.$space-xs; + grid-template-columns: awsui.$space-l 1fr; + grid-template-rows: repeat(2, auto); + padding-block: 0; + padding-inline: 0; + + > hr { + background-color: awsui.$color-border-divider-default; + border-block: 0; + border-inline: 0; + grid-column: 1; + block-size: 100%; + inline-size: awsui.$space-xxxs; + } + } + + > li:first-child > hr { + grid-row: 2 / span 2; + } + + > li:not(:first-child) > .expandable-step-label { + margin-block-start: awsui.$space-m; + } + + > li:last-child > hr { + grid-row: 1; + } + + > li:only-of-type > hr { + display: none; + } + + > li:not(:first-child):not(:last-child) > hr { + grid-row: 1 / span 3; + } + + > li.expandable-step-active > .expandable-step-link { + cursor: text; + } + + > li.expandable-step-active > .expandable-step-link > .expandable-step-circle { + background-color: awsui.$color-background-control-checked; + box-shadow: + 0 0 0 3px awsui.$color-background-container-content, + 0 0 0 5px awsui.$color-background-control-checked, + 0 0 0 7px awsui.$color-background-container-content; + } + + > li.expandable-step-disabled > .expandable-step-link { + cursor: text; + } + + > li.expandable-step-disabled > .expandable-step-link > .expandable-step-circle { + background-color: awsui.$color-background-container-content; + box-shadow: + 0 0 0 2px awsui.$color-text-interactive-disabled, + 0 0 0 4px awsui.$color-background-container-content; + } + + > li.expandable-step-enabled > .expandable-step-link > .expandable-step-circle { + background-color: awsui.$color-text-interactive-default; + box-shadow: + 0 0 0 2px awsui.$color-text-interactive-default, + 0 0 0 4px awsui.$color-background-container-content; + } + /* stylelint-enable selector-max-type, no-descending-specificity */ +} diff --git a/src/wizard/wizard-form.tsx b/src/wizard/wizard-form.tsx index eabf011791..9d82e08efb 100644 --- a/src/wizard/wizard-form.tsx +++ b/src/wizard/wizard-form.tsx @@ -45,6 +45,8 @@ interface WizardFormProps extends InternalBaseComponentProps { onPrimaryClick: () => void; onStepClick: (stepIndex: number) => void; onSkipToClick: (stepIndex: number) => void; + isStepNavigationExpanded: boolean; + onStepNavigationExpandChange: (expanded: boolean) => void; } export const STEP_NAME_SELECTOR = `[${DATA_ATTR_FUNNEL_KEY}="${FUNNEL_KEY_STEP_NAME}"]`; @@ -91,6 +93,8 @@ function WizardForm({ onPrimaryClick, onStepClick, onSkipToClick, + isStepNavigationExpanded, + onStepNavigationExpandChange, }: WizardFormProps & { stepHeaderRef: MutableRefObject }) { const rootRef = useRef(); const ref = useMergeRefs(rootRef, __internalRootRef); @@ -142,6 +146,8 @@ function WizardForm({ onStepClick, onSkipToClick, steps, + expanded: isStepNavigationExpanded, + onExpandChange: onStepNavigationExpandChange, }; return ( diff --git a/src/wizard/wizard-step-list.tsx b/src/wizard/wizard-step-list.tsx index 06efc0c07e..4e3a9568d9 100644 --- a/src/wizard/wizard-step-list.tsx +++ b/src/wizard/wizard-step-list.tsx @@ -95,29 +95,35 @@ export default function WizardStepList({ onSkipToClick, steps, }: WizardStepListProps) { + const stateMap: Record = { + [StepStatusValues.Active]: 'active', + [StepStatusValues.Unvisited]: 'disabled', + [StepStatusValues.Visited]: 'enabled', + [StepStatusValues.Next]: 'enabled', + }; + return ( -