Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Switch out ReachUI's MenuButton for RadixUI's DropDownMenu #120

Merged
merged 2 commits into from
Dec 9, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@
"dependencies": {
"@hookform/resolvers": "^2.6.0",
"@next/bundle-analyzer": "^11.0.0",
"@radix-ui/react-dropdown-menu": "^0.1.1",
"@reach/alert": "^0.16.0",
"@reach/dialog": "^0.15.3",
"@reach/menu-button": "^0.16.2",
"@reach/tabs": "^0.16.4",
"@tailwindcss/aspect-ratio": "^0.2.1",
"axios": "^0.21.1",
Expand Down
88 changes: 38 additions & 50 deletions src/modules/layouts/components/UserMenu/UserMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,66 +11,54 @@ import { useAuth } from '@modules/user-auth/contexts/AuthContext';
import AuthApi from '@services/AuthApi';

const UserMenu: VFC = () => {
const { t } = useTranslation();
const router = useRouter();
const { isAuthenticated } = useAuth();
const { registerModal, loginModal } = useModals();

return (
<Menu button={<UserMenuButton />}>
{isAuthenticated ? (
<AuthenticatedUserMenuContent />
<MenuItemGroup>
<MenuLink href="/account">
{t('common:routes.account.index')}
</MenuLink>
</MenuItemGroup>
) : (
<UnauthenticatedUserMenuContent />
<MenuItemGroup tw="font-semibold">
<MenuItem onSelect={registerModal?.show}>
{t('common:userMenu.register')}
</MenuItem>
Andrewnt219 marked this conversation as resolved.
Show resolved Hide resolved
<MenuItem onSelect={loginModal?.show}>
{t('common:userMenu.login')}
</MenuItem>
</MenuItemGroup>
)}
</Menu>
);
};

const UnauthenticatedUserMenuContent: VFC = () => {
const { t } = useTranslation();
const { registerModal, loginModal } = useModals();
return (
<>
<MenuItemGroup tw="font-semibold">
<MenuItem onSelect={registerModal?.show}>
{t('common:userMenu.register')}
</MenuItem>
<MenuItem onSelect={loginModal?.show}>
{t('common:userMenu.login')}
</MenuItem>
</MenuItemGroup>
<MenuItemGroup label={t('common:userMenu.quickLinks')}>
<MenuLink href="/">{t('common:routes.home')}</MenuLink>
</MenuItemGroup>
</>
);
};

const AuthenticatedUserMenuContent: VFC = () => {
const { t } = useTranslation();
const router = useRouter();
return (
<>
<MenuItemGroup>
<MenuLink href="/account">{t('common:routes.account.index')}</MenuLink>
</MenuItemGroup>
<MenuItemGroup label={t('common:userMenu.quickLinks')}>
<MenuLink href="/">{t('common:routes.home')}</MenuLink>
<MenuLink href="/account/security">
{t('common:routes.account.security')}
</MenuLink>
<MenuLink href="/wishlist">{t('common:routes.wishlist')}</MenuLink>
{isAuthenticated && (
<>
<MenuLink href="/account/security">
{t('common:routes.account.security')}
</MenuLink>
<MenuLink href="/wishlist">{t('common:routes.wishlist')}</MenuLink>
</>
)}
</MenuItemGroup>
<MenuItemGroup>
<MenuItem
tw="text-danger"
onSelect={async () => {
await router.push('/');
await AuthApi.signOut();
}}
>
{t('common:userMenu.logout')}
</MenuItem>
</MenuItemGroup>
</>
{isAuthenticated && (
<MenuItemGroup>
<MenuItem
tw="text-danger"
onSelect={async () => {
await router.push('/');
await AuthApi.signOut();
}}
>
{t('common:userMenu.logout')}
</MenuItem>
</MenuItemGroup>
)}
</Menu>
);
};

Expand Down
25 changes: 14 additions & 11 deletions src/pages/_app.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { CustomNextPage } from '@/next';
import type { AppProps } from 'next/app';
import { IdProvider as RadixIdProvider } from '@radix-ui/react-id';
import SWRDefaultConfigProvider from '@libs/swr/SWRDefaultConfigProvider';
import { AuthProvider } from '@modules/user-auth/contexts/AuthContext';
import { ModalProvider } from '@ui/Modal/ModalContext';
Expand All @@ -20,17 +21,19 @@ function MyApp({ Component, pageProps }: MyAppProps) {
const getLayout = Component.getLayout ?? ((page) => page);

return (
<SWRDefaultConfigProvider>
<AuthProvider>
<SnackbarProvider>
<ModalProvider>
<TwinStyles />
<GlobalStyle />
{getLayout(<Component {...pageProps} />)}
</ModalProvider>
</SnackbarProvider>
</AuthProvider>
</SWRDefaultConfigProvider>
<RadixIdProvider>
<SWRDefaultConfigProvider>
<AuthProvider>
<SnackbarProvider>
<ModalProvider>
<TwinStyles />
<GlobalStyle />
{getLayout(<Component {...pageProps} />)}
</ModalProvider>
</SnackbarProvider>
</AuthProvider>
</SWRDefaultConfigProvider>
</RadixIdProvider>
);
}

Expand Down
2 changes: 1 addition & 1 deletion src/ui/Button/Link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
BaseProps,
} from './styles';

type LinkProps = ComponentProps<'a'> &
export type LinkProps = ComponentProps<'a'> &
BaseProps & {
href: string;
nextLinkProps?: Omit<NextLinkProps, 'href'>;
Expand Down
61 changes: 26 additions & 35 deletions src/ui/Menu/Menu.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,18 @@
import { Children, isValidElement, ReactNode, FC } from 'react';
import {
Menu as ReachMenu,
MenuButton as ReachMenuButton,
MenuList as ReachMenuList,
MenuListProps as ReachMenuListProps,
} from '@reach/menu-button';
import { menuStyle } from './styles';
import { ReactNode, FC } from 'react';
import * as RadixDropdownMenu from '@radix-ui/react-dropdown-menu';
import tw, { styled } from 'twin.macro';

type MenuProps = ReachMenuListProps & {
type MenuProps = RadixDropdownMenu.DropdownMenuProps & {
/**
* *A single {@link JSX.Element}* as the button to control the popup menu to be rendered where {@link Menu} is used.
*
* **Note:** A valid button here must be an HTML native element or an element that implement the {@link React.forwardRef} API
* or the menu won't be interactive.
* **Note:** A valid button here must be an HTML native element or an element that implement the {@link React.forwardRef} API.
*/
button: ReactNode;
/**
* Props passed into the popup menu element
*/
menuPopupProps?: RadixDropdownMenu.DropdownMenuContentProps;
};

/**
Expand All @@ -29,35 +27,28 @@ type MenuProps = ReachMenuListProps & {
* <MenuLink href="/url-of-link-1">Link 1</MenuLink>
* </Menu>
* ```
*
* **Note:** Avoid rendering items asynchronously as it throws off focusing order.
* Rendering 2 completely separated item list if need to.
*
* @see `@modules/layouts/components/UserMenu/UserMenu.tsx`
*/
const Menu: FC<MenuProps> = ({ button, children, ...props }) => {
const btn = Children.only(button);

const Menu: FC<MenuProps> = ({
button,
children,
menuPopupProps,
...props
}) => {
return (
<ReachMenu>
<ReachMenuButton
as={getMenuButtonComponentType(btn)}
{...(isValidElement(btn) && btn.props)}
/>
<ReachMenuList css={menuStyle} {...props}>
<RadixDropdownMenu.Root {...props}>
<RadixDropdownMenu.Trigger asChild>{button}</RadixDropdownMenu.Trigger>
<MenuContent align="start" sideOffset={8} {...menuPopupProps}>
{children}
</ReachMenuList>
</ReachMenu>
</MenuContent>
</RadixDropdownMenu.Root>
);
};

export default Menu;

/**
* Getter for the element type of the menu-control button
* (i.e.: `ButtonGhost` in the case of the example above)
* A component provides styling for {@link RadixDropdownMenu.Content} (the menu popup)
*/
const getMenuButtonComponentType = (button: ReactNode) =>
isValidElement(button) && typeof button.type !== 'string'
? button.type
: 'button';

export default Menu;
const MenuContent = styled(RadixDropdownMenu.Content)`
${tw`font-normal bg-white min-w-[12.5rem] shadow-z8 rounded`}
`;
16 changes: 9 additions & 7 deletions src/ui/Menu/MenuItem.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import {
MenuItem as ReachMenuItem,
MenuItemProps as ReachMenuItemProps,
} from '@reach/menu-button';
import { FC } from 'react';
import * as RadixDropdownMenu from '@radix-ui/react-dropdown-menu';
import BaseMenuItem from './MenuItemBase';

type MenuItemProps = ReachMenuItemProps;
type MenuItemProps = RadixDropdownMenu.DropdownMenuItemProps;

/**
* An item to be used within a `Menu` with an `onSelect` handler to perform user-defined action upon sleected.
*/
const MenuItem: FC<MenuItemProps> = (props) => {
return <ReachMenuItem as="span" {...props} />;
const MenuItem: FC<MenuItemProps> = ({ children, ...props }) => {
return (
<BaseMenuItem {...props} asChild>
<span>{children}</span>
</BaseMenuItem>
);
};

export default MenuItem;
15 changes: 15 additions & 0 deletions src/ui/Menu/MenuItemBase.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import * as RadixDropdownMenu from '@radix-ui/react-dropdown-menu';
import tw, { styled } from 'twin.macro';

/**
* A component provides styling for items used within a `Menu`
*/
const MenuItemBase = styled(RadixDropdownMenu.Item)`
${tw`font-inherit block px-md py-sm hover:bg-light`}

&:focus-visible {
${tw`ring-2 ring-dark`}
}
`;

export default MenuItemBase;
60 changes: 41 additions & 19 deletions src/ui/Menu/MenuItemGroup.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { useUuid } from '@hooks/useUuid';
import { FC, ReactNode } from 'react';
import tw, { styled } from 'twin.macro';
import * as RadixDropdownMenu from '@radix-ui/react-dropdown-menu';
import Text from '@ui/Text/Text';
import { ComponentProps, FC, ReactNode, useMemo } from 'react';

type MenuItemGroupProps = ComponentProps<'div'> & {
type MenuItemGroupProps = RadixDropdownMenu.DropdownMenuGroupProps & {
/**
* Label of the MenuItemGroup
*/
Expand All @@ -25,7 +26,7 @@ type MenuItemGroupProps = ComponentProps<'div'> & {
* <MenuLink href="/url-to-link-2">Item 2</MenuLink>
* <MenuLink href="/url-to-link-3">Item 3</MenuLink>
* </MenuItemGroup>
* <MenuItemGroup label={Group 3}> // You can mix as well
* <MenuItemGroup label={`Group 3`}> // You can mix as well
* <MenuItem onSelect={() => { ... }}>Item 3</MenuItem>
* <MenuItem onSelect={() => { ... }}>Item 4</MenuItem>
* <MenuLink href="/url-to-link-3">Item 4</MenuLink>
Expand All @@ -38,23 +39,44 @@ const MenuItemGroup: FC<MenuItemGroupProps> = ({
children,
...props
}) => {
const id = useUuid();
const labelId = useMemo(() => `Menu-MenuItemGroup-Title-${id}`, [id]);
return (
<div
role="group"
aria-labelledby={label ? labelId : undefined}
data-label={label}
{...props}
>
{label && (
<Text id={labelId} variant="overline" tw="px-md py-sm uppercase">
{label}
</Text>
)}
{children}
</div>
<>
<MenuGroup {...props}>
{label && (
<MenuLabel asChild>
<Text variant="overline">{label}</Text>
</MenuLabel>
)}
{children}
</MenuGroup>
<MenuSeparator asChild>
<hr />
</MenuSeparator>
</>
);
};

export default MenuItemGroup;

/**
* A component provides styling for {@link RadixDropdownMenu.Group}
*/
const MenuGroup = styled(RadixDropdownMenu.Group)`
${tw`py-sm`}
`;

/**
* A component provides styling for {@link RadixDropdownMenu.Label}
*/
const MenuLabel = styled(RadixDropdownMenu.Label)`
${tw`px-md py-sm uppercase`}
`;

/**
* A component provides styling for {@link RadixDropdownMenu.Separator}
*/
const MenuSeparator = styled(RadixDropdownMenu.Separator)`
&:last-child {
${tw`hidden`}
}
`;