From f7ebc4fef43ec1f7e675ad95e7d2c93e8cc56f8b Mon Sep 17 00:00:00 2001 From: Amanuel Sisay Date: Fri, 24 Jul 2026 00:04:35 +0200 Subject: [PATCH 01/14] feat: add annotation to steps --- pages/steps/permutations-utils.tsx | 42 +++++++++++ pages/steps/permutations.page.tsx | 2 + .../__snapshots__/documenter.test.ts.snap | 26 ++++++- .../test-utils-selectors.test.tsx.snap | 1 + src/steps/__tests__/steps.test.tsx | 42 +++++++++++ src/steps/interfaces.ts | 2 + src/steps/internal.tsx | 20 +++++- src/steps/styles.scss | 70 ++++++++++++++++--- src/test-utils/dom/steps/index.ts | 7 ++ 9 files changed, 199 insertions(+), 13 deletions(-) diff --git a/pages/steps/permutations-utils.tsx b/pages/steps/permutations-utils.tsx index 6e9dac6e3d..78c51ca91c 100644 --- a/pages/steps/permutations-utils.tsx +++ b/pages/steps/permutations-utils.tsx @@ -322,6 +322,48 @@ export const logSteps: ReadonlyArray = [ }, ]; +export const timelineSteps: ReadonlyArray = [ + { + annotation: , + status: 'log', + header: Provided preferences, + }, + { + annotation: , + status: 'success', + statusIconAriaLabel: 'Success', + header: 'Created environment', + }, + { + annotation: , + status: 'error', + statusIconAriaLabel: 'Error', + header: 'Validation failed', + details: 'One or more resources could not be validated.', + }, +]; + +export const timelineStepsWithVaryingAnnotations: ReadonlyArray = [ + { + annotation: , + status: 'log', + header: 'Shorter timestamp', + }, + { + annotation: ( + + ), + status: 'log', + header: 'Long timestamp', + }, + { + status: 'loading', + header: 'No annotation yet', + }, +]; + export const initialStepsInteractive: ReadonlyArray = [ { status: 'loading', diff --git a/pages/steps/permutations.page.tsx b/pages/steps/permutations.page.tsx index 035ccfa6f5..ed7b8d2c5e 100644 --- a/pages/steps/permutations.page.tsx +++ b/pages/steps/permutations.page.tsx @@ -23,6 +23,8 @@ const stepsPermutations = createPermutations([ variants.failedSteps, variants.allStatusesSteps, variants.logSteps, + variants.timelineSteps, + variants.timelineStepsWithVaryingAnnotations, variants.initialStepsInteractive, variants.loadingStepsInteractive, variants.loadingSteps2Interactive, diff --git a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap index a46d8dafb5..a817adf4f7 100644 --- a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap +++ b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap @@ -27962,7 +27962,8 @@ Each step definition has the following properties: * \`status\` (string) - Status of the step corresponding to a status indicator. The \`log\` status renders a neutral dot marker. * \`statusIconAriaLabel\` - (string) - (Optional) Alternative text for the status icon. * \`header\` (ReactNode) - Summary corresponding to the step. - * \`details\` (ReactNode) - (Optional) Additional information corresponding to the step.", + * \`details\` (ReactNode) - (Optional) Additional information corresponding to the step. + * \`annotation\` (ReactNode) - (Optional) Content rendered at the start of the step, before the icon. Typically a timestamp in a timeline view.", "name": "steps", "optional": false, "type": "ReadonlyArray", @@ -45415,6 +45416,20 @@ Returns the current value of the input.", }, { "methods": [ + { + "description": "Finds the annotation of a step", + "name": "findAnnotation", + "parameters": [], + "returnType": { + "isNullable": true, + "name": "ElementWrapper", + "typeArguments": [ + { + "name": "HTMLElement", + }, + ], + }, + }, { "description": "Finds the details of a step", "name": "findDetails", @@ -55021,6 +55036,15 @@ Supported options: }, { "methods": [ + { + "description": "Finds the annotation of a step", + "name": "findAnnotation", + "parameters": [], + "returnType": { + "isNullable": false, + "name": "ElementWrapper", + }, + }, { "description": "Finds the details of a step", "name": "findDetails", diff --git a/src/__tests__/snapshot-tests/__snapshots__/test-utils-selectors.test.tsx.snap b/src/__tests__/snapshot-tests/__snapshots__/test-utils-selectors.test.tsx.snap index b0220aa79a..c1d4fc8b09 100644 --- a/src/__tests__/snapshot-tests/__snapshots__/test-utils-selectors.test.tsx.snap +++ b/src/__tests__/snapshot-tests/__snapshots__/test-utils-selectors.test.tsx.snap @@ -639,6 +639,7 @@ exports[`test-utils selectors 1`] = ` "awsui_root_1cbgc", ], "steps": [ + "awsui_annotation_gxp9y", "awsui_container_gxp9y", "awsui_details_gxp9y", "awsui_header_gxp9y", diff --git a/src/steps/__tests__/steps.test.tsx b/src/steps/__tests__/steps.test.tsx index baf889db18..c64d0a8283 100644 --- a/src/steps/__tests__/steps.test.tsx +++ b/src/steps/__tests__/steps.test.tsx @@ -271,4 +271,46 @@ describe('Steps', () => { expect(wrapper.findItems()[0].findHeader()?.findStatusIndicator()).toBeNull(); }); }); + + describe('annotation', () => { + test('renders annotation content', () => { + const wrapper = renderSteps({ steps: [{ header: 'Event', status: 'log', annotation: '10:30' }] }); + + expect(wrapper.findItems()[0].findAnnotation()!.getElement()).toHaveTextContent('10:30'); + }); + + test('does not render annotation when not provided', () => { + const wrapper = renderSteps({ steps: [{ header: 'Event', status: 'success' }] }); + + expect(wrapper.findItems()[0].findAnnotation()).toBeNull(); + }); + + test('renders annotation content in horizontal mode', () => { + const wrapper = renderSteps({ + steps: [{ header: 'Event', status: 'log', annotation: '10:30' }], + orientation: 'horizontal', + }); + + expect(wrapper.findItems()[0].findAnnotation()!.getElement()).toHaveTextContent('10:30'); + }); + + test('renders annotation when using renderStep', () => { + const wrapper = renderSteps({ + steps: [{ header: 'Event', status: 'log', annotation: '10:30' }], + renderStep: (step: StepsProps.Step) => ({ header: Custom: {step.header} }), + }); + + expect(wrapper.findItems()[0].findAnnotation()!.getElement()).toHaveTextContent('10:30'); + }); + + test('renders annotation in horizontal mode when using renderStep', () => { + const wrapper = renderSteps({ + steps: [{ header: 'Event', status: 'log', annotation: '10:30' }], + orientation: 'horizontal', + renderStep: (step: StepsProps.Step) => ({ header: Custom: {step.header} }), + }); + + expect(wrapper.findItems()[0].findAnnotation()!.getElement()).toHaveTextContent('10:30'); + }); + }); }); diff --git a/src/steps/interfaces.ts b/src/steps/interfaces.ts index 75f234e24e..41b17edf09 100644 --- a/src/steps/interfaces.ts +++ b/src/steps/interfaces.ts @@ -12,6 +12,7 @@ export interface StepsProps extends BaseComponentProps { * * `statusIconAriaLabel` - (string) - (Optional) Alternative text for the status icon. * * `header` (ReactNode) - Summary corresponding to the step. * * `details` (ReactNode) - (Optional) Additional information corresponding to the step. + * * `annotation` (ReactNode) - (Optional) Content rendered at the start of the step, before the icon. Typically a timestamp in a timeline view. */ steps: ReadonlyArray; /** @@ -62,6 +63,7 @@ export namespace StepsProps { statusIconAriaLabel?: string; header: React.ReactNode; details?: React.ReactNode; + annotation?: React.ReactNode; } export type Orientation = 'vertical' | 'horizontal'; diff --git a/src/steps/internal.tsx b/src/steps/internal.tsx index 9f86ce2e28..8ad1a94736 100644 --- a/src/steps/internal.tsx +++ b/src/steps/internal.tsx @@ -28,6 +28,13 @@ const statusToColor: Record = { log: 'text-status-inactive', }; +const StepAnnotation = ({ children }: { children: StepsProps.Step['annotation'] }) => { + if (children === undefined || children === null) { + return null; + } + return
{children}
; +}; + const CustomStep = ({ step, orientation, @@ -39,7 +46,7 @@ const CustomStep = ({ renderStep: Required['renderStep']; hideConnectors: boolean; }) => { - const { status, statusIconAriaLabel } = step; + const { status, statusIconAriaLabel, annotation } = step; const { header, details, icon } = renderStep(step); const iconNode = icon ? icon : ; const connectorClassName = clsx(styles.connector, hideConnectors && styles['connector-hidden']); @@ -47,6 +54,7 @@ const CustomStep = ({ if (orientation === 'horizontal') { return (
  • + {annotation}
    {iconNode}
    @@ -60,9 +68,11 @@ const CustomStep = ({ // Vertical orientation: render the icon and the connector together in a column-1 "rail" so the // connector starts directly beneath the icon and stretches the full height of the step. Unlike // placing the header in the same row as the icon, this keeps the vertical line continuous even - // when the custom header wraps onto multiple lines. + // when the custom header wraps onto multiple lines. `annotation` (for example, a timeline timestamp) + // is rendered before the rail. return (
  • + {annotation}
    {iconNode}
    @@ -80,12 +90,14 @@ const InternalStep = ({ statusIconAriaLabel, header, details, + annotation, orientation, hideConnectors, }: StepsProps.Step & { orientation: StepsProps.Orientation; hideConnectors: boolean }) => { const connectorClassName = clsx(styles.connector, hideConnectors && styles['connector-hidden']); return (
  • + {annotation}
    {orientation === 'vertical' ? ( @@ -124,6 +136,7 @@ const InternalSteps = ({ ...props }: InternalStepsProps) => { const hideConnectors = connectorLines === 'none'; + const hasAnnotations = steps.some(step => step.annotation !== undefined && step.annotation !== null); return (
      diff --git a/src/steps/styles.scss b/src/steps/styles.scss index e4d504ff4d..22f4385502 100644 --- a/src/steps/styles.scss +++ b/src/steps/styles.scss @@ -27,6 +27,11 @@ grid-template-columns: awsui.$space-static-l 1fr; grid-template-rows: minmax(awsui.$space-static-l, auto); + > .annotation { + min-inline-size: 0; + color: awsui.$color-text-status-inactive; + } + > .header { display: flex; gap: awsui.$space-xxs; @@ -65,24 +70,28 @@ } } } - .horizontal { > .list { display: grid; align-items: flex-start; grid-template-columns: repeat(auto-fit, minmax(0, 1fr)); + // Shared rows: annotation | icon and connector | header | details. + grid-template-rows: auto minmax(awsui.$space-static-l, auto) auto auto; grid-auto-flow: column; > .container { - display: grid; - grid-template-columns: awsui.$space-static-l 1fr; - grid-template-rows: minmax(awsui.$space-static-l, auto); + // Spanning the subgrid rows keeps steps with different-height annotations aligned. + grid-row: 1 / -1; + grid-template-rows: subgrid; align-items: center; - > .header { - display: flex; + > .annotation { grid-row: 1; grid-column: 1 / span 2; + } + + > .header { + grid-row: 2; align-items: center; > .connector { @@ -92,7 +101,6 @@ border-block: 0; border-inline: 0; min-block-size: 0; - inset-inline-end: 0; block-size: awsui.$border-divider-list-width; inline-size: auto; @@ -102,13 +110,13 @@ } > .horizontal-header { - grid-row: 2; + grid-row: 3; grid-column: 1 / span 3; padding-inline-end: awsui.$space-xs; } > .details { - grid-row: 3; + grid-row: 4; grid-column: 1 / span 3; padding-inline-end: awsui.$space-xs; } @@ -156,6 +164,50 @@ } } +// Shared columns via subgrid: widest annotation | icon and connector | remaining content. +.root > .list.with-annotation { + display: grid; + grid-template-columns: max-content awsui.$space-static-l 1fr; + + > .container { + grid-column: 1 / -1; + grid-template-columns: subgrid; + + > .annotation { + grid-row: 1; + grid-column: 1; + padding-inline-end: awsui.$space-xxs; + } + } + + // Header spans the icon and content columns; connector and details align beneath each part. + > .container:not(.custom-vertical) { + > .header { + grid-column: 2 / span 2; + } + + > .connector { + grid-column: 2; + } + + > .details { + grid-column: 3; + } + } + + // Custom DOM: rail in the icon column, header and details in the content column. + > .container.custom-vertical { + > .rail { + grid-column: 2; + } + + > .content { + grid-column: 3; + } + } +} + +// Hide the connector after the final custom/rail step. Placed last so its higher specificity .root > .list > .container.custom-vertical:last-of-type > .rail > .connector { display: none; } diff --git a/src/test-utils/dom/steps/index.ts b/src/test-utils/dom/steps/index.ts index e8e607a131..ee14f316b2 100644 --- a/src/test-utils/dom/steps/index.ts +++ b/src/test-utils/dom/steps/index.ts @@ -18,6 +18,13 @@ class StepWrapper extends ComponentWrapper { findDetails(): ElementWrapper | null { return this.findByClassName(styles.details); } + + /** + * Finds the annotation of a step + */ + findAnnotation(): ElementWrapper | null { + return this.findByClassName(styles.annotation); + } } export default class StepsWrapper extends ComponentWrapper { static rootSelector: string = styles.root; From 2afe7ee418c26fcc816fb9cbcd8e615db961fdeb Mon Sep 17 00:00:00 2001 From: Amanuel Sisay Date: Fri, 24 Jul 2026 00:15:45 +0200 Subject: [PATCH 02/14] chore: add custom steps permutation --- pages/steps/permutations-custom-steps.page.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pages/steps/permutations-custom-steps.page.tsx b/pages/steps/permutations-custom-steps.page.tsx index 720a1c0fc1..ec8dbd01c7 100644 --- a/pages/steps/permutations-custom-steps.page.tsx +++ b/pages/steps/permutations-custom-steps.page.tsx @@ -12,7 +12,7 @@ import * as variants from './permutations-utils'; const stepsPermutations = createPermutations([ { - steps: [variants.allStatusesSteps, variants.successfulSteps], + steps: [variants.allStatusesSteps, variants.successfulSteps, variants.timelineStepsWithVaryingAnnotations], ariaLabel: ['test label'], orientation: ['vertical', 'horizontal'], renderStep: [ @@ -25,6 +25,12 @@ const stepsPermutations = createPermutations([ details: step.details && Custom details for {step.details}, icon: , }), + step => ({ + annotation: step.annotation, + header: step.header, + details: step.details && Custom details for {step.details}, + icon: , + }), ], }, ]); From a620509e30dce225d44d4a9ef1ee81f1c95f0f26 Mon Sep 17 00:00:00 2001 From: Amanuel Sisay Date: Fri, 24 Jul 2026 03:12:29 +0200 Subject: [PATCH 03/14] fix: wrap annotation and connector line --- src/steps/styles.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/steps/styles.scss b/src/steps/styles.scss index 22f4385502..eada7a45f1 100644 --- a/src/steps/styles.scss +++ b/src/steps/styles.scss @@ -167,7 +167,7 @@ // Shared columns via subgrid: widest annotation | icon and connector | remaining content. .root > .list.with-annotation { display: grid; - grid-template-columns: max-content awsui.$space-static-l 1fr; + grid-template-columns: fit-content(50%) awsui.$space-static-l 1fr; > .container { grid-column: 1 / -1; From cb87db37a4f5ee891d1fd1684d4fadcb093dcb3e Mon Sep 17 00:00:00 2001 From: Amanuel Sisay Date: Fri, 24 Jul 2026 03:46:12 +0200 Subject: [PATCH 04/14] fix: connect line should fill when annotation wrap --- src/steps/internal.tsx | 10 +++++++--- src/steps/styles.scss | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/src/steps/internal.tsx b/src/steps/internal.tsx index 8ad1a94736..44ba623d54 100644 --- a/src/steps/internal.tsx +++ b/src/steps/internal.tsx @@ -95,14 +95,18 @@ const InternalStep = ({ hideConnectors, }: StepsProps.Step & { orientation: StepsProps.Orientation; hideConnectors: boolean }) => { const connectorClassName = clsx(styles.connector, hideConnectors && styles['connector-hidden']); + const hasAnnotation = annotation !== undefined && annotation !== null; return (
    1. {annotation}
      {orientation === 'vertical' ? ( - - {header} - + <> + + {header} + + {hasAnnotation &&
      } + ) : ( <> diff --git a/src/steps/styles.scss b/src/steps/styles.scss index eada7a45f1..539c4616d7 100644 --- a/src/steps/styles.scss +++ b/src/steps/styles.scss @@ -70,6 +70,42 @@ } } } + +.vertical { + > .list { + // The continuation starts below the rendered StatusIndicator and fills only extra height created + // by a wrapped annotation. The main connector remains in row 2 to span the details area. + > .container > .header { + display: grid; + grid-template-columns: subgrid; + grid-template-rows: auto 1fr; + gap: awsui.$space-xxs 0; + + > :first-child { + grid-column: 1 / -1; + } + + > .connector-continuation { + grid-row: 2; + grid-column: 1; + justify-self: center; + margin-block: 0; + margin-inline: 0; + border-block: 0; + border-inline: 0; + inline-size: awsui.$border-divider-list-width; + block-size: auto; + position: relative; + inset-inline-end: awsui.$space-static-xxxs; + } + } + + > :last-of-type > .header > .connector-continuation { + display: none; + } + } +} + .horizontal { > .list { display: grid; @@ -101,6 +137,7 @@ border-block: 0; border-inline: 0; min-block-size: 0; + inset-inline-end: 0; block-size: awsui.$border-divider-list-width; inline-size: auto; From d60e3ba902dcdf1bc87be2f8573c6dc4d94db943 Mon Sep 17 00:00:00 2001 From: Amanuel Sisay Date: Fri, 24 Jul 2026 11:12:53 +0200 Subject: [PATCH 05/14] chore: create separate permutuation page for annotation --- pages/steps/permutations-annotation.page.tsx | 80 +++++++++++++++++++ .../steps/permutations-custom-steps.page.tsx | 8 +- pages/steps/permutations-utils.tsx | 42 ---------- pages/steps/permutations.page.tsx | 2 - 4 files changed, 81 insertions(+), 51 deletions(-) create mode 100644 pages/steps/permutations-annotation.page.tsx diff --git a/pages/steps/permutations-annotation.page.tsx b/pages/steps/permutations-annotation.page.tsx new file mode 100644 index 0000000000..e01d0c7ed8 --- /dev/null +++ b/pages/steps/permutations-annotation.page.tsx @@ -0,0 +1,80 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React from 'react'; + +import { Icon } from '~components'; +import Steps, { StepsProps } from '~components/steps'; + +import { SimplePage } from '../app/templates'; +import createPermutations from '../utils/permutations'; +import PermutationsView from '../utils/permutations-view'; + +export const stepsWithAnnotation: ReadonlyArray = [ + { + annotation: , + status: 'log', + header: 'Provided preferences', + }, + { + annotation: , + status: 'success', + statusIconAriaLabel: 'Success', + header: 'Created environment', + }, + { + annotation: , + status: 'error', + statusIconAriaLabel: 'Error', + header: 'Validation failed', + details: 'One or more resources could not be validated.', + }, +]; + +export const varyingLengthAnnotationsSteps: ReadonlyArray = [ + { + annotation: , + status: 'log', + header: 'Shorter timestamp', + }, + { + annotation: ( + + ), + status: 'log', + header: 'Long timestamp', + }, + { + status: 'loading', + header: 'No annotation yet', + }, +]; + +const stepsPermutations = createPermutations([ + { + steps: [varyingLengthAnnotationsSteps, stepsWithAnnotation], + ariaLabel: ['test label'], + orientation: ['vertical', 'horizontal'], + renderStep: [ + undefined, + step => ({ + annotation: step.annotation, + header: step.header, + details: step.details && Custom details for {step.details}, + icon: , + }), + ], + }, +]); + +export default function StepsPermutations() { + return ( + +
      {}
      } + /> +
      + ); +} diff --git a/pages/steps/permutations-custom-steps.page.tsx b/pages/steps/permutations-custom-steps.page.tsx index ec8dbd01c7..720a1c0fc1 100644 --- a/pages/steps/permutations-custom-steps.page.tsx +++ b/pages/steps/permutations-custom-steps.page.tsx @@ -12,7 +12,7 @@ import * as variants from './permutations-utils'; const stepsPermutations = createPermutations([ { - steps: [variants.allStatusesSteps, variants.successfulSteps, variants.timelineStepsWithVaryingAnnotations], + steps: [variants.allStatusesSteps, variants.successfulSteps], ariaLabel: ['test label'], orientation: ['vertical', 'horizontal'], renderStep: [ @@ -25,12 +25,6 @@ const stepsPermutations = createPermutations([ details: step.details && Custom details for {step.details}, icon: , }), - step => ({ - annotation: step.annotation, - header: step.header, - details: step.details && Custom details for {step.details}, - icon: , - }), ], }, ]); diff --git a/pages/steps/permutations-utils.tsx b/pages/steps/permutations-utils.tsx index 78c51ca91c..6e9dac6e3d 100644 --- a/pages/steps/permutations-utils.tsx +++ b/pages/steps/permutations-utils.tsx @@ -322,48 +322,6 @@ export const logSteps: ReadonlyArray = [ }, ]; -export const timelineSteps: ReadonlyArray = [ - { - annotation: , - status: 'log', - header: Provided preferences, - }, - { - annotation: , - status: 'success', - statusIconAriaLabel: 'Success', - header: 'Created environment', - }, - { - annotation: , - status: 'error', - statusIconAriaLabel: 'Error', - header: 'Validation failed', - details: 'One or more resources could not be validated.', - }, -]; - -export const timelineStepsWithVaryingAnnotations: ReadonlyArray = [ - { - annotation: , - status: 'log', - header: 'Shorter timestamp', - }, - { - annotation: ( - - ), - status: 'log', - header: 'Long timestamp', - }, - { - status: 'loading', - header: 'No annotation yet', - }, -]; - export const initialStepsInteractive: ReadonlyArray = [ { status: 'loading', diff --git a/pages/steps/permutations.page.tsx b/pages/steps/permutations.page.tsx index ed7b8d2c5e..035ccfa6f5 100644 --- a/pages/steps/permutations.page.tsx +++ b/pages/steps/permutations.page.tsx @@ -23,8 +23,6 @@ const stepsPermutations = createPermutations([ variants.failedSteps, variants.allStatusesSteps, variants.logSteps, - variants.timelineSteps, - variants.timelineStepsWithVaryingAnnotations, variants.initialStepsInteractive, variants.loadingStepsInteractive, variants.loadingSteps2Interactive, From dee540846f1dcfcc20296a063cf71919d6b30540 Mon Sep 17 00:00:00 2001 From: Amanuel Sisay Date: Fri, 24 Jul 2026 17:01:30 +0200 Subject: [PATCH 06/14] fix: Align step details with status indicator --- pages/steps/permutations-annotation.page.tsx | 12 ++++ src/steps/__tests__/steps.test.tsx | 6 +- src/steps/internal.tsx | 15 ++-- src/steps/styles.scss | 76 +++++++++++--------- 4 files changed, 66 insertions(+), 43 deletions(-) diff --git a/pages/steps/permutations-annotation.page.tsx b/pages/steps/permutations-annotation.page.tsx index e01d0c7ed8..bce5657438 100644 --- a/pages/steps/permutations-annotation.page.tsx +++ b/pages/steps/permutations-annotation.page.tsx @@ -43,6 +43,7 @@ export const varyingLengthAnnotationsSteps: ReadonlyArray = [ ), status: 'log', + details: 'One or more resources could not be validated. A detail in the middle', header: 'Long timestamp', }, { @@ -64,8 +65,19 @@ const stepsPermutations = createPermutations([ details: step.details && Custom details for {step.details}, icon: , }), + step => ({ + annotation: step.annotation, + header: This step header ({step.header}) is wrapped in a custom HTML tag and has very long content, + details: step.details && Custom details for {step.details}, + }), ], }, + { + connectorLines: ['none'], + orientation: ['vertical', 'horizontal'], + steps: [varyingLengthAnnotationsSteps], + ariaLabel: ['test label'], + }, ]); export default function StepsPermutations() { diff --git a/src/steps/__tests__/steps.test.tsx b/src/steps/__tests__/steps.test.tsx index c64d0a8283..0e79a12d37 100644 --- a/src/steps/__tests__/steps.test.tsx +++ b/src/steps/__tests__/steps.test.tsx @@ -191,7 +191,7 @@ describe('Steps', () => { 'renders visible connector lines when orientation=$orientation and connectorLines=$connectorLines', ({ orientation, connectorLines }) => { const wrapper = renderSteps({ steps: successfulSteps, orientation, connectorLines }); - expect(wrapper.findAllByClassName(stepsStyles.connector)).toHaveLength(4); + expect(wrapper.findAllByClassName(stepsStyles.connector)).not.toHaveLength(0); expect(wrapper.findAllByClassName(stepsStyles['connector-hidden'])).toHaveLength(0); } ); @@ -200,8 +200,8 @@ describe('Steps', () => { 'hides connector lines by marking every connector when orientation=$orientation and connectorLines="none"', ({ orientation }) => { const wrapper = renderSteps({ steps: successfulSteps, orientation, connectorLines: 'none' }); - expect(wrapper.findAllByClassName(stepsStyles.connector)).toHaveLength(4); - expect(wrapper.findAllByClassName(stepsStyles['connector-hidden'])).toHaveLength(4); + const connectors = wrapper.findAllByClassName(stepsStyles.connector); + expect(wrapper.findAllByClassName(stepsStyles['connector-hidden'])).toHaveLength(connectors.length); } ); diff --git a/src/steps/internal.tsx b/src/steps/internal.tsx index 44ba623d54..ea66531138 100644 --- a/src/steps/internal.tsx +++ b/src/steps/internal.tsx @@ -105,7 +105,10 @@ const InternalStep = ({ {header} - {hasAnnotation &&
      } + {details &&
      {details}
      } + {(hasAnnotation || details) && ( +
      + )} ) : ( <> @@ -119,11 +122,13 @@ const InternalStep = ({ {orientation === 'vertical' ? (
      ) : ( -
      - {header} -
      + <> +
      + {header} +
      + {details &&
      {details}
      } + )} - {details &&
      {details}
      }
    2. ); }; diff --git a/src/steps/styles.scss b/src/steps/styles.scss index 539c4616d7..840e241cdc 100644 --- a/src/steps/styles.scss +++ b/src/steps/styles.scss @@ -71,41 +71,6 @@ } } -.vertical { - > .list { - // The continuation starts below the rendered StatusIndicator and fills only extra height created - // by a wrapped annotation. The main connector remains in row 2 to span the details area. - > .container > .header { - display: grid; - grid-template-columns: subgrid; - grid-template-rows: auto 1fr; - gap: awsui.$space-xxs 0; - - > :first-child { - grid-column: 1 / -1; - } - - > .connector-continuation { - grid-row: 2; - grid-column: 1; - justify-self: center; - margin-block: 0; - margin-inline: 0; - border-block: 0; - border-inline: 0; - inline-size: awsui.$border-divider-list-width; - block-size: auto; - position: relative; - inset-inline-end: awsui.$space-static-xxxs; - } - } - - > :last-of-type > .header > .connector-continuation { - display: none; - } - } -} - .horizontal { > .list { display: grid; @@ -167,6 +132,47 @@ } } +.vertical { + > .list { + // Status and details share this nested grid so annotation height does not affect their spacing. + // The continuation runs beside details and fills any extra annotation height. + > .container > .header { + display: grid; + grid-template-columns: subgrid; + grid-template-rows: auto 1fr; + gap: 0; + + > :first-child { + grid-column: 1 / -1; + } + + > .details { + grid-row: 2; + grid-column: 2; + margin-block-end: awsui.$space-static-xs; + } + + > .connector-continuation { + grid-row: 2; + grid-column: 1; + justify-self: center; + margin-block: 0; + margin-inline: 0; + border-block: 0; + border-inline: 0; + inline-size: awsui.$border-divider-list-width; + block-size: auto; + position: relative; + inset-inline-end: awsui.$space-static-xxxs; + } + } + + > :last-of-type > .header > .connector-continuation { + display: none; + } + } +} + // Vertical custom steps: a column-1 "rail" stacks the icon above a connector that grows to fill // the full height of the step (header + details), keeping the vertical line continuous and // connecting consecutive steps regardless of header height. Placed after `.horizontal` so From aa03d9d97e1f56335fc024bef3f12871cb613dbc Mon Sep 17 00:00:00 2001 From: Amanuel Sisay Date: Fri, 24 Jul 2026 17:02:06 +0200 Subject: [PATCH 07/14] chore: factorize details --- src/steps/internal.tsx | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/steps/internal.tsx b/src/steps/internal.tsx index ea66531138..237173fbda 100644 --- a/src/steps/internal.tsx +++ b/src/steps/internal.tsx @@ -35,6 +35,13 @@ const StepAnnotation = ({ children }: { children: StepsProps.Step['annotation'] return
      {children}
      ; }; +const StepDetails = ({ children }: { children: StepsProps.Step['details'] }) => { + if (children === undefined || children === null) { + return null; + } + return
      {children}
      ; +}; + const CustomStep = ({ step, orientation, @@ -60,7 +67,7 @@ const CustomStep = ({
    {header}
    - {details &&
    {details}
    } + {details}
  • ); } @@ -79,7 +86,7 @@ const CustomStep = ({
    {header}
    - {details &&
    {details}
    } + {details}
    ); @@ -105,7 +112,7 @@ const InternalStep = ({ {header} - {details &&
    {details}
    } + {details} {(hasAnnotation || details) && (
    )} @@ -126,7 +133,7 @@ const InternalStep = ({
    {header}
    - {details &&
    {details}
    } + {details} )} From 42a86351f2e824cbb639744823a8f32ceb3aa25d Mon Sep 17 00:00:00 2001 From: Amanuel Sisay Date: Mon, 27 Jul 2026 00:08:17 +0200 Subject: [PATCH 08/14] fix: alignment on horizontal and vertical steps --- src/steps/styles.scss | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/steps/styles.scss b/src/steps/styles.scss index 840e241cdc..3f69fd2f85 100644 --- a/src/steps/styles.scss +++ b/src/steps/styles.scss @@ -28,6 +28,7 @@ grid-template-rows: minmax(awsui.$space-static-l, auto); > .annotation { + justify-self: flex-end; min-inline-size: 0; color: awsui.$color-text-status-inactive; } @@ -87,6 +88,8 @@ align-items: center; > .annotation { + justify-self: flex-start; + align-self: flex-end; grid-row: 1; grid-column: 1 / span 2; } @@ -111,16 +114,19 @@ } } - > .horizontal-header { - grid-row: 3; + > .horizontal-header, + > .details { + align-self: flex-start; grid-column: 1 / span 3; padding-inline-end: awsui.$space-xs; } + > .horizontal-header { + grid-row: 3; + } + > .details { grid-row: 4; - grid-column: 1 / span 3; - padding-inline-end: awsui.$space-xs; } } From 6c4cc4154cb224da12c92298feb6b113637755ea Mon Sep 17 00:00:00 2001 From: Amanuel Sisay Date: Mon, 27 Jul 2026 00:45:28 +0200 Subject: [PATCH 09/14] fix: Use a single vertical step connector --- src/steps/internal.tsx | 9 ++------- src/steps/styles.scss | 25 ++----------------------- 2 files changed, 4 insertions(+), 30 deletions(-) diff --git a/src/steps/internal.tsx b/src/steps/internal.tsx index 237173fbda..f4cf28ae4e 100644 --- a/src/steps/internal.tsx +++ b/src/steps/internal.tsx @@ -102,7 +102,6 @@ const InternalStep = ({ hideConnectors, }: StepsProps.Step & { orientation: StepsProps.Orientation; hideConnectors: boolean }) => { const connectorClassName = clsx(styles.connector, hideConnectors && styles['connector-hidden']); - const hasAnnotation = annotation !== undefined && annotation !== null; return (
  • {annotation} @@ -113,9 +112,7 @@ const InternalStep = ({ {header} {details} - {(hasAnnotation || details) && ( -
    - )} +
    ) : ( <> @@ -126,9 +123,7 @@ const InternalStep = ({ )} - {orientation === 'vertical' ? ( -
    - ) : ( + {orientation === 'horizontal' && ( <>
    {header} diff --git a/src/steps/styles.scss b/src/steps/styles.scss index 3f69fd2f85..0556f37ade 100644 --- a/src/steps/styles.scss +++ b/src/steps/styles.scss @@ -46,23 +46,6 @@ grid-column: 2; margin-block-end: awsui.$space-static-xs; } - - > .connector { - grid-row: 2; - grid-column: 1; - margin-block: 0; - border-block: 0; - border-inline: 0; - inline-size: awsui.$border-divider-list-width; - block-size: auto; - min-block-size: awsui.$space-static-xs; - position: relative; - inset-inline-end: awsui.$space-static-xxxs; - } - } - - > :last-of-type > .connector { - display: none; } &.custom > .details { @@ -140,8 +123,7 @@ .vertical { > .list { - // Status and details share this nested grid so annotation height does not affect their spacing. - // The continuation runs beside details and fills any extra annotation height. + // A single connector fills extra annotation/details height and provides the default spacing otherwise. > .container > .header { display: grid; grid-template-columns: subgrid; @@ -168,6 +150,7 @@ border-inline: 0; inline-size: awsui.$border-divider-list-width; block-size: auto; + min-block-size: awsui.$space-static-xs; position: relative; inset-inline-end: awsui.$space-static-xxxs; } @@ -235,10 +218,6 @@ grid-column: 2 / span 2; } - > .connector { - grid-column: 2; - } - > .details { grid-column: 3; } From 51840a8d0cd5042faec2dffc3e6c12d11a8b1bb6 Mon Sep 17 00:00:00 2001 From: Amanuel Sisay Date: Mon, 27 Jul 2026 00:54:58 +0200 Subject: [PATCH 10/14] revert: reverts alignment in horizontal fix --- src/steps/styles.scss | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/steps/styles.scss b/src/steps/styles.scss index 0556f37ade..e885830b40 100644 --- a/src/steps/styles.scss +++ b/src/steps/styles.scss @@ -97,19 +97,16 @@ } } - > .horizontal-header, - > .details { - align-self: flex-start; - grid-column: 1 / span 3; - padding-inline-end: awsui.$space-xs; - } - > .horizontal-header { grid-row: 3; + grid-column: 1 / span 3; + padding-inline-end: awsui.$space-xs; } > .details { grid-row: 4; + grid-column: 1 / span 3; + padding-inline-end: awsui.$space-xs; } } From 384ae25aeb76a8074ca17d15f1d009f184b318eb Mon Sep 17 00:00:00 2001 From: Amanuel Sisay Date: Mon, 27 Jul 2026 01:27:53 +0200 Subject: [PATCH 11/14] fix: alignment on horizontal steps --- src/steps/internal.tsx | 10 ++++++---- src/steps/styles.scss | 16 ++++++++-------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/steps/internal.tsx b/src/steps/internal.tsx index f4cf28ae4e..1786090aba 100644 --- a/src/steps/internal.tsx +++ b/src/steps/internal.tsx @@ -66,8 +66,10 @@ const CustomStep = ({ {iconNode}
    -
    {header}
    - {details} +
    +
    {header}
    + {details} +
  • ); } @@ -124,12 +126,12 @@ const InternalStep = ({ )} {orientation === 'horizontal' && ( - <> +
    {header}
    {details} - +
    )} ); diff --git a/src/steps/styles.scss b/src/steps/styles.scss index e885830b40..92016d362e 100644 --- a/src/steps/styles.scss +++ b/src/steps/styles.scss @@ -60,8 +60,8 @@ display: grid; align-items: flex-start; grid-template-columns: repeat(auto-fit, minmax(0, 1fr)); - // Shared rows: annotation | icon and connector | header | details. - grid-template-rows: auto minmax(awsui.$space-static-l, auto) auto auto; + // Shared rows: annotation | icon and connector | header and details. + grid-template-rows: auto minmax(awsui.$space-static-l, auto) auto; grid-auto-flow: column; > .container { @@ -97,16 +97,16 @@ } } - > .horizontal-header { + > .content { + align-self: flex-start; grid-row: 3; grid-column: 1 / span 3; + min-inline-size: 0; padding-inline-end: awsui.$space-xs; - } - > .details { - grid-row: 4; - grid-column: 1 / span 3; - padding-inline-end: awsui.$space-xs; + > .details { + margin-block-end: awsui.$space-static-xs; + } } } From d94948295b49540f105b6f09da8ae496d31c5dd4 Mon Sep 17 00:00:00 2001 From: Amanuel Sisay Date: Mon, 27 Jul 2026 01:56:26 +0200 Subject: [PATCH 12/14] chore: add more details to permutations of annotation page --- pages/steps/permutations-annotation.page.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pages/steps/permutations-annotation.page.tsx b/pages/steps/permutations-annotation.page.tsx index bce5657438..6b89c6b3a5 100644 --- a/pages/steps/permutations-annotation.page.tsx +++ b/pages/steps/permutations-annotation.page.tsx @@ -20,6 +20,7 @@ export const stepsWithAnnotation: ReadonlyArray = [ status: 'success', statusIconAriaLabel: 'Success', header: 'Created environment', + details: 'Environment created successfully.', }, { annotation: , @@ -35,6 +36,7 @@ export const varyingLengthAnnotationsSteps: ReadonlyArray = [ annotation: , status: 'log', header: 'Shorter timestamp', + details: 'This is used to test the shorter timestamp and and we have detail here', }, { annotation: ( @@ -49,6 +51,7 @@ export const varyingLengthAnnotationsSteps: ReadonlyArray = [ { status: 'loading', header: 'No annotation yet', + details: 'This is used to test without annotation.', }, ]; From 435b1dd84114a43f8f99f97fe52cc36d7a1cbc2b Mon Sep 17 00:00:00 2001 From: Amanuel Sisay Date: Mon, 27 Jul 2026 02:02:22 +0200 Subject: [PATCH 13/14] fix: add padding between horizontal timelines --- src/steps/styles.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/src/steps/styles.scss b/src/steps/styles.scss index 92016d362e..c9181c1075 100644 --- a/src/steps/styles.scss +++ b/src/steps/styles.scss @@ -75,6 +75,7 @@ align-self: flex-end; grid-row: 1; grid-column: 1 / span 2; + padding-inline-end: awsui.$space-xs; } > .header { From 79df3dd06f0762ad9ef7e3198abbaa47f89147c7 Mon Sep 17 00:00:00 2001 From: Amanuel Sisay Date: Fri, 24 Jul 2026 05:14:56 +0200 Subject: [PATCH 14/14] test: add test permutation, and playground --- pages/steps/playground.page.tsx | 166 ++++++++++++++++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 pages/steps/playground.page.tsx diff --git a/pages/steps/playground.page.tsx b/pages/steps/playground.page.tsx new file mode 100644 index 0000000000..ea36b3620d --- /dev/null +++ b/pages/steps/playground.page.tsx @@ -0,0 +1,166 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +import React, { useState } from 'react'; + +import Checkbox from '~components/checkbox'; +import ColumnLayout from '~components/column-layout'; +import Container from '~components/container'; +import FormField from '~components/form-field'; +import Header from '~components/header'; +import Select from '~components/select'; +import SpaceBetween from '~components/space-between'; +import Steps, { StepsProps } from '~components/steps'; +import Textarea from '~components/textarea'; + +import { SimplePage } from '../app/templates'; + +const statusValues: ReadonlyArray = [ + 'log', + 'success', + 'error', + 'warning', + 'info', + 'loading', + 'in-progress', + 'pending', + 'stopped', + 'not-started', +]; + +const orientationOptions = [ + { label: 'Vertical', value: 'vertical' }, + { label: 'Horizontal', value: 'horizontal' }, +]; + +const defaultStepsJson = JSON.stringify( + [ + { + annotation: '9:00 AM', + header: 'Request received', + status: 'log', + statusIconAriaLabel: 'Log entry', + details: 'The request was added to the activity log.', + }, + { + annotation: '9:05 AM', + header: 'Processing request', + status: 'in-progress', + statusIconAriaLabel: 'In progress', + details: 'The request is being processed.', + }, + { + annotation: '9:10 AM', + header: 'Request completed', + status: 'success', + statusIconAriaLabel: 'Success', + details: 'The request completed successfully.', + }, + ], + null, + 2 +); + +function parseSteps(value: string): { steps: ReadonlyArray; errorText?: string } { + try { + const parsed: unknown = JSON.parse(value); + if (!Array.isArray(parsed)) { + throw new Error('Enter a JSON list of steps.'); + } + + const steps = parsed.map((item, index): StepsProps.Step => { + if (!item || typeof item !== 'object') { + throw new Error(`Step ${index + 1} must be an object.`); + } + + const { annotation, header, status, statusIconAriaLabel, details } = item as Record; + if (typeof header !== 'string') { + throw new Error(`Step ${index + 1} must have a string header.`); + } + if (typeof status !== 'string' || !statusValues.includes(status as StepsProps.Status)) { + throw new Error(`Step ${index + 1} has an invalid status.`); + } + if (annotation !== undefined && typeof annotation !== 'string') { + throw new Error(`Step ${index + 1} annotation must be a string.`); + } + if (details !== undefined && typeof details !== 'string') { + throw new Error(`Step ${index + 1} details must be a string.`); + } + if (statusIconAriaLabel !== undefined && typeof statusIconAriaLabel !== 'string') { + throw new Error(`Step ${index + 1} statusIconAriaLabel must be a string.`); + } + + return { + annotation: annotation === undefined ? undefined : {annotation}, + header: {header}, + details: details === undefined ? undefined : {details}, + status: status as StepsProps.Status, + statusIconAriaLabel, + }; + }); + + return { steps }; + } catch (error) { + return { steps: [], errorText: error instanceof Error ? error.message : 'Invalid JSON.' }; + } +} + +export default function StepsPlayground() { + const [stepsJson, setStepsJson] = useState(defaultStepsJson); + const [orientation, setOrientation] = useState('vertical'); + const [showConnector, setShowConnector] = useState(true); + const [isRtl, setIsRtl] = useState(false); + const { steps, errorText } = parseSteps(stepsJson); + + return ( + + +