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 1 commit
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
3 changes: 2 additions & 1 deletion next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ const bundleAnalyzer = require('@next/bundle-analyzer')({
const nextTranslate = require('next-translate');

const nextConfig = {
reactStrictMode: true,
// Temporary set to `false` to disable mismatch `id` error caused by Radix UI
reactStrictMode: false,
Andrewnt219 marked this conversation as resolved.
Show resolved Hide resolved
webpack: (config) => {
// Unset client-side javascript that only works server-side
config.resolve.fallback = { fs: false, module: false };
Expand Down
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
59 changes: 24 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 { ReactNode, FC } from 'react';
import * as RadixDropdownMenu from '@radix-ui/react-dropdown-menu';
import { menuStyle } from './styles';

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,26 @@ 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>
<RadixDropdownMenu.Content
css={menuStyle}
align="start"
sideOffset={8}
{...menuPopupProps}
>
{children}
</ReachMenuList>
</ReachMenu>
</RadixDropdownMenu.Content>
</RadixDropdownMenu.Root>
);
};

/**
* Getter for the element type of the menu-control button
* (i.e.: `ButtonGhost` in the case of the example above)
*/
const getMenuButtonComponentType = (button: ReactNode) =>
isValidElement(button) && typeof button.type !== 'string'
? button.type
: 'button';

export default Menu;
10 changes: 4 additions & 6 deletions src/ui/Menu/MenuItem.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
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 { menuItemStyle } from './styles';

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} />;
return <RadixDropdownMenu.Item css={menuItemStyle} {...props} />;
};

export default MenuItem;
43 changes: 24 additions & 19 deletions src/ui/Menu/MenuItemGroup.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import { useUuid } from '@hooks/useUuid';
import { FC, ReactNode } from 'react';
import * as RadixDropdownMenu from '@radix-ui/react-dropdown-menu';
import Text from '@ui/Text/Text';
import { ComponentProps, FC, ReactNode, useMemo } from 'react';
import {
menuGroupLabelStyle,
menuGroupStyle,
menuSeparatorStyle,
} from './styles';

type MenuItemGroupProps = ComponentProps<'div'> & {
type MenuItemGroupProps = RadixDropdownMenu.DropdownMenuGroupProps & {
/**
* Label of the MenuItemGroup
*/
Expand All @@ -25,7 +30,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,22 +43,22 @@ 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>
<>
<RadixDropdownMenu.Group css={menuGroupStyle} {...props}>
{label && (
<RadixDropdownMenu.Label asChild>
<Text variant="overline" css={menuGroupLabelStyle}>
{label}
</Text>
</RadixDropdownMenu.Label>
)}
{children}
</RadixDropdownMenu.Group>
<RadixDropdownMenu.Separator asChild css={menuSeparatorStyle}>
<hr />
</RadixDropdownMenu.Separator>
</>
);
};

Expand Down
23 changes: 14 additions & 9 deletions src/ui/Menu/MenuLink.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
import {
MenuLink as ReachMenuLink,
MenuLinkProps as ReachMenuLinkProps,
} from '@reach/menu-button';
import { LinkBase } from '@ui/Button/Link';
import { FC } from 'react';
import * as RadixDropdownMenu from '@radix-ui/react-dropdown-menu';
import { LinkBase, LinkProps } from '@ui/Button/Link';
import { menuItemStyle } from './styles';

type MenuLinkProps = ReachMenuLinkProps & {
href: string;
type MenuLinkProps = Omit<LinkProps, 'ref'> & {
/**
* Props passed into the popup menu item
*/
menuItemProps?: RadixDropdownMenu.DropdownMenuItemProps;
};

/**
* An item to be used within a `Menu` with an `href` prop to navigate user to a URL upon sleected.
*/
const MenuLink: FC<MenuLinkProps> = (props) => {
return <ReachMenuLink as={LinkBase} {...props} />;
const MenuLink: FC<MenuLinkProps> = ({ menuItemProps, ...props }) => {
return (
<RadixDropdownMenu.Item {...menuItemProps} asChild>
<LinkBase css={menuItemStyle} {...props}></LinkBase>
</RadixDropdownMenu.Item>
);
};

export default MenuLink;
1 change: 0 additions & 1 deletion src/ui/Menu/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,3 @@ export { default as Menu } from './Menu';
export { default as MenuItemGroup } from './MenuItemGroup';
export { default as MenuItem } from './MenuItem';
export { default as MenuLink } from './MenuLink';
export { useMenuButtonContext as useMenu } from '@reach/menu-button';