Skip to content

Commit b790481

Browse files
committed
[LG-5563] feat(Wizard) Adds Wizard (#3161)
* initial Wizard component * Creates basic Wizard.tsx component Prompt: In the newly created package, create the Wizard component. Note: these docs mention `Wizard.Step` and `Wizard.Footer`. DO NOT create these yet. They will be created later The `@leafygreen-ui/wizard` is a general-purpose, multi-step page template, designed to create guided in-app flows and wizards: Based on the MultiStepWizard component in MMS, and intended to be used in the Product Deletion template. Feature Overview: - Takes in all Steps in the flow as children. - Renders the appropriate content for the current step - Internally handles step changing (with optional external control) Non-goals: - We will not be implementing this across MMS (MultiStepWizard is currently used in 26 files) - This will not support different url routes per step Wizard component The root flow component. Controls the rendering of the appropriate step based on a controlled prop, or uncontrolled internal state. Example ```tsx const [activeStep, setActiveStep] = useState(0) <Wizard activeStep={activeStep}> <Wizard.Step title="Step 1" description={<>Some description with a <Link>link</Link></>} > Some Content. Lorem ipsum dolor. </Wizard.Step> <Wizard.Step /> <Wizard.Step /> <Wizard.Footer backButtonProps={{ onClick: setActiveStep(x--) }} cancelButtonProps={{}} primaryButtonProps={{ onClick: setActiveStep(x++), variant: 'danger', disabled }} /> </Wizard> ``` Props: ```ts activeStep?: number; onStepChange?: (step: number) => void showStepper?: boolean; // omit for v1 ``` State: `[activeStep, setActiveStep] = useState<number> // if none provided as a prop` Events: - `onStepChange` : fired when the activeStep changes - this should still fire when controlled? Rendering: - Renders the appropriate Step based on the activeStep prop/state - Renders the Footer element, with enabled/hidden buttons based on the activeStep - If activeStep === 0, hides back button - Injects setActiveStep into Back and Primary buttons (if uncontrolled) * Creates WizardStep and WizardFooter Prompt: The Footer and Step components have been scaffolded. Create both components with the following spec: Step: A single Step in the multi-step flow. Must be rendered within a Wizard. ```ts title: ReactNode; description: ReactNode; children: ReactNode; ``` Footer: The footer element for the Wizard. A wrapper around LeafyGreen `FormFooter`, but allows us to optionally inject event handlers into the buttons. ``` backButtonProps: ButtonProps; cancelButtonProps: ButtonProps; primaryButtonProps: ButtonProps; ``` * footer& step stories * temp useWizardControlledValue * fix useWizardControlledValue * update Footer * Update package.json * use typography in Step * update descendants * update packages * the rest of the owl * update width * fix nits * Squashed commit of the following: commit c826033 Author: Adam Thompson <adam.thompson@mongodb.com> Date: Tue Sep 30 15:54:03 2025 -0400 Update isChildWithProperty.spec.tsx commit 01585d3 Merge: f3570c4 94745fb Author: Adam Thompson <adam.thompson@mongodb.com> Date: Tue Sep 30 13:28:59 2025 -0400 Merge branch 'main' into ac/cc-utils commit f3570c4 Author: Adam Thompson <adam.thompson@mongodb.com> Date: Tue Sep 30 13:28:37 2025 -0400 rm todo commit becf667 Author: Adam Thompson <adam.thompson@mongodb.com> Date: Fri Sep 26 16:50:05 2025 -0400 rm wizard commit f8463ac Author: Adam Thompson <adam.thompson@mongodb.com> Date: Fri Sep 26 16:50:00 2025 -0400 update index files commit 5e0d157 Author: Adam Thompson <adam.thompson@mongodb.com> Date: Fri Sep 26 16:49:50 2025 -0400 adds 2 level fragment test commit caf8a93 Author: Adam Thompson <2414030+TheSonOfThomp@users.noreply.github.com> Date: Fri Sep 26 16:39:09 2025 -0400 Update packages/lib/src/childQueries/findChildren/findChildren.ts Co-authored-by: Stephen Lee <stephen.lee@mongodb.com> commit ee977a1 Author: Adam Thompson <2414030+TheSonOfThomp@users.noreply.github.com> Date: Fri Sep 26 16:38:18 2025 -0400 Update packages/lib/src/childQueries/findChild/findChild.tsx Co-authored-by: Stephen Lee <stephen.lee@mongodb.com> commit ee32a26 Merge: ac2c485 366e851 Author: Adam Thompson <2414030+TheSonOfThomp@users.noreply.github.com> Date: Thu Sep 25 15:20:23 2025 -0400 Merge branch 'main' into ac/cc-utils commit ac2c485 Author: Adam Thompson <adam.thompson@mongodb.com> Date: Thu Sep 25 14:03:09 2025 -0400 Create lib-find-children.md commit 9cd7489 Author: Adam Thompson <adam.thompson@mongodb.com> Date: Thu Sep 25 14:00:05 2025 -0400 Update findChildren.ts commit 90e8208 Author: Adam Thompson <adam.thompson@mongodb.com> Date: Thu Sep 25 13:59:35 2025 -0400 Update findChildren.ts commit d7ae970 Author: Adam Thompson <adam.thompson@mongodb.com> Date: Thu Sep 25 13:52:04 2025 -0400 update findChild/children with unwrapRootFragment commit a64ff9e Author: Adam Thompson <adam.thompson@mongodb.com> Date: Thu Sep 25 13:49:27 2025 -0400 Creates unwrapRootFragment commit 000f713 Author: Adam Thompson <2414030+TheSonOfThomp@users.noreply.github.com> Date: Thu Sep 25 13:05:35 2025 -0400 Apply suggestions from code review `allChildren.length === 1` Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> commit c6d9c9d Author: Adam Thompson <adam.thompson@mongodb.com> Date: Thu Sep 25 13:00:30 2025 -0400 Update index.ts commit c369957 Author: Adam Thompson <adam.thompson@mongodb.com> Date: Thu Sep 25 13:00:12 2025 -0400 mv child queries commit 5fe4f9d Author: Adam Thompson <adam.thompson@mongodb.com> Date: Thu Sep 25 12:59:35 2025 -0400 update index files commit c9261c8 Author: Adam Thompson <adam.thompson@mongodb.com> Date: Thu Sep 25 12:58:48 2025 -0400 mv componentQueries commit be05c4d Author: Adam Thompson <adam.thompson@mongodb.com> Date: Thu Sep 25 12:55:19 2025 -0400 Update findChildren.spec.tsx commit f493f6d Author: Adam Thompson <adam.thompson@mongodb.com> Date: Thu Sep 25 12:46:47 2025 -0400 update findChild tests commit 74f5f7e Author: Adam Thompson <adam.thompson@mongodb.com> Date: Thu Sep 25 12:46:28 2025 -0400 Fix isChildWithProperty tests commit 5439034 Author: Adam Thompson <adam.thompson@mongodb.com> Date: Wed Sep 24 19:05:18 2025 -0400 findChildren commit aa89584 Author: Adam Thompson <adam.thompson@mongodb.com> Date: Wed Sep 24 19:05:10 2025 -0400 Update findChild.tsx commit dda7ad5 Author: Adam Thompson <adam.thompson@mongodb.com> Date: Wed Sep 24 19:05:01 2025 -0400 isChildWithProperty commit ae3a41b Author: Adam Thompson <adam.thompson@mongodb.com> Date: Wed Sep 24 17:02:37 2025 -0400 mv existing utils * adds findChildren * adds TextNode * Update Wizard.spec.tsx * minor fixes * spread rest * adds wizard context assertions * fix exports * fix exports * Update TextNode.tsx * creates compound component * lint * update CompoundSubComponent api * update packages * add WizardProvider * update stories * Wizard * update findChild/ren * spread className * add "exceeded steps" warning * adds warning tests
1 parent f7672a7 commit b790481

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+1359
-172
lines changed

.changeset/descendants-exports.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@leafygreen-ui/descendants': minor
3+
---
4+
5+
Exports `Position` enum. Removes type annotation from `Direction` export

.changeset/lib-find-children.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@leafygreen-ui/lib': minor
3+
---
4+
5+
Adds `findChildren` utility to `lib`. Also adds `unwrapRootFragment` and `isChildWithProperty` helpers

packages/descendants/src/Highlight/index.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
export type {
1+
export {
22
Direction,
3-
HighlightChangeHandler,
4-
HighlightContextProps,
5-
HighlightHookReturnType,
6-
Index,
7-
UseHighlightOptions,
3+
type HighlightChangeHandler,
4+
type HighlightContextProps,
5+
type HighlightHookReturnType,
6+
type Index,
7+
Position,
8+
type UseHighlightOptions,
89
} from './highlight.types';
910
export {
1011
createHighlightContext,

packages/descendants/src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,14 @@ export {
1515
// Highlight
1616
export {
1717
createHighlightContext,
18-
type Direction,
18+
Direction,
1919
type HighlightChangeHandler,
2020
type HighlightContextProps,
2121
type HighlightContextType,
2222
type HighlightHookReturnType,
2323
HighlightProvider,
2424
type Index,
25+
Position,
2526
useHighlight,
2627
useHighlightContext,
2728
type UseHighlightOptions,

packages/lib/src/childQueries/findChild/findChild.spec.tsx

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ Baz.displayName = 'Baz';
3030
(Bar as any).isBar = true;
3131
(Baz as any).isBaz = true;
3232

33-
describe('packages/lib/findChild', () => {
33+
describe('packages/compound-component/findChild', () => {
3434
test('should find a child component with matching static property', () => {
3535
// Create an iterable to test different iteration scenarios
3636
const children = [<Foo text="Foo" />, <Bar text="Bar" />];
@@ -77,6 +77,30 @@ describe('packages/lib/findChild', () => {
7777
expect((found as React.ReactElement).props.text).toBe('also-in-fragment');
7878
});
7979

80+
test('should find mapped children', () => {
81+
const COUNT = 5;
82+
const children = new Array(COUNT).fill(null).map((_, i) => {
83+
return <Foo text={`Foo number ${i}`} />;
84+
});
85+
86+
const found = findChild(children, 'isFoo');
87+
expect((found as React.ReactElement).props.text).toBe('Foo number 0');
88+
});
89+
90+
test('should find deeply mapped children', () => {
91+
const COUNT = 5;
92+
const children = (
93+
<>
94+
{new Array(COUNT).fill(null).map((_, i) => {
95+
return <Foo text={`Foo number ${i}`} />;
96+
})}
97+
</>
98+
);
99+
100+
const found = findChild(children, 'isFoo');
101+
expect((found as React.ReactElement).props.text).toBe('Foo number 0');
102+
});
103+
80104
test('should NOT find components in deeply nested fragments (search depth limitation)', () => {
81105
const children = (
82106
<React.Fragment>

packages/lib/src/childQueries/findChild/findChild.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,9 @@ export const findChild = (
4242
}
4343

4444
const allChildren = unwrapRootFragment(children);
45+
if (!allChildren) return;
4546

46-
return allChildren?.find(child =>
47-
isChildWithProperty(child, staticProperty),
48-
) as ReactElement | undefined;
47+
return allChildren
48+
.flat()
49+
.find(child => isChildWithProperty(child, staticProperty)) as ReactElement;
4950
};

packages/lib/src/childQueries/findChildren/findChildren.spec.tsx

Lines changed: 121 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ Baz.displayName = 'Baz';
3030
(Bar as any).isBar = true;
3131
(Baz as any).isBaz = true;
3232

33-
describe('packages/lib/findChildren', () => {
33+
describe('packages/compound-component/findChildren', () => {
3434
describe('basic functionality', () => {
3535
it('should find all children with matching static property', () => {
3636
const children = [
@@ -67,120 +67,142 @@ describe('packages/lib/findChildren', () => {
6767
});
6868
});
6969

70-
describe('empty and null children handling', () => {
71-
it('should handle null children', () => {
72-
const found = findChildren(null, 'isFoo');
73-
expect(found).toEqual([]);
70+
it('should find mapped children', () => {
71+
const COUNT = 5;
72+
const children = new Array(COUNT).fill(null).map((_, i) => {
73+
return <Foo text={`Foo number ${i}`} />;
7474
});
7575

76-
it('should handle undefined children', () => {
77-
const found = findChildren(undefined, 'isFoo');
78-
expect(found).toEqual([]);
79-
});
76+
const found = findChildren(children, 'isFoo');
77+
expect(found).toHaveLength(COUNT);
78+
});
8079

81-
it('should handle empty fragment', () => {
82-
const children = <></>;
83-
const found = findChildren(children, 'isFoo');
84-
expect(found).toEqual([]);
85-
});
80+
it('should find deeply mapped children', () => {
81+
const COUNT = 5;
82+
const children = (
83+
<>
84+
{new Array(COUNT).fill(null).map((_, i) => {
85+
return <Foo text={`Foo number ${i}`} />;
86+
})}
87+
</>
88+
);
89+
90+
const found = findChildren(children, 'isFoo');
91+
expect(found).toHaveLength(COUNT);
92+
});
93+
});
8694

87-
it('should handle empty array children', () => {
88-
const children: Array<React.ReactElement> = [];
89-
const found = findChildren(children, 'isFoo');
90-
expect(found).toEqual([]);
91-
});
95+
describe('empty and null children handling', () => {
96+
it('should handle null children', () => {
97+
const found = findChildren(null, 'isFoo');
98+
expect(found).toEqual([]);
9299
});
93100

94-
describe('Fragment handling', () => {
95-
it('should handle single-level fragment children', () => {
96-
const children = (
97-
<React.Fragment>
98-
<Foo text="foo-in-fragment" />
99-
<Bar text="bar-in-fragment" />
100-
<Foo text="another-foo" />
101-
</React.Fragment>
102-
);
101+
it('should handle undefined children', () => {
102+
const found = findChildren(undefined, 'isFoo');
103+
expect(found).toEqual([]);
104+
});
103105

104-
const found = findChildren(children, 'isFoo');
105-
expect(found).toHaveLength(2);
106-
expect(found[0].props.text).toBe('foo-in-fragment');
107-
expect(found[1].props.text).toBe('another-foo');
108-
});
106+
it('should handle empty fragment', () => {
107+
const children = <></>;
108+
const found = findChildren(children, 'isFoo');
109+
expect(found).toEqual([]);
110+
});
111+
112+
it('should handle empty array children', () => {
113+
const children: Array<React.ReactElement> = [];
114+
const found = findChildren(children, 'isFoo');
115+
expect(found).toEqual([]);
116+
});
117+
});
109118

110-
it('should NOT find children in deeply nested Fragments', () => {
111-
const children = (
119+
describe('Fragment handling', () => {
120+
it('should handle single-level fragment children', () => {
121+
const children = (
122+
<React.Fragment>
123+
<Foo text="foo-in-fragment" />
124+
<Bar text="bar-in-fragment" />
125+
<Foo text="another-foo" />
126+
</React.Fragment>
127+
);
128+
129+
const found = findChildren(children, 'isFoo');
130+
expect(found).toHaveLength(2);
131+
expect(found[0].props.text).toBe('foo-in-fragment');
132+
expect(found[1].props.text).toBe('another-foo');
133+
});
134+
135+
it('should NOT find children in deeply nested Fragments', () => {
136+
const children = (
137+
<React.Fragment>
138+
<Foo text="direct-foo" />
112139
<React.Fragment>
113-
<Foo text="direct-foo" />
114140
<React.Fragment>
115-
<React.Fragment>
116-
<Foo text="deeply-nested-foo" />
117-
</React.Fragment>
141+
<Foo text="deeply-nested-foo" />
118142
</React.Fragment>
119-
<Bar text="direct-bar" />
120143
</React.Fragment>
121-
);
122-
123-
// Should only find direct children, not double-nested ones
124-
const found = findChildren(children, 'isFoo');
125-
expect(found).toHaveLength(1);
126-
expect(found[0].props.text).toBe('direct-foo');
127-
});
144+
<Bar text="direct-bar" />
145+
</React.Fragment>
146+
);
147+
148+
// Should only find direct children, not double-nested ones
149+
const found = findChildren(children, 'isFoo');
150+
expect(found).toHaveLength(1);
151+
expect(found[0].props.text).toBe('direct-foo');
128152
});
153+
});
129154

130-
describe('styled components', () => {
131-
it('should work with styled components from @emotion/styled', () => {
132-
const StyledFoo = styled(Foo)`
133-
background-color: red;
134-
padding: 8px;
135-
`;
136-
137-
const children = [
138-
<Foo text="regular-foo" />,
139-
<StyledFoo text="styled-foo" />,
140-
<StyledFoo text="styled-foo-two" />,
141-
<Bar text="regular-bar" />,
142-
<Foo text="another-foo" />,
143-
];
144-
145-
const found = findChildren(children, 'isFoo');
146-
expect(found).toHaveLength(4);
147-
expect(found.map(c => c.props.text)).toEqual([
148-
'regular-foo',
149-
'styled-foo',
150-
'styled-foo-two',
151-
'another-foo',
152-
]);
153-
154-
// Verify the styled component is actually styled
155-
const styledComponent = found[1];
156-
const styledType = styledComponent.type as any;
157-
const hasEmotionProps = !!(
158-
styledType.target || styledType.__emotion_base
159-
);
160-
expect(hasEmotionProps).toBe(true);
161-
});
155+
describe('styled components', () => {
156+
it('should work with styled components from @emotion/styled', () => {
157+
const StyledFoo = styled(Foo)`
158+
background-color: red;
159+
padding: 8px;
160+
`;
161+
162+
const children = [
163+
<Foo text="regular-foo" />,
164+
<StyledFoo text="styled-foo" />,
165+
<StyledFoo text="styled-foo-two" />,
166+
<Bar text="regular-bar" />,
167+
<Foo text="another-foo" />,
168+
];
169+
170+
const found = findChildren(children, 'isFoo');
171+
expect(found).toHaveLength(4);
172+
expect(found.map(c => c.props.text)).toEqual([
173+
'regular-foo',
174+
'styled-foo',
175+
'styled-foo-two',
176+
'another-foo',
177+
]);
178+
179+
// Verify the styled component is actually styled
180+
const styledComponent = found[1];
181+
const styledType = styledComponent.type as any;
182+
const hasEmotionProps = !!(styledType.target || styledType.__emotion_base);
183+
expect(hasEmotionProps).toBe(true);
162184
});
185+
});
163186

164-
describe('search depth limitations', () => {
165-
it('should NOT find deeply nested components', () => {
166-
const children = [
167-
<Fragment>
168-
<Foo text="single-fragment" />
169-
</Fragment>,
187+
describe('search depth limitations', () => {
188+
it('should NOT find deeply nested components', () => {
189+
const children = [
190+
<Fragment>
191+
<Foo text="single-fragment" />
192+
</Fragment>,
193+
<Fragment>
170194
<Fragment>
171-
<Fragment>
172-
<Foo text="double-nested" />
173-
</Fragment>
174-
</Fragment>,
175-
<div>
176-
<Foo text="inside-div" />
177-
</div>,
178-
<Foo text="direct-child" />,
179-
];
180-
181-
const found = findChildren(children, 'isFoo');
182-
expect(found).toHaveLength(1);
183-
expect(found[0].props.text).toBe('direct-child');
184-
});
195+
<Foo text="double-nested" />
196+
</Fragment>
197+
</Fragment>,
198+
<div>
199+
<Foo text="inside-div" />
200+
</div>,
201+
<Foo text="direct-child" />,
202+
];
203+
204+
const found = findChildren(children, 'isFoo');
205+
expect(found).toHaveLength(1);
206+
expect(found[0].props.text).toBe('direct-child');
185207
});
186208
});

packages/lib/src/childQueries/findChildren/findChildren.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { unwrapRootFragment } from '../unwrapRootFragment';
1515
* **Styled Component Support:** Checks component.target and component.__emotion_base
1616
* for styled() wrapped components.
1717
*
18-
* * @example
18+
* @example
1919
* ```ts
2020
* // ✅ Will find: Direct children
2121
* findChildren([
@@ -56,7 +56,9 @@ export const findChildren = (
5656

5757
if (!allChildren) return [];
5858

59-
return allChildren.filter(child =>
60-
isChildWithProperty(child, staticProperty),
61-
) as Array<ReactElement>;
59+
return allChildren
60+
.flat()
61+
.filter(child =>
62+
isChildWithProperty(child, staticProperty),
63+
) as Array<ReactElement>;
6264
};

packages/wizard/README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
21
# Wizard
32

43
![npm (scoped)](https://img.shields.io/npm/v/@leafygreen-ui/wizard.svg)
4+
55
#### [View on MongoDB.design](https://www.mongodb.design/component/wizard/live-example/)
66

77
## Installation
@@ -23,4 +23,3 @@ yarn add @leafygreen-ui/wizard
2323
```shell
2424
npm install @leafygreen-ui/wizard
2525
```
26-

0 commit comments

Comments
 (0)