Skip to content

Commit

Permalink
dev: Added popovers to navigation elements with children
Browse files Browse the repository at this point in the history
  • Loading branch information
eric-crowell committed Jul 1, 2024
1 parent fe1f68d commit 5919941
Show file tree
Hide file tree
Showing 13 changed files with 999 additions and 24 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"react-aria": "nightly",
"react-aria-components": "nightly",
"react-dom": "19.0.0-rc-8971381549-20240625",
"react-stately": "nightly",
"storybook": "^8.2.0-alpha.10",
"tailwind-merge": "^2.3.0",
"tailwindcss": "^3.4.3",
Expand Down
1 change: 1 addition & 0 deletions packages/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"@tailwindcss/typography": "^0.5.13",
"react-aria": "nightly",
"react-aria-components": "nightly",
"react-stately": "nightly",
"tailwindcss-animate": "^1.0.7",
"tailwind-merge": "^2.3.0",
"tailwindcss-react-aria-components": "^1.1.3"
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/src/components/Drawer/Drawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export function Drawer({
}
}, [ onOpenChange, onClose, onOpen, dismissable, drawer.open, controls ]);

useDialogRegister(id);
useDialogRegister(id, 'drawer');

return (
<ModalOverlay
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/src/components/Popover/Popover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export function Popover({
useOutsidePress(popoverRef, handleOutsidePress);

// Register the popover dialog.
useDialogRegister(id);
useDialogRegister(id, 'popover');

const handleOpenChange = useCallback((next: boolean) => {
if (!next) {
Expand Down
6 changes: 3 additions & 3 deletions packages/ui/src/hooks/useDialogRegister.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ import { dialogActions } from '@do-ob/ui/reducer';
import { use, useEffect } from 'react';
import { DialogDispatchContext } from '@do-ob/ui/context';

export function useDialogRegister(id: string) {
export function useDialogRegister(id: string, type?: 'modal' | 'popover' | 'drawer') {
const dispatch = use(DialogDispatchContext);

useEffect(() => {
dispatch(dialogActions.register(id));
dispatch(dialogActions.register(id, type));
return () => {
dispatch(dialogActions.unregister(id));
};
}, [ dispatch, id ]);
}, [ dispatch, id, type ]);
};
4 changes: 3 additions & 1 deletion packages/ui/src/reducers/dialog.actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export type DialogAction = {
type: 'dialog/register',
payload: {
id: string;
type?: 'modal' | 'popover' | 'drawer';
}
} | {
type: 'dialog/unregister',
Expand Down Expand Up @@ -31,11 +32,12 @@ export type DialogAction = {
/**
* Registers a dialog.
*/
export function register(id: string): DialogAction {
export function register(id: string, type?: 'modal' | 'popover' | 'drawer'): DialogAction {
return {
type: 'dialog/register',
payload: {
id,
type,
}
};
}
Expand Down
19 changes: 18 additions & 1 deletion packages/ui/src/reducers/dialog.reducer.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@

import type { DialogAction } from './dialog.actions';

export interface DialogState {
items: Record<string, {
id: string;
open: boolean;
type?: 'modal' | 'popover' | 'drawer';
triggerRef?: React.RefObject<HTMLElement | null>;
}>
}
Expand All @@ -23,6 +23,8 @@ export function reducer(

const { type, payload } = action;

console.log({ type, payload });

switch (type) {
case 'dialog/register':
return {
Expand All @@ -32,6 +34,7 @@ export function reducer(
[payload.id]: {
id: payload.id,
open: false,
type: payload.type,
}
}
};
Expand All @@ -52,6 +55,20 @@ export function reducer(
if(!state.items[payload.id]) {
return state;
}

// Ensure all other popovers are closed when a popover is opened.
if (state.items[payload.id].open === true && state.items[payload.id].type === 'popover') {
state.items = Object.values(state.items).reduce((acc, dialog) => {
if (dialog.type === 'popover') {
acc[dialog.id] = {
...dialog,
open: false,
};
}
return acc;
}, {} as DialogState['items']);
}

return {
...state,
items: {
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/src/tailwind.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import type { Config } from 'tailwindcss';
import tailwindPlugin from 'tailwindcss/plugin';
import tailwindColors from 'tailwindcss/colors';
import { join } from 'node:path';
import { join } from 'path';
import tailwindReactAria from 'tailwindcss-react-aria-components';
import tailwindContainerQueries from '@tailwindcss/container-queries';
import tailwindAnimate from 'tailwindcss-animate';
Expand Down
9 changes: 6 additions & 3 deletions packages/ui/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

export * from './types/actions';
export * from './types/locale';

Expand Down Expand Up @@ -56,11 +55,15 @@ export interface Link {
title: string;

/**
* Nested links.
* Subitems
*/
links?: Link[];
items?: Link[];
}

/**
* A Link Tree
*/

export type Socials = 'facebook' | 'x' | 'instagram' | 'linkedin' | 'youtube';

/**
Expand Down
12 changes: 8 additions & 4 deletions packages/ui/src/widgets/Navigation/Navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Bars3Icon } from '@do-ob/ui/icons';
import { NavigationProps } from './Navigation.types';
import { NavigationTabs } from './NavigationTabs';
import { useDialogControl } from '@do-ob/ui/hooks';
import { NavigationPopovers } from './NavigationPopovers';

export function Navigation(props: NavigationProps & React.HTMLAttributes<HTMLDivElement>) {

Expand All @@ -19,7 +20,8 @@ export function Navigation(props: NavigationProps & React.HTMLAttributes<HTMLDiv
...divProps
} = props;

const drawerId = useId();
const id = useId();
const drawerId = `${id}-drawer`;
const ref = useRef<HTMLDivElement>(null);
const overflowing = useOverflow(ref, 'x');

Expand All @@ -34,7 +36,7 @@ export function Navigation(props: NavigationProps & React.HTMLAttributes<HTMLDiv
'relative overflow-hidden p-2',
className
)} {...divProps}>
<NavigationTabs overflowing={overflowing} base={props} />
<NavigationTabs id={id} overflowing={overflowing} base={props} />

<div className="absolute left-0 top-0 flex h-full items-center p-2">
<Button
Expand All @@ -51,18 +53,20 @@ export function Navigation(props: NavigationProps & React.HTMLAttributes<HTMLDiv
</Button>
</div>

{orientation === 'horizontal' ? (
{orientation === 'horizontal' ? (<>
<Drawer id={drawerId} title={label} direction="left">
<NavigationTabs
base={{
...props,
orientation: 'vertical'
}}
id={id}
overflowing={false}
onSelectionChange={handleSelectionChange}
/>
</Drawer>
) :null}
<NavigationPopovers base={props} id={id} />
</>) :null}

</div>
);
Expand Down
27 changes: 27 additions & 0 deletions packages/ui/src/widgets/Navigation/NavigationPopovers.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Popover } from '@do-ob/ui/components';
import { NavigationProps } from './Navigation.types';

export function NavigationPopovers({
base,
id,
}: { base: NavigationProps, id: string }) {

const {
links = [],
} = base;

return (
<>
{links.map((link) => (
<Popover
key={link.url}
id={`${id}-${link.url}`}
placement="bottom"
nonmodal
>
<h2>{link.title}</h2>
</Popover>
))}
</>
);
}
54 changes: 45 additions & 9 deletions packages/ui/src/widgets/Navigation/NavigationTabs.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,34 @@
import { use } from 'react';
'use client';

import { createRef, use, useRef } from 'react';
import { NavigationProps } from './Navigation.types';
import { DoobUiContext } from '@do-ob/ui/context';
import { DialogDispatchContext, DoobUiContext } from '@do-ob/ui/context';
import { Tab, TabList, Tabs } from 'react-aria-components';
import { cn, interactiveStyles } from '@do-ob/ui/utility';
import { nop } from '@do-ob/core';
import { ChevronDownIcon } from '@do-ob/ui/icons';
import { dialogActions } from '@do-ob/ui/reducer';

export function NavigationTabs({
base: {
label = 'Site Navigation',
links = [],
orientation = 'horizontal',
},
id,
overflowing,
onSelectionChange = nop,
}: {
base: NavigationProps;
id: string;
overflowing?: boolean;
onSelectionChange?: (key: string) => void;
}) {

const tabRefs = useRef<Record<string, HTMLElement | null>>({});

const { pathname } = use(DoobUiContext);
const dispatch = use(DialogDispatchContext);

let selected: string | null = null;
links
Expand All @@ -44,10 +52,28 @@ export function NavigationTabs({
)}
keyboardActivation="manual"
onSelectionChange={(key) => {
console.log('key', key);

// Call the onSelectionChange callback if the selected path matches the key
if(key === selected) {
onSelectionChange(key);
}

// Only for horizontal navigation.
if(orientation === 'horizontal') {
const selectedLink = links.find((link) => link.url === key);

// If the selected link has sub-items, open a dialog.
if(selectedLink?.items?.length) {
const element = tabRefs.current[key] ?? null;

const ref = createRef<HTMLElement | null>();
ref.current = element;
dispatch(dialogActions.open(
`${id}-${selectedLink.url}`,
ref
));
}
}
}}
>
<TabList className="flex gap-1 orientation-horizontal:flex-row orientation-vertical:flex-col" aria-label={label}>
Expand All @@ -62,9 +88,9 @@ export function NavigationTabs({
'group relative inline-flex h-11 flex-row items-center gap-1 rounded px-3 active:text-primary hover:text-primary selected:font-bold dark:active:text-primary-dark dark:hover:text-primary-dark [&>*:first-child]:selected:bg-primary',
orientation === 'horizontal' ? 'justify-center [&>*:first-child]:selected:h-[6px]' : 'justify-start [&>*:first-child]:selected:w-[6px]',
)}
key={link.title}
key={link.url}
id={link.url}
href={link.links?.length ? undefined : link.url}
href={link.items?.length ? undefined : link.url}
>
<div
className={cn(
Expand All @@ -73,10 +99,20 @@ export function NavigationTabs({
)}
aria-hidden="true"
></div>
{link.title}
{link.links?.length ? (
<ChevronDownIcon className="size-4" />
) : null}
<div
ref={(el) => {
tabRefs.current[link.url] = el;
}}
className={cn(
'flex size-full flex-row gap-1',
orientation === 'horizontal' ? 'items-center justify-center' : 'items-center justify-start'
)}
>
{link.title}
{orientation === 'horizontal' && link.items?.length ? (
<ChevronDownIcon className="size-4" />
) : null}
</div>
</Tab>
))}
</TabList>
Expand Down
Loading

0 comments on commit 5919941

Please sign in to comment.