diff --git a/package.json b/package.json index 1d692f3..0f0a4bc 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "react-aria": "nightly", "react-aria-components": "nightly", "react-dom": "19.0.0-rc-8971381549-20240625", + "reselect": "^5.1.1", "storybook": "^8.2.0-alpha.10", "tailwind-merge": "^2.3.0", "tailwindcss": "^3.4.3", diff --git a/packages/ui/src/components/Button/Button.tsx b/packages/ui/src/components/Button/Button.tsx index e475f9e..4064eca 100644 --- a/packages/ui/src/components/Button/Button.tsx +++ b/packages/ui/src/components/Button/Button.tsx @@ -5,6 +5,7 @@ import { Button as AriaButton, Link as AriaLink } from 'react-aria-components'; import { fillStyles, emptyStyles, cn, interactiveStyles } from '@do-ob/ui/utility'; import { ButtonProps, ButtonVariant, ButtonSize } from './Button.types'; import { Polymorphic } from '@do-ob/ui/types'; +import { useDialogControl } from '@do-ob/ui/hooks'; /** * Define tailwind classes for the variants. @@ -59,9 +60,12 @@ export function Button< endContent = null, iconify = false, href, + dialog, ...props }: ButtonProps & Polymorphic) { + const dialogControlProps = useDialogControl(dialog); + const Tag = as ?? (href ? AriaLink : AriaButton); const isExternal = href && (href.startsWith('http://') || href.startsWith('https://')); @@ -101,6 +105,7 @@ export function Button< className )} {...linkProps} + {...dialogControlProps} {...props} > {startContent && {startContent}} diff --git a/packages/ui/src/components/Button/Button.types.ts b/packages/ui/src/components/Button/Button.types.ts index 2b2700e..4cecf70 100644 --- a/packages/ui/src/components/Button/Button.types.ts +++ b/packages/ui/src/components/Button/Button.types.ts @@ -13,4 +13,5 @@ export interface ButtonProps { className?: string; iconify?: boolean; href?: string; + dialog?: string; } diff --git a/packages/ui/src/components/Drawer/Drawer.stories.tsx b/packages/ui/src/components/Drawer/Drawer.stories.tsx index 7bd186b..e10b94b 100644 --- a/packages/ui/src/components/Drawer/Drawer.stories.tsx +++ b/packages/ui/src/components/Drawer/Drawer.stories.tsx @@ -2,7 +2,6 @@ import type { Meta, StoryObj } from '@storybook/react'; import { Drawer } from './Drawer'; import { Button } from '@do-ob/ui/components'; -import { useDrawerControl } from '@do-ob/ui/hooks'; const meta = { component: Drawer, @@ -15,10 +14,8 @@ type Story = StoryObj; export const Controlled: Story = { render: function Render(args) { - const controllerProps = useDrawerControl('example'); - return (<> - + ); }, diff --git a/packages/ui/src/components/Drawer/Drawer.tsx b/packages/ui/src/components/Drawer/Drawer.tsx index f50ae2f..471481c 100644 --- a/packages/ui/src/components/Drawer/Drawer.tsx +++ b/packages/ui/src/components/Drawer/Drawer.tsx @@ -1,12 +1,13 @@ 'use client'; -import { ModalOverlay, Modal, Dialog } from 'react-aria-components'; +import { ModalOverlay, Modal, Dialog, Heading } from 'react-aria-components'; // import { cn } from '@do-ob/ui/utility'; import type { DrawerProps } from './Drawer.types'; import { nop } from '@do-ob/core'; import { dialogActions } from '@do-ob/ui/reducer'; -import { useDebounce, useDispatch, useSelector } from '@do-ob/ui/hooks'; -import { useEffect } from 'react'; +import { useDebounce } from '@do-ob/ui/hooks'; +import { use, useEffect } from 'react'; +import { DialogContext, DialogDispatchContext } from '@do-ob/ui/context'; export function Drawer({ name, @@ -19,9 +20,9 @@ export function Drawer({ // ...props }: DrawerProps & React.HTMLAttributes) { - const id = `drawer/${name}`; - const drawer = useSelector((state) => state.dialog.items[id]) ?? { id, open: false }; - const dispatch = useDispatch(); + const id = name; + const drawer = use(DialogContext).items[id] ?? { id, open: false }; + const dispatch = use(DialogDispatchContext); const isOpen = useDebounce(!!drawer.open, 300); const handleOpenChange = (next: boolean) => { @@ -37,13 +38,10 @@ export function Drawer({ }; useEffect(() => { - dispatch(dialogActions.register(id)); - return () => { dispatch(dialogActions.unregister(id)); }; - }, [ dispatch, id ]); return ( @@ -64,6 +62,7 @@ export function Drawer({ }} > + {name} {children} diff --git a/packages/ui/src/context.ts b/packages/ui/src/context.ts index 528511e..25cee85 100644 --- a/packages/ui/src/context.ts +++ b/packages/ui/src/context.ts @@ -5,8 +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'; + +export * from './context/DialogsContext'; /** * Context properties for the do-ob ui provider @@ -38,16 +38,6 @@ export interface DoobUiContextProps { * Toggle the theme mode. */ modeToggle?: () => void; - - /** - * The user interface (ui) state. - */ - state: State; - - /** - * The user interface (ui) dispatch. - */ - dispatch: React.Dispatch; } /** @@ -59,8 +49,6 @@ export const doobUiContextDefaultProps: DoobUiContextProps = { pathname: '', mode: 'light', modeToggle: nop, - state: initialState, - dispatch: nop }; /** diff --git a/packages/ui/src/context/DialogsContext.ts b/packages/ui/src/context/DialogsContext.ts new file mode 100644 index 0000000..cb24b31 --- /dev/null +++ b/packages/ui/src/context/DialogsContext.ts @@ -0,0 +1,8 @@ +import { Dispatch, createContext } from 'react'; +import { reducer as dialogReducer } from '../reducers/dialog.reducer'; +import { DialogAction } from '../reducers/dialog.actions'; +import { nop } from '@do-ob/core'; + +export const DialogContext = createContext(dialogReducer()); + +export const DialogDispatchContext = createContext>(nop); diff --git a/packages/ui/src/hooks.ts b/packages/ui/src/hooks.ts index 436afb2..68cfa9f 100644 --- a/packages/ui/src/hooks.ts +++ b/packages/ui/src/hooks.ts @@ -4,6 +4,4 @@ export * from './hooks/useTypewriter'; export * from './hooks/useOverflow'; export * from './hooks/useDebounce'; export * from './hooks/usePathname'; -export * from './hooks/useSelector'; -export * from './hooks/useDispatch'; -export * from './hooks/useDrawerControl'; +export * from './hooks/useDialogControl'; diff --git a/packages/ui/src/hooks/useDialogControl.ts b/packages/ui/src/hooks/useDialogControl.ts new file mode 100644 index 0000000..22b0986 --- /dev/null +++ b/packages/ui/src/hooks/useDialogControl.ts @@ -0,0 +1,15 @@ +import { dialogActions } from '@do-ob/ui/reducer'; +import { use, useCallback } from 'react'; +import { DialogDispatchContext } from '@do-ob/ui/context'; + +export function useDialogControl(name: string = '') { + const dispatch = use(DialogDispatchContext); + + const onPress = useCallback(() => { + dispatch(dialogActions.toggle(name)); + }, [ dispatch, name ]); + + return { + onPress, + }; +}; diff --git a/packages/ui/src/hooks/useDispatch.ts b/packages/ui/src/hooks/useDispatch.ts deleted file mode 100644 index 13a895a..0000000 --- a/packages/ui/src/hooks/useDispatch.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { DoobUiContext } from '@do-ob/ui/context'; -import { use } from 'react'; - -export function useDispatch() { - const { dispatch } = use(DoobUiContext); - return dispatch; -}; diff --git a/packages/ui/src/hooks/useDrawerControl.ts b/packages/ui/src/hooks/useDrawerControl.ts deleted file mode 100644 index da03e82..0000000 --- a/packages/ui/src/hooks/useDrawerControl.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { dialogActions } from '@do-ob/ui/reducer'; -import { useDispatch } from './useDispatch'; -import { useSelector } from './useSelector'; - -export function useDrawerControl(name: string) { - const id = `drawer/${name}`; - const drawer = useSelector((state) => state.dialog.items[id]); - const dispatch = useDispatch(); - - const onPress = () => { - if (!drawer) { - return; - } - if (drawer?.open) { - console.log('closing'); - dispatch(dialogActions.close(id)); - } else { - console.log('opening'); - dispatch(dialogActions.open(id)); - } - }; - - return drawer ? { - onPress - } : {}; -}; diff --git a/packages/ui/src/hooks/useSelector.ts b/packages/ui/src/hooks/useSelector.ts deleted file mode 100644 index 8acd4bf..0000000 --- a/packages/ui/src/hooks/useSelector.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { DoobUiContext } from '@do-ob/ui/context'; -import type { State } from '@do-ob/ui/reducer'; -import { use, useCallback } from 'react'; - -export function useSelector(selector: (state: State) => TSelected): TSelected { - const { state } = use(DoobUiContext); - return useCallback(() => selector(state), [ state, selector ])(); -}; diff --git a/packages/ui/src/provider.tsx b/packages/ui/src/provider.tsx index 258ee0f..b0cde80 100644 --- a/packages/ui/src/provider.tsx +++ b/packages/ui/src/provider.tsx @@ -3,9 +3,15 @@ import React from 'react'; import { RouterProvider } from 'react-aria-components'; -import { DoobUiContext, DoobUiContextProps, doobUiContextDefaultProps } from '@do-ob/ui/context'; +import { + DoobUiContext, + DoobUiContextProps, + doobUiContextDefaultProps, + DialogContext, + DialogDispatchContext, +} from '@do-ob/ui/context'; import { useMode, usePathname } from '@do-ob/ui/hooks'; -import { reducer, initialState } from '@do-ob/ui/reducer'; +import { dialogReducer } from '@do-ob/ui/reducer'; export interface DoobUiProviderProps { /** @@ -43,7 +49,7 @@ export function DoobUiProvider({ ...props }: React.PropsWithChildren) { - const [ state, dispatch ] = React.useReducer(reducer, initialState); + const [ dialogState, dialogDispatch ] = React.useReducer(dialogReducer, dialogReducer()); const pathname = usePathname(pathnameProp); const { mode, modeToggle } = useMode(props.mode); @@ -53,13 +59,15 @@ export function DoobUiProvider({ - {children} + + + {children} + + ); diff --git a/packages/ui/src/reducer.ts b/packages/ui/src/reducer.ts index 7d51764..df31f7b 100644 --- a/packages/ui/src/reducer.ts +++ b/packages/ui/src/reducer.ts @@ -1,24 +1,3 @@ -import { reducer as dialogReducer } from './reducers/dialog.reducer'; +export { reducer as dialogReducer } from './reducers/dialog.reducer'; export * as dialogActions from './reducers/dialog.actions'; -import type { DialogState } from './reducers/dialog.reducer'; -import type { DialogAction } from './reducers/dialog.actions'; - -export interface State { - dialog: DialogState; -} - -export type Action = DialogAction; - -export const initialState: State = { - dialog: dialogReducer() -}; - -export function reducer(state: State = initialState, action: unknown = {}) { - - console.log({ action }); - - return { - ...state, - dialog: dialogReducer(state.dialog, action as DialogAction), - }; -} +export type * from './reducers/dialog.reducer'; diff --git a/packages/ui/src/reducers/dialog.actions.ts b/packages/ui/src/reducers/dialog.actions.ts index 77fb3e3..c21555f 100644 --- a/packages/ui/src/reducers/dialog.actions.ts +++ b/packages/ui/src/reducers/dialog.actions.ts @@ -8,6 +8,11 @@ export type DialogAction = { payload: { id: string; } +} | { + type: 'dialog/toggle', + payload: { + id: string; + } } | { type: 'dialog/open', payload: { @@ -44,6 +49,18 @@ export function unregister(id: string): DialogAction { }; } +/** + * Toggles a dialog between open and closed. + */ +export function toggle(id: string): DialogAction { + return { + type: 'dialog/toggle', + payload: { + id, + } + }; +} + /** * Opens a dialog. */ diff --git a/packages/ui/src/reducers/dialog.reducer.ts b/packages/ui/src/reducers/dialog.reducer.ts index 5d75609..581acba 100644 --- a/packages/ui/src/reducers/dialog.reducer.ts +++ b/packages/ui/src/reducers/dialog.reducer.ts @@ -35,6 +35,9 @@ export function reducer( } }; case 'dialog/unregister': + if(!state.items[payload.id]) { + return state; + } return { ...state, items: Object.values(state.items).reduce((acc, dialog) => { @@ -44,7 +47,24 @@ export function reducer( return acc; }, {} as DialogState['items']) }; + case 'dialog/toggle': + if(!state.items[payload.id]) { + return state; + } + return { + ...state, + items: { + ...state.items, + [payload.id]: { + ...state.items[payload.id], + open: !state.items[payload.id].open, + } + } + }; case 'dialog/open': + if(!state.items[payload.id]) { + return state; + } return { ...state, items: { @@ -56,6 +76,9 @@ export function reducer( } }; case 'dialog/close': + if(!state.items[payload.id]) { + return state; + } return { ...state, items: { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 92408be..b78617c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -113,6 +113,9 @@ importers: react-dom: specifier: 19.0.0-rc-8971381549-20240625 version: 19.0.0-rc-8971381549-20240625(react@19.0.0-rc-8971381549-20240625) + reselect: + specifier: ^5.1.1 + version: 5.1.1 storybook: specifier: ^8.2.0-alpha.10 version: 8.2.0-alpha.10(@babel/preset-env@7.24.7(@babel/core@7.24.7))(react-dom@19.0.0-rc-8971381549-20240625(react@19.0.0-rc-8971381549-20240625))(react@19.0.0-rc-8971381549-20240625) @@ -5770,6 +5773,9 @@ packages: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} + reselect@5.1.1: + resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -15224,6 +15230,8 @@ snapshots: require-from-string@2.0.2: {} + reselect@5.1.1: {} + resolve-from@4.0.0: {} resolve-from@5.0.0: {}