From f899514e5ad734eca6e291af8c5e8510a104d144 Mon Sep 17 00:00:00 2001 From: "Visal .In" Date: Sat, 26 Aug 2023 20:26:10 +0700 Subject: [PATCH 1/8] add option list and favor it over context menu --- .../components/AvoidOffscreen/index.tsx | 67 +++++++++++++++++++ .../components/DropContainer/index.tsx | 6 ++ .../DropContainer/styles.module.scss | 6 ++ .../components/OptionList/OptionListGroup.tsx | 3 + .../components/OptionList/OptionListItem.tsx | 47 +++++++++++++ src/renderer/components/OptionList/index.tsx | 5 ++ .../components/OptionList/styles.module.scss | 57 ++++++++++++++++ src/stories/AvoidOffscreen.stories.tsx | 28 ++++++++ src/stories/OptionList.stories.tsx | 58 ++++++++++++++++ 9 files changed, 277 insertions(+) create mode 100644 src/renderer/components/AvoidOffscreen/index.tsx create mode 100644 src/renderer/components/DropContainer/index.tsx create mode 100644 src/renderer/components/DropContainer/styles.module.scss create mode 100644 src/renderer/components/OptionList/OptionListGroup.tsx create mode 100644 src/renderer/components/OptionList/OptionListItem.tsx create mode 100644 src/renderer/components/OptionList/index.tsx create mode 100644 src/renderer/components/OptionList/styles.module.scss create mode 100644 src/stories/AvoidOffscreen.stories.tsx create mode 100644 src/stories/OptionList.stories.tsx diff --git a/src/renderer/components/AvoidOffscreen/index.tsx b/src/renderer/components/AvoidOffscreen/index.tsx new file mode 100644 index 0000000..5c22d4e --- /dev/null +++ b/src/renderer/components/AvoidOffscreen/index.tsx @@ -0,0 +1,67 @@ +import { + PropsWithChildren, + useCallback, + useEffect, + useRef, + useState, +} from 'react'; + +export default function AvoidOffscreen({ children }: PropsWithChildren) { + const ref = useRef(null); + + const [offsetTop, setOffsetTop] = useState(0); + const [offsetLeft, setOffsetLeft] = useState(0); + const [flip, setFlip] = useState(false); + + const computePosition = useCallback(() => { + if (ref.current) { + const bound = ref.current.getBoundingClientRect(); + const viewportWidth = + window.innerWidth || document.documentElement.clientWidth; + const viewportHeight = + window.innerHeight || document.documentElement.clientHeight; + + setOffsetTop(Math.min(0, viewportHeight - bound.bottom)); + + if (!flip) { + setFlip(bound.right > viewportWidth); + if ( + ref.current.parentElement && + ref.current.parentElement.style.position === 'fixed' + ) { + setOffsetLeft(bound.width); + } + } + } + }, [ref, setOffsetTop, setFlip, offsetLeft, flip]); + + useEffect(() => { + if (ref.current) { + const resizeObserver = new ResizeObserver(() => { + computePosition(); + }); + + resizeObserver.observe(ref.current); + return () => resizeObserver.disconnect(); + } + }, [ref, computePosition]); + + console.log(flip, offsetTop); + + return ( +
+ {children} +
+ ); +} diff --git a/src/renderer/components/DropContainer/index.tsx b/src/renderer/components/DropContainer/index.tsx new file mode 100644 index 0000000..476ac00 --- /dev/null +++ b/src/renderer/components/DropContainer/index.tsx @@ -0,0 +1,6 @@ +import { PropsWithChildren } from 'react'; +import styles from './styles.module.scss'; + +export default function DropContainer({ children }: PropsWithChildren) { + return
{children}
; +} diff --git a/src/renderer/components/DropContainer/styles.module.scss b/src/renderer/components/DropContainer/styles.module.scss new file mode 100644 index 0000000..6493e3d --- /dev/null +++ b/src/renderer/components/DropContainer/styles.module.scss @@ -0,0 +1,6 @@ +.container { + background: var(--color-surface); + border-radius: 8px; + padding: 3px 0; + border: 1px solid var(--color-border); +} diff --git a/src/renderer/components/OptionList/OptionListGroup.tsx b/src/renderer/components/OptionList/OptionListGroup.tsx new file mode 100644 index 0000000..3a3d262 --- /dev/null +++ b/src/renderer/components/OptionList/OptionListGroup.tsx @@ -0,0 +1,3 @@ +export default function OptionListGroup() { + return
; +} diff --git a/src/renderer/components/OptionList/OptionListItem.tsx b/src/renderer/components/OptionList/OptionListItem.tsx new file mode 100644 index 0000000..267170e --- /dev/null +++ b/src/renderer/components/OptionList/OptionListItem.tsx @@ -0,0 +1,47 @@ +import { PropsWithChildren, ReactElement } from 'react'; +import styles from './styles.module.scss'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faChevronRight } from '@fortawesome/free-solid-svg-icons'; +import AvoidOffscreen from '../AvoidOffscreen'; + +interface OptionListItemProps { + label: string; + icon?: ReactElement; + selected?: boolean; + right?: string; + labelWidth?: number; + disabled?: boolean; + destructive: boolean; + separator: boolean; + onClick?: (e: React.MouseEvent) => void; +} + +export default function OptionListItem({ + label, + icon, + right, + labelWidth, + children, + onClick, +}: PropsWithChildren) { + return ( +
+
+
{icon}
+
+ {label} +
+
{right}
+
+ {children && } +
+
+ + {children && ( +
+ {children} +
+ )} +
+ ); +} diff --git a/src/renderer/components/OptionList/index.tsx b/src/renderer/components/OptionList/index.tsx new file mode 100644 index 0000000..4552b78 --- /dev/null +++ b/src/renderer/components/OptionList/index.tsx @@ -0,0 +1,5 @@ +import { PropsWithChildren } from 'react'; + +export default function OptionList({ children }: PropsWithChildren) { + return
{children}
; +} diff --git a/src/renderer/components/OptionList/styles.module.scss b/src/renderer/components/OptionList/styles.module.scss new file mode 100644 index 0000000..0e1ca8b --- /dev/null +++ b/src/renderer/components/OptionList/styles.module.scss @@ -0,0 +1,57 @@ +.outerItem { + margin: 0 5px; + position: relative; + + &:hover { + > .item { + background: var(--color-surface-hover); + } + + > .child { + visibility: visible; + } + } + + .child { + visibility: hidden; + display: content; + } +} + +.item { + display: flex; + cursor: pointer; + border-radius: 4px; + + .label { + padding: 7px 5px; + flex-grow: 1; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .icon { + align-items: center; + justify-content: center; + display: flex; + margin-left: 10px; + width: 15px; + flex-shrink: 0; + } + + .right { + padding: 7px 5px; + margin: 0 5px; + flex-shrink: 0; + } + + .arrow { + width: 20px; + margin: 0 5px; + flex-shrink: 0; + align-items: center; + justify-content: center; + display: flex; + } +} diff --git a/src/stories/AvoidOffscreen.stories.tsx b/src/stories/AvoidOffscreen.stories.tsx new file mode 100644 index 0000000..2f8ddbc --- /dev/null +++ b/src/stories/AvoidOffscreen.stories.tsx @@ -0,0 +1,28 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import StorybookContainer from './StoryContainer'; +import AvoidOffscreen from 'renderer/components/AvoidOffscreen'; + +function StoryPage() { + return ( + +
+ +
+
+
+
+ ); +} + +const meta = { + title: 'Components/AvoidOffScreen', + component: StoryPage, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const AvoidOffScreen: Story = { + name: 'Offscreen component will auto correct', + args: {}, +}; diff --git a/src/stories/OptionList.stories.tsx b/src/stories/OptionList.stories.tsx new file mode 100644 index 0000000..25a7565 --- /dev/null +++ b/src/stories/OptionList.stories.tsx @@ -0,0 +1,58 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import StorybookContainer from './StoryContainer'; +import OptionList from 'renderer/components/OptionList'; +import OptionListItem from 'renderer/components/OptionList/OptionListItem'; +import DropContainer from 'renderer/components/DropContainer'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faCopy } from '@fortawesome/free-solid-svg-icons'; + +function StoryPage() { + return ( + +
+ + + + } + /> + + + + + + + + + + + + + + + + + + + + +
+
+ ); +} + +const meta = { + title: 'Components/OptionList', + component: StoryPage, + tags: ['autodocs'], +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const OptionListStory: Story = { + args: {}, +}; From 69fad7a791f08bab95e8f595470347fc02d90fac Mon Sep 17 00:00:00 2001 From: "Visal .In" Date: Sat, 26 Aug 2023 21:09:08 +0700 Subject: [PATCH 2/8] replace context with option list --- .../components/AvoidOffscreen/index.tsx | 11 +- src/renderer/components/ContextMenu/index.tsx | 101 +++++++++--------- .../components/ContextMenu/styles.module.scss | 92 ---------------- .../components/OptionList/OptionListItem.tsx | 29 ++++- .../components/OptionList/styles.module.scss | 24 +++++ 5 files changed, 105 insertions(+), 152 deletions(-) diff --git a/src/renderer/components/AvoidOffscreen/index.tsx b/src/renderer/components/AvoidOffscreen/index.tsx index 5c22d4e..6ae4b3d 100644 --- a/src/renderer/components/AvoidOffscreen/index.tsx +++ b/src/renderer/components/AvoidOffscreen/index.tsx @@ -8,7 +8,7 @@ import { export default function AvoidOffscreen({ children }: PropsWithChildren) { const ref = useRef(null); - + const [computed, setComputed] = useState(false); const [offsetTop, setOffsetTop] = useState(0); const [offsetLeft, setOffsetLeft] = useState(0); const [flip, setFlip] = useState(false); @@ -23,6 +23,8 @@ export default function AvoidOffscreen({ children }: PropsWithChildren) { setOffsetTop(Math.min(0, viewportHeight - bound.bottom)); + if (bound.width === 0) return false; + if (!flip) { setFlip(bound.right > viewportWidth); if ( @@ -32,8 +34,10 @@ export default function AvoidOffscreen({ children }: PropsWithChildren) { setOffsetLeft(bound.width); } } + + setComputed(true); } - }, [ref, setOffsetTop, setFlip, offsetLeft, flip]); + }, [ref, setOffsetTop, setFlip, offsetLeft, flip, setComputed]); useEffect(() => { if (ref.current) { @@ -46,12 +50,11 @@ export default function AvoidOffscreen({ children }: PropsWithChildren) { } }, [ref, computePosition]); - console.log(flip, offsetTop); - return (
void; minWidth?: number; }>) { - const menuRef = useRef(null); - const [menuWidth, setMenuWidth] = useState(0); - const [menuHeight, setMenuHeight] = useState(0); - useEffect(() => { const onDocumentClicked = () => { onClose(); @@ -61,36 +59,27 @@ export default function ContextMenu({ }; }, [onClose]); - useEffect(() => { - if (menuRef.current) { - const { width, height } = menuRef.current.getBoundingClientRect(); - setMenuWidth(width); - setMenuHeight(height); - } - }, [status.open]); - - const viewportWidth = - window.innerWidth || document.documentElement.clientWidth; - const viewportHeight = - window.innerHeight || document.documentElement.clientHeight; - const menuStyle: CSSProperties = { - visibility: status.open ? 'visible' : 'hidden', - top: Math.min(status.y, viewportHeight - menuHeight - 10), - left: Math.min(status.x, viewportWidth - menuWidth - 10), minWidth, + left: status.x, + top: status.y, }; return ( -
e.stopPropagation()} - > -
    {children}
-
+ {status.open && ( +
e.stopPropagation()} + > + + + {children} + + +
+ )}
); } @@ -115,25 +104,35 @@ ContextMenu.Item = function ({ }, [handleClose, onClick]); return ( -
  • -
    - {tick ? : icon} -
    -
    - {text} -
    -
    - {hotkey} -
    -
  • + destructive={destructive} + disabled={disabled} + right={hotkey} + icon={tick ? : icon} + labelWidth={300} + separator={separator} + /> + //
  • + //
    + // {tick ? : icon} + //
    + //
    + // {text} + //
    + //
    + // {hotkey} + //
    + //
  • ); }; diff --git a/src/renderer/components/ContextMenu/styles.module.scss b/src/renderer/components/ContextMenu/styles.module.scss index 13dc34c..f67f5f4 100644 --- a/src/renderer/components/ContextMenu/styles.module.scss +++ b/src/renderer/components/ContextMenu/styles.module.scss @@ -2,96 +2,4 @@ position: fixed; background: var(--color-surface); z-index: 90000; - border-radius: 8px; - padding: 3px 0; - border: 1px solid var(--color-border); - - ul { - list-style: none; - display: grid; - grid-template-columns: 30px minmax(150px, auto) auto; - - > li { - display: contents; - - > div { - span { - display: block; - height: 100%; - width: 100%; - padding: 7px 5px; - cursor: pointer; - } - } - - > div:last-child { - padding-right: 4px; - - span { - border-top-right-radius: 5px; - border-bottom-right-radius: 5px; - } - } - - > div:first-child { - padding-left: 4px; - - span { - border-top-left-radius: 5px; - border-bottom-left-radius: 5px; - } - } - - &:hover > div { - span { - background: var(--color-surface-hover); - } - } - } - - > li.disabled { - > div { - span { - color: var(--color-text-disabled); - } - } - - &:hover > div { - span { - background: inherit; - } - } - } - - > li.separator { - > div { - padding-bottom: 6px; - margin-bottom: 6px; - border-bottom: 1px solid var(--color-border); - } - } - - > li.destructive { - > div { - color: var(--color-critical); - } - } - } -} - -.icon { - display: flex; - align-items: center; - justify-content: center; - - img { - width: 20px; - height: 20px; - } -} - -.text { - span { - padding-right: 20px !important; - } } diff --git a/src/renderer/components/OptionList/OptionListItem.tsx b/src/renderer/components/OptionList/OptionListItem.tsx index 267170e..37f9b51 100644 --- a/src/renderer/components/OptionList/OptionListItem.tsx +++ b/src/renderer/components/OptionList/OptionListItem.tsx @@ -1,4 +1,4 @@ -import { PropsWithChildren, ReactElement } from 'react'; +import { PropsWithChildren, ReactElement, useMemo } from 'react'; import styles from './styles.module.scss'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faChevronRight } from '@fortawesome/free-solid-svg-icons'; @@ -11,8 +11,8 @@ interface OptionListItemProps { right?: string; labelWidth?: number; disabled?: boolean; - destructive: boolean; - separator: boolean; + destructive?: boolean; + separator?: boolean; onClick?: (e: React.MouseEvent) => void; } @@ -23,10 +23,29 @@ export default function OptionListItem({ labelWidth, children, onClick, + disabled, + destructive, + separator, }: PropsWithChildren) { + const className = useMemo(() => { + return [ + styles.item, + disabled ? styles.disabled : undefined, + !disabled && destructive ? styles.destructive : undefined, + ] + .filter(Boolean) + .join(' '); + }, [disabled, destructive, separator]); + + const outterClassName = useMemo(() => { + return [styles.outerItem, separator ? styles.separator : undefined] + .filter(Boolean) + .join(' '); + }, []); + return ( -
    -
    +
    +
    {icon}
    {label} diff --git a/src/renderer/components/OptionList/styles.module.scss b/src/renderer/components/OptionList/styles.module.scss index 0e1ca8b..6313618 100644 --- a/src/renderer/components/OptionList/styles.module.scss +++ b/src/renderer/components/OptionList/styles.module.scss @@ -7,6 +7,10 @@ background: var(--color-surface-hover); } + > .item.disabled { + background: inherit; + } + > .child { visibility: visible; } @@ -36,8 +40,14 @@ justify-content: center; display: flex; margin-left: 10px; + margin-right: 3px; width: 15px; flex-shrink: 0; + + img { + width: 15; + height: 15px; + } } .right { @@ -55,3 +65,17 @@ display: flex; } } + +.destructive { + color: var(--color-critical); +} + +.disabled { + color: var(--color-text-disabled); +} + +.separator { + padding-bottom: 6px; + margin-bottom: 6px; + border-bottom: 1px solid var(--color-border); +} From 6ac4a8ae8c36b20140ac98a2693854589484018a Mon Sep 17 00:00:00 2001 From: "Visal .In" Date: Sat, 26 Aug 2023 21:54:25 +0700 Subject: [PATCH 3/8] change the context menu usage --- .../components/AvoidOffscreen/index.tsx | 13 +- .../ContextMenu/AttachedContextMenu.tsx | 13 +- src/renderer/components/ContextMenu/index.tsx | 170 ++++++------------ .../components/ContextMenu/styles.module.scss | 5 - .../components/OptionList/OptionListItem.tsx | 16 +- src/renderer/components/OptionList/index.tsx | 7 +- src/renderer/contexts/ContextMenuProvider.tsx | 29 +-- src/stories/OptionList.stories.tsx | 30 ++-- 8 files changed, 116 insertions(+), 167 deletions(-) delete mode 100644 src/renderer/components/ContextMenu/styles.module.scss diff --git a/src/renderer/components/AvoidOffscreen/index.tsx b/src/renderer/components/AvoidOffscreen/index.tsx index 6ae4b3d..e84edd0 100644 --- a/src/renderer/components/AvoidOffscreen/index.tsx +++ b/src/renderer/components/AvoidOffscreen/index.tsx @@ -50,6 +50,13 @@ export default function AvoidOffscreen({ children }: PropsWithChildren) { } }, [ref, computePosition]); + const flipTranslateStyle = { + right: '100%', + transform: `translateX(-${offsetLeft}px)`, + }; + const flipNormalStyle = { right: '100%' }; + const flipStyle = offsetLeft ? flipTranslateStyle : flipNormalStyle; + return (
    {children} diff --git a/src/renderer/components/ContextMenu/AttachedContextMenu.tsx b/src/renderer/components/ContextMenu/AttachedContextMenu.tsx index cdcb5dd..e84bbf6 100644 --- a/src/renderer/components/ContextMenu/AttachedContextMenu.tsx +++ b/src/renderer/components/ContextMenu/AttachedContextMenu.tsx @@ -1,4 +1,4 @@ -import { ReactElement, useEffect, useRef, useState } from 'react'; +import { ReactElement, useRef, useEffect, useState } from 'react'; import ContextMenu, { ContextMenuItemProps } from '.'; interface AttachedContextMenuProps { @@ -39,16 +39,15 @@ export default function AttachedContextMenu({ {activator({ isOpened: open })}
    { setOpen(false); }} - > - {items.map((item, idx) => ( - - ))} - + /> ); } diff --git a/src/renderer/components/ContextMenu/index.tsx b/src/renderer/components/ContextMenu/index.tsx index 7766774..00640d5 100644 --- a/src/renderer/components/ContextMenu/index.tsx +++ b/src/renderer/components/ContextMenu/index.tsx @@ -1,56 +1,66 @@ -import { - CSSProperties, - PropsWithChildren, - ReactElement, - createContext, - useCallback, - useContext, - useEffect, -} from 'react'; -import styles from './styles.module.scss'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faCheck } from '@fortawesome/free-solid-svg-icons'; +import { useEffect } from 'react'; import AvoidOffscreen from '../AvoidOffscreen'; import OptionList from '../OptionList'; -import OptionListItem from '../OptionList/OptionListItem'; +import OptionListItem, { + OptionListItemProps, +} from '../OptionList/OptionListItem'; import DropContainer from '../DropContainer'; -export interface ContextMenuItemProps { - text: string; - icon?: ReactElement; - hotkey?: string; - disabled?: boolean; - destructive?: boolean; - onClick?: () => void; - separator?: boolean; - tick?: boolean; +export interface ContextMenuItemProps extends OptionListItemProps { + children?: ContextMenuItemProps[]; } -interface ContextMenuStatus { +interface ContextMenuProps { + items: ContextMenuItemProps[]; x: number; y: number; - open: boolean; + open?: boolean; + minWidth?: number; + onClose?: () => void; } -const ContextMenuContext = createContext<{ handleClose: () => void }>({ - handleClose: () => { - return; - }, -}); +function renderArrayOfMenu( + items: ContextMenuItemProps[], + onClose?: () => void, + minWidth?: number +) { + return ( + + + {items.map((value, idx) => { + const { children, onClick, ...props } = value; + if (children && children.length > 0) { + return ( + { + if (onClick) onClick(e); + if (onClose) onClose(); + }} + > + {renderArrayOfMenu(children, onClose)} + + ); + } + return ; + })} + + + ); +} export default function ContextMenu({ - children, - status, + items, + x, + y, onClose, + open, minWidth, -}: PropsWithChildren<{ - status: ContextMenuStatus; - onClose: () => void; - minWidth?: number; -}>) { +}: ContextMenuProps) { useEffect(() => { const onDocumentClicked = () => { - onClose(); + if (onClose) onClose(); }; document.addEventListener('mousedown', onDocumentClicked); @@ -59,80 +69,14 @@ export default function ContextMenu({ }; }, [onClose]); - const menuStyle: CSSProperties = { - minWidth, - left: status.x, - top: status.y, - }; - - return ( - - {status.open && ( -
    e.stopPropagation()} - > - - - {children} - - -
    - )} -
    - ); + return open ? ( +
    e.stopPropagation()} + > + + {renderArrayOfMenu(items, onClose, minWidth)} + +
    + ) : null; } - -ContextMenu.Item = function ({ - text, - onClick, - icon, - tick, - disabled, - destructive, - separator, - hotkey, -}: ContextMenuItemProps) { - const { handleClose } = useContext(ContextMenuContext); - - const onMenuClicked = useCallback(() => { - if (onClick) { - onClick(); - } - handleClose(); - }, [handleClose, onClick]); - - return ( - : icon} - labelWidth={300} - separator={separator} - /> - //
  • - //
    - // {tick ? : icon} - //
    - //
    - // {text} - //
    - //
    - // {hotkey} - //
    - //
  • - ); -}; diff --git a/src/renderer/components/ContextMenu/styles.module.scss b/src/renderer/components/ContextMenu/styles.module.scss deleted file mode 100644 index f67f5f4..0000000 --- a/src/renderer/components/ContextMenu/styles.module.scss +++ /dev/null @@ -1,5 +0,0 @@ -.contextMenu { - position: fixed; - background: var(--color-surface); - z-index: 90000; -} diff --git a/src/renderer/components/OptionList/OptionListItem.tsx b/src/renderer/components/OptionList/OptionListItem.tsx index 37f9b51..35c114c 100644 --- a/src/renderer/components/OptionList/OptionListItem.tsx +++ b/src/renderer/components/OptionList/OptionListItem.tsx @@ -1,25 +1,27 @@ import { PropsWithChildren, ReactElement, useMemo } from 'react'; import styles from './styles.module.scss'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faChevronRight } from '@fortawesome/free-solid-svg-icons'; +import { faCheck, faChevronRight } from '@fortawesome/free-solid-svg-icons'; import AvoidOffscreen from '../AvoidOffscreen'; -interface OptionListItemProps { - label: string; +export interface OptionListItemProps { + text: string; icon?: ReactElement; selected?: boolean; right?: string; labelWidth?: number; disabled?: boolean; + tick?: boolean; destructive?: boolean; separator?: boolean; onClick?: (e: React.MouseEvent) => void; } export default function OptionListItem({ - label, + text, icon, right, + tick, labelWidth, children, onClick, @@ -46,9 +48,11 @@ export default function OptionListItem({ return (
    -
    {icon}
    +
    + {tick ? : icon} +
    - {label} + {text}
    {right}
    diff --git a/src/renderer/components/OptionList/index.tsx b/src/renderer/components/OptionList/index.tsx index 4552b78..5d42a10 100644 --- a/src/renderer/components/OptionList/index.tsx +++ b/src/renderer/components/OptionList/index.tsx @@ -1,5 +1,8 @@ import { PropsWithChildren } from 'react'; -export default function OptionList({ children }: PropsWithChildren) { - return
    {children}
    ; +export default function OptionList({ + children, + minWidth, +}: PropsWithChildren<{ minWidth?: number }>) { + return
    {children}
    ; } diff --git a/src/renderer/contexts/ContextMenuProvider.tsx b/src/renderer/contexts/ContextMenuProvider.tsx index 9a3f2bb..0dc2343 100644 --- a/src/renderer/contexts/ContextMenuProvider.tsx +++ b/src/renderer/contexts/ContextMenuProvider.tsx @@ -1,3 +1,4 @@ +import NotImplementCallback from 'libs/NotImplementCallback'; import { useState, PropsWithChildren, @@ -16,15 +17,9 @@ const ContextMenuContext = createContext<{ setMenuItem: (items: ContextMenuItemProps[]) => void; open: boolean; }>({ - handleContextMenu: () => { - throw 'Not implemented'; - }, - handleClick: () => { - throw 'Not implemented'; - }, - setMenuItem: () => { - throw 'Not implemented'; - }, + handleContextMenu: NotImplementCallback, + handleClick: NotImplementCallback, + setMenuItem: NotImplementCallback, open: false, }); @@ -114,16 +109,22 @@ export function ContextMenuProvider({ children }: PropsWithChildren) { [setStatus] ); + console.log('re-render'); + return ( {children} - - {menuItem.map((itemProps, idx) => { - return ; - })} - + {status.open && ( + + )} ); } diff --git a/src/stories/OptionList.stories.tsx b/src/stories/OptionList.stories.tsx index 25a7565..4260929 100644 --- a/src/stories/OptionList.stories.tsx +++ b/src/stories/OptionList.stories.tsx @@ -12,31 +12,31 @@ function StoryPage() {
    - + } /> - - + + - - - - - - - - - + + + + + + + + + - - + +
    From c7bedfa0215cc44cb141690ab7789f104ec361b4 Mon Sep 17 00:00:00 2001 From: "Visal .In" Date: Sat, 26 Aug 2023 22:15:53 +0700 Subject: [PATCH 4/8] fix some bug in flipping --- .../components/AvoidOffscreen/index.tsx | 22 +++++-------------- .../components/OptionList/styles.module.scss | 10 ++++----- src/renderer/contexts/ContextMenuProvider.tsx | 2 -- .../QueryResultViewer/QueryResultTable.tsx | 5 +++++ 4 files changed, 15 insertions(+), 24 deletions(-) diff --git a/src/renderer/components/AvoidOffscreen/index.tsx b/src/renderer/components/AvoidOffscreen/index.tsx index e84edd0..53bbb90 100644 --- a/src/renderer/components/AvoidOffscreen/index.tsx +++ b/src/renderer/components/AvoidOffscreen/index.tsx @@ -10,7 +10,6 @@ export default function AvoidOffscreen({ children }: PropsWithChildren) { const ref = useRef(null); const [computed, setComputed] = useState(false); const [offsetTop, setOffsetTop] = useState(0); - const [offsetLeft, setOffsetLeft] = useState(0); const [flip, setFlip] = useState(false); const computePosition = useCallback(() => { @@ -21,23 +20,19 @@ export default function AvoidOffscreen({ children }: PropsWithChildren) { const viewportHeight = window.innerHeight || document.documentElement.clientHeight; - setOffsetTop(Math.min(0, viewportHeight - bound.bottom)); + if (offsetTop === 0) { + setOffsetTop(Math.min(0, viewportHeight - bound.bottom)); + } if (bound.width === 0) return false; if (!flip) { setFlip(bound.right > viewportWidth); - if ( - ref.current.parentElement && - ref.current.parentElement.style.position === 'fixed' - ) { - setOffsetLeft(bound.width); - } } setComputed(true); } - }, [ref, setOffsetTop, setFlip, offsetLeft, flip, setComputed]); + }, [ref, setOffsetTop, setFlip, flip, offsetTop, setComputed]); useEffect(() => { if (ref.current) { @@ -50,13 +45,6 @@ export default function AvoidOffscreen({ children }: PropsWithChildren) { } }, [ref, computePosition]); - const flipTranslateStyle = { - right: '100%', - transform: `translateX(-${offsetLeft}px)`, - }; - const flipNormalStyle = { right: '100%' }; - const flipStyle = offsetLeft ? flipTranslateStyle : flipNormalStyle; - return (
    {children} diff --git a/src/renderer/components/OptionList/styles.module.scss b/src/renderer/components/OptionList/styles.module.scss index 6313618..a4ba618 100644 --- a/src/renderer/components/OptionList/styles.module.scss +++ b/src/renderer/components/OptionList/styles.module.scss @@ -2,6 +2,10 @@ margin: 0 5px; position: relative; + .child { + display: none; + } + &:hover { > .item { background: var(--color-surface-hover); @@ -12,14 +16,10 @@ } > .child { - visibility: visible; + display: contents; } } - .child { - visibility: hidden; - display: content; - } } .item { diff --git a/src/renderer/contexts/ContextMenuProvider.tsx b/src/renderer/contexts/ContextMenuProvider.tsx index 0dc2343..a37a747 100644 --- a/src/renderer/contexts/ContextMenuProvider.tsx +++ b/src/renderer/contexts/ContextMenuProvider.tsx @@ -109,8 +109,6 @@ export function ContextMenuProvider({ children }: PropsWithChildren) { [setStatus] ); - console.log('re-render'); - return ( selectedCell?.insert(null), separator: true, }, + { + text: 'Transform', + disabled: !selectedCell, + children: [{ text: 'Bcrypt Password Hash' }, { text: 'MD5' }], + }, { text: 'Copy', hotkey: 'Ctrl + C', From cd73c1731b2bef213cc944c8cb1b066f55d282cf Mon Sep 17 00:00:00 2001 From: "Visal .In" Date: Sun, 27 Aug 2023 08:29:06 +0700 Subject: [PATCH 5/8] add copy as json --- src/renderer/App.css | 2 +- src/renderer/components/ContextMenu/index.tsx | 25 +++-- .../components/OptionList/OptionListItem.tsx | 9 +- .../QueryResultViewer/QueryResultTable.tsx | 83 ++------------ .../QueryResultViewer/TableCellManager.tsx | 2 +- .../useDataTableContextMenu.tsx | 105 ++++++++++++++++++ 6 files changed, 136 insertions(+), 90 deletions(-) create mode 100644 src/renderer/screens/DatabaseScreen/QueryResultViewer/useDataTableContextMenu.tsx diff --git a/src/renderer/App.css b/src/renderer/App.css index 34f03ae..de772f0 100644 --- a/src/renderer/App.css +++ b/src/renderer/App.css @@ -16,7 +16,7 @@ body { --color-text: #000; --color-text-link: #1a64f4; - --color-text-disabled: #555; + --color-text-disabled: #aaa; /* For Button */ --color-surface-light: #ecf0f1; diff --git a/src/renderer/components/ContextMenu/index.tsx b/src/renderer/components/ContextMenu/index.tsx index 00640d5..d878937 100644 --- a/src/renderer/components/ContextMenu/index.tsx +++ b/src/renderer/components/ContextMenu/index.tsx @@ -29,21 +29,22 @@ function renderArrayOfMenu( {items.map((value, idx) => { const { children, onClick, ...props } = value; + + const overrideOnClick = (e: React.MouseEvent) => { + if (onClick) onClick(e); + if (onClose) onClose(); + }; + if (children && children.length > 0) { return ( - { - if (onClick) onClick(e); - if (onClose) onClose(); - }} - > + {renderArrayOfMenu(children, onClose)} ); } - return ; + return ( + + ); })} @@ -69,10 +70,14 @@ export default function ContextMenu({ }; }, [onClose]); + console.log(items); + return open ? (
    e.stopPropagation()} + onMouseDown={(e) => { + e.stopPropagation(); + }} > {renderArrayOfMenu(items, onClose, minWidth)} diff --git a/src/renderer/components/OptionList/OptionListItem.tsx b/src/renderer/components/OptionList/OptionListItem.tsx index 35c114c..97492d5 100644 --- a/src/renderer/components/OptionList/OptionListItem.tsx +++ b/src/renderer/components/OptionList/OptionListItem.tsx @@ -46,7 +46,10 @@ export default function OptionListItem({ }, []); return ( -
    +
    {tick ? : icon} @@ -56,11 +59,11 @@ export default function OptionListItem({
    {right}
    - {children && } + {children && !disabled && }
    - {children && ( + {children && !disabled && (
    {children}
    diff --git a/src/renderer/screens/DatabaseScreen/QueryResultViewer/QueryResultTable.tsx b/src/renderer/screens/DatabaseScreen/QueryResultViewer/QueryResultTable.tsx index feeefa8..2016453 100644 --- a/src/renderer/screens/DatabaseScreen/QueryResultViewer/QueryResultTable.tsx +++ b/src/renderer/screens/DatabaseScreen/QueryResultViewer/QueryResultTable.tsx @@ -4,13 +4,11 @@ import TableCell from 'renderer/screens/DatabaseScreen/QueryResultViewer/TableCe import { QueryResultHeader } from 'types/SqlResult'; import { getUpdatableTable } from 'libs/GenerateSqlFromChanges'; import { useSchema } from 'renderer/contexts/SchemaProvider'; -import { useContextMenu } from 'renderer/contexts/ContextMenuProvider'; import { useQueryResultChange } from 'renderer/contexts/QueryResultChangeProvider'; import { useTableCellManager } from './TableCellManager'; import OptimizeTable from 'renderer/components/OptimizeTable'; import Icon from 'renderer/components/Icon'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faPlusCircle, faTimesCircle } from '@fortawesome/free-solid-svg-icons'; +import useDataTableContextMenu from './useDataTableContextMenu'; interface QueryResultTableProps { headers: QueryResultHeader[]; @@ -52,78 +50,13 @@ function QueryResultTable({ setSelectedRowsIndex(selectedRows); }; - const { handleContextMenu } = useContextMenu(() => { - const selectedCell = cellManager.getFocusCell(); - - return [ - { - text: 'Insert new row', - onClick: () => { - collector.createNewRow(); - }, - icon: , - }, - { - text: 'Remove selected rows', - destructive: true, - disabled: selectedRowsIndex.length === 0, - onClick: () => { - for (const selectedRowIndex of selectedRowsIndex) { - collector.removeRow( - selectedRowIndex - newRowCount < 0 - ? selectedRowIndex - newRowCount - : selectedRowIndex - newRowCount + page * pageSize - ); - } - }, - icon: , - separator: true, - }, - { - text: 'Insert NULL', - disabled: !selectedCell, - onClick: () => selectedCell?.insert(null), - separator: true, - }, - { - text: 'Transform', - disabled: !selectedCell, - children: [{ text: 'Bcrypt Password Hash' }, { text: 'MD5' }], - }, - { - text: 'Copy', - hotkey: 'Ctrl + C', - disabled: !selectedCell, - onClick: () => selectedCell?.copy(), - }, - { - text: 'Paste', - hotkey: 'Ctrl + V', - disabled: !selectedCell, - onClick: () => selectedCell?.paste(), - separator: true, - }, - { - text: `Discard All Changes`, - destructive: true, - disabled: !collector.getChangesCount(), - onClick: () => { - const rows = collector.getChanges().changes; - - for (const row of rows) { - for (const col of row.cols) { - const cell = cellManager.get(row.row, col.col); - if (cell) { - cell.discard(); - } - } - } - - collector.clear(); - }, - }, - ]; - }, [collector, newRowCount, selectedRowsIndex, page, pageSize]); + const { handleContextMenu } = useDataTableContextMenu({ + data: result, + cellManager, + collector, + newRowCount, + selectedRowsIndex, + }); const data: { data: Record; rowIndex: number }[] = useMemo(() => { diff --git a/src/renderer/screens/DatabaseScreen/QueryResultViewer/TableCellManager.tsx b/src/renderer/screens/DatabaseScreen/QueryResultViewer/TableCellManager.tsx index 2e20aa2..7375c81 100644 --- a/src/renderer/screens/DatabaseScreen/QueryResultViewer/TableCellManager.tsx +++ b/src/renderer/screens/DatabaseScreen/QueryResultViewer/TableCellManager.tsx @@ -1,7 +1,7 @@ import { createContext, PropsWithChildren, useMemo, useContext } from 'react'; import { TableEditableCellHandler } from './TableCell/TableEditableCell'; -class TableCellManager { +export class TableCellManager { protected focused: [number, number] | null = null; protected cells: Record< string, diff --git a/src/renderer/screens/DatabaseScreen/QueryResultViewer/useDataTableContextMenu.tsx b/src/renderer/screens/DatabaseScreen/QueryResultViewer/useDataTableContextMenu.tsx new file mode 100644 index 0000000..c1f29fd --- /dev/null +++ b/src/renderer/screens/DatabaseScreen/QueryResultViewer/useDataTableContextMenu.tsx @@ -0,0 +1,105 @@ +import { faPlusCircle, faTimesCircle } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import ResultChangeCollector from 'libs/ResultChangeCollector'; +import { useContextMenu } from 'renderer/contexts/ContextMenuProvider'; +import { TableCellManager } from './TableCellManager'; + +interface DataTableContextMenuDeps { + collector: ResultChangeCollector; + cellManager: TableCellManager; + selectedRowsIndex: number[]; + newRowCount: number; + data: { data: Record; rowIndex: number }[]; +} + +export default function useDataTableContextMenu({ + collector, + cellManager, + selectedRowsIndex, + newRowCount, + data, +}: DataTableContextMenuDeps) { + return useContextMenu(() => { + const selectedCell = cellManager.getFocusCell(); + + function onCopyAsJson() { + window.navigator.clipboard.writeText( + JSON.stringify( + selectedRowsIndex.map((rowIndex) => data[rowIndex].data), + undefined, + 2 + ) + ); + } + + return [ + { + text: 'Insert new row', + onClick: () => { + collector.createNewRow(); + }, + icon: , + }, + { + text: 'Remove selected rows', + destructive: true, + disabled: selectedRowsIndex.length === 0, + onClick: () => { + for (const selectedRowIndex of selectedRowsIndex) { + collector.removeRow(data[selectedRowIndex].rowIndex); + } + }, + icon: , + separator: true, + }, + { + text: 'Insert NULL', + disabled: !selectedCell, + onClick: () => selectedCell?.insert(null), + separator: true, + }, + { + text: 'Copy', + hotkey: 'Ctrl + C', + disabled: !selectedCell, + onClick: () => selectedCell?.copy(), + }, + { + text: 'Copy Selected Rows As', + disabled: !selectedCell, + children: [ + { text: 'As Excel', disabled: true }, + { text: 'As CSV', disabled: true }, + { text: 'As JSON', onClick: onCopyAsJson }, + { text: 'As SQL', disabled: true }, + ], + }, + { + text: 'Paste', + hotkey: 'Ctrl + V', + disabled: !selectedCell, + onClick: () => selectedCell?.paste(), + separator: true, + }, + { + text: `Discard All Changes`, + destructive: true, + disabled: !collector.getChangesCount(), + onClick: () => { + const rows = collector.getChanges().changes; + + for (const row of rows) { + for (const col of row.cols) { + const cell = cellManager.get(row.row, col.col); + if (cell) { + cell.discard(); + } + } + } + + collector.clear(); + }, + }, + ]; + }, [collector, newRowCount, selectedRowsIndex, data]); +} From 0dbdfae7f0cf3921ce808eabd1d79a57c935f0ba Mon Sep 17 00:00:00 2001 From: "Visal .In" Date: Sun, 27 Aug 2023 08:32:58 +0700 Subject: [PATCH 6/8] toolbar now no longer rely on useContextMenuProvider --- src/renderer/components/ContextMenu/index.tsx | 2 -- src/renderer/components/Toolbar/index.tsx | 19 ++++++++++++------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/renderer/components/ContextMenu/index.tsx b/src/renderer/components/ContextMenu/index.tsx index d878937..4de3a1f 100644 --- a/src/renderer/components/ContextMenu/index.tsx +++ b/src/renderer/components/ContextMenu/index.tsx @@ -70,8 +70,6 @@ export default function ContextMenu({ }; }, [onClose]); - console.log(items); - return open ? (
    items, [items]); - return ( -
  • - {icon && {icon}} - {text} -
  • + { + return ( +
  • + {icon && {icon}} + {text} +
  • + ); + }} + /> ); }; From ff87bfb8f96e6f3d05bf9ccbbcf85915ad071ce5 Mon Sep 17 00:00:00 2001 From: "Visal .In" Date: Sun, 27 Aug 2023 08:39:06 +0700 Subject: [PATCH 7/8] fixing code smell --- src/renderer/components/ContextMenu/index.tsx | 5 ++-- src/renderer/components/Toolbar/index.tsx | 25 ++++++++----------- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/src/renderer/components/ContextMenu/index.tsx b/src/renderer/components/ContextMenu/index.tsx index 4de3a1f..74c7620 100644 --- a/src/renderer/components/ContextMenu/index.tsx +++ b/src/renderer/components/ContextMenu/index.tsx @@ -28,6 +28,7 @@ function renderArrayOfMenu( {items.map((value, idx) => { + const key = value.text + idx; const { children, onClick, ...props } = value; const overrideOnClick = (e: React.MouseEvent) => { @@ -37,13 +38,13 @@ function renderArrayOfMenu( if (children && children.length > 0) { return ( - + {renderArrayOfMenu(children, onClose)} ); } return ( - + ); })} diff --git a/src/renderer/components/Toolbar/index.tsx b/src/renderer/components/Toolbar/index.tsx index 35080d3..c02104e 100644 --- a/src/renderer/components/Toolbar/index.tsx +++ b/src/renderer/components/Toolbar/index.tsx @@ -1,4 +1,4 @@ -import { PropsWithChildren, ReactNode } from 'react'; +import { PropsWithChildren, ReactNode, useCallback } from 'react'; import styles from './styles.module.scss'; import { ContextMenuItemProps } from '../ContextMenu'; import AttachedContextMenu from '../ContextMenu/AttachedContextMenu'; @@ -93,17 +93,14 @@ Toolbar.ContextMenu = function ({ text: string; icon?: ReactNode; }) { - return ( - { - return ( -
  • - {icon && {icon}} - {text} -
  • - ); - }} - /> - ); + const activator = useCallback(() => { + return ( +
  • + {icon && {icon}} + {text} +
  • + ); + }, []); + + return ; }; From 52157df1f416f218455b6578abcff54f996a3eb6 Mon Sep 17 00:00:00 2001 From: "Visal .In" Date: Sun, 27 Aug 2023 08:48:40 +0700 Subject: [PATCH 8/8] fixing Sonar error --- src/renderer/components/Toolbar/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/components/Toolbar/index.tsx b/src/renderer/components/Toolbar/index.tsx index c02104e..9f05240 100644 --- a/src/renderer/components/Toolbar/index.tsx +++ b/src/renderer/components/Toolbar/index.tsx @@ -84,7 +84,7 @@ Toolbar.TextField = function ({ ); }; -Toolbar.ContextMenu = function ({ +Toolbar.ContextMenu = function ToolbarContextMenu({ items, icon, text,