From 6454d2caefcd352dd7c07fa23daada1a1516b19f Mon Sep 17 00:00:00 2001 From: maxichrome Date: Fri, 27 May 2022 07:48:13 -0500 Subject: [PATCH] implement WIP context menu on folder icon --- packages/interface/src/App.tsx | 12 ++- .../src/components/device/Device.tsx | 48 ++++++++- .../src/components/file/FileItem.tsx | 12 ++- .../src/components/layout/MenuOverlay.tsx | 101 ++++++++++++++++++ packages/ui/src/ContextMenu.tsx | 9 +- packages/ui/src/index.ts | 1 + 6 files changed, 172 insertions(+), 11 deletions(-) create mode 100644 packages/interface/src/components/layout/MenuOverlay.tsx diff --git a/packages/interface/src/App.tsx b/packages/interface/src/App.tsx index fc9e682de..804ee4e96 100644 --- a/packages/interface/src/App.tsx +++ b/packages/interface/src/App.tsx @@ -17,7 +17,9 @@ import { useLocation, useNavigate } from 'react-router-dom'; + import { Sidebar } from './components/file/Sidebar'; +import { MenuOverlay } from './components/layout/MenuOverlay'; import { Modal } from './components/layout/Modal'; import SlideUp from './components/transitions/SlideUp'; import { useCoreEvents } from './hooks/useCoreEvents'; @@ -27,12 +29,12 @@ import { ExplorerScreen } from './screens/Explorer'; import { OverviewScreen } from './screens/Overview'; import { RedirectPage } from './screens/Redirect'; import { SettingsScreen } from './screens/Settings'; +import { TagScreen } from './screens/Tag'; import ExperimentalSettings from './screens/settings/ExperimentalSettings'; import GeneralSettings from './screens/settings/GeneralSettings'; import LibrarySettings from './screens/settings/LibrarySettings'; import LocationSettings from './screens/settings/LocationSettings'; import SecuritySettings from './screens/settings/SecuritySettings'; -import { TagScreen } from './screens/Tag'; import './style.scss'; const queryClient = new QueryClient(); @@ -220,9 +222,11 @@ export default function App(props: AppProps) { {/* @ts-ignore */} - - {props.useMemoryRouter ? : } - + + + {props.useMemoryRouter ? : } + + diff --git a/packages/interface/src/components/device/Device.tsx b/packages/interface/src/components/device/Device.tsx index f972f2f90..35ba3e961 100644 --- a/packages/interface/src/components/device/Device.tsx +++ b/packages/interface/src/components/device/Device.tsx @@ -1,19 +1,24 @@ import { KeyIcon } from '@heroicons/react/outline'; import { CogIcon, LockClosedIcon } from '@heroicons/react/solid'; -import { Button } from '@sd/ui'; +import { Button, ContextMenu } from '@sd/ui'; import { + ArrowArcRight, Cloud, Desktop, DeviceMobileCamera, DotsSixVertical, Laptop, Phone, - PhoneX + PhoneX, + PlusCircle, + Share, + Trash } from 'phosphor-react'; import React, { useState } from 'react'; import LoadingIcons, { Rings } from 'react-loading-icons'; import FileItem from '../file/FileItem'; +import { useMenu } from '../layout/MenuOverlay'; import ProgressBar from '../primitive/ProgressBar'; export interface DeviceProps { @@ -27,11 +32,13 @@ export interface DeviceProps { export function Device(props: DeviceProps) { const [selectedFile, setSelectedFile] = useState(null); + const menu = useMenu(); function handleSelect(key: string) { if (selectedFile === key) setSelectedFile(null); else setSelectedFile(key); } + return (
@@ -85,6 +92,43 @@ export function Device(props: DeviceProps) { key={key} selected={selectedFile == location.name} onClick={() => handleSelect(location.name)} + onContextMenu={(e) => { + e.preventDefault(); + e.stopPropagation(); + + menu.showMenu( + , + { x: e.clientX, y: e.clientY }, + e.target as HTMLElement + ); + }} fileName={location.name} folder /> diff --git a/packages/interface/src/components/file/FileItem.tsx b/packages/interface/src/components/file/FileItem.tsx index 73851165b..cb4003832 100644 --- a/packages/interface/src/components/file/FileItem.tsx +++ b/packages/interface/src/components/file/FileItem.tsx @@ -1,5 +1,5 @@ import clsx from 'clsx'; -import React from 'react'; +import React, { MouseEventHandler } from 'react'; import icons from '../../assets/icons'; import { ReactComponent as Folder } from '../../assets/svg/folder.svg'; @@ -11,7 +11,8 @@ interface Props extends DefaultProps { format?: string; folder?: boolean; selected?: boolean; - onClick?: () => void; + onClick?: MouseEventHandler; + onContextMenu?: MouseEventHandler; } export default function FileItem(props: Props) { @@ -26,7 +27,12 @@ export default function FileItem(props: Props) { // ); // }; return ( -
+
; +type Position = { + x: number; + y: number; +}; + +export interface MenuContextData { + currentMenu?: { + clickPosition: Position; + clickedElement: HTMLElement; + menuElement: MenuElement; + }; +} + +export interface MenuContextActions { + showMenu: (menu: MenuElement, clickPosition: Position, clickedElement: HTMLElement) => void; + dismiss: () => void; +} + +export const MenuContext = React.createContext({ + showMenu() {}, + dismiss() {} +}); + +export const useMenu = () => React.useContext(MenuContext); + +export const MenuOverlay: React.FC<{ children: React.ReactNode }> = (props) => { + const { children } = props; + + const [menuState, setMenuState] = React.useState({}); + + const overlay = React.useRef(null); + + const showMenu: MenuContextActions['showMenu'] = React.useCallback( + (menu, clickPosition, clickedElement) => { + setMenuState({ + currentMenu: { + menuElement: menu, + clickPosition, + clickedElement + } + }); + }, + [setMenuState] + ); + + const dismiss: MenuContextActions['dismiss'] = React.useCallback(() => { + setMenuState({}); + }, [setMenuState]); + + useLayoutEffect(() => { + if (menuState.currentMenu) overlay.current?.focus(); + else overlay.current?.blur(); + }, [menuState]); + + return ( + + {children} +
{ + if (e.key === 'Escape') { + e.stopPropagation(); + + setMenuState({}); + } + }} + onClick={() => { + setMenuState({}); + }} + onContextMenu={(e) => { + e.preventDefault(); + }} + > + {menuState.currentMenu && React.isValidElement(menuState.currentMenu?.menuElement) && ( +
+ {React.cloneElement(menuState.currentMenu!.menuElement, { + className: 'absolute', + style: { + left: menuState.currentMenu?.clickPosition.x + 3, + top: menuState.currentMenu?.clickPosition.y + 3 + } + })} +
+ )} +
+
+ ); +}; diff --git a/packages/ui/src/ContextMenu.tsx b/packages/ui/src/ContextMenu.tsx index bf24c71dc..b3b78be36 100644 --- a/packages/ui/src/ContextMenu.tsx +++ b/packages/ui/src/ContextMenu.tsx @@ -15,15 +15,20 @@ export interface ContextMenuProps { heading?: string; items: ContextMenuItem[]; }[]; + className?: string; } export const ContextMenu: React.FC = (props) => { - const { sections = [] } = props; + const { sections = [], className, ...rest } = props; return (
{sections.map((sec, i) => ( <> diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index 1992f2cee..8f7f97726 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -1,2 +1,3 @@ export * from './Button'; export * from './Dropdown'; +export * from './ContextMenu';