From 69193461eafdc8f312a77b42155bec2cb7db6b9b Mon Sep 17 00:00:00 2001 From: Sebastien-Ahkrin Date: Fri, 4 Nov 2022 14:35:06 +0100 Subject: [PATCH 1/4] feat: add context menu dropdown --- src/components/menu/DropdownMenu.tsx | 67 ++++++++++++++++-- .../menu/useContextMenuPlacement.ts | 70 +++++++++++++++++++ stories/components/dropdown.stories.tsx | 31 +++++++- 3 files changed, 162 insertions(+), 6 deletions(-) create mode 100644 src/components/menu/useContextMenuPlacement.ts diff --git a/src/components/menu/DropdownMenu.tsx b/src/components/menu/DropdownMenu.tsx index 516f5879..3dbdda4a 100644 --- a/src/components/menu/DropdownMenu.tsx +++ b/src/components/menu/DropdownMenu.tsx @@ -1,6 +1,7 @@ +import { assertNotNull } from '@/utils/assert'; import { Menu } from '@headlessui/react'; import type { Placement } from '@popperjs/core'; -import { ReactNode, useRef, useState } from 'react'; +import { ReactNode, Ref, useRef, useState } from 'react'; import { usePopper } from 'react-popper'; import { useOnClickOutside } from '../hooks/useOnClickOutside'; import { useOnOff } from '../hooks/useOnOff'; @@ -8,6 +9,7 @@ import { useOnOff } from '../hooks/useOnOff'; import { Portal } from '../Portal'; import { MenuItems, MenuOption, MenuOptions } from './MenuItems'; +import { useContextMenuPlacement } from './useContextMenuPlacement'; interface DropdownMenuBaseProps { /** @@ -24,18 +26,73 @@ interface DropdownMenuClickProps extends DropdownMenuBaseProps { * Node to be inside the Button */ children: ReactNode; + trigger: 'click'; +} + +interface DropdownMenuContextProps extends DropdownMenuBaseProps { + trigger: 'contextMenu'; + children: ReactNode; } export type DropdownMenuProps = - // | DropdownMenuContextProps - DropdownMenuClickProps; + | DropdownMenuContextProps + | DropdownMenuClickProps; export default function DropdownMenu(props: DropdownMenuProps) { - return {props.children}; + const { trigger, ...otherProps } = props; + + if (trigger === 'contextMenu') { + return ; + } + + return ( + {props.children} + ); +} + +function DropdownContextMenu(props: Omit, 'trigger'>) { + const { children, ...otherProps } = props; + + const { + isPopperElementOpen, + closePopperElement, + handleContextMenu, + setPopperElement, + styles, + attributes, + } = useContextMenuPlacement(); + + const ref = useRef(null); + useOnClickOutside(ref, closePopperElement); + + console.log(isPopperElementOpen); + return ( +
+ {isPopperElementOpen && ( +
+
+ + + +
+
+ )} + + {children} +
+ ); } function DropdownClickMenu( - props: DropdownMenuProps & { children: ReactNode }, + props: Omit, 'trigger'> & { children: ReactNode }, ) { const { placement = 'bottom-start', ...otherProps } = props; diff --git a/src/components/menu/useContextMenuPlacement.ts b/src/components/menu/useContextMenuPlacement.ts new file mode 100644 index 00000000..5a81bba8 --- /dev/null +++ b/src/components/menu/useContextMenuPlacement.ts @@ -0,0 +1,70 @@ +import React, { useCallback, useMemo, useState } from 'react'; +import { usePopper } from 'react-popper'; + +interface RefState { + clientX: number; + clientY: number; + pageX: number; + pageY: number; +} + +export function useContextMenuPlacement() { + const [positionState, setPositionState] = useState(null); + + const handleContextMenu = useCallback( + (event: React.MouseEvent) => { + const { clientX, clientY, pageX, pageY } = event; + event.preventDefault(); + + setPositionState({ + clientX, + clientY, + pageX, + pageY, + }); + }, + [], + ); + + const boudingClientRect = useMemo(() => { + return { + top: positionState?.clientY || 0, + left: positionState?.clientX || 0, + x: positionState?.pageX || 0, + y: positionState?.pageY || 0, + + bottom: 0, + right: 0, + height: 0, + width: 0, + toJSON: () => '', + }; + }, [positionState]); + + const virtualElement = useMemo(() => { + return { + getBoundingClientRect: () => boudingClientRect, + }; + }, [positionState]); + + const [popperElement, setPopperElement] = useState( + null, + ); + + const { styles, attributes, state } = usePopper( + virtualElement, + popperElement, + ); + + return { + setPopperElement, + styles, + attributes, + popperState: state, + isPopperElementOpen: positionState !== null, + handleContextMenu, + closePopperElement: () => { + setPositionState(null); + }, + }; +} diff --git a/stories/components/dropdown.stories.tsx b/stories/components/dropdown.stories.tsx index b61d03f5..6b730f31 100644 --- a/stories/components/dropdown.stories.tsx +++ b/stories/components/dropdown.stories.tsx @@ -52,12 +52,41 @@ export function Dropdown() { }, []); return ( - {}} options={options}> + {}} options={options}> Default workspace ); } +export function ContextDropdown() { + const options = useMemo>(() => { + return [ + { label: 'Default workspace', type: 'option' }, + { label: 'Exercise', type: 'option', icon: }, + { type: 'divider' }, + { label: 'Test', type: 'option', disabled: true }, + { label: 'Test 2', type: 'option' }, + ]; + }, []); + + return ( + {}} options={options}> +
+

Hello, World!

+
+
+ ); +} + export function WithIcon() { const options = useMemo>(() => { return [ From b753d2565f57b0c0ee2db3ad25041e690feb5c77 Mon Sep 17 00:00:00 2001 From: Sebastien-Ahkrin Date: Fri, 4 Nov 2022 14:35:50 +0100 Subject: [PATCH 2/4] refactor: reword interfaces --- src/components/menu/useContextMenuPlacement.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/menu/useContextMenuPlacement.ts b/src/components/menu/useContextMenuPlacement.ts index 5a81bba8..16d07a81 100644 --- a/src/components/menu/useContextMenuPlacement.ts +++ b/src/components/menu/useContextMenuPlacement.ts @@ -1,7 +1,7 @@ import React, { useCallback, useMemo, useState } from 'react'; import { usePopper } from 'react-popper'; -interface RefState { +interface PositionState { clientX: number; clientY: number; pageX: number; @@ -9,7 +9,9 @@ interface RefState { } export function useContextMenuPlacement() { - const [positionState, setPositionState] = useState(null); + const [positionState, setPositionState] = useState( + null, + ); const handleContextMenu = useCallback( (event: React.MouseEvent) => { From 1542f260154412ba981b71fa3df2c47534a09884 Mon Sep 17 00:00:00 2001 From: Sebastien-Ahkrin Date: Fri, 4 Nov 2022 16:53:30 +0100 Subject: [PATCH 3/4] refactor: change the placement of DropdownMenu --- src/components/menu/DropdownMenu.tsx | 6 ++---- src/components/menu/useContextMenuPlacement.ts | 8 ++++++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/components/menu/DropdownMenu.tsx b/src/components/menu/DropdownMenu.tsx index 3dbdda4a..e3bba232 100644 --- a/src/components/menu/DropdownMenu.tsx +++ b/src/components/menu/DropdownMenu.tsx @@ -1,7 +1,6 @@ -import { assertNotNull } from '@/utils/assert'; import { Menu } from '@headlessui/react'; import type { Placement } from '@popperjs/core'; -import { ReactNode, Ref, useRef, useState } from 'react'; +import { ReactNode, useRef, useState } from 'react'; import { usePopper } from 'react-popper'; import { useOnClickOutside } from '../hooks/useOnClickOutside'; import { useOnOff } from '../hooks/useOnOff'; @@ -60,12 +59,11 @@ function DropdownContextMenu(props: Omit, 'trigger'>) { setPopperElement, styles, attributes, - } = useContextMenuPlacement(); + } = useContextMenuPlacement(otherProps.placement || 'right-start'); const ref = useRef(null); useOnClickOutside(ref, closePopperElement); - console.log(isPopperElementOpen); return (
( null, ); @@ -47,7 +48,7 @@ export function useContextMenuPlacement() { return { getBoundingClientRect: () => boudingClientRect, }; - }, [positionState]); + }, [positionState, boudingClientRect]); const [popperElement, setPopperElement] = useState( null, @@ -56,6 +57,9 @@ export function useContextMenuPlacement() { const { styles, attributes, state } = usePopper( virtualElement, popperElement, + { + placement, + }, ); return { From f5b0ef29d9722404ab12e095037c04181357ff1b Mon Sep 17 00:00:00 2001 From: Sebastien-Ahkrin Date: Tue, 8 Nov 2022 10:45:51 +0100 Subject: [PATCH 4/4] refactor: fix eslint error and change to styled & add tabIndex --- src/components/menu/DropdownMenu.tsx | 13 ++++++----- .../menu/useContextMenuPlacement.ts | 2 +- stories/components/dropdown.stories.tsx | 22 +++++++++---------- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/src/components/menu/DropdownMenu.tsx b/src/components/menu/DropdownMenu.tsx index e3bba232..b03c72bd 100644 --- a/src/components/menu/DropdownMenu.tsx +++ b/src/components/menu/DropdownMenu.tsx @@ -1,3 +1,4 @@ +import styled from '@emotion/styled'; import { Menu } from '@headlessui/react'; import type { Placement } from '@popperjs/core'; import { ReactNode, useRef, useState } from 'react'; @@ -49,6 +50,10 @@ export default function DropdownMenu(props: DropdownMenuProps) { ); } +const HandleMenuContextDiv = styled.div` + display: contents; +`; + function DropdownContextMenu(props: Omit, 'trigger'>) { const { children, ...otherProps } = props; @@ -65,11 +70,7 @@ function DropdownContextMenu(props: Omit, 'trigger'>) { useOnClickOutside(ref, closePopperElement); return ( -
+ {isPopperElementOpen && (
(props: Omit, 'trigger'>) { )} {children} -
+ ); } diff --git a/src/components/menu/useContextMenuPlacement.ts b/src/components/menu/useContextMenuPlacement.ts index f788a389..9562def6 100644 --- a/src/components/menu/useContextMenuPlacement.ts +++ b/src/components/menu/useContextMenuPlacement.ts @@ -48,7 +48,7 @@ export function useContextMenuPlacement(placement: Placement) { return { getBoundingClientRect: () => boudingClientRect, }; - }, [positionState, boudingClientRect]); + }, [boudingClientRect]); const [popperElement, setPopperElement] = useState( null, diff --git a/stories/components/dropdown.stories.tsx b/stories/components/dropdown.stories.tsx index 6b730f31..6f8e4a3d 100644 --- a/stories/components/dropdown.stories.tsx +++ b/stories/components/dropdown.stories.tsx @@ -58,6 +58,15 @@ export function Dropdown() { ); } +const DivContextDropdown = styled.div` + height: 500px; + width: 500px; + border: 1px solid black; + display: flex; + justify-content: center; + align-items: center; +`; + export function ContextDropdown() { const options = useMemo>(() => { return [ @@ -71,18 +80,9 @@ export function ContextDropdown() { return ( {}} options={options}> -
+

Hello, World!

-
+
); }