Skip to content

Commit 494faf4

Browse files
committed
chore: Wizard with custom steps poc
1 parent ad43d71 commit 494faf4

File tree

12 files changed

+320
-17
lines changed

12 files changed

+320
-17
lines changed
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import React from 'react';
5+
import clsx from 'clsx';
6+
7+
import { Badge, Box, SpaceBetween } from '~components';
8+
9+
import styles from '../styles.scss';
10+
11+
export function StepHeader({
12+
visited,
13+
active,
14+
isNew,
15+
onClick,
16+
children,
17+
}: {
18+
children: React.ReactNode;
19+
visited: boolean;
20+
isNew: boolean;
21+
active: boolean;
22+
onClick: () => void;
23+
}) {
24+
return (
25+
<button
26+
onClick={onClick}
27+
tabIndex={active ? -1 : 0}
28+
aria-current="step"
29+
className={clsx(styles['steps-button'], {
30+
[styles['steps-button-visited']]: visited,
31+
[styles['steps-button-active']]: active,
32+
})}
33+
>
34+
<SpaceBetween size="xs" direction="horizontal" alignItems="center">
35+
<span>{children}</span>
36+
{isNew ? <Badge color="blue">New</Badge> : null}
37+
</SpaceBetween>
38+
</button>
39+
);
40+
}
41+
42+
export function StepDescription({ children }: { children: React.ReactNode }) {
43+
return (
44+
<Box fontSize="body-s" color="text-body-secondary" margin={{ bottom: 's' }}>
45+
{children}
46+
</Box>
47+
);
48+
}

pages/0_wizard_and_steps/styles.scss

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
@use '~design-tokens' as awsui;
7+
8+
.steps-button {
9+
border-block: none;
10+
border-inline: none;
11+
border-start-start-radius: 0;
12+
border-start-end-radius: 0;
13+
border-end-start-radius: 0;
14+
border-end-end-radius: 0;
15+
padding-block: 0;
16+
padding-inline: 0;
17+
background: none;
18+
cursor: pointer;
19+
color: awsui.$color-text-status-inactive;
20+
21+
&.steps-button-visited {
22+
text-decoration: underline;
23+
color: awsui.$color-text-link-default;
24+
}
25+
26+
&-active {
27+
font-weight: bold;
28+
color: awsui.$color-text-interactive-active;
29+
cursor: text;
30+
user-select: text;
31+
32+
&.steps-button-visited {
33+
text-decoration: none;
34+
color: awsui.$color-text-interactive-active;
35+
}
36+
}
37+
38+
&:focus {
39+
outline-color: awsui.$color-border-item-focused;
40+
}
41+
}
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import React, { useState } from 'react';
5+
6+
import {
7+
AppLayout,
8+
Box,
9+
Checkbox,
10+
Container,
11+
Drawer,
12+
FormField,
13+
Header,
14+
Select,
15+
SpaceBetween,
16+
StatusIndicatorProps,
17+
Steps,
18+
} from '~components';
19+
import Button from '~components/button';
20+
import I18nProvider from '~components/i18n';
21+
import messages from '~components/i18n/messages/all.en';
22+
import Wizard from '~components/wizard';
23+
24+
import { i18nStrings as wizardI18nStrings } from '../wizard/common';
25+
import { StepDescription, StepHeader } from './common/steps-components';
26+
27+
const stepsMetadata = [
28+
{ title: 'Step 1: Workflow overview', subtitle: 'Review step progress' },
29+
{ title: 'Step 2: Select EC2 instance', subtitle: 'Create or choose target EC2 instance' },
30+
{ title: 'Step 3: Select S3 bucket', subtitle: 'Pick S3 bucket access' },
31+
{ title: 'Step 4: Configure IAM role', subtitle: 'Set permissions and policies' },
32+
{ title: 'Step 5: Updated S3 bucket policy', subtitle: 'Configure bucket access rules' },
33+
{ title: 'Step 6: Updated KMS key policy', subtitle: 'Set encryption permissions based on the S3 bucket selection' },
34+
];
35+
36+
const statusOptions = [{ value: 'success' }, { value: 'in-progress' }, { value: 'pending' }, { value: 'error' }];
37+
38+
interface StateSettings {
39+
status: StatusIndicatorProps.Type;
40+
isNew: boolean;
41+
}
42+
43+
export default function WizardPage() {
44+
const [activeStepIndex, setActiveStepIndex] = useState(3);
45+
const [stepStates, setStepStates] = useState<StateSettings[]>([
46+
{ status: 'success', isNew: false },
47+
{ status: 'error', isNew: false },
48+
{ status: 'success', isNew: false },
49+
{ status: 'in-progress', isNew: false },
50+
{ status: 'pending', isNew: false },
51+
{ status: 'pending', isNew: true },
52+
]);
53+
const changeStepSettings = (index: number, settings: (prev: StateSettings) => StateSettings) => {
54+
setStepStates(prev => {
55+
const copy = [...prev];
56+
copy[index] = settings(copy[index]);
57+
return copy;
58+
});
59+
};
60+
const changeStepStatus = (index: number, status: StatusIndicatorProps.Type) =>
61+
changeStepSettings(index, prev => ({ ...prev, status }));
62+
const changeStepNew = (index: number, isNew: boolean) => changeStepSettings(index, prev => ({ ...prev, isNew }));
63+
const getStatusProps = (status: StatusIndicatorProps.Type) => {
64+
switch (status) {
65+
case 'success':
66+
return { status: 'success', statusIconAriaLabel: 'success' } as const;
67+
case 'in-progress':
68+
return { status: 'in-progress', statusIconAriaLabel: 'in progress', statusColorOverride: 'blue' } as const;
69+
case 'pending':
70+
return { status: 'pending', statusIconAriaLabel: 'pending' } as const;
71+
case 'error':
72+
default:
73+
return { status: 'error', statusIconAriaLabel: 'error' } as const;
74+
}
75+
};
76+
const steps = stepsMetadata.map((_, index) => {
77+
const { title, subtitle } = stepsMetadata[index];
78+
const { status, isNew } = stepStates[index];
79+
const statusProps = getStatusProps(status);
80+
return {
81+
title,
82+
...statusProps,
83+
header: (
84+
<StepHeader
85+
visited={status === 'success' || status === 'error'}
86+
active={activeStepIndex === index}
87+
onClick={() => setActiveStepIndex(index)}
88+
isNew={isNew}
89+
>
90+
{title}
91+
</StepHeader>
92+
),
93+
details: <StepDescription>{subtitle}</StepDescription>,
94+
content: (
95+
<SpaceBetween size="s">
96+
<Container>
97+
<div style={{ height: 400 }}>Content {index + 1}</div>
98+
</Container>
99+
</SpaceBetween>
100+
),
101+
};
102+
});
103+
104+
const [activeDrawerId, setActiveDrawerId] = useState<null | string>('settings');
105+
return (
106+
<I18nProvider messages={[messages]} locale="en">
107+
<AppLayout
108+
navigationHide={true}
109+
activeDrawerId={activeDrawerId}
110+
onDrawerChange={({ detail }) => setActiveDrawerId(detail.activeDrawerId)}
111+
drawers={[
112+
{
113+
id: 'settings',
114+
content: (
115+
<Drawer header={<Header variant="h2">Steps settings</Header>}>
116+
<SpaceBetween size="m">
117+
{stepsMetadata.map(({ title }, index) => (
118+
<FormField key={title} label={`Step ${index + 1} settings`}>
119+
<SpaceBetween size="xs">
120+
<Select
121+
options={statusOptions}
122+
selectedOption={statusOptions.find(o => o.value === stepStates[index].status)!}
123+
onChange={({ detail }) =>
124+
changeStepStatus(index, detail.selectedOption.value as StatusIndicatorProps.Type)
125+
}
126+
></Select>
127+
128+
<Checkbox
129+
checked={stepStates[index].isNew}
130+
onChange={({ detail }) => changeStepNew(index, detail.checked)}
131+
>
132+
New
133+
</Checkbox>
134+
</SpaceBetween>
135+
</FormField>
136+
))}
137+
</SpaceBetween>
138+
</Drawer>
139+
),
140+
trigger: { iconName: 'settings' },
141+
ariaLabels: {
142+
drawerName: 'Steps settings',
143+
triggerButton: 'Open steps settings',
144+
closeButton: 'Close steps settings',
145+
},
146+
},
147+
]}
148+
contentType="wizard"
149+
content={
150+
<Wizard
151+
id="wizard"
152+
steps={steps}
153+
i18nStrings={wizardI18nStrings}
154+
activeStepIndex={activeStepIndex}
155+
onNavigate={e => setActiveStepIndex(e.detail.requestedStepIndex)}
156+
secondaryActions={activeStepIndex === 2 ? <Button>Save as draft</Button> : null}
157+
customNavigationSide={
158+
<Box>
159+
<Steps steps={steps} />
160+
</Box>
161+
}
162+
customNavigationTop={<Box>Custom nav top!</Box>}
163+
/>
164+
}
165+
/>
166+
</I18nProvider>
167+
);
168+
}

pages/app/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ function isAppLayoutPage(pageId?: string) {
4242
'prompt-input/simple',
4343
'funnel-analytics/static-single-page-flow',
4444
'funnel-analytics/static-multi-page-flow',
45+
'0_wizard_and_steps',
4546
];
4647
return pageId !== undefined && appLayoutPages.some(match => pageId.includes(match));
4748
}

src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26259,6 +26259,16 @@ Use this if you need to wait for a response from the server before the user can
2625926259
},
2626026260
],
2626126261
"regions": [
26262+
{
26263+
"description": "Overrides wizard steps navigation with a custom one when it is rendered on the side.",
26264+
"isDefault": false,
26265+
"name": "customNavigationSide",
26266+
},
26267+
{
26268+
"description": "Overrides wizard steps navigation with a custom one when it is rendered on the top.",
26269+
"isDefault": false,
26270+
"name": "customNavigationTop",
26271+
},
2626226272
{
2626326273
"description": "Specifies left-aligned secondary actions for the wizard. Use a button dropdown if multiple actions are required.",
2626426274
"isDefault": false,

src/steps/index.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ const Steps = ({ steps, ...props }: StepsProps) => {
1616
const baseProps = getBaseProps(props);
1717
const baseComponentProps = useBaseComponent('Steps');
1818
const externalProps = getExternalProps(props);
19-
2019
return <InternalSteps {...baseProps} {...baseComponentProps} {...externalProps} steps={steps} />;
2120
};
2221

src/steps/interfaces.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,12 @@ export interface StepsProps extends BaseComponentProps {
3333

3434
export namespace StepsProps {
3535
export type Status = StatusIndicatorProps.Type;
36+
export type Color = StatusIndicatorProps.Color;
3637

3738
export interface Step {
3839
status: Status;
3940
statusIconAriaLabel?: string;
41+
statusColorOverride?: Color;
4042
header: React.ReactNode;
4143
details?: React.ReactNode;
4244
}

src/steps/internal.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@ import styles from './styles.css.js';
1212

1313
type InternalStepsProps = SomeRequired<StepsProps, 'steps'> & InternalBaseComponentProps<HTMLDivElement>;
1414

15-
const InternalStep = ({ status, statusIconAriaLabel, header, details }: StepsProps.Step) => {
15+
const InternalStep = ({ status, statusIconAriaLabel, statusColorOverride, header, details }: StepsProps.Step) => {
1616
return (
1717
<li className={styles.container}>
1818
<div className={styles.header}>
19-
<StatusIndicator type={status} iconAriaLabel={statusIconAriaLabel}>
19+
<StatusIndicator type={status} colorOverride={statusColorOverride} iconAriaLabel={statusIconAriaLabel}>
2020
{header}
2121
</StatusIndicator>
2222
</div>
@@ -47,6 +47,7 @@ const InternalSteps = ({
4747
key={index}
4848
status={step.status}
4949
statusIconAriaLabel={step.statusIconAriaLabel}
50+
statusColorOverride={step.statusColorOverride}
5051
header={step.header}
5152
details={step.details}
5253
/>

src/wizard/interfaces.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,16 @@ export interface WizardProps extends BaseComponentProps {
129129
* or `skip` (when navigated using navigation pane or the *skip-to* button to the previously unvisited step).
130130
*/
131131
onNavigate?: NonCancelableEventHandler<WizardProps.NavigateDetail>;
132+
133+
/**
134+
* Overrides wizard steps navigation with a custom one when it is rendered on the side.
135+
*/
136+
customNavigationSide?: React.ReactNode;
137+
138+
/**
139+
* Overrides wizard steps navigation with a custom one when it is rendered on the top.
140+
*/
141+
customNavigationTop?: React.ReactNode;
132142
}
133143

134144
export namespace WizardProps {

src/wizard/internal.tsx

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ export default function InternalWizard({
4545
onCancel,
4646
onSubmit,
4747
onNavigate,
48+
customNavigationSide,
49+
customNavigationTop,
4850
__internalRootRef,
4951
__injectAnalyticsComponentMetadata = false,
5052
...rest
@@ -179,23 +181,30 @@ export default function InternalWizard({
179181
<div
180182
className={clsx(styles.wizard, isVisualRefresh && styles.refresh, smallContainer && styles['small-container'])}
181183
>
182-
<WizardNavigation
183-
activeStepIndex={actualActiveStepIndex}
184-
farthestStepIndex={farthestStepIndex.current}
185-
allowSkipTo={allowSkipTo}
186-
hidden={smallContainer}
187-
i18nStrings={i18nStrings}
188-
isLoadingNextStep={isLoadingNextStep}
189-
onStepClick={onStepClick}
190-
onSkipToClick={onSkipToClick}
191-
steps={steps}
192-
/>
184+
{customNavigationSide ? (
185+
smallContainer ? null : (
186+
<div className={styles['navigation-custom']}>{customNavigationSide}</div>
187+
)
188+
) : (
189+
<WizardNavigation
190+
activeStepIndex={actualActiveStepIndex}
191+
farthestStepIndex={farthestStepIndex.current}
192+
allowSkipTo={allowSkipTo}
193+
hidden={smallContainer}
194+
i18nStrings={i18nStrings}
195+
isLoadingNextStep={isLoadingNextStep}
196+
onStepClick={onStepClick}
197+
onSkipToClick={onSkipToClick}
198+
steps={steps}
199+
/>
200+
)}
193201
<div
194202
className={clsx(styles.form, isVisualRefresh && styles.refresh, smallContainer && styles['small-container'])}
195203
>
196204
<WizardForm
197205
steps={steps}
198206
showCollapsedSteps={smallContainer}
207+
customCollapsedSteps={customNavigationTop}
199208
i18nStrings={i18nStrings}
200209
submitButtonText={submitButtonText}
201210
activeStepIndex={actualActiveStepIndex}

0 commit comments

Comments
 (0)