mirror of
https://github.com/spacedriveapp/spacedrive.git
synced 2026-04-29 19:03:03 -04:00
implement WIP context menu on folder icon
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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
|
||||
/>
|
||||
|
||||
@@ -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',
|
||||
|
||||
101
packages/interface/src/components/layout/MenuOverlay.tsx
Normal file
101
packages/interface/src/components/layout/MenuOverlay.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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) => (
|
||||
<>
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './Button';
|
||||
export * from './Dropdown';
|
||||
export * from './ContextMenu';
|
||||
|
||||
Reference in New Issue
Block a user