Skip to content

Commit 760df87

Browse files
committed
the rest of the owl
1 parent a520abb commit 760df87

File tree

10 files changed

+184
-67
lines changed

10 files changed

+184
-67
lines changed
Lines changed: 80 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,62 @@
1+
/* eslint-disable no-console */
12
import React from 'react';
3+
import { faker } from '@faker-js/faker';
4+
import { StoryMetaType } from '@lg-tools/storybook-utils';
25
import { StoryObj } from '@storybook/react';
36

7+
import Card from '@leafygreen-ui/card';
8+
49
import { Wizard } from '.';
510

11+
faker.seed(0);
12+
613
export default {
714
title: 'Components/Wizard',
815
component: Wizard,
9-
};
16+
parameters: {
17+
default: 'LiveExample',
18+
},
19+
decorators: [
20+
Fn => (
21+
<div style={{ margin: -100, height: '100vh' }}>
22+
<Fn />
23+
</div>
24+
),
25+
],
26+
} satisfies StoryMetaType<typeof Wizard>;
1027

1128
export const LiveExample: StoryObj<typeof Wizard> = {
1229
parameters: {
1330
controls: {
1431
exclude: ['children', 'activeStep', 'onStepChange'],
1532
},
1633
},
17-
render: props => <Wizard {...props}></Wizard>,
34+
render: props => (
35+
<Wizard {...props}>
36+
{['Apple', 'Banana', 'Carrot'].map((title, i) => (
37+
<Wizard.Step
38+
key={i}
39+
title={`Step ${i + 1}: ${title}`}
40+
description={faker.lorem.paragraph()}
41+
>
42+
<Card>{faker.lorem.paragraph(10)}</Card>
43+
</Wizard.Step>
44+
))}
45+
<Wizard.Footer
46+
backButtonProps={{
47+
onClick: () => console.log('[Storybook] Clicked Back'),
48+
}}
49+
cancelButtonProps={{
50+
children: 'Cancel',
51+
onClick: () => console.log('[Storybook] Clicked Cancel'),
52+
}}
53+
primaryButtonProps={{
54+
children: 'Primary',
55+
onClick: () => console.log('[Storybook] Clicked Primary'),
56+
}}
57+
/>
58+
</Wizard>
59+
),
1860
};
1961

2062
export const Controlled: StoryObj<typeof Wizard> = {
@@ -23,14 +65,47 @@ export const Controlled: StoryObj<typeof Wizard> = {
2365
exclude: ['children', 'onStepChange'],
2466
},
2567
},
68+
args: {
69+
activeStep: 0,
70+
},
2671
render: ({ activeStep, ...props }) => {
2772
return (
2873
<Wizard
2974
activeStep={activeStep}
30-
// eslint-disable-next-line no-console
31-
onStepChange={x => console.log(`Set activeStep to ${x}`)}
75+
onStepChange={x =>
76+
console.log(`[Storybook] activeStep should change to ${x}`)
77+
}
3278
{...props}
33-
></Wizard>
79+
>
80+
{['Apple', 'Banana', 'Carrot'].map((title, i) => (
81+
<Wizard.Step
82+
key={i}
83+
title={`Step ${i + 1}: ${title}`}
84+
description={faker.lorem.paragraph()}
85+
>
86+
<Card>
87+
<p>
88+
This Wizard is controlled. Clicking the buttons will not do
89+
anything. Use the Storybook controls to see the next step
90+
</p>
91+
{faker.lorem.paragraph(10)}
92+
</Card>
93+
</Wizard.Step>
94+
))}
95+
<Wizard.Footer
96+
backButtonProps={{
97+
onClick: () => console.log('[Storybook] Clicked Back'),
98+
}}
99+
cancelButtonProps={{
100+
children: 'Cancel',
101+
onClick: () => console.log('[Storybook] Clicked Cancel'),
102+
}}
103+
primaryButtonProps={{
104+
children: 'Primary',
105+
onClick: () => console.log('[Storybook] Clicked Primary'),
106+
}}
107+
/>
108+
</Wizard>
34109
);
35110
},
36111
};

packages/wizard/src/Wizard/Wizard.styles.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import { css } from '@leafygreen-ui/emotion';
22

33
export const wizardContainerStyles = css`
4+
width: 100%;
5+
height: 100%;
46
display: flex;
57
flex-direction: column;
68
gap: 24px;
9+
outline: 1px solid red;
710
`;
811

912
export const stepContentStyles = css`

packages/wizard/src/Wizard/Wizard.tsx

Lines changed: 36 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
1-
import React, { Children, cloneElement, isValidElement } from 'react';
1+
import React, { Children, isValidElement } from 'react';
22

3+
import { Direction } from '@leafygreen-ui/descendants';
4+
import { findChild } from '@leafygreen-ui/lib';
5+
6+
import { WIZARD_FOOTER_KEY } from '../constants';
37
import { useWizardControlledValue } from '../utils/useWizardControlledValue/useWizardControlledValue';
8+
import { WizardContext } from '../WizardContext/WizardContext';
49
import { WizardFooter } from '../WizardFooter';
510
import { WizardStep } from '../WizardStep';
611

@@ -16,23 +21,10 @@ export function Wizard({
1621
const {
1722
isControlled,
1823
value: activeStep,
19-
setValue: setInternalActiveStep,
24+
setValue: setActiveStep,
2025
} = useWizardControlledValue<number>(activeStepProp, undefined, 0);
2126

22-
// Handle step changes
23-
const handleStepChange = (newStep: number) => {
24-
if (!isControlled) {
25-
setInternalActiveStep(newStep);
26-
}
27-
onStepChange?.(newStep);
28-
};
29-
30-
// Filter children to separate steps from footer
31-
const childrenArray = Children.toArray(children);
32-
33-
// For now, we'll look for components with displayName ending in 'Step' or 'Footer'
34-
// This will be more precise once Wizard.Step and Wizard.Footer are implemented
35-
const stepChildren = childrenArray.filter(child => {
27+
const stepChildren = Children.toArray(children).filter(child => {
3628
if (isValidElement(child)) {
3729
const displayName = (child.type as any)?.displayName;
3830
return displayName && displayName.includes('Step');
@@ -41,38 +33,41 @@ export function Wizard({
4133
return false;
4234
});
4335

44-
const footerChild = childrenArray.find(child => {
45-
if (isValidElement(child)) {
46-
const displayName = (child.type as any)?.displayName;
47-
return displayName && displayName.includes('Footer');
36+
const updateStep = (direction: Direction) => {
37+
const getNextStep = (curr: number) => {
38+
switch (direction) {
39+
case Direction.Next:
40+
return Math.min(curr + 1, stepChildren.length - 1);
41+
case Direction.Prev:
42+
return Math.max(curr - 1, 0);
43+
}
44+
};
45+
46+
if (!isControlled) {
47+
setActiveStep(getNextStep);
4848
}
4949

50-
return false;
51-
});
50+
onStepChange?.(getNextStep(activeStep));
51+
};
52+
53+
const footerChild = findChild(children, WIZARD_FOOTER_KEY);
5254

5355
// Get the current step to render
5456
const currentStep = stepChildren[activeStep] || null;
5557

56-
// Clone footer with step navigation handlers if it exists
57-
const clonedFooter =
58-
footerChild && isValidElement(footerChild)
59-
? cloneElement(footerChild as React.ReactElement<any>, {
60-
activeStep,
61-
totalSteps: stepChildren.length,
62-
onStepChange: handleStepChange,
63-
isControlled,
64-
})
65-
: null;
66-
6758
return (
68-
<div className={wizardContainerStyles}>
69-
<code>activeStep: {activeStep}</code>
70-
{/* Render current step */}
71-
<div className={stepContentStyles}>{currentStep}</div>
72-
73-
{/* Render footer */}
74-
{clonedFooter}
75-
</div>
59+
<WizardContext.Provider
60+
value={{
61+
activeStep,
62+
updateStep,
63+
}}
64+
>
65+
<div className={wizardContainerStyles}>
66+
<div className={stepContentStyles}>{currentStep}</div>
67+
{/* Render footer */}
68+
{footerChild}
69+
</div>
70+
</WizardContext.Provider>
7671
);
7772
}
7873

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { createContext, useContext } from 'react';
2+
3+
import { Direction } from '@leafygreen-ui/descendants';
4+
5+
interface WizardContextData {
6+
activeStep: number;
7+
updateStep: (direction: Direction) => void;
8+
}
9+
10+
export const WizardContext = createContext<WizardContextData>({
11+
activeStep: 0,
12+
updateStep: () => {},
13+
});
14+
15+
export const useWizardContext = () => useContext(WizardContext);
Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,48 @@
1-
import React from 'react';
1+
import React, { MouseEventHandler } from 'react';
22

3+
import { Direction } from '@leafygreen-ui/descendants';
34
import FormFooter from '@leafygreen-ui/form-footer';
45

6+
import { WIZARD_FOOTER_KEY } from '../constants';
7+
import { useWizardContext } from '../WizardContext/WizardContext';
8+
59
import { WizardFooterProps } from './WizardFooter.types';
610

7-
export function WizardFooter({
11+
export const WizardFooter = ({
812
backButtonProps,
913
cancelButtonProps,
1014
primaryButtonProps,
11-
}: WizardFooterProps) {
12-
// Handle back button click
15+
}: WizardFooterProps) => {
16+
const { activeStep, updateStep } = useWizardContext();
17+
18+
const handleBackButtonClick: MouseEventHandler<HTMLButtonElement> = e => {
19+
updateStep(Direction.Prev);
20+
backButtonProps?.onClick?.(e);
21+
};
22+
23+
const handlePrimaryButtonClick: MouseEventHandler<HTMLButtonElement> = e => {
24+
updateStep(Direction.Next);
25+
primaryButtonProps.onClick?.(e);
26+
};
1327

1428
return (
1529
<FormFooter
16-
backButtonProps={backButtonProps}
30+
backButtonProps={
31+
activeStep > 0
32+
? {
33+
...backButtonProps,
34+
onClick: handleBackButtonClick,
35+
}
36+
: undefined
37+
}
1738
cancelButtonProps={cancelButtonProps}
18-
primaryButtonProps={primaryButtonProps}
39+
primaryButtonProps={{
40+
...primaryButtonProps,
41+
onClick: handlePrimaryButtonClick,
42+
}}
1943
/>
2044
);
21-
}
45+
};
2246

2347
WizardFooter.displayName = 'WizardFooter';
48+
WizardFooter[WIZARD_FOOTER_KEY] = true;
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { css } from '@leafygreen-ui/emotion';
2+
import { spacing } from '@leafygreen-ui/tokens';
23

3-
export const stepContentStyles = css`
4-
/* Content styles */
4+
export const stepStyles = css`
5+
padding: 0 ${spacing[1800]}px;
56
`;

packages/wizard/src/WizardStep/WizardStep.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@ import React from 'react';
22

33
import { Description, H3 } from '@leafygreen-ui/typography';
44

5-
import { stepContentStyles } from './WizardStep.styles';
5+
import { stepStyles } from './WizardStep.styles';
66
import { WizardStepProps } from './WizardStep.types';
77

88
export function WizardStep({ title, description, children }: WizardStepProps) {
99
return (
10-
<div>
10+
<div className={stepStyles}>
1111
<H3>{title}</H3>
1212
{description && <Description>{description}</Description>}
13-
<div className={stepContentStyles}>{children}</div>
13+
<div>{children}</div>
1414
</div>
1515
);
1616
}

packages/wizard/src/WizardStep/WizardStep.types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export interface WizardStepProps {
99
/**
1010
* The description of the step
1111
*/
12-
description: ReactNode;
12+
description?: ReactNode;
1313

1414
/**
1515
* The content of the step

packages/wizard/src/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export const WIZARD_STEP_KEY = 'isWizardStep';
2+
export const WIZARD_FOOTER_KEY = 'isWizardFooter';

packages/wizard/src/utils/useWizardControlledValue/useWizardControlledValue.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useEffect, useMemo, useState } from 'react';
1+
import { Dispatch, SetStateAction, useEffect, useMemo, useState } from 'react';
22
import isUndefined from 'lodash/isUndefined';
33

44
import { usePrevious } from '@leafygreen-ui/hooks';
@@ -15,7 +15,7 @@ interface ControlledValueReturnObject<T extends any> {
1515
* Either updates the uncontrolled value,
1616
* or calls the provided `onChange` callback
1717
*/
18-
setValue: (newVal?: T, ...args: Array<any>) => void;
18+
setValue: Dispatch<SetStateAction<T>>;
1919
}
2020

2121
/**
@@ -61,14 +61,14 @@ export const useWizardControlledValue = <T extends any>(
6161

6262
// We set the initial value to either the `value`
6363
// or the temporary `initialValue` prop
64-
const initialValue = useMemo(
65-
() => (isControlled ? valueProp : initialProp),
64+
const initialValue: T = useMemo(
65+
() => (isControlled ? (valueProp as T) : (initialProp as T)),
6666
[initialProp, isControlled, valueProp],
6767
);
6868

6969
// Keep track of the internal value state
70-
const [uncontrolledValue, setUncontrolledValue] = useState<T | undefined>(
71-
initialValue,
70+
const [uncontrolledValue, setUncontrolledValue] = useState<T>(
71+
initialValue as T,
7272
);
7373

7474
// The returned value is wither the provided value prop
@@ -79,12 +79,13 @@ export const useWizardControlledValue = <T extends any>(
7979
);
8080

8181
// A wrapper around `handleChange` that fires a simulated event
82-
const setValue = (newVal: T | undefined) => {
82+
const setValue: Dispatch<SetStateAction<T>> = newVal => {
8383
if (!isControlled) {
8484
setUncontrolledValue(newVal);
8585
}
8686

87-
onChange?.(newVal);
87+
const val = typeof newVal === 'function' ? (newVal as Function)() : newVal;
88+
onChange?.(val);
8889
};
8990

9091
return {

0 commit comments

Comments
 (0)