diff --git a/src/client/webapp/elements/contexts/components/context.tsx b/src/client/webapp/elements/contexts/components/context.tsx index 50a9400..10e5859 100644 --- a/src/client/webapp/elements/contexts/components/context.tsx +++ b/src/client/webapp/elements/contexts/components/context.tsx @@ -1,9 +1,9 @@ -import React, { FC, ReactNode, RefObject } from 'react'; +import React, { FC, ReactNode, RefObject, useRef } from 'react'; import { IAlignment } from '../../require/elements-util'; import ReactHelper from '../../require/react-helper'; export interface ContextProps { - rootRef: RefObject; + rootRef?: RefObject; relativeToRef?: RefObject; relativeToPos?: { x: number, y: number }; alignment: IAlignment; @@ -14,12 +14,14 @@ export interface ContextProps { const Context: FC = (props: ContextProps) => { const { rootRef, relativeToRef, relativeToPos, alignment, children } = props; + const myRootRef = useRef(null); + const [ className ] = ReactHelper.useAlignment( - rootRef, relativeToRef ?? null, relativeToPos ?? null, alignment, 'context react' + rootRef ?? myRootRef, relativeToRef ?? null, relativeToPos ?? null, alignment, 'context react' ); return ( -
+
{children}
); diff --git a/src/client/webapp/elements/contexts/context-hover-basic.tsx b/src/client/webapp/elements/contexts/context-hover-basic.tsx new file mode 100644 index 0000000..a4186b8 --- /dev/null +++ b/src/client/webapp/elements/contexts/context-hover-basic.tsx @@ -0,0 +1,45 @@ +import React, { FC, ReactNode, RefObject, useMemo } from 'react'; +import { ShouldNeverHappenError } from '../../data-types'; +import Context from './components/context'; + +export enum BasicHoverSide { + LEFT = 'left', + RIGHT = 'right', + TOP = 'top', + BOTTOM = 'bottom', +} + +export interface BasicHoverProps { + side: BasicHoverSide; + relativeToRef: RefObject; + children: ReactNode; +} + +const BasicHover: FC = (props: BasicHoverProps) => { + const { side, relativeToRef, children } = props; + + const alignment = useMemo(() => { + switch (side) { + case BasicHoverSide.LEFT: + return { centerY: 'centerY', right: 'left' }; + case BasicHoverSide.RIGHT: + return { centerY: 'centerY', left: 'right' }; + case BasicHoverSide.TOP: + return { bottom: 'top', centerX: 'centerX' }; + case BasicHoverSide.BOTTOM: + return { top: 'bottom', centerX: 'centerX' }; + default: + throw new ShouldNeverHappenError('invalid side'); + } + }, [ side ]); + + return ( + +
+ {children} +
+
+ ); +} + +export default BasicHover; diff --git a/src/client/webapp/elements/lists/components/channel-element.scss b/src/client/webapp/elements/lists/components/channel-element.scss index e69de29..304d82e 100644 --- a/src/client/webapp/elements/lists/components/channel-element.scss +++ b/src/client/webapp/elements/lists/components/channel-element.scss @@ -0,0 +1,18 @@ +@import "../../../styles/theme.scss"; + +.channel .channel-gear-hover { + display: flex; + align-items: center; + line-height: 1; + + .tab { + color: $background-floating; // svg + } + + .text { + color: $text-normal; + background-color: $background-floating; + padding: 8px; + border-radius: 4px; + } +} \ No newline at end of file diff --git a/src/client/webapp/elements/lists/components/channel-element.tsx b/src/client/webapp/elements/lists/components/channel-element.tsx index d0d3175..0e272fe 100644 --- a/src/client/webapp/elements/lists/components/channel-element.tsx +++ b/src/client/webapp/elements/lists/components/channel-element.tsx @@ -9,6 +9,8 @@ import CombinedGuild from '../../../guild-combined'; import ChannelOverlay from '../../overlays/overlay-channel'; import BaseElements from '../../require/base-elements'; import ElementsUtil from '../../require/elements-util'; +import ReactHelper from '../../require/react-helper'; +import BasicHover, { BasicHoverSide } from '../../contexts/context-hover-basic'; export interface ChannelElementProps { guild: CombinedGuild; @@ -25,6 +27,19 @@ const ChannelElement: FC = (props: ChannelElementProps) => const baseClassName = activeChannel?.id === channel.id ? 'channel text active' : 'channel text'; + const [ modifyContextHover, modifyMouseEnterCallable, modifyMouseLeaveCallable ] = ReactHelper.useContextHover( + () => { + return ( + +
+
{BaseElements.TAB_LEFT}
+
Modify Channel
+
+
+ ); + }, [ modifyRef ] + ); + const setSelfActiveChannel = useCallback((event: MouseEvent) => { if (modifyRef.current?.contains(event.target as Node)) return; // ignore "modify" button clicks setActiveChannel(channel); @@ -39,7 +54,11 @@ const ChannelElement: FC = (props: ChannelElementProps) =>
{BaseElements.TEXT_CHANNEL_ICON}
{channel.name}
-
{BaseElements.COG}
+
{BaseElements.COG}
+ {modifyContextHover}
); } diff --git a/src/client/webapp/elements/require/base-elements.tsx b/src/client/webapp/elements/require/base-elements.tsx index ed540ae..9ecf34d 100644 --- a/src/client/webapp/elements/require/base-elements.tsx +++ b/src/client/webapp/elements/require/base-elements.tsx @@ -230,6 +230,11 @@ export default class BaseElements { Z` } }; + static TAB_LEFT = ( + + + + ); static Q_TAB_LEFT = { ns: 'http://www.w3.org/2000/svg', tag: 'svg', width: 10, height: 20, viewBox: '0 0 8 12', content: { ns: 'http://www.w3.org/2000/svg', tag: 'path', fill: 'currentColor', //'fill-rule': 'evenodd', 'clip-rule': 'evenodd', diff --git a/src/client/webapp/elements/require/react-helper.ts b/src/client/webapp/elements/require/react-helper.ts index 2623062..d4e82a7 100644 --- a/src/client/webapp/elements/require/react-helper.ts +++ b/src/client/webapp/elements/require/react-helper.ts @@ -440,7 +440,7 @@ export default class ReactHelper { }, []); const contextMenu = useMemo(() => { return createContextMenu(close); - }, [ close, ...createContextMenuDeps ]); + }, [ close, createContextMenu, ...createContextMenuDeps ]); const toggle = useCallback(() => { setIsOpen(oldIsOpen => !!contextMenu && !oldIsOpen); @@ -451,4 +451,28 @@ export default class ReactHelper { return [ isOpen ? contextMenu : null, toggle, close, open, isOpen ]; } + + static useContextHover( + createContextHover: () => ReactNode, + createContextHoverDeps: DependencyList + ): [ + contextHover: ReactNode, + mouseEnterCallable: () => void, + mouseLeaveCallable: () => void + ] { + const [ isOpen, setIsOpen ] = useState(false); + + const contextHover = useMemo(() => { + return createContextHover(); + }, [ createContextHover, ...createContextHoverDeps ]); + + const mouseEnterCallable = useCallback(() => { + setIsOpen(true); + }, []); + const mouseLeaveCallable = useCallback(() => { + setIsOpen(false); + }, []); + + return [ isOpen ? contextHover : null, mouseEnterCallable, mouseLeaveCallable ]; + } } \ No newline at end of file diff --git a/src/client/webapp/styles/contexts.scss b/src/client/webapp/styles/contexts.scss index f1fa078..63207dd 100644 --- a/src/client/webapp/styles/contexts.scss +++ b/src/client/webapp/styles/contexts.scss @@ -4,6 +4,7 @@ .context { position: fixed; + z-index: 1; // Since useEffect gets called after the element is rendered, hide it until // it gets aligned