From 488f1a6e69baab8331c55f4945a2a2f5584189a4 Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Wed, 24 Sep 2025 14:39:21 -0400 Subject: [PATCH 01/28] initial Wizard component --- packages/wizard/README.md | 3 +- packages/wizard/src/Wizard.stories.tsx | 35 +++++++-- packages/wizard/src/Wizard/Wizard.spec.tsx | 7 +- packages/wizard/src/Wizard/Wizard.styles.ts | 12 ++- packages/wizard/src/Wizard/Wizard.tsx | 73 ++++++++++++++++++- packages/wizard/src/Wizard/Wizard.types.ts | 19 ++++- packages/wizard/src/Wizard/index.ts | 3 +- packages/wizard/src/index.ts | 2 +- .../wizard/src/testing/getTestUtils.spec.tsx | 6 +- .../wizard/src/testing/getTestUtils.types.ts | 2 +- 10 files changed, 133 insertions(+), 29 deletions(-) diff --git a/packages/wizard/README.md b/packages/wizard/README.md index f6d912208f..e9d23c5f71 100644 --- a/packages/wizard/README.md +++ b/packages/wizard/README.md @@ -1,7 +1,7 @@ - # Wizard ![npm (scoped)](https://img.shields.io/npm/v/@leafygreen-ui/wizard.svg) + #### [View on MongoDB.design](https://www.mongodb.design/component/wizard/live-example/) ## Installation @@ -23,4 +23,3 @@ yarn add @leafygreen-ui/wizard ```shell npm install @leafygreen-ui/wizard ``` - diff --git a/packages/wizard/src/Wizard.stories.tsx b/packages/wizard/src/Wizard.stories.tsx index 4c4b56cf46..906189c013 100644 --- a/packages/wizard/src/Wizard.stories.tsx +++ b/packages/wizard/src/Wizard.stories.tsx @@ -1,17 +1,36 @@ - import React from 'react'; -import { StoryFn } from '@storybook/react'; +import { StoryObj } from '@storybook/react'; import { Wizard } from '.'; export default { title: 'Components/Wizard', component: Wizard, -} - -const Template: StoryFn = (props) => ( - -); +}; -export const Basic = Template.bind({}); +export const LiveExample: StoryObj = { + parameters: { + controls: { + exclude: ['children', 'activeStep', 'onStepChange'], + }, + }, + render: props => , +}; +export const Controlled: StoryObj = { + parameters: { + controls: { + exclude: ['children', 'onStepChange'], + }, + }, + render: ({ activeStep, ...props }) => { + return ( + console.log(`Set activeStep to ${x}`)} + {...props} + > + ); + }, +}; diff --git a/packages/wizard/src/Wizard/Wizard.spec.tsx b/packages/wizard/src/Wizard/Wizard.spec.tsx index 07591fd2e6..179952dae4 100644 --- a/packages/wizard/src/Wizard/Wizard.spec.tsx +++ b/packages/wizard/src/Wizard/Wizard.spec.tsx @@ -1,11 +1,8 @@ - import React from 'react'; import { render } from '@testing-library/react'; import { Wizard } from '.'; describe('packages/wizard', () => { - test('condition', () => { - - }) -}) + test('condition', () => {}); +}); diff --git a/packages/wizard/src/Wizard/Wizard.styles.ts b/packages/wizard/src/Wizard/Wizard.styles.ts index 928608f58d..77476b439e 100644 --- a/packages/wizard/src/Wizard/Wizard.styles.ts +++ b/packages/wizard/src/Wizard/Wizard.styles.ts @@ -1,4 +1,12 @@ - import { css } from '@leafygreen-ui/emotion'; -export const baseStyles = css``; +export const wizardContainerStyles = css` + display: flex; + flex-direction: column; + gap: 24px; +`; + +export const stepContentStyles = css` + flex: 1; + min-height: 0; /* Allow content to shrink */ +`; diff --git a/packages/wizard/src/Wizard/Wizard.tsx b/packages/wizard/src/Wizard/Wizard.tsx index 112fe70c75..72d0cb0363 100644 --- a/packages/wizard/src/Wizard/Wizard.tsx +++ b/packages/wizard/src/Wizard/Wizard.tsx @@ -1,8 +1,75 @@ -import React from 'react'; +import React, { Children, cloneElement, isValidElement, useState } from 'react'; + +import { stepContentStyles, wizardContainerStyles } from './Wizard.styles'; import { WizardProps } from './Wizard.types'; -export function Wizard({}: WizardProps) { - return
your content here
; +export function Wizard({ + activeStep: controlledActiveStep, + onStepChange, + children, +}: WizardProps) { + // Internal state for uncontrolled mode + const [internalActiveStep, setInternalActiveStep] = useState(0); + + // Use controlled prop if provided, otherwise use internal state + const isControlled = controlledActiveStep !== undefined; + const activeStep = isControlled ? controlledActiveStep : internalActiveStep; + + // Handle step changes + const handleStepChange = (newStep: number) => { + if (!isControlled) { + setInternalActiveStep(newStep); + } + onStepChange?.(newStep); + }; + + // Filter children to separate steps from footer + const childrenArray = Children.toArray(children); + + // For now, we'll look for components with displayName ending in 'Step' or 'Footer' + // This will be more precise once Wizard.Step and Wizard.Footer are implemented + const stepChildren = childrenArray.filter(child => { + if (isValidElement(child)) { + const displayName = (child.type as any)?.displayName; + return displayName && displayName.includes('Step'); + } + + return false; + }); + + const footerChild = childrenArray.find(child => { + if (isValidElement(child)) { + const displayName = (child.type as any)?.displayName; + return displayName && displayName.includes('Footer'); + } + + return false; + }); + + // Get the current step to render + const currentStep = stepChildren[activeStep] || null; + + // Clone footer with step navigation handlers if it exists + const clonedFooter = + footerChild && isValidElement(footerChild) + ? cloneElement(footerChild as React.ReactElement, { + activeStep, + totalSteps: stepChildren.length, + onStepChange: handleStepChange, + isControlled, + }) + : null; + + return ( +
+ activeStep: {activeStep} + {/* Render current step */} +
{currentStep}
+ + {/* Render footer */} + {clonedFooter} +
+ ); } Wizard.displayName = 'Wizard'; diff --git a/packages/wizard/src/Wizard/Wizard.types.ts b/packages/wizard/src/Wizard/Wizard.types.ts index cfa270475f..e87d22b8c3 100644 --- a/packages/wizard/src/Wizard/Wizard.types.ts +++ b/packages/wizard/src/Wizard/Wizard.types.ts @@ -1 +1,18 @@ -export interface WizardProps {} \ No newline at end of file +import { ComponentPropsWithRef, ReactNode } from 'react'; + +export interface WizardProps extends ComponentPropsWithRef<'div'> { + /** + * The current active step index (0-based). If provided, the component operates in controlled mode. + */ + activeStep?: number; + + /** + * Callback fired when the active step changes + */ + onStepChange?: (step: number) => void; + + /** + * The wizard steps and footer as children + */ + children: ReactNode; +} diff --git a/packages/wizard/src/Wizard/index.ts b/packages/wizard/src/Wizard/index.ts index 82aa8f69a6..639f0423de 100644 --- a/packages/wizard/src/Wizard/index.ts +++ b/packages/wizard/src/Wizard/index.ts @@ -1,3 +1,2 @@ - -export { Wizard } from './Wizard'; +export { Wizard } from './Wizard'; export { type WizardProps } from './Wizard.types'; diff --git a/packages/wizard/src/index.ts b/packages/wizard/src/index.ts index cfbd7d46d8..623c12a572 100644 --- a/packages/wizard/src/index.ts +++ b/packages/wizard/src/index.ts @@ -1 +1 @@ -export { Wizard, type WizardProps } from './Wizard'; \ No newline at end of file +export { Wizard, type WizardProps } from './Wizard'; diff --git a/packages/wizard/src/testing/getTestUtils.spec.tsx b/packages/wizard/src/testing/getTestUtils.spec.tsx index 99117014a5..6e928dca63 100644 --- a/packages/wizard/src/testing/getTestUtils.spec.tsx +++ b/packages/wizard/src/testing/getTestUtils.spec.tsx @@ -4,7 +4,5 @@ import { render } from '@testing-library/react'; import { Wizard } from '.'; describe('packages/wizard/getTestUtils', () => { - test('condition', () => { - - }) -}) + test('condition', () => {}); +}); diff --git a/packages/wizard/src/testing/getTestUtils.types.ts b/packages/wizard/src/testing/getTestUtils.types.ts index 50d2fb417a..4b2df87c73 100644 --- a/packages/wizard/src/testing/getTestUtils.types.ts +++ b/packages/wizard/src/testing/getTestUtils.types.ts @@ -1 +1 @@ -export interface TestUtilsReturnType {} \ No newline at end of file +export interface TestUtilsReturnType {} From 71cb724cba9f6558959714f0a5cf3d86e4f28bfe Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Wed, 24 Sep 2025 14:46:26 -0400 Subject: [PATCH 02/28] 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) Some description with a link} > Some Content. Lorem ipsum dolor. ``` Props: ```ts activeStep?: number; onStepChange?: (step: number) => void showStepper?: boolean; // omit for v1 ``` State: `[activeStep, setActiveStep] = useState // 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) --- packages/wizard/src/Wizard/Wizard.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/wizard/src/Wizard/Wizard.tsx b/packages/wizard/src/Wizard/Wizard.tsx index 72d0cb0363..3a5ae9d711 100644 --- a/packages/wizard/src/Wizard/Wizard.tsx +++ b/packages/wizard/src/Wizard/Wizard.tsx @@ -8,6 +8,7 @@ export function Wizard({ onStepChange, children, }: WizardProps) { + // TODO: replace with `useControlledValue` // Internal state for uncontrolled mode const [internalActiveStep, setInternalActiveStep] = useState(0); From cf2e9c3d8ea8c3ca86e19a1d6432216e29c77c48 Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Wed, 24 Sep 2025 15:12:39 -0400 Subject: [PATCH 03/28] 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; ``` --- packages/wizard/package.json | 2 + packages/wizard/src/Wizard/Wizard.tsx | 5 ++ packages/wizard/src/Wizard/Wizard.types.ts | 10 +++ packages/wizard/src/Wizard/index.ts | 2 +- .../src/WizardFooter/WizardFooter.spec.tsx | 7 +- .../src/WizardFooter/WizardFooter.styles.ts | 1 - .../wizard/src/WizardFooter/WizardFooter.tsx | 66 ++++++++++++++++++- .../src/WizardFooter/WizardFooter.types.ts | 29 +++++++- packages/wizard/src/WizardFooter/index.ts | 3 +- .../wizard/src/WizardStep/WizardStep.spec.tsx | 7 +- .../src/WizardStep/WizardStep.styles.ts | 20 +++++- packages/wizard/src/WizardStep/WizardStep.tsx | 18 ++++- .../wizard/src/WizardStep/WizardStep.types.ts | 19 +++++- packages/wizard/src/WizardStep/index.ts | 3 +- packages/wizard/src/index.ts | 2 + packages/wizard/tsconfig.json | 6 ++ pnpm-lock.yaml | 6 ++ 17 files changed, 182 insertions(+), 24 deletions(-) diff --git a/packages/wizard/package.json b/packages/wizard/package.json index 91384840b9..71df134bd1 100644 --- a/packages/wizard/package.json +++ b/packages/wizard/package.json @@ -28,7 +28,9 @@ "access": "public" }, "dependencies": { + "@leafygreen-ui/button": "workspace:^", "@leafygreen-ui/emotion": "workspace:^", + "@leafygreen-ui/form-footer": "workspace:^", "@leafygreen-ui/lib": "workspace:^", "@lg-tools/test-harnesses": "workspace:^" }, diff --git a/packages/wizard/src/Wizard/Wizard.tsx b/packages/wizard/src/Wizard/Wizard.tsx index 3a5ae9d711..154aada638 100644 --- a/packages/wizard/src/Wizard/Wizard.tsx +++ b/packages/wizard/src/Wizard/Wizard.tsx @@ -1,5 +1,8 @@ import React, { Children, cloneElement, isValidElement, useState } from 'react'; +import { WizardFooter } from '../WizardFooter'; +import { WizardStep } from '../WizardStep'; + import { stepContentStyles, wizardContainerStyles } from './Wizard.styles'; import { WizardProps } from './Wizard.types'; @@ -74,3 +77,5 @@ export function Wizard({ } Wizard.displayName = 'Wizard'; +Wizard.Step = WizardStep; +Wizard.Footer = WizardFooter; diff --git a/packages/wizard/src/Wizard/Wizard.types.ts b/packages/wizard/src/Wizard/Wizard.types.ts index e87d22b8c3..7fc1a3901a 100644 --- a/packages/wizard/src/Wizard/Wizard.types.ts +++ b/packages/wizard/src/Wizard/Wizard.types.ts @@ -1,5 +1,8 @@ import { ComponentPropsWithRef, ReactNode } from 'react'; +import { WizardFooter } from '../WizardFooter'; +import { WizardStep } from '../WizardStep'; + export interface WizardProps extends ComponentPropsWithRef<'div'> { /** * The current active step index (0-based). If provided, the component operates in controlled mode. @@ -16,3 +19,10 @@ export interface WizardProps extends ComponentPropsWithRef<'div'> { */ children: ReactNode; } + +export interface WizardComponent { + (props: WizardProps): JSX.Element; + Step: typeof WizardStep; + Footer: typeof WizardFooter; + displayName: string; +} diff --git a/packages/wizard/src/Wizard/index.ts b/packages/wizard/src/Wizard/index.ts index 639f0423de..a6d6cd5342 100644 --- a/packages/wizard/src/Wizard/index.ts +++ b/packages/wizard/src/Wizard/index.ts @@ -1,2 +1,2 @@ export { Wizard } from './Wizard'; -export { type WizardProps } from './Wizard.types'; +export { type WizardComponent, type WizardProps } from './Wizard.types'; diff --git a/packages/wizard/src/WizardFooter/WizardFooter.spec.tsx b/packages/wizard/src/WizardFooter/WizardFooter.spec.tsx index f0081b35c3..20e3d25c1a 100644 --- a/packages/wizard/src/WizardFooter/WizardFooter.spec.tsx +++ b/packages/wizard/src/WizardFooter/WizardFooter.spec.tsx @@ -1,11 +1,8 @@ - import React from 'react'; import { render } from '@testing-library/react'; import { WizardFooter } from '.'; describe('packages/wizard-footer', () => { - test('condition', () => { - - }) -}) + test('condition', () => {}); +}); diff --git a/packages/wizard/src/WizardFooter/WizardFooter.styles.ts b/packages/wizard/src/WizardFooter/WizardFooter.styles.ts index 928608f58d..356c065c34 100644 --- a/packages/wizard/src/WizardFooter/WizardFooter.styles.ts +++ b/packages/wizard/src/WizardFooter/WizardFooter.styles.ts @@ -1,4 +1,3 @@ - import { css } from '@leafygreen-ui/emotion'; export const baseStyles = css``; diff --git a/packages/wizard/src/WizardFooter/WizardFooter.tsx b/packages/wizard/src/WizardFooter/WizardFooter.tsx index f0b6c5519a..16e8b6fb17 100644 --- a/packages/wizard/src/WizardFooter/WizardFooter.tsx +++ b/packages/wizard/src/WizardFooter/WizardFooter.tsx @@ -1,8 +1,70 @@ import React from 'react'; + +import FormFooter from '@leafygreen-ui/form-footer'; + import { WizardFooterProps } from './WizardFooter.types'; -export function WizardFooter({}: WizardFooterProps) { - return
your content here
; +export function WizardFooter({ + backButtonProps, + cancelButtonProps, + primaryButtonProps, + activeStep = 0, + totalSteps = 1, + onStepChange, + isControlled: _isControlled, +}: WizardFooterProps) { + // Handle back button click + const handleBackClick = () => { + if (activeStep > 0) { + onStepChange?.(activeStep - 1); + } + }; + + // Handle primary button click (forward navigation) + const handlePrimaryClick = () => { + if (activeStep < totalSteps - 1) { + onStepChange?.(activeStep + 1); + } + }; + + // Merge navigation handlers with user-provided props + const mergedBackButtonProps = backButtonProps + ? { + ...backButtonProps, + onClick: (event: React.MouseEvent) => { + backButtonProps.onClick?.(event); + if (!event.defaultPrevented) { + handleBackClick(); + } + }, + } + : undefined; + + const mergedPrimaryButtonProps = primaryButtonProps + ? { + ...primaryButtonProps, + onClick: (event: React.MouseEvent) => { + primaryButtonProps.onClick?.(event); + if (!event.defaultPrevented) { + handlePrimaryClick(); + } + }, + } + : undefined; + + // Hide back button if we're on the first step + const finalBackButtonProps = + activeStep === 0 ? undefined : mergedBackButtonProps; + + return ( + + ); } WizardFooter.displayName = 'WizardFooter'; diff --git a/packages/wizard/src/WizardFooter/WizardFooter.types.ts b/packages/wizard/src/WizardFooter/WizardFooter.types.ts index 02f3f87b43..aa0e38e45d 100644 --- a/packages/wizard/src/WizardFooter/WizardFooter.types.ts +++ b/packages/wizard/src/WizardFooter/WizardFooter.types.ts @@ -1 +1,28 @@ -export interface WizardFooterProps {} \ No newline at end of file +import { FormFooterProps } from '@leafygreen-ui/form-footer'; + +export interface WizardFooterProps { + /** + * Props for the back button (left-most button) + */ + backButtonProps?: FormFooterProps['backButtonProps']; + + /** + * Props for the cancel button (center button) + */ + cancelButtonProps?: FormFooterProps['cancelButtonProps']; + + /** + * Props for the primary button (right-most button) + */ + primaryButtonProps?: FormFooterProps['primaryButtonProps']; + + // Internal props passed by the Wizard component + /** @internal */ + activeStep?: number; + /** @internal */ + totalSteps?: number; + /** @internal */ + onStepChange?: (step: number) => void; + /** @internal */ + isControlled?: boolean; +} diff --git a/packages/wizard/src/WizardFooter/index.ts b/packages/wizard/src/WizardFooter/index.ts index bc9a177cfe..10bb26030a 100644 --- a/packages/wizard/src/WizardFooter/index.ts +++ b/packages/wizard/src/WizardFooter/index.ts @@ -1,3 +1,2 @@ - -export { WizardFooter } from './WizardFooter'; +export { WizardFooter } from './WizardFooter'; export { type WizardFooterProps } from './WizardFooter.types'; diff --git a/packages/wizard/src/WizardStep/WizardStep.spec.tsx b/packages/wizard/src/WizardStep/WizardStep.spec.tsx index fb00cde028..ebb974fe02 100644 --- a/packages/wizard/src/WizardStep/WizardStep.spec.tsx +++ b/packages/wizard/src/WizardStep/WizardStep.spec.tsx @@ -1,11 +1,8 @@ - import React from 'react'; import { render } from '@testing-library/react'; import { WizardStep } from '.'; describe('packages/wizard-step', () => { - test('condition', () => { - - }) -}) + test('condition', () => {}); +}); diff --git a/packages/wizard/src/WizardStep/WizardStep.styles.ts b/packages/wizard/src/WizardStep/WizardStep.styles.ts index 928608f58d..6b7e383c33 100644 --- a/packages/wizard/src/WizardStep/WizardStep.styles.ts +++ b/packages/wizard/src/WizardStep/WizardStep.styles.ts @@ -1,4 +1,20 @@ - import { css } from '@leafygreen-ui/emotion'; -export const baseStyles = css``; +export const stepTitleStyles = css` + font-size: 24px; + font-weight: 600; + line-height: 1.33; + margin-bottom: 8px; + color: inherit; +`; + +export const stepDescriptionStyles = css` + font-size: 16px; + line-height: 1.5; + margin-bottom: 24px; + color: inherit; +`; + +export const stepContentStyles = css` + /* Content styles */ +`; diff --git a/packages/wizard/src/WizardStep/WizardStep.tsx b/packages/wizard/src/WizardStep/WizardStep.tsx index 6c699df9e8..df3af59093 100644 --- a/packages/wizard/src/WizardStep/WizardStep.tsx +++ b/packages/wizard/src/WizardStep/WizardStep.tsx @@ -1,8 +1,22 @@ import React from 'react'; + +import { + stepContentStyles, + stepDescriptionStyles, + stepTitleStyles, +} from './WizardStep.styles'; import { WizardStepProps } from './WizardStep.types'; -export function WizardStep({}: WizardStepProps) { - return
your content here
; +export function WizardStep({ title, description, children }: WizardStepProps) { + return ( +
+
{title}
+ {description && ( +
{description}
+ )} +
{children}
+
+ ); } WizardStep.displayName = 'WizardStep'; diff --git a/packages/wizard/src/WizardStep/WizardStep.types.ts b/packages/wizard/src/WizardStep/WizardStep.types.ts index 3998534991..4657bb145f 100644 --- a/packages/wizard/src/WizardStep/WizardStep.types.ts +++ b/packages/wizard/src/WizardStep/WizardStep.types.ts @@ -1 +1,18 @@ -export interface WizardStepProps {} \ No newline at end of file +import { ReactNode } from 'react'; + +export interface WizardStepProps { + /** + * The title of the step + */ + title: ReactNode; + + /** + * The description of the step + */ + description: ReactNode; + + /** + * The content of the step + */ + children: ReactNode; +} diff --git a/packages/wizard/src/WizardStep/index.ts b/packages/wizard/src/WizardStep/index.ts index 866f9c3f6c..f7e0b02596 100644 --- a/packages/wizard/src/WizardStep/index.ts +++ b/packages/wizard/src/WizardStep/index.ts @@ -1,3 +1,2 @@ - -export { WizardStep } from './WizardStep'; +export { WizardStep } from './WizardStep'; export { type WizardStepProps } from './WizardStep.types'; diff --git a/packages/wizard/src/index.ts b/packages/wizard/src/index.ts index 623c12a572..e1baba1fd2 100644 --- a/packages/wizard/src/index.ts +++ b/packages/wizard/src/index.ts @@ -1 +1,3 @@ export { Wizard, type WizardProps } from './Wizard'; +export { WizardFooter, type WizardFooterProps } from './WizardFooter'; +export { WizardStep, type WizardStepProps } from './WizardStep'; diff --git a/packages/wizard/tsconfig.json b/packages/wizard/tsconfig.json index 5a0f368e7f..d245893b68 100644 --- a/packages/wizard/tsconfig.json +++ b/packages/wizard/tsconfig.json @@ -9,9 +9,15 @@ "include": ["src/**/*"], "exclude": ["**/*.spec.*", "**/*.stories.*"], "references": [ + { + "path": "../button" + }, { "path": "../emotion" }, + { + "path": "../form-footer" + }, { "path": "../lib" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c4e76f3070..87ca9611dd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3792,9 +3792,15 @@ importers: packages/wizard: dependencies: + '@leafygreen-ui/button': + specifier: workspace:^ + version: link:../button '@leafygreen-ui/emotion': specifier: workspace:^ version: link:../emotion + '@leafygreen-ui/form-footer': + specifier: workspace:^ + version: link:../form-footer '@leafygreen-ui/lib': specifier: workspace:^ version: link:../lib From 85db2a6c0d0edbf5f60fcfc19d83e0d6168caf56 Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Wed, 24 Sep 2025 15:58:29 -0400 Subject: [PATCH 04/28] footer& step stories --- .../src/WizardFooter/WizardFooter.stories.tsx | 132 ++++++++++++++++++ .../src/WizardStep/WizardStep.stories.tsx | 79 +++++++++++ 2 files changed, 211 insertions(+) create mode 100644 packages/wizard/src/WizardFooter/WizardFooter.stories.tsx create mode 100644 packages/wizard/src/WizardStep/WizardStep.stories.tsx diff --git a/packages/wizard/src/WizardFooter/WizardFooter.stories.tsx b/packages/wizard/src/WizardFooter/WizardFooter.stories.tsx new file mode 100644 index 0000000000..902095f157 --- /dev/null +++ b/packages/wizard/src/WizardFooter/WizardFooter.stories.tsx @@ -0,0 +1,132 @@ +import React, { useState } from 'react'; +import { + storybookArgTypes, + StoryMetaType, + StoryType, +} from '@lg-tools/storybook-utils'; + +import { WizardFooter } from '.'; + +const meta: StoryMetaType = { + title: 'Components/Wizard/WizardFooter', + component: WizardFooter, + parameters: { + default: 'LiveExample', + }, + argTypes: { + backButtonProps: { control: 'object' }, + cancelButtonProps: { control: 'object' }, + primaryButtonProps: { control: 'object' }, + activeStep: { control: 'number' }, + totalSteps: { control: 'number' }, + onStepChange: storybookArgTypes.func, + isControlled: { control: 'boolean' }, + }, +}; + +export default meta; + +const Template: StoryType = args => { + const [step, setStep] = useState(args.activeStep || 0); + + return ( + { + setStep(newStep); + args.onStepChange?.(newStep); + }} + /> + ); +}; + +export const LiveExample = Template.bind({}); +LiveExample.args = { + activeStep: 1, + totalSteps: 3, + backButtonProps: { + children: 'Back', + }, + cancelButtonProps: { + children: 'Cancel', + }, + primaryButtonProps: { + children: 'Next', + variant: 'primary', + }, +}; + +export const FirstStep = Template.bind({}); +FirstStep.args = { + activeStep: 0, + totalSteps: 3, + cancelButtonProps: { + children: 'Cancel', + }, + primaryButtonProps: { + children: 'Get Started', + variant: 'primary', + }, +}; + +export const LastStep = Template.bind({}); +LastStep.args = { + activeStep: 2, + totalSteps: 3, + backButtonProps: { + children: 'Back', + }, + cancelButtonProps: { + children: 'Cancel', + }, + primaryButtonProps: { + children: 'Finish', + variant: 'primary', + }, +}; + +export const DangerousAction = Template.bind({}); +DangerousAction.args = { + activeStep: 1, + totalSteps: 2, + backButtonProps: { + children: 'Back', + }, + cancelButtonProps: { + children: 'Cancel', + }, + primaryButtonProps: { + children: 'Delete Resource', + variant: 'danger', + }, +}; + +export const WithCustomHandlers = Template.bind({}); +WithCustomHandlers.args = { + activeStep: 1, + totalSteps: 3, + backButtonProps: { + children: 'Go Back', + onClick: () => alert('Custom back handler'), + }, + cancelButtonProps: { + children: 'Exit', + onClick: () => alert('Custom cancel handler'), + }, + primaryButtonProps: { + children: 'Continue', + variant: 'primary', + onClick: () => alert('Custom primary handler'), + }, +}; + +export const MinimalFooter = Template.bind({}); +MinimalFooter.args = { + activeStep: 0, + totalSteps: 1, + primaryButtonProps: { + children: 'Done', + variant: 'primary', + }, +}; diff --git a/packages/wizard/src/WizardStep/WizardStep.stories.tsx b/packages/wizard/src/WizardStep/WizardStep.stories.tsx new file mode 100644 index 0000000000..fade5bcd60 --- /dev/null +++ b/packages/wizard/src/WizardStep/WizardStep.stories.tsx @@ -0,0 +1,79 @@ +import React from 'react'; +import { + storybookArgTypes, + StoryMetaType, + StoryType, +} from '@lg-tools/storybook-utils'; + +import { WizardStep } from '.'; + +const meta: StoryMetaType = { + title: 'Components/Wizard/WizardStep', + component: WizardStep, + parameters: { + default: 'LiveExample', + }, + argTypes: { + title: storybookArgTypes.children, + description: storybookArgTypes.children, + children: storybookArgTypes.children, + }, +}; + +export default meta; + +const Template: StoryType = args => ; + +export const LiveExample = Template.bind({}); +LiveExample.args = { + title: 'Step 1: Basic Information', + description: 'Please provide your basic information to get started.', + children: ( +
+

This is the content of the step.

+

You can include forms, instructions, or any other content here.

+
+ ), +}; + +export const WithLongDescription = Template.bind({}); +WithLongDescription.args = { + title: 'Step 2: Detailed Configuration', + description: ( +
+

+ This step involves more complex configuration options. Please read + carefully before proceeding. +

+
    +
  • Configure your primary settings
  • +
  • Set up your preferences
  • +
  • Review the terms and conditions
  • +
+
+ ), + children: ( +
+

Complex form content would go here...

+ +
+ ), +}; + +export const MinimalStep = Template.bind({}); +MinimalStep.args = { + title: 'Final Step', + description: 'Review and submit.', + children:

Simple content for the final step.

, +}; + +export const WithoutDescription = Template.bind({}); +WithoutDescription.args = { + title: 'Step Without Description', + children: ( +
+

This step doesn't have a description.

+

Sometimes you may want to omit the description for simpler steps.

+
+ ), +}; From e3c65c88a6362d97b1a0d7263596b87cf93f591f Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Wed, 24 Sep 2025 16:44:26 -0400 Subject: [PATCH 05/28] temp useWizardControlledValue --- packages/wizard/src/Wizard/Wizard.tsx | 18 ++-- .../utils/useWizardControlledValue/index.ts | 1 + .../useWizardControlledValue.ts | 95 +++++++++++++++++++ 3 files changed, 105 insertions(+), 9 deletions(-) create mode 100644 packages/wizard/src/utils/useWizardControlledValue/index.ts create mode 100644 packages/wizard/src/utils/useWizardControlledValue/useWizardControlledValue.ts diff --git a/packages/wizard/src/Wizard/Wizard.tsx b/packages/wizard/src/Wizard/Wizard.tsx index 154aada638..bdf2c35956 100644 --- a/packages/wizard/src/Wizard/Wizard.tsx +++ b/packages/wizard/src/Wizard/Wizard.tsx @@ -1,5 +1,6 @@ -import React, { Children, cloneElement, isValidElement, useState } from 'react'; +import React, { Children, cloneElement, isValidElement } from 'react'; +import { useWizardControlledValue } from '../utils/useWizardControlledValue/useWizardControlledValue'; import { WizardFooter } from '../WizardFooter'; import { WizardStep } from '../WizardStep'; @@ -7,17 +8,16 @@ import { stepContentStyles, wizardContainerStyles } from './Wizard.styles'; import { WizardProps } from './Wizard.types'; export function Wizard({ - activeStep: controlledActiveStep, + activeStep: activeStepProp, onStepChange, children, }: WizardProps) { - // TODO: replace with `useControlledValue` - // Internal state for uncontrolled mode - const [internalActiveStep, setInternalActiveStep] = useState(0); - - // Use controlled prop if provided, otherwise use internal state - const isControlled = controlledActiveStep !== undefined; - const activeStep = isControlled ? controlledActiveStep : internalActiveStep; + // Controlled/Uncontrolled activeStep value + const { + isControlled, + value: activeStep, + setValue: setInternalActiveStep, + } = useWizardControlledValue(activeStepProp, undefined, 0); // Handle step changes const handleStepChange = (newStep: number) => { diff --git a/packages/wizard/src/utils/useWizardControlledValue/index.ts b/packages/wizard/src/utils/useWizardControlledValue/index.ts new file mode 100644 index 0000000000..b515757d26 --- /dev/null +++ b/packages/wizard/src/utils/useWizardControlledValue/index.ts @@ -0,0 +1 @@ +export { useControlledValue } from './useControlledValue'; diff --git a/packages/wizard/src/utils/useWizardControlledValue/useWizardControlledValue.ts b/packages/wizard/src/utils/useWizardControlledValue/useWizardControlledValue.ts new file mode 100644 index 0000000000..18326bf013 --- /dev/null +++ b/packages/wizard/src/utils/useWizardControlledValue/useWizardControlledValue.ts @@ -0,0 +1,95 @@ +import { useEffect, useMemo, useState } from 'react'; +import isUndefined from 'lodash/isUndefined'; + +import { usePrevious } from '@leafygreen-ui/hooks'; +import { consoleOnce } from '@leafygreen-ui/lib'; + +interface ControlledValueReturnObject { + /** Whether the value is controlled */ + isControlled: boolean; + + /** The controlled or uncontrolled value */ + value: T; + + /** + * Either updates the uncontrolled value, + * or calls the provided `onChange` callback + */ + setValue: (newVal?: T, ...args: Array) => void; +} + +/** + * A hook that enables a component to be both controlled or uncontrolled. + * + * Returns a {@link ControlledValueReturnObject} + * @deprecated Use `useControlled` from `@leafygreen-ui/hooks` instead + * https://github.com/mongodb/leafygreen-ui/pull/3153 + */ +export const useWizardControlledValue = ( + valueProp?: T, + onChange?: (val?: T, ...args: Array) => void, + initialProp?: T, +): ControlledValueReturnObject => { + // Initially set isControlled to the existence of `valueProp`. + // If the value prop changes from undefined to something defined, + // then isControlled is set to true, + // and will remain true for the life of the component + const [isControlled, setControlled] = useState(!isUndefined(valueProp)); + useEffect(() => { + setControlled(isControlled || !isUndefined(valueProp)); + }, [isControlled, valueProp]); + + const wasControlled = usePrevious(isControlled); + + useEffect(() => { + if (isUndefined(isControlled) || isUndefined(wasControlled)) return; + + if (isControlled !== wasControlled) { + const err = `WARN: A component changed from ${ + wasControlled ? 'controlled' : 'uncontrolled' + } to ${ + isControlled ? 'controlled' : 'uncontrolled' + }. This can cause issues with React states. ${ + isControlled + ? 'To control a component, but have an initially empty input, consider setting the `value` prop to `null`.' + : '' + }`; + + consoleOnce.warn(err); + } + }, [isControlled, wasControlled]); + + // We set the initial value to either the `value` + // or the temporary `initialValue` prop + const initialValue = useMemo( + () => (isControlled ? valueProp : initialProp), + [initialProp, isControlled, valueProp], + ); + + // Keep track of the internal value state + const [uncontrolledValue, setUncontrolledValue] = useState( + initialValue, + ); + + // The returned value is wither the provided value prop + // or the uncontrolled value + const value = useMemo( + () => (isControlled ? (valueProp as T) : (uncontrolledValue as T)), + [isControlled, uncontrolledValue, valueProp], + ); + + // A wrapper around `handleChange` that fires a simulated event + const setValue = (newVal: T | undefined) => { + if (!isControlled) { + setUncontrolledValue(newVal); + } + + onChange?.(newVal); + }; + + return { + isControlled, + value, + setValue, + }; +}; From 76cb00c07f35d467656cdcd54922aed6f875fe3b Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Fri, 26 Sep 2025 16:29:46 -0400 Subject: [PATCH 06/28] fix useWizardControlledValue --- packages/wizard/src/utils/useWizardControlledValue/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/wizard/src/utils/useWizardControlledValue/index.ts b/packages/wizard/src/utils/useWizardControlledValue/index.ts index b515757d26..07ea91dbb7 100644 --- a/packages/wizard/src/utils/useWizardControlledValue/index.ts +++ b/packages/wizard/src/utils/useWizardControlledValue/index.ts @@ -1 +1 @@ -export { useControlledValue } from './useControlledValue'; +export { useWizardControlledValue } from './useWizardControlledValue'; From 17a55ccfbbbb5527148dcad362a5938d0c2709a5 Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Fri, 26 Sep 2025 16:30:20 -0400 Subject: [PATCH 07/28] update Footer --- .../src/WizardFooter/WizardFooter.stories.tsx | 173 ++++++------------ .../wizard/src/WizardFooter/WizardFooter.tsx | 51 +----- .../src/WizardFooter/WizardFooter.types.ts | 12 +- 3 files changed, 61 insertions(+), 175 deletions(-) diff --git a/packages/wizard/src/WizardFooter/WizardFooter.stories.tsx b/packages/wizard/src/WizardFooter/WizardFooter.stories.tsx index 902095f157..17811859c9 100644 --- a/packages/wizard/src/WizardFooter/WizardFooter.stories.tsx +++ b/packages/wizard/src/WizardFooter/WizardFooter.stories.tsx @@ -1,132 +1,75 @@ -import React, { useState } from 'react'; -import { - storybookArgTypes, - StoryMetaType, - StoryType, -} from '@lg-tools/storybook-utils'; +import React from 'react'; +import { StoryMetaType } from '@lg-tools/storybook-utils'; +import { StoryObj } from '@storybook/react'; -import { WizardFooter } from '.'; +import { Variant } from '@leafygreen-ui/button'; +import Icon, { glyphs } from '@leafygreen-ui/icon'; -const meta: StoryMetaType = { +import { WizardFooter, type WizardFooterProps } from '.'; + +type PrimaryButtonVariant = + Required['primaryButtonProps']['variant']; +interface StoryArgs { + backButtonText: string; + backButtonIcon: keyof typeof glyphs; + cancelButtonText: string; + primaryButtonText: string; + primaryButtonIcon: keyof typeof glyphs; + primaryButtonVariant: PrimaryButtonVariant; +} + +const meta: StoryMetaType = { title: 'Components/Wizard/WizardFooter', component: WizardFooter, parameters: { default: 'LiveExample', + controls: { + exclude: ['backButtonProps', 'cancelButtonProps', 'primaryButtonProps'], + }, }, + args: {}, argTypes: { - backButtonProps: { control: 'object' }, - cancelButtonProps: { control: 'object' }, - primaryButtonProps: { control: 'object' }, - activeStep: { control: 'number' }, - totalSteps: { control: 'number' }, - onStepChange: storybookArgTypes.func, - isControlled: { control: 'boolean' }, + backButtonText: { control: 'text' }, + backButtonIcon: { control: 'select', options: Object.keys(glyphs) }, + cancelButtonText: { control: 'text' }, + primaryButtonText: { control: 'text' }, + primaryButtonIcon: { control: 'select', options: Object.keys(glyphs) }, + primaryButtonVariant: { + control: 'select', + options: [Variant.Primary, Variant.Danger], + }, }, }; export default meta; -const Template: StoryType = args => { - const [step, setStep] = useState(args.activeStep || 0); - - return ( +export const LiveExample: StoryObj = { + args: { + backButtonText: 'Back', + backButtonIcon: 'ArrowLeft', + cancelButtonText: 'Cancel', + primaryButtonText: 'Continue', + primaryButtonIcon: 'Ellipsis', + primaryButtonVariant: Variant.Primary, + }, + render: args => ( { - setStep(newStep); - args.onStepChange?.(newStep); + backButtonProps={{ + leftGlyph: args.backButtonIcon ? ( + + ) : undefined, + children: args.backButtonText, + }} + cancelButtonProps={{ + children: args.cancelButtonText, + }} + primaryButtonProps={{ + leftGlyph: args.primaryButtonIcon ? ( + + ) : undefined, + children: args.primaryButtonText, + variant: args.primaryButtonVariant, }} /> - ); -}; - -export const LiveExample = Template.bind({}); -LiveExample.args = { - activeStep: 1, - totalSteps: 3, - backButtonProps: { - children: 'Back', - }, - cancelButtonProps: { - children: 'Cancel', - }, - primaryButtonProps: { - children: 'Next', - variant: 'primary', - }, -}; - -export const FirstStep = Template.bind({}); -FirstStep.args = { - activeStep: 0, - totalSteps: 3, - cancelButtonProps: { - children: 'Cancel', - }, - primaryButtonProps: { - children: 'Get Started', - variant: 'primary', - }, -}; - -export const LastStep = Template.bind({}); -LastStep.args = { - activeStep: 2, - totalSteps: 3, - backButtonProps: { - children: 'Back', - }, - cancelButtonProps: { - children: 'Cancel', - }, - primaryButtonProps: { - children: 'Finish', - variant: 'primary', - }, -}; - -export const DangerousAction = Template.bind({}); -DangerousAction.args = { - activeStep: 1, - totalSteps: 2, - backButtonProps: { - children: 'Back', - }, - cancelButtonProps: { - children: 'Cancel', - }, - primaryButtonProps: { - children: 'Delete Resource', - variant: 'danger', - }, -}; - -export const WithCustomHandlers = Template.bind({}); -WithCustomHandlers.args = { - activeStep: 1, - totalSteps: 3, - backButtonProps: { - children: 'Go Back', - onClick: () => alert('Custom back handler'), - }, - cancelButtonProps: { - children: 'Exit', - onClick: () => alert('Custom cancel handler'), - }, - primaryButtonProps: { - children: 'Continue', - variant: 'primary', - onClick: () => alert('Custom primary handler'), - }, -}; - -export const MinimalFooter = Template.bind({}); -MinimalFooter.args = { - activeStep: 0, - totalSteps: 1, - primaryButtonProps: { - children: 'Done', - variant: 'primary', - }, + ), }; diff --git a/packages/wizard/src/WizardFooter/WizardFooter.tsx b/packages/wizard/src/WizardFooter/WizardFooter.tsx index 16e8b6fb17..e314e7f97d 100644 --- a/packages/wizard/src/WizardFooter/WizardFooter.tsx +++ b/packages/wizard/src/WizardFooter/WizardFooter.tsx @@ -8,61 +8,14 @@ export function WizardFooter({ backButtonProps, cancelButtonProps, primaryButtonProps, - activeStep = 0, - totalSteps = 1, - onStepChange, - isControlled: _isControlled, }: WizardFooterProps) { // Handle back button click - const handleBackClick = () => { - if (activeStep > 0) { - onStepChange?.(activeStep - 1); - } - }; - - // Handle primary button click (forward navigation) - const handlePrimaryClick = () => { - if (activeStep < totalSteps - 1) { - onStepChange?.(activeStep + 1); - } - }; - - // Merge navigation handlers with user-provided props - const mergedBackButtonProps = backButtonProps - ? { - ...backButtonProps, - onClick: (event: React.MouseEvent) => { - backButtonProps.onClick?.(event); - if (!event.defaultPrevented) { - handleBackClick(); - } - }, - } - : undefined; - - const mergedPrimaryButtonProps = primaryButtonProps - ? { - ...primaryButtonProps, - onClick: (event: React.MouseEvent) => { - primaryButtonProps.onClick?.(event); - if (!event.defaultPrevented) { - handlePrimaryClick(); - } - }, - } - : undefined; - - // Hide back button if we're on the first step - const finalBackButtonProps = - activeStep === 0 ? undefined : mergedBackButtonProps; return ( ); } diff --git a/packages/wizard/src/WizardFooter/WizardFooter.types.ts b/packages/wizard/src/WizardFooter/WizardFooter.types.ts index aa0e38e45d..d18b20c7ea 100644 --- a/packages/wizard/src/WizardFooter/WizardFooter.types.ts +++ b/packages/wizard/src/WizardFooter/WizardFooter.types.ts @@ -14,15 +14,5 @@ export interface WizardFooterProps { /** * Props for the primary button (right-most button) */ - primaryButtonProps?: FormFooterProps['primaryButtonProps']; - - // Internal props passed by the Wizard component - /** @internal */ - activeStep?: number; - /** @internal */ - totalSteps?: number; - /** @internal */ - onStepChange?: (step: number) => void; - /** @internal */ - isControlled?: boolean; + primaryButtonProps: FormFooterProps['primaryButtonProps']; } From aef300e2aff751a21f4210db5b3c77ec865a4727 Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Fri, 26 Sep 2025 16:31:07 -0400 Subject: [PATCH 08/28] Update package.json --- packages/wizard/package.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/wizard/package.json b/packages/wizard/package.json index 71df134bd1..0cbb6e6f86 100644 --- a/packages/wizard/package.json +++ b/packages/wizard/package.json @@ -32,8 +32,12 @@ "@leafygreen-ui/emotion": "workspace:^", "@leafygreen-ui/form-footer": "workspace:^", "@leafygreen-ui/lib": "workspace:^", + "@leafygreen-ui/typography": "workspace:^", "@lg-tools/test-harnesses": "workspace:^" }, + "devDependencies" : { + "@leafygreen-ui/icon": "workspace:^" + }, "homepage": "https://github.com/mongodb/leafygreen-ui/tree/main/packages/wizard", "repository": { "type": "git", From f5d32af71782e572f93a88496913e1a6dfc4afa0 Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Fri, 26 Sep 2025 16:33:33 -0400 Subject: [PATCH 09/28] use typography in Step --- .../wizard/src/WizardStep/WizardStep.styles.ts | 15 --------------- packages/wizard/src/WizardStep/WizardStep.tsx | 14 +++++--------- 2 files changed, 5 insertions(+), 24 deletions(-) diff --git a/packages/wizard/src/WizardStep/WizardStep.styles.ts b/packages/wizard/src/WizardStep/WizardStep.styles.ts index 6b7e383c33..2d39619f78 100644 --- a/packages/wizard/src/WizardStep/WizardStep.styles.ts +++ b/packages/wizard/src/WizardStep/WizardStep.styles.ts @@ -1,20 +1,5 @@ import { css } from '@leafygreen-ui/emotion'; -export const stepTitleStyles = css` - font-size: 24px; - font-weight: 600; - line-height: 1.33; - margin-bottom: 8px; - color: inherit; -`; - -export const stepDescriptionStyles = css` - font-size: 16px; - line-height: 1.5; - margin-bottom: 24px; - color: inherit; -`; - export const stepContentStyles = css` /* Content styles */ `; diff --git a/packages/wizard/src/WizardStep/WizardStep.tsx b/packages/wizard/src/WizardStep/WizardStep.tsx index df3af59093..b161050078 100644 --- a/packages/wizard/src/WizardStep/WizardStep.tsx +++ b/packages/wizard/src/WizardStep/WizardStep.tsx @@ -1,19 +1,15 @@ import React from 'react'; -import { - stepContentStyles, - stepDescriptionStyles, - stepTitleStyles, -} from './WizardStep.styles'; +import { Description, H3 } from '@leafygreen-ui/typography'; + +import { stepContentStyles } from './WizardStep.styles'; import { WizardStepProps } from './WizardStep.types'; export function WizardStep({ title, description, children }: WizardStepProps) { return (
-
{title}
- {description && ( -
{description}
- )} +

{title}

+ {description && {description}}
{children}
); From 9d5f5799ac2c3b485c9c746010bddb564482fbba Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Fri, 26 Sep 2025 18:26:28 -0400 Subject: [PATCH 10/28] update descendants --- .changeset/descendants-exports.md | 5 +++++ packages/descendants/src/Highlight/index.ts | 13 +++++++------ packages/descendants/src/index.ts | 3 ++- 3 files changed, 14 insertions(+), 7 deletions(-) create mode 100644 .changeset/descendants-exports.md diff --git a/.changeset/descendants-exports.md b/.changeset/descendants-exports.md new file mode 100644 index 0000000000..dc9167bd65 --- /dev/null +++ b/.changeset/descendants-exports.md @@ -0,0 +1,5 @@ +--- +'@leafygreen-ui/descendants': minor +--- + +Exports `Position` enum. Removes type annotation from `Direction` export diff --git a/packages/descendants/src/Highlight/index.ts b/packages/descendants/src/Highlight/index.ts index 32c2e6536f..0ac8e5e878 100644 --- a/packages/descendants/src/Highlight/index.ts +++ b/packages/descendants/src/Highlight/index.ts @@ -1,10 +1,11 @@ -export type { +export { Direction, - HighlightChangeHandler, - HighlightContextProps, - HighlightHookReturnType, - Index, - UseHighlightOptions, + type HighlightChangeHandler, + type HighlightContextProps, + type HighlightHookReturnType, + type Index, + Position, + type UseHighlightOptions, } from './highlight.types'; export { createHighlightContext, diff --git a/packages/descendants/src/index.ts b/packages/descendants/src/index.ts index c5c6aada04..5f722dab2b 100644 --- a/packages/descendants/src/index.ts +++ b/packages/descendants/src/index.ts @@ -15,13 +15,14 @@ export { // Highlight export { createHighlightContext, - type Direction, + Direction, type HighlightChangeHandler, type HighlightContextProps, type HighlightContextType, type HighlightHookReturnType, HighlightProvider, type Index, + Position, useHighlight, useHighlightContext, type UseHighlightOptions, From b18d5b96cfd8643a0862c123cd6770aa5462cce2 Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Fri, 26 Sep 2025 18:26:41 -0400 Subject: [PATCH 11/28] update packages --- packages/wizard/package.json | 3 ++- pnpm-lock.yaml | 10 ++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/wizard/package.json b/packages/wizard/package.json index 0cbb6e6f86..18685e8e0c 100644 --- a/packages/wizard/package.json +++ b/packages/wizard/package.json @@ -36,7 +36,8 @@ "@lg-tools/test-harnesses": "workspace:^" }, "devDependencies" : { - "@leafygreen-ui/icon": "workspace:^" + "@leafygreen-ui/icon": "workspace:^", + "@faker-js/faker": "^8.0.0" }, "homepage": "https://github.com/mongodb/leafygreen-ui/tree/main/packages/wizard", "repository": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 87ca9611dd..0fd8eb3ad3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3804,9 +3804,19 @@ importers: '@leafygreen-ui/lib': specifier: workspace:^ version: link:../lib + '@leafygreen-ui/typography': + specifier: workspace:^ + version: link:../typography '@lg-tools/test-harnesses': specifier: workspace:^ version: link:../../tools/test-harnesses + devDependencies: + '@faker-js/faker': + specifier: ^8.0.0 + version: 8.0.2 + '@leafygreen-ui/icon': + specifier: workspace:^ + version: link:../icon tools/build: dependencies: From 8f62f2ec0c92a13a7eabdcc0d91939374d661aa7 Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Fri, 26 Sep 2025 18:27:00 -0400 Subject: [PATCH 12/28] the rest of the owl --- packages/wizard/src/Wizard.stories.tsx | 85 +++++++++++++++++-- packages/wizard/src/Wizard/Wizard.styles.ts | 3 + packages/wizard/src/Wizard/Wizard.tsx | 77 ++++++++--------- .../wizard/src/WizardContext/WizardContext.ts | 15 ++++ .../wizard/src/WizardFooter/WizardFooter.tsx | 39 +++++++-- .../src/WizardStep/WizardStep.styles.ts | 5 +- packages/wizard/src/WizardStep/WizardStep.tsx | 6 +- .../wizard/src/WizardStep/WizardStep.types.ts | 2 +- packages/wizard/src/constants.ts | 2 + .../useWizardControlledValue.ts | 17 ++-- 10 files changed, 184 insertions(+), 67 deletions(-) create mode 100644 packages/wizard/src/WizardContext/WizardContext.ts create mode 100644 packages/wizard/src/constants.ts diff --git a/packages/wizard/src/Wizard.stories.tsx b/packages/wizard/src/Wizard.stories.tsx index 906189c013..9102d347f9 100644 --- a/packages/wizard/src/Wizard.stories.tsx +++ b/packages/wizard/src/Wizard.stories.tsx @@ -1,12 +1,29 @@ +/* eslint-disable no-console */ import React from 'react'; +import { faker } from '@faker-js/faker'; +import { StoryMetaType } from '@lg-tools/storybook-utils'; import { StoryObj } from '@storybook/react'; +import Card from '@leafygreen-ui/card'; + import { Wizard } from '.'; +faker.seed(0); + export default { title: 'Components/Wizard', component: Wizard, -}; + parameters: { + default: 'LiveExample', + }, + decorators: [ + Fn => ( +
+ +
+ ), + ], +} satisfies StoryMetaType; export const LiveExample: StoryObj = { parameters: { @@ -14,7 +31,32 @@ export const LiveExample: StoryObj = { exclude: ['children', 'activeStep', 'onStepChange'], }, }, - render: props => , + render: props => ( + + {['Apple', 'Banana', 'Carrot'].map((title, i) => ( + + {faker.lorem.paragraph(10)} + + ))} + console.log('[Storybook] Clicked Back'), + }} + cancelButtonProps={{ + children: 'Cancel', + onClick: () => console.log('[Storybook] Clicked Cancel'), + }} + primaryButtonProps={{ + children: 'Primary', + onClick: () => console.log('[Storybook] Clicked Primary'), + }} + /> + + ), }; export const Controlled: StoryObj = { @@ -23,14 +65,47 @@ export const Controlled: StoryObj = { exclude: ['children', 'onStepChange'], }, }, + args: { + activeStep: 0, + }, render: ({ activeStep, ...props }) => { return ( console.log(`Set activeStep to ${x}`)} + onStepChange={x => + console.log(`[Storybook] activeStep should change to ${x}`) + } {...props} - > + > + {['Apple', 'Banana', 'Carrot'].map((title, i) => ( + + +

+ This Wizard is controlled. Clicking the buttons will not do + anything. Use the Storybook controls to see the next step +

+ {faker.lorem.paragraph(10)} +
+
+ ))} + console.log('[Storybook] Clicked Back'), + }} + cancelButtonProps={{ + children: 'Cancel', + onClick: () => console.log('[Storybook] Clicked Cancel'), + }} + primaryButtonProps={{ + children: 'Primary', + onClick: () => console.log('[Storybook] Clicked Primary'), + }} + /> +
); }, }; diff --git a/packages/wizard/src/Wizard/Wizard.styles.ts b/packages/wizard/src/Wizard/Wizard.styles.ts index 77476b439e..1290d0b305 100644 --- a/packages/wizard/src/Wizard/Wizard.styles.ts +++ b/packages/wizard/src/Wizard/Wizard.styles.ts @@ -1,9 +1,12 @@ import { css } from '@leafygreen-ui/emotion'; export const wizardContainerStyles = css` + width: 100%; + height: 100%; display: flex; flex-direction: column; gap: 24px; + outline: 1px solid red; `; export const stepContentStyles = css` diff --git a/packages/wizard/src/Wizard/Wizard.tsx b/packages/wizard/src/Wizard/Wizard.tsx index bdf2c35956..43ca40071e 100644 --- a/packages/wizard/src/Wizard/Wizard.tsx +++ b/packages/wizard/src/Wizard/Wizard.tsx @@ -1,6 +1,11 @@ -import React, { Children, cloneElement, isValidElement } from 'react'; +import React, { Children, isValidElement } from 'react'; +import { Direction } from '@leafygreen-ui/descendants'; +import { findChild } from '@leafygreen-ui/lib'; + +import { WIZARD_FOOTER_KEY } from '../constants'; import { useWizardControlledValue } from '../utils/useWizardControlledValue/useWizardControlledValue'; +import { WizardContext } from '../WizardContext/WizardContext'; import { WizardFooter } from '../WizardFooter'; import { WizardStep } from '../WizardStep'; @@ -16,23 +21,10 @@ export function Wizard({ const { isControlled, value: activeStep, - setValue: setInternalActiveStep, + setValue: setActiveStep, } = useWizardControlledValue(activeStepProp, undefined, 0); - // Handle step changes - const handleStepChange = (newStep: number) => { - if (!isControlled) { - setInternalActiveStep(newStep); - } - onStepChange?.(newStep); - }; - - // Filter children to separate steps from footer - const childrenArray = Children.toArray(children); - - // For now, we'll look for components with displayName ending in 'Step' or 'Footer' - // This will be more precise once Wizard.Step and Wizard.Footer are implemented - const stepChildren = childrenArray.filter(child => { + const stepChildren = Children.toArray(children).filter(child => { if (isValidElement(child)) { const displayName = (child.type as any)?.displayName; return displayName && displayName.includes('Step'); @@ -41,38 +33,41 @@ export function Wizard({ return false; }); - const footerChild = childrenArray.find(child => { - if (isValidElement(child)) { - const displayName = (child.type as any)?.displayName; - return displayName && displayName.includes('Footer'); + const updateStep = (direction: Direction) => { + const getNextStep = (curr: number) => { + switch (direction) { + case Direction.Next: + return Math.min(curr + 1, stepChildren.length - 1); + case Direction.Prev: + return Math.max(curr - 1, 0); + } + }; + + if (!isControlled) { + setActiveStep(getNextStep); } - return false; - }); + onStepChange?.(getNextStep(activeStep)); + }; + + const footerChild = findChild(children, WIZARD_FOOTER_KEY); // Get the current step to render const currentStep = stepChildren[activeStep] || null; - // Clone footer with step navigation handlers if it exists - const clonedFooter = - footerChild && isValidElement(footerChild) - ? cloneElement(footerChild as React.ReactElement, { - activeStep, - totalSteps: stepChildren.length, - onStepChange: handleStepChange, - isControlled, - }) - : null; - return ( -
- activeStep: {activeStep} - {/* Render current step */} -
{currentStep}
- - {/* Render footer */} - {clonedFooter} -
+ +
+
{currentStep}
+ {/* Render footer */} + {footerChild} +
+
); } diff --git a/packages/wizard/src/WizardContext/WizardContext.ts b/packages/wizard/src/WizardContext/WizardContext.ts new file mode 100644 index 0000000000..dd41f60fd4 --- /dev/null +++ b/packages/wizard/src/WizardContext/WizardContext.ts @@ -0,0 +1,15 @@ +import { createContext, useContext } from 'react'; + +import { Direction } from '@leafygreen-ui/descendants'; + +interface WizardContextData { + activeStep: number; + updateStep: (direction: Direction) => void; +} + +export const WizardContext = createContext({ + activeStep: 0, + updateStep: () => {}, +}); + +export const useWizardContext = () => useContext(WizardContext); diff --git a/packages/wizard/src/WizardFooter/WizardFooter.tsx b/packages/wizard/src/WizardFooter/WizardFooter.tsx index e314e7f97d..c39da6be28 100644 --- a/packages/wizard/src/WizardFooter/WizardFooter.tsx +++ b/packages/wizard/src/WizardFooter/WizardFooter.tsx @@ -1,23 +1,48 @@ -import React from 'react'; +import React, { MouseEventHandler } from 'react'; +import { Direction } from '@leafygreen-ui/descendants'; import FormFooter from '@leafygreen-ui/form-footer'; +import { WIZARD_FOOTER_KEY } from '../constants'; +import { useWizardContext } from '../WizardContext/WizardContext'; + import { WizardFooterProps } from './WizardFooter.types'; -export function WizardFooter({ +export const WizardFooter = ({ backButtonProps, cancelButtonProps, primaryButtonProps, -}: WizardFooterProps) { - // Handle back button click +}: WizardFooterProps) => { + const { activeStep, updateStep } = useWizardContext(); + + const handleBackButtonClick: MouseEventHandler = e => { + updateStep(Direction.Prev); + backButtonProps?.onClick?.(e); + }; + + const handlePrimaryButtonClick: MouseEventHandler = e => { + updateStep(Direction.Next); + primaryButtonProps.onClick?.(e); + }; return ( 0 + ? { + ...backButtonProps, + onClick: handleBackButtonClick, + } + : undefined + } cancelButtonProps={cancelButtonProps} - primaryButtonProps={primaryButtonProps} + primaryButtonProps={{ + ...primaryButtonProps, + onClick: handlePrimaryButtonClick, + }} /> ); -} +}; WizardFooter.displayName = 'WizardFooter'; +WizardFooter[WIZARD_FOOTER_KEY] = true; diff --git a/packages/wizard/src/WizardStep/WizardStep.styles.ts b/packages/wizard/src/WizardStep/WizardStep.styles.ts index 2d39619f78..b38acdf587 100644 --- a/packages/wizard/src/WizardStep/WizardStep.styles.ts +++ b/packages/wizard/src/WizardStep/WizardStep.styles.ts @@ -1,5 +1,6 @@ import { css } from '@leafygreen-ui/emotion'; +import { spacing } from '@leafygreen-ui/tokens'; -export const stepContentStyles = css` - /* Content styles */ +export const stepStyles = css` + padding: 0 ${spacing[1800]}px; `; diff --git a/packages/wizard/src/WizardStep/WizardStep.tsx b/packages/wizard/src/WizardStep/WizardStep.tsx index b161050078..d0b46a155d 100644 --- a/packages/wizard/src/WizardStep/WizardStep.tsx +++ b/packages/wizard/src/WizardStep/WizardStep.tsx @@ -2,15 +2,15 @@ import React from 'react'; import { Description, H3 } from '@leafygreen-ui/typography'; -import { stepContentStyles } from './WizardStep.styles'; +import { stepStyles } from './WizardStep.styles'; import { WizardStepProps } from './WizardStep.types'; export function WizardStep({ title, description, children }: WizardStepProps) { return ( -
+

{title}

{description && {description}} -
{children}
+
{children}
); } diff --git a/packages/wizard/src/WizardStep/WizardStep.types.ts b/packages/wizard/src/WizardStep/WizardStep.types.ts index 4657bb145f..6d654b2d70 100644 --- a/packages/wizard/src/WizardStep/WizardStep.types.ts +++ b/packages/wizard/src/WizardStep/WizardStep.types.ts @@ -9,7 +9,7 @@ export interface WizardStepProps { /** * The description of the step */ - description: ReactNode; + description?: ReactNode; /** * The content of the step diff --git a/packages/wizard/src/constants.ts b/packages/wizard/src/constants.ts new file mode 100644 index 0000000000..cdbac183a2 --- /dev/null +++ b/packages/wizard/src/constants.ts @@ -0,0 +1,2 @@ +export const WIZARD_STEP_KEY = 'isWizardStep'; +export const WIZARD_FOOTER_KEY = 'isWizardFooter'; diff --git a/packages/wizard/src/utils/useWizardControlledValue/useWizardControlledValue.ts b/packages/wizard/src/utils/useWizardControlledValue/useWizardControlledValue.ts index 18326bf013..1081c3691c 100644 --- a/packages/wizard/src/utils/useWizardControlledValue/useWizardControlledValue.ts +++ b/packages/wizard/src/utils/useWizardControlledValue/useWizardControlledValue.ts @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useState } from 'react'; +import { Dispatch, SetStateAction, useEffect, useMemo, useState } from 'react'; import isUndefined from 'lodash/isUndefined'; import { usePrevious } from '@leafygreen-ui/hooks'; @@ -15,7 +15,7 @@ interface ControlledValueReturnObject { * Either updates the uncontrolled value, * or calls the provided `onChange` callback */ - setValue: (newVal?: T, ...args: Array) => void; + setValue: Dispatch>; } /** @@ -61,14 +61,14 @@ export const useWizardControlledValue = ( // We set the initial value to either the `value` // or the temporary `initialValue` prop - const initialValue = useMemo( - () => (isControlled ? valueProp : initialProp), + const initialValue: T = useMemo( + () => (isControlled ? (valueProp as T) : (initialProp as T)), [initialProp, isControlled, valueProp], ); // Keep track of the internal value state - const [uncontrolledValue, setUncontrolledValue] = useState( - initialValue, + const [uncontrolledValue, setUncontrolledValue] = useState( + initialValue as T, ); // The returned value is wither the provided value prop @@ -79,12 +79,13 @@ export const useWizardControlledValue = ( ); // A wrapper around `handleChange` that fires a simulated event - const setValue = (newVal: T | undefined) => { + const setValue: Dispatch> = newVal => { if (!isControlled) { setUncontrolledValue(newVal); } - onChange?.(newVal); + const val = typeof newVal === 'function' ? (newVal as Function)() : newVal; + onChange?.(val); }; return { From 85fe610c1a5c7623728e40cdd7cdf42c6012ec39 Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Fri, 26 Sep 2025 18:32:57 -0400 Subject: [PATCH 13/28] update width --- packages/wizard/src/Wizard.stories.tsx | 2 +- packages/wizard/src/WizardFooter/WizardFooter.styles.ts | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/wizard/src/Wizard.stories.tsx b/packages/wizard/src/Wizard.stories.tsx index 9102d347f9..c26ccb4f5b 100644 --- a/packages/wizard/src/Wizard.stories.tsx +++ b/packages/wizard/src/Wizard.stories.tsx @@ -18,7 +18,7 @@ export default { }, decorators: [ Fn => ( -
+
), diff --git a/packages/wizard/src/WizardFooter/WizardFooter.styles.ts b/packages/wizard/src/WizardFooter/WizardFooter.styles.ts index 356c065c34..90e2e8cc60 100644 --- a/packages/wizard/src/WizardFooter/WizardFooter.styles.ts +++ b/packages/wizard/src/WizardFooter/WizardFooter.styles.ts @@ -1,3 +1,5 @@ import { css } from '@leafygreen-ui/emotion'; -export const baseStyles = css``; +export const baseStyles = css` + width: 100%; +`; From 4f19bf297308ecba7a3399ef521e54e3b8d57764 Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Tue, 30 Sep 2025 15:53:10 -0400 Subject: [PATCH 14/28] fix nits --- packages/wizard/src/Wizard/Wizard.styles.ts | 4 ++-- packages/wizard/src/Wizard/Wizard.tsx | 4 ++-- packages/wizard/src/WizardFooter/WizardFooter.tsx | 4 ++-- packages/wizard/src/constants.ts | 8 ++++++-- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/packages/wizard/src/Wizard/Wizard.styles.ts b/packages/wizard/src/Wizard/Wizard.styles.ts index 1290d0b305..c6ca33aaee 100644 --- a/packages/wizard/src/Wizard/Wizard.styles.ts +++ b/packages/wizard/src/Wizard/Wizard.styles.ts @@ -1,12 +1,12 @@ import { css } from '@leafygreen-ui/emotion'; +import { spacing } from '@leafygreen-ui/tokens'; export const wizardContainerStyles = css` width: 100%; height: 100%; display: flex; flex-direction: column; - gap: 24px; - outline: 1px solid red; + gap: ${spacing[600]}px; `; export const stepContentStyles = css` diff --git a/packages/wizard/src/Wizard/Wizard.tsx b/packages/wizard/src/Wizard/Wizard.tsx index 43ca40071e..68bd357d02 100644 --- a/packages/wizard/src/Wizard/Wizard.tsx +++ b/packages/wizard/src/Wizard/Wizard.tsx @@ -3,7 +3,6 @@ import React, { Children, isValidElement } from 'react'; import { Direction } from '@leafygreen-ui/descendants'; import { findChild } from '@leafygreen-ui/lib'; -import { WIZARD_FOOTER_KEY } from '../constants'; import { useWizardControlledValue } from '../utils/useWizardControlledValue/useWizardControlledValue'; import { WizardContext } from '../WizardContext/WizardContext'; import { WizardFooter } from '../WizardFooter'; @@ -11,6 +10,7 @@ import { WizardStep } from '../WizardStep'; import { stepContentStyles, wizardContainerStyles } from './Wizard.styles'; import { WizardProps } from './Wizard.types'; +import { WizardSubComponentProperties } from '../constants'; export function Wizard({ activeStep: activeStepProp, @@ -50,7 +50,7 @@ export function Wizard({ onStepChange?.(getNextStep(activeStep)); }; - const footerChild = findChild(children, WIZARD_FOOTER_KEY); + const footerChild = findChild(children, WizardSubComponentProperties.Footer); // Get the current step to render const currentStep = stepChildren[activeStep] || null; diff --git a/packages/wizard/src/WizardFooter/WizardFooter.tsx b/packages/wizard/src/WizardFooter/WizardFooter.tsx index c39da6be28..d242fe429e 100644 --- a/packages/wizard/src/WizardFooter/WizardFooter.tsx +++ b/packages/wizard/src/WizardFooter/WizardFooter.tsx @@ -3,10 +3,10 @@ import React, { MouseEventHandler } from 'react'; import { Direction } from '@leafygreen-ui/descendants'; import FormFooter from '@leafygreen-ui/form-footer'; -import { WIZARD_FOOTER_KEY } from '../constants'; import { useWizardContext } from '../WizardContext/WizardContext'; import { WizardFooterProps } from './WizardFooter.types'; +import { WizardSubComponentProperties } from '../constants'; export const WizardFooter = ({ backButtonProps, @@ -45,4 +45,4 @@ export const WizardFooter = ({ }; WizardFooter.displayName = 'WizardFooter'; -WizardFooter[WIZARD_FOOTER_KEY] = true; +WizardFooter[WizardSubComponentProperties.Footer] = true; diff --git a/packages/wizard/src/constants.ts b/packages/wizard/src/constants.ts index cdbac183a2..38d0121456 100644 --- a/packages/wizard/src/constants.ts +++ b/packages/wizard/src/constants.ts @@ -1,2 +1,6 @@ -export const WIZARD_STEP_KEY = 'isWizardStep'; -export const WIZARD_FOOTER_KEY = 'isWizardFooter'; +export const WizardSubComponentProperties = { + Step: 'isWizardStep', + Footer: 'isWizardFooter', +} as const; +export type WizardSubComponentProperties = + (typeof WizardSubComponentProperties)[keyof typeof WizardSubComponentProperties]; From 326ae5cb0a0a877a45ef08424b216f3cf7d2c64a Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Tue, 30 Sep 2025 15:55:34 -0400 Subject: [PATCH 15/28] Squashed commit of the following: commit c8260339daec472f2bfa45499c5fa1ad29195163 Author: Adam Thompson Date: Tue Sep 30 15:54:03 2025 -0400 Update isChildWithProperty.spec.tsx commit 01585d35fb95f29ae33132c69e46ad1447ae5813 Merge: f3570c4dc 94745fb5a Author: Adam Thompson Date: Tue Sep 30 13:28:59 2025 -0400 Merge branch 'main' into ac/cc-utils commit f3570c4dc8fe739db755c8f03cb134e70803390a Author: Adam Thompson Date: Tue Sep 30 13:28:37 2025 -0400 rm todo commit becf667062f0f5f6471295106e2347d0d87220a5 Author: Adam Thompson Date: Fri Sep 26 16:50:05 2025 -0400 rm wizard commit f8463ac5d1f8bbe4982b0b8f00f7c289c4b880af Author: Adam Thompson Date: Fri Sep 26 16:50:00 2025 -0400 update index files commit 5e0d157861de78bd68b3a0a2e97a90465c5c3d19 Author: Adam Thompson Date: Fri Sep 26 16:49:50 2025 -0400 adds 2 level fragment test commit caf8a93d9e8fc466a8ad1473735b5e88a9e49e36 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 commit ee977a1c198c8368db5ec33f2af81df4f02b3089 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 commit ee32a26a94fbade49836d7aa2fe2585c76e5c69d Merge: ac2c48548 366e8515c 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 ac2c485484b206591856ccc463bec9f6ae612c02 Author: Adam Thompson Date: Thu Sep 25 14:03:09 2025 -0400 Create lib-find-children.md commit 9cd7489c6c6c557e55c9a455b6bd6d32c62e56f4 Author: Adam Thompson Date: Thu Sep 25 14:00:05 2025 -0400 Update findChildren.ts commit 90e8208132bf86ae9410d33a73074ae222c9842e Author: Adam Thompson Date: Thu Sep 25 13:59:35 2025 -0400 Update findChildren.ts commit d7ae970488b742498fe95b9e4227f3d0e6c1a01b Author: Adam Thompson Date: Thu Sep 25 13:52:04 2025 -0400 update findChild/children with unwrapRootFragment commit a64ff9ebcd7b990fee1ff3647f7afb4b5ea2c20e Author: Adam Thompson Date: Thu Sep 25 13:49:27 2025 -0400 Creates unwrapRootFragment commit 000f71361f1af4370540e9ccb4c5b5a280b7c506 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 c6d9c9df76172b6c87a11df19c4080c6550e1324 Author: Adam Thompson Date: Thu Sep 25 13:00:30 2025 -0400 Update index.ts commit c3699570bcbd98e01facee4f6a06720b6ffa548b Author: Adam Thompson Date: Thu Sep 25 13:00:12 2025 -0400 mv child queries commit 5fe4f9d58f70e03a0c83ae3fe49d7963b22c9ceb Author: Adam Thompson Date: Thu Sep 25 12:59:35 2025 -0400 update index files commit c9261c8e29776e64d4ab06d8020c0a6ccef3a8bc Author: Adam Thompson Date: Thu Sep 25 12:58:48 2025 -0400 mv componentQueries commit be05c4d518ad02cc02ecfc864a309d8bd98f6e1b Author: Adam Thompson Date: Thu Sep 25 12:55:19 2025 -0400 Update findChildren.spec.tsx commit f493f6d58ac1ceaf3d7aea6901affa4e1e73e769 Author: Adam Thompson Date: Thu Sep 25 12:46:47 2025 -0400 update findChild tests commit 74f5f7e48dea9637c8a6f19566e52a8450f9f9fc Author: Adam Thompson Date: Thu Sep 25 12:46:28 2025 -0400 Fix isChildWithProperty tests commit 5439034bc2b9bbabb83d0577fb417f12231bb12d Author: Adam Thompson Date: Wed Sep 24 19:05:18 2025 -0400 findChildren commit aa89584e801012d6623c6dc0526f5a31db56caf9 Author: Adam Thompson Date: Wed Sep 24 19:05:10 2025 -0400 Update findChild.tsx commit dda7ad54e043711db2ce384e86329c8195565e21 Author: Adam Thompson Date: Wed Sep 24 19:05:01 2025 -0400 isChildWithProperty commit ae3a41b61f012e445dddd77edd5dad713fb0a68f Author: Adam Thompson Date: Wed Sep 24 17:02:37 2025 -0400 mv existing utils --- .changeset/lib-find-children.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/lib-find-children.md diff --git a/.changeset/lib-find-children.md b/.changeset/lib-find-children.md new file mode 100644 index 0000000000..7c0127e7d5 --- /dev/null +++ b/.changeset/lib-find-children.md @@ -0,0 +1,5 @@ +--- +'@leafygreen-ui/lib': minor +--- + +Adds `findChildren` utility to `lib`. Also adds `unwrapRootFragment` and `isChildWithProperty` helpers From 3e778c0ee1e2a5fc615298081a8820a7d9624331 Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Tue, 30 Sep 2025 16:25:21 -0400 Subject: [PATCH 16/28] adds findChildren --- packages/wizard/src/Wizard/Wizard.tsx | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/packages/wizard/src/Wizard/Wizard.tsx b/packages/wizard/src/Wizard/Wizard.tsx index 68bd357d02..07c9817089 100644 --- a/packages/wizard/src/Wizard/Wizard.tsx +++ b/packages/wizard/src/Wizard/Wizard.tsx @@ -1,7 +1,7 @@ import React, { Children, isValidElement } from 'react'; import { Direction } from '@leafygreen-ui/descendants'; -import { findChild } from '@leafygreen-ui/lib'; +import { findChild, findChildren } from '@leafygreen-ui/lib'; import { useWizardControlledValue } from '../utils/useWizardControlledValue/useWizardControlledValue'; import { WizardContext } from '../WizardContext/WizardContext'; @@ -24,14 +24,11 @@ export function Wizard({ setValue: setActiveStep, } = useWizardControlledValue(activeStepProp, undefined, 0); - const stepChildren = Children.toArray(children).filter(child => { - if (isValidElement(child)) { - const displayName = (child.type as any)?.displayName; - return displayName && displayName.includes('Step'); - } - - return false; - }); + const stepChildren = findChildren( + children, + WizardSubComponentProperties.Step, + ); + const footerChild = findChild(children, WizardSubComponentProperties.Footer); const updateStep = (direction: Direction) => { const getNextStep = (curr: number) => { @@ -50,8 +47,6 @@ export function Wizard({ onStepChange?.(getNextStep(activeStep)); }; - const footerChild = findChild(children, WizardSubComponentProperties.Footer); - // Get the current step to render const currentStep = stepChildren[activeStep] || null; @@ -64,7 +59,6 @@ export function Wizard({ >
{currentStep}
- {/* Render footer */} {footerChild}
From 39c3c64a7565377394dce902e9e552b3fb9f2c61 Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Tue, 30 Sep 2025 16:31:57 -0400 Subject: [PATCH 17/28] adds TextNode --- packages/wizard/src/WizardStep/TextNode.tsx | 31 +++++++++++++++++++ packages/wizard/src/WizardStep/WizardStep.tsx | 8 +++-- 2 files changed, 36 insertions(+), 3 deletions(-) create mode 100644 packages/wizard/src/WizardStep/TextNode.tsx diff --git a/packages/wizard/src/WizardStep/TextNode.tsx b/packages/wizard/src/WizardStep/TextNode.tsx new file mode 100644 index 0000000000..e4c5ee3410 --- /dev/null +++ b/packages/wizard/src/WizardStep/TextNode.tsx @@ -0,0 +1,31 @@ +import React, { PropsWithChildren } from 'react'; +import { Polymorph, PolymorphicAs } from '@leafygreen-ui/polymorphic'; + +/** + * Wraps a string in the provided `as` component, + * or renders the provided `ReactNode`. + * + * Useful when rendering `children` props that can be any react node + * + * @example + * ``` + * Hello! //

Hello!

+ * ``` + * + * @example + * ``` + *

Hello!

//

Hello!

+ * ``` + * + */ +// TODO: Move to `Typography` +export const TextNode = ({ + children, + as, +}: PropsWithChildren<{ as?: PolymorphicAs }>) => { + return typeof children === 'string' || typeof children === 'number' ? ( + {children} + ) : ( + children + ); +}; diff --git a/packages/wizard/src/WizardStep/WizardStep.tsx b/packages/wizard/src/WizardStep/WizardStep.tsx index d0b46a155d..52c6954134 100644 --- a/packages/wizard/src/WizardStep/WizardStep.tsx +++ b/packages/wizard/src/WizardStep/WizardStep.tsx @@ -1,18 +1,20 @@ import React from 'react'; -import { Description, H3 } from '@leafygreen-ui/typography'; +import { TextNode, Description, H3 } from '@leafygreen-ui/typography'; import { stepStyles } from './WizardStep.styles'; import { WizardStepProps } from './WizardStep.types'; +import { WizardSubComponentProperties } from '../constants'; export function WizardStep({ title, description, children }: WizardStepProps) { return (
-

{title}

- {description && {description}} + {title} + {description && {description}}
{children}
); } WizardStep.displayName = 'WizardStep'; +WizardStep[WizardSubComponentProperties.Step] = true; From 7d6b3ecb02bae3fb6321a0bb145798b4a94745b0 Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Tue, 30 Sep 2025 18:06:51 -0400 Subject: [PATCH 18/28] Update Wizard.spec.tsx --- packages/wizard/src/Wizard/Wizard.spec.tsx | 244 ++++++++++++++++++++- 1 file changed, 243 insertions(+), 1 deletion(-) diff --git a/packages/wizard/src/Wizard/Wizard.spec.tsx b/packages/wizard/src/Wizard/Wizard.spec.tsx index 179952dae4..ba864bec85 100644 --- a/packages/wizard/src/Wizard/Wizard.spec.tsx +++ b/packages/wizard/src/Wizard/Wizard.spec.tsx @@ -1,8 +1,250 @@ import React from 'react'; import { render } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { Wizard } from '.'; describe('packages/wizard', () => { - test('condition', () => {}); + describe('rendering', () => { + test('renders first Wizard.Step', () => { + const { getByText, getByTestId, queryByText, queryByTestId } = render( + + +
Step 1 content
+
+ +
Step 2 content
+
+
, + ); + expect(getByTestId('step-1-content')).toBeInTheDocument(); + expect(queryByTestId('step-2-content')).not.toBeInTheDocument(); + }); + + test('renders Wizard.Footer', () => { + const { getByTestId } = render( + + +
Content
+
+ +
, + ); + + expect(getByTestId('wizard-footer')).toBeInTheDocument(); + }); + + test('does not render any other elements', () => { + const { container, getByText, getByTestId, getByRole, queryByTestId } = + render( + +
This should not render
+
, + ); + + // Non-wizard elements should not be rendered + expect(queryByTestId('invalid-element-1')).not.toBeInTheDocument(); + }); + + test('renders correct step when activeStep is provided', () => { + const { queryByTestId, getByTestId } = render( + + +
Step 1 content
+
+ +
Step 2 content
+
+
, + ); + + // Should render the second step when activeStep is 1 + expect(queryByTestId('step-1-content')).not.toBeInTheDocument(); + expect(getByTestId('step-2-content')).toBeInTheDocument(); + }); + + test('does not render back button on first step', () => { + const { queryByRole, getByRole } = render( + + +
Content 1
+
+ +
Content 2
+
+ +
, + ); + + // Back button should not be rendered on first step + expect(queryByRole('button', { name: 'Back' })).not.toBeInTheDocument(); + expect(getByRole('button', { name: 'Next' })).toBeInTheDocument(); + }); + + test('renders back button on second step', () => { + const { getByRole } = render( + + +
Content 1
+
+ +
Content 2
+
+ +
, + ); + + expect(getByRole('button', { name: 'Back' })).toBeInTheDocument(); + expect(getByRole('button', { name: 'Next' })).toBeInTheDocument(); + }); + }); + + describe('interaction', () => { + test('calls `onStepChange` when incrementing step', async () => { + const onStepChange = jest.fn(); + + const { getByRole } = render( + + +
Content 1
+
+ +
Content 2
+
+ +
, + ); + + await userEvent.click(getByRole('button', { name: 'Next' })); + + expect(onStepChange).toHaveBeenCalledWith(1); + }); + + test('calls `onStepChange` when decrementing step', async () => { + const onStepChange = jest.fn(); + + const { getByRole } = render( + + +
Content 1
+
+ +
Content 2
+
+ +
, + ); + + await userEvent.click(getByRole('button', { name: 'Back' })); + + expect(onStepChange).toHaveBeenCalledWith(0); + }); + + test('calls custom button onClick handlers', async () => { + const onStepChange = jest.fn(); + const onBackClick = jest.fn(); + const onPrimaryClick = jest.fn(); + const onCancelClick = jest.fn(); + + const { getByRole } = render( + + +
Content 1
+
+ +
Content 2
+
+ +
, + ); + + await userEvent.click(getByRole('button', { name: 'Back' })); + expect(onBackClick).toHaveBeenCalled(); + expect(onStepChange).toHaveBeenCalledWith(0); + + await userEvent.click(getByRole('button', { name: 'Next' })); + expect(onPrimaryClick).toHaveBeenCalled(); + expect(onStepChange).toHaveBeenCalledWith(1); + + await userEvent.click(getByRole('button', { name: 'Cancel' })); + expect(onCancelClick).toHaveBeenCalled(); + }); + + describe('uncontrolled', () => { + test('does not increment step beyond Steps count', async () => { + const { getByText, queryByText, getByRole, queryByRole } = render( + + +
Content 1
+
+ +
Content 2
+
+ +
, + ); + + // Start at step 1 + expect(getByText('Step 1')).toBeInTheDocument(); + + // Click next to go to step 2 + await userEvent.click(getByRole('button', { name: 'Next' })); + expect(getByText('Step 2')).toBeInTheDocument(); + expect(queryByText('Step 1')).not.toBeInTheDocument(); + + // Click next again - should stay at step 2 (last step) + await userEvent.click(getByRole('button', { name: 'Next' })); + expect(getByText('Step 2')).toBeInTheDocument(); + expect(queryByText('Step 1')).not.toBeInTheDocument(); + }); + }); + + describe('controlled', () => { + test('does not change steps internally when controlled', async () => { + const onStepChange = jest.fn(); + + const { getByText, queryByText, getByRole, queryByRole } = render( + + +
Content 1
+
+ +
Content 2
+
+ +
, + ); + + // Should start at step 1 + expect(getByText('Step 1')).toBeInTheDocument(); + + // Click next + await userEvent.click(getByRole('button', { name: 'Next' })); + + // Should still be at step 1 since it's controlled + expect(getByText('Step 1')).toBeInTheDocument(); + expect(queryByText('Step 2')).not.toBeInTheDocument(); + + // But onStepChange should have been called + expect(onStepChange).toHaveBeenCalledWith(1); + }); + }); + }); }); From 212b4fcc1dd32cd1ed0cb38dc11257f63de635b5 Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Tue, 30 Sep 2025 18:07:04 -0400 Subject: [PATCH 19/28] minor fixes --- packages/wizard/src/Wizard.stories.tsx | 2 +- packages/wizard/src/WizardFooter/WizardFooter.types.ts | 2 +- packages/wizard/src/WizardStep/WizardStep.tsx | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/wizard/src/Wizard.stories.tsx b/packages/wizard/src/Wizard.stories.tsx index c26ccb4f5b..d920911775 100644 --- a/packages/wizard/src/Wizard.stories.tsx +++ b/packages/wizard/src/Wizard.stories.tsx @@ -11,7 +11,7 @@ import { Wizard } from '.'; faker.seed(0); export default { - title: 'Components/Wizard', + title: 'Composition/Data Display/Wizard', component: Wizard, parameters: { default: 'LiveExample', diff --git a/packages/wizard/src/WizardFooter/WizardFooter.types.ts b/packages/wizard/src/WizardFooter/WizardFooter.types.ts index d18b20c7ea..cf2617761d 100644 --- a/packages/wizard/src/WizardFooter/WizardFooter.types.ts +++ b/packages/wizard/src/WizardFooter/WizardFooter.types.ts @@ -1,6 +1,6 @@ import { FormFooterProps } from '@leafygreen-ui/form-footer'; -export interface WizardFooterProps { +export interface WizardFooterProps extends React.ComponentProps<'footer'> { /** * Props for the back button (left-most button) */ diff --git a/packages/wizard/src/WizardStep/WizardStep.tsx b/packages/wizard/src/WizardStep/WizardStep.tsx index 52c6954134..dc13e5cb99 100644 --- a/packages/wizard/src/WizardStep/WizardStep.tsx +++ b/packages/wizard/src/WizardStep/WizardStep.tsx @@ -1,10 +1,11 @@ import React from 'react'; -import { TextNode, Description, H3 } from '@leafygreen-ui/typography'; +import { Description, H3 } from '@leafygreen-ui/typography'; import { stepStyles } from './WizardStep.styles'; import { WizardStepProps } from './WizardStep.types'; import { WizardSubComponentProperties } from '../constants'; +import { TextNode } from './TextNode'; export function WizardStep({ title, description, children }: WizardStepProps) { return ( From 805ba2fda314503b736395eb55487aaf9280b10f Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Tue, 30 Sep 2025 18:12:45 -0400 Subject: [PATCH 20/28] spread rest --- packages/wizard/src/Wizard/Wizard.spec.tsx | 11 +++++------ packages/wizard/src/Wizard/Wizard.tsx | 3 ++- packages/wizard/src/WizardFooter/WizardFooter.tsx | 2 ++ packages/wizard/src/WizardStep/WizardStep.types.ts | 3 ++- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/packages/wizard/src/Wizard/Wizard.spec.tsx b/packages/wizard/src/Wizard/Wizard.spec.tsx index ba864bec85..da37f7f7b1 100644 --- a/packages/wizard/src/Wizard/Wizard.spec.tsx +++ b/packages/wizard/src/Wizard/Wizard.spec.tsx @@ -39,12 +39,11 @@ describe('packages/wizard', () => { }); test('does not render any other elements', () => { - const { container, getByText, getByTestId, getByRole, queryByTestId } = - render( - -
This should not render
-
, - ); + const { queryByTestId } = render( + +
This should not render
+
, + ); // Non-wizard elements should not be rendered expect(queryByTestId('invalid-element-1')).not.toBeInTheDocument(); diff --git a/packages/wizard/src/Wizard/Wizard.tsx b/packages/wizard/src/Wizard/Wizard.tsx index 07c9817089..8feb359fcb 100644 --- a/packages/wizard/src/Wizard/Wizard.tsx +++ b/packages/wizard/src/Wizard/Wizard.tsx @@ -16,6 +16,7 @@ export function Wizard({ activeStep: activeStepProp, onStepChange, children, + ...rest }: WizardProps) { // Controlled/Uncontrolled activeStep value const { @@ -57,7 +58,7 @@ export function Wizard({ updateStep, }} > -
+
{currentStep}
{footerChild}
diff --git a/packages/wizard/src/WizardFooter/WizardFooter.tsx b/packages/wizard/src/WizardFooter/WizardFooter.tsx index d242fe429e..0b510e658a 100644 --- a/packages/wizard/src/WizardFooter/WizardFooter.tsx +++ b/packages/wizard/src/WizardFooter/WizardFooter.tsx @@ -12,6 +12,7 @@ export const WizardFooter = ({ backButtonProps, cancelButtonProps, primaryButtonProps, + ...rest }: WizardFooterProps) => { const { activeStep, updateStep } = useWizardContext(); @@ -27,6 +28,7 @@ export const WizardFooter = ({ return ( 0 ? { diff --git a/packages/wizard/src/WizardStep/WizardStep.types.ts b/packages/wizard/src/WizardStep/WizardStep.types.ts index 6d654b2d70..b0e9e97f70 100644 --- a/packages/wizard/src/WizardStep/WizardStep.types.ts +++ b/packages/wizard/src/WizardStep/WizardStep.types.ts @@ -1,6 +1,7 @@ import { ReactNode } from 'react'; -export interface WizardStepProps { +export interface WizardStepProps + extends Omit, 'title'> { /** * The title of the step */ From 66bf36bd938f2ba0b2abe7569b41b65c852ae6c4 Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Tue, 30 Sep 2025 18:19:32 -0400 Subject: [PATCH 21/28] adds wizard context assertions --- packages/wizard/src/Wizard/Wizard.tsx | 1 + .../wizard/src/WizardContext/WizardContext.ts | 2 ++ .../src/WizardFooter/WizardFooter.spec.tsx | 28 ++++++++++++++++++- .../wizard/src/WizardFooter/WizardFooter.tsx | 10 ++++++- .../wizard/src/WizardStep/WizardStep.spec.tsx | 22 ++++++++++++++- packages/wizard/src/WizardStep/WizardStep.tsx | 20 +++++++++++-- 6 files changed, 78 insertions(+), 5 deletions(-) diff --git a/packages/wizard/src/Wizard/Wizard.tsx b/packages/wizard/src/Wizard/Wizard.tsx index 8feb359fcb..3aa6801d2c 100644 --- a/packages/wizard/src/Wizard/Wizard.tsx +++ b/packages/wizard/src/Wizard/Wizard.tsx @@ -54,6 +54,7 @@ export function Wizard({ return ( void; } export const WizardContext = createContext({ + isWizardContext: false, activeStep: 0, updateStep: () => {}, }); diff --git a/packages/wizard/src/WizardFooter/WizardFooter.spec.tsx b/packages/wizard/src/WizardFooter/WizardFooter.spec.tsx index 20e3d25c1a..746e7c9463 100644 --- a/packages/wizard/src/WizardFooter/WizardFooter.spec.tsx +++ b/packages/wizard/src/WizardFooter/WizardFooter.spec.tsx @@ -2,7 +2,33 @@ import React from 'react'; import { render } from '@testing-library/react'; import { WizardFooter } from '.'; +import { Wizard } from '../Wizard/Wizard'; describe('packages/wizard-footer', () => { - test('condition', () => {}); + test('does not render outside WizardContext', () => { + const { container } = render( + + Content + , + ); + + expect(container.firstChild).toBeNull(); + }); + test('renders in WizardContext', () => { + const { getByTestId } = render( + + + Content + + , + ); + + expect(getByTestId('footer')).toBeInTheDocument(); + }); }); diff --git a/packages/wizard/src/WizardFooter/WizardFooter.tsx b/packages/wizard/src/WizardFooter/WizardFooter.tsx index 0b510e658a..beb2bd7eac 100644 --- a/packages/wizard/src/WizardFooter/WizardFooter.tsx +++ b/packages/wizard/src/WizardFooter/WizardFooter.tsx @@ -7,6 +7,7 @@ import { useWizardContext } from '../WizardContext/WizardContext'; import { WizardFooterProps } from './WizardFooter.types'; import { WizardSubComponentProperties } from '../constants'; +import { consoleOnce } from '@leafygreen-ui/lib'; export const WizardFooter = ({ backButtonProps, @@ -14,7 +15,7 @@ export const WizardFooter = ({ primaryButtonProps, ...rest }: WizardFooterProps) => { - const { activeStep, updateStep } = useWizardContext(); + const { isWizardContext, activeStep, updateStep } = useWizardContext(); const handleBackButtonClick: MouseEventHandler = e => { updateStep(Direction.Prev); @@ -26,6 +27,13 @@ export const WizardFooter = ({ primaryButtonProps.onClick?.(e); }; + if (!isWizardContext) { + consoleOnce.error( + 'Wizard.Footer component must be used within a Wizard context.', + ); + return null; + } + return ( { - test('condition', () => {}); + test('does not render outside WizardContext', () => { + const { container } = render( + + Content + , + ); + + expect(container.firstChild).toBeNull(); + }); + test('renders in WizardContext', () => { + const { getByTestId } = render( + + + Content + + , + ); + + expect(getByTestId('step-1')).toBeInTheDocument(); + }); }); diff --git a/packages/wizard/src/WizardStep/WizardStep.tsx b/packages/wizard/src/WizardStep/WizardStep.tsx index dc13e5cb99..0b18a3129c 100644 --- a/packages/wizard/src/WizardStep/WizardStep.tsx +++ b/packages/wizard/src/WizardStep/WizardStep.tsx @@ -6,10 +6,26 @@ import { stepStyles } from './WizardStep.styles'; import { WizardStepProps } from './WizardStep.types'; import { WizardSubComponentProperties } from '../constants'; import { TextNode } from './TextNode'; +import { useWizardContext } from '../WizardContext/WizardContext'; +import { consoleOnce } from '@leafygreen-ui/lib'; + +export function WizardStep({ + title, + description, + children, + ...rest +}: WizardStepProps) { + const { isWizardContext } = useWizardContext(); + + if (!isWizardContext) { + consoleOnce.error( + 'Wizard.Step component must be used within a Wizard context.', + ); + return null; + } -export function WizardStep({ title, description, children }: WizardStepProps) { return ( -
+
{title} {description && {description}}
{children}
From f9b446b5540371d0862e8d530487c7b9978aaf96 Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Tue, 30 Sep 2025 18:22:07 -0400 Subject: [PATCH 22/28] fix exports --- packages/wizard/src/Wizard/Wizard.tsx | 2 +- packages/wizard/src/WizardContext/WizardContext.ts | 2 +- packages/wizard/src/WizardContext/index.ts | 5 +++++ packages/wizard/src/WizardFooter/WizardFooter.spec.tsx | 2 +- packages/wizard/src/WizardFooter/WizardFooter.tsx | 2 +- packages/wizard/src/WizardStep/WizardStep.tsx | 2 +- packages/wizard/src/index.ts | 9 +++++++-- 7 files changed, 17 insertions(+), 7 deletions(-) create mode 100644 packages/wizard/src/WizardContext/index.ts diff --git a/packages/wizard/src/Wizard/Wizard.tsx b/packages/wizard/src/Wizard/Wizard.tsx index 3aa6801d2c..350f5fd3e0 100644 --- a/packages/wizard/src/Wizard/Wizard.tsx +++ b/packages/wizard/src/Wizard/Wizard.tsx @@ -4,7 +4,7 @@ import { Direction } from '@leafygreen-ui/descendants'; import { findChild, findChildren } from '@leafygreen-ui/lib'; import { useWizardControlledValue } from '../utils/useWizardControlledValue/useWizardControlledValue'; -import { WizardContext } from '../WizardContext/WizardContext'; +import { WizardContext } from '../WizardContext'; import { WizardFooter } from '../WizardFooter'; import { WizardStep } from '../WizardStep'; diff --git a/packages/wizard/src/WizardContext/WizardContext.ts b/packages/wizard/src/WizardContext/WizardContext.ts index b7f2ce571e..5bb84163f2 100644 --- a/packages/wizard/src/WizardContext/WizardContext.ts +++ b/packages/wizard/src/WizardContext/WizardContext.ts @@ -2,7 +2,7 @@ import { createContext, useContext } from 'react'; import { Direction } from '@leafygreen-ui/descendants'; -interface WizardContextData { +export interface WizardContextData { isWizardContext: boolean; activeStep: number; updateStep: (direction: Direction) => void; diff --git a/packages/wizard/src/WizardContext/index.ts b/packages/wizard/src/WizardContext/index.ts new file mode 100644 index 0000000000..da84037079 --- /dev/null +++ b/packages/wizard/src/WizardContext/index.ts @@ -0,0 +1,5 @@ +export { + WizardContext, + type WizardContextData, + useWizardContext, +} from './WizardContext'; diff --git a/packages/wizard/src/WizardFooter/WizardFooter.spec.tsx b/packages/wizard/src/WizardFooter/WizardFooter.spec.tsx index 746e7c9463..3437d27b5c 100644 --- a/packages/wizard/src/WizardFooter/WizardFooter.spec.tsx +++ b/packages/wizard/src/WizardFooter/WizardFooter.spec.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { render } from '@testing-library/react'; import { WizardFooter } from '.'; -import { Wizard } from '../Wizard/Wizard'; +import { Wizard } from '../Wizard'; describe('packages/wizard-footer', () => { test('does not render outside WizardContext', () => { diff --git a/packages/wizard/src/WizardFooter/WizardFooter.tsx b/packages/wizard/src/WizardFooter/WizardFooter.tsx index beb2bd7eac..314808ab3d 100644 --- a/packages/wizard/src/WizardFooter/WizardFooter.tsx +++ b/packages/wizard/src/WizardFooter/WizardFooter.tsx @@ -3,7 +3,7 @@ import React, { MouseEventHandler } from 'react'; import { Direction } from '@leafygreen-ui/descendants'; import FormFooter from '@leafygreen-ui/form-footer'; -import { useWizardContext } from '../WizardContext/WizardContext'; +import { useWizardContext } from '../WizardContext'; import { WizardFooterProps } from './WizardFooter.types'; import { WizardSubComponentProperties } from '../constants'; diff --git a/packages/wizard/src/WizardStep/WizardStep.tsx b/packages/wizard/src/WizardStep/WizardStep.tsx index 0b18a3129c..4b28109c1b 100644 --- a/packages/wizard/src/WizardStep/WizardStep.tsx +++ b/packages/wizard/src/WizardStep/WizardStep.tsx @@ -6,7 +6,7 @@ import { stepStyles } from './WizardStep.styles'; import { WizardStepProps } from './WizardStep.types'; import { WizardSubComponentProperties } from '../constants'; import { TextNode } from './TextNode'; -import { useWizardContext } from '../WizardContext/WizardContext'; +import { useWizardContext } from '../WizardContext'; import { consoleOnce } from '@leafygreen-ui/lib'; export function WizardStep({ diff --git a/packages/wizard/src/index.ts b/packages/wizard/src/index.ts index e1baba1fd2..79e76de62f 100644 --- a/packages/wizard/src/index.ts +++ b/packages/wizard/src/index.ts @@ -1,3 +1,8 @@ export { Wizard, type WizardProps } from './Wizard'; -export { WizardFooter, type WizardFooterProps } from './WizardFooter'; -export { WizardStep, type WizardStepProps } from './WizardStep'; +export { + WizardContext, + WizardContextData, + useWizardContext, +} from './WizardContext'; +export { type WizardFooterProps } from './WizardFooter'; +export { type WizardStepProps } from './WizardStep'; From e1077dba270f6051e67de7e50e84cd69d7921544 Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Mon, 6 Oct 2025 12:11:56 -0400 Subject: [PATCH 23/28] fix exports --- packages/wizard/src/WizardContext/index.ts | 2 +- packages/wizard/src/index.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/wizard/src/WizardContext/index.ts b/packages/wizard/src/WizardContext/index.ts index da84037079..c2c46e1543 100644 --- a/packages/wizard/src/WizardContext/index.ts +++ b/packages/wizard/src/WizardContext/index.ts @@ -1,5 +1,5 @@ export { + useWizardContext, WizardContext, type WizardContextData, - useWizardContext, } from './WizardContext'; diff --git a/packages/wizard/src/index.ts b/packages/wizard/src/index.ts index 79e76de62f..1d5270af64 100644 --- a/packages/wizard/src/index.ts +++ b/packages/wizard/src/index.ts @@ -1,8 +1,8 @@ export { Wizard, type WizardProps } from './Wizard'; export { - WizardContext, - WizardContextData, useWizardContext, + WizardContext, + type WizardContextData, } from './WizardContext'; export { type WizardFooterProps } from './WizardFooter'; export { type WizardStepProps } from './WizardStep'; From 646d001264630bc2280ffcd8eeb0913a5d3be59a Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Tue, 7 Oct 2025 14:14:24 -0400 Subject: [PATCH 24/28] Update TextNode.tsx --- packages/wizard/src/WizardStep/TextNode.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/wizard/src/WizardStep/TextNode.tsx b/packages/wizard/src/WizardStep/TextNode.tsx index e4c5ee3410..e75679d7ed 100644 --- a/packages/wizard/src/WizardStep/TextNode.tsx +++ b/packages/wizard/src/WizardStep/TextNode.tsx @@ -1,4 +1,5 @@ import React, { PropsWithChildren } from 'react'; + import { Polymorph, PolymorphicAs } from '@leafygreen-ui/polymorphic'; /** @@ -26,6 +27,6 @@ export const TextNode = ({ return typeof children === 'string' || typeof children === 'number' ? ( {children} ) : ( - children + <>{children} ); }; From 16a78f15e8f8ba6e9a328b061cba82f913d12c49 Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Tue, 7 Oct 2025 14:17:09 -0400 Subject: [PATCH 25/28] creates compound component --- packages/wizard/src/Wizard/Wizard.tsx | 113 ++++++++++-------- .../wizard/src/WizardFooter/WizardFooter.tsx | 104 ++++++++-------- packages/wizard/src/WizardStep/WizardStep.tsx | 54 ++++----- .../wizard/src/utils/CompoundComponent.tsx | 27 +++++ .../wizard/src/utils/CompoundSubComponent.tsx | 21 ++++ 5 files changed, 190 insertions(+), 129 deletions(-) create mode 100644 packages/wizard/src/utils/CompoundComponent.tsx create mode 100644 packages/wizard/src/utils/CompoundSubComponent.tsx diff --git a/packages/wizard/src/Wizard/Wizard.tsx b/packages/wizard/src/Wizard/Wizard.tsx index 350f5fd3e0..ff7171681d 100644 --- a/packages/wizard/src/Wizard/Wizard.tsx +++ b/packages/wizard/src/Wizard/Wizard.tsx @@ -1,8 +1,10 @@ -import React, { Children, isValidElement } from 'react'; +import React from 'react'; import { Direction } from '@leafygreen-ui/descendants'; import { findChild, findChildren } from '@leafygreen-ui/lib'; +import { WizardSubComponentProperties } from '../constants'; +import { CompoundComponent } from '../utils/CompoundComponent'; import { useWizardControlledValue } from '../utils/useWizardControlledValue/useWizardControlledValue'; import { WizardContext } from '../WizardContext'; import { WizardFooter } from '../WizardFooter'; @@ -10,63 +12,68 @@ import { WizardStep } from '../WizardStep'; import { stepContentStyles, wizardContainerStyles } from './Wizard.styles'; import { WizardProps } from './Wizard.types'; -import { WizardSubComponentProperties } from '../constants'; -export function Wizard({ - activeStep: activeStepProp, - onStepChange, - children, - ...rest -}: WizardProps) { - // Controlled/Uncontrolled activeStep value - const { - isControlled, - value: activeStep, - setValue: setActiveStep, - } = useWizardControlledValue(activeStepProp, undefined, 0); - - const stepChildren = findChildren( +export const Wizard = CompoundComponent( + ({ + activeStep: activeStepProp, + onStepChange, children, - WizardSubComponentProperties.Step, - ); - const footerChild = findChild(children, WizardSubComponentProperties.Footer); + ...rest + }: WizardProps) => { + // Controlled/Uncontrolled activeStep value + const { + isControlled, + value: activeStep, + setValue: setActiveStep, + } = useWizardControlledValue(activeStepProp, undefined, 0); - const updateStep = (direction: Direction) => { - const getNextStep = (curr: number) => { - switch (direction) { - case Direction.Next: - return Math.min(curr + 1, stepChildren.length - 1); - case Direction.Prev: - return Math.max(curr - 1, 0); - } - }; + const stepChildren = findChildren( + children, + WizardSubComponentProperties.Step, + ); + const footerChild = findChild( + children, + WizardSubComponentProperties.Footer, + ); - if (!isControlled) { - setActiveStep(getNextStep); - } + const updateStep = (direction: Direction) => { + const getNextStep = (curr: number) => { + switch (direction) { + case Direction.Next: + return Math.min(curr + 1, stepChildren.length - 1); + case Direction.Prev: + return Math.max(curr - 1, 0); + } + }; - onStepChange?.(getNextStep(activeStep)); - }; + if (!isControlled) { + setActiveStep(getNextStep); + } - // Get the current step to render - const currentStep = stepChildren[activeStep] || null; + onStepChange?.(getNextStep(activeStep)); + }; - return ( - -
-
{currentStep}
- {footerChild} -
-
- ); -} + // Get the current step to render + const currentStep = stepChildren[activeStep] || null; -Wizard.displayName = 'Wizard'; -Wizard.Step = WizardStep; -Wizard.Footer = WizardFooter; + return ( + +
+
{currentStep}
+ {footerChild} +
+
+ ); + }, + { + displayName: 'Wizard', + Step: WizardStep, + Footer: WizardFooter, + }, +); diff --git a/packages/wizard/src/WizardFooter/WizardFooter.tsx b/packages/wizard/src/WizardFooter/WizardFooter.tsx index 314808ab3d..9f0423bb19 100644 --- a/packages/wizard/src/WizardFooter/WizardFooter.tsx +++ b/packages/wizard/src/WizardFooter/WizardFooter.tsx @@ -1,58 +1,64 @@ import React, { MouseEventHandler } from 'react'; import { Direction } from '@leafygreen-ui/descendants'; -import FormFooter from '@leafygreen-ui/form-footer'; +import { FormFooter } from '@leafygreen-ui/form-footer'; +import { consoleOnce } from '@leafygreen-ui/lib'; +import { WizardSubComponentProperties } from '../constants'; +import { CompoundSubComponent } from '../utils/CompoundSubComponent'; import { useWizardContext } from '../WizardContext'; import { WizardFooterProps } from './WizardFooter.types'; -import { WizardSubComponentProperties } from '../constants'; -import { consoleOnce } from '@leafygreen-ui/lib'; -export const WizardFooter = ({ - backButtonProps, - cancelButtonProps, - primaryButtonProps, - ...rest -}: WizardFooterProps) => { - const { isWizardContext, activeStep, updateStep } = useWizardContext(); - - const handleBackButtonClick: MouseEventHandler = e => { - updateStep(Direction.Prev); - backButtonProps?.onClick?.(e); - }; - - const handlePrimaryButtonClick: MouseEventHandler = e => { - updateStep(Direction.Next); - primaryButtonProps.onClick?.(e); - }; - - if (!isWizardContext) { - consoleOnce.error( - 'Wizard.Footer component must be used within a Wizard context.', +export const WizardFooter = CompoundSubComponent( + ({ + backButtonProps, + cancelButtonProps, + primaryButtonProps, + ...rest + }: WizardFooterProps) => { + const { isWizardContext, activeStep, updateStep } = useWizardContext(); + + const handleBackButtonClick: MouseEventHandler = e => { + updateStep(Direction.Prev); + backButtonProps?.onClick?.(e); + }; + + const handlePrimaryButtonClick: MouseEventHandler< + HTMLButtonElement + > = e => { + updateStep(Direction.Next); + primaryButtonProps.onClick?.(e); + }; + + if (!isWizardContext) { + consoleOnce.error( + 'Wizard.Footer component must be used within a Wizard context.', + ); + return null; + } + + return ( + 0 + ? { + ...backButtonProps, + onClick: handleBackButtonClick, + } + : undefined + } + cancelButtonProps={cancelButtonProps} + primaryButtonProps={{ + ...primaryButtonProps, + onClick: handlePrimaryButtonClick, + }} + /> ); - return null; - } - - return ( - 0 - ? { - ...backButtonProps, - onClick: handleBackButtonClick, - } - : undefined - } - cancelButtonProps={cancelButtonProps} - primaryButtonProps={{ - ...primaryButtonProps, - onClick: handlePrimaryButtonClick, - }} - /> - ); -}; - -WizardFooter.displayName = 'WizardFooter'; -WizardFooter[WizardSubComponentProperties.Footer] = true; + }, + { + displayName: 'WizardFooter', + [WizardSubComponentProperties.Footer]: true, + }, +); diff --git a/packages/wizard/src/WizardStep/WizardStep.tsx b/packages/wizard/src/WizardStep/WizardStep.tsx index 4b28109c1b..a50d35044d 100644 --- a/packages/wizard/src/WizardStep/WizardStep.tsx +++ b/packages/wizard/src/WizardStep/WizardStep.tsx @@ -1,37 +1,37 @@ import React from 'react'; +import { consoleOnce } from '@leafygreen-ui/lib'; import { Description, H3 } from '@leafygreen-ui/typography'; -import { stepStyles } from './WizardStep.styles'; -import { WizardStepProps } from './WizardStep.types'; import { WizardSubComponentProperties } from '../constants'; -import { TextNode } from './TextNode'; +import { CompoundSubComponent } from '../utils/CompoundSubComponent'; import { useWizardContext } from '../WizardContext'; -import { consoleOnce } from '@leafygreen-ui/lib'; -export function WizardStep({ - title, - description, - children, - ...rest -}: WizardStepProps) { - const { isWizardContext } = useWizardContext(); +import { TextNode } from './TextNode'; +import { stepStyles } from './WizardStep.styles'; +import { WizardStepProps } from './WizardStep.types'; - if (!isWizardContext) { - consoleOnce.error( - 'Wizard.Step component must be used within a Wizard context.', - ); - return null; - } +export const WizardStep = CompoundSubComponent( + ({ title, description, children, ...rest }: WizardStepProps) => { + const { isWizardContext } = useWizardContext(); - return ( -
- {title} - {description && {description}} -
{children}
-
- ); -} + if (!isWizardContext) { + consoleOnce.error( + 'Wizard.Step component must be used within a Wizard context.', + ); + return null; + } -WizardStep.displayName = 'WizardStep'; -WizardStep[WizardSubComponentProperties.Step] = true; + return ( +
+ {title} + {description && {description}} +
{children}
+
+ ); + }, + { + displayName: 'WizardStep', + [WizardSubComponentProperties.Step]: true, + }, +); diff --git a/packages/wizard/src/utils/CompoundComponent.tsx b/packages/wizard/src/utils/CompoundComponent.tsx new file mode 100644 index 0000000000..2af7309c04 --- /dev/null +++ b/packages/wizard/src/utils/CompoundComponent.tsx @@ -0,0 +1,27 @@ +import { FunctionComponent } from 'react'; + +/** Generic properties applied to a Compound component */ +export interface CompoundComponentProperties { + displayName: string; + [key: string]: any; // Typed as `any` to avoid issues with a mapped object +} + +/** Return type of a CompoundComponent */ +export type CompoundComponentType< + Props extends {} = {}, + Properties extends CompoundComponentProperties = CompoundComponentProperties, +> = FunctionComponent & Properties; + +/** + * Factory function used to create a compound component parent + * @returns {CompoundComponentType} + */ +export const CompoundComponent = < + Props extends {} = {}, + Properties extends CompoundComponentProperties = CompoundComponentProperties, +>( + componentRenderFn: FunctionComponent, + properties: Properties, +): CompoundComponentType => { + return Object.assign(componentRenderFn, properties); +}; diff --git a/packages/wizard/src/utils/CompoundSubComponent.tsx b/packages/wizard/src/utils/CompoundSubComponent.tsx new file mode 100644 index 0000000000..7ff75306ee --- /dev/null +++ b/packages/wizard/src/utils/CompoundSubComponent.tsx @@ -0,0 +1,21 @@ +import { FunctionComponent } from 'react'; + +export interface SubComponentProperties { + displayName: string; + [k: string]: any; +} + +export type SubComponentType< + Props extends {} = {}, + Properties extends SubComponentProperties = SubComponentProperties, +> = FunctionComponent & Properties; + +export const CompoundSubComponent = < + Props extends {} = {}, + Properties extends SubComponentProperties = SubComponentProperties, +>( + componentRenderFn: FunctionComponent, + properties: Properties, +): SubComponentType => { + return Object.assign(componentRenderFn, properties); +}; From 9b01de1002b1468cfa2ae65d68d7384b49dac797 Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Tue, 7 Oct 2025 14:24:24 -0400 Subject: [PATCH 26/28] lint --- packages/wizard/src/WizardFooter/WizardFooter.spec.tsx | 3 ++- packages/wizard/src/WizardStep/WizardStep.spec.tsx | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/wizard/src/WizardFooter/WizardFooter.spec.tsx b/packages/wizard/src/WizardFooter/WizardFooter.spec.tsx index 3437d27b5c..5bf606c5f0 100644 --- a/packages/wizard/src/WizardFooter/WizardFooter.spec.tsx +++ b/packages/wizard/src/WizardFooter/WizardFooter.spec.tsx @@ -1,9 +1,10 @@ import React from 'react'; import { render } from '@testing-library/react'; -import { WizardFooter } from '.'; import { Wizard } from '../Wizard'; +import { WizardFooter } from '.'; + describe('packages/wizard-footer', () => { test('does not render outside WizardContext', () => { const { container } = render( diff --git a/packages/wizard/src/WizardStep/WizardStep.spec.tsx b/packages/wizard/src/WizardStep/WizardStep.spec.tsx index 2f1dab139e..0e312c3bcd 100644 --- a/packages/wizard/src/WizardStep/WizardStep.spec.tsx +++ b/packages/wizard/src/WizardStep/WizardStep.spec.tsx @@ -1,9 +1,10 @@ import React from 'react'; import { render } from '@testing-library/react'; -import { WizardStep } from '.'; import { Wizard } from '../Wizard/Wizard'; +import { WizardStep } from '.'; + describe('packages/wizard-step', () => { test('does not render outside WizardContext', () => { const { container } = render( From fec94fd7094066a10260c048658841e33e7f08a4 Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Tue, 7 Oct 2025 15:08:17 -0400 Subject: [PATCH 27/28] update CompoundSubComponent api --- .../wizard/src/WizardFooter/WizardFooter.tsx | 2 +- packages/wizard/src/WizardStep/WizardStep.tsx | 2 +- .../wizard/src/utils/CompoundSubComponent.tsx | 42 ++++++++++++++----- 3 files changed, 33 insertions(+), 13 deletions(-) diff --git a/packages/wizard/src/WizardFooter/WizardFooter.tsx b/packages/wizard/src/WizardFooter/WizardFooter.tsx index 9f0423bb19..0b90cf82f8 100644 --- a/packages/wizard/src/WizardFooter/WizardFooter.tsx +++ b/packages/wizard/src/WizardFooter/WizardFooter.tsx @@ -59,6 +59,6 @@ export const WizardFooter = CompoundSubComponent( }, { displayName: 'WizardFooter', - [WizardSubComponentProperties.Footer]: true, + key: WizardSubComponentProperties.Footer, }, ); diff --git a/packages/wizard/src/WizardStep/WizardStep.tsx b/packages/wizard/src/WizardStep/WizardStep.tsx index a50d35044d..83a5a1a39d 100644 --- a/packages/wizard/src/WizardStep/WizardStep.tsx +++ b/packages/wizard/src/WizardStep/WizardStep.tsx @@ -32,6 +32,6 @@ export const WizardStep = CompoundSubComponent( }, { displayName: 'WizardStep', - [WizardSubComponentProperties.Step]: true, + key: WizardSubComponentProperties.Step, }, ); diff --git a/packages/wizard/src/utils/CompoundSubComponent.tsx b/packages/wizard/src/utils/CompoundSubComponent.tsx index 7ff75306ee..f60f9603b6 100644 --- a/packages/wizard/src/utils/CompoundSubComponent.tsx +++ b/packages/wizard/src/utils/CompoundSubComponent.tsx @@ -1,21 +1,41 @@ import { FunctionComponent } from 'react'; -export interface SubComponentProperties { +interface SubComponentProperties { displayName: string; - [k: string]: any; + key: Key; } export type SubComponentType< + Key extends string, Props extends {} = {}, - Properties extends SubComponentProperties = SubComponentProperties, -> = FunctionComponent & Properties; +> = FunctionComponent & { + [key in Key]: true; +}; -export const CompoundSubComponent = < - Props extends {} = {}, - Properties extends SubComponentProperties = SubComponentProperties, ->( +/** + * Factory function to create a compound sub-component with a static `key` property. + * Sets the given `key` property on the resulting component to true. + * + * @example + * ```tsx + * const MySubComponent = CompoundSubComponent(() =>
, { + * displayName: 'MySubComponent', + * key: 'isSubComponent' + * }) + * MySubComponent.isSubComponent // true + * ``` + * + * @param componentRenderFn The component render function + * @param properties Object describing the `displayName` and `key` + * @returns {SubComponentType} + */ +export const CompoundSubComponent = ( componentRenderFn: FunctionComponent, - properties: Properties, -): SubComponentType => { - return Object.assign(componentRenderFn, properties); + properties: SubComponentProperties, +): SubComponentType => { + const { key, ...rest } = properties; + return Object.assign(componentRenderFn, { + ...rest, + [key]: true, + }) as SubComponentType; }; From f89a0c4022a2473fb21ae25a02a8c1b1a2bd8b1d Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Tue, 7 Oct 2025 15:10:52 -0400 Subject: [PATCH 28/28] update packages --- packages/wizard/package.json | 4 ++++ pnpm-lock.yaml | 12 ++++++++++++ 2 files changed, 16 insertions(+) diff --git a/packages/wizard/package.json b/packages/wizard/package.json index 18685e8e0c..1d5213c36e 100644 --- a/packages/wizard/package.json +++ b/packages/wizard/package.json @@ -29,9 +29,13 @@ }, "dependencies": { "@leafygreen-ui/button": "workspace:^", + "@leafygreen-ui/descendants": "workspace:^", "@leafygreen-ui/emotion": "workspace:^", "@leafygreen-ui/form-footer": "workspace:^", + "@leafygreen-ui/hooks": "workspace:^", "@leafygreen-ui/lib": "workspace:^", + "@leafygreen-ui/polymorphic": "workspace:^", + "@leafygreen-ui/tokens": "workspace:^", "@leafygreen-ui/typography": "workspace:^", "@lg-tools/test-harnesses": "workspace:^" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0fd8eb3ad3..d70919e807 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3795,15 +3795,27 @@ importers: '@leafygreen-ui/button': specifier: workspace:^ version: link:../button + '@leafygreen-ui/descendants': + specifier: workspace:^ + version: link:../descendants '@leafygreen-ui/emotion': specifier: workspace:^ version: link:../emotion '@leafygreen-ui/form-footer': specifier: workspace:^ version: link:../form-footer + '@leafygreen-ui/hooks': + specifier: workspace:^ + version: link:../hooks '@leafygreen-ui/lib': specifier: workspace:^ version: link:../lib + '@leafygreen-ui/polymorphic': + specifier: workspace:^ + version: link:../polymorphic + '@leafygreen-ui/tokens': + specifier: workspace:^ + version: link:../tokens '@leafygreen-ui/typography': specifier: workspace:^ version: link:../typography