Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stepper (Stepper Tracker) - Nested Steps #4458

Closed
wants to merge 22 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
975913c
feat(stepper): nested steps with nested components
chrispcode Nov 12, 2024
0b260ea
fix(stepper): inconsistencies with density
chrispcode Nov 13, 2024
d196a6c
feat(stepper): working state
chrispcode Nov 15, 2024
4578912
feat(stepper): accessibility on single depth steps
chrispcode Nov 19, 2024
ac5af7a
feat(stepper): general improvements and documentation
chrispcode Nov 20, 2024
9075fd2
feat(stepper): wip locked stage
chrispcode Nov 21, 2024
48d2cc1
refactor(stepper): remove icon customization
chrispcode Nov 21, 2024
84deec3
feat(stepper): add support for reduced motion
chrispcode Nov 21, 2024
60126e0
docs(stepper): add more realistic example of nested steps
chrispcode Nov 21, 2024
0a0fb53
feat(stepper, semantic-icon-provider): implement sip in stepper
chrispcode Nov 27, 2024
a39f4db
fix(stepper): align icon to salt-size-base
chrispcode Nov 27, 2024
3f6da16
feat(stepper): add useStepReducer hook
chrispcode Nov 27, 2024
3176b49
docs(stepper, site): add some documentation
chrispcode Nov 27, 2024
eb07e4e
docs(stepper, site): add nesting and hook documentation
chrispcode Nov 28, 2024
d0921fc
fix(stepper): most lint issues
chrispcode Nov 28, 2024
be279f7
fix(stepper, semantic-icon-provider): lint issues
chrispcode Nov 28, 2024
a078487
fix(stepper, site): linting issues
chrispcode Nov 28, 2024
2233ee1
refactor(stepper): minor conceptual changes
chrispcode Nov 29, 2024
c5a2967
fix(stepper): typechecks
chrispcode Nov 29, 2024
ed4bcc5
docs(stepper): add changeset
chrispcode Nov 29, 2024
c2ad762
fix(stepper): remove unnecessary css
chrispcode Nov 29, 2024
566e018
fix(stepper): lint issues
chrispcode Nov 29, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
167 changes: 167 additions & 0 deletions .changeset/tender-ads-learn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
---
"@salt-ds/core": patch
"@salt-ds/lab": patch
---

Stepper, Step and useSteppedReducer

We are exited to introduce a new (sort of) salt component, the `Stepper`, currently in lab for you to test out and give feedback. It serves as a replacement to the current `SteppedTracker`.

The `Stepper` 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 `<Stepper />` is meant to be used in conjunction with it's buddies, the `<Step />` component and the `useSteppedReducer()` hook.

In it's simples form the `Stepper` can be used like so:

```tsx
import { Stepper, Step } from "@salt-ds/lab";

function Example() {
return (
<Stepper>
<Step title="Step 1" stage="completed" />
<Step title="Step 2" stage="active" />
<Step title="Step 3" stage="pending" />
</Stepper>
);
}
```

The Stepper component supports nested steps, which can be used to represent sub-steps within a step. This can be done by nesting `<Step />` components within another `<Step />` 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, Stepper } from "@salt-ds/lab";

export const NestedSteps = () => {
return (
<StackLayout style={{ minWidth: "240px" }}>
<Stepper orientation="vertical">
<Step label="Step 1" stage="completed">
<Step label="Step 1.1" stage="completed" />
</Step>
<Step label="Step 2" stage="inprogress">
<Step label="Step 2.1" stage="active" />
<Step label="Step 2.2" stage="pending">
<Step label="Step 2.2.1" stage="pending" />
<Step label="Step 2.2.2" stage="pending" />
<Step label="Step 2.2.3" stage="pending" />
</Step>
</Step>
<Step label="Step 3">
<Step label="Step 3.1" stage="pending" />
<Step label="Step 3.2" stage="pending" />
<Step label="Step 3.3" stage="pending">
<Step label="Step 3.3.1" stage="pending" />
<Step label="Step 3.3.2" stage="pending" />
<Step label="Step 3.3.3" stage="pending" />
</Step>
</Step>
</Stepper>
</StackLayout>
);
};
```

The `Stepper` 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 `useSteppedReducer()` hook comes in. It is a custom hook that helps you manage the state of a stepper component with nested steps with ease. It has a built-in algorithm that determine 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-1-3-1`), the hook will figure out the rest. This is what we call `autoStage`.

The `useSteppedReducer()` is used like so:

```tsx
import { StackLayout, SegmentedButtonGroup, Button } from "@salt-ds/core";
import { Step, Stepper, useSteppedReducer } from "@salt-ds/lab";

const initialSteps = [
{
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" },
] satisfies Step.Props[];

export const Example = () => {
const [state, dispatch] = useSteppedReducer(initialSteps);

return (
<StackLayout style={{ width: 240 }}>
<Stepper orientation="vertical">
{state.steps.map((step) => (
<Step key={step.id} {...step} />
))}
</Stepper>
<SegmentedButtonGroup>
{state.started && (
<Button
onClick={() => {
dispatch({ type: "previous" });
}}
>
Previous
</Button>
)}
{!state.ended && (
<Button
onClick={() => {
dispatch({ type: "next" });
}}
>
Next
</Button>
)}
</SegmentedButtonGroup>
<SegmentedButtonGroup>
{state.started && !state.ended && (
<>
<Button
onClick={() => {
dispatch({ type: "error" });
}}
>
Error
</Button>
<Button
onClick={() => {
dispatch({ type: "warning" });
}}
>
Warning
</Button>
<Button
onClick={() => {
dispatch({ type: "clear" });
}}
>
Clear
</Button>
</>
)}
</SegmentedButtonGroup>
</StackLayout>
);
};
```
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import {
CloseIcon,
ErrorSolidIcon,
InfoSolidIcon,
LockedIcon,
OverflowMenuIcon,
ProgressInprogressIcon,
StepActiveIcon,
StepDefaultIcon,
StepSuccessIcon,
Expand Down Expand Up @@ -48,6 +50,8 @@ export type SemanticIconMap = {
PendingIcon: ElementType;
ActiveIcon: ElementType;
CompletedIcon: ElementType;
LockedIcon: ElementType;
InProgressIcon: ElementType;
};

export interface SemanticIconProviderProps {
Expand Down Expand Up @@ -84,6 +88,8 @@ const defaultIconMap: SemanticIconMap = {
PendingIcon: StepDefaultIcon,
ActiveIcon: StepActiveIcon,
CompletedIcon: StepSuccessIcon,
LockedIcon: LockedIcon,
InProgressIcon: ProgressInprogressIcon,
};

const SemanticIconContext = createContext<SemanticIconMap>(defaultIconMap);
Expand Down
1 change: 1 addition & 0 deletions packages/lab/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export * from "./skip-link";
export * from "./slider";
export * from "./static-list";
export * from "./stepped-tracker";
export * from "./stepper";
export * from "./stepper-input";
export * from "./system-status";
export * from "./tabs";
Expand Down
52 changes: 52 additions & 0 deletions packages/lab/src/stepper/Step.Connector.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
.saltStepConnector {
--saltStep-connector-borderWidth: 2px;

grid-area: connector;
min-height: var(--salt-spacing-300);

transition-duration: inherit;
transition-timing-function: inherit;
transition-property: opacity, min-height;
}

.saltStepper-horizontal .saltStepConnector {
/* Distance between Icon and Connector */
--saltStep-connector-horizontal-gap: var(--salt-spacing-100);

position: absolute;
top: calc(var(--icon-size) / 2 - var(--saltStep-connector-borderWidth));
left: calc(50% + calc(var(--icon-size) / 2 + var(--saltStep-connector-horizontal-gap)));
right: calc(-50% + calc(var(--icon-size) / 2 + var(--saltStep-connector-horizontal-gap)));

border-top-width: var(--saltStep-connector-borderWidth);
border-top-style: var(--salt-track-borderStyle-incomplete);
border-top-color: var(--salt-track-borderColor);
}

.saltStepper-horizontal .saltStep-stage-completed .saltStepConnector {
border-top-style: var(--salt-track-borderStyle-complete);
}

.saltStepper-vertical .saltStepConnector {
height: 100%;
min-height: 24px;
justify-self: center;

border-left-width: var(--saltStep-connector-borderWidth);
border-left-style: var(--salt-track-borderStyle-incomplete);
border-left-color: var(--salt-track-borderColor);
}

.saltStepper-vertical .saltStep-stage-completed .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;
}
26 changes: 26 additions & 0 deletions packages/lab/src/stepper/Step.Connector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { makePrefixer } from "@salt-ds/core";
import { useComponentCssInjection } from "@salt-ds/styles";
import { useWindow } from "@salt-ds/window";
import clsx from "clsx";

import stepConnectorCSS from "./Step.Connector.css";

export namespace StepConnector {
export interface Props {
className?: string;
}
}

const withBaseName = makePrefixer("saltStepConnector");

export function StepConnector({ className }: StepConnector.Props) {
const targetWindow = useWindow();

useComponentCssInjection({
testId: "salt-step-connector",
css: stepConnectorCSS,
window: targetWindow,
});

return <div className={clsx(withBaseName(), className)} />;
}
16 changes: 16 additions & 0 deletions packages/lab/src/stepper/Step.Description.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
.saltStepDescription {
grid-area: description;
}

.saltStepper-vertical .saltStepDescription {
padding-bottom: var(--salt-spacing-300);
padding-left: calc((var(--saltStep-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);
}
36 changes: 36 additions & 0 deletions packages/lab/src/stepper/Step.Description.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
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 namespace StepDescription {
export interface Props extends TextProps<"div"> {}
}

const withBaseName = makePrefixer("saltStepDescription");

export function StepDescription({
id,
className,
styleAs = "label",
...props
}: StepDescription.Props) {
const targetWindow = useWindow();

useComponentCssInjection({
testId: "salt-step-description",
css: stepDescriptionCSS,
window: targetWindow,
});

return (
<Text
id={id}
styleAs="label"
className={clsx(withBaseName(), className)}
{...props}
/>
);
}
3 changes: 3 additions & 0 deletions packages/lab/src/stepper/Step.ExpandTrigger.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.saltStepExpandTrigger {
grid-area: expand;
}
45 changes: 45 additions & 0 deletions packages/lab/src/stepper/Step.ExpandTrigger.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Button, type ButtonProps } from "@salt-ds/core";
import { makePrefixer } from "@salt-ds/core";
import { ChevronDownIcon, ChevronUpIcon } from "@salt-ds/icons";
import { useComponentCssInjection } from "@salt-ds/styles";
import { useWindow } from "@salt-ds/window";
import clsx from "clsx";

import stepExpandTriggerCSS from "./Step.ExpandTrigger.css";

export namespace StepExpandTrigger {
export interface Props extends ButtonProps {
expanded: boolean;
}
}

const withBaseName = makePrefixer("saltStepExpandTrigger");

export function StepExpandTrigger({
expanded,
className,
...props
}: StepExpandTrigger.Props) {
const targetWindow = useWindow();

useComponentCssInjection({
testId: "salt-step-expand-trigger",
css: stepExpandTriggerCSS,
window: targetWindow,
});

return (
<Button
appearance="transparent"
sentiment="neutral"
className={clsx(withBaseName(), className)}
{...props}
>
{expanded ? (
<ChevronUpIcon aria-hidden />
) : (
<ChevronDownIcon aria-hidden />
)}
</Button>
);
}
Loading
Loading