Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
95 changes: 95 additions & 0 deletions pages/steps/permutations-annotation.page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// 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<StepsProps.Step> = [
{
annotation: <time dateTime="2024-05-01T15:01:23Z">3:01:23 PM</time>,
status: 'log',
header: 'Provided preferences',
},
{
annotation: <time dateTime="2024-05-01T15:03:10Z">3:03:10 PM</time>,
status: 'success',
statusIconAriaLabel: 'Success',
header: 'Created environment',
details: 'Environment created successfully.',
},
{
annotation: <time dateTime="2024-05-01T15:04:45Z">3:04:45 PM</time>,
status: 'error',
statusIconAriaLabel: 'Error',
header: 'Validation failed',
details: 'One or more resources could not be validated.',
},
];

export const varyingLengthAnnotationsSteps: ReadonlyArray<StepsProps.Step> = [
{
annotation: <time dateTime="2024-05-01T09:00:00Z">May 05, 2024, 9:00 AM</time>,
status: 'log',
header: 'Shorter timestamp',
details: 'This is used to test the shorter timestamp and and we have detail here',
},
{
annotation: (
<time dateTime="2024-12-31T23:59:59+14:00" title="December 31, 2024, 11:59:59 PM">
December 31, 2024, 11:59 PM
</time>
),
status: 'log',
details: 'One or more resources could not be validated. A detail in the middle',
header: 'Long timestamp',
},
{
status: 'loading',
header: 'No annotation yet',
details: 'This is used to test without annotation.',
},
];

const stepsPermutations = createPermutations<StepsProps>([
{
steps: [varyingLengthAnnotationsSteps, stepsWithAnnotation],
ariaLabel: ['test label'],
orientation: ['vertical', 'horizontal'],
renderStep: [
undefined,
step => ({
annotation: step.annotation,
header: step.header,
details: step.details && <i>Custom details for {step.details}</i>,
icon: <Icon ariaLabel="log" name="dot" variant="normal" />,
}),
step => ({
annotation: step.annotation,
header: <b>This step header ({step.header}) is wrapped in a custom HTML tag and has very long content</b>,
details: step.details && <i>Custom details for {step.details}</i>,
}),
],
},
{
connectorLines: ['none'],
orientation: ['vertical', 'horizontal'],
steps: [varyingLengthAnnotationsSteps],
ariaLabel: ['test label'],
},
]);

export default function StepsPermutations() {
return (
<SimplePage screenshotArea={{ disableAnimations: true }} title="Steps permutations: custom steps">
<PermutationsView
permutations={stepsPermutations}
render={permutation => <div>{<Steps {...permutation} />}</div>}
/>
</SimplePage>
);
}
166 changes: 166 additions & 0 deletions pages/steps/playground.page.tsx
Original file line number Diff line number Diff line change
@@ -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<StepsProps.Status> = [
'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<StepsProps.Step>; 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<string, unknown>;
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 : <span>{annotation}</span>,
header: <span>{header}</span>,
details: details === undefined ? undefined : <span>{details}</span>,
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<StepsProps.Orientation>('vertical');
const [showConnector, setShowConnector] = useState(true);
const [isRtl, setIsRtl] = useState(false);
const { steps, errorText } = parseSteps(stepsJson);

return (
<SimplePage
title="Steps playground"
subtitle="Edit the JSON list of steps and inspect the result."
settings={
<SpaceBetween size="l">
<FormField
label="Steps JSON"
description="Edit the annotation, header, status, statusIconAriaLabel, and details for each step."
errorText={errorText}
>
<Textarea
value={stepsJson}
rows={18}
invalid={!!errorText}
onChange={({ detail }) => setStepsJson(detail.value)}
/>
</FormField>

<ColumnLayout columns={2} variant="text-grid">
<FormField label="Orientation">
<Select
selectedOption={orientationOptions.find(option => option.value === orientation) ?? null}
options={orientationOptions}
onChange={({ detail }) => setOrientation(detail.selectedOption.value as StepsProps.Orientation)}
/>
</FormField>
</ColumnLayout>

<SpaceBetween direction="horizontal" size="l">
<Checkbox checked={showConnector} onChange={({ detail }) => setShowConnector(detail.checked)}>
Show connector
</Checkbox>
<Checkbox checked={isRtl} onChange={({ detail }) => setIsRtl(detail.checked)}>
RTL
</Checkbox>
</SpaceBetween>
</SpaceBetween>
}
>
<Container header={<Header variant="h2">Preview</Header>}>
<div dir={isRtl ? 'rtl' : 'ltr'}>
<Steps
ariaLabel="Editable steps"
connectorLines={showConnector ? 'visible' : 'none'}
orientation={orientation}
steps={steps}
/>
</div>
</Container>
</SimplePage>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<StepsProps.Step>",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
48 changes: 45 additions & 3 deletions src/steps/__tests__/steps.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
);
Expand All @@ -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);
}
);

Expand Down Expand Up @@ -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: <span>Custom: {step.header}</span> }),
});

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: <span>Custom: {step.header}</span> }),
});

expect(wrapper.findItems()[0].findAnnotation()!.getElement()).toHaveTextContent('10:30');
});
});
});
Loading
Loading