react hover indicators

implemented in channel list element
This commit is contained in:
Michael Peters 2021-12-26 14:25:59 -06:00
parent f7433c23be
commit 26139af512
7 changed files with 120 additions and 6 deletions

View File

@ -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<HTMLDivElement>;
rootRef?: RefObject<HTMLDivElement>;
relativeToRef?: RefObject<HTMLElement>;
relativeToPos?: { x: number, y: number };
alignment: IAlignment;
@ -14,12 +14,14 @@ export interface ContextProps {
const Context: FC<ContextProps> = (props: ContextProps) => {
const { rootRef, relativeToRef, relativeToPos, alignment, children } = props;
const myRootRef = useRef<HTMLDivElement>(null);
const [ className ] = ReactHelper.useAlignment(
rootRef, relativeToRef ?? null, relativeToPos ?? null, alignment, 'context react'
rootRef ?? myRootRef, relativeToRef ?? null, relativeToPos ?? null, alignment, 'context react'
);
return (
<div ref={rootRef} className={className}>
<div ref={rootRef ?? myRootRef} className={className}>
{children}
</div>
);

View File

@ -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<HTMLElement>;
children: ReactNode;
}
const BasicHover: FC<BasicHoverProps> = (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 (
<Context alignment={alignment} relativeToRef={relativeToRef}>
<div className={'basic-hover ' + side}>
{children}
</div>
</Context>
);
}
export default BasicHover;

View File

@ -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;
}
}

View File

@ -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<ChannelElementProps> = (props: ChannelElementProps) =>
const baseClassName = activeChannel?.id === channel.id ? 'channel text active' : 'channel text';
const [ modifyContextHover, modifyMouseEnterCallable, modifyMouseLeaveCallable ] = ReactHelper.useContextHover(
() => {
return (
<BasicHover side={BasicHoverSide.RIGHT} relativeToRef={modifyRef}>
<div className="channel-gear-hover">
<div className="tab">{BaseElements.TAB_LEFT}</div>
<div className="text">Modify Channel</div>
</div>
</BasicHover>
);
}, [ modifyRef ]
);
const setSelfActiveChannel = useCallback((event: MouseEvent<HTMLDivElement>) => {
if (modifyRef.current?.contains(event.target as Node)) return; // ignore "modify" button clicks
setActiveChannel(channel);
@ -39,7 +54,11 @@ const ChannelElement: FC<ChannelElementProps> = (props: ChannelElementProps) =>
<div className={baseClassName} onClick={setSelfActiveChannel}>
<div className="icon">{BaseElements.TEXT_CHANNEL_ICON}</div>
<div className="name">{channel.name}</div>
<div className="modify" ref={modifyRef} onClick={launchModify}>{BaseElements.COG}</div>
<div
className="modify" ref={modifyRef} onClick={launchModify}
onMouseEnter={modifyMouseEnterCallable} onMouseLeave={modifyMouseLeaveCallable}
>{BaseElements.COG}</div>
{modifyContextHover}
</div>
);
}

View File

@ -230,6 +230,11 @@ export default class BaseElements {
Z` }
};
static TAB_LEFT = (
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="20" viewBox="0 0 8 12">
<path fill="currentColor" d="M 0,6 L 8,12 L 8,0 Z"></path>
</svg>
);
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',

View File

@ -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<boolean>(false);
const contextHover = useMemo(() => {
return createContextHover();
}, [ createContextHover, ...createContextHoverDeps ]);
const mouseEnterCallable = useCallback(() => {
setIsOpen(true);
}, []);
const mouseLeaveCallable = useCallback(() => {
setIsOpen(false);
}, []);
return [ isOpen ? contextHover : null, mouseEnterCallable, mouseLeaveCallable ];
}
}

View File

@ -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