-
Notifications
You must be signed in to change notification settings - Fork 237
feat: add annotation to steps #4687
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
f7ebc4f
2afe7ee
a620509
cb87db3
d60e3ba
dee5408
aa03d9d
42a8635
6c4cc41
51840a8
384ae25
d949482
435b1dd
5611a6b
3fdd3f6
c3e0987
4516939
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 with annotation"> | ||
| <PermutationsView | ||
| permutations={stepsPermutations} | ||
| render={permutation => <div>{<Steps {...permutation} />}</div>} | ||
| /> | ||
| </SimplePage> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -28,6 +28,20 @@ const statusToColor: Record<StepsProps.Status, BoxProps.Color> = { | |
| log: 'text-status-inactive', | ||
| }; | ||
|
|
||
| const StepAnnotation = ({ children }: { children: StepsProps.Step['annotation'] }) => { | ||
| if (children === undefined || children === null) { | ||
| return null; | ||
| } | ||
| return <div className={styles.annotation}>{children}</div>; | ||
| }; | ||
|
|
||
| const StepDetails = ({ children }: { children: StepsProps.Step['details'] }) => { | ||
| if (children === undefined || children === null) { | ||
| return null; | ||
| } | ||
| return <div className={styles.details}>{children}</div>; | ||
| }; | ||
|
|
||
| const CustomStep = ({ | ||
| step, | ||
| orientation, | ||
|
|
@@ -39,37 +53,42 @@ const CustomStep = ({ | |
| renderStep: Required<StepsProps>['renderStep']; | ||
| hideConnectors: boolean; | ||
| }) => { | ||
| const { status, statusIconAriaLabel } = step; | ||
| const { status, statusIconAriaLabel, annotation } = step; | ||
| const { header, details, icon } = renderStep(step); | ||
| const iconNode = icon ? icon : <InternalStatusIcon type={status} iconAriaLabel={statusIconAriaLabel} />; | ||
| const connectorClassName = clsx(styles.connector, hideConnectors && styles['connector-hidden']); | ||
|
|
||
| if (orientation === 'horizontal') { | ||
| return ( | ||
| <li className={styles.container}> | ||
| <div className={styles.header}> | ||
| <StepAnnotation>{annotation}</StepAnnotation> | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we include the annotation in
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In the API proposal, we agreed not to include annotation in renderStep because builders can already customize it as a ReactNode. If a use case emerges, we can always add it later, but I’d prefer not to block this PR on it. |
||
| <div className={styles['step-layout']}> | ||
| {iconNode} | ||
| <hr className={connectorClassName} role="none" /> | ||
| </div> | ||
| <div className={styles['horizontal-header']}>{header}</div> | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A separate |
||
| {details && <div className={styles.details}>{details}</div>} | ||
| <div className={styles.content}> | ||
| <div className={styles.header}>{header}</div> | ||
| <StepDetails>{details}</StepDetails> | ||
| </div> | ||
| </li> | ||
| ); | ||
| } | ||
|
|
||
| // 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 ( | ||
| <li className={clsx(styles.container, styles['custom-vertical'])}> | ||
| <StepAnnotation>{annotation}</StepAnnotation> | ||
| <div className={styles.rail}> | ||
| {iconNode} | ||
| <hr className={connectorClassName} role="none" /> | ||
| </div> | ||
| <div className={styles.content}> | ||
| <div className={styles.header}>{header}</div> | ||
| {details && <div className={styles.details}>{details}</div>} | ||
| <StepDetails>{details}</StepDetails> | ||
| </div> | ||
| </li> | ||
| ); | ||
|
|
@@ -80,17 +99,25 @@ 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 ( | ||
| <li className={styles.container}> | ||
| <div className={styles.header}> | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This wrapper was named |
||
| <StepAnnotation>{annotation}</StepAnnotation> | ||
| <div className={styles['step-layout']}> | ||
| {orientation === 'vertical' ? ( | ||
| <InternalStatusIndicator type={status} iconAriaLabel={statusIconAriaLabel}> | ||
| {header} | ||
| </InternalStatusIndicator> | ||
| <> | ||
| <div className={styles.header}> | ||
| <InternalStatusIndicator type={status} iconAriaLabel={statusIconAriaLabel}> | ||
| {header} | ||
| </InternalStatusIndicator> | ||
| </div> | ||
| <StepDetails>{details}</StepDetails> | ||
| <hr className={clsx(connectorClassName, styles['connector-continuation'])} role="none" /> | ||
| </> | ||
| ) : ( | ||
| <> | ||
| <InternalBox color={statusToColor[status]}> | ||
|
|
@@ -100,14 +127,14 @@ const InternalStep = ({ | |
| </> | ||
| )} | ||
| </div> | ||
| {orientation === 'vertical' ? ( | ||
| <hr className={connectorClassName} role="none" /> | ||
| ) : ( | ||
| <div className={styles['horizontal-header']}> | ||
| <InternalBox color={statusToColor[status]}>{header}</InternalBox> | ||
| {orientation === 'horizontal' && ( | ||
| <div className={styles.content}> | ||
| <div className={styles.header}> | ||
| <InternalBox color={statusToColor[status]}>{header}</InternalBox> | ||
| </div> | ||
| <StepDetails>{details}</StepDetails> | ||
| </div> | ||
| )} | ||
| {details && <div className={styles.details}>{details}</div>} | ||
| </li> | ||
| ); | ||
| }; | ||
|
|
@@ -124,14 +151,15 @@ const InternalSteps = ({ | |
| ...props | ||
| }: InternalStepsProps) => { | ||
| const hideConnectors = connectorLines === 'none'; | ||
| const hasAnnotations = steps.some(step => step.annotation !== undefined && step.annotation !== null); | ||
| return ( | ||
| <div | ||
| {...props} | ||
| className={clsx(styles.root, props.className, orientation === 'horizontal' ? styles.horizontal : styles.vertical)} | ||
| ref={__internalRootRef} | ||
| > | ||
| <ol | ||
| className={styles.list} | ||
| className={clsx(styles.list, orientation === 'vertical' && hasAnnotations && styles['with-annotation'])} | ||
| aria-label={ariaLabel} | ||
| aria-labelledby={ariaLabelledby} | ||
| aria-describedby={ariaDescribedby} | ||
|
|
@@ -152,6 +180,7 @@ const InternalSteps = ({ | |
| statusIconAriaLabel={step.statusIconAriaLabel} | ||
| header={step.header} | ||
| details={step.details} | ||
| annotation={step.annotation} | ||
| orientation={orientation} | ||
| hideConnectors={hideConnectors} | ||
| /> | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This element does not contain the header, so using the header class here is incorrect. It contains the icon and connector. This is unexpected behavior, which means our test util findHeader is not also being able to find it. The refactor helps to make sure
findHeaderfinds the header.