From 47887845c2fadd42baace2091bcee8ea8eb52f01 Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Wed, 24 Sep 2025 13:58:45 -0400 Subject: [PATCH 1/8] scaffolds wizard package --- packages/wizard/README.md | 26 +++++++++++ packages/wizard/package.json | 43 +++++++++++++++++++ packages/wizard/src/Wizard.stories.tsx | 17 ++++++++ packages/wizard/src/Wizard/Wizard.spec.tsx | 11 +++++ packages/wizard/src/Wizard/Wizard.styles.ts | 4 ++ packages/wizard/src/Wizard/Wizard.tsx | 8 ++++ packages/wizard/src/Wizard/Wizard.types.ts | 1 + packages/wizard/src/Wizard/index.ts | 3 ++ packages/wizard/src/index.ts | 1 + .../wizard/src/testing/getTestUtils.spec.tsx | 10 +++++ packages/wizard/src/testing/getTestUtils.tsx | 15 +++++++ .../wizard/src/testing/getTestUtils.types.ts | 1 + packages/wizard/src/testing/index.ts | 2 + packages/wizard/src/utils/getLgIds.ts | 12 ++++++ packages/wizard/tsconfig.json | 22 ++++++++++ 15 files changed, 176 insertions(+) create mode 100644 packages/wizard/README.md create mode 100644 packages/wizard/package.json create mode 100644 packages/wizard/src/Wizard.stories.tsx create mode 100644 packages/wizard/src/Wizard/Wizard.spec.tsx create mode 100644 packages/wizard/src/Wizard/Wizard.styles.ts create mode 100644 packages/wizard/src/Wizard/Wizard.tsx create mode 100644 packages/wizard/src/Wizard/Wizard.types.ts create mode 100644 packages/wizard/src/Wizard/index.ts create mode 100644 packages/wizard/src/index.ts create mode 100644 packages/wizard/src/testing/getTestUtils.spec.tsx create mode 100644 packages/wizard/src/testing/getTestUtils.tsx create mode 100644 packages/wizard/src/testing/getTestUtils.types.ts create mode 100644 packages/wizard/src/testing/index.ts create mode 100644 packages/wizard/src/utils/getLgIds.ts create mode 100644 packages/wizard/tsconfig.json diff --git a/packages/wizard/README.md b/packages/wizard/README.md new file mode 100644 index 0000000000..f6d912208f --- /dev/null +++ b/packages/wizard/README.md @@ -0,0 +1,26 @@ + +# 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 + +### PNPM + +```shell +pnpm add @leafygreen-ui/wizard +``` + +### Yarn + +```shell +yarn add @leafygreen-ui/wizard +``` + +### NPM + +```shell +npm install @leafygreen-ui/wizard +``` + diff --git a/packages/wizard/package.json b/packages/wizard/package.json new file mode 100644 index 0000000000..91384840b9 --- /dev/null +++ b/packages/wizard/package.json @@ -0,0 +1,43 @@ + +{ + "name": "@leafygreen-ui/wizard", + "version": "0.1.0", + "description": "LeafyGreen UI Kit Wizard", + "main": "./dist/umd/index.js", + "module": "./dist/esm/index.js", + "types": "./dist/types/index.d.ts", + "license": "Apache-2.0", + "exports": { + ".": { + "require": "./dist/umd/index.js", + "import": "./dist/esm/index.js", + "types": "./dist/types/index.d.ts" + }, + "./testing": { + "require": "./dist/umd/testing/index.js", + "import": "./dist/esm/testing/index.js", + "types": "./dist/types/testing/index.d.ts" + } + }, + "scripts": { + "build": "lg-build bundle", + "tsc": "lg-build tsc", + "docs": "lg-build docs" + }, + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@leafygreen-ui/emotion": "workspace:^", + "@leafygreen-ui/lib": "workspace:^", + "@lg-tools/test-harnesses": "workspace:^" + }, + "homepage": "https://github.com/mongodb/leafygreen-ui/tree/main/packages/wizard", + "repository": { + "type": "git", + "url": "https://github.com/mongodb/leafygreen-ui" + }, + "bugs": { + "url": "https://jira.mongodb.org/projects/LG/summary" + } +} diff --git a/packages/wizard/src/Wizard.stories.tsx b/packages/wizard/src/Wizard.stories.tsx new file mode 100644 index 0000000000..4c4b56cf46 --- /dev/null +++ b/packages/wizard/src/Wizard.stories.tsx @@ -0,0 +1,17 @@ + +import React from 'react'; +import { StoryFn } from '@storybook/react'; + +import { Wizard } from '.'; + +export default { + title: 'Components/Wizard', + component: Wizard, +} + +const Template: StoryFn = (props) => ( + +); + +export const Basic = Template.bind({}); + diff --git a/packages/wizard/src/Wizard/Wizard.spec.tsx b/packages/wizard/src/Wizard/Wizard.spec.tsx new file mode 100644 index 0000000000..07591fd2e6 --- /dev/null +++ b/packages/wizard/src/Wizard/Wizard.spec.tsx @@ -0,0 +1,11 @@ + +import React from 'react'; +import { render } from '@testing-library/react'; + +import { Wizard } from '.'; + +describe('packages/wizard', () => { + test('condition', () => { + + }) +}) diff --git a/packages/wizard/src/Wizard/Wizard.styles.ts b/packages/wizard/src/Wizard/Wizard.styles.ts new file mode 100644 index 0000000000..928608f58d --- /dev/null +++ b/packages/wizard/src/Wizard/Wizard.styles.ts @@ -0,0 +1,4 @@ + +import { css } from '@leafygreen-ui/emotion'; + +export const baseStyles = css``; diff --git a/packages/wizard/src/Wizard/Wizard.tsx b/packages/wizard/src/Wizard/Wizard.tsx new file mode 100644 index 0000000000..112fe70c75 --- /dev/null +++ b/packages/wizard/src/Wizard/Wizard.tsx @@ -0,0 +1,8 @@ +import React from 'react'; +import { WizardProps } from './Wizard.types'; + +export function Wizard({}: WizardProps) { + return
your content here
; +} + +Wizard.displayName = 'Wizard'; diff --git a/packages/wizard/src/Wizard/Wizard.types.ts b/packages/wizard/src/Wizard/Wizard.types.ts new file mode 100644 index 0000000000..cfa270475f --- /dev/null +++ b/packages/wizard/src/Wizard/Wizard.types.ts @@ -0,0 +1 @@ +export interface WizardProps {} \ No newline at end of file diff --git a/packages/wizard/src/Wizard/index.ts b/packages/wizard/src/Wizard/index.ts new file mode 100644 index 0000000000..82aa8f69a6 --- /dev/null +++ b/packages/wizard/src/Wizard/index.ts @@ -0,0 +1,3 @@ + +export { Wizard } from './Wizard'; +export { type WizardProps } from './Wizard.types'; diff --git a/packages/wizard/src/index.ts b/packages/wizard/src/index.ts new file mode 100644 index 0000000000..cfbd7d46d8 --- /dev/null +++ b/packages/wizard/src/index.ts @@ -0,0 +1 @@ +export { Wizard, type WizardProps } from './Wizard'; \ No newline at end of file diff --git a/packages/wizard/src/testing/getTestUtils.spec.tsx b/packages/wizard/src/testing/getTestUtils.spec.tsx new file mode 100644 index 0000000000..99117014a5 --- /dev/null +++ b/packages/wizard/src/testing/getTestUtils.spec.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import { render } from '@testing-library/react'; + +import { Wizard } from '.'; + +describe('packages/wizard/getTestUtils', () => { + test('condition', () => { + + }) +}) diff --git a/packages/wizard/src/testing/getTestUtils.tsx b/packages/wizard/src/testing/getTestUtils.tsx new file mode 100644 index 0000000000..ad89a6e99d --- /dev/null +++ b/packages/wizard/src/testing/getTestUtils.tsx @@ -0,0 +1,15 @@ +import { findByLgId, getByLgId, queryByLgId } from '@lg-tools/test-harnesses'; + +import { LgIdString } from '@leafygreen-ui/lib'; + +import { DEFAULT_LGID_ROOT, getLgIds } from '../utils/getLgIds'; + +import { TestUtilsReturnType } from './getTestUtils.types'; + +export const getTestUtils = ( + lgId: LgIdString = DEFAULT_LGID_ROOT, +): TestUtilsReturnType => { + const lgIds = getLgIds(lgId); + + return {}; +}; diff --git a/packages/wizard/src/testing/getTestUtils.types.ts b/packages/wizard/src/testing/getTestUtils.types.ts new file mode 100644 index 0000000000..50d2fb417a --- /dev/null +++ b/packages/wizard/src/testing/getTestUtils.types.ts @@ -0,0 +1 @@ +export interface TestUtilsReturnType {} \ No newline at end of file diff --git a/packages/wizard/src/testing/index.ts b/packages/wizard/src/testing/index.ts new file mode 100644 index 0000000000..4c102995fa --- /dev/null +++ b/packages/wizard/src/testing/index.ts @@ -0,0 +1,2 @@ +export { getTestUtils } from './getTestUtils'; +export { type TestUtilsReturnType } from './getTestUtils.types'; diff --git a/packages/wizard/src/utils/getLgIds.ts b/packages/wizard/src/utils/getLgIds.ts new file mode 100644 index 0000000000..9590c84563 --- /dev/null +++ b/packages/wizard/src/utils/getLgIds.ts @@ -0,0 +1,12 @@ +import { LgIdString } from '@leafygreen-ui/lib'; + +export const DEFAULT_LGID_ROOT = 'lg-wizard'; + +export const getLgIds = (root: LgIdString = DEFAULT_LGID_ROOT) => { + const ids = { + root, + } as const; + return ids; +}; + +export type GetLgIdsReturnType = ReturnType; diff --git a/packages/wizard/tsconfig.json b/packages/wizard/tsconfig.json new file mode 100644 index 0000000000..5a0f368e7f --- /dev/null +++ b/packages/wizard/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "@lg-tools/build/config/package.tsconfig.json", + "compilerOptions": { + "paths": { + "@leafygreen-ui/icon/dist/*": ["../icon/src/generated/*"], + "@leafygreen-ui/*": ["../*/src"] + } + }, + "include": ["src/**/*"], + "exclude": ["**/*.spec.*", "**/*.stories.*"], + "references": [ + { + "path": "../emotion" + }, + { + "path": "../lib" + }, + { + "path": "../../tools/test-harnesses" + } + ] +} From d65608b16037626cdef0b07e1e96a7c36c2c4a42 Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Wed, 24 Sep 2025 14:09:55 -0400 Subject: [PATCH 2/8] Update pnpm-lock.yaml --- pnpm-lock.yaml | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 131b21f04a..2b6c136d2f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3608,6 +3608,18 @@ importers: specifier: workspace:^ version: link:../../tools/build + packages/wizard: + dependencies: + '@leafygreen-ui/emotion': + specifier: workspace:^ + version: link:../emotion + '@leafygreen-ui/lib': + specifier: workspace:^ + version: link:../lib + '@lg-tools/test-harnesses': + specifier: workspace:^ + version: link:../../tools/test-harnesses + tools/build: dependencies: '@babel/core': @@ -6604,14 +6616,14 @@ packages: peerDependencies: '@typescript-eslint/parser': ^8.44.0 eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <6.0.0' + typescript: ~5.8.0 '@typescript-eslint/parser@8.44.0': resolution: {integrity: sha512-VGMpFQGUQWYT9LfnPcX8ouFojyrZ/2w3K5BucvxL/spdNehccKhB4jUyB1yBCXpr2XFm0jkECxgrpXBW2ipoAw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <6.0.0' + typescript: ~5.8.0 '@typescript-eslint/project-service@8.38.0': resolution: {integrity: sha512-dbK7Jvqcb8c9QfH01YB6pORpqX1mn5gDZc9n63Ak/+jD67oWXn3Gs0M6vddAN+eDXBCS5EmNWzbSxsn9SzFWWg==} @@ -6623,7 +6635,7 @@ packages: resolution: {integrity: sha512-ZeaGNraRsq10GuEohKTo4295Z/SuGcSq2LzfGlqiuEvfArzo/VRrT0ZaJsVPuKZ55lVbNk8U6FcL+ZMH8CoyVA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - typescript: '>=4.8.4 <6.0.0' + typescript: ~5.8.0 '@typescript-eslint/scope-manager@5.62.0': resolution: {integrity: sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==} @@ -6647,14 +6659,14 @@ packages: resolution: {integrity: sha512-x5Y0+AuEPqAInc6yd0n5DAcvtoQ/vyaGwuX5HE9n6qAefk1GaedqrLQF8kQGylLUb9pnZyLf+iEiL9fr8APDtQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - typescript: '>=4.8.4 <6.0.0' + typescript: ~5.8.0 '@typescript-eslint/type-utils@8.44.0': resolution: {integrity: sha512-9cwsoSxJ8Sak67Be/hD2RNt/fsqmWnNE1iHohG8lxqLSNY8xNfyY7wloo5zpW3Nu9hxVgURevqfcH6vvKCt6yg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <6.0.0' + typescript: ~5.8.0 '@typescript-eslint/types@5.62.0': resolution: {integrity: sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==} @@ -6687,7 +6699,7 @@ packages: resolution: {integrity: sha512-lqNj6SgnGcQZwL4/SBJ3xdPEfcBuhCG8zdcwCPgYcmiPLgokiNDKlbPzCwEwu7m279J/lBYWtDYL+87OEfn8Jw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - typescript: '>=4.8.4 <6.0.0' + typescript: ~5.8.0 '@typescript-eslint/utils@5.62.0': resolution: {integrity: sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==} @@ -6700,7 +6712,7 @@ packages: engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <5.9.0' + typescript: ~5.8.0 '@typescript-eslint/utils@8.44.0': resolution: {integrity: sha512-nktOlVcg3ALo0mYlV+L7sWUD58KG4CMj1rb2HUVOO4aL3K/6wcD+NERqd0rrA5Vg06b42YhF6cFxeixsp9Riqg==} @@ -10745,12 +10757,6 @@ packages: trough@2.2.0: resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} - ts-api-utils@1.4.3: - resolution: {integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==} - engines: {node: '>=16'} - peerDependencies: - typescript: ~5.8.0 - ts-api-utils@2.1.0: resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} engines: {node: '>=18.12'} From 5f12ec9795e5b66a01ecf13808b4e16e4d201c7b Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Wed, 24 Sep 2025 14:11:46 -0400 Subject: [PATCH 3/8] Update ALL_PACKAGES.ts --- tools/install/src/ALL_PACKAGES.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/install/src/ALL_PACKAGES.ts b/tools/install/src/ALL_PACKAGES.ts index 578bb9fd4e..0e21243b1f 100644 --- a/tools/install/src/ALL_PACKAGES.ts +++ b/tools/install/src/ALL_PACKAGES.ts @@ -75,6 +75,7 @@ export const ALL_PACKAGES = [ '@leafygreen-ui/toolbar', '@leafygreen-ui/tooltip', '@leafygreen-ui/typography', + '@leafygreen-ui/wizard', '@lg-charts/chart-card', '@lg-charts/colors', '@lg-charts/core', From 7825a3b92336cc9907a854546db17ceb01cbda82 Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Wed, 24 Sep 2025 14:57:06 -0400 Subject: [PATCH 4/8] scaffold WizardFooter --- .../wizard/src/WizardFooter/WizardFooter.spec.tsx | 11 +++++++++++ .../wizard/src/WizardFooter/WizardFooter.styles.ts | 4 ++++ packages/wizard/src/WizardFooter/WizardFooter.tsx | 8 ++++++++ .../wizard/src/WizardFooter/WizardFooter.types.ts | 1 + packages/wizard/src/WizardFooter/index.ts | 3 +++ 5 files changed, 27 insertions(+) create mode 100644 packages/wizard/src/WizardFooter/WizardFooter.spec.tsx create mode 100644 packages/wizard/src/WizardFooter/WizardFooter.styles.ts create mode 100644 packages/wizard/src/WizardFooter/WizardFooter.tsx create mode 100644 packages/wizard/src/WizardFooter/WizardFooter.types.ts create mode 100644 packages/wizard/src/WizardFooter/index.ts diff --git a/packages/wizard/src/WizardFooter/WizardFooter.spec.tsx b/packages/wizard/src/WizardFooter/WizardFooter.spec.tsx new file mode 100644 index 0000000000..f0081b35c3 --- /dev/null +++ b/packages/wizard/src/WizardFooter/WizardFooter.spec.tsx @@ -0,0 +1,11 @@ + +import React from 'react'; +import { render } from '@testing-library/react'; + +import { WizardFooter } from '.'; + +describe('packages/wizard-footer', () => { + test('condition', () => { + + }) +}) diff --git a/packages/wizard/src/WizardFooter/WizardFooter.styles.ts b/packages/wizard/src/WizardFooter/WizardFooter.styles.ts new file mode 100644 index 0000000000..928608f58d --- /dev/null +++ b/packages/wizard/src/WizardFooter/WizardFooter.styles.ts @@ -0,0 +1,4 @@ + +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 new file mode 100644 index 0000000000..f0b6c5519a --- /dev/null +++ b/packages/wizard/src/WizardFooter/WizardFooter.tsx @@ -0,0 +1,8 @@ +import React from 'react'; +import { WizardFooterProps } from './WizardFooter.types'; + +export function WizardFooter({}: WizardFooterProps) { + return
your content here
; +} + +WizardFooter.displayName = 'WizardFooter'; diff --git a/packages/wizard/src/WizardFooter/WizardFooter.types.ts b/packages/wizard/src/WizardFooter/WizardFooter.types.ts new file mode 100644 index 0000000000..02f3f87b43 --- /dev/null +++ b/packages/wizard/src/WizardFooter/WizardFooter.types.ts @@ -0,0 +1 @@ +export interface WizardFooterProps {} \ No newline at end of file diff --git a/packages/wizard/src/WizardFooter/index.ts b/packages/wizard/src/WizardFooter/index.ts new file mode 100644 index 0000000000..bc9a177cfe --- /dev/null +++ b/packages/wizard/src/WizardFooter/index.ts @@ -0,0 +1,3 @@ + +export { WizardFooter } from './WizardFooter'; +export { type WizardFooterProps } from './WizardFooter.types'; From 06db9cd4ac253a8d2a41ef8947767a645cf1e0d5 Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Wed, 24 Sep 2025 14:57:19 -0400 Subject: [PATCH 5/8] scaffold wizard step --- packages/wizard/src/WizardStep/WizardStep.spec.tsx | 11 +++++++++++ packages/wizard/src/WizardStep/WizardStep.styles.ts | 4 ++++ packages/wizard/src/WizardStep/WizardStep.tsx | 8 ++++++++ packages/wizard/src/WizardStep/WizardStep.types.ts | 1 + packages/wizard/src/WizardStep/index.ts | 3 +++ 5 files changed, 27 insertions(+) create mode 100644 packages/wizard/src/WizardStep/WizardStep.spec.tsx create mode 100644 packages/wizard/src/WizardStep/WizardStep.styles.ts create mode 100644 packages/wizard/src/WizardStep/WizardStep.tsx create mode 100644 packages/wizard/src/WizardStep/WizardStep.types.ts create mode 100644 packages/wizard/src/WizardStep/index.ts diff --git a/packages/wizard/src/WizardStep/WizardStep.spec.tsx b/packages/wizard/src/WizardStep/WizardStep.spec.tsx new file mode 100644 index 0000000000..fb00cde028 --- /dev/null +++ b/packages/wizard/src/WizardStep/WizardStep.spec.tsx @@ -0,0 +1,11 @@ + +import React from 'react'; +import { render } from '@testing-library/react'; + +import { WizardStep } from '.'; + +describe('packages/wizard-step', () => { + test('condition', () => { + + }) +}) diff --git a/packages/wizard/src/WizardStep/WizardStep.styles.ts b/packages/wizard/src/WizardStep/WizardStep.styles.ts new file mode 100644 index 0000000000..928608f58d --- /dev/null +++ b/packages/wizard/src/WizardStep/WizardStep.styles.ts @@ -0,0 +1,4 @@ + +import { css } from '@leafygreen-ui/emotion'; + +export const baseStyles = css``; diff --git a/packages/wizard/src/WizardStep/WizardStep.tsx b/packages/wizard/src/WizardStep/WizardStep.tsx new file mode 100644 index 0000000000..6c699df9e8 --- /dev/null +++ b/packages/wizard/src/WizardStep/WizardStep.tsx @@ -0,0 +1,8 @@ +import React from 'react'; +import { WizardStepProps } from './WizardStep.types'; + +export function WizardStep({}: WizardStepProps) { + return
your content here
; +} + +WizardStep.displayName = 'WizardStep'; diff --git a/packages/wizard/src/WizardStep/WizardStep.types.ts b/packages/wizard/src/WizardStep/WizardStep.types.ts new file mode 100644 index 0000000000..3998534991 --- /dev/null +++ b/packages/wizard/src/WizardStep/WizardStep.types.ts @@ -0,0 +1 @@ +export interface WizardStepProps {} \ No newline at end of file diff --git a/packages/wizard/src/WizardStep/index.ts b/packages/wizard/src/WizardStep/index.ts new file mode 100644 index 0000000000..866f9c3f6c --- /dev/null +++ b/packages/wizard/src/WizardStep/index.ts @@ -0,0 +1,3 @@ + +export { WizardStep } from './WizardStep'; +export { type WizardStepProps } from './WizardStep.types'; From 6417bf5372b79bbe6711616048f58c73ad883cb7 Mon Sep 17 00:00:00 2001 From: Adam Thompson <2414030+TheSonOfThomp@users.noreply.github.com> Date: Wed, 8 Oct 2025 12:36:00 -0400 Subject: [PATCH 6/8] [LG-5563] feat(Wizard) Adds Wizard (#3161) * initial Wizard component * Creates basic Wizard.tsx component Prompt: In the newly created package, create the Wizard component. Note: these docs mention `Wizard.Step` and `Wizard.Footer`. DO NOT create these yet. They will be created later The `@leafygreen-ui/wizard` is a general-purpose, multi-step page template, designed to create guided in-app flows and wizards: Based on the MultiStepWizard component in MMS, and intended to be used in the Product Deletion template. Feature Overview: - Takes in all Steps in the flow as children. - Renders the appropriate content for the current step - Internally handles step changing (with optional external control) Non-goals: - We will not be implementing this across MMS (MultiStepWizard is currently used in 26 files) - This will not support different url routes per step Wizard component The root flow component. Controls the rendering of the appropriate step based on a controlled prop, or uncontrolled internal state. Example ```tsx const [activeStep, setActiveStep] = useState(0) 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) * Creates WizardStep and WizardFooter Prompt: The Footer and Step components have been scaffolded. Create both components with the following spec: Step: A single Step in the multi-step flow. Must be rendered within a Wizard. ```ts title: ReactNode; description: ReactNode; children: ReactNode; ``` Footer: The footer element for the Wizard. A wrapper around LeafyGreen `FormFooter`, but allows us to optionally inject event handlers into the buttons. ``` backButtonProps: ButtonProps; cancelButtonProps: ButtonProps; primaryButtonProps: ButtonProps; ``` * footer& step stories * temp useWizardControlledValue * fix useWizardControlledValue * update Footer * Update package.json * use typography in Step * update descendants * update packages * the rest of the owl * update width * fix nits * Squashed commit of the following: commit 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 * adds findChildren * adds TextNode * Update Wizard.spec.tsx * minor fixes * spread rest * adds wizard context assertions * fix exports * fix exports * Update TextNode.tsx * creates compound component * lint * update CompoundSubComponent api * update packages * add WizardProvider * update stories * Wizard * update findChild/ren * spread className * add "exceeded steps" warning * adds warning tests --- .changeset/descendants-exports.md | 5 + .changeset/lib-find-children.md | 5 + packages/descendants/src/Highlight/index.ts | 13 +- packages/descendants/src/index.ts | 3 +- .../childQueries/findChild/findChild.spec.tsx | 26 +- .../src/childQueries/findChild/findChild.tsx | 7 +- .../findChildren/findChildren.spec.tsx | 220 +++++++------ .../childQueries/findChildren/findChildren.ts | 10 +- packages/wizard/README.md | 3 +- packages/wizard/package.json | 11 + packages/wizard/src/Wizard.stories.tsx | 119 ++++++- packages/wizard/src/Wizard/Wizard.spec.tsx | 294 +++++++++++++++++- packages/wizard/src/Wizard/Wizard.styles.ts | 15 +- packages/wizard/src/Wizard/Wizard.tsx | 93 +++++- packages/wizard/src/Wizard/Wizard.types.ts | 29 +- packages/wizard/src/Wizard/index.ts | 5 +- .../src/WizardContext/WizardContext.tsx | 35 +++ packages/wizard/src/WizardContext/index.ts | 6 + .../src/WizardFooter/WizardFooter.spec.tsx | 32 +- .../src/WizardFooter/WizardFooter.stories.tsx | 84 +++++ .../src/WizardFooter/WizardFooter.styles.ts | 5 +- .../wizard/src/WizardFooter/WizardFooter.tsx | 68 +++- .../src/WizardFooter/WizardFooter.types.ts | 19 +- packages/wizard/src/WizardFooter/index.ts | 3 +- packages/wizard/src/WizardStep/TextNode.tsx | 32 ++ .../wizard/src/WizardStep/WizardStep.spec.tsx | 26 +- .../src/WizardStep/WizardStep.stories.tsx | 74 +++++ .../src/WizardStep/WizardStep.styles.ts | 6 +- packages/wizard/src/WizardStep/WizardStep.tsx | 38 ++- .../wizard/src/WizardStep/WizardStep.types.ts | 20 +- packages/wizard/src/WizardStep/index.ts | 3 +- packages/wizard/src/constants.ts | 6 + packages/wizard/src/index.ts | 9 +- .../wizard/src/testing/getTestUtils.spec.tsx | 6 +- .../wizard/src/testing/getTestUtils.types.ts | 2 +- .../wizard/src/utils/CompoundComponent.tsx | 27 ++ .../wizard/src/utils/CompoundSubComponent.tsx | 41 +++ .../utils/useWizardControlledValue/index.ts | 1 + .../useWizardControlledValue.ts | 96 ++++++ packages/wizard/tsconfig.json | 6 + pnpm-lock.yaml | 28 ++ 41 files changed, 1359 insertions(+), 172 deletions(-) create mode 100644 .changeset/descendants-exports.md create mode 100644 .changeset/lib-find-children.md create mode 100644 packages/wizard/src/WizardContext/WizardContext.tsx create mode 100644 packages/wizard/src/WizardContext/index.ts create mode 100644 packages/wizard/src/WizardFooter/WizardFooter.stories.tsx create mode 100644 packages/wizard/src/WizardStep/TextNode.tsx create mode 100644 packages/wizard/src/WizardStep/WizardStep.stories.tsx create mode 100644 packages/wizard/src/constants.ts create mode 100644 packages/wizard/src/utils/CompoundComponent.tsx create mode 100644 packages/wizard/src/utils/CompoundSubComponent.tsx create mode 100644 packages/wizard/src/utils/useWizardControlledValue/index.ts create mode 100644 packages/wizard/src/utils/useWizardControlledValue/useWizardControlledValue.ts 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/.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 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, diff --git a/packages/lib/src/childQueries/findChild/findChild.spec.tsx b/packages/lib/src/childQueries/findChild/findChild.spec.tsx index c322277615..fb902c7c5c 100644 --- a/packages/lib/src/childQueries/findChild/findChild.spec.tsx +++ b/packages/lib/src/childQueries/findChild/findChild.spec.tsx @@ -30,7 +30,7 @@ Baz.displayName = 'Baz'; (Bar as any).isBar = true; (Baz as any).isBaz = true; -describe('packages/lib/findChild', () => { +describe('packages/compound-component/findChild', () => { test('should find a child component with matching static property', () => { // Create an iterable to test different iteration scenarios const children = [, ]; @@ -77,6 +77,30 @@ describe('packages/lib/findChild', () => { expect((found as React.ReactElement).props.text).toBe('also-in-fragment'); }); + test('should find mapped children', () => { + const COUNT = 5; + const children = new Array(COUNT).fill(null).map((_, i) => { + return ; + }); + + const found = findChild(children, 'isFoo'); + expect((found as React.ReactElement).props.text).toBe('Foo number 0'); + }); + + test('should find deeply mapped children', () => { + const COUNT = 5; + const children = ( + <> + {new Array(COUNT).fill(null).map((_, i) => { + return ; + })} + + ); + + const found = findChild(children, 'isFoo'); + expect((found as React.ReactElement).props.text).toBe('Foo number 0'); + }); + test('should NOT find components in deeply nested fragments (search depth limitation)', () => { const children = ( diff --git a/packages/lib/src/childQueries/findChild/findChild.tsx b/packages/lib/src/childQueries/findChild/findChild.tsx index 26e552cf54..5c7a8e9b1c 100644 --- a/packages/lib/src/childQueries/findChild/findChild.tsx +++ b/packages/lib/src/childQueries/findChild/findChild.tsx @@ -42,8 +42,9 @@ export const findChild = ( } const allChildren = unwrapRootFragment(children); + if (!allChildren) return; - return allChildren?.find(child => - isChildWithProperty(child, staticProperty), - ) as ReactElement | undefined; + return allChildren + .flat() + .find(child => isChildWithProperty(child, staticProperty)) as ReactElement; }; diff --git a/packages/lib/src/childQueries/findChildren/findChildren.spec.tsx b/packages/lib/src/childQueries/findChildren/findChildren.spec.tsx index 6327c51f63..023ae7c7df 100644 --- a/packages/lib/src/childQueries/findChildren/findChildren.spec.tsx +++ b/packages/lib/src/childQueries/findChildren/findChildren.spec.tsx @@ -30,7 +30,7 @@ Baz.displayName = 'Baz'; (Bar as any).isBar = true; (Baz as any).isBaz = true; -describe('packages/lib/findChildren', () => { +describe('packages/compound-component/findChildren', () => { describe('basic functionality', () => { it('should find all children with matching static property', () => { const children = [ @@ -67,120 +67,142 @@ describe('packages/lib/findChildren', () => { }); }); - describe('empty and null children handling', () => { - it('should handle null children', () => { - const found = findChildren(null, 'isFoo'); - expect(found).toEqual([]); + it('should find mapped children', () => { + const COUNT = 5; + const children = new Array(COUNT).fill(null).map((_, i) => { + return ; }); - it('should handle undefined children', () => { - const found = findChildren(undefined, 'isFoo'); - expect(found).toEqual([]); - }); + const found = findChildren(children, 'isFoo'); + expect(found).toHaveLength(COUNT); + }); - it('should handle empty fragment', () => { - const children = <>; - const found = findChildren(children, 'isFoo'); - expect(found).toEqual([]); - }); + it('should find deeply mapped children', () => { + const COUNT = 5; + const children = ( + <> + {new Array(COUNT).fill(null).map((_, i) => { + return ; + })} + + ); + + const found = findChildren(children, 'isFoo'); + expect(found).toHaveLength(COUNT); + }); +}); - it('should handle empty array children', () => { - const children: Array = []; - const found = findChildren(children, 'isFoo'); - expect(found).toEqual([]); - }); +describe('empty and null children handling', () => { + it('should handle null children', () => { + const found = findChildren(null, 'isFoo'); + expect(found).toEqual([]); }); - describe('Fragment handling', () => { - it('should handle single-level fragment children', () => { - const children = ( - - - - - - ); + it('should handle undefined children', () => { + const found = findChildren(undefined, 'isFoo'); + expect(found).toEqual([]); + }); - const found = findChildren(children, 'isFoo'); - expect(found).toHaveLength(2); - expect(found[0].props.text).toBe('foo-in-fragment'); - expect(found[1].props.text).toBe('another-foo'); - }); + it('should handle empty fragment', () => { + const children = <>; + const found = findChildren(children, 'isFoo'); + expect(found).toEqual([]); + }); + + it('should handle empty array children', () => { + const children: Array = []; + const found = findChildren(children, 'isFoo'); + expect(found).toEqual([]); + }); +}); - it('should NOT find children in deeply nested Fragments', () => { - const children = ( +describe('Fragment handling', () => { + it('should handle single-level fragment children', () => { + const children = ( + + + + + + ); + + const found = findChildren(children, 'isFoo'); + expect(found).toHaveLength(2); + expect(found[0].props.text).toBe('foo-in-fragment'); + expect(found[1].props.text).toBe('another-foo'); + }); + + it('should NOT find children in deeply nested Fragments', () => { + const children = ( + + - - - - + - - ); - - // Should only find direct children, not double-nested ones - const found = findChildren(children, 'isFoo'); - expect(found).toHaveLength(1); - expect(found[0].props.text).toBe('direct-foo'); - }); + + + ); + + // Should only find direct children, not double-nested ones + const found = findChildren(children, 'isFoo'); + expect(found).toHaveLength(1); + expect(found[0].props.text).toBe('direct-foo'); }); +}); - describe('styled components', () => { - it('should work with styled components from @emotion/styled', () => { - const StyledFoo = styled(Foo)` - background-color: red; - padding: 8px; - `; - - const children = [ - , - , - , - , - , - ]; - - const found = findChildren(children, 'isFoo'); - expect(found).toHaveLength(4); - expect(found.map(c => c.props.text)).toEqual([ - 'regular-foo', - 'styled-foo', - 'styled-foo-two', - 'another-foo', - ]); - - // Verify the styled component is actually styled - const styledComponent = found[1]; - const styledType = styledComponent.type as any; - const hasEmotionProps = !!( - styledType.target || styledType.__emotion_base - ); - expect(hasEmotionProps).toBe(true); - }); +describe('styled components', () => { + it('should work with styled components from @emotion/styled', () => { + const StyledFoo = styled(Foo)` + background-color: red; + padding: 8px; + `; + + const children = [ + , + , + , + , + , + ]; + + const found = findChildren(children, 'isFoo'); + expect(found).toHaveLength(4); + expect(found.map(c => c.props.text)).toEqual([ + 'regular-foo', + 'styled-foo', + 'styled-foo-two', + 'another-foo', + ]); + + // Verify the styled component is actually styled + const styledComponent = found[1]; + const styledType = styledComponent.type as any; + const hasEmotionProps = !!(styledType.target || styledType.__emotion_base); + expect(hasEmotionProps).toBe(true); }); +}); - describe('search depth limitations', () => { - it('should NOT find deeply nested components', () => { - const children = [ - - - , +describe('search depth limitations', () => { + it('should NOT find deeply nested components', () => { + const children = [ + + + , + - - - - , -
- -
, - , - ]; - - const found = findChildren(children, 'isFoo'); - expect(found).toHaveLength(1); - expect(found[0].props.text).toBe('direct-child'); - }); + +
+ , +
+ +
, + , + ]; + + const found = findChildren(children, 'isFoo'); + expect(found).toHaveLength(1); + expect(found[0].props.text).toBe('direct-child'); }); }); diff --git a/packages/lib/src/childQueries/findChildren/findChildren.ts b/packages/lib/src/childQueries/findChildren/findChildren.ts index 48632be6f1..b5f12cb0d7 100644 --- a/packages/lib/src/childQueries/findChildren/findChildren.ts +++ b/packages/lib/src/childQueries/findChildren/findChildren.ts @@ -15,7 +15,7 @@ import { unwrapRootFragment } from '../unwrapRootFragment'; * **Styled Component Support:** Checks component.target and component.__emotion_base * for styled() wrapped components. * - * * @example + * @example * ```ts * // ✅ Will find: Direct children * findChildren([ @@ -56,7 +56,9 @@ export const findChildren = ( if (!allChildren) return []; - return allChildren.filter(child => - isChildWithProperty(child, staticProperty), - ) as Array; + return allChildren + .flat() + .filter(child => + isChildWithProperty(child, staticProperty), + ) as Array; }; 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/package.json b/packages/wizard/package.json index 91384840b9..1d5213c36e 100644 --- a/packages/wizard/package.json +++ b/packages/wizard/package.json @@ -28,10 +28,21 @@ "access": "public" }, "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:^" }, + "devDependencies" : { + "@leafygreen-ui/icon": "workspace:^", + "@faker-js/faker": "^8.0.0" + }, "homepage": "https://github.com/mongodb/leafygreen-ui/tree/main/packages/wizard", "repository": { "type": "git", diff --git a/packages/wizard/src/Wizard.stories.tsx b/packages/wizard/src/Wizard.stories.tsx index 4c4b56cf46..bd0e73875a 100644 --- a/packages/wizard/src/Wizard.stories.tsx +++ b/packages/wizard/src/Wizard.stories.tsx @@ -1,17 +1,118 @@ - +/* eslint-disable no-console */ import React from 'react'; -import { StoryFn } from '@storybook/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 { css } from '@leafygreen-ui/emotion'; import { Wizard } from '.'; +faker.seed(0); + export default { - title: 'Components/Wizard', + title: 'Composition/Wizard', component: Wizard, -} - -const Template: StoryFn = (props) => ( - -); + parameters: { + default: 'LiveExample', + }, + decorators: [ + Fn => ( +
+ +
+ ), + ], +} satisfies StoryMetaType; -export const Basic = Template.bind({}); +export const LiveExample: StoryObj = { + parameters: { + controls: { + exclude: ['children', 'activeStep', 'onStepChange'], + }, + }, + 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 = { + parameters: { + controls: { + exclude: ['children', 'onStepChange'], + }, + }, + args: { + activeStep: 0, + }, + render: ({ activeStep, ...props }) => { + return ( + + 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.spec.tsx b/packages/wizard/src/Wizard/Wizard.spec.tsx index 07591fd2e6..31d0906c46 100644 --- a/packages/wizard/src/Wizard/Wizard.spec.tsx +++ b/packages/wizard/src/Wizard/Wizard.spec.tsx @@ -1,11 +1,297 @@ - 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 { getByTestId, 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 { 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 } = 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 } = 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); + }); + + test('warns when activeStep exceeds number of steps', () => { + const consoleWarnSpy = jest + .spyOn(console, 'warn') + .mockImplementation(() => {}); + + render( + + +
Content 1
+
+ +
Content 2
+
+
, + ); + + expect(consoleWarnSpy).toHaveBeenCalledWith( + 'LeafyGreen Wizard received (zero-indexed) `activeStep` prop exceeding the number of Steps provided\n', + 'Received activeStep: 5, Wizard.Steps count: 2', + ); + + consoleWarnSpy.mockRestore(); + }); + + test('warns when activeStep is negative', () => { + const consoleWarnSpy = jest + .spyOn(console, 'warn') + .mockImplementation(() => {}); + + render( + + +
Content 1
+
+ +
Content 2
+
+
, + ); + + expect(consoleWarnSpy).toHaveBeenCalledWith( + 'LeafyGreen Wizard received (zero-indexed) `activeStep` prop exceeding the number of Steps provided\n', + 'Received activeStep: -1, Wizard.Steps count: 2', + ); - }) -}) + consoleWarnSpy.mockRestore(); + }); + }); + }); +}); diff --git a/packages/wizard/src/Wizard/Wizard.styles.ts b/packages/wizard/src/Wizard/Wizard.styles.ts index 928608f58d..c6ca33aaee 100644 --- a/packages/wizard/src/Wizard/Wizard.styles.ts +++ b/packages/wizard/src/Wizard/Wizard.styles.ts @@ -1,4 +1,15 @@ - 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: ${spacing[600]}px; +`; -export const baseStyles = css``; +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..6add408f04 100644 --- a/packages/wizard/src/Wizard/Wizard.tsx +++ b/packages/wizard/src/Wizard/Wizard.tsx @@ -1,8 +1,93 @@ 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 { WizardProvider } from '../WizardContext/WizardContext'; +import { WizardFooter } from '../WizardFooter'; +import { WizardStep } from '../WizardStep'; + +import { stepContentStyles, wizardContainerStyles } from './Wizard.styles'; import { WizardProps } from './Wizard.types'; -export function Wizard({}: WizardProps) { - return
your content here
; -} +export const Wizard = CompoundComponent( + ({ + activeStep: activeStepProp, + onStepChange, + children, + ...rest + }: WizardProps) => { + const stepChildren = findChildren( + children, + WizardSubComponentProperties.Step, + ); + const footerChild = findChild( + children, + WizardSubComponentProperties.Footer, + ); + + // Controlled/Uncontrolled activeStep value + const { + isControlled, + value: activeStep, + setValue: setActiveStep, + } = useWizardControlledValue(activeStepProp, undefined, 0); + + if ( + activeStepProp && + (activeStepProp < 0 || activeStepProp >= stepChildren.length) + ) { + // Not consoleOnce, since we want to warn again if the step changes + console.warn( + 'LeafyGreen Wizard received (zero-indexed) `activeStep` prop exceeding the number of Steps provided\n', + `Received activeStep: ${activeStepProp}, Wizard.Steps count: ${stepChildren.length}`, + ); + } + + 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); + } + + onStepChange?.(getNextStep(activeStep)); + }; + + // Get the current step to render + const currentStep = stepChildren[activeStep] || null; + + return ( + +
+
{currentStep}
+ {footerChild} +
+
+ ); + }, + { + displayName: 'Wizard', + Step: WizardStep, + Footer: WizardFooter, + }, +); -Wizard.displayName = 'Wizard'; +/** + * 🤚 Wizard. + * 🤚 Wizard. + * 🤚 Wizard. + * ... + * 🤚 Wizard. 🤚 Wizard. 🤚 Wizard. + * https://youtu.be/5jGWMtEhS1c + */ diff --git a/packages/wizard/src/Wizard/Wizard.types.ts b/packages/wizard/src/Wizard/Wizard.types.ts index cfa270475f..7fc1a3901a 100644 --- a/packages/wizard/src/Wizard/Wizard.types.ts +++ b/packages/wizard/src/Wizard/Wizard.types.ts @@ -1 +1,28 @@ -export interface WizardProps {} \ No newline at end of file +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. + */ + activeStep?: number; + + /** + * Callback fired when the active step changes + */ + onStepChange?: (step: number) => void; + + /** + * The wizard steps and footer as children + */ + 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 82aa8f69a6..a6d6cd5342 100644 --- a/packages/wizard/src/Wizard/index.ts +++ b/packages/wizard/src/Wizard/index.ts @@ -1,3 +1,2 @@ - -export { Wizard } from './Wizard'; -export { type WizardProps } from './Wizard.types'; +export { Wizard } from './Wizard'; +export { type WizardComponent, type WizardProps } from './Wizard.types'; diff --git a/packages/wizard/src/WizardContext/WizardContext.tsx b/packages/wizard/src/WizardContext/WizardContext.tsx new file mode 100644 index 0000000000..c3d50b9d3f --- /dev/null +++ b/packages/wizard/src/WizardContext/WizardContext.tsx @@ -0,0 +1,35 @@ +import React, { createContext, PropsWithChildren, useContext } from 'react'; + +import { Direction } from '@leafygreen-ui/descendants'; + +export interface WizardContextData { + isWizardContext: boolean; + activeStep: number; + updateStep: (direction: Direction) => void; +} + +export const WizardContext = createContext({ + isWizardContext: false, + activeStep: 0, + updateStep: () => {}, +}); + +export const WizardProvider = ({ + children, + activeStep, + updateStep, +}: PropsWithChildren>) => { + return ( + + {children} + + ); +}; + +export const useWizardContext = () => useContext(WizardContext); diff --git a/packages/wizard/src/WizardContext/index.ts b/packages/wizard/src/WizardContext/index.ts new file mode 100644 index 0000000000..4e4bfdda83 --- /dev/null +++ b/packages/wizard/src/WizardContext/index.ts @@ -0,0 +1,6 @@ +export { + useWizardContext, + WizardContext, + type WizardContextData, + WizardProvider, +} from './WizardContext'; diff --git a/packages/wizard/src/WizardFooter/WizardFooter.spec.tsx b/packages/wizard/src/WizardFooter/WizardFooter.spec.tsx index f0081b35c3..5bf606c5f0 100644 --- a/packages/wizard/src/WizardFooter/WizardFooter.spec.tsx +++ b/packages/wizard/src/WizardFooter/WizardFooter.spec.tsx @@ -1,11 +1,35 @@ - import React from 'react'; import { render } from '@testing-library/react'; +import { Wizard } from '../Wizard'; + import { WizardFooter } from '.'; 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.stories.tsx b/packages/wizard/src/WizardFooter/WizardFooter.stories.tsx new file mode 100644 index 0000000000..a332249682 --- /dev/null +++ b/packages/wizard/src/WizardFooter/WizardFooter.stories.tsx @@ -0,0 +1,84 @@ +import React from 'react'; +import { StoryMetaType } from '@lg-tools/storybook-utils'; +import { StoryObj } from '@storybook/react'; + +import { Variant } from '@leafygreen-ui/button'; +import { glyphs, Icon } from '@leafygreen-ui/icon'; + +import { WizardProvider } from '../WizardContext'; + +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: 'Composition/Wizard/WizardFooter', + component: WizardFooter, + parameters: { + default: 'LiveExample', + controls: { + exclude: ['backButtonProps', 'cancelButtonProps', 'primaryButtonProps'], + }, + }, + args: {}, + argTypes: { + 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; + +export const LiveExample: StoryObj = { + args: { + backButtonText: 'Back', + backButtonIcon: 'ArrowLeft', + cancelButtonText: 'Cancel', + primaryButtonText: 'Continue', + primaryButtonIcon: 'Ellipsis', + primaryButtonVariant: Variant.Primary, + }, + decorators: [ + Story => ( + {}}> + + + ), + ], + render: args => ( + + ) : undefined, + children: args.backButtonText, + }} + cancelButtonProps={{ + children: args.cancelButtonText, + }} + primaryButtonProps={{ + leftGlyph: args.primaryButtonIcon ? ( + + ) : undefined, + children: args.primaryButtonText, + variant: args.primaryButtonVariant, + }} + /> + ), +}; diff --git a/packages/wizard/src/WizardFooter/WizardFooter.styles.ts b/packages/wizard/src/WizardFooter/WizardFooter.styles.ts index 928608f58d..90e2e8cc60 100644 --- a/packages/wizard/src/WizardFooter/WizardFooter.styles.ts +++ b/packages/wizard/src/WizardFooter/WizardFooter.styles.ts @@ -1,4 +1,5 @@ - import { css } from '@leafygreen-ui/emotion'; -export const baseStyles = css``; +export const baseStyles = css` + width: 100%; +`; diff --git a/packages/wizard/src/WizardFooter/WizardFooter.tsx b/packages/wizard/src/WizardFooter/WizardFooter.tsx index f0b6c5519a..6a8d7d99c2 100644 --- a/packages/wizard/src/WizardFooter/WizardFooter.tsx +++ b/packages/wizard/src/WizardFooter/WizardFooter.tsx @@ -1,8 +1,66 @@ -import React from 'react'; +import React, { MouseEventHandler } from 'react'; + +import { Direction } from '@leafygreen-ui/descendants'; +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'; -export function WizardFooter({}: WizardFooterProps) { - return
your content here
; -} +export const WizardFooter = CompoundSubComponent( + ({ + backButtonProps, + cancelButtonProps, + primaryButtonProps, + className, + ...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; + } -WizardFooter.displayName = 'WizardFooter'; + return ( + 0 + ? { + ...backButtonProps, + onClick: handleBackButtonClick, + } + : undefined + } + cancelButtonProps={cancelButtonProps} + primaryButtonProps={{ + ...primaryButtonProps, + onClick: handlePrimaryButtonClick, + }} + /> + ); + }, + { + displayName: 'WizardFooter', + key: WizardSubComponentProperties.Footer, + }, +); diff --git a/packages/wizard/src/WizardFooter/WizardFooter.types.ts b/packages/wizard/src/WizardFooter/WizardFooter.types.ts index 02f3f87b43..cf2617761d 100644 --- a/packages/wizard/src/WizardFooter/WizardFooter.types.ts +++ b/packages/wizard/src/WizardFooter/WizardFooter.types.ts @@ -1 +1,18 @@ -export interface WizardFooterProps {} \ No newline at end of file +import { FormFooterProps } from '@leafygreen-ui/form-footer'; + +export interface WizardFooterProps extends React.ComponentProps<'footer'> { + /** + * 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']; +} 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/TextNode.tsx b/packages/wizard/src/WizardStep/TextNode.tsx new file mode 100644 index 0000000000..e75679d7ed --- /dev/null +++ b/packages/wizard/src/WizardStep/TextNode.tsx @@ -0,0 +1,32 @@ +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.spec.tsx b/packages/wizard/src/WizardStep/WizardStep.spec.tsx index fb00cde028..0e312c3bcd 100644 --- a/packages/wizard/src/WizardStep/WizardStep.spec.tsx +++ b/packages/wizard/src/WizardStep/WizardStep.spec.tsx @@ -1,11 +1,29 @@ - import React from 'react'; import { render } from '@testing-library/react'; +import { Wizard } from '../Wizard/Wizard'; + import { WizardStep } from '.'; describe('packages/wizard-step', () => { - 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.stories.tsx b/packages/wizard/src/WizardStep/WizardStep.stories.tsx new file mode 100644 index 0000000000..c917ead80f --- /dev/null +++ b/packages/wizard/src/WizardStep/WizardStep.stories.tsx @@ -0,0 +1,74 @@ +import React from 'react'; +import { storybookArgTypes, StoryMetaType } from '@lg-tools/storybook-utils'; +import { StoryObj } from '@storybook/react'; + +import { Body } from '@leafygreen-ui/typography'; + +import { WizardProvider } from '../WizardContext'; + +import { WizardStep } from '.'; + +const meta: StoryMetaType = { + title: 'Composition/Wizard/WizardStep', + component: WizardStep, + parameters: { + default: 'LiveExample', + }, + decorators: [ + Story => ( + {}}> + + + ), + ], + argTypes: { + title: storybookArgTypes.children, + description: storybookArgTypes.children, + children: storybookArgTypes.children, + }, +}; + +export default meta; + +export const LiveExample: StoryObj = { + 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. + +
+ ), + }, + render: args => , +}; + +export const WithLongDescription: StoryObj = { + 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... + +
+ ), + }, +}; diff --git a/packages/wizard/src/WizardStep/WizardStep.styles.ts b/packages/wizard/src/WizardStep/WizardStep.styles.ts index 928608f58d..b38acdf587 100644 --- a/packages/wizard/src/WizardStep/WizardStep.styles.ts +++ b/packages/wizard/src/WizardStep/WizardStep.styles.ts @@ -1,4 +1,6 @@ - import { css } from '@leafygreen-ui/emotion'; +import { spacing } from '@leafygreen-ui/tokens'; -export const baseStyles = css``; +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 6c699df9e8..28ea51c963 100644 --- a/packages/wizard/src/WizardStep/WizardStep.tsx +++ b/packages/wizard/src/WizardStep/WizardStep.tsx @@ -1,8 +1,38 @@ import React from 'react'; + +import { cx } from '@leafygreen-ui/emotion'; +import { consoleOnce } from '@leafygreen-ui/lib'; +import { Description, H3 } from '@leafygreen-ui/typography'; + +import { WizardSubComponentProperties } from '../constants'; +import { CompoundSubComponent } from '../utils/CompoundSubComponent'; +import { useWizardContext } from '../WizardContext'; + +import { TextNode } from './TextNode'; +import { stepStyles } from './WizardStep.styles'; import { WizardStepProps } from './WizardStep.types'; -export function WizardStep({}: WizardStepProps) { - return
your content here
; -} +export const WizardStep = CompoundSubComponent( + ({ title, description, children, className, ...rest }: WizardStepProps) => { + const { isWizardContext } = useWizardContext(); + + if (!isWizardContext) { + consoleOnce.error( + 'Wizard.Step component must be used within a Wizard context.', + ); + return null; + } -WizardStep.displayName = 'WizardStep'; + return ( +
+ {title} + {description && {description}} +
{children}
+
+ ); + }, + { + displayName: 'WizardStep', + key: WizardSubComponentProperties.Step, + }, +); diff --git a/packages/wizard/src/WizardStep/WizardStep.types.ts b/packages/wizard/src/WizardStep/WizardStep.types.ts index 3998534991..b0e9e97f70 100644 --- a/packages/wizard/src/WizardStep/WizardStep.types.ts +++ b/packages/wizard/src/WizardStep/WizardStep.types.ts @@ -1 +1,19 @@ -export interface WizardStepProps {} \ No newline at end of file +import { ReactNode } from 'react'; + +export interface WizardStepProps + extends Omit, 'title'> { + /** + * 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/constants.ts b/packages/wizard/src/constants.ts new file mode 100644 index 0000000000..38d0121456 --- /dev/null +++ b/packages/wizard/src/constants.ts @@ -0,0 +1,6 @@ +export const WizardSubComponentProperties = { + Step: 'isWizardStep', + Footer: 'isWizardFooter', +} as const; +export type WizardSubComponentProperties = + (typeof WizardSubComponentProperties)[keyof typeof WizardSubComponentProperties]; diff --git a/packages/wizard/src/index.ts b/packages/wizard/src/index.ts index cfbd7d46d8..1d5270af64 100644 --- a/packages/wizard/src/index.ts +++ b/packages/wizard/src/index.ts @@ -1 +1,8 @@ -export { Wizard, type WizardProps } from './Wizard'; \ No newline at end of file +export { Wizard, type WizardProps } from './Wizard'; +export { + useWizardContext, + WizardContext, + type WizardContextData, +} from './WizardContext'; +export { type WizardFooterProps } from './WizardFooter'; +export { type WizardStepProps } from './WizardStep'; 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 {} 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..f60f9603b6 --- /dev/null +++ b/packages/wizard/src/utils/CompoundSubComponent.tsx @@ -0,0 +1,41 @@ +import { FunctionComponent } from 'react'; + +interface SubComponentProperties { + displayName: string; + key: Key; +} + +export type SubComponentType< + Key extends string, + Props extends {} = {}, +> = FunctionComponent & { + [key in Key]: true; +}; + +/** + * 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: SubComponentProperties, +): SubComponentType => { + const { key, ...rest } = properties; + return Object.assign(componentRenderFn, { + ...rest, + [key]: true, + }) as SubComponentType; +}; diff --git a/packages/wizard/src/utils/useWizardControlledValue/index.ts b/packages/wizard/src/utils/useWizardControlledValue/index.ts new file mode 100644 index 0000000000..07ea91dbb7 --- /dev/null +++ b/packages/wizard/src/utils/useWizardControlledValue/index.ts @@ -0,0 +1 @@ +export { useWizardControlledValue } from './useWizardControlledValue'; diff --git a/packages/wizard/src/utils/useWizardControlledValue/useWizardControlledValue.ts b/packages/wizard/src/utils/useWizardControlledValue/useWizardControlledValue.ts new file mode 100644 index 0000000000..1081c3691c --- /dev/null +++ b/packages/wizard/src/utils/useWizardControlledValue/useWizardControlledValue.ts @@ -0,0 +1,96 @@ +import { Dispatch, SetStateAction, 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: Dispatch>; +} + +/** + * 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: T = useMemo( + () => (isControlled ? (valueProp as T) : (initialProp as T)), + [initialProp, isControlled, valueProp], + ); + + // Keep track of the internal value state + const [uncontrolledValue, setUncontrolledValue] = useState( + initialValue as T, + ); + + // 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: Dispatch> = newVal => { + if (!isControlled) { + setUncontrolledValue(newVal); + } + + const val = typeof newVal === 'function' ? (newVal as Function)() : newVal; + onChange?.(val); + }; + + return { + isControlled, + value, + setValue, + }; +}; 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..d70919e807 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3792,15 +3792,43 @@ importers: packages/wizard: dependencies: + '@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 '@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 28dc06bed2197af1e02ee871d4545e7aa1216159 Mon Sep 17 00:00:00 2001 From: Adam Thompson <2414030+TheSonOfThomp@users.noreply.github.com> Date: Thu, 9 Oct 2025 17:54:52 -0400 Subject: [PATCH 7/8] chore(Wizard) Updates wizard utilities to use `hooks` and `compound-components` (#3200) * install cc * use CC in wiz * useControlled * rm isControlled check * lint --- packages/wizard/package.json | 1 + packages/wizard/src/Wizard/Wizard.tsx | 49 +++++----- .../wizard/src/WizardFooter/WizardFooter.tsx | 2 +- packages/wizard/src/WizardStep/WizardStep.tsx | 2 +- .../wizard/src/utils/CompoundComponent.tsx | 27 ------ .../wizard/src/utils/CompoundSubComponent.tsx | 41 -------- .../utils/useWizardControlledValue/index.ts | 1 - .../useWizardControlledValue.ts | 96 ------------------- packages/wizard/tsconfig.json | 6 ++ pnpm-lock.yaml | 3 + 10 files changed, 37 insertions(+), 191 deletions(-) delete mode 100644 packages/wizard/src/utils/CompoundComponent.tsx delete mode 100644 packages/wizard/src/utils/CompoundSubComponent.tsx delete mode 100644 packages/wizard/src/utils/useWizardControlledValue/index.ts delete mode 100644 packages/wizard/src/utils/useWizardControlledValue/useWizardControlledValue.ts diff --git a/packages/wizard/package.json b/packages/wizard/package.json index 1d5213c36e..5264c0ab18 100644 --- a/packages/wizard/package.json +++ b/packages/wizard/package.json @@ -29,6 +29,7 @@ }, "dependencies": { "@leafygreen-ui/button": "workspace:^", + "@leafygreen-ui/compound-component": "workspace:^", "@leafygreen-ui/descendants": "workspace:^", "@leafygreen-ui/emotion": "workspace:^", "@leafygreen-ui/form-footer": "workspace:^", diff --git a/packages/wizard/src/Wizard/Wizard.tsx b/packages/wizard/src/Wizard/Wizard.tsx index 6add408f04..ad5ed5d165 100644 --- a/packages/wizard/src/Wizard/Wizard.tsx +++ b/packages/wizard/src/Wizard/Wizard.tsx @@ -1,11 +1,14 @@ -import React from 'react'; +import React, { useCallback } from 'react'; +import { + CompoundComponent, + findChild, + findChildren, +} from '@leafygreen-ui/compound-component'; import { Direction } from '@leafygreen-ui/descendants'; -import { findChild, findChildren } from '@leafygreen-ui/lib'; +import { useControlled } from '@leafygreen-ui/hooks'; import { WizardSubComponentProperties } from '../constants'; -import { CompoundComponent } from '../utils/CompoundComponent'; -import { useWizardControlledValue } from '../utils/useWizardControlledValue/useWizardControlledValue'; import { WizardProvider } from '../WizardContext/WizardContext'; import { WizardFooter } from '../WizardFooter'; import { WizardStep } from '../WizardStep'; @@ -30,11 +33,8 @@ export const Wizard = CompoundComponent( ); // Controlled/Uncontrolled activeStep value - const { - isControlled, - value: activeStep, - setValue: setActiveStep, - } = useWizardControlledValue(activeStepProp, undefined, 0); + const { value: activeStep, updateValue: setActiveStep } = + useControlled(activeStepProp, onStepChange, 0); if ( activeStepProp && @@ -47,22 +47,23 @@ export const Wizard = CompoundComponent( ); } - 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 updateStep = useCallback( + (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); - } - - onStepChange?.(getNextStep(activeStep)); - }; + // TODO pass getNextStep into setter as callback https://jira.mongodb.org/browse/LG-5607 + const nextStep = getNextStep(activeStep); + setActiveStep(nextStep); + }, + [activeStep, setActiveStep, stepChildren.length], + ); // 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 6a8d7d99c2..2a46c9201a 100644 --- a/packages/wizard/src/WizardFooter/WizardFooter.tsx +++ b/packages/wizard/src/WizardFooter/WizardFooter.tsx @@ -1,11 +1,11 @@ import React, { MouseEventHandler } from 'react'; +import { CompoundSubComponent } from '@leafygreen-ui/compound-component'; import { Direction } from '@leafygreen-ui/descendants'; 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'; diff --git a/packages/wizard/src/WizardStep/WizardStep.tsx b/packages/wizard/src/WizardStep/WizardStep.tsx index 28ea51c963..4af5eba958 100644 --- a/packages/wizard/src/WizardStep/WizardStep.tsx +++ b/packages/wizard/src/WizardStep/WizardStep.tsx @@ -1,11 +1,11 @@ import React from 'react'; +import { CompoundSubComponent } from '@leafygreen-ui/compound-component'; import { cx } from '@leafygreen-ui/emotion'; import { consoleOnce } from '@leafygreen-ui/lib'; import { Description, H3 } from '@leafygreen-ui/typography'; import { WizardSubComponentProperties } from '../constants'; -import { CompoundSubComponent } from '../utils/CompoundSubComponent'; import { useWizardContext } from '../WizardContext'; import { TextNode } from './TextNode'; diff --git a/packages/wizard/src/utils/CompoundComponent.tsx b/packages/wizard/src/utils/CompoundComponent.tsx deleted file mode 100644 index 2af7309c04..0000000000 --- a/packages/wizard/src/utils/CompoundComponent.tsx +++ /dev/null @@ -1,27 +0,0 @@ -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 deleted file mode 100644 index f60f9603b6..0000000000 --- a/packages/wizard/src/utils/CompoundSubComponent.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { FunctionComponent } from 'react'; - -interface SubComponentProperties { - displayName: string; - key: Key; -} - -export type SubComponentType< - Key extends string, - Props extends {} = {}, -> = FunctionComponent & { - [key in Key]: true; -}; - -/** - * 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: SubComponentProperties, -): SubComponentType => { - const { key, ...rest } = properties; - return Object.assign(componentRenderFn, { - ...rest, - [key]: true, - }) as SubComponentType; -}; diff --git a/packages/wizard/src/utils/useWizardControlledValue/index.ts b/packages/wizard/src/utils/useWizardControlledValue/index.ts deleted file mode 100644 index 07ea91dbb7..0000000000 --- a/packages/wizard/src/utils/useWizardControlledValue/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { useWizardControlledValue } from './useWizardControlledValue'; diff --git a/packages/wizard/src/utils/useWizardControlledValue/useWizardControlledValue.ts b/packages/wizard/src/utils/useWizardControlledValue/useWizardControlledValue.ts deleted file mode 100644 index 1081c3691c..0000000000 --- a/packages/wizard/src/utils/useWizardControlledValue/useWizardControlledValue.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { Dispatch, SetStateAction, 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: Dispatch>; -} - -/** - * 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: T = useMemo( - () => (isControlled ? (valueProp as T) : (initialProp as T)), - [initialProp, isControlled, valueProp], - ); - - // Keep track of the internal value state - const [uncontrolledValue, setUncontrolledValue] = useState( - initialValue as T, - ); - - // 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: Dispatch> = newVal => { - if (!isControlled) { - setUncontrolledValue(newVal); - } - - const val = typeof newVal === 'function' ? (newVal as Function)() : newVal; - onChange?.(val); - }; - - return { - isControlled, - value, - setValue, - }; -}; diff --git a/packages/wizard/tsconfig.json b/packages/wizard/tsconfig.json index d245893b68..26dc97f771 100644 --- a/packages/wizard/tsconfig.json +++ b/packages/wizard/tsconfig.json @@ -9,6 +9,12 @@ "include": ["src/**/*"], "exclude": ["**/*.spec.*", "**/*.stories.*"], "references": [ + { + "path": "../button" + }, + { + "path": "../compound-component" + }, { "path": "../button" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f6ca4c6abe..a31f6d0f5e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3730,6 +3730,9 @@ importers: '@leafygreen-ui/button': specifier: workspace:^ version: link:../button + '@leafygreen-ui/compound-component': + specifier: workspace:^ + version: link:../compound-component '@leafygreen-ui/descendants': specifier: workspace:^ version: link:../descendants From 3d508454ffa21b229da4b7462b3e3766eab5a1ab Mon Sep 17 00:00:00 2001 From: Adam Thompson Date: Fri, 10 Oct 2025 16:44:14 -0400 Subject: [PATCH 8/8] init wizard changeset --- .changeset/wizard.md | 5 +++++ packages/wizard/package.json | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/wizard.md diff --git a/.changeset/wizard.md b/.changeset/wizard.md new file mode 100644 index 0000000000..9d2bf9262d --- /dev/null +++ b/.changeset/wizard.md @@ -0,0 +1,5 @@ +--- +'@leafygreen-ui/wizard': minor +--- + +Initial Wizard package release diff --git a/packages/wizard/package.json b/packages/wizard/package.json index 5264c0ab18..be08f12182 100644 --- a/packages/wizard/package.json +++ b/packages/wizard/package.json @@ -1,7 +1,7 @@ { "name": "@leafygreen-ui/wizard", - "version": "0.1.0", + "version": "0.0.1", "description": "LeafyGreen UI Kit Wizard", "main": "./dist/umd/index.js", "module": "./dist/esm/index.js",