-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(react-router): initial package w/ Wizard, Step components (#20)
- Loading branch information
1 parent
3e0bffd
commit e918544
Showing
13 changed files
with
369 additions
and
67 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
{ | ||
"name": "@robo-wizard/react-router", | ||
"version": "1.0.0", | ||
"description": "RoboWizard components and hooks for react-router", | ||
"main": "./dist/robo-wizard_react-router.umd.js", | ||
"module": "./dist/robo-wizard_react-router.es.js", | ||
"private": false, | ||
"publishConfig": { | ||
"access": "public" | ||
}, | ||
"exports": { | ||
".": { | ||
"import": "./dist/robo-wizard_react-router.es.js", | ||
"require": "./dist/robo-wizard_react-router.umd.js" | ||
} | ||
}, | ||
"types": "dist/index.d.ts", | ||
"typedocMain": "src/index.ts", | ||
"sideEffects": false, | ||
"scripts": { | ||
"start": "vite build --watch", | ||
"build": "vite build", | ||
"test": "vitest", | ||
"prepare": "vite build" | ||
}, | ||
"keywords": [ | ||
"wizard", | ||
"workflow", | ||
"state machine", | ||
"xstate" | ||
], | ||
"author": "HipsterBrown", | ||
"homepage": "http://robo-wizard.js.org", | ||
"license": "MIT", | ||
"repository": { | ||
"type": "git", | ||
"url": "https://github.com/HipsterBrown/robo-wizard.git", | ||
"directory": "packages/react-router" | ||
}, | ||
"dependencies": { | ||
"robo-wizard": "^1.1.0", | ||
"@robo-wizard/react": "^1.0.0" | ||
}, | ||
"devDependencies": { | ||
"@testing-library/jest-dom": "^5.16.4", | ||
"@testing-library/react": "^13.2.0", | ||
"@testing-library/react-hooks": "^8.0.0", | ||
"@types/react": "^18.0.8", | ||
"@types/react-dom": "^18.0.3", | ||
"@vitejs/plugin-react": "^1.3.2", | ||
"jsdom": "^19.0.0", | ||
"react": "^18.1.0", | ||
"react-dom": "^18.1.0", | ||
"react-router": "^6.0.0", | ||
"vite": "^2.9.8", | ||
"vite-plugin-dts": "^1.1.1", | ||
"vitest": "^0.10.0" | ||
}, | ||
"peerDependencies": { | ||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0", | ||
"react-router": "^6.0.0" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import { Route, RouteProps } from 'react-router' | ||
import type { StepConfig } from 'robo-wizard' | ||
|
||
/** | ||
* @param props - accept same props as the [Route component from react-router](https://reactrouter.com/docs/en/v6/api#routes-and-route), excepte for `path`, as well as [[StepConfig]] from `robo-wizard` | ||
**/ | ||
export function Step({ name, ...routeProps }: Exclude<RouteProps, 'path'> & StepConfig) { | ||
return <Route path={name} {...routeProps} />; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from "./Step"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
import { Children, createContext, useContext, useEffect, PropsWithChildren, ReactNode, ReactElement } from 'react'; | ||
import { Route, Routes, useNavigate, useRoutes, Navigate, useLocation } from 'react-router'; | ||
import type { RoboWizard, BaseValues, StepConfig } from 'robo-wizard' | ||
import { useWizard } from '@robo-wizard/react' | ||
import { Step } from '../components' | ||
|
||
const WizardContext = createContext<null | RoboWizard>(null); | ||
|
||
export type WizardProviderProps<Values extends object> = PropsWithChildren<{ | ||
initialValues?: Values; | ||
}> | ||
|
||
function isReactElement(child: ReactNode): child is ReactElement { | ||
return typeof child === 'object' && child !== null && 'type' in child; | ||
} | ||
|
||
/** | ||
* @typeParam Values Optional object to describe values being gathered by the wizard | ||
* @param props | ||
* | ||
* Create a routed wizard experience under a Router from [react-router](https://reactrouter.com) | ||
* | ||
* An example set up for the Wizard: | ||
* ```tsx | ||
* function App() { | ||
* return ( | ||
* <BrowserRouter> | ||
* <Wizard<Values> initialValues={{ firstName: '', lastName: '' }}> | ||
* <Step name="first" element={<First />} /> | ||
* <Step name="second" element={<Second />} /> | ||
* <Step name="third" element={<Third />} /> | ||
* </Wizard> | ||
* </BrowserRouter> | ||
* ) | ||
* } | ||
* ``` | ||
* | ||
* An example step component: | ||
* ```tsx | ||
* const First = () => { | ||
* const wizard = useWizardContext<Values>(); | ||
* | ||
* const onSubmit = (event: FormEvent<HTMLFormElement>) => { | ||
* event.preventDefault(); | ||
* const values = Object.fromEntries(new FormData(event.currentTarget)) | ||
* wizard.goToNextStep({ values }) | ||
* } | ||
* | ||
* return ( | ||
* <> | ||
* <p>{wizard.currentStep} step</p> | ||
* <form onSubmit={onSubmit}> | ||
* <div> | ||
* <label htmlFor="firstName" id="firstName-label"> | ||
* First Name: | ||
* </label> | ||
* <input | ||
* type="text" | ||
* name="firstName" | ||
* id="firstName" | ||
* aria-labelledby="firstName-label" | ||
* defaultValue={wizard.currentValues.firstName} | ||
* autoFocus={true} | ||
* /> | ||
* </div> | ||
* <div> | ||
* <button type="button" onClick={() => wizard.goToPreviousStep()}role="link" >Previous</button> | ||
* <button type="submit">Next</button> | ||
* </div> | ||
* </form> | ||
* </> | ||
* ) | ||
* } | ||
* ``` | ||
**/ | ||
export function Wizard<Values extends object = BaseValues>({ children, initialValues = {} as Values }: WizardProviderProps<Values>) { | ||
if (typeof children !== 'object' || children === null) throw new Error('WizardProvider must have at least one child Step component') | ||
|
||
const steps = Children.map(children, child => { | ||
if (isReactElement(child) && child.type === Step) { | ||
return { ...child.props as StepConfig, path: child.props.name as string }; | ||
} | ||
return null; | ||
})?.filter(Boolean) ?? []; | ||
const navigate = useNavigate(); | ||
const location = useLocation(); | ||
const wizard = useWizard(steps, initialValues, { | ||
navigate: () => { | ||
navigate(wizard.currentStep); | ||
} | ||
}); | ||
const step = useRoutes(steps) | ||
const stepFromLocation = location.pathname.split('/').pop(); | ||
|
||
useEffect(() => { | ||
if (stepFromLocation && stepFromLocation !== wizard.currentStep) { | ||
wizard.sync({ step: stepFromLocation }) | ||
} | ||
}, [stepFromLocation, wizard]) | ||
|
||
return ( | ||
<WizardContext.Provider value={Object.create(wizard)}> | ||
{step} | ||
<Routes> | ||
<Route index={true} element={<Navigate to={String(steps[0]?.name)} replace={true} />} /> | ||
</Routes> | ||
</WizardContext.Provider> | ||
) | ||
} | ||
|
||
/** | ||
* @typeParam Values - object describing the [[currentValues]] gathered by [[RoboWizard]] | ||
* | ||
* Access the [[RoboWizard]] from the [[Wizard]] Context Provider | ||
**/ | ||
export function useWizardContext<Values extends object = BaseValues>() { | ||
const wizard = useContext(WizardContext); | ||
if (wizard === null) throw new Error('useWizardContext must be used within WizardProvider') | ||
return wizard as RoboWizard<Values>; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export * from "./context"; | ||
export * from "./components"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
import { describe, it, expect } from 'vitest' | ||
import { render, screen, fireEvent } from '@testing-library/react' | ||
import { MemoryRouter, useNavigate } from 'react-router' | ||
import { Wizard, Step, useWizardContext } from '../src' | ||
|
||
function TestStep() { | ||
const wizard = useWizardContext() | ||
const navigate = useNavigate() | ||
|
||
return ( | ||
<> | ||
<h1>{wizard.currentStep} step</h1> | ||
<button type="button" onClick={() => navigate(-1)}>Back</button> | ||
<button type="button" onClick={() => wizard.goToNextStep()}>Next</button> | ||
</> | ||
) | ||
} | ||
|
||
describe('Wizard', () => { | ||
const subject = () => ( | ||
<MemoryRouter> | ||
<Wizard> | ||
<Step name="first" element={<TestStep />} /> | ||
<Step name="second" element={<TestStep />} /> | ||
<Step name="third" element={<TestStep />} /> | ||
</Wizard> | ||
</MemoryRouter> | ||
) | ||
|
||
it('navigates through steps', () => { | ||
render(subject()); | ||
|
||
expect(screen.getByText('first step')).toBeTruthy(); | ||
|
||
fireEvent.click(screen.getByText('Next')) | ||
|
||
expect(screen.getByText('second step')).toBeTruthy(); | ||
|
||
fireEvent.click(screen.getByText('Next')) | ||
|
||
expect(screen.getByText('third step')).toBeTruthy(); | ||
|
||
fireEvent.click(screen.getByText('Back')) | ||
|
||
expect(screen.getByText('second step')).toBeTruthy(); | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
import '@testing-library/jest-dom' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
{ | ||
"extends": "../../tsconfig.json", | ||
"include": [ | ||
"./src", | ||
"./types" | ||
], | ||
"exclude": [ | ||
"node_modules", | ||
"test", | ||
"dist" | ||
], | ||
"compilerOptions": { | ||
"rootDir": "src", | ||
"lib": [ | ||
"DOM", | ||
"DOM.Iterable", | ||
"ES2019" | ||
], | ||
"jsx": "react-jsx", | ||
"target": "es2020", | ||
"skipLibCheck": true | ||
} | ||
} |
Oops, something went wrong.