Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
ba66f17
feat: first test for new wizard
Who-is-PS Jan 22, 2026
37f59b7
feat: Add example with popover for wizard
Who-is-PS Jan 26, 2026
de32932
feat: Add example for expandable section
Who-is-PS Jan 29, 2026
087e51d
feat: Wizard with expandable section
Who-is-PS Jan 30, 2026
bce987c
chore: Update snapshot tests
Who-is-PS Jan 30, 2026
916815d
feat: Update tests and refactor files for wizard
Who-is-PS Feb 2, 2026
db297ce
fix: Add can skip test
Who-is-PS Feb 2, 2026
d61db64
fix: Remove unnecessary files
Who-is-PS Feb 2, 2026
f7e66dd
clean up
Who-is-PS Feb 2, 2026
6845787
clean up
Who-is-PS Feb 2, 2026
f58bca1
clean up
Who-is-PS Feb 2, 2026
a4cdafb
clean up
Who-is-PS Feb 2, 2026
7408dc0
clean up
Who-is-PS Feb 2, 2026
16247d4
update snapshot tests
Who-is-PS Feb 2, 2026
2f8da78
add new test for wizard
Who-is-PS Feb 3, 2026
0db5c96
Merge branch 'main' into dev-v3-philosr-wizard-a11y
Who-is-PS Feb 3, 2026
643c92b
update tests for wizard compoennt
Who-is-PS Feb 3, 2026
f29b177
include new tests for codecov
Who-is-PS Feb 3, 2026
fab1ac2
include new tests for codecov
Who-is-PS Feb 3, 2026
77cf02a
update tests
Who-is-PS Feb 3, 2026
5f49f11
update functionality and style
Who-is-PS Feb 3, 2026
bc55c28
update states for wizard
Who-is-PS Feb 3, 2026
6869560
refactor step navigation to share code between desktop and expandable…
Who-is-PS Feb 4, 2026
627d90e
update test for desktop for wizard
Who-is-PS Feb 4, 2026
e283dca
update tests
Who-is-PS Feb 4, 2026
35142de
remove tests
Who-is-PS Feb 4, 2026
3f12302
refacting wizard header
Who-is-PS Feb 5, 2026
7d0a982
aria-describedby fix
Who-is-PS Feb 6, 2026
e90a450
update styling
Who-is-PS Feb 6, 2026
9f78bfc
create app layout page
Who-is-PS Feb 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
280 changes: 280 additions & 0 deletions pages/wizard/with-app-layout.page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,280 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React, { useState } from 'react';

import AppLayout from '~components/app-layout';
import Box from '~components/box';
import BreadcrumbGroup from '~components/breadcrumb-group';
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 Select, { SelectProps } from '~components/select';
import SpaceBetween from '~components/space-between';
import Tiles from '~components/tiles';
import Wizard, { WizardProps } from '~components/wizard';

import ScreenshotArea from '../utils/screenshot-area';
import { i18nStrings } from './common';

const appLayoutLabels = {
navigation: 'Side navigation',
navigationToggle: 'Open navigation',
navigationClose: 'Close navigation',
notifications: 'Notifications',
tools: 'Tools',
toolsToggle: 'Open tools',
toolsClose: 'Close tools',
};

// Step 1: Choose instance type
function Step1Content() {
const [instanceType, setInstanceType] = useState('t3.micro');

return (
<SpaceBetween size="l">
<Container header={<Header variant="h2">Instance type</Header>}>
<Tiles
value={instanceType}
onChange={({ detail }) => setInstanceType(detail.value)}
columns={3}
items={[
{
value: 't3.micro',
label: 't3.micro',
description: '1 vCPU, 1 GiB memory',
},
{
value: 't3.small',
label: 't3.small',
description: '2 vCPU, 2 GiB memory',
},
{
value: 't3.medium',
label: 't3.medium',
description: '2 vCPU, 4 GiB memory',
},
{
value: 't3.large',
label: 't3.large',
description: '2 vCPU, 8 GiB memory',
},
{
value: 'm5.large',
label: 'm5.large',
description: '2 vCPU, 8 GiB memory',
},
{
value: 'm5.xlarge',
label: 'm5.xlarge',
description: '4 vCPU, 16 GiB memory',
},
]}
/>
</Container>
</SpaceBetween>
);
}

// Step 2: Configure storage
function Step2Content() {
const [volumeSize, setVolumeSize] = useState('30');
const [volumeType, setVolumeType] = useState<SelectProps.Option | null>({
value: 'gp3',
label: 'General Purpose SSD (gp3)',
});

return (
<SpaceBetween size="l">
<Container header={<Header variant="h2">Storage configuration</Header>}>
<SpaceBetween size="l">
<FormField label="Volume size (GiB)" description="Size of the root volume">
<Input value={volumeSize} onChange={({ detail }) => setVolumeSize(detail.value)} type="number" />
</FormField>
<FormField label="Volume type" description="The type of EBS volume">
<Select
selectedOption={volumeType}
onChange={({ detail }) => setVolumeType(detail.selectedOption)}
options={[
{ value: 'gp3', label: 'General Purpose SSD (gp3)' },
{ value: 'gp2', label: 'General Purpose SSD (gp2)' },
{ value: 'io1', label: 'Provisioned IOPS SSD (io1)' },
{ value: 'st1', label: 'Throughput Optimized HDD (st1)' },
]}
/>
</FormField>
</SpaceBetween>
</Container>
</SpaceBetween>
);
}

// Step 3: Configure security
function Step3Content() {
const [securityOption, setSecurityOption] = useState('new');
const [keyPair, setKeyPair] = useState<SelectProps.Option | null>(null);

return (
<SpaceBetween size="l">
<Container header={<Header variant="h2">Security group</Header>}>
<RadioGroup
value={securityOption}
onChange={({ detail }) => setSecurityOption(detail.value)}
items={[
{
value: 'new',
label: 'Create a new security group',
description: 'A new security group will be created with default rules',
},
{
value: 'existing',
label: 'Select existing security groups',
description: 'Choose from your existing security groups',
},
]}
/>
</Container>
<Container header={<Header variant="h2">Key pair (login)</Header>}>
<FormField label="Key pair name" description="A key pair is used to securely connect to your instance">
<Select
selectedOption={keyPair}
onChange={({ detail }) => setKeyPair(detail.selectedOption)}
placeholder="Select a key pair"
options={[
{ value: 'my-key-pair', label: 'my-key-pair' },
{ value: 'dev-key', label: 'dev-key' },
{ value: 'production-key', label: 'production-key' },
]}
/>
</FormField>
</Container>
</SpaceBetween>
);
}

// Step 4: Add tags (optional)
function Step4Content() {
const [tagKey, setTagKey] = useState('Name');
const [tagValue, setTagValue] = useState('');

return (
<SpaceBetween size="l">
<Container header={<Header variant="h2">Tags</Header>}>
<SpaceBetween size="l">
<FormField label="Key" description="Tag key">
<Input value={tagKey} onChange={({ detail }) => setTagKey(detail.value)} />
</FormField>
<FormField label="Value" description="Tag value">
<Input
value={tagValue}
onChange={({ detail }) => setTagValue(detail.value)}
placeholder="Enter tag value"
/>
</FormField>
</SpaceBetween>
</Container>
</SpaceBetween>
);
}

// Step 5: Review
function Step5Content() {
return (
<SpaceBetween size="l">
<Container header={<Header variant="h2">Review instance configuration</Header>}>
<ColumnLayout columns={2} variant="text-grid">
<SpaceBetween size="l">
<div>
<Box variant="awsui-key-label">Instance type</Box>
<div>t3.micro</div>
</div>
<div>
<Box variant="awsui-key-label">Storage</Box>
<div>30 GiB gp3</div>
</div>
</SpaceBetween>
<SpaceBetween size="l">
<div>
<Box variant="awsui-key-label">Security group</Box>
<div>Create new</div>
</div>
<div>
<Box variant="awsui-key-label">Key pair</Box>
<div>my-key-pair</div>
</div>
</SpaceBetween>
</ColumnLayout>
</Container>
</SpaceBetween>
);
}

const steps: WizardProps.Step[] = [
{
title: 'Choose instance type',
info: <Link variant="info">Info</Link>,
description: 'Select the hardware configuration for your instance.',
content: <Step1Content />,
},
{
title: 'Configure storage',
info: <Link variant="info">Info</Link>,
description: 'Configure the storage options for your instance.',
content: <Step2Content />,
},
{
title: 'Configure security',
info: <Link variant="info">Info</Link>,
description: 'Set up security groups and key pairs.',
content: <Step3Content />,
},
{
title: 'Add tags',
info: <Link variant="info">Info</Link>,
description: 'Add tags to help organize and identify your resources.',
isOptional: true,
content: <Step4Content />,
},
{
title: 'Review and launch',
description: 'Review your configuration before launching the instance.',
content: <Step5Content />,
},
];

export default function WizardWithAppLayoutPage() {
const [activeStepIndex, setActiveStepIndex] = useState(0);

return (
<ScreenshotArea gutters={false}>
<AppLayout
contentType="wizard"
ariaLabels={appLayoutLabels}
navigationHide={true}
toolsHide={true}
breadcrumbs={
<BreadcrumbGroup
items={[
{ text: 'EC2', href: '#' },
{ text: 'Instances', href: '#' },
{ text: 'Launch instance', href: '#' },
]}
/>
}
content={
<Wizard
steps={steps}
i18nStrings={i18nStrings}
activeStepIndex={activeStepIndex}
onNavigate={({ detail }) => setActiveStepIndex(detail.requestedStepIndex)}
onCancel={() => console.log('Cancelled')}
onSubmit={() => console.log('Submitted')}
/>
}
/>
</ScreenshotArea>
);
}
23 changes: 23 additions & 0 deletions src/wizard/__integ__/wizard.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,29 @@ describe('Wizard keyboard navigation', () => {
});
});

describe('Wizard narrow viewport navigation', () => {
test(
'shows expandable step navigation at narrow viewport',
useBrowser(async browser => {
const page = new WizardPageObject(browser);
// Set narrow viewport first using page object with object syntax
await page.setWindowSize({ width: 320, height: 600 });
await browser.url('/#/light/wizard/simple?visualRefresh=true');
await page.waitForVisible(wizardWrapper.findPrimaryButton().toSelector());

// Collapsed steps container should exist at narrow viewport
// Note: Using attribute selector because CSS class names are hashed in the build output
// Using isExisting() because ExpandableSection header has screenreader-only styling when collapsed
const collapsedStepsSelector = `${wizardWrapper.toSelector()} [class*="collapsed-steps"]`;
await expect(page.isExisting(collapsedStepsSelector)).resolves.toBe(true);

// Sidebar navigation should be hidden at narrow viewport
const navigationSelector = `${wizardWrapper.toSelector()} [class*="navigation"]`;
await expect(page.isDisplayed(navigationSelector)).resolves.toBe(false);
})
);
});

describe('Wizard scroll to top upon navigation', () => {
test(
'in window',
Expand Down
23 changes: 22 additions & 1 deletion src/wizard/__tests__/wizard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import TestI18nProvider from '../../../lib/components/i18n/testing';
import createWrapper from '../../../lib/components/test-utils/dom';
import WizardWrapper from '../../../lib/components/test-utils/dom/wizard';
import Wizard, { WizardProps } from '../../../lib/components/wizard';
import { handleStepNavigation, StepStatusValues } from '../../../lib/components/wizard/wizard-step-list';
import { DEFAULT_I18N_SETS, DEFAULT_STEPS } from './common';

import liveRegionStyles from '../../../lib/components/live-region/test-classes/styles.css.js';
Expand Down Expand Up @@ -66,7 +67,9 @@ describe('i18nStrings', () => {
i18nStrings.navigationAriaLabel
);

wrapper.findAllByClassName(styles['navigation-link-label']).forEach((label, index) => {
// Only test labels from the desktop navigation (not the expandable collapsed-steps navigation)
const desktopNav = wrapper.findByClassName(styles.navigation);
desktopNav!.findAllByClassName(styles['navigation-link-label']).forEach((label, index) => {
const expectedTitle = i18nStrings.stepNumberLabel!(index + 1);
const expectedLabel = DEFAULT_STEPS[index].isOptional
? `${expectedTitle} - ${i18nStrings.optional}`
Expand Down Expand Up @@ -619,6 +622,24 @@ describe('Custom primary actions', () => {
});
});

describe('handleStepNavigation', () => {
test('calls onStepClick for visited steps', () => {
const onStepClick = jest.fn();
const onSkipToClick = jest.fn();
handleStepNavigation(2, StepStatusValues.Visited, onStepClick, onSkipToClick);
expect(onStepClick).toHaveBeenCalledWith(2);
expect(onSkipToClick).not.toHaveBeenCalled();
});

test('calls onSkipToClick for next steps', () => {
const onStepClick = jest.fn();
const onSkipToClick = jest.fn();
handleStepNavigation(3, StepStatusValues.Next, onStepClick, onSkipToClick);
expect(onSkipToClick).toHaveBeenCalledWith(3);
expect(onStepClick).not.toHaveBeenCalled();
});
});

describe('i18n', () => {
test('supports rendering static strings using i18n provider', () => {
const { container } = render(
Expand Down
Loading
Loading