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
126 changes: 126 additions & 0 deletions pages/steps/playground.page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// 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 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 Input from '~components/input';
import Select, { SelectProps } from '~components/select';
import SpaceBetween from '~components/space-between';
import Steps, { StepsProps } from '~components/steps';

import ScreenshotArea from '../utils/screenshot-area';

const orientationOptions: ReadonlyArray<SelectProps.Option> = [{ value: 'vertical' }, { value: 'horizontal' }];

// The three step shapes worth exercising: a status step with details, a status step without
// details, and a neutral (no status) event.
const baseEntries: ReadonlyArray<{ status?: StepsProps.Status; header: string }> = [
{ status: 'success', header: 'Created environment' },
{ status: 'error', header: 'Validation failed' },
{ status: undefined, header: 'Plain event (no status)' },
];

export default function StepsPlayground() {
const [orientation, setOrientation] = useState<SelectProps.Option>(orientationOptions[0]);
const [ariaLabel, setAriaLabel] = useState('Steps playground');

const [withTimestamps, setWithTimestamps] = useState(true);
const [timeAsElement, setTimeAsElement] = useState(true);
const [withDetails, setWithDetails] = useState(true);
const [useCustomRender, setUseCustomRender] = useState(false);

const times = ['09:00 AM', '10:30 AM', '2 hr ago'];

const getHeaderStart = (timeText: string): React.ReactNode => {
if (!withTimestamps) {
return undefined;
}
if (timeAsElement) {
return (
<time dateTime="2024-05-01T15:01:23Z" title="May 1, 2024, 3:01:23 PM (UTC)">
{timeText}
</time>
);
}
return timeText;
};

const steps: ReadonlyArray<StepsProps.Step> = baseEntries.map((entry, index) => ({
status: entry.status,
statusIconAriaLabel: entry.status ? entry.status : undefined,
header: entry.header,
details: withDetails ? (
<Box fontSize="body-s" color="text-body-secondary">
Additional information for “{entry.header}”.
</Box>
) : undefined,
headerStart: getHeaderStart(times[index % times.length]),
}));

const renderStep: StepsProps['renderStep'] = useCustomRender
? step => ({
header: <Box fontWeight="bold">Custom: {step.header}</Box>,
details: step.details ? <Box fontSize="body-s">{step.details}</Box> : undefined,
})
: undefined;

return (
<ScreenshotArea disableAnimations={true}>
<Box padding="l">
<SpaceBetween size="l">
<Header variant="h1">Steps playground</Header>

<Container header={<Header variant="h2">Properties</Header>}>
<SpaceBetween size="m">
<ColumnLayout columns={2} borders="vertical">
<FormField label="Orientation">
<Select
selectedOption={orientation}
options={orientationOptions}
onChange={({ detail }) => setOrientation(detail.selectedOption)}
/>
</FormField>
<FormField label="ariaLabel">
<Input value={ariaLabel} onChange={({ detail }) => setAriaLabel(detail.value)} />
</FormField>
</ColumnLayout>

<ColumnLayout columns={2}>
<Checkbox checked={withTimestamps} onChange={({ detail }) => setWithTimestamps(detail.checked)}>
Show <code>headerStart</code> (timestamps)
</Checkbox>
<Checkbox
checked={timeAsElement}
disabled={!withTimestamps}
onChange={({ detail }) => setTimeAsElement(detail.checked)}
>
Timestamp as <code>&lt;time&gt;</code> element
</Checkbox>
<Checkbox checked={withDetails} onChange={({ detail }) => setWithDetails(detail.checked)}>
Show <code>details</code>
</Checkbox>
<Checkbox checked={useCustomRender} onChange={({ detail }) => setUseCustomRender(detail.checked)}>
Use <code>renderStep</code>
</Checkbox>
</ColumnLayout>
</SpaceBetween>
</Container>

<Container header={<Header variant="h2">Preview</Header>}>
<Steps
ariaLabel={ariaLabel || undefined}
orientation={orientation.value as StepsProps.Orientation}
steps={steps}
renderStep={renderStep}
/>
</Container>
</SpaceBetween>
</Box>
</ScreenshotArea>
);
}
120 changes: 120 additions & 0 deletions pages/steps/timeline.page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React from 'react';

import Link from '~components/link';
import SpaceBetween from '~components/space-between';
import Steps, { StepsProps } from '~components/steps';

import ScreenshotArea from '../utils/screenshot-area';

const timelineWithLinks: ReadonlyArray<StepsProps.Step> = [
{
headerStart: <time dateTime="2024-05-01T15:01:23Z">3:01:23 PM</time>,
header: <Link href="#m1">Provided preferences</Link>,
},
{
headerStart: <time dateTime="2024-05-01T15:03:10Z">3:03:10 PM</time>,
header: <Link href="#m2">Created environment: CloudAppConfig</Link>,
},
{
headerStart: <time dateTime="2024-05-01T15:04:45Z">3:04:45 PM</time>,
header: 'Waiting for approval',
},
];

const timelineWithStatuses: ReadonlyArray<StepsProps.Step> = [
{
headerStart: <time dateTime="2024-05-01T15:01:23Z">3:01:23 PM</time>,
status: 'success',
statusIconAriaLabel: 'Success',
header: 'Created environment',
},
{
headerStart: <time dateTime="2024-05-01T15:03:10Z">3:03:10 PM</time>,
status: 'loading',
statusIconAriaLabel: 'Loading',
header: 'Checking EKS clusters',
},
{
headerStart: <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.',
},
];

const mixedTimeline: ReadonlyArray<StepsProps.Step> = [
{
headerStart: <time dateTime="2024-05-01T15:01:23Z">3:01:23 PM</time>,
status: 'success',
statusIconAriaLabel: 'Success',
header: 'Created environment',
},
{
// No headerStart: the leading column is still reserved so the dots stay aligned.
header: <Link href="#m2">Provided preferences</Link>,
},
{
headerStart: <time dateTime="2024-05-01T15:06:02Z">3:06:02 PM</time>,
header: 'Waiting for approval',
},
];

// Values of very different lengths, to verify the shared leading column keeps the dots/rail
// aligned. The column is sized via `max-content` to the widest value, so the long value widens the
// column for every row (it does not wrap or truncate). The long value is placed in the middle to
// confirm the column width is shared across all rows, not per-row.
const varyingWidthTimeline: ReadonlyArray<StepsProps.Step> = [
{
headerStart: <time dateTime="2024-05-01T09:00:00Z">9:00 AM</time>,
status: 'success',
statusIconAriaLabel: 'Success',
header: 'Short timestamp',
},
{
headerStart: (
<time dateTime="2024-12-31T23:59:59+14:00" title="December 31, 2024, 11:59:59 PM (UTC+14:00)">
December 31, 2024, 11:59:59 PM (UTC+14:00)
</time>
),
status: 'error',
statusIconAriaLabel: 'Error',
header: 'Long absolute timestamp that widens the shared leading column',
details: 'The leading column grows to fit this value on one line, and every row shares that width.',
},
{
headerStart: '2 hr ago',
header: 'Relative, short',
},
];

export default function StepsTimelinePage() {
return (
<ScreenshotArea disableAnimations={true}>
<h1>Steps — timeline</h1>
<SpaceBetween size="xxl">
<div>
<h2>Timeline (timestamps + neutral dots, anchor-link labels)</h2>
<Steps ariaLabel="Conversation overview" steps={timelineWithLinks} />
</div>

<div>
<h2>Timeline combined with status icons</h2>
<Steps ariaLabel="Deployment timeline" steps={timelineWithStatuses} />
</div>

<div>
<h2>Mixed steps (some without a timestamp)</h2>
<Steps ariaLabel="Mixed timeline" steps={mixedTimeline} />
</div>

<div>
<h2>Varying widths (short vs. long timestamps)</h2>
<Steps ariaLabel="Varying width timeline" steps={varyingWidthTimeline} />
</div>
</SpaceBetween>
</ScreenshotArea>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -27528,10 +27528,11 @@ The function is called for each step and should return an object with the follow
"description": "An array of individual steps

Each step definition has the following properties:
* \`status\` (string) - Status of the step corresponding to a status indicator.
* \`statusIconAriaLabel\` - (string) - (Optional) Alternative text for the status icon.
* \`status\` (string) - (Optional) Status of the step corresponding to a status indicator. If not set, the step renders a neutral dot instead of a status icon.
* \`statusIconAriaLabel\` - (string) - (Optional) Alternative text for the status icon or neutral dot.
* \`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.
* \`headerStart\` (ReactNode) - (Optional) Content rendered in a leading column at the start of the step, before the icon. Typically a timestamp in a timeline view. Applies to the default \`vertical\` orientation and is ignored when \`orientation="horizontal"\`.",
"name": "steps",
"optional": false,
"type": "ReadonlyArray<StepsProps.Step>",
Expand Down Expand Up @@ -44344,6 +44345,21 @@ Returns the current value of the input.",
],
},
},
{
"description": "Finds the leading content (e.g. a timestamp) rendered at the start of a step.
Returns null when the step has no \`headerStart\`.",
"name": "findHeaderStart",
"parameters": [],
"returnType": {
"isNullable": true,
"name": "ElementWrapper",
"typeArguments": [
{
"name": "HTMLElement",
},
],
},
},
],
"name": "StepWrapper",
},
Expand Down Expand Up @@ -53511,6 +53527,16 @@ Note: when used with collection-hooks the \`trackBy\` is set automatically from
"name": "ElementWrapper",
},
},
{
"description": "Finds the leading content (e.g. a timestamp) rendered at the start of a step.
Returns null when the step has no \`headerStart\`.",
"name": "findHeaderStart",
"parameters": [],
"returnType": {
"isNullable": false,
"name": "ElementWrapper",
},
},
],
"name": "StepWrapper",
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -641,6 +641,7 @@ exports[`test-utils selectors 1`] = `
"steps": [
"awsui_container_gxp9y",
"awsui_details_gxp9y",
"awsui_header-start_gxp9y",
"awsui_header_gxp9y",
"awsui_root_gxp9y",
],
Expand Down
40 changes: 23 additions & 17 deletions src/status-indicator/internal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,26 +19,31 @@ import { StatusIndicatorProps } from './interfaces';

import styles from './styles.css.js';

export interface InternalStatusIndicatorProps
extends SomeRequired<StatusIndicatorProps, 'type'>,
InternalBaseComponentProps {
/**
* Play an animation on the error icon when first rendered
*/
__animate?: boolean;
export type InternalStatusIndicatorProps = Omit<SomeRequired<StatusIndicatorProps, 'type'>, 'type'> &
InternalBaseComponentProps & {
/**
* Status type. The internal `'neutral'` value renders a plain dot (used by Steps for events that
* have no status); it is intentionally not exposed on the public `StatusIndicatorProps.Type`.
*/
type: StatusIndicatorProps.Type | 'neutral';

/**
* Size of icon.
*/
__size?: IconProps.Size;
/**
* Play an animation on the error icon when first rendered
*/
__animate?: boolean;

/**
* The CSS behavior of the status indicator container element.
*/
__display?: 'inline' | 'inline-block';
}
/**
* Size of icon.
*/
__size?: IconProps.Size;

/**
* The CSS behavior of the status indicator container element.
*/
__display?: 'inline' | 'inline-block';
};

const typeToIcon: (size: IconProps.Size) => Record<StatusIndicatorProps.Type, JSX.Element> = size => ({
const typeToIcon: (size: IconProps.Size) => Record<InternalStatusIndicatorProps['type'], JSX.Element> = size => ({
error: <InternalIcon name="status-negative" size={size} />,
warning: <InternalIcon name="status-warning" size={size} />,
success: <InternalIcon name="status-positive" size={size} />,
Expand All @@ -48,6 +53,7 @@ const typeToIcon: (size: IconProps.Size) => Record<StatusIndicatorProps.Type, JS
'in-progress': <InternalIcon name="status-in-progress" size={size} />,
loading: <InternalSpinner />,
'not-started': <InternalIcon name="status-not-started" size={size} />,
neutral: <InternalIcon name="dot" size={size} />,
});

interface InternalStatusIconProps extends Pick<InternalStatusIndicatorProps, 'type' | 'iconAriaLabel'> {
Expand Down
2 changes: 2 additions & 0 deletions src/status-indicator/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ $_status-colors: (
'in-progress': awsui.$color-text-status-inactive,
'loading': awsui.$color-text-status-inactive,
'not-started': awsui.$color-text-status-inactive,
'neutral': awsui.$color-text-status-inactive,
);

$_color-overrides: (
Expand All @@ -40,6 +41,7 @@ $_status-backgrounds: (
'in-progress': awsui.$color-background-status-indicator-neutral,
'loading': awsui.$color-background-status-indicator-neutral,
'not-started': awsui.$color-background-status-indicator-neutral,
'neutral': awsui.$color-background-status-indicator-neutral,
);

$_background-overrides: (
Expand Down
Loading
Loading