feat(ui): ux improvements & redesign

This is a squash merge of a bajillion messy small commits created while iterating on the UI component library and redesign.
This commit is contained in:
psychedelicious
2023-12-29 00:03:21 +11:00
committed by Kent Keirsey
parent a47d91f0e7
commit f0b102d830
889 changed files with 16645 additions and 15595 deletions

View File

@@ -0,0 +1,52 @@
import type { Meta, StoryObj } from '@storybook/react';
import { InvMenuItem } from 'common/components/InvMenu/InvMenuItem';
import { InvMenuList } from 'common/components/InvMenu/InvMenuList';
import { InvText } from 'common/components/InvText/wrapper';
import { useCallback } from 'react';
import { FaCopy, FaDownload, FaTrash } from 'react-icons/fa6';
import { InvContextMenu } from './InvContextMenu';
const meta: Meta<typeof InvContextMenu> = {
title: 'Primitives/InvContextMenu',
tags: ['autodocs'],
component: InvContextMenu,
};
export default meta;
type Story = StoryObj<typeof InvContextMenu>;
const Component = () => {
const renderMenuFunc = useCallback(
() => (
<InvMenuList>
<InvMenuItem icon={<FaDownload />} command="⌘S">
Download
</InvMenuItem>
<InvMenuItem icon={<FaCopy />} command="⌘C">
Create a Copy
</InvMenuItem>
<InvMenuItem>Mark as Draft</InvMenuItem>
<InvMenuItem icon={<FaTrash />} isDestructive>
Delete
</InvMenuItem>
<InvMenuItem>Attend a Workshop</InvMenuItem>
</InvMenuList>
),
[]
);
return (
<InvContextMenu<HTMLParagraphElement> renderMenu={renderMenuFunc}>
{(ref) => (
<InvText ref={ref} p={5} bg="base.500">
Right-click me
</InvText>
)}
</InvContextMenu>
);
};
export const Default: Story = {
render: Component,
};

View File

@@ -0,0 +1,114 @@
/**
* This is a copy-paste of https://github.com/lukasbach/chakra-ui-contextmenu with a small change.
*
* The reactflow background element somehow prevents the chakra `useOutsideClick()` hook from working.
* With a menu open, clicking on the reactflow background element doesn't close the menu.
*
* Reactflow does provide an `onPaneClick` to handle clicks on the background element, but it is not
* straightforward to programatically close the menu.
*
* As a (hopefully temporary) workaround, we will use a dirty hack:
* - create `globalContextMenuCloseTrigger: number` in `ui` slice
* - increment it in `onPaneClick` (and wherever else we want to close the menu)
* - `useEffect()` to close the menu when `globalContextMenuCloseTrigger` changes
*/
import type { MenuButtonProps, MenuProps, PortalProps } from '@chakra-ui/react';
import { Portal, useEventListener } from '@chakra-ui/react';
import { InvMenu, InvMenuButton } from 'common/components/InvMenu/wrapper';
import { useGlobalMenuCloseTrigger } from 'common/hooks/useGlobalMenuCloseTrigger';
import { useCallback, useEffect, useRef, useState } from 'react';
export interface InvContextMenuProps<T extends HTMLElement = HTMLDivElement> {
renderMenu: () => JSX.Element | null;
children: (ref: React.MutableRefObject<T | null>) => JSX.Element | null;
menuProps?: Omit<MenuProps, 'children'> & { children?: React.ReactNode };
portalProps?: Omit<PortalProps, 'children'> & { children?: React.ReactNode };
menuButtonProps?: MenuButtonProps;
}
export const InvContextMenu = <T extends HTMLElement = HTMLElement>(
props: InvContextMenuProps<T>
) => {
const [isOpen, setIsOpen] = useState(false);
const [isRendered, setIsRendered] = useState(false);
const [isDeferredOpen, setIsDeferredOpen] = useState(false);
const [position, setPosition] = useState<[number, number]>([0, 0]);
const targetRef = useRef<T>(null);
useEffect(() => {
if (isOpen) {
setTimeout(() => {
setIsRendered(true);
setTimeout(() => {
setIsDeferredOpen(true);
});
});
} else {
setIsDeferredOpen(false);
const timeout = setTimeout(() => {
setIsRendered(isOpen);
}, 1000);
return () => clearTimeout(timeout);
}
}, [isOpen]);
const onClose = useCallback(() => {
setIsOpen(false);
setIsDeferredOpen(false);
setIsRendered(false);
}, []);
// This is the change from the original chakra-ui-contextmenu
// Close all menus when the globalContextMenuCloseTrigger changes
useGlobalMenuCloseTrigger(onClose);
useEventListener('contextmenu', (e) => {
if (
targetRef.current?.contains(e.target as HTMLElement) ||
e.target === targetRef.current
) {
e.preventDefault();
setIsOpen(true);
setPosition([e.pageX, e.pageY]);
} else {
setIsOpen(false);
}
});
const onCloseHandler = useCallback(() => {
props.menuProps?.onClose?.();
setIsOpen(false);
}, [props.menuProps]);
return (
<>
{props.children(targetRef)}
{isRendered && (
<Portal {...props.portalProps}>
<InvMenu
isLazy
isOpen={isDeferredOpen}
gutter={0}
onClose={onCloseHandler}
{...props.menuProps}
>
<InvMenuButton
aria-hidden={true}
w={1}
h={1}
position="absolute"
left={position[0]}
top={position[1]}
cursor="default"
bg="transparent"
size="sm"
_hover={{ bg: 'transparent' }}
{...props.menuButtonProps}
/>
{props.renderMenu()}
</InvMenu>
</Portal>
)}
</>
);
};