Skip to content

Commit

Permalink
feat: Added ui state for dialog and modal management
Browse files Browse the repository at this point in the history
  • Loading branch information
eric-crowell committed Jun 29, 2024
1 parent 9d6bc8e commit 79d273c
Show file tree
Hide file tree
Showing 10 changed files with 272 additions and 48 deletions.
15 changes: 15 additions & 0 deletions packages/ui/src/components/Drawer/Drawer.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Meta, StoryObj } from '@storybook/react';

import { Drawer } from './Drawer';
import { useState } from 'react';

const meta = {
component: Drawer,
Expand All @@ -19,3 +20,17 @@ export const Default: Story = {
open: true,
}
};

export const Controlled: Story = {
render: (args) => {

const [ open, openSet ] = useState(false);

Check failure on line 27 in packages/ui/src/components/Drawer/Drawer.stories.tsx

View workflow job for this annotation

GitHub Actions / release / integrity / Test

React Hook "useState" is called in function "render" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. React Hook names must start with the word "use"

return (<>
<button onClick={() => openSet(true)}>Click me</button>
<Drawer {...args} open={open} onClose={() => openSet(false)} />
</>);
},
args: {}
};

15 changes: 14 additions & 1 deletion packages/ui/src/components/Drawer/Drawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,34 @@ import { useDebounce } from '@do-ob/ui/hooks';

export function Drawer({
open = false,
dismissable = true,
onClose = nop,
onOpen = nop,
onOpenChange = nop,
children,
// className,
// ...props
}: DrawerProps & React.HTMLAttributes<HTMLElement>) {

const isOpen = useDebounce(open, 300);

const handleOpenChange = (next: boolean) => {
onOpenChange(next);
if (!next) {
onClose();
} else {
onOpen();
}
};

return (
<ModalOverlay
className="fixed inset-0 bg-black/40 backdrop-blur-[2px] transition-all duration-300 entering:bg-transparent entering:backdrop-blur-0 exiting:bg-transparent exiting:backdrop-blur-0"
isOpen={open}
isDismissable={dismissable}
isEntering={!open}
isExiting={!open && isOpen}
onOpenChange={onClose}
onOpenChange={handleOpenChange}
>
<Modal
className="fixed w-1/3 min-w-80 bg-white shadow-md transition-all duration-500 entering:translate-x-full exiting:translate-x-full"
Expand Down
11 changes: 11 additions & 0 deletions packages/ui/src/components/Drawer/Drawer.types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
export interface DrawerProps {
/**
* Controls the open state of the drawer.
*/
open?: boolean;

/**
* If the drawer can be dismissed by clicking outside of it.
*/
dismissable?: boolean;

onClose?: () => void;

onOpen?: () => void;
}
14 changes: 14 additions & 0 deletions packages/ui/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<Action>;
}

/**
Expand All @@ -47,6 +59,8 @@ export const doobUiContextDefaultProps: DoobUiContextProps = {
pathname: '',
mode: 'light',
modeToggle: nop,
state: initialState,
dispatch: nop
};

/**
Expand Down
1 change: 1 addition & 0 deletions packages/ui/src/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export * from './hooks/useMode';
export * from './hooks/useTypewriter';
export * from './hooks/useOverflow';
export * from './hooks/useDebounce';
export * from './hooks/usePathname';
46 changes: 46 additions & 0 deletions packages/ui/src/hooks/usePathname.ts
Original file line number Diff line number Diff line change
@@ -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<string>(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<typeof originalMethod>);
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;
};
54 changes: 7 additions & 47 deletions packages/ui/src/provider.tsx
Original file line number Diff line number Diff line change
@@ -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 {
/**
Expand Down Expand Up @@ -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<string>(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
*/
Expand All @@ -87,6 +43,8 @@ export function DoobUiProvider({
...props
}: React.PropsWithChildren<DoobUiProviderProps>) {

const [ state, dispatch ] = React.useReducer(reducer, initialState);

const pathname = usePathname(pathnameProp);
const { mode, modeToggle } = useMode(props.mode);

Expand All @@ -95,6 +53,8 @@ export function DoobUiProvider({
<DoobUiContext.Provider value={{
...doobUiContextDefaultProps,
...props,
state,
dispatch,
pathname,
mode,
modeToggle
Expand Down
19 changes: 19 additions & 0 deletions packages/ui/src/reducer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { reducer as drawerReducer } from './reducers/drawer.reducer';
import type { DrawerState } from './reducers/drawer.reducer';
import type { DrawerAction } from './reducers/drawer.actions';

export interface State {
drawer: DrawerState;
}

export type Action = DrawerAction;

export const initialState: State = {
drawer: drawerReducer()
};

export function reducer(state: State = initialState, action: unknown = {}) {
return {
drawer: drawerReducer(state.drawer, action as DrawerAction),
};
}
69 changes: 69 additions & 0 deletions packages/ui/src/reducers/drawer.actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
export type DrawerAction = {
type: 'drawer/register',
payload: {
id: string;
}
} | {
type: 'drawer/unregister',
payload: {
id: string;
}
} | {
type: 'drawer/open',
payload: {
id: string;
}
} | {
type: 'drawer/close',
payload: {
id: string;
}
};

/**
* Registers a drawer.
*/
export function register(id: string): DrawerAction {
return {
type: 'drawer/register',
payload: {
id,
}
};
}

/**
* Unregisters a drawer.
*/
export function unregister(id: string): DrawerAction {
return {
type: 'drawer/unregister',
payload: {
id,
}
};
}

/**
* Opens a drawer.
*/
export function open(id: string): DrawerAction {
return {
type: 'drawer/open',
payload: {
id,
}
};
}

/**
* Closes a drawer.
*/
export function close(id: string): DrawerAction {
return {
type: 'drawer/close',
payload: {
id,
}
};
}
Loading

0 comments on commit 79d273c

Please sign in to comment.