Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
dae23a2
rm step wrapper
TheSonOfThomp Oct 22, 2025
9c6c588
rm descendants dep
TheSonOfThomp Oct 22, 2025
79774a0
export WizardProvider
TheSonOfThomp Oct 22, 2025
c2f40f9
delete-wizard-demo
TheSonOfThomp Nov 4, 2025
7efcfc5
Update pnpm
TheSonOfThomp Nov 7, 2025
2b2ad96
fix wizard changes
TheSonOfThomp Nov 14, 2025
b3300d4
Adds `requiresAcknowledgement` prop to Wizard.Step
TheSonOfThomp Nov 21, 2025
c0baca5
Implements `isAcknowledged` state inside provider
TheSonOfThomp Nov 21, 2025
e5acf36
Update Wizard.stories.tsx
TheSonOfThomp Nov 21, 2025
daaa1c7
rm delete demo
TheSonOfThomp Nov 21, 2025
47bd132
Update wizard.md
TheSonOfThomp Nov 21, 2025
679a4a0
rm temp changesets
TheSonOfThomp Nov 21, 2025
4f49ad8
Update README.md
TheSonOfThomp Nov 21, 2025
0d85320
Update WizardStep.spec.tsx
TheSonOfThomp Nov 21, 2025
f848f24
footer tests
TheSonOfThomp Nov 21, 2025
fb638d3
Update Wizard.spec.tsx
TheSonOfThomp Nov 21, 2025
6d45f0e
update package json
TheSonOfThomp Nov 21, 2025
01f43ba
update provider props
TheSonOfThomp Nov 21, 2025
cbb87fe
revert toast changes?
TheSonOfThomp Nov 21, 2025
72c9c1f
Update .npmrc
TheSonOfThomp Nov 21, 2025
751644f
Update pnpm-lock.yaml
TheSonOfThomp Nov 21, 2025
947d899
Update WizardStep.spec.tsx
TheSonOfThomp Nov 21, 2025
6439be0
exports form footer types
TheSonOfThomp Nov 24, 2025
b2c8178
Update WizardFooter.types.ts
TheSonOfThomp Nov 24, 2025
5419883
adds `totalSteps` to wizard context
TheSonOfThomp Nov 24, 2025
bf9cbe8
fix bad merge
TheSonOfThomp Nov 24, 2025
1e92da7
adds LGIDs
TheSonOfThomp Nov 21, 2025
0d7650c
adds test utils
TheSonOfThomp Nov 21, 2025
35274d8
lint
TheSonOfThomp Nov 21, 2025
c77268a
fix bad merge
TheSonOfThomp Nov 24, 2025
649c699
removes Step test utils
TheSonOfThomp Nov 24, 2025
aa6aedb
add layout comments
TheSonOfThomp Nov 24, 2025
1fdc3e2
form-footer lgids
TheSonOfThomp Nov 24, 2025
265e1a1
updates wizard testids
TheSonOfThomp Nov 24, 2025
6842bbb
updates readme
TheSonOfThomp Nov 25, 2025
f919ecc
updates tsdoc
TheSonOfThomp Nov 25, 2025
4f024b1
fixes tests
TheSonOfThomp Nov 25, 2025
4fd3668
fixes ack reset test
TheSonOfThomp Nov 25, 2025
5701b76
Squashed commit of the following:
TheSonOfThomp Nov 25, 2025
e2de46c
Update WizardStep.spec.tsx
TheSonOfThomp Nov 25, 2025
7d00ad2
Update WizardContext.tsx
TheSonOfThomp Nov 25, 2025
95432ad
Merge branch 'LG-5562-wizard-updates' into LG-5566-wizard-test-utils-…
TheSonOfThomp Nov 25, 2025
982ef72
Update WizardStep.spec.tsx
TheSonOfThomp Nov 25, 2025
b469712
Squashed commit of the following:
TheSonOfThomp Nov 25, 2025
4b32ed6
fixes stories
TheSonOfThomp Nov 25, 2025
465f760
Squashed commit of the following:
TheSonOfThomp Nov 25, 2025
1106f38
Update WizardStep.stories.tsx
TheSonOfThomp Nov 25, 2025
02cfbf6
Merge branch 'LG-5562-wizard-updates' into LG-5566-wizard-test-utils-…
TheSonOfThomp Nov 25, 2025
fd56d83
Update packages/wizard/src/testing/getTestUtils.tsx
TheSonOfThomp Nov 26, 2025
47e4c4f
Merge branch 'at/wizard-integration' into LG-5566-wizard-test-utils-l…
TheSonOfThomp Nov 26, 2025
93f5e82
Update README.md
TheSonOfThomp Nov 26, 2025
e182b11
use Button test utils
TheSonOfThomp Nov 26, 2025
f402b88
use test utils
TheSonOfThomp Nov 26, 2025
1c32c53
Update pnpm-lock.yaml
TheSonOfThomp Nov 26, 2025
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
5 changes: 5 additions & 0 deletions .changeset/form-footer-lgids.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@leafygreen-ui/form-footer': patch
---

Passes `data-lgid` to the root `footer` element
1 change: 1 addition & 0 deletions packages/form-footer/src/FormFooter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export default function FormFooter({
<LeafyGreenProvider darkMode={darkMode}>
<footer
data-testid={lgIds.root}
data-lgid={lgIds.root}
className={getFormFooterStyles({ theme, className })}
{...rest}
>
Expand Down
48 changes: 48 additions & 0 deletions packages/wizard/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,3 +108,51 @@ const MyWizardStepContents = () => {
### Wizard.Footer

The `Wizard.Footer` is a convenience wrapper around the `FormFooter` component. Each step should render its own Footer component

# Test Harnesses

## getTestUtils()

`getTestUtils()` is a util that allows consumers to reliably interact with LG `Wizard` in a product test suite. If the `Wizard` component cannot be found, an error will be thrown.

### Usage

```tsx
import { getTestUtils } from '@leafygreen-ui/wizard';

const utils = getTestUtils(lgId?: `lg-${string}`); // lgId refers to the custom `data-lgid` attribute passed to `Wizard`. It defaults to 'lg-wizard' if left empty.
```

### Test Utils

```tsx
const {
getFooter,
queryFooter,
findFooter,
getPrimaryButton,
queryPrimaryButton,
findPrimaryButton,
getBackButton,
queryBackButton,
findBackButton,
getCancelButton,
queryCancelButton,
findCancelButton,
} = getTestUtils();
```

| Util | Description | Returns |
| ---------------------- | ------------------------------------------------------------- | ----------------------------- |
| `getFooter()` | Returns the footer element | `HTMLElement` |
| `queryFooter()` | Returns the footer element or null if not found | `HTMLElement` \| `null` |
| `findFooter()` | Returns a promise that resolves to the footer element | `Promise<HTMLElement>` |
| `getPrimaryButton()` | Returns the primary button element | `HTMLButtonElement` |
| `queryPrimaryButton()` | Returns the primary button element or null if not found | `HTMLButtonElement` \| `null` |
| `findPrimaryButton()` | Returns a promise that resolves to the primary button element | `Promise<HTMLButtonElement>` |
| `getBackButton()` | Returns the back button element | `HTMLButtonElement` |
| `queryBackButton()` | Returns the back button element or null if not found | `HTMLButtonElement` \| `null` |
| `findBackButton()` | Returns a promise that resolves to the back button element | `Promise<HTMLButtonElement>` |
| `getCancelButton()` | Returns the cancel button element | `HTMLButtonElement` |
| `queryCancelButton()` | Returns the cancel button element or null if not found | `HTMLButtonElement` \| `null` |
| `findCancelButton()` | Returns a promise that resolves to the cancel button element | `Promise<HTMLButtonElement>` |
2 changes: 1 addition & 1 deletion packages/wizard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"access": "public"
},
"dependencies": {
"@leafygreen-ui/button": "workspace:^",
"@leafygreen-ui/compound-component": "workspace:^",
"@leafygreen-ui/emotion": "workspace:^",
"@leafygreen-ui/form-footer": "workspace:^",
Expand All @@ -38,7 +39,6 @@
},
"devDependencies": {
"@faker-js/faker": "^8.0.0",
"@leafygreen-ui/button": "workspace:^",
"@leafygreen-ui/badge": "workspace:^",
"@leafygreen-ui/banner": "workspace:^",
"@leafygreen-ui/card": "workspace:^",
Expand Down
78 changes: 46 additions & 32 deletions packages/wizard/src/Wizard/Wizard.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from 'react';
import { render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

import { getTestUtils } from '../testing';
import { useWizardStepContext } from '../WizardStep';

import { Wizard } from '.';
Expand Down Expand Up @@ -69,7 +70,7 @@ describe('packages/wizard', () => {
});

test('does not render back button on first step', () => {
const { queryByRole, getByRole } = render(
render(
<Wizard activeStep={0}>
<Wizard.Step>
<div data-testid="step-1-content">Content 1</div>
Expand All @@ -88,13 +89,15 @@ describe('packages/wizard', () => {
</Wizard>,
);

const { queryBackButton, getPrimaryButton } = getTestUtils();

// Back button should not be rendered on first step
expect(queryByRole('button', { name: 'Back' })).not.toBeInTheDocument();
expect(getByRole('button', { name: 'Next' })).toBeInTheDocument();
expect(queryBackButton()).not.toBeInTheDocument();
expect(getPrimaryButton()).toBeInTheDocument();
});

test('renders back button on second step', () => {
const { getByRole } = render(
render(
<Wizard activeStep={1}>
<Wizard.Step>
<div data-testid="step-1-content">Content 1</div>
Expand All @@ -113,16 +116,18 @@ describe('packages/wizard', () => {
</Wizard>,
);

expect(getByRole('button', { name: 'Back' })).toBeInTheDocument();
expect(getByRole('button', { name: 'Next' })).toBeInTheDocument();
const { getBackButton, getPrimaryButton } = getTestUtils();

expect(getBackButton()).toBeInTheDocument();
expect(getPrimaryButton()).toBeInTheDocument();
});
});

describe('interaction', () => {
test('calls `onStepChange` when incrementing step', async () => {
const onStepChange = jest.fn();

const { getByRole } = render(
render(
<Wizard activeStep={0} onStepChange={onStepChange}>
<Wizard.Step>
<div data-testid="step-1-content">Content 1</div>
Expand All @@ -135,15 +140,16 @@ describe('packages/wizard', () => {
</Wizard>,
);

await userEvent.click(getByRole('button', { name: 'Next' }));
const { getPrimaryButton } = getTestUtils();
userEvent.click(getPrimaryButton());

expect(onStepChange).toHaveBeenCalledWith(1);
});

test('calls `onStepChange` when decrementing step', async () => {
const onStepChange = jest.fn();

const { getByRole } = render(
render(
<Wizard activeStep={1} onStepChange={onStepChange}>
<Wizard.Step>
<div data-testid="step-1-content">Content 1</div>
Expand All @@ -162,7 +168,8 @@ describe('packages/wizard', () => {
</Wizard>,
);

await userEvent.click(getByRole('button', { name: 'Back' }));
const { getBackButton } = getTestUtils();
userEvent.click(getBackButton());

expect(onStepChange).toHaveBeenCalledWith(0);
});
Expand All @@ -173,7 +180,7 @@ describe('packages/wizard', () => {
const onPrimaryClick = jest.fn();
const onCancelClick = jest.fn();

const { getByRole } = render(
render(
<Wizard activeStep={1} onStepChange={onStepChange}>
<Wizard.Step>
<div data-testid="step-1-content">Content 1</div>
Expand All @@ -194,21 +201,24 @@ describe('packages/wizard', () => {
</Wizard>,
);

await userEvent.click(getByRole('button', { name: 'Back' }));
const { getBackButton, getPrimaryButton, getCancelButton } =
getTestUtils();

userEvent.click(getBackButton());
expect(onBackClick).toHaveBeenCalled();
expect(onStepChange).toHaveBeenCalledWith(0);

await userEvent.click(getByRole('button', { name: 'Next' }));
userEvent.click(getPrimaryButton());
expect(onPrimaryClick).toHaveBeenCalled();
expect(onStepChange).toHaveBeenCalledWith(1);

await userEvent.click(getByRole('button', { name: 'Cancel' }));
userEvent.click(getCancelButton());
expect(onCancelClick).toHaveBeenCalled();
});

describe('uncontrolled', () => {
test('does not increment step beyond Steps count', async () => {
const { getByTestId, queryByTestId, getByRole } = render(
const { getByTestId, queryByTestId } = render(
<Wizard>
<Wizard.Step>
<div data-testid="step-1-content">Content 1</div>
Expand All @@ -221,16 +231,18 @@ describe('packages/wizard', () => {
</Wizard>,
);

const { getPrimaryButton } = getTestUtils();

// Start at step 1
expect(getByTestId('step-1-content')).toBeInTheDocument();

// Click next to go to step 2
await userEvent.click(getByRole('button', { name: 'Next' }));
userEvent.click(getPrimaryButton());
expect(getByTestId('step-2-content')).toBeInTheDocument();
expect(queryByTestId('step-1-content')).not.toBeInTheDocument();

// Click next again - should stay at step 2 (last step)
await userEvent.click(getByRole('button', { name: 'Next' }));
userEvent.click(getPrimaryButton());
expect(getByTestId('step-2-content')).toBeInTheDocument();
expect(queryByTestId('step-1-content')).not.toBeInTheDocument();
});
Expand All @@ -240,7 +252,7 @@ describe('packages/wizard', () => {
test('does not change steps internally when controlled', async () => {
const onStepChange = jest.fn();

const { getByTestId, queryByTestId, getByRole } = render(
const { getByTestId, queryByTestId } = render(
<Wizard activeStep={0} onStepChange={onStepChange}>
<Wizard.Step>
<div data-testid="step-1-content">Content 1</div>
Expand All @@ -253,11 +265,13 @@ describe('packages/wizard', () => {
</Wizard>,
);

const { getPrimaryButton } = getTestUtils();

// Should start at step 1
expect(getByTestId('step-1-content')).toBeInTheDocument();

// Click next
await userEvent.click(getByRole('button', { name: 'Next' }));
userEvent.click(getPrimaryButton());

// Should still be at step 1 since it's controlled
expect(getByTestId('step-1-content')).toBeInTheDocument();
Expand Down Expand Up @@ -318,7 +332,7 @@ describe('packages/wizard', () => {

describe('requiresAcknowledgement', () => {
test('disables primary button when requiresAcknowledgement is true and not acknowledged', () => {
const { getByRole } = render(
render(
<Wizard>
<Wizard.Step requiresAcknowledgement>
<div data-testid="step-1-content">Content 1</div>
Expand All @@ -327,8 +341,8 @@ describe('packages/wizard', () => {
</Wizard>,
);

const primaryButton = getByRole('button', { name: 'Next' });
expect(primaryButton).toHaveAttribute('aria-disabled', 'true');
const { isPrimaryButtonDisabled } = getTestUtils();
expect(isPrimaryButtonDisabled()).toBe(true);
});

test('enables primary button when requiresAcknowledgement is true and acknowledged', async () => {
Expand All @@ -349,18 +363,18 @@ describe('packages/wizard', () => {
</Wizard>,
);

const primaryButton = getByRole('button', { name: 'Next' });
expect(primaryButton).toHaveAttribute('aria-disabled', 'true');
const { isPrimaryButtonDisabled } = getTestUtils();
expect(isPrimaryButtonDisabled()).toBe(true);

// Acknowledge the step
const acknowledgeButton = getByRole('button', { name: 'Acknowledge' });
await userEvent.click(acknowledgeButton);
userEvent.click(acknowledgeButton);

expect(primaryButton).toHaveAttribute('aria-disabled', 'false');
expect(isPrimaryButtonDisabled()).toBe(false);
});

test('enables primary button when requiresAcknowledgement is false', () => {
const { getByRole } = render(
render(
<Wizard>
<Wizard.Step requiresAcknowledgement={false}>
<div data-testid="step-1-content">Content 1</div>
Expand All @@ -369,12 +383,12 @@ describe('packages/wizard', () => {
</Wizard>,
);

const primaryButton = getByRole('button', { name: 'Next' });
expect(primaryButton).toHaveAttribute('aria-disabled', 'false');
const { isPrimaryButtonDisabled } = getTestUtils();
expect(isPrimaryButtonDisabled()).toBe(false);
});

test('enables primary button when requiresAcknowledgement is not set (default)', () => {
const { getByRole } = render(
render(
<Wizard>
<Wizard.Step>
<div data-testid="step-1-content">Content 1</div>
Expand All @@ -383,8 +397,8 @@ describe('packages/wizard', () => {
</Wizard>,
);

const primaryButton = getByRole('button', { name: 'Next' });
expect(primaryButton).toHaveAttribute('aria-disabled', 'false');
const { isPrimaryButtonDisabled } = getTestUtils();
expect(isPrimaryButtonDisabled()).toBe(false);
});
});
});
Expand Down
14 changes: 13 additions & 1 deletion packages/wizard/src/Wizard/Wizard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,21 @@ import {
import { useControlled } from '@leafygreen-ui/hooks';

import { WizardSubComponentProperties } from '../constants';
import { getLgIds } from '../utils/getLgIds';
import { WizardProvider } from '../WizardContext/WizardContext';
import { WizardFooter } from '../WizardFooter';
import { WizardStep } from '../WizardStep';

import { WizardProps } from './Wizard.types';

export const Wizard = CompoundComponent(
({ activeStep: activeStepProp, onStepChange, children }: WizardProps) => {
({
activeStep: activeStepProp,
onStepChange,
children,
'data-lgid': dataLgId,
}: WizardProps) => {
const lgIds = getLgIds(dataLgId);
const stepChildren = findChildren(
children,
WizardSubComponentProperties.Step,
Expand Down Expand Up @@ -47,11 +54,16 @@ export const Wizard = CompoundComponent(
[setActiveStep, stepChildren.length],
);

/**
* NB: We're intentionally do _not_ wrap the `Wizard` (or `WizardStep`) component in a container element.
* This is done to ensure the Wizard is flexible, and can be rendered in any containing layout.
*/
return (
<WizardProvider
activeStep={activeStep}
updateStep={updateStep}
totalSteps={stepChildren.length}
lgIds={lgIds}
>
{stepChildren.map((child, i) => (i === activeStep ? child : null))}
</WizardProvider>
Expand Down
4 changes: 3 additions & 1 deletion packages/wizard/src/Wizard/Wizard.types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { ReactNode } from 'react';

export interface WizardProps {
import { LgIdProps } from '@leafygreen-ui/lib';

export interface WizardProps extends LgIdProps {
/**
* The current active step index (0-based).
*
Expand Down
Loading
Loading