From 79d273cb48029c386b7157098f88d1aa3dbe4553 Mon Sep 17 00:00:00 2001 From: Eric Crowell Date: Sat, 29 Jun 2024 19:49:48 +0200 Subject: [PATCH] feat: Added ui state for dialog and modal management --- .../src/components/Drawer/Drawer.stories.tsx | 15 ++++ packages/ui/src/components/Drawer/Drawer.tsx | 15 +++- .../ui/src/components/Drawer/Drawer.types.ts | 11 +++ packages/ui/src/context.ts | 14 ++++ packages/ui/src/hooks.ts | 1 + packages/ui/src/hooks/usePathname.ts | 46 +++++++++++ packages/ui/src/provider.tsx | 54 ++----------- packages/ui/src/reducer.ts | 19 +++++ packages/ui/src/reducers/drawer.actions.ts | 69 +++++++++++++++++ packages/ui/src/reducers/drawer.reducer.ts | 76 +++++++++++++++++++ 10 files changed, 272 insertions(+), 48 deletions(-) create mode 100644 packages/ui/src/hooks/usePathname.ts create mode 100644 packages/ui/src/reducer.ts create mode 100644 packages/ui/src/reducers/drawer.actions.ts create mode 100644 packages/ui/src/reducers/drawer.reducer.ts diff --git a/packages/ui/src/components/Drawer/Drawer.stories.tsx b/packages/ui/src/components/Drawer/Drawer.stories.tsx index 286596a..2647d1f 100644 --- a/packages/ui/src/components/Drawer/Drawer.stories.tsx +++ b/packages/ui/src/components/Drawer/Drawer.stories.tsx @@ -1,6 +1,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import { Drawer } from './Drawer'; +import { useState } from 'react'; const meta = { component: Drawer, @@ -19,3 +20,17 @@ export const Default: Story = { open: true, } }; + +export const Controlled: Story = { + render: (args) => { + + const [ open, openSet ] = useState(false); + + return (<> + + openSet(false)} /> + ); + }, + args: {} +}; + diff --git a/packages/ui/src/components/Drawer/Drawer.tsx b/packages/ui/src/components/Drawer/Drawer.tsx index a3bd319..db48414 100644 --- a/packages/ui/src/components/Drawer/Drawer.tsx +++ b/packages/ui/src/components/Drawer/Drawer.tsx @@ -8,7 +8,10 @@ import { useDebounce } from '@do-ob/ui/hooks'; export function Drawer({ open = false, + dismissable = true, onClose = nop, + onOpen = nop, + onOpenChange = nop, children, // className, // ...props @@ -16,13 +19,23 @@ export function Drawer({ const isOpen = useDebounce(open, 300); + const handleOpenChange = (next: boolean) => { + onOpenChange(next); + if (!next) { + onClose(); + } else { + onOpen(); + } + }; + return ( void; + + onOpen?: () => void; } diff --git a/packages/ui/src/context.ts b/packages/ui/src/context.ts index 8e29ff5..528511e 100644 --- a/packages/ui/src/context.ts +++ b/packages/ui/src/context.ts @@ -5,6 +5,8 @@ import React from 'react'; import { ThemeMode } from '@do-ob/ui/types'; import { nop } from '@do-ob/core'; import { createContext } from 'react'; +import type { Action, State } from './reducer'; +import { initialState } from './reducer'; /** * Context properties for the do-ob ui provider @@ -36,6 +38,16 @@ export interface DoobUiContextProps { * Toggle the theme mode. */ modeToggle?: () => void; + + /** + * The user interface (ui) state. + */ + state: State; + + /** + * The user interface (ui) dispatch. + */ + dispatch: React.Dispatch; } /** @@ -47,6 +59,8 @@ export const doobUiContextDefaultProps: DoobUiContextProps = { pathname: '', mode: 'light', modeToggle: nop, + state: initialState, + dispatch: nop }; /** diff --git a/packages/ui/src/hooks.ts b/packages/ui/src/hooks.ts index 325c1e4..7a2eea1 100644 --- a/packages/ui/src/hooks.ts +++ b/packages/ui/src/hooks.ts @@ -3,3 +3,4 @@ export * from './hooks/useMode'; export * from './hooks/useTypewriter'; export * from './hooks/useOverflow'; export * from './hooks/useDebounce'; +export * from './hooks/usePathname'; diff --git a/packages/ui/src/hooks/usePathname.ts b/packages/ui/src/hooks/usePathname.ts new file mode 100644 index 0000000..f40e942 --- /dev/null +++ b/packages/ui/src/hooks/usePathname.ts @@ -0,0 +1,46 @@ +import { useEffect, useState } from 'react'; + +/** + * A simple hook to get the current pathname + */ +export function usePathname(override?: string): string { + const [ pathname, setPathname ] = useState(override ?? window.location.pathname); + + useEffect(() => { + if(override) { + setPathname(override); + return; + } + + const handleLocationChange = () => { + setPathname(window.location.pathname); + }; + + // Function to wrap history methods to dispatch events + const wrapHistoryMethod = (method: 'pushState' | 'replaceState') => { + const originalMethod = history[method]; + + return function (this: History, ...args: never[]) { + const result = originalMethod.apply(this, args as unknown as Parameters); + window.dispatchEvent(new Event('locationchange')); + return result; + }; + }; + + // Wrap the pushState and replaceState methods + history.pushState = wrapHistoryMethod('pushState'); + history.replaceState = wrapHistoryMethod('replaceState'); + + // Listen for popstate and custom locationchange events + window.addEventListener('popstate', handleLocationChange); + window.addEventListener('locationchange', handleLocationChange); + + // Clean up the event listeners on component unmount + return () => { + window.removeEventListener('popstate', handleLocationChange); + window.removeEventListener('locationchange', handleLocationChange); + }; + }, [ override ]); + + return pathname; +}; diff --git a/packages/ui/src/provider.tsx b/packages/ui/src/provider.tsx index 8e3d6ed..258ee0f 100644 --- a/packages/ui/src/provider.tsx +++ b/packages/ui/src/provider.tsx @@ -1,10 +1,11 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ + 'use client'; import React from 'react'; import { RouterProvider } from 'react-aria-components'; import { DoobUiContext, DoobUiContextProps, doobUiContextDefaultProps } from '@do-ob/ui/context'; -import { useMode } from '@do-ob/ui/hooks'; +import { useMode, usePathname } from '@do-ob/ui/hooks'; +import { reducer, initialState } from '@do-ob/ui/reducer'; export interface DoobUiProviderProps { /** @@ -32,51 +33,6 @@ export interface DoobUiProviderProps { mode?: DoobUiContextProps['mode']; } -/** - * A simple hook to get the current pathname - */ -const usePathname = (override: string): string => { - const [ pathname, setPathname ] = React.useState(override ?? window.location.pathname); - - React.useEffect(() => { - if(override) { - setPathname(override); - return; - } - - const handleLocationChange = () => { - setPathname(window.location.pathname); - }; - - // Function to wrap history methods to dispatch events - const wrapHistoryMethod = (method: 'pushState' | 'replaceState') => { - const originalMethod = history[method]; - - return function (this: History, ...args: any[]) { - const result = originalMethod.apply(this, args as any); - window.dispatchEvent(new Event('locationchange')); - return result; - }; - }; - - // Wrap the pushState and replaceState methods - history.pushState = wrapHistoryMethod('pushState'); - history.replaceState = wrapHistoryMethod('replaceState'); - - // Listen for popstate and custom locationchange events - window.addEventListener('popstate', handleLocationChange); - window.addEventListener('locationchange', handleLocationChange); - - // Clean up the event listeners on component unmount - return () => { - window.removeEventListener('popstate', handleLocationChange); - window.removeEventListener('locationchange', handleLocationChange); - }; - }, [ override ]); - - return pathname; -}; - /** * The provider for the doob context */ @@ -87,6 +43,8 @@ export function DoobUiProvider({ ...props }: React.PropsWithChildren) { + const [ state, dispatch ] = React.useReducer(reducer, initialState); + const pathname = usePathname(pathnameProp); const { mode, modeToggle } = useMode(props.mode); @@ -95,6 +53,8 @@ export function DoobUiProvider({ { + if (drawer.id !== payload.id) { + acc[drawer.id] = drawer; + } + return acc; + }, {} as DrawerState['drawers']) + }; + case 'drawer/open': + return { + ...state, + drawers: { + ...state.drawers, + [payload.id]: { + ...state.drawers[payload.id], + open: true, + } + } + }; + case 'drawer/close': + return { + ...state, + drawers: { + ...state.drawers, + [payload.id]: { + ...state.drawers[payload.id], + open: false, + } + } + }; + default: + return state; + } + +} +