implement WIP context menu on folder icon

This commit is contained in:
maxichrome
2022-05-27 07:48:13 -05:00
parent 5a4bf68f5a
commit 6454d2caef
6 changed files with 172 additions and 11 deletions

View File

@@ -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 */}
<QueryClientProvider client={queryClient} contextSharing={false}>
<AppPropsContext.Provider value={Object.assign({ isFocused: true }, props)}>
<ClientProvider>
{props.useMemoryRouter ? <MemoryRouterContainer /> : <BrowserRouterContainer />}
</ClientProvider>
<MenuOverlay>
<ClientProvider>
{props.useMemoryRouter ? <MemoryRouterContainer /> : <BrowserRouterContainer />}
</ClientProvider>
</MenuOverlay>
</AppPropsContext.Provider>
</QueryClientProvider>
</ErrorBoundary>

View File

@@ -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 | string>(null);
const menu = useMenu();
function handleSelect(key: string) {
if (selectedFile === key) setSelectedFile(null);
else setSelectedFile(key);
}
return (
<div className="w-full bg-gray-600 border rounded-md border-gray-550 ">
<div className="flex flex-row items-center px-4 pt-2 pb-2">
@@ -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(
<ContextMenu
sections={[
{
items: [
{
label: 'Share',
icon: Share,
onClick() {}
}
]
},
{
items: [
{
label: 'Move to Library...',
icon: ArrowArcRight,
onClick() {}
},
{
label: 'Delete',
icon: Trash,
danger: true,
onClick() {}
}
]
}
]}
/>,
{ x: e.clientX, y: e.clientY },
e.target as HTMLElement
);
}}
fileName={location.name}
folder
/>

View File

@@ -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<HTMLDivElement>;
onContextMenu?: MouseEventHandler<HTMLDivElement>;
}
export default function FileItem(props: Props) {
@@ -26,7 +27,12 @@ export default function FileItem(props: Props) {
// );
// };
return (
<div onClick={props.onClick} className="inline-block w-[100px] mb-3" draggable>
<div
onClick={props.onClick}
onContextMenu={props.onContextMenu}
className="inline-block w-[100px] mb-3"
draggable
>
<div
className={clsx(
'border-2 border-transparent rounded-lg text-center w-[100px] h-[100px] mb-1',

View File

@@ -0,0 +1,101 @@
import clsx from 'clsx';
import React, { useEffect, useLayoutEffect } from 'react';
type MenuElement = React.ReactElement<{ style?: React.CSSProperties; className?: string }>;
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<MenuContextData & MenuContextActions>({
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<MenuContextData>({});
const overlay = React.useRef<HTMLDivElement>(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 (
<MenuContext.Provider
value={{
showMenu,
dismiss,
currentMenu: menuState.currentMenu
}}
>
{children}
<div
className={clsx('absolute top-0 left-0 w-screen h-screen pointer-events-none', {
'pointer-events-auto': menuState.currentMenu
})}
ref={overlay}
onKeyDownCapture={(e) => {
if (e.key === 'Escape') {
e.stopPropagation();
setMenuState({});
}
}}
onClick={() => {
setMenuState({});
}}
onContextMenu={(e) => {
e.preventDefault();
}}
>
{menuState.currentMenu && React.isValidElement(menuState.currentMenu?.menuElement) && (
<div className="relative">
{React.cloneElement(menuState.currentMenu!.menuElement, {
className: 'absolute',
style: {
left: menuState.currentMenu?.clickPosition.x + 3,
top: menuState.currentMenu?.clickPosition.y + 3
}
})}
</div>
)}
</div>
</MenuContext.Provider>
);
};

View File

@@ -15,15 +15,20 @@ export interface ContextMenuProps {
heading?: string;
items: ContextMenuItem[];
}[];
className?: string;
}
export const ContextMenu: React.FC<ContextMenuProps> = (props) => {
const { sections = [] } = props;
const { sections = [], className, ...rest } = props;
return (
<div
role="menu"
className="flex flex-col select-none cursor-default bg-gray-600 text-gray-100 text-left text-sm font-semibold rounded p-1.5 gap-1.5 border-2 border-gray-500"
className={clsx(
'flex flex-col select-none cursor-default bg-gray-600 text-gray-100 text-left text-sm font-semibold rounded p-1.5 gap-1.5 border-2 border-gray-500',
className
)}
{...rest}
>
{sections.map((sec, i) => (
<>

View File

@@ -1,2 +1,3 @@
export * from './Button';
export * from './Dropdown';
export * from './ContextMenu';