diff --git a/.changeset/little-lemons-laugh.md b/.changeset/little-lemons-laugh.md new file mode 100644 index 00000000000..1f1fb1830f5 --- /dev/null +++ b/.changeset/little-lemons-laugh.md @@ -0,0 +1,5 @@ +--- +"@salt-ds/core": minor +--- + +Added `LockedIcon` and `InProgressIcon` to the default icon map in SemanticIconProvider. diff --git a/.changeset/tender-ads-learn.md b/.changeset/tender-ads-learn.md new file mode 100644 index 00000000000..4c0e44e81d0 --- /dev/null +++ b/.changeset/tender-ads-learn.md @@ -0,0 +1,213 @@ +--- +"@salt-ds/lab": patch +--- + +Refactored SteppedTracker, added Step and useStepReducer + +The `SteppedTracker` is a component that helps you manage a series of steps in a process. It provides a way to navigate between steps, and to track the progress of the process. + +The `` is meant to be used in conjunction with the `` component and potentially the `useStepReducer()` hook. + +In it's simplest form the `SteppedTracker` can be used like so: + +```tsx +import { SteppedTracker, Step } from "@salt-ds/lab"; + +function Example() { + return ( + + + + + + ); +} +``` + +The SteppedTracker component supports nested steps, which can be used to represent sub-steps within a step. This can be done by nesting `` components within another `` component. We advise you not to go above 2 levels deep, as it becomes hard to follow for the user. + +```tsx +import { StackLayout } from "@salt-ds/core"; +import { Step, SteppedTracker } from "@salt-ds/lab"; + +export function NestedSteps() { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + ); +} +``` + +The `SteppedTracker` component is a purely presentational component, meaning that you need to manage the state of the steps yourself. That however becomes tricky when dealing with nested steps. This is where the `useStepReducer()` hook comes in. It is a custom hook that helps you manage the state of a `SteppedTracker` component with nested steps with ease. It has a built-in algorithm that determines the stage of all steps above and below the active step. All you need to do is add `stage: 'active'` to the desired step (see `step-3-3` in the hook example below), the hook will figure out the rest. This is what we call `autoStage`. + +Migrating from the previous SteppedTracker API + +Before: + +```tsx +function Before() { + return ( + + + Step One + + + Step Two + + + Step Three + + + Step Four + + + ); +} +``` + +After: + +```tsx +function After() { + return ( + + + + + + + ); +} +``` + +Before: + +```tsx +function Before() { + return ( + + + Step 1 + + + Step 1.1 + + + Step 1.2 + + + Step 1.3 + + + Step 2 + + + Step 3 + + + Step 3.1 + + + Step 3.2 + + + Step 3.3 + + + Step 3.4 + + + Step 4 + + + ); +} +``` + +After + +```tsx +function After() { + return ( + + + + + + + + + + + + + + + + ); +} +``` + +or you can utilize the hook for nested scenarios, such as the one above + +```tsx +import { Step, SteppedTracker, useStepReducer } from "@salt-ds/lab"; + +export function AfterWithHook() { + const [state, dispatch] = useStepReducer([ + { + key: "step-1", + label: "Step 1", + substeps: [ + { key: "step-1-1", label: "Step 1.1" }, + { key: "step-1-2", label: "Step 1.2" }, + { key: "step-1-3", label: "Step 1.3" }, + ], + }, + { key: "step-2", label: "Step 2" }, + { + key: "step-3", + label: "Step 3", + substeps: [ + { key: "step-3-1", label: "Step 3.1" }, + { key: "step-3-2", label: "Step 3.2" }, + { key: "step-3-3", label: "Step 3.3", stage: "active" }, + { key: "step-3-4", label: "Step 3.4" }, + ], + }, + { key: "step-4", label: "Step 4" }, + ]); + + return ( + + + {state.steps.map((step) => ( + + ))} + + + ); +} +``` diff --git a/packages/core/src/semantic-icon-provider/SemanticIconProvider.tsx b/packages/core/src/semantic-icon-provider/SemanticIconProvider.tsx index c8b19c46142..dd29b050af2 100644 --- a/packages/core/src/semantic-icon-provider/SemanticIconProvider.tsx +++ b/packages/core/src/semantic-icon-provider/SemanticIconProvider.tsx @@ -7,7 +7,9 @@ import { CloseIcon, ErrorSolidIcon, InfoSolidIcon, + LockedIcon, OverflowMenuIcon, + ProgressInprogressIcon, StepActiveIcon, StepDefaultIcon, StepSuccessIcon, @@ -48,6 +50,8 @@ export type SemanticIconMap = { PendingIcon: ElementType; ActiveIcon: ElementType; CompletedIcon: ElementType; + LockedIcon: ElementType; + InProgressIcon: ElementType; }; export interface SemanticIconProviderProps { @@ -84,6 +88,8 @@ const defaultIconMap: SemanticIconMap = { PendingIcon: StepDefaultIcon, ActiveIcon: StepActiveIcon, CompletedIcon: StepSuccessIcon, + LockedIcon: LockedIcon, + InProgressIcon: ProgressInprogressIcon, }; const SemanticIconContext = createContext(defaultIconMap); diff --git a/packages/lab/src/__tests__/__e2e__/stepped-tracker/SteppedTracker.cy.tsx b/packages/lab/src/__tests__/__e2e__/stepped-tracker/SteppedTracker.cy.tsx index 106e3f00c4d..88b780e6a66 100644 --- a/packages/lab/src/__tests__/__e2e__/stepped-tracker/SteppedTracker.cy.tsx +++ b/packages/lab/src/__tests__/__e2e__/stepped-tracker/SteppedTracker.cy.tsx @@ -1,234 +1,55 @@ -import * as steppedTrackerStories from "@stories/stepped-tracker/stepped-tracker.stories"; -import { composeStories } from "@storybook/react"; -import { - StepLabel, - SteppedTracker, - TrackerStep, -} from "../../../stepped-tracker"; +import { Step, SteppedTracker } from "@salt-ds/lab"; -import { checkAccessibility } from "../../../../../../cypress/tests/checkAccessibility"; - -const composedStories = composeStories(steppedTrackerStories); - -describe("GIVEN a SteppedTracker", () => { - checkAccessibility(composedStories); - - it("should render TrackerStep components as children", () => { - const labels = ["Step 1", "Step 2", "Step 3"]; - const activeStep = 1; - - const TestComponent = ( - - {labels.map((label, key) => ( - - {label} - - ))} - - ); - - cy.mount(TestComponent); - labels.forEach((label) => { - cy.findByText(label).should("be.visible"); - }); - }); - - it("should indicate the active step with aria-current", () => { - const labels = ["Step 1", "Step 2", "Step 3"]; - const activeStep = 1; - - const TestComponent = ( - - {labels.map((label, key) => ( - - {label} - - ))} - - ); - - cy.mount(TestComponent); - - cy.findAllByRole("listitem") - .filter(`:nth-child(${activeStep + 1})`) - .should("have.attr", "aria-current", "step"); - - cy.findAllByRole("listitem") - .not(`:nth-child(${activeStep + 1})`) - .should("not.have.attr", "aria-current"); - }); - - it("should indicate the active step with an active icon", () => { - const labels = ["Step 1", "Step 2", "Step 3"]; - const activeStep = 1; - - const TestComponent = ( - - {labels.map((label, key) => ( - - {label} - - ))} - - ); - - cy.mount(TestComponent); - - cy.findAllByRole("listitem") - .filter(`:nth-child(${activeStep + 1})`) - .findByTestId("StepActiveIcon") - .should("exist"); - cy.findAllByRole("listitem") - .not(`:nth-child(${activeStep + 1})`) - .findByTestId("StepActiveIcon") - .should("not.exist"); - }); - - it("should indicate the completed step with a completed icon", () => { - const labels = ["Step 1", "Step 2", "Step 3"]; - const activeStep = 1; - const completedStep = 2; - - const TestComponent = ( - - {labels.map((label, key) => ( - - {label} - - ))} - +describe("", () => { + it("should expand/collapse when trigger is clicked (depth 1)", () => { + cy.mount( + + + + + + + , ); - cy.mount(TestComponent); + cy.findByText("Step 1").should("be.visible"); + cy.findByText("Step 1.1").should("not.be.visible"); - cy.findAllByRole("listitem") - .filter(`:nth-child(${completedStep + 1})`) - .findByTestId("StepSuccessIcon") - .should("exist"); - cy.findAllByRole("listitem") - .not(`:nth-child(${completedStep + 1})`) - .findByTestId("StepSuccessIcon") - .should("not.exist"); - }); - - it("should show completed icon if stage prop is completed and step is active", () => { - const labels = ["Step 1", "Step 2", "Step 3"]; - - const stepNum = 1; - - const TestComponent = ( - - {labels.map((label, key) => ( - - {label} - - ))} - - ); - - cy.mount(TestComponent); - - cy.findAllByRole("listitem") - .filter(`:nth-child(${stepNum + 1})`) - .findByTestId("StepSuccessIcon") - .should("exist"); - cy.findAllByRole("listitem") - .not(`:nth-child(${stepNum + 1})`) - .findByTestId("StepActiveIcon") - .should("not.exist"); - }); - - it("should show warning icon if status prop is warning", () => { - const labels = ["Step 1", "Step 2", "Step 3"]; - - const TestComponent = ( - - {labels.map((label, key) => ( - - {label} - - ))} - - ); + cy.findByRole("button", { expanded: false }).click(); - cy.mount(TestComponent); - - cy.findAllByRole("listitem") - .filter(`:nth-child(${2})`) - .findByTestId("WarningSolidIcon") - .should("exist"); + cy.findByText("Step 1").should("be.visible"); + cy.findByText("Step 1.1").should("be.visible"); }); - it("should show completed icon if status prop is warning but stage prop is completed", () => { - const labels = ["Step 1", "Step 2", "Step 3"]; - - const TestComponent = ( - - {labels.map((label, key) => ( - - {label} - - ))} - + it("should expand/collapse when trigger is clicked (depth 2)", () => { + cy.mount( + + + + + + + + + + + , ); - cy.mount(TestComponent); - - cy.findAllByRole("listitem") - .filter(`:nth-child(${2})`) - .findByTestId("StepSuccessIcon") - .should("exist"); - }); - - it("should show active icon if status prop is warning but step is active", () => { - const labels = ["Step 1", "Step 2", "Step 3"]; + cy.findByText("Step 1").should("be.visible"); + cy.findByText("Step 1.1").should("not.be.visible"); + cy.findByText("Step 1.1.1").should("not.be.visible"); - const TestComponent = ( - - {labels.map((label, key) => ( - - {label} - - ))} - - ); + cy.findByRole("button", { expanded: false }).click(); - cy.mount(TestComponent); - - cy.findAllByRole("listitem") - .filter(`:nth-child(${2})`) - .findByTestId("StepActiveIcon") - .should("exist"); - }); - - it("should show error icon if status prop is error", () => { - const labels = ["Step 1", "Step 2", "Step 3"]; - - const TestComponent = ( - - {labels.map((label, key) => ( - - {label} - - ))} - - ); + cy.findByText("Step 1").should("be.visible"); + cy.findByText("Step 1.1").should("be.visible"); + cy.findByText("Step 1.1.1").should("not.be.visible"); - cy.mount(TestComponent); + cy.findByRole("button", { expanded: false }).click(); - cy.findAllByRole("listitem") - .filter(`:nth-child(${2})`) - .findByTestId("ErrorSolidIcon") - .should("exist"); + cy.findByText("Step 1").should("be.visible"); + cy.findByText("Step 1.1").should("be.visible"); + cy.findByText("Step 1.1.1").should("be.visible"); }); }); diff --git a/packages/lab/src/stepped-tracker/Step.Connector.css b/packages/lab/src/stepped-tracker/Step.Connector.css new file mode 100644 index 00000000000..e520139eb13 --- /dev/null +++ b/packages/lab/src/stepped-tracker/Step.Connector.css @@ -0,0 +1,49 @@ +.saltStepConnector { + grid-area: connector; + + transition-duration: inherit; + transition-timing-function: inherit; + transition-property: opacity, min-height; +} + +.saltSteppedTracker-horizontal .saltStepConnector { + position: absolute; + transform: translateY(-100%); + top: calc(var(--step-icon-size) / 2); + left: calc(50% + calc(var(--step-icon-size) / 2 + var(--salt-spacing-100))); + right: calc(-50% + calc(var(--step-icon-size) / 2 + var(--salt-spacing-100))); + + border-top-width: var(--salt-size-border-strong); + border-top-style: var(--salt-track-borderStyle-incomplete); + border-top-color: var(--salt-track-borderColor); +} + +.saltSteppedTracker-horizontal .saltStep-stage-completed > .saltStepConnector, +.saltSteppedTracker-horizontal .saltStep-stage-inprogress > .saltStepConnector { + border-top-style: var(--salt-track-borderStyle-complete); +} + +.saltSteppedTracker-vertical .saltStepConnector { + min-height: var(--salt-size-base); + align-self: stretch; + justify-self: center; + + border-left-width: var(--salt-size-border-strong); + border-left-style: var(--salt-track-borderStyle-incomplete); + border-left-color: var(--salt-track-borderColor); +} + +.saltSteppedTracker-vertical .saltStep-stage-completed > .saltStepConnector, +.saltSteppedTracker-vertical .saltStep-stage-inprogress > .saltStepConnector { + border-left-style: var(--salt-track-borderStyle-complete); +} + +.saltStep-depth-0.saltStep:not(.saltStep-expanded):last-child > .saltStepConnector { + opacity: 0; + min-height: 0; +} + +.saltStep-depth-0.saltStep-expanded:last-child .saltStep:not(.saltStep-expanded):last-child .saltStepConnector { + opacity: 0; + min-height: 0; +} diff --git a/packages/lab/src/stepped-tracker/Step.Connector.tsx b/packages/lab/src/stepped-tracker/Step.Connector.tsx new file mode 100644 index 00000000000..35cb81db152 --- /dev/null +++ b/packages/lab/src/stepped-tracker/Step.Connector.tsx @@ -0,0 +1,19 @@ +import { makePrefixer } from "@salt-ds/core"; +import { useComponentCssInjection } from "@salt-ds/styles"; +import { useWindow } from "@salt-ds/window"; + +import stepConnectorCSS from "./Step.Connector.css"; + +const withBaseName = makePrefixer("saltStepConnector"); + +export function StepConnector() { + const targetWindow = useWindow(); + + useComponentCssInjection({ + testId: "salt-step-connector", + css: stepConnectorCSS, + window: targetWindow, + }); + + return
; +} diff --git a/packages/lab/src/stepped-tracker/Step.Description.css b/packages/lab/src/stepped-tracker/Step.Description.css new file mode 100644 index 00000000000..870f527cc1e --- /dev/null +++ b/packages/lab/src/stepped-tracker/Step.Description.css @@ -0,0 +1,16 @@ +.saltStepDescription { + grid-area: description; +} + +.saltSteppedTracker-vertical .saltStepDescription { + padding-bottom: var(--salt-spacing-300); + padding-left: calc((var(--step-depth)) * var(--salt-spacing-100)); +} + +.saltStep-status-warning > .saltStepDescription { + color: var(--salt-status-warning-foreground-informative); +} + +.saltStep-status-error > .saltStepDescription { + color: var(--salt-status-error-foreground-informative); +} diff --git a/packages/lab/src/stepped-tracker/Step.Description.tsx b/packages/lab/src/stepped-tracker/Step.Description.tsx new file mode 100644 index 00000000000..904654aa851 --- /dev/null +++ b/packages/lab/src/stepped-tracker/Step.Description.tsx @@ -0,0 +1,34 @@ +import { Text, type TextProps, makePrefixer } from "@salt-ds/core"; +import { useComponentCssInjection } from "@salt-ds/styles"; +import { useWindow } from "@salt-ds/window"; +import clsx from "clsx"; + +import stepDescriptionCSS from "./Step.Description.css"; + +export interface StepDescriptionProps extends TextProps<"div"> {} + +const withBaseName = makePrefixer("saltStepDescription"); + +export function StepDescription({ + id, + className, + styleAs = "label", + ...props +}: StepDescriptionProps) { + const targetWindow = useWindow(); + + useComponentCssInjection({ + testId: "salt-step-description", + css: stepDescriptionCSS, + window: targetWindow, + }); + + return ( + + ); +} diff --git a/packages/lab/src/stepped-tracker/Step.ExpandTrigger.css b/packages/lab/src/stepped-tracker/Step.ExpandTrigger.css new file mode 100644 index 00000000000..587c416413e --- /dev/null +++ b/packages/lab/src/stepped-tracker/Step.ExpandTrigger.css @@ -0,0 +1,7 @@ +.saltStepExpandTrigger { + grid-area: expand; +} + +.saltButton.saltStepExpandTrigger:focus-visible { + outline-offset: calc(-1 * var(--salt-focused-outlineWidth)); +} diff --git a/packages/lab/src/stepped-tracker/Step.ExpandTrigger.tsx b/packages/lab/src/stepped-tracker/Step.ExpandTrigger.tsx new file mode 100644 index 00000000000..41152644d0e --- /dev/null +++ b/packages/lab/src/stepped-tracker/Step.ExpandTrigger.tsx @@ -0,0 +1,42 @@ +import { Button, type ButtonProps } from "@salt-ds/core"; +import { makePrefixer } from "@salt-ds/core"; +import { useIcon } from "@salt-ds/core"; +import { useComponentCssInjection } from "@salt-ds/styles"; +import { useWindow } from "@salt-ds/window"; +import clsx from "clsx"; + +import stepExpandTriggerCSS from "./Step.ExpandTrigger.css"; + +export interface StepExpandTriggerProps extends ButtonProps { + expanded: boolean; +} + +const withBaseName = makePrefixer("saltStepExpandTrigger"); + +export function StepExpandTrigger({ + id, + expanded, + className, + ...props +}: StepExpandTriggerProps) { + const { CollapseIcon, ExpandIcon } = useIcon(); + const targetWindow = useWindow(); + + useComponentCssInjection({ + testId: "salt-step-expand-trigger", + css: stepExpandTriggerCSS, + window: targetWindow, + }); + + return ( + + ); +} diff --git a/packages/lab/src/stepped-tracker/Step.Icon.css b/packages/lab/src/stepped-tracker/Step.Icon.css new file mode 100644 index 00000000000..171cafecd9b --- /dev/null +++ b/packages/lab/src/stepped-tracker/Step.Icon.css @@ -0,0 +1,40 @@ +.saltStepIcon { + grid-area: icon; + + display: flex; + justify-content: center; + align-items: center; + justify-self: center; +} + +.saltSteppedTracker-vertical .saltStepIcon { + height: var(--salt-size-base); +} + +.saltStep-stage-pending > .saltStepIcon { + --saltIcon-color: var(--salt-status-static-foreground); +} + +.saltStep-stage-locked > .saltStepIcon { + --saltIcon-color: var(--salt-status-static-foreground); +} + +.saltStep-stage-inprogress > .saltStepIcon { + --saltIcon-color: var(--salt-status-info-foreground-decorative); +} + +.saltStep-stage-active > .saltStepIcon { + --saltIcon-color: var(--salt-status-info-foreground-decorative); +} + +.saltStep-stage-completed > .saltStepIcon { + --saltIcon-color: var(--salt-status-success-foreground-decorative); +} + +.saltStep-status-warning > .saltStepIcon { + --saltIcon-color: var(--salt-status-warning-foreground-decorative); +} + +.saltStep-status-error > .saltStepIcon { + --saltIcon-color: var(--salt-status-error-foreground-decorative); +} diff --git a/packages/lab/src/stepped-tracker/Step.Icon.tsx b/packages/lab/src/stepped-tracker/Step.Icon.tsx new file mode 100644 index 00000000000..99d1876007b --- /dev/null +++ b/packages/lab/src/stepped-tracker/Step.Icon.tsx @@ -0,0 +1,66 @@ +import { makePrefixer, useIcon } from "@salt-ds/core"; +import type { IconProps } from "@salt-ds/icons"; +import { useComponentCssInjection } from "@salt-ds/styles"; +import { useWindow } from "@salt-ds/window"; +import clsx from "clsx"; +import { useMemo } from "react"; + +import stepIconCSS from "./Step.Icon.css"; + +import type { StepStage, StepStatus } from "./Step.types"; + +export interface StepIconProps extends IconProps { + stage: StepStage; + status?: StepStatus; + sizeMultiplier?: IconProps["size"]; +} + +const withBaseName = makePrefixer("saltStepIcon"); + +export function StepIcon({ + status, + stage, + size, + sizeMultiplier = size || 1.5, + className, + ...props +}: StepIconProps) { + const targetWindow = useWindow(); + const IconComponent = useStepIcon({ stage, status }); + + useComponentCssInjection({ + testId: "salt-step-icon", + css: stepIconCSS, + window: targetWindow, + }); + + return ( + + ); +} + +function useStepIcon({ + stage, + status, +}: Pick) { + const icons = useIcon(); + + const stepIconMap = useMemo( + () => ({ + error: icons.ErrorIcon, + warning: icons.WarningIcon, + active: icons.ActiveIcon, + completed: icons.CompletedIcon, + pending: icons.PendingIcon, + inprogress: icons.InProgressIcon, + locked: icons.LockedIcon, + }), + [icons], + ); + + return stepIconMap[status || stage]; +} diff --git a/packages/lab/src/stepped-tracker/Step.Label.css b/packages/lab/src/stepped-tracker/Step.Label.css new file mode 100644 index 00000000000..2a44806805e --- /dev/null +++ b/packages/lab/src/stepped-tracker/Step.Label.css @@ -0,0 +1,17 @@ +.saltStepLabel { + grid-area: label; +} + +.saltSteppedTracker-horizontal .saltStepLabel { + margin-top: var(--salt-spacing-50); +} + +.saltSteppedTracker-vertical .saltStepLabel { + padding-top: calc((var(--salt-size-base) - var(--salt-text-label-lineHeight)) / 2); + padding-bottom: calc((var(--salt-size-base) - var(--salt-text-label-lineHeight)) / 2); + padding-left: calc((var(--step-depth)) * var(--salt-spacing-100)); +} + +.saltStep-depth-0 > .saltText.saltStepLabel { + font-weight: var(--salt-text-fontWeight-strong); +} diff --git a/packages/lab/src/stepped-tracker/Step.Label.tsx b/packages/lab/src/stepped-tracker/Step.Label.tsx new file mode 100644 index 00000000000..49820dcbe08 --- /dev/null +++ b/packages/lab/src/stepped-tracker/Step.Label.tsx @@ -0,0 +1,37 @@ +import { Text, type TextProps, makePrefixer } from "@salt-ds/core"; +import { useComponentCssInjection } from "@salt-ds/styles"; +import { useWindow } from "@salt-ds/window"; +import clsx from "clsx"; + +import stepLabelCSS from "./Step.Label.css"; + +export interface StepLabelProps extends TextProps<"div"> {} + +const withBaseName = makePrefixer("saltStepLabel"); + +export function StepLabel({ + id, + className, + styleAs = "label", + children, + ...props +}: StepLabelProps) { + const targetWindow = useWindow(); + + useComponentCssInjection({ + testId: "salt-step-label", + css: stepLabelCSS, + window: targetWindow, + }); + + return ( + + {children} + + ); +} diff --git a/packages/lab/src/stepped-tracker/Step.SROnly.css b/packages/lab/src/stepped-tracker/Step.SROnly.css new file mode 100644 index 00000000000..69392cdab06 --- /dev/null +++ b/packages/lab/src/stepped-tracker/Step.SROnly.css @@ -0,0 +1,6 @@ +.saltStepSROnly { + position: fixed; + top: 0; + left: 0; + transform: translate(-100%, -100%); +} diff --git a/packages/lab/src/stepped-tracker/Step.SROnly.tsx b/packages/lab/src/stepped-tracker/Step.SROnly.tsx new file mode 100644 index 00000000000..657559da4d4 --- /dev/null +++ b/packages/lab/src/stepped-tracker/Step.SROnly.tsx @@ -0,0 +1,28 @@ +import { makePrefixer } from "@salt-ds/core"; +import { useComponentCssInjection } from "@salt-ds/styles"; +import { useWindow } from "@salt-ds/window"; +import type { ComponentPropsWithoutRef, ReactNode } from "react"; + +import stepSROnlyCSS from "./Step.SROnly.css"; + +const withBaseName = makePrefixer("saltStepSROnly"); + +export interface StepSROnlyProps extends ComponentPropsWithoutRef<"div"> { + children?: ReactNode; +} + +export function StepSROnly({ children, ...props }: StepSROnlyProps) { + const targetWindow = useWindow(); + + useComponentCssInjection({ + testId: "salt-step-sr-only", + css: stepSROnlyCSS, + window: targetWindow, + }); + + return ( +
+ {children} +
+ ); +} diff --git a/packages/lab/src/stepped-tracker/Step.css b/packages/lab/src/stepped-tracker/Step.css new file mode 100644 index 00000000000..110dd64101c --- /dev/null +++ b/packages/lab/src/stepped-tracker/Step.css @@ -0,0 +1,60 @@ +.saltStep { + /* Copy of size calculations of */ + --step-icon-base-size: var(--salt-size-icon, 12px); + --step-icon-size-multiplier: var(--saltIcon-size-multiplier, 1.5); + --step-icon-size: calc(var(--step-icon-base-size) * var(--step-icon-size-multiplier)); + --step-depth: var(--saltStep-depth, 0); +} + +.saltSteppedTracker-horizontal .saltStep { + position: relative; + display: grid; + grid-template-columns: 1fr; + grid-template-rows: repeat(3, min-content); + grid-template-areas: + "icon" + "label" + "description"; + justify-items: center; + align-items: center; + text-align: center; + flex: 1; + padding: 0 var(--salt-spacing-25); +} + +.saltSteppedTracker-vertical .saltStep { + display: grid; + grid-template-columns: var(--step-icon-size) 1fr min-content; + grid-template-areas: + "icon label expand" + "connector description ." + "stepped-tracker stepped-tracker stepped-tracker"; + justify-items: start; + align-items: start; + gap: 0 var(--salt-spacing-100); + width: 100%; + transition-duration: inherit; + transition-timing-function: inherit; + transition-property: grid-template-rows; +} + +.saltSteppedTracker-vertical .saltStep-terminal { + grid-template-areas: + "icon label label" + "connector description description" + "stepped-tracker stepped-tracker stepped-tracker"; +} + +.saltSteppedTracker-vertical > .saltStep.saltStep-expanded { + grid-template-rows: + var(--salt-size-base) + min-content + 1fr; +} + +.saltSteppedTracker-vertical > .saltStep.saltStep-collapsed { + grid-template-rows: + var(--salt-size-base) + min-content + 0fr; +} diff --git a/packages/lab/src/stepped-tracker/Step.tsx b/packages/lab/src/stepped-tracker/Step.tsx new file mode 100644 index 00000000000..43c33d41630 --- /dev/null +++ b/packages/lab/src/stepped-tracker/Step.tsx @@ -0,0 +1,173 @@ +import { makePrefixer, useControlled, useId } from "@salt-ds/core"; +import { useComponentCssInjection } from "@salt-ds/styles"; +import { useWindow } from "@salt-ds/window"; +import clsx from "clsx"; +import { type CSSProperties, useContext, useEffect } from "react"; + +import { StepConnector } from "./Step.Connector"; +import { StepDescription } from "./Step.Description"; +import { StepExpandTrigger } from "./Step.ExpandTrigger"; +import { StepIcon } from "./Step.Icon"; +import { StepLabel } from "./Step.Label"; +import { StepSROnly } from "./Step.SROnly"; +import stepCSS from "./Step.css"; +import { SteppedTracker } from "./SteppedTracker"; +import { DepthContext } from "./SteppedTracker.Provider"; + +import type { StepProps } from "./Step.types"; + +const withBaseName = makePrefixer("saltStep"); + +export function Step({ + id: idProp, + label, + description, + status, + stage = "pending", + expanded: expandedProp, + defaultExpanded, + onToggle, + className, + style, + substeps, + children, + ...props +}: StepProps) { + const id = useId(idProp); + const targetWindow = useWindow(); + const depth = useContext(DepthContext); + + const [expanded, setExpanded] = useControlled({ + name: "Step", + state: "expanded", + controlled: expandedProp, + default: Boolean(defaultExpanded), + }); + + useComponentCssInjection({ + testId: "salt-step", + css: stepCSS, + window: targetWindow, + }); + + useEffect(() => { + if (process.env.NODE_ENV !== "production") { + if (depth === -1) { + console.warn( + " should be used within a component!", + ); + } + + if (depth > 2) { + console.warn(" should not be nested more than 2 levels deep!"); + } + } + }, [depth]); + + const ariaCurrent = stage === "active" ? "step" : undefined; + const iconSizeMultiplier = depth === 0 ? 1.5 : 1; + const hasNestedSteps = !!children || !!substeps; + const state = status || stage; + + const labelId = `${id}-label`; + const descriptionId = `${id}-description`; + const expandTriggerId = `${id}-expand-trigger`; + const nestedSteppedTrackerId = `${id}-nested-stepped-tracker`; + + const srOnly = { + stateId: `${id}-sr-only-state`, + stateText: state !== "active" ? state : undefined, + substepsId: `${id}-sr-only-substeps`, + substepsText: "substeps", + toggleSubstepsId: `${id}-sr-only-toggle-substeps`, + toggleSubstepsText: "toggle substeps", + }; + + return ( +
  • + + {label} {description} {srOnly.stateText} + + + {srOnly.toggleSubstepsText} + + + {srOnly.substepsText} + + + {srOnly.stateText} + + + + {label && ( + + {label} + + )} + {description && ( + + {description} + + )} + {hasNestedSteps && ( + { + onToggle?.(event); + setExpanded(!expanded); + }} + /> + )} + {hasNestedSteps && ( + + )} +
  • + ); +} diff --git a/packages/lab/src/stepped-tracker/Step.types.ts b/packages/lab/src/stepped-tracker/Step.types.ts new file mode 100644 index 00000000000..f33ca2f8abf --- /dev/null +++ b/packages/lab/src/stepped-tracker/Step.types.ts @@ -0,0 +1,29 @@ +import type { ButtonProps } from "@salt-ds/core"; +import type { ComponentPropsWithoutRef, ReactNode } from "react"; + +export interface StepProps + extends Omit, "onToggle"> { + label?: ReactNode; + description?: ReactNode; + status?: StepStatus; + stage?: StepStage; + expanded?: boolean; + defaultExpanded?: boolean; + onToggle?: ButtonProps["onClick"]; + substeps?: StepRecord[]; + children?: ReactNode; +} + +export type StepRecord = + | (Omit & { id: string }) + | (Omit & { key: string }); + +export type StepStatus = "warning" | "error"; +export type StepStage = + | "pending" + | "locked" + | "completed" + | "inprogress" + | "active"; + +export type StepDepth = number; diff --git a/packages/lab/src/stepped-tracker/StepLabel/StepLabel.css b/packages/lab/src/stepped-tracker/StepLabel/StepLabel.css deleted file mode 100644 index a181d8d4903..00000000000 --- a/packages/lab/src/stepped-tracker/StepLabel/StepLabel.css +++ /dev/null @@ -1,11 +0,0 @@ -.saltStepLabel { - width: 100%; -} - -.saltSteppedTracker.saltSteppedTracker-horizontal .saltStepLabel { - text-align: center; -} - -.saltSteppedTracker.saltSteppedTracker-vertical .saltStepLabel { - text-align: left; -} diff --git a/packages/lab/src/stepped-tracker/StepLabel/StepLabel.tsx b/packages/lab/src/stepped-tracker/StepLabel/StepLabel.tsx deleted file mode 100644 index 50e2d519000..00000000000 --- a/packages/lab/src/stepped-tracker/StepLabel/StepLabel.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { Label, type TextProps, makePrefixer } from "@salt-ds/core"; -import { useComponentCssInjection } from "@salt-ds/styles"; -import { useWindow } from "@salt-ds/window"; -import { clsx } from "clsx"; -import { type ReactNode, forwardRef } from "react"; - -import stepLabelCss from "./StepLabel.css"; - -const withBaseName = makePrefixer("saltStepLabel"); - -export interface StepLabelProps extends TextProps<"label"> { - /** - * The content of Step Label - */ - children?: ReactNode; -} - -export const StepLabel = forwardRef( - function StepLabel({ children, className, ...rest }, ref) { - const targetWindow = useWindow(); - useComponentCssInjection({ - testId: "salt-step-label", - css: stepLabelCss, - window: targetWindow, - }); - - return ( - - ); - }, -); diff --git a/packages/lab/src/stepped-tracker/StepLabel/index.ts b/packages/lab/src/stepped-tracker/StepLabel/index.ts deleted file mode 100644 index f2952bdcc0c..00000000000 --- a/packages/lab/src/stepped-tracker/StepLabel/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./StepLabel"; diff --git a/packages/lab/src/stepped-tracker/SteppedTracker.Provider.tsx b/packages/lab/src/stepped-tracker/SteppedTracker.Provider.tsx new file mode 100644 index 00000000000..5114066d6bf --- /dev/null +++ b/packages/lab/src/stepped-tracker/SteppedTracker.Provider.tsx @@ -0,0 +1,28 @@ +import { type ReactNode, createContext, useContext } from "react"; + +import type { StepDepth } from "./Step.types"; +import type { SteppedTrackerOrientation } from "./SteppedTracker.types"; + +export const DepthContext = createContext(-1); +export const OrientationContext = + createContext("horizontal"); + +export interface SteppedTrackerProviderProps { + orientation: SteppedTrackerOrientation; + children: ReactNode; +} + +export function SteppedTrackerProvider({ + orientation: orientationProp, + children, +}: SteppedTrackerProviderProps) { + const depth = useContext(DepthContext); + + return ( + + + {children} + + + ); +} diff --git a/packages/lab/src/stepped-tracker/SteppedTracker.css b/packages/lab/src/stepped-tracker/SteppedTracker.css index d8cd374cb61..f1215bc0382 100644 --- a/packages/lab/src/stepped-tracker/SteppedTracker.css +++ b/packages/lab/src/stepped-tracker/SteppedTracker.css @@ -1,17 +1,39 @@ .saltSteppedTracker { + grid-area: stepped-tracker; + width: 100%; + height: 100%; + margin: 0; padding: 0; - text-indent: 0; list-style-type: none; - display: flex; - width: 100%; - position: relative; + transition-duration: var(--salt-duration-perceptible); + transition-timing-function: ease-in-out; + transition-property: opacity, visibility; +} + +@media (prefers-reduced-motion) { + .saltSteppedTracker { + transition-duration: var(--salt-duration-instant); + } } -.saltSteppedTracker.saltSteppedTracker-horizontal { +.saltSteppedTracker-horizontal { + display: flex; flex-direction: row; } -.saltSteppedTracker.saltSteppedTracker-vertical { +.saltSteppedTracker-vertical { + display: flex; flex-direction: column; + overflow: hidden; +} + +.saltSteppedTracker-vertical > .saltStep.saltStep-expanded > .saltSteppedTracker { + opacity: 1; + visibility: visible; +} + +.saltSteppedTracker-vertical > .saltStep.saltStep-collapsed > .saltSteppedTracker { + opacity: 0; + visibility: hidden; } diff --git a/packages/lab/src/stepped-tracker/SteppedTracker.tsx b/packages/lab/src/stepped-tracker/SteppedTracker.tsx index 7c17cfaa045..a84b2f9b597 100644 --- a/packages/lab/src/stepped-tracker/SteppedTracker.tsx +++ b/packages/lab/src/stepped-tracker/SteppedTracker.tsx @@ -2,91 +2,41 @@ import { makePrefixer } from "@salt-ds/core"; import { useComponentCssInjection } from "@salt-ds/styles"; import { useWindow } from "@salt-ds/window"; import { clsx } from "clsx"; -import { - Children, - type ComponentPropsWithoutRef, - type ReactElement, - type ReactNode, - forwardRef, - isValidElement, - useEffect, -} from "react"; +import { forwardRef, useContext } from "react"; import { + OrientationContext, SteppedTrackerProvider, - TrackerStepProvider, -} from "./SteppedTrackerContext"; - -import steppedTrackerCss from "./SteppedTracker.css"; +} from "./SteppedTracker.Provider"; +import SteppedTrackerCSS from "./SteppedTracker.css"; +import type { SteppedTrackerProps } from "./SteppedTracker.types"; const withBaseName = makePrefixer("saltSteppedTracker"); -export interface SteppedTrackerProps extends ComponentPropsWithoutRef<"ul"> { - /** - * The index of the current activeStep - */ - activeStep: number; - /** - * Should be one or more TrackerStep components - */ - children: ReactNode; - /** - * The orientation of the SteppedTracker. Defaults to `horizontal` - */ - orientation?: "horizontal" | "vertical"; -} - -const useCheckInvalidChildren = (children: ReactNode) => { - useEffect(() => { - if (process.env.NODE_ENV !== "production") { - let hasInvalidChild = false; - Children.forEach(children, (child) => { - if (!isValidElement(child)) { - hasInvalidChild = true; - } - }); - - if (hasInvalidChild) { - console.error( - "Invalid child: children of SteppedTracker must be a TrackerStep component", - ); - } - } - }, [children]); -}; - -export const SteppedTracker = forwardRef( +export const SteppedTracker = forwardRef( function SteppedTracker( - { - children, - className, - activeStep, - orientation = "horizontal", - ...restProps - }, + { orientation: orientationProp, children, className, ...props }, ref, - ): ReactElement { + ) { const targetWindow = useWindow(); + const orientationContext = useContext(OrientationContext); + const orientation = orientationProp || orientationContext; + useComponentCssInjection({ - testId: "salt-stepped-tracker", - css: steppedTrackerCss, + testId: "salt-SteppedTracker", + css: SteppedTrackerCSS, window: targetWindow, }); - useCheckInvalidChildren(children); - - const totalSteps = Children.count(children); return ( - -
      +
        - {Children.map(children, (child, i) => ( - {child} - ))} -
    + {children} +
    ); }, diff --git a/packages/lab/src/stepped-tracker/SteppedTracker.types.ts b/packages/lab/src/stepped-tracker/SteppedTracker.types.ts new file mode 100644 index 00000000000..49b9259b997 --- /dev/null +++ b/packages/lab/src/stepped-tracker/SteppedTracker.types.ts @@ -0,0 +1,8 @@ +import type { ComponentProps, ReactNode } from "react"; + +export interface SteppedTrackerProps extends ComponentProps<"ol"> { + orientation?: SteppedTrackerOrientation; + children: ReactNode; +} + +export type SteppedTrackerOrientation = "horizontal" | "vertical"; diff --git a/packages/lab/src/stepped-tracker/SteppedTrackerContext.tsx b/packages/lab/src/stepped-tracker/SteppedTrackerContext.tsx deleted file mode 100644 index b0a5d8ed9c8..00000000000 --- a/packages/lab/src/stepped-tracker/SteppedTrackerContext.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { type ReactNode, createContext, useContext, useMemo } from "react"; - -export interface SteppedTrackerContextType { - activeStep: number; - totalSteps: number; - isWithinSteppedTracker: boolean; -} - -const defaultSteppedTrackerContext = { - activeStep: 0, - totalSteps: 1, - isWithinSteppedTracker: false, -}; - -const SteppedTrackerContext = createContext( - defaultSteppedTrackerContext as unknown as SteppedTrackerContextType, -); - -type SteppedTrackerProviderProps = Omit< - SteppedTrackerContextType, - "isWithinSteppedTracker" -> & { - children: ReactNode; -}; - -export const SteppedTrackerProvider = ({ - activeStep, - totalSteps, - children, -}: SteppedTrackerProviderProps) => { - const steppedTrackerContextValue: SteppedTrackerContextType = useMemo( - () => ({ - activeStep, - totalSteps, - isWithinSteppedTracker: true, - }), - [activeStep, totalSteps], - ); - - return ( - - {children} - - ); -}; - -export const useSteppedTrackerContext = () => useContext(SteppedTrackerContext); - -type TrackerStepNumberContextType = number; - -const TrackerStepContext = createContext(0); - -export const useTrackerStepContext = () => useContext(TrackerStepContext); - -interface TrackerStepProviderProps { - stepNumber: number; - children: ReactNode; -} - -export const TrackerStepProvider = ({ - children, - stepNumber, -}: TrackerStepProviderProps) => { - return ( - - {children} - - ); -}; diff --git a/packages/lab/src/stepped-tracker/TrackerConnector/TrackerConnector.css b/packages/lab/src/stepped-tracker/TrackerConnector/TrackerConnector.css deleted file mode 100644 index a30de12be9f..00000000000 --- a/packages/lab/src/stepped-tracker/TrackerConnector/TrackerConnector.css +++ /dev/null @@ -1,35 +0,0 @@ -.saltTrackerConnector { - --trackerConnector-style-default: var(--saltTrackerConnector-style-default, var(--salt-track-borderStyle-incomplete)); - --trackerConnector-style-active: var(--saltTrackerConnector-style-active, var(--salt-track-borderStyle-active)); - --trackerConnector-color: var(--saltTrackerConnector-color, var(--salt-track-borderColor)); - --trackerConnector-thickness: var(--saltTrackerConnector-thickness, var(--salt-size-border-strong)); - --trackerConnector-margin: var(--saltTrackerConnector-margin, var(--salt-spacing-100)); - --trackerConnector-style-border: var(--trackerConnector-style-default); -} - -.saltTrackerConnector { - position: absolute; -} - -.saltSteppedTracker.saltSteppedTracker-horizontal .saltTrackerConnector { - border-top-width: var(--trackerConnector-thickness); - border-top-style: var(--trackerConnector-style-border); - border-top-color: var(--trackerConnector-color); - width: calc(100% - (var(--saltIcon-size)) - (var(--trackerConnector-margin) * 2)); - left: calc(50% + (var(--saltIcon-size) / 2) + var(--trackerConnector-margin)); - top: calc(var(--saltIcon-size) / 2 - (var(--trackerConnector-thickness) / 2)); - height: 0; -} - -.saltSteppedTracker.saltSteppedTracker-vertical .saltTrackerConnector { - top: calc(50% + (var(--saltIcon-size) / 2) + var(--trackerConnector-margin)); - left: calc((var(--saltIcon-size) / 2) - (var(--trackerConnector-thickness) / 2)); - height: calc(100% - (var(--saltIcon-size)) - (var(--trackerConnector-margin) * 2)); - border-left-width: var(--trackerConnector-thickness); - border-left-style: var(--trackerConnector-style-border); - border-left-color: var(--trackerConnector-color); -} - -.saltTrackerConnector.saltTrackerConnector-active { - --trackerConnector-style-border: var(--trackerConnector-style-active); -} diff --git a/packages/lab/src/stepped-tracker/TrackerConnector/TrackerConnector.tsx b/packages/lab/src/stepped-tracker/TrackerConnector/TrackerConnector.tsx deleted file mode 100644 index 538231fb62f..00000000000 --- a/packages/lab/src/stepped-tracker/TrackerConnector/TrackerConnector.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { makePrefixer } from "@salt-ds/core"; -import { clsx } from "clsx"; - -import { useComponentCssInjection } from "@salt-ds/styles"; -import { useWindow } from "@salt-ds/window"; - -import trackerConnectorCss from "./TrackerConnector.css"; - -const withBaseName = makePrefixer("saltTrackerConnector"); - -type ConnectorState = "default" | "active"; - -interface TrackerConnectorProps { - /** - * The state of the connector, which acts as an indicator of whether the active step is ahead/behind - */ - state: ConnectorState; -} - -export const TrackerConnector = ({ state }: TrackerConnectorProps) => { - const targetWindow = useWindow(); - useComponentCssInjection({ - testId: "salt-tracker-connector", - css: trackerConnectorCss, - window: targetWindow, - }); - - return ; -}; diff --git a/packages/lab/src/stepped-tracker/TrackerConnector/index.ts b/packages/lab/src/stepped-tracker/TrackerConnector/index.ts deleted file mode 100644 index a6ec98435ef..00000000000 --- a/packages/lab/src/stepped-tracker/TrackerConnector/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { TrackerConnector } from "./TrackerConnector"; diff --git a/packages/lab/src/stepped-tracker/TrackerStep/TrackerStep.css b/packages/lab/src/stepped-tracker/TrackerStep/TrackerStep.css deleted file mode 100644 index 40d3690fd53..00000000000 --- a/packages/lab/src/stepped-tracker/TrackerStep/TrackerStep.css +++ /dev/null @@ -1,69 +0,0 @@ -.saltTrackerStep { - --trackerStep-icon-color-active: var(--saltTrackerStep-icon-color-active, var(--salt-status-info-foreground-decorative)); - --trackerStep-icon-color-completed: var(--saltTrackerStep-icon-color-completed, var(--salt-status-success-foreground-decorative)); - --trackerStep-icon-color-warning: var(--saltTrackerStep-icon-color-warning, var(--salt-status-warning-foreground-decorative)); - --trackerStep-icon-color-error: var(--saltTrackerStep-icon-color-error, var(--salt-status-error-foreground-decorative)); - - --trackerStep-icon-color: var(--saltTrackerStep-icon-color, var(--salt-status-static-foreground)); - - --saltIcon-color: var(--trackerStep-icon-color); - /* We redefine Icon Size so we can use it in calc functions in the trackerConnector */ - --saltIcon-size: var(--saltTrackerStep-icon-size, max(var(--salt-size-icon), 12px)); -} - -.saltTrackerStep { - margin: 0; - text-indent: 0; - list-style-type: none; - flex: 1; - position: relative; - display: flex; - align-items: center; - width: var(--saltTrackerStep-width, 100%); - gap: var(--salt-spacing-50); - color: var(--salt-content-primary-foreground); - font-size: var(--salt-text-label-fontSize); - line-height: var(--salt-text-label-lineHeight); -} - -.saltSteppedTracker.saltSteppedTracker-horizontal .saltTrackerStep { - flex-direction: column; - padding: 0 var(--salt-spacing-25); -} - -.saltSteppedTracker.saltSteppedTracker-vertical .saltTrackerStep { - flex-direction: row; - min-height: calc(var(--salt-size-base) * 2); - width: 100%; -} - -/* Pseudo-class applied to the root element on focus */ -.saltTrackerStep:focus-visible { - outline-style: var(--salt-focused-outlineStyle); - outline-width: var(--salt-focused-outlineWidth); - outline-color: var(--salt-focused-outlineColor); - outline-offset: var(--salt-focused-outlineOffset); -} - -.saltTrackerStep .saltTrackerStep-body { - width: 100%; - display: flex; - align-items: center; - flex-direction: column; -} - -.saltTrackerStep.saltTrackerStep-status-warning { - --trackerStep-icon-color: var(--trackerStep-icon-color-warning); -} - -.saltTrackerStep.saltTrackerStep-status-error { - --trackerStep-icon-color: var(--trackerStep-icon-color-error); -} - -.saltTrackerStep.saltTrackerStep-active { - --trackerStep-icon-color: var(--trackerStep-icon-color-active); -} - -.saltTrackerStep.saltTrackerStep-stage-completed { - --trackerStep-icon-color: var(--trackerStep-icon-color-completed); -} diff --git a/packages/lab/src/stepped-tracker/TrackerStep/TrackerStep.tsx b/packages/lab/src/stepped-tracker/TrackerStep/TrackerStep.tsx deleted file mode 100644 index 67dab321702..00000000000 --- a/packages/lab/src/stepped-tracker/TrackerStep/TrackerStep.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import { type ValidationStatus, makePrefixer, useIcon } from "@salt-ds/core"; -import { useComponentCssInjection } from "@salt-ds/styles"; -import { useWindow } from "@salt-ds/window"; -import { clsx } from "clsx"; -import { type ComponentPropsWithoutRef, forwardRef, useEffect } from "react"; -import { TrackerConnector } from "../TrackerConnector"; - -import { - useSteppedTrackerContext, - useTrackerStepContext, -} from "../SteppedTrackerContext"; - -import trackerStepCss from "./TrackerStep.css"; - -const withBaseName = makePrefixer("saltTrackerStep"); - -type StageOptions = "pending" | "completed"; -type StatusOptions = Extract; - -export interface TrackerStepProps extends ComponentPropsWithoutRef<"li"> { - /** - * The stage of the step: "pending" or "completed" (note, "active" is derived from "activeStep" in parent SteppedTracker component) - */ - stage?: StageOptions; - /** - * The status of the step: warning or error - * - * If the stage is completed or active, the status prop will be ignored - */ - status?: StatusOptions; -} - -const useCheckWithinSteppedTracker = (isWithinSteppedTracker: boolean) => { - useEffect(() => { - if (process.env.NODE_ENV !== "production") { - if (!isWithinSteppedTracker) { - console.error( - "The TrackerStep component must be placed within a SteppedTracker component", - ); - } - } - }, [isWithinSteppedTracker]); -}; - -const parseIconName = ({ - stage, - status, - active, -}: { - stage: StageOptions; - status?: StatusOptions; - active: boolean; -}) => { - if (stage === "completed") return "completed"; - if (active) return "active"; - if (status) return status; - return stage; -}; - -export const TrackerStep = forwardRef( - function TrackerStep(props, ref) { - const { - stage = "pending", - status, - style, - className, - children, - ...restProps - } = props; - - const targetWindow = useWindow(); - useComponentCssInjection({ - testId: "salt-tracker-step", - css: trackerStepCss, - window: targetWindow, - }); - const { ErrorIcon, WarningIcon, CompletedIcon, ActiveIcon, PendingIcon } = - useIcon(); - const { activeStep, totalSteps, isWithinSteppedTracker } = - useSteppedTrackerContext(); - const stepNumber = useTrackerStepContext(); - - useCheckWithinSteppedTracker(isWithinSteppedTracker); - - const isActive = activeStep === stepNumber; - const iconName = parseIconName({ stage, status, active: isActive }); - const iconMap = { - pending: PendingIcon, - active: ActiveIcon, - completed: CompletedIcon, - warning: WarningIcon, - error: ErrorIcon, - }; - - const Icon = iconMap[iconName]; - const connectorState = activeStep > stepNumber ? "active" : "default"; - const hasConnector = stepNumber < totalSteps - 1; - - const innerStyle = { - ...style, - "--saltTrackerStep-width": `${100 / totalSteps}%`, - }; - - return ( -
  • - - {hasConnector && } -
    {children}
    -
  • - ); - }, -); diff --git a/packages/lab/src/stepped-tracker/TrackerStep/index.ts b/packages/lab/src/stepped-tracker/TrackerStep/index.ts deleted file mode 100644 index 9d5bb908596..00000000000 --- a/packages/lab/src/stepped-tracker/TrackerStep/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { TrackerStep } from "./TrackerStep"; -export type { TrackerStepProps } from "./TrackerStep"; diff --git a/packages/lab/src/stepped-tracker/index.ts b/packages/lab/src/stepped-tracker/index.ts index 2c670a149f2..4df204a962d 100644 --- a/packages/lab/src/stepped-tracker/index.ts +++ b/packages/lab/src/stepped-tracker/index.ts @@ -1,3 +1,8 @@ export * from "./SteppedTracker"; -export * from "./TrackerStep"; -export * from "./StepLabel"; +export * from "./SteppedTracker.types"; + +export * from "./Step"; +export * from "./Step.types"; + +export * from "./useStepReducer"; +export * from "./stepReducer.types"; diff --git a/packages/lab/src/stepped-tracker/stepReducer.ts b/packages/lab/src/stepped-tracker/stepReducer.ts new file mode 100644 index 00000000000..b31ad859cfb --- /dev/null +++ b/packages/lab/src/stepped-tracker/stepReducer.ts @@ -0,0 +1,124 @@ +import type { StepReducerAction, StepReducerState } from "./stepReducer.types"; +import { assignSteps, autoStageSteps, flattenSteps, resetSteps } from "./utils"; + +export default function stepReducer( + state: StepReducerState, + action: StepReducerAction, +) { + if (action.type === "next") { + if (state.activeStep?.status === "error") { + return state; + } + + const steps = resetSteps(state.steps); + const flatSteps = flattenSteps(steps); + + if (state.nextStep) { + const activeStepIndex = state.activeStepIndex + 1; + const activeStep = flatSteps[activeStepIndex]; + const previousStep = flatSteps[activeStepIndex - 1] || null; + const nextStep = flatSteps[activeStepIndex + 1] || null; + + if (activeStep) { + activeStep.stage = "active"; + } + + return { + steps: autoStageSteps(steps), + flatSteps, + activeStepIndex, + activeStep, + previousStep, + nextStep, + started: true, + ended: false, + }; + } + + const activeStepIndex = flatSteps.length; + const previousStep = flatSteps.at(-1); + const activeStep = null; + const nextStep = null; + + return { + steps: assignSteps(steps, "completed"), + flatSteps, + activeStepIndex, + activeStep, + previousStep, + nextStep, + started: true, + ended: true, + } as StepReducerState; + } + + if (action.type === "previous") { + if (state.activeStep?.status === "error") { + return state; + } + + const steps = resetSteps(state.steps); + const flatSteps = flattenSteps(steps); + + if (state.previousStep) { + const activeStepIndex = state.activeStepIndex - 1; + const activeStep = flatSteps[activeStepIndex]; + const previousStep = flatSteps[activeStepIndex - 1] || null; + const nextStep = flatSteps[activeStepIndex + 1] || null; + + if (activeStep) { + activeStep.stage = "active"; + } + + return { + steps: autoStageSteps(steps), + flatSteps, + activeStepIndex, + activeStep, + previousStep, + nextStep, + started: true, + ended: false, + } as StepReducerState; + } + + const activeStepIndex = -1; + const activeStep = null; + const previousStep = null; + const nextStep = flatSteps.at(0); + + return { + steps: assignSteps(steps, "pending"), + flatSteps, + activeStepIndex, + activeStep, + previousStep, + nextStep, + ended: false, + started: false, + } as StepReducerState; + } + + if (action.type === "error") { + if (state.activeStep) { + state.activeStep.status = "error"; + return { ...state }; + } + } + + if (action.type === "warning") { + if (state.activeStep) { + state.activeStep.status = "warning"; + return { ...state }; + } + } + + if (action.type === "clear") { + if (state.activeStep) { + state.activeStep.status = undefined; + return { ...state }; + } + } + + return state; +} diff --git a/packages/lab/src/stepped-tracker/stepReducer.types.ts b/packages/lab/src/stepped-tracker/stepReducer.types.ts new file mode 100644 index 00000000000..cf60ccdee47 --- /dev/null +++ b/packages/lab/src/stepped-tracker/stepReducer.types.ts @@ -0,0 +1,23 @@ +import type { StepRecord } from "."; + +export interface StepReducerState { + steps: StepRecord[]; + flatSteps: StepRecord[]; + activeStep: StepRecord | null; + previousStep: StepRecord | null; + nextStep: StepRecord | null; + activeStepIndex: number; + started: boolean; + ended: boolean; +} + +export type StepReducerAction = + | { type: "next" } + | { type: "previous" } + | { type: "error" } + | { type: "warning" } + | { type: "clear" }; + +export interface StepReducerOptions { + activeStepId?: string; +} diff --git a/packages/lab/src/stepped-tracker/useStepReducer.ts b/packages/lab/src/stepped-tracker/useStepReducer.ts new file mode 100644 index 00000000000..68b2312ffa7 --- /dev/null +++ b/packages/lab/src/stepped-tracker/useStepReducer.ts @@ -0,0 +1,19 @@ +import { useMemo, useReducer } from "react"; + +import stepReducer from "./stepReducer"; +import { initStepReducerState } from "./utils"; + +import type { StepRecord } from "./Step.types"; +import type { StepReducerOptions } from "./stepReducer.types"; + +export function useStepReducer( + initialSteps: StepRecord[], + options?: StepReducerOptions, +) { + const state = useMemo( + () => initStepReducerState(initialSteps, options), + [initialSteps, options], + ); + + return useReducer(stepReducer, state); +} diff --git a/packages/lab/src/stepped-tracker/utils.spec.tsx b/packages/lab/src/stepped-tracker/utils.spec.tsx new file mode 100644 index 00000000000..de8565dc978 --- /dev/null +++ b/packages/lab/src/stepped-tracker/utils.spec.tsx @@ -0,0 +1,502 @@ +import { describe, expect, it } from "vitest"; + +import { + assignSteps, + autoStageSteps, + flattenSteps, + initStepReducerState, + resetSteps, +} from "./utils"; + +import type { StepRecord } from "./Step.types"; + +describe("SteppedTracker > utils.ts", () => { + describe("resetSteps", () => { + it("should set the stage of all steps to undefined", () => { + const steps: StepRecord[] = [ + { id: "1", stage: "completed" }, + { id: "2", stage: "active" }, + { id: "3", stage: "pending" }, + ]; + + const result = resetSteps(steps); + + expect(result).toEqual([{ id: "1" }, { id: "2" }, { id: "3" }]); + }); + it("should set the stage of nested steps to undefined", () => { + const steps: StepRecord[] = [ + { + id: "1", + stage: "completed", + substeps: [ + { + id: "1.1", + stage: "completed", + substeps: [ + { id: "1.1.1", stage: "completed" }, + { id: "1.1.2", stage: "completed" }, + ], + }, + { + id: "1.2", + stage: "inprogress", + substeps: [ + { id: "1.2.1", stage: "completed" }, + { id: "1.2.2", stage: "active" }, + { id: "1.2.3", stage: "pending" }, + ], + }, + { id: "1.3", stage: "pending" }, + ], + }, + { id: "2", stage: "pending" }, + { id: "3", stage: "pending" }, + ]; + + const result = resetSteps(steps); + + expect(result).toEqual([ + { + id: "1", + substeps: [ + { + id: "1.1", + substeps: [{ id: "1.1.1" }, { id: "1.1.2" }], + }, + { + id: "1.2", + substeps: [{ id: "1.2.1" }, { id: "1.2.2" }, { id: "1.2.3" }], + }, + { id: "1.3" }, + ], + }, + { id: "2" }, + { id: "3" }, + ]); + }); + }); + describe("assignSteps", () => { + it("should assign an array of steps to a stage (completed)", () => { + const steps: StepRecord[] = [{ id: "1" }, { id: "2" }, { id: "3" }]; + + const result = assignSteps(steps, "completed"); + + expect(result).toEqual([ + { id: "1", stage: "completed" }, + { id: "2", stage: "completed" }, + { id: "3", stage: "completed" }, + ]); + }); + it("should assign an array of steps to a stage (pending)", () => { + const steps: StepRecord[] = [{ id: "1" }, { id: "2" }, { id: "3" }]; + + const result = assignSteps(steps, "pending"); + + expect(result).toEqual([ + { id: "1", stage: "pending" }, + { id: "2", stage: "pending" }, + { id: "3", stage: "pending" }, + ]); + }); + it("should assign an array of nested steps to a stage", () => { + const steps: StepRecord[] = [ + { id: "1" }, + { + id: "2", + substeps: [{ id: "2.1" }, { id: "2.2" }], + }, + { + id: "3", + substeps: [ + { + id: "3.1", + substeps: [{ id: "3.1.1" }], + }, + ], + }, + ]; + + const result = assignSteps(steps, "completed"); + + expect(result).toEqual([ + { id: "1", stage: "completed" }, + { + id: "2", + stage: "completed", + substeps: [ + { id: "2.1", stage: "completed" }, + { id: "2.2", stage: "completed" }, + ], + }, + { + id: "3", + stage: "completed", + substeps: [ + { + id: "3.1", + stage: "completed", + substeps: [{ id: "3.1.1", stage: "completed" }], + }, + ], + }, + ]); + }); + }); + describe("autoStageSteps", () => { + it("should return pending if no active, nor completed step", () => { + const config: StepRecord[] = [{ id: "1" }, { id: "2" }, { id: "3" }]; + + expect(autoStageSteps(config)).toEqual([ + { id: "1", stage: "pending" }, + { id: "2", stage: "pending" }, + { id: "3", stage: "pending" }, + ]); + }); + it("should set steps before active step to completed", () => { + const config: StepRecord[] = [ + { id: "1" }, + { id: "2" }, + { id: "3", stage: "active" }, + ]; + + const result = autoStageSteps(config); + + expect(result[0]).toHaveProperty("stage", "completed"); + expect(result[1]).toHaveProperty("stage", "completed"); + expect(result[2]).toHaveProperty("stage", "active"); + }); + it("should set steps after active step to pending", () => { + const config: StepRecord[] = [ + { id: "1", stage: "active" }, + { id: "2" }, + { id: "3" }, + ]; + + const result = autoStageSteps(config); + + expect(result[0]).toHaveProperty("stage", "active"); + expect(result[1]).toHaveProperty("stage", "pending"); + expect(result[2]).toHaveProperty("stage", "pending"); + }); + it("should set steps with active substeps to inprogress on top step", () => { + const config: StepRecord[] = [ + { + id: "1", + substeps: [ + { id: "1.1" }, + { id: "1.2", stage: "active" }, + { id: "1.3" }, + ], + }, + { id: "2" }, + { id: "3" }, + ]; + + const result = autoStageSteps(config); + + const expected: StepRecord[] = [ + { + id: "1", + stage: "inprogress", + substeps: [ + { id: "1.1", stage: "completed" }, + { id: "1.2", stage: "active" }, + { id: "1.3", stage: "pending" }, + ], + }, + { id: "2", stage: "pending" }, + { id: "3", stage: "pending" }, + ]; + + expect(result).toEqual(expected); + }); + it("should set steps with active substeps to inprogress on middle step", () => { + const config: StepRecord[] = [ + { id: "1" }, + { + id: "2", + substeps: [ + { id: "2.1" }, + { id: "2.2" }, + { id: "2.3", stage: "active" }, + ], + }, + { id: "3" }, + ]; + + const result = autoStageSteps(config); + + const expected: StepRecord[] = [ + { id: "1", stage: "completed" }, + { + id: "2", + stage: "inprogress", + substeps: [ + { id: "2.1", stage: "completed" }, + { id: "2.2", stage: "completed" }, + { id: "2.3", stage: "active" }, + ], + }, + { id: "3", stage: "pending" }, + ]; + + expect(result).toEqual(expected); + }); + it("should set steps with active substeps to inprogress on middle step with substeps above", () => { + const config: StepRecord[] = [ + { + id: "1", + substeps: [{ id: "1.1" }, { id: "1.2" }, { id: "1.3" }], + }, + { + id: "2", + substeps: [ + { id: "2.1" }, + { id: "2.2", stage: "active" }, + { id: "2.3" }, + ], + }, + { id: "3" }, + ]; + + const expected: StepRecord[] = [ + { + id: "1", + stage: "completed", + substeps: [ + { id: "1.1", stage: "completed" }, + { id: "1.2", stage: "completed" }, + { id: "1.3", stage: "completed" }, + ], + }, + { + id: "2", + stage: "inprogress", + substeps: [ + { id: "2.1", stage: "completed" }, + { id: "2.2", stage: "active" }, + { id: "2.3", stage: "pending" }, + ], + }, + { id: "3", stage: "pending" }, + ]; + + expect(autoStageSteps(config)).toEqual(expected); + }); + it("should set steps with active substeps to inprogress on middle step with substeps above", () => { + const config: StepRecord[] = [ + { + id: "1", + substeps: [{ id: "1.1" }, { id: "1.2" }, { id: "1.3" }], + }, + { + id: "2", + substeps: [ + { id: "2.1" }, + { + id: "2.2", + substeps: [ + { id: "2.2.1" }, + { id: "2.2.2", stage: "active" }, + { id: "2.2.3" }, + ], + }, + { id: "2.3" }, + ], + }, + { id: "3" }, + ]; + + const expected: StepRecord[] = [ + { + id: "1", + stage: "completed", + substeps: [ + { id: "1.1", stage: "completed" }, + { id: "1.2", stage: "completed" }, + { id: "1.3", stage: "completed" }, + ], + }, + { + id: "2", + stage: "inprogress", + substeps: [ + { id: "2.1", stage: "completed" }, + { + id: "2.2", + stage: "inprogress", + substeps: [ + { id: "2.2.1", stage: "completed" }, + { id: "2.2.2", stage: "active" }, + { id: "2.2.3", stage: "pending" }, + ], + }, + { id: "2.3", stage: "pending" }, + ], + }, + { id: "3", stage: "pending" }, + ]; + + expect(autoStageSteps(config)).toEqual(expected); + }); + }); + describe("flattenSteps", () => { + it("should return a the same array if no substeps", () => { + const steps: StepRecord[] = [{ id: "1" }, { id: "2" }, { id: "3" }]; + + expect(flattenSteps(steps)).toEqual(steps); + }); + it("should return flattenSteps array of steps (depth 1)", () => { + const steps: StepRecord[] = [ + { id: "1" }, + { + id: "2", + substeps: [{ id: "2.1" }, { id: "2.2", stage: "active" }], + }, + { id: "3" }, + ]; + + expect(flattenSteps(steps)).toEqual([ + { id: "1" }, + { id: "2.1" }, + { id: "2.2", stage: "active" }, + { id: "3" }, + ]); + }); + it("should return flattenSteps array of steps (depth 2)", () => { + const steps: StepRecord[] = [ + { id: "1" }, + { + id: "2", + substeps: [ + { id: "2.1" }, + { + id: "2.2", + substeps: [{ id: "2.2.1" }, { id: "2.2.2", stage: "active" }], + }, + ], + }, + { id: "3" }, + ]; + + expect(flattenSteps(steps)).toEqual([ + { id: "1" }, + { id: "2.1" }, + { id: "2.2.1" }, + { id: "2.2.2", stage: "active" }, + { id: "3" }, + ]); + }); + }); + describe("initStepReducerState", () => { + it("should work when active stage is in the beginning of initialSteps ", () => { + const initialSteps: StepRecord[] = [ + { id: "1", stage: "active" }, + { id: "2" }, + { id: "3" }, + ]; + + const state = initStepReducerState(initialSteps); + + expect(state.activeStepIndex).toEqual(0); + expect(state.activeStep).toEqual(state.steps[0]); + expect(state.nextStep).toEqual(state.steps[1]); + expect(state.previousStep).toEqual(null); + + expect(state.steps[0]).toHaveProperty("stage", "active"); + expect(state.steps[1]).toHaveProperty("stage", "pending"); + expect(state.steps[2]).toHaveProperty("stage", "pending"); + + expect(state.started).toEqual(true); + expect(state.ended).toEqual(false); + }); + + it("should work when active stage is in the middle of initialSteps ", () => { + const initialSteps: StepRecord[] = [ + { id: "1" }, + { id: "2", stage: "active" }, + { id: "3" }, + ]; + + const state = initStepReducerState(initialSteps); + + expect(state.activeStepIndex).toEqual(1); + expect(state.activeStep).toEqual(state.steps[1]); + expect(state.nextStep).toEqual(state.steps[2]); + expect(state.previousStep).toEqual(state.steps[0]); + + expect(state.steps[0]).toHaveProperty("stage", "completed"); + expect(state.steps[1]).toHaveProperty("stage", "active"); + expect(state.steps[2]).toHaveProperty("stage", "pending"); + + expect(state.started).toEqual(true); + expect(state.ended).toEqual(false); + }); + + it("should work when active stage is in the middle of initialSteps ", () => { + const initialSteps: StepRecord[] = [ + { id: "1" }, + { id: "2" }, + { id: "3", stage: "active" }, + ]; + + const state = initStepReducerState(initialSteps); + + expect(state.activeStepIndex).toEqual(2); + expect(state.activeStep).toEqual(state.steps[2]); + expect(state.nextStep).toEqual(null); + expect(state.previousStep).toEqual(state.steps[1]); + + expect(state.steps[0]).toHaveProperty("stage", "completed"); + expect(state.steps[1]).toHaveProperty("stage", "completed"); + expect(state.steps[2]).toHaveProperty("stage", "active"); + + expect(state.started).toEqual(true); + expect(state.ended).toEqual(false); + }); + + it("should work when no active stage set", () => { + const initialSteps: StepRecord[] = [ + { id: "1" }, + { id: "2" }, + { id: "3" }, + ]; + + const state = initStepReducerState(initialSteps); + + expect(state.activeStepIndex).toEqual(-1); + expect(state.activeStep).toEqual(null); + expect(state.nextStep).toEqual(state.steps[0]); + expect(state.previousStep).toEqual(null); + + expect(state.steps[0]).toHaveProperty("stage", "pending"); + expect(state.steps[1]).toHaveProperty("stage", "pending"); + expect(state.steps[2]).toHaveProperty("stage", "pending"); + + expect(state.started).toEqual(false); + expect(state.ended).toEqual(false); + }); + + it("should work when no active stage set, but first is completed ", () => { + const initialSteps: StepRecord[] = [ + { id: "1", stage: "completed" }, + { id: "2", stage: "completed" }, + { id: "3", stage: "completed" }, + ]; + + const state = initStepReducerState(initialSteps); + + expect(state.activeStepIndex).toEqual(state.flatSteps.length); + expect(state.activeStep).toEqual(null); + expect(state.nextStep).toEqual(null); + expect(state.previousStep).toEqual(state.steps[2]); + + expect(state.steps[0]).toHaveProperty("stage", "completed"); + expect(state.steps[1]).toHaveProperty("stage", "completed"); + expect(state.steps[2]).toHaveProperty("stage", "completed"); + + expect(state.started).toEqual(true); + expect(state.ended).toEqual(true); + }); + }); +}); diff --git a/packages/lab/src/stepped-tracker/utils.ts b/packages/lab/src/stepped-tracker/utils.ts new file mode 100644 index 00000000000..0084ff4a338 --- /dev/null +++ b/packages/lab/src/stepped-tracker/utils.ts @@ -0,0 +1,117 @@ +import type { StepRecord, StepStage } from "./Step.types"; +import type { StepReducerOptions, StepReducerState } from "./stepReducer.types"; + +export function assignSteps( + steps: StepRecord[], + stage?: StepStage, +): StepRecord[] { + return steps.map((step) => { + step.stage = stage; + if (step.substeps) { + step.substeps = assignSteps(step.substeps, stage); + } + + return step; + }); +} + +export function resetSteps(steps: StepRecord[]): StepRecord[] { + return assignSteps(steps, undefined); +} + +export function autoStageSteps( + steps: StepRecord[], + options?: StepReducerOptions, +): StepRecord[] { + function autoStageHelper(steps: StepRecord[]): StepRecord[] | null { + const pivotIndex = steps.findIndex( + (step) => + (step?.id && + options?.activeStepId && + step.id === options.activeStepId) || + step.stage === "active" || + step.stage === "inprogress", + ); + + if (pivotIndex !== -1) { + const activeStep = steps[pivotIndex]; + + activeStep.stage ||= "active"; + + const previousSteps = assignSteps( + steps.slice(0, pivotIndex), + "completed", + ); + const nextSteps = assignSteps(steps.slice(pivotIndex + 1), "pending"); + + return [...previousSteps, activeStep, ...nextSteps] as StepRecord[]; + } + + return steps.reduce( + (acc, step, index) => { + if (step.substeps) { + const substeps = autoStageHelper(step.substeps); + + if (substeps) { + steps[index].substeps = substeps; + steps[index].stage = "inprogress"; + + return autoStageHelper(steps); + } + } + + return acc; + }, + null as StepRecord[] | null, + ); + } + + return ( + autoStageHelper(steps) || assignSteps(steps, steps[0].stage || "pending") + ); +} + +export function flattenSteps(steps: StepRecord[]): StepRecord[] { + return steps.reduce((acc, step) => { + if (step.substeps) { + acc.push(...flattenSteps(step.substeps)); + + return acc; + } + + acc.push(step); + + return acc; + }, [] as StepRecord[]); +} + +export function initStepReducerState( + initialSteps: StepRecord[], + options?: StepReducerOptions, +) { + const steps = autoStageSteps(initialSteps, options); + const flatSteps = flattenSteps(steps); + const started = !flatSteps.every((step) => step.stage === "pending"); + const ended = flatSteps.every((step) => step.stage === "completed"); + + let activeStepIndex = flatSteps.findIndex((step) => step.stage === "active"); + + if (activeStepIndex === -1 && ended) { + activeStepIndex = flatSteps.length; + } + + const activeStep = flatSteps[activeStepIndex] || null; + const previousStep = flatSteps[activeStepIndex - 1] || null; + const nextStep = flatSteps[activeStepIndex + 1] || null; + + return { + steps, + flatSteps, + activeStep, + previousStep, + nextStep, + activeStepIndex, + ended, + started, + } as StepReducerState; +} diff --git a/packages/lab/stories/stepped-tracker/stepped-tracker.qa.stories.tsx b/packages/lab/stories/stepped-tracker/stepped-tracker.qa.stories.tsx index aaaebbff7fe..78da652377c 100644 --- a/packages/lab/stories/stepped-tracker/stepped-tracker.qa.stories.tsx +++ b/packages/lab/stories/stepped-tracker/stepped-tracker.qa.stories.tsx @@ -1,164 +1,222 @@ import { StackLayout } from "@salt-ds/core"; -import { StepLabel, SteppedTracker, TrackerStep } from "@salt-ds/lab"; -import type { Meta, StoryFn } from "@storybook/react"; +import { Step, SteppedTracker } from "@salt-ds/lab"; import { QAContainer, type QAContainerProps } from "docs/components"; +import type { Meta, StoryFn } from "@storybook/react"; + export default { - title: "Lab/Stepped Tracker/Stepped Tracker QA", + title: "Lab/SteppedTracker/SteppedTracker QA", component: SteppedTracker, - subcomponents: { TrackerStep, StepLabel }, + subcomponents: { Step }, } as Meta; -export const Basic: StoryFn = (props) => { +export const Horizontal: StoryFn = (props) => { return ( - + - - - Step One - - - Step Two - - - Step Three - - - Step Four - - - - - Step One - - - Step Two - - - Step Three - - - Step Four - - - - - Completed with warning - - - Active with warning - - - Completed with error - - - Completed - - - - - Completed and active - - - Warning - - - Error - - - Default - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +Horizontal.parameters = { + chromatic: { disableSnapshot: false }, +}; + +export const HorizontalVariations: StoryFn = (props) => { + return ( + + + + + + + + + + ); }; -Basic.parameters = { +HorizontalVariations.parameters = { chromatic: { disableSnapshot: false }, }; export const Vertical: StoryFn = (props) => { return ( - + - - - Step One - - - Step Two - - - Step Three - - - Step Four - - - - - Step One - - - Step Two - - - Step Three - - - Step Four - - - - - Step One - - - Step Two - - - Step Three - - - Step Four - - - - - Completed - - - Warning - - - Error - - - Default - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); }; -Basic.parameters = { +Vertical.parameters = { chromatic: { disableSnapshot: false }, }; + +export const VerticalVariations: StoryFn = (props) => { + return ( + + + + + + + + + + + + + + ); +}; + +VerticalVariations.parameters = { + chromatic: { disableSnapshot: false }, +}; + +export const VerticalNesting: StoryFn = (props) => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/packages/lab/stories/stepped-tracker/stepped-tracker.stories.tsx b/packages/lab/stories/stepped-tracker/stepped-tracker.stories.tsx index 89fcee9158a..ba6ca1e7f37 100644 --- a/packages/lab/stories/stepped-tracker/stepped-tracker.stories.tsx +++ b/packages/lab/stories/stepped-tracker/stepped-tracker.stories.tsx @@ -1,324 +1,355 @@ -import { useState } from "react"; - -import { Button, FlexLayout, StackLayout, Tooltip } from "@salt-ds/core"; -import { RefreshIcon } from "@salt-ds/icons"; -import { StepLabel, SteppedTracker, TrackerStep } from "@salt-ds/lab"; +import { + Button, + FlexLayout, + SegmentedButtonGroup, + StackLayout, +} from "@salt-ds/core"; +import { Step, SteppedTracker, useStepReducer } from "@salt-ds/lab"; import type { Meta, StoryFn } from "@storybook/react"; export default { - title: "Lab/Stepped Tracker", + title: "Lab/SteppedTracker", component: SteppedTracker, - subcomponents: { TrackerStep, StepLabel }, + subcomponents: { Step }, } as Meta; -interface Step { - label: string; - state: "pending" | "completed"; -} - -type Steps = Step[]; - -const sampleSteps: Steps = [ - { - label: "Step One", - state: "pending", - }, - { - label: "Step Two", - state: "pending", - }, - { - label: "Step Three", - state: "pending", - }, - { - label: "Step Four", - state: "pending", - }, -]; - -export const Basic: StoryFn = () => { +export const Horizontal: StoryFn = () => { return ( - - - Step One - - - Step Two - - - Step Three - - - Step Four - + + + + - - - Step One - - - Step Two - - - Step Three - - - Step Four - - - - - Step One - - - Step Two - - - Step Three - - - Step Four - + + ); +}; + +export const HorizontalVariations: StoryFn = () => { + return ( + + + + + + + + + ); }; -export const Status: StoryFn = () => { +export const HorizontalLongText: StoryFn = () => { return ( -
    - - - Completed - - - Active - - - Warning - - - Error - - - Default - + + + + -
    + ); }; -export const SingleVertical: StoryFn = () => { +export const HorizontalInteractiveUsingSteppedReducer: StoryFn< + typeof SteppedTracker +> = () => { + const [state, dispatch] = useStepReducer([ + { key: "step-1", label: "Step 1" }, + { key: "step-2", label: "Step 2", stage: "active" }, + { key: "step-3", label: "Step 3" }, + ]); + return ( - - - Step One - - - Step Two - - - Step Three - - - Step Four - - + + + {state.steps.map((step) => ( + + ))} + + + {state.started && ( + + )} + {!state.ended && ( + + )} + + ); }; -export const BasicVertical: StoryFn = () => { +export const Vertical: StoryFn = () => { return ( - - - Step One - - - Step Two - - - Step Three - - - Step Four - - - - - Step One - - - Step Two - - - Step Three - - - Step Four - - - - - Step One - - - Step Two - - - Step Three - - - Step Four - + + + + ); }; -export const AutoProgress: StoryFn = () => { - const [activeStep, setActiveStep] = useState(0); - const [steps, setSteps] = useState(sampleSteps); - const totalSteps = steps.length; - - const onComplete = () => { - if (activeStep < totalSteps - 1) { - setActiveStep((old) => old + 1); - } - - setSteps((old) => - old.map((step, i) => - i === activeStep - ? { - ...step, - state: "completed", - } - : step, - ), - ); - }; - - const onRefresh = () => { - setActiveStep(0); - setSteps(sampleSteps); - }; - +export const VerticalVariations: StoryFn = () => { return ( - - - {steps.map(({ label, state }) => ( - - {label} - - ))} + + + + + + + + + - - - - - - ); }; -export const WrappingLabel: StoryFn = () => { +export const VerticalLongText: StoryFn = () => { return ( - - - Step One - - - - Step Two: I am a label that wraps on smaller screen sizes - - - - - Step Three: I am a label that wraps on smaller screen sizes - - - - Step Four - + + + + ); }; -export const NonSequentialProgress: StoryFn = () => { - const [activeStep, setActiveStep] = useState(0); - const [steps, setSteps] = useState(sampleSteps); - const totalSteps = steps.length; +export const VerticalDepth1: StoryFn = () => { + return ( + + + + + + + + + + + - const onNext = () => { - setActiveStep((old) => (old < steps.length - 1 ? old + 1 : old)); - }; + + + + + + + + ); +}; - const onPrevious = () => { - setActiveStep((old) => (old > 0 ? old - 1 : old)); - }; +export const VerticalDepth2 = () => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; - const onToggleStep = () => { - setSteps((old) => - old.map((step, i) => - i === activeStep - ? { - ...step, - state: step.state === "pending" ? "completed" : "pending", - } - : step, - ), - ); - }; +export const VerticalInteractiveUsingSteppedReducer: StoryFn< + typeof SteppedTracker +> = () => { + const [state, dispatch] = useStepReducer( + [ + { + id: "step-1", + label: "Step 1", + defaultExpanded: true, + substeps: [ + { id: "step-1-1", label: "Step 1.1" }, + { id: "step-1-2", label: "Step 1.2" }, + { + id: "step-1-3", + label: "Step 1.3", + defaultExpanded: true, + substeps: [ + { id: "step-1-3-1", label: "Step 1.3.1" }, + { id: "step-1-3-2", label: "Step 1.3.2" }, + { + id: "step-1-3-3", + label: "Step 1.3.3", + description: "This is just a description text", + }, + ], + }, + { id: "step-1-4", label: "Step 1.4" }, + ], + }, + { id: "step-2", label: "Step 2" }, + { id: "step-3", label: "Step 3" }, + ], + { activeStepId: "step-1-3-2" }, + ); return ( - - - {steps.map(({ label, state }) => ( - - {label} - + + + {state.steps.map((step) => ( + ))} - - - - - + + {state.started && ( + + )} + {!state.ended && ( + + )} + + + {state.started && !state.ended && ( + <> + + + + + )} + + + ); +}; + +export const BareBones: StoryFn = () => { + return ( + + + + + + ); }; diff --git a/site/docs/components/stepped-tracker/accessibility.mdx b/site/docs/components/stepped-tracker/accessibility.mdx index c3d732734d1..22ba7ab8fad 100644 --- a/site/docs/components/stepped-tracker/accessibility.mdx +++ b/site/docs/components/stepped-tracker/accessibility.mdx @@ -8,4 +8,29 @@ data: $ref: ./#/data --- -Stepped trackers use `aria-current="step"` to indicate the current active step. +## Keyboard interactions + +Keyboard interactions apply to only Steppers with steps with nested substeps. +In those cases the parent step renders automatically a button that allows +for expanding or collapsing the substeps. + + + + +- Moves the focus down to the next expand button. +- By default all steps with substeps are collapsed, meaning only top level + expand buttons will be focusable. Initially the top most step will be focused. +- If the substeps contain more substeps, the focus will move to the inside steps only if the parent is expanded. + + + + +- Moves the focus up to the previous expand button. + + + + +- Applicable to only steps with substeps. Those steps are in the collapsed state by default. Using this action on an the arrow button expands or collapses the step. + + + diff --git a/site/docs/components/stepped-tracker/examples.mdx b/site/docs/components/stepped-tracker/examples.mdx index b580b902a16..94fa81603ad 100644 --- a/site/docs/components/stepped-tracker/examples.mdx +++ b/site/docs/components/stepped-tracker/examples.mdx @@ -1,65 +1,81 @@ --- -title: - $ref: ./#/title -layout: DetailComponent -sidebar: - exclude: true -data: - $ref: ./#/data + title: + $ref: ./#/title + layout: DetailComponent + sidebar: + exclude: true + data: + $ref: ./#/data --- - - ## Basic -`SteppedTracker` contains multiple `TrackerStep` child components. Each `TrackerStep` indicates its current status through its icon and color. In addition, the connectors indicate the current active step and the user’s progress through the process. - -You can add a label to the `TrackerStep` using the `StepLabel` component. - - - - - -## Stage and Status + -`SteppedTracker` supports multiple `stage` and `status` values for each `TrackerStep` child component. +The `SteppedTracker` component accepts `Step` components as children. +You can change -Available `stage` values are `"pending"` and `"completed"`. **Note:** `"active"` is not a valid value: the active step is defined by the `activeStep` prop on the `SteppedTracker` component. + -Available `status` values are `"warning"` and `"error"`. +## Orientation -When stage and status are both set, the order of precedence for which icon is show is as follows: "completed" > "active" > "error|warning" > "pending" +### Horizontal (default) - + - +### Vertical -## Vertical + -You can vertically orientate a stepped tracker, with labels displayed on the right of the icon in the step node. +## Stage + Status -### Best practices + -Use a vertical stepped tracker when horizontal space is limited in the context of the application or when there are a large number of steps in the process – for example, more than 8. +The `Step` component changes based on the `stage` and `status` props. +In case they are both set, `status` will take precedence over `stage`. +These 2 props control various aspects of the component, such as: +icon selection, description text color and track (connector) style. - - - + -## Step progression +## Nested Steps -In normal circumstances, once the user completes a step, you should advance the active step, and change the status of the current active step in unison. + + When using the `SteppedTracker` component in vertical orientation, you can + nest `Step` components other `Step` components. When done so, the parent + `Step` renders another `SteppedTracker` and places the nested `Steps` + (children) inside of it. Additionally an expand button is added on the parent + `Step` to expand or collapse the substeps. The parent Step containing the + active Step child children are to have a stage of `inprogress`. + - +## Hook - + -## Nonsequential progress +The `SteppedTracker` and `Step` component are purely presentational components. +If you want to control the state of the `SteppedTracker` you can utilize the `useStepReducer` hook. -It may not be appropriate in some circumstances, but it's possible to control the state of steps and the active step independently if users can revisit previous steps or complete steps nonsequentially. + - + + The `useStepReducer` hook is smart! If you add `stage: 'active'` to any step, + it will automatically determine the stages of all other steps (autoStage), + both before and after the active one. Inside the state object, you can find + useful properties such as `steps`, `flatSteps`, `activeStep`, `previousStep`, + `nextStep`, `started` and `ended`. The dispatch method as in the native + useReducer hook accepts an action object with a type. Acceptable action types + are: 'next', 'previous', 'warning', 'error' and 'clear'. The warning and error + types will set the status of the active step to 'warning' or 'error'. The + clear type will reset the status of the active step to undefined. + diff --git a/site/docs/components/stepped-tracker/index.mdx b/site/docs/components/stepped-tracker/index.mdx index 0940d80c61d..b8b126f6c4d 100644 --- a/site/docs/components/stepped-tracker/index.mdx +++ b/site/docs/components/stepped-tracker/index.mdx @@ -1,7 +1,7 @@ --- -title: Stepped tracker +title: Stepped Tracker data: - description: "`SteppedTracker` visually communicates a user’s progress through a linear process. It gives the user context about where they are in the process, indicating the remaining steps and the state of all steps. It can communicate new information, errors, warnings or successful completion of a process or task." + description: "`Stepped Tracker` visually communicates a user’s progress through a linear process. It gives the user context about where they are, which steps have they completed, if any errors or warnings have occurred, and how many steps are left." sourceCodeUrl: "https://github.com/jpmorganchase/salt-ds/tree/main/packages/lab/src/stepped-tracker" package: name: "@salt-ds/lab" @@ -17,11 +17,8 @@ data: ] relatedComponents: [ - { name: "Breadcrumbs", relationship: "similarTo" }, - { name: "Pagination", relationship: "similarTo" }, - { name: "Progress", relationship: "similarTo" }, - { name: "Icon", relationship: "contains" }, - { name: "Status indicator", relationship: "contains" }, + { name: "SemanticIconProvider", relationship: "contains" }, + { name: "Accordion", relationship: "similarTo" }, ] # Leave this as is diff --git a/site/docs/components/stepped-tracker/usage.mdx b/site/docs/components/stepped-tracker/usage.mdx index 06b897068f7..c534f95335f 100644 --- a/site/docs/components/stepped-tracker/usage.mdx +++ b/site/docs/components/stepped-tracker/usage.mdx @@ -22,14 +22,14 @@ data: ## Content -Stepped tracker labels never truncate, only wrap. Therefore, ensure they're short and self-explanatory. +Step labels never truncate, only wrap. Therefore, ensure they're short and self-explanatory. ## Import To import `SteppedTracker` and related components from the lab Salt package, use: ``` -import { SteppedTracker, TrackerStep, StepLabel } from "@salt-ds/lab"; +import { SteppedTracker, Step, useStepReducer } from "@salt-ds/lab"; ``` ## Props @@ -38,10 +38,10 @@ import { SteppedTracker, TrackerStep, StepLabel } from "@salt-ds/lab"; -### `TrackerStep` +### `Step` - + -### `StepLabel` +### `useStepReducer` - + diff --git a/site/src/examples/dialog/Sizes.tsx b/site/src/examples/dialog/Sizes.tsx index d6d3f71f227..dcf3cc6eebb 100644 --- a/site/src/examples/dialog/Sizes.tsx +++ b/site/src/examples/dialog/Sizes.tsx @@ -11,7 +11,7 @@ import { StackLayout, useId, } from "@salt-ds/core"; -import { StepLabel, SteppedTracker, TrackerStep } from "@salt-ds/lab"; +import { Step, SteppedTracker } from "@salt-ds/lab"; import { type ReactElement, useState } from "react"; const SmallDialog = (): ReactElement => { @@ -206,19 +206,11 @@ const LargeDialog = (): ReactElement => { /> } endItem={ - - - Beneficiary - - - Amount - - - Account - - - Delivery - + + + + + } /> diff --git a/site/src/examples/stepped-tracker/Basic.tsx b/site/src/examples/stepped-tracker/Basic.tsx index ee11a27bbc3..ec61ffdee7d 100644 --- a/site/src/examples/stepped-tracker/Basic.tsx +++ b/site/src/examples/stepped-tracker/Basic.tsx @@ -1,73 +1,13 @@ import { StackLayout } from "@salt-ds/core"; -import { StepLabel, SteppedTracker, TrackerStep } from "@salt-ds/lab"; -import type { ReactElement } from "react"; +import { Step, SteppedTracker } from "@salt-ds/lab"; -export const Basic = (): ReactElement => { +export const Basic = () => { return ( - - - - Step One - - - Step Two - - - Step Three - - - Step Four - - - - - Step One - - - Step Two - - - Step Three - - - Step Four - - - - - Step One - - - Step Two - - - Step Three - - - Step Four - - - - - Completed - - - Active - - - Warning - - - Error - - - Default - + + + + + ); diff --git a/site/src/examples/stepped-tracker/Hook.tsx b/site/src/examples/stepped-tracker/Hook.tsx new file mode 100644 index 00000000000..9123de1867e --- /dev/null +++ b/site/src/examples/stepped-tracker/Hook.tsx @@ -0,0 +1,32 @@ +import { Button, FlexLayout } from "@salt-ds/core"; +import { StackLayout } from "@salt-ds/core"; +import { + Step, + type StepRecord, + SteppedTracker, + useStepReducer, +} from "@salt-ds/lab"; + +const initialSteps = [ + { id: "step-1", label: "Step 1" }, + { id: "step-2", label: "Step 2" }, + { id: "step-3", label: "Step 3" }, +] as StepRecord[]; + +export function Hook() { + const [state, dispatch] = useStepReducer(initialSteps); + + return ( + + + {state.steps.map((step) => ( + + ))} + + + + + + + ); +} diff --git a/site/src/examples/stepped-tracker/HookAdvanced.tsx b/site/src/examples/stepped-tracker/HookAdvanced.tsx new file mode 100644 index 00000000000..2f6185e9201 --- /dev/null +++ b/site/src/examples/stepped-tracker/HookAdvanced.tsx @@ -0,0 +1,101 @@ +import { Button, SegmentedButtonGroup, StackLayout } from "@salt-ds/core"; +import { + Step, + type StepRecord, + SteppedTracker, + useStepReducer, +} from "@salt-ds/lab"; + +const initialSteps: StepRecord[] = [ + { + id: "step-1", + label: "Step 1", + defaultExpanded: true, + substeps: [ + { id: "step-1-1", label: "Step 1.1" }, + { id: "step-1-2", label: "Step 1.2" }, + { + id: "step-1-3", + label: "Step 1.3", + defaultExpanded: true, + substeps: [ + { + id: "step-1-3-1", + label: "Step 1.3.1", + stage: "active", + }, + { id: "step-1-3-2", label: "Step 1.3.2" }, + { + id: "step-1-3-3", + label: "Step 1.3.3", + description: "This is just a description text", + }, + ], + }, + { id: "step-1-4", label: "Step 1.4" }, + ], + }, + { id: "step-2", label: "Step 2" }, + { id: "step-3", label: "Step 3" }, +]; + +export const HookAdvanced = () => { + const [state, dispatch] = useStepReducer(initialSteps); + + return ( + + + {state.steps.map((step) => ( + + ))} + + + {state.started && ( + + )} + {!state.ended && ( + + )} + + + {state.started && !state.ended && ( + <> + + + + + )} + + + ); +}; diff --git a/site/src/examples/stepped-tracker/NestedSteps.tsx b/site/src/examples/stepped-tracker/NestedSteps.tsx new file mode 100644 index 00000000000..ad9bc61954c --- /dev/null +++ b/site/src/examples/stepped-tracker/NestedSteps.tsx @@ -0,0 +1,31 @@ +import { StackLayout } from "@salt-ds/core"; +import { Step, SteppedTracker } from "@salt-ds/lab"; + +export const NestedSteps = () => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/site/src/examples/stepped-tracker/NonSequentialProgress.tsx b/site/src/examples/stepped-tracker/NonSequentialProgress.tsx deleted file mode 100644 index 52acaba45bd..00000000000 --- a/site/src/examples/stepped-tracker/NonSequentialProgress.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import { Button, FlexLayout, StackLayout } from "@salt-ds/core"; -import { StepLabel, SteppedTracker, TrackerStep } from "@salt-ds/lab"; -import { type ReactElement, useState } from "react"; - -type Step = { - label: string; - stage: "pending" | "completed"; -}; - -type Steps = Step[]; - -const sampleSteps: Steps = [ - { - label: "Step One", - stage: "pending", - }, - { - label: "Step Two", - stage: "pending", - }, - { - label: "Step Three", - stage: "pending", - }, - { - label: "Step Four", - stage: "pending", - }, -]; - -export const NonSequentialProgress = (): ReactElement => { - const [activeStep, setActiveStep] = useState(0); - const [steps, setSteps] = useState(sampleSteps); - const totalSteps = steps.length; - - const onNext = () => { - setActiveStep((old) => (old < steps.length - 1 ? old + 1 : old)); - }; - - const onPrevious = () => { - setActiveStep((old) => (old > 0 ? old - 1 : old)); - }; - - const onToggleStep = () => { - setSteps((old) => - old.map((step, i) => - i === activeStep - ? { - ...step, - stage: step.stage === "pending" ? "completed" : "pending", - } - : step, - ), - ); - }; - - return ( - - - {steps.map(({ label, stage }, key) => ( - - {label} - - ))} - - - - - - - - ); -}; diff --git a/site/src/examples/stepped-tracker/OrientationHorizontal.tsx b/site/src/examples/stepped-tracker/OrientationHorizontal.tsx new file mode 100644 index 00000000000..6dd3177e2c0 --- /dev/null +++ b/site/src/examples/stepped-tracker/OrientationHorizontal.tsx @@ -0,0 +1,14 @@ +import { StackLayout } from "@salt-ds/core"; +import { Step, SteppedTracker } from "@salt-ds/lab"; + +export const OrientationHorizontal = () => { + return ( + + + + + + + + ); +}; diff --git a/site/src/examples/stepped-tracker/OrientationVertical.tsx b/site/src/examples/stepped-tracker/OrientationVertical.tsx new file mode 100644 index 00000000000..44ad8322ed4 --- /dev/null +++ b/site/src/examples/stepped-tracker/OrientationVertical.tsx @@ -0,0 +1,14 @@ +import { StackLayout } from "@salt-ds/core"; +import { Step, SteppedTracker } from "@salt-ds/lab"; + +export const OrientationVertical = () => { + return ( + + + + + + + + ); +}; diff --git a/site/src/examples/stepped-tracker/StageAndStatus.tsx b/site/src/examples/stepped-tracker/StageAndStatus.tsx deleted file mode 100644 index da3549f5cd6..00000000000 --- a/site/src/examples/stepped-tracker/StageAndStatus.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { StackLayout } from "@salt-ds/core"; -import { StepLabel, SteppedTracker, TrackerStep } from "@salt-ds/lab"; -import type { ReactElement } from "react"; - -export const StageAndStatus = (): ReactElement => { - return ( - - - - Completed - - - Active - - - Warning - - - Error - - - Default - - - - ); -}; diff --git a/site/src/examples/stepped-tracker/StageStatus.tsx b/site/src/examples/stepped-tracker/StageStatus.tsx new file mode 100644 index 00000000000..2425d2f8e94 --- /dev/null +++ b/site/src/examples/stepped-tracker/StageStatus.tsx @@ -0,0 +1,18 @@ +import { StackLayout } from "@salt-ds/core"; +import { Step, SteppedTracker } from "@salt-ds/lab"; + +export const StageStatus = () => { + return ( + + + + + + + + + + + + ); +}; diff --git a/site/src/examples/stepped-tracker/StepProgression.tsx b/site/src/examples/stepped-tracker/StepProgression.tsx deleted file mode 100644 index 7276d821363..00000000000 --- a/site/src/examples/stepped-tracker/StepProgression.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import { Button, FlexLayout, StackLayout, Tooltip } from "@salt-ds/core"; -import { RefreshIcon } from "@salt-ds/icons"; -import { StepLabel, SteppedTracker, TrackerStep } from "@salt-ds/lab"; -import { type ReactElement, useState } from "react"; - -type Step = { - label: string; - stage: "pending" | "completed"; -}; - -type Steps = Step[]; - -const sampleSteps: Steps = [ - { - label: "Step One", - stage: "pending", - }, - { - label: "Step Two", - stage: "pending", - }, - { - label: "Step Three", - stage: "pending", - }, - { - label: "Step Four", - stage: "pending", - }, -]; - -export const StepProgression = (): ReactElement => { - const [activeStep, setActiveStep] = useState(0); - const [steps, setSteps] = useState(sampleSteps); - const totalSteps = steps.length; - - const onComplete = () => { - if (activeStep < totalSteps - 1) { - setActiveStep((old) => old + 1); - } - - setSteps((old) => - old.map((step, i) => - i === activeStep - ? { - ...step, - stage: "completed", - } - : step, - ), - ); - }; - - const onRefresh = () => { - setActiveStep(0); - setSteps(sampleSteps); - }; - - return ( - - - {steps.map(({ label, stage }, key) => ( - - {label} - - ))} - - - - - - - - - ); -}; diff --git a/site/src/examples/stepped-tracker/Vertical.tsx b/site/src/examples/stepped-tracker/Vertical.tsx deleted file mode 100644 index bde21779408..00000000000 --- a/site/src/examples/stepped-tracker/Vertical.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { StackLayout } from "@salt-ds/core"; -import { StepLabel, SteppedTracker, TrackerStep } from "@salt-ds/lab"; -import type { ReactElement } from "react"; - -export const Vertical = (): ReactElement => { - return ( - - - - Step One - - - Step Two - - - Step Three - - - Step Four - - - - - Step One - - - Step Two - - - Step Three - - - Step Four - - - - - Step One - - - Step Two - - - Step Three - - - Step Four - - - - ); -}; diff --git a/site/src/examples/stepped-tracker/index.ts b/site/src/examples/stepped-tracker/index.ts index 38ef3cc446f..a7c31504369 100644 --- a/site/src/examples/stepped-tracker/index.ts +++ b/site/src/examples/stepped-tracker/index.ts @@ -1,5 +1,7 @@ export * from "./Basic"; -export * from "./StageAndStatus"; -export * from "./Vertical"; -export * from "./StepProgression"; -export * from "./NonSequentialProgress"; +export * from "./OrientationHorizontal"; +export * from "./OrientationVertical"; +export * from "./StageStatus"; +export * from "./NestedSteps"; +export * from "./Hook"; +export * from "./HookAdvanced";