From 5a4bf68f5a801a8677b8dedca9ab3969932656be Mon Sep 17 00:00:00 2001 From: maxichrome Date: Fri, 27 May 2022 06:56:56 -0500 Subject: [PATCH 01/16] add baseline context menu --- packages/ui/package.json | 1 + packages/ui/src/ContextMenu.stories.tsx | 45 +++++++++++++++++ packages/ui/src/ContextMenu.tsx | 64 ++++++++++++++++++++++++ pnpm-lock.yaml | Bin 607496 -> 607566 bytes 4 files changed, 110 insertions(+) create mode 100644 packages/ui/src/ContextMenu.stories.tsx create mode 100644 packages/ui/src/ContextMenu.tsx diff --git a/packages/ui/package.json b/packages/ui/package.json index 00bfb9ba9..bdeae2568 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -20,6 +20,7 @@ "@headlessui/react": "^1.5.0", "@heroicons/react": "^1.0.6", "clsx": "^1.1.1", + "phosphor-react": "^1.4.1", "postcss": "^8.4.12", "react": "^18.0.0", "react-dom": "^18.0.0", diff --git a/packages/ui/src/ContextMenu.stories.tsx b/packages/ui/src/ContextMenu.stories.tsx new file mode 100644 index 000000000..a8cdeae60 --- /dev/null +++ b/packages/ui/src/ContextMenu.stories.tsx @@ -0,0 +1,45 @@ +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import { FileText, Plus, Trash } from 'phosphor-react'; +import React from 'react'; + +import { ContextMenu } from './ContextMenu'; + +export default { + title: 'UI/Context Menu', + component: ContextMenu, + argTypes: {}, + parameters: {}, + args: {} +} as ComponentMeta; + +const Template: ComponentStory = (args) => ; + +export const Default = Template.bind({}); +Default.args = { + sections: [ + { + items: [ + { + label: 'New Item', + icon: Plus, + onClick: () => alert('Item clicked') + } + ] + }, + { + items: [ + { + label: 'View Info', + icon: FileText, + onClick: () => alert('Info!!!') + }, + { + label: 'Delete', + icon: Trash, + danger: true, + onClick: () => alert('Delete item clicked') + } + ] + } + ] +}; diff --git a/packages/ui/src/ContextMenu.tsx b/packages/ui/src/ContextMenu.tsx new file mode 100644 index 000000000..bf24c71dc --- /dev/null +++ b/packages/ui/src/ContextMenu.tsx @@ -0,0 +1,64 @@ +import clsx from 'clsx'; +import type { Icon } from 'phosphor-react'; +import { Question } from 'phosphor-react'; +import React from 'react'; + +export interface ContextMenuItem { + label: string; + icon?: Icon; + danger?: boolean; + onClick: () => void; +} + +export interface ContextMenuProps { + sections?: { + heading?: string; + items: ContextMenuItem[]; + }[]; +} + +export const ContextMenu: React.FC = (props) => { + const { sections = [] } = props; + + return ( +
+ {sections.map((sec, i) => ( + <> + {i !== 0 &&
} + +
+ {sec.heading && ( + {sec.heading} + )} + +
    + {sec.items.map(({ icon: ItemIcon = Question, ...item }) => ( +
  • + +
  • + ))} +
+
+ + ))} +
+ ); +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7db76f35a7e63b9e58416cb3b67ae39fe5de6844..09468011f9859619df4670aaf6daff27c83fb8fb 100644 GIT binary patch delta 90 zcmeC^Qa#tDx`A15GN*#FF3*+*{2I=FmY^l({pE>{zibEqj{fx`#ya}AZ7w$ YW*}w(Vpbq#17da{=GeYZpL2pJ04q5gGynhq From 6454d2caefcd352dd7c07fa23daada1a1516b19f Mon Sep 17 00:00:00 2001 From: maxichrome Date: Fri, 27 May 2022 07:48:13 -0500 Subject: [PATCH 02/16] 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'; From 8f04cbe4e1e0ae2a8ff6d0200175a0b2b0f4564c Mon Sep 17 00:00:00 2001 From: maxichrome Date: Fri, 27 May 2022 21:10:47 -0500 Subject: [PATCH 03/16] add light-theme context menu --- packages/ui/.storybook/main.js | 3 ++- packages/ui/package.json | 1 + packages/ui/src/ContextMenu.stories.tsx | 6 +++--- packages/ui/src/ContextMenu.tsx | 10 ++++++---- pnpm-lock.yaml | Bin 607566 -> 608393 bytes 5 files changed, 12 insertions(+), 8 deletions(-) diff --git a/packages/ui/.storybook/main.js b/packages/ui/.storybook/main.js index 27b41382d..7ec0c6f1b 100644 --- a/packages/ui/.storybook/main.js +++ b/packages/ui/.storybook/main.js @@ -12,7 +12,8 @@ module.exports = { implementation: require('postcss') } } - } + }, + 'storybook-tailwind-dark-mode' ], webpackFinal: async (config) => { config.module.rules.push({ diff --git a/packages/ui/package.json b/packages/ui/package.json index bdeae2568..4c63a217c 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -46,6 +46,7 @@ "postcss-loader": "^7.0.0", "sass": "^1.50.0", "sass-loader": "^13.0.0", + "storybook-tailwind-dark-mode": "^1.0.12", "style-loader": "^3.3.1", "typescript": "^4.6.3" } diff --git a/packages/ui/src/ContextMenu.stories.tsx b/packages/ui/src/ContextMenu.stories.tsx index a8cdeae60..364ed372e 100644 --- a/packages/ui/src/ContextMenu.stories.tsx +++ b/packages/ui/src/ContextMenu.stories.tsx @@ -22,7 +22,7 @@ Default.args = { { label: 'New Item', icon: Plus, - onClick: () => alert('Item clicked') + onClick: () => {} } ] }, @@ -31,13 +31,13 @@ Default.args = { { label: 'View Info', icon: FileText, - onClick: () => alert('Info!!!') + onClick: () => {} }, { label: 'Delete', icon: Trash, danger: true, - onClick: () => alert('Delete item clicked') + onClick: () => {} } ] } diff --git a/packages/ui/src/ContextMenu.tsx b/packages/ui/src/ContextMenu.tsx index b3b78be36..fab3e9b13 100644 --- a/packages/ui/src/ContextMenu.tsx +++ b/packages/ui/src/ContextMenu.tsx @@ -25,14 +25,16 @@ export const ContextMenu: React.FC = (props) => {
{sections.map((sec, i) => ( <> - {i !== 0 &&
} + {i !== 0 && ( +
+ )}
{sec.heading && ( @@ -48,9 +50,9 @@ export const ContextMenu: React.FC = (props) => { textAlign: 'inherit' }} className={clsx( - 'flex flex-row gap-1.5 items-center cursor-default rounded-sm flex-1 px-1.5 py-1 focus:bg-gray-500 hover:bg-gray-500', + 'flex flex-row gap-1.5 items-center cursor-default rounded-sm flex-1 px-1.5 py-1 focus-visible:bg-gray-150 hover:bg-gray-150 dark:focus-visible:bg-gray-500 dark:hover:bg-gray-500', { - 'text-red-400': item.danger + 'text-red-600 dark:text-red-400': item.danger } )} onClick={item.onClick} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 09468011f9859619df4670aaf6daff27c83fb8fb..d26218877439a8f02ffeef58a45ad2f4e9fd4fa1 100644 GIT binary patch delta 500 zcmX@trP?`Bb;Bk-<@}<`r2PDB-IB!2obt@P6y21>qHNvV{FGEHg*Zb!13g2d>5N89 zqMP^Wsj@2JQw>$@xQ|h3I)@PxU-Nt8_V>n&K+FWh%-i1^vzS^1-q z9t#k&0x{e6rFrZnvlVfBLLcgfjoC~Np$7V1MU_#xg(k)+AqG{EiRD=(fsRqBktw-G zsaZK$29;r^0p^||>3#uG=B7?%21TZ2`JQIJCK*OW1;O6tmce=D#)Y}LxoH)d!KVHN zWyL-@rH<1NrZVYH<6+`ti!;+R(zBdi$j>ARV}DFy5}o#mksFCGF@3%SLb2cqCRv!| z0tY6A=|_c`RHm15Gu2N|C}bCz{-B6Sd3r%OJL~id(-=9Yzvbp&ZF0`>cs9S%Q)QhV-^?V|oNdycZNdn|OhC-MJ==su+e+3@*EGt& zt02WKqSQIlB&FCZ->ozr5VHa?+xCxn>^Ek&hwyT258>rBwFUsH CZzZ Date: Fri, 27 May 2022 21:27:14 -0500 Subject: [PATCH 04/16] add context menu helper --- .../src/components/file/FileItem.tsx | 173 ++++++++++-------- .../src/components/layout/MenuOverlay.tsx | 30 ++- 2 files changed, 128 insertions(+), 75 deletions(-) diff --git a/packages/interface/src/components/file/FileItem.tsx b/packages/interface/src/components/file/FileItem.tsx index cb4003832..f9fa64c52 100644 --- a/packages/interface/src/components/file/FileItem.tsx +++ b/packages/interface/src/components/file/FileItem.tsx @@ -1,8 +1,10 @@ import clsx from 'clsx'; +import { ArrowArcRight, Share, Trash } from 'phosphor-react'; import React, { MouseEventHandler } from 'react'; import icons from '../../assets/icons'; import { ReactComponent as Folder } from '../../assets/svg/folder.svg'; +import { WithContextMenu } from '../layout/MenuOverlay'; import { DefaultProps } from '../primitive/types'; interface Props extends DefaultProps { @@ -12,7 +14,6 @@ interface Props extends DefaultProps { folder?: boolean; selected?: boolean; onClick?: MouseEventHandler; - onContextMenu?: MouseEventHandler; } export default function FileItem(props: Props) { @@ -27,83 +28,107 @@ export default function FileItem(props: Props) { // ); // }; return ( -
-
- {props.folder ? ( -
-
- -
-
- ) : ( -
- - - - - - -
- {/* @ts-ignore */} - {props.iconName && icons[props.iconName] ? ( - (() => { - // @ts-ignore - let Icon = icons[props.iconName]; - return ( - - ); - })() - ) : ( - <> - )} - - {props.format} - -
-
- )} -
-
- +
+
- {props.fileName} - + {props.folder ? ( +
+
+ +
+
+ ) : ( +
+ + + + + + +
+ {/* @ts-ignore */} + {props.iconName && icons[props.iconName] ? ( + (() => { + // @ts-ignore + let Icon = icons[props.iconName]; + return ( + + ); + })() + ) : ( + <> + )} + + {props.format} + +
+
+ )} +
+
+ + {props.fileName} + +
-
+ ); } diff --git a/packages/interface/src/components/layout/MenuOverlay.tsx b/packages/interface/src/components/layout/MenuOverlay.tsx index 7d0fa4548..786f815e3 100644 --- a/packages/interface/src/components/layout/MenuOverlay.tsx +++ b/packages/interface/src/components/layout/MenuOverlay.tsx @@ -1,5 +1,6 @@ +import { ContextMenu, ContextMenuProps } from '@sd/ui'; import clsx from 'clsx'; -import React, { useEffect, useLayoutEffect } from 'react'; +import React, { MouseEventHandler, useLayoutEffect } from 'react'; type MenuElement = React.ReactElement<{ style?: React.CSSProperties; className?: string }>; type Position = { @@ -27,6 +28,33 @@ export const MenuContext = React.createContext React.useContext(MenuContext); +export const WithContextMenu: React.FC<{ + menu: ContextMenuProps['sections']; + children: React.ReactElement<{ onContextMenu: MouseEventHandler }>; +}> = (props) => { + const { menu: sections = [], children } = props; + + const menu = useMenu(); + + return ( + <> + {React.isValidElement(children) && + React.cloneElement(children, { + onContextMenu(e: React.MouseEvent) { + e.preventDefault(); + e.stopPropagation(); + + menu.showMenu( + , + { x: e.clientX, y: e.clientY }, + e.target as HTMLElement + ); + } + })} + + ); +}; + export const MenuOverlay: React.FC<{ children: React.ReactNode }> = (props) => { const { children } = props; From 8fafd7093726f8502a03c7bacdd5fd89217c980c Mon Sep 17 00:00:00 2001 From: maxichrome Date: Fri, 27 May 2022 21:27:49 -0500 Subject: [PATCH 05/16] remove manual menu listener & parameterize demo files --- .../src/components/device/Device.tsx | 82 ++----------------- packages/interface/src/screens/Overview.tsx | 20 ++++- 2 files changed, 24 insertions(+), 78 deletions(-) diff --git a/packages/interface/src/components/device/Device.tsx b/packages/interface/src/components/device/Device.tsx index 35ba3e961..7d1fce831 100644 --- a/packages/interface/src/components/device/Device.tsx +++ b/packages/interface/src/components/device/Device.tsx @@ -1,21 +1,9 @@ import { KeyIcon } from '@heroicons/react/outline'; import { CogIcon, LockClosedIcon } from '@heroicons/react/solid'; -import { Button, ContextMenu } from '@sd/ui'; -import { - ArrowArcRight, - Cloud, - Desktop, - DeviceMobileCamera, - DotsSixVertical, - Laptop, - Phone, - PhoneX, - PlusCircle, - Share, - Trash -} from 'phosphor-react'; +import { Button } from '@sd/ui'; +import { Cloud, Desktop, DeviceMobileCamera, DotsSixVertical, Laptop } from 'phosphor-react'; import React, { useState } from 'react'; -import LoadingIcons, { Rings } from 'react-loading-icons'; +import { Rings } from 'react-loading-icons'; import FileItem from '../file/FileItem'; import { useMenu } from '../layout/MenuOverlay'; @@ -25,9 +13,8 @@ export interface DeviceProps { name: string; size: string; type: 'laptop' | 'desktop' | 'phone' | 'server'; - locations: { name: string }[]; + locations: { name: string; folder?: boolean; format?: string; icon?: string }[]; runningJob?: { amount: number; task: string }; - removeThisSoon?: boolean; } export function Device(props: DeviceProps) { @@ -90,67 +77,14 @@ export function Device(props: DeviceProps) { {props.locations.map((location, key) => ( 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 + folder={location.folder} + format={location.format} + iconName={location.icon} /> ))} - {props.removeThisSoon && ( - <> - handleSelect('tsx')} - fileName="App.tsx" - format="tsx" - iconName="reactts" - /> - handleSelect('vite')} - fileName="vite.config.js" - format="vite" - iconName="vite" - /> - - )}
); diff --git a/packages/interface/src/screens/Overview.tsx b/packages/interface/src/screens/Overview.tsx index e12a0fe48..a3e5cea31 100644 --- a/packages/interface/src/screens/Overview.tsx +++ b/packages/interface/src/screens/Overview.tsx @@ -115,20 +115,32 @@ export const OverviewScreen: React.FC<{}> = (props) => { name={clientState?.client_name ?? ''} size="1.4TB" runningJob={{ amount: 65, task: 'Generating preview media' }} - locations={[{ name: 'Pictures' }, { name: 'Downloads' }, { name: 'Minecraft' }]} + locations={[ + { name: 'Pictures', folder: true }, + { name: 'Downloads', folder: true }, + { name: 'Minecraft', folder: true } + ]} type="laptop" />
From 473208a38719bb10f028635aa27545baa78f9448 Mon Sep 17 00:00:00 2001 From: maxichrome Date: Sat, 28 May 2022 00:24:45 -0500 Subject: [PATCH 06/16] improve context menu hitboxes --- packages/ui/src/ContextMenu.tsx | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/packages/ui/src/ContextMenu.tsx b/packages/ui/src/ContextMenu.tsx index fab3e9b13..ad0ab6ff7 100644 --- a/packages/ui/src/ContextMenu.tsx +++ b/packages/ui/src/ContextMenu.tsx @@ -25,7 +25,7 @@ export const ContextMenu: React.FC = (props) => {
= (props) => { {sec.heading} )} -
    +
      {sec.items.map(({ icon: ItemIcon = Question, ...item }) => (
    • ))} From e884b6d4b4f8aff07bdd690e22e1e44e462949a2 Mon Sep 17 00:00:00 2001 From: maxichrome Date: Sat, 28 May 2022 00:28:52 -0500 Subject: [PATCH 07/16] more gap stuff --- packages/ui/src/ContextMenu.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/ContextMenu.tsx b/packages/ui/src/ContextMenu.tsx index ad0ab6ff7..26d3253c9 100644 --- a/packages/ui/src/ContextMenu.tsx +++ b/packages/ui/src/ContextMenu.tsx @@ -41,7 +41,7 @@ export const ContextMenu: React.FC = (props) => { {sec.heading} )} -
        +
          {sec.items.map(({ icon: ItemIcon = Question, ...item }) => (
        • From f3733b486b369dd3e10a061208c427f043194425 Mon Sep 17 00:00:00 2001 From: maxichrome Date: Sat, 28 May 2022 05:35:54 -0500 Subject: [PATCH 12/16] add test context actions --- .../src/components/file/FileItem.tsx | 62 ++++++++++++------- 1 file changed, 39 insertions(+), 23 deletions(-) diff --git a/packages/interface/src/components/file/FileItem.tsx b/packages/interface/src/components/file/FileItem.tsx index 253bd4d3c..41ff9b9b9 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 { ArrowArcRight, Share, Trash } from 'phosphor-react'; +import { FilePlus, FileText, Share, Trash } from 'phosphor-react'; import React, { MouseEventHandler } from 'react'; import icons from '../../assets/icons'; @@ -30,30 +30,46 @@ export default function FileItem(props: Props) { return (
          From fe2cd9be6b0b600db2ccb8c61fdcdac350dad2c8 Mon Sep 17 00:00:00 2001 From: maxichrome Date: Sat, 28 May 2022 05:37:52 -0500 Subject: [PATCH 13/16] refactor context menu data structure --- .../src/components/layout/MenuOverlay.tsx | 4 +- packages/ui/src/ContextMenu.stories.tsx | 46 ++++++------- packages/ui/src/ContextMenu.tsx | 65 ++++++++++--------- 3 files changed, 57 insertions(+), 58 deletions(-) diff --git a/packages/interface/src/components/layout/MenuOverlay.tsx b/packages/interface/src/components/layout/MenuOverlay.tsx index 786f815e3..2d4970ffe 100644 --- a/packages/interface/src/components/layout/MenuOverlay.tsx +++ b/packages/interface/src/components/layout/MenuOverlay.tsx @@ -29,7 +29,7 @@ export const MenuContext = React.createContext React.useContext(MenuContext); export const WithContextMenu: React.FC<{ - menu: ContextMenuProps['sections']; + menu: ContextMenuProps['items']; children: React.ReactElement<{ onContextMenu: MouseEventHandler }>; }> = (props) => { const { menu: sections = [], children } = props; @@ -45,7 +45,7 @@ export const WithContextMenu: React.FC<{ e.stopPropagation(); menu.showMenu( - , + , { x: e.clientX, y: e.clientY }, e.target as HTMLElement ); diff --git a/packages/ui/src/ContextMenu.stories.tsx b/packages/ui/src/ContextMenu.stories.tsx index 364ed372e..9920e941b 100644 --- a/packages/ui/src/ContextMenu.stories.tsx +++ b/packages/ui/src/ContextMenu.stories.tsx @@ -16,30 +16,26 @@ const Template: ComponentStory = (args) => {} - } - ] - }, - { - items: [ - { - label: 'View Info', - icon: FileText, - onClick: () => {} - }, - { - label: 'Delete', - icon: Trash, - danger: true, - onClick: () => {} - } - ] - } + items: [ + [ + { + label: 'New Item', + icon: Plus, + onClick: () => {} + } + ], + [ + { + label: 'View Info', + icon: FileText, + onClick: () => {} + }, + { + label: 'Delete', + icon: Trash, + danger: true, + onClick: () => {} + } + ] ] }; diff --git a/packages/ui/src/ContextMenu.tsx b/packages/ui/src/ContextMenu.tsx index 0c261edf9..35774eee4 100644 --- a/packages/ui/src/ContextMenu.tsx +++ b/packages/ui/src/ContextMenu.tsx @@ -11,56 +11,59 @@ export interface ContextMenuItem { } export interface ContextMenuProps { - sections?: { - heading?: string; - items: ContextMenuItem[]; - }[]; + items?: (ContextMenuItem | string)[][]; className?: string; } export const ContextMenu: React.FC = (props) => { - const { sections = [], className, ...rest } = props; + const { items = [], className, ...rest } = props; return (
          - {sections.map((sec, i) => ( + {items.map((sec, i) => ( <> {i !== 0 && ( -
          +
          )}
          - {sec.heading && ( - {sec.heading} - )} -
            - {sec.items.map(({ icon: ItemIcon = Question, ...item }) => ( -
          • - -
          • - ))} + {sec.map((item) => { + if (typeof item === 'string') + return {item}; + + const { icon: ItemIcon = Question } = item; + + return ( +
          • + +
          • + ); + })}
          From 301f7ab87520db78dfa70814701c5deae06e2c6a Mon Sep 17 00:00:00 2001 From: maxichrome Date: Sat, 28 May 2022 07:21:22 -0500 Subject: [PATCH 14/16] disable system context menu --- packages/interface/src/App.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/interface/src/App.tsx b/packages/interface/src/App.tsx index 804ee4e96..5042d8c92 100644 --- a/packages/interface/src/App.tsx +++ b/packages/interface/src/App.tsx @@ -2,7 +2,7 @@ import '@fontsource/inter/variable.css'; import { BaseTransport, ClientProvider, setTransport } from '@sd/client'; // global window type extensions // only load at TS compile time -import type {} from '@sd/client/src/window'; +import type { } from '@sd/client/src/window'; import { Button } from '@sd/ui'; import clsx from 'clsx'; import React, { useContext, useEffect, useState } from 'react'; @@ -17,7 +17,6 @@ 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'; @@ -29,14 +28,15 @@ 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(); export const AppPropsContext = React.createContext(null); @@ -73,6 +73,12 @@ function AppLayout() { return (
          { + // TODO: allow this on some UI text at least + // disable default browser context menu + e.preventDefault(); + return false; + }} className={clsx( 'flex flex-row h-screen overflow-hidden text-gray-900 bg-white select-none dark:text-white dark:bg-gray-650', isWindowRounded && 'rounded-xl', From 56bb0895c33b736c86c232de5ebb0b546e2cbd36 Mon Sep 17 00:00:00 2001 From: Benjamin Akar Date: Sat, 28 May 2022 18:36:41 +0200 Subject: [PATCH 15/16] chore: import cleanups --- packages/interface/src/components/file/Inspector.tsx | 3 +-- packages/interface/src/components/layout/Modal.tsx | 2 -- packages/interface/src/screens/Overview.tsx | 8 ++------ 3 files changed, 3 insertions(+), 10 deletions(-) diff --git a/packages/interface/src/components/file/Inspector.tsx b/packages/interface/src/components/file/Inspector.tsx index 273173a88..42969ac1c 100644 --- a/packages/interface/src/components/file/Inspector.tsx +++ b/packages/interface/src/components/file/Inspector.tsx @@ -7,8 +7,7 @@ import { Heart, Link } from 'phosphor-react'; import React from 'react'; import { default as types } from '../../constants/file-types.json'; -import { Input, TextArea } from '../primitive'; -import { useExplorerState } from './FileList'; +import { TextArea } from '../primitive'; import FileThumb from './FileThumb'; interface MetaItemProps { diff --git a/packages/interface/src/components/layout/Modal.tsx b/packages/interface/src/components/layout/Modal.tsx index 524aaf0f5..3c2ee67a3 100644 --- a/packages/interface/src/components/layout/Modal.tsx +++ b/packages/interface/src/components/layout/Modal.tsx @@ -5,8 +5,6 @@ import clsx from 'clsx'; import React from 'react'; import { useNavigate } from 'react-router-dom'; -import { MacWindowControls } from '../file/Sidebar'; - export interface ModalProps { full?: boolean; children: React.ReactNode; diff --git a/packages/interface/src/screens/Overview.tsx b/packages/interface/src/screens/Overview.tsx index ac8bd08c2..d5374f18c 100644 --- a/packages/interface/src/screens/Overview.tsx +++ b/packages/interface/src/screens/Overview.tsx @@ -1,16 +1,12 @@ -import { CloudIcon } from '@heroicons/react/outline'; -import { CogIcon, MenuIcon, PlusIcon } from '@heroicons/react/solid'; +import { MenuIcon, PlusIcon } from '@heroicons/react/solid'; import { useBridgeQuery } from '@sd/client'; import { Button } from '@sd/ui'; import byteSize from 'byte-size'; -import { DotsSixVertical, Laptop, LineSegments, Plus } from 'phosphor-react'; -import React, { useState } from 'react'; +import React from 'react'; import { Device } from '../components/device/Device'; -import FileItem from '../components/file/FileItem'; import Dialog from '../components/layout/Dialog'; import { Input } from '../components/primitive'; -import { InputContainer } from '../components/primitive/InputContainer'; interface StatItemProps { name: string; From 5738fa982377cbaf5246e8c1ed059dec8bb8ad10 Mon Sep 17 00:00:00 2001 From: maxichrome Date: Sun, 29 May 2022 18:13:17 -0500 Subject: [PATCH 16/16] add context menu nesting --- packages/interface/src/App.tsx | 9 +- .../src/components/file/FileItem.tsx | 25 ++-- .../src/components/layout/MenuOverlay.tsx | 126 +----------------- packages/ui/package.json | 1 + packages/ui/src/ContextMenu.tsx | 110 +++++++++------ pnpm-lock.yaml | Bin 610259 -> 611090 bytes 6 files changed, 96 insertions(+), 175 deletions(-) diff --git a/packages/interface/src/App.tsx b/packages/interface/src/App.tsx index 7bdfb1cde..f4a46f668 100644 --- a/packages/interface/src/App.tsx +++ b/packages/interface/src/App.tsx @@ -16,7 +16,6 @@ import { } 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'; @@ -223,11 +222,9 @@ export default function App(props: AppProps) { {/* @ts-ignore */} - - - {props.useMemoryRouter ? : } - - + + {props.useMemoryRouter ? : } + diff --git a/packages/interface/src/components/file/FileItem.tsx b/packages/interface/src/components/file/FileItem.tsx index 41ff9b9b9..5acd177f6 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 { FilePlus, FileText, Share, Trash } from 'phosphor-react'; +import { ArrowLineRight, FilePlus, FileText, Plus, Share, Trash } from 'phosphor-react'; import React, { MouseEventHandler } from 'react'; import icons from '../../assets/icons'; @@ -43,6 +43,7 @@ export default function FileItem(props: Props) { icon: Share, onClick() { navigator.share?.({ + title: 'Spacedrive', text: 'Check out this cool app', url: 'https://spacedrive.com' }); @@ -51,15 +52,19 @@ export default function FileItem(props: Props) { ], [ { - label: 'Copy to Library...', - icon: FilePlus, - onClick() { - if (window?.location) { - window.location.href = 'https://www.youtube.com/watch?v=dQw4w9WgXcQ'; - } else { - alert('Please view tutorial: https://youtu.be/dQw4w9WgXcQ'); - } - } + label: 'More More More', + icon: Plus, + onClick() {}, + children: [ + [ + { + label: 'Super cool nested item', + onClick() { + alert("That's right, you clicked the cool item"); + } + } + ] + ] } ], [ diff --git a/packages/interface/src/components/layout/MenuOverlay.tsx b/packages/interface/src/components/layout/MenuOverlay.tsx index 2d4970ffe..8ba92b395 100644 --- a/packages/interface/src/components/layout/MenuOverlay.tsx +++ b/packages/interface/src/components/layout/MenuOverlay.tsx @@ -1,129 +1,17 @@ -import { ContextMenu, ContextMenuProps } from '@sd/ui'; -import clsx from 'clsx'; -import React, { MouseEventHandler, 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({ - showMenu() {}, - dismiss() {} -}); - -export const useMenu = () => React.useContext(MenuContext); +import { ContextMenu, ContextMenuProps, Root, Trigger } from '@sd/ui'; +import React, { ComponentProps } from 'react'; export const WithContextMenu: React.FC<{ menu: ContextMenuProps['items']; - children: React.ReactElement<{ onContextMenu: MouseEventHandler }>; + children: ComponentProps['children']; }> = (props) => { const { menu: sections = [], children } = props; - const menu = useMenu(); - return ( - <> - {React.isValidElement(children) && - React.cloneElement(children, { - onContextMenu(e: React.MouseEvent) { - e.preventDefault(); - e.stopPropagation(); + + {children} - menu.showMenu( - , - { x: e.clientX, y: e.clientY }, - e.target as HTMLElement - ); - } - })} - - ); -}; - -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/package.json b/packages/ui/package.json index 4c63a217c..18d0389c1 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -19,6 +19,7 @@ "dependencies": { "@headlessui/react": "^1.5.0", "@heroicons/react": "^1.0.6", + "@radix-ui/react-context-menu": "^0.1.6", "clsx": "^1.1.1", "phosphor-react": "^1.4.1", "postcss": "^8.4.12", diff --git a/packages/ui/src/ContextMenu.tsx b/packages/ui/src/ContextMenu.tsx index 35774eee4..25e5ecd7f 100644 --- a/packages/ui/src/ContextMenu.tsx +++ b/packages/ui/src/ContextMenu.tsx @@ -1,73 +1,103 @@ +import * as ContextMenuPrimitive from '@radix-ui/react-context-menu'; +import { Root, Trigger } from '@radix-ui/react-context-menu'; import clsx from 'clsx'; -import type { Icon } from 'phosphor-react'; +import { CaretRight, Icon } from 'phosphor-react'; import { Question } from 'phosphor-react'; -import React from 'react'; +import React, { ComponentPropsWithRef } from 'react'; export interface ContextMenuItem { label: string; icon?: Icon; danger?: boolean; onClick: () => void; + + children?: ContextMenuSection[]; } +export type ContextMenuSection = (ContextMenuItem | string)[]; + export interface ContextMenuProps { - items?: (ContextMenuItem | string)[][]; + items?: ContextMenuSection[]; className?: string; } export const ContextMenu: React.FC = (props) => { - const { items = [], className, ...rest } = props; + const { items: sections = [], className, ...rest } = props; return ( -
          - {items.map((sec, i) => ( + {sections.map((sec, i) => ( <> {i !== 0 && ( -
          + )} -
          -
            - {sec.map((item) => { - if (typeof item === 'string') - return {item}; - - const { icon: ItemIcon = Question } = item; - + + {sec.map((item) => { + if (typeof item === 'string') return ( -
          • - -
          • + + {item} + ); - })} -
          -
          + + const { icon: ItemIcon = Question } = item; + + let ItemComponent: + | typeof ContextMenuPrimitive.Item + | typeof ContextMenuPrimitive.TriggerItem = ContextMenuPrimitive.Item; + + if ((item.children?.length ?? 0) > 0) + ItemComponent = ((props) => ( + + + {props.children} + + + + + )) as typeof ContextMenuPrimitive.TriggerItem; + + return ( + +
          + {} + + + {item.label} + + + {(item.children?.length ?? 0) > 0 && ( + + )} +
          +
          + ); + })} + ))} -
          + ); }; + +export { Trigger, Root }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 672c47324cb485701eab75c4252cbbc0dcca0562..e3a1aed5212d6d845325e2dfb494dcdd3bec2141 100644 GIT binary patch delta 265 zcmcaSUv<(l)eR9k(_gJ%6ywv)P0cG+w^E2R&@p9_A&~yn8~vFl)?5>28^0j(>3-mdWRYm zCYm`qC8id-Xop97YFlK67-V@E1p4?Shgo=+8B};`=SKOMxVh!Io1`XNdKi>xSLUTB znI@+>Ii(d_rs{k9TI9M^xw#c2rlzJB76<0|_!U-8|5(JRI9=foqttYREsUMh_pW6$ znjXBBv1+>4PDab=7mhNjPQUHUXg+=Z21fbmAGR>Ew|`#62*gZ4%nZaVK+FonY(UJu K{qri02TK5yk!0}z delta 74 zcmbQVO!e}7)eR9klXpt-Ox_|XI{CK_|8#psCa%rjO?MCZ39xMR>RxKS%