-
+ (
- {label && (
-
- {label}
-
- )}
- {this.props.flat ? null : (
-
{
- this.menuBtnRef = ref;
- }}
- className={cn('rounded-full flex items-center justify-center', {
- 'bg-gray-light': displayed,
- 'w-10 h-10': !label,
- })}
- role="button"
- >
-
-
- )}
-
-
-
- {items
- .filter(({ hidden }) => !hidden)
- .map(({ onClick, text, icon, disabled = false }) => (
-
{}}
- className={disabled ? 'cursor-not-allowed' : ''}
- role="menuitem"
- >
-
- {icon && (
-
- {/* @ts-ignore */}
-
-
- )}
-
{text}
+ {items
+ .filter(({ hidden }) => !hidden)
+ .map(({ onClick, text, icon, disabled = false }) => (
+
{}}
+ className={disabled ? 'cursor-not-allowed' : ''}
+ role="menuitem"
+ >
+
+ {icon && (
+
+ {/* @ts-ignore */}
+
+
+ )}
+
{text}
+
-
- ))}
+ ))}
+
+ )}
+ >
+
+ {label && (
+
+ {label}
+
+ )}
+ {this.props.flat ? null : (
+
{
+ this.menuBtnRef = ref;
+ }}
+ className={cn('rounded-full flex items-center justify-center', {
+ 'bg-gray-light': displayed,
+ 'w-10 h-10': !label,
+ })}
+ role="button"
+ >
+
+
+ )}
-
+
);
}
}
diff --git a/frontend/app/components/ui/ItemMenu/itemMenu.module.css b/frontend/app/components/ui/ItemMenu/itemMenu.module.css
index 477ef7ffb..7583e9975 100644
--- a/frontend/app/components/ui/ItemMenu/itemMenu.module.css
+++ b/frontend/app/components/ui/ItemMenu/itemMenu.module.css
@@ -40,9 +40,9 @@
white-space: nowrap;
z-index: 20;
- position: absolute;
- right: 0px;
- top: 37px;
+ /* position: absolute; */
+ /* right: 0px; */
+ /* top: 37px; */
min-width: 150px;
background-color: $white;
border-radius: 3px;
diff --git a/frontend/app/components/ui/Popover/Popover.tsx b/frontend/app/components/ui/Popover/Popover.tsx
new file mode 100644
index 000000000..9f64af9f6
--- /dev/null
+++ b/frontend/app/components/ui/Popover/Popover.tsx
@@ -0,0 +1,86 @@
+import React, { cloneElement, useMemo, useState } from 'react';
+import {
+ Placement,
+ offset,
+ flip,
+ shift,
+ autoUpdate,
+ useFloating,
+ useInteractions,
+ useRole,
+ useDismiss,
+ useId,
+ useClick,
+ FloatingFocusManager,
+} from '@floating-ui/react-dom-interactions';
+import { mergeRefs } from 'react-merge-refs';
+import { INDEXES } from 'App/constants/zindex';
+
+interface Props {
+ render: (data: { close: () => void; labelId: string; descriptionId: string }) => React.ReactNode;
+ placement?: Placement;
+ children: JSX.Element;
+}
+
+const Popover = ({ children, render, placement }: Props) => {
+ const [open, setOpen] = useState(false);
+
+ const { x, y, reference, floating, strategy, context } = useFloating({
+ open,
+ onOpenChange: setOpen,
+ middleware: [offset(5), flip(), shift()],
+ placement,
+ whileElementsMounted: autoUpdate,
+ });
+
+ const id = useId();
+ const labelId = `${id}-label`;
+ const descriptionId = `${id}-description`;
+
+ const { getReferenceProps, getFloatingProps } = useInteractions([
+ useClick(context),
+ useRole(context),
+ useDismiss(context),
+ ]);
+
+ // Preserve the consumer's ref
+ const ref = useMemo(() => mergeRefs([reference, (children as any).ref]), [reference, children]);
+
+ return (
+ <>
+ {cloneElement(children, getReferenceProps({ ref, ...children.props }))}
+ {open && (
+
+
+ {render({
+ labelId,
+ descriptionId,
+ close: () => {
+ setOpen(false);
+ },
+ })}
+
+
+ )}
+ >
+ );
+};
+
+export default Popover;
diff --git a/frontend/app/components/ui/Popover/index.ts b/frontend/app/components/ui/Popover/index.ts
new file mode 100644
index 000000000..44c04deaf
--- /dev/null
+++ b/frontend/app/components/ui/Popover/index.ts
@@ -0,0 +1 @@
+export { default } from './Popover';
diff --git a/frontend/app/components/ui/Tooltip/FloatingTooltip.tsx b/frontend/app/components/ui/Tooltip/FloatingTooltip.tsx
new file mode 100644
index 000000000..6713d487b
--- /dev/null
+++ b/frontend/app/components/ui/Tooltip/FloatingTooltip.tsx
@@ -0,0 +1,121 @@
+import * as React from 'react';
+import { mergeRefs } from 'react-merge-refs';
+import {
+ useFloating,
+ autoUpdate,
+ offset,
+ flip,
+ shift,
+ useHover,
+ useFocus,
+ useDismiss,
+ useRole,
+ useInteractions,
+ FloatingPortal,
+} from '@floating-ui/react-dom-interactions';
+import type { Placement } from '@floating-ui/react-dom-interactions';
+import { INDEXES } from 'App/constants/zindex';
+
+export function useTooltipState({
+ disabled = false,
+ initialOpen = false,
+ placement = 'top',
+ delay,
+}: {
+ disabled?: boolean;
+ initialOpen?: boolean;
+ placement?: Placement;
+ delay?: number;
+} = {}) {
+ const [open, setOpen] = React.useState(initialOpen);
+
+ React.useEffect(() => {
+ if (disabled) {
+ setOpen(false);
+ }
+ }, [disabled]);
+
+ const data = useFloating({
+ placement,
+ open,
+ onOpenChange: setOpen,
+ whileElementsMounted: autoUpdate,
+ middleware: [offset(5), flip(), shift()],
+ });
+
+ const context = data.context;
+
+ const hover = useHover(context, { move: false, restMs: delay, enabled: !disabled });
+ const focus = useFocus(context);
+ const dismiss = useDismiss(context);
+ const role = useRole(context, { role: 'tooltip' });
+
+ const interactions = useInteractions([hover, focus, dismiss, role]);
+
+ return React.useMemo(
+ () => ({
+ open,
+ setOpen,
+ ...interactions,
+ ...data,
+ }),
+ [open, setOpen, interactions, data]
+ );
+}
+
+type TooltipState = ReturnType
;
+
+export const TooltipAnchor = React.forwardRef<
+ HTMLElement,
+ React.HTMLProps & {
+ state: TooltipState;
+ asChild?: boolean;
+ }
+>(function TooltipAnchor({ children, state, asChild = false, ...props }, propRef) {
+ const childrenRef = (children as any).ref;
+ const ref = React.useMemo(
+ () => mergeRefs([state.reference, propRef, childrenRef]),
+ [state.reference, propRef, childrenRef]
+ );
+
+ // `asChild` allows the user to pass any element as the anchor
+ if (asChild && React.isValidElement(children)) {
+ return React.cloneElement(
+ children,
+ state.getReferenceProps({ ref, ...props, ...children.props })
+ );
+ }
+
+ return (
+
+ );
+});
+
+export const FloatingTooltip = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLProps & { state: TooltipState }
+>(function Tooltip({ state, ...props }, propRef) {
+ const ref = React.useMemo(() => mergeRefs([state.floating, propRef]), [state.floating, propRef]);
+
+ return (
+
+ {state.open && (
+
+ )}
+
+ );
+});
diff --git a/frontend/app/components/ui/Tooltip/Tooltip.tsx b/frontend/app/components/ui/Tooltip/Tooltip.tsx
index 6a14ce3f7..777f52d9a 100644
--- a/frontend/app/components/ui/Tooltip/Tooltip.tsx
+++ b/frontend/app/components/ui/Tooltip/Tooltip.tsx
@@ -1,51 +1,31 @@
import React from 'react';
-import { Popup } from 'UI';
+import { useTooltipState, TooltipAnchor, FloatingTooltip } from './FloatingTooltip';
+import type { Placement } from '@floating-ui/react-dom-interactions';
+import cn from 'classnames';
interface Props {
- timeout: number
- position: string
- tooltip: string
- trigger: React.ReactNode
+ title?: any;
+ children: any;
+ disabled?: boolean;
+ open?: boolean;
+ placement?: Placement;
+ className?: string;
+ delay?: number;
+}
+function Tooltip(props: Props) {
+ const { title, disabled = false, open = false, placement, className = '', delay = 500 } = props;
+ const state = useTooltipState({ disabled: disabled, placement, delay });
+ return (
+ <>
+ {props.children}
+
+ {title}
+
+ >
+ );
}
-export default class Tooltip extends React.PureComponent {
- static defaultProps = {
- timeout: 500,
- }
- state = {
- open: false,
- }
- mouseOver = false
- onMouseEnter = () => {
- this.mouseOver = true;
- setTimeout(() => {
- if (this.mouseOver) this.setState({ open: true });
- }, this.props.timeout)
- }
- onMouseLeave = () => {
- this.mouseOver = false;
- this.setState({
- open: false,
- });
- }
-
- render() {
- const { trigger, tooltip, position } = this.props;
- const { open } = this.state;
- return (
-
-
- { trigger }
-
-
- );
- }
-}
\ No newline at end of file
+export default Tooltip;
diff --git a/frontend/app/components/ui/Tooltip/Tooltip.tsx_ b/frontend/app/components/ui/Tooltip/Tooltip.tsx_
new file mode 100644
index 000000000..04bd3424c
--- /dev/null
+++ b/frontend/app/components/ui/Tooltip/Tooltip.tsx_
@@ -0,0 +1,47 @@
+import React from 'react';
+import { Popup } from 'UI';
+import { useTooltipState, TooltipAnchor, FloatingTooltip } from './FloatingTooltip';
+
+interface Props {
+ timeout: number;
+ position: string;
+ tooltip: string;
+ trigger: React.ReactNode;
+}
+
+export default class Tooltip extends React.PureComponent {
+ static defaultProps = {
+ timeout: 500,
+ };
+ state = {
+ open: false,
+ };
+ mouseOver = false;
+ onMouseEnter = () => {
+ this.mouseOver = true;
+ setTimeout(() => {
+ if (this.mouseOver) this.setState({ open: true });
+ }, this.props.timeout);
+ };
+ onMouseLeave = () => {
+ this.mouseOver = false;
+ this.setState({
+ open: false,
+ });
+ };
+
+ render() {
+ const { trigger, tooltip, position } = this.props;
+ const { open } = this.state;
+ return (
+
+
+ {trigger}
+
+
+ );
+ }
+}
diff --git a/frontend/app/components/ui/index.js b/frontend/app/components/ui/index.js
index 9000531a6..5c8c7813c 100644
--- a/frontend/app/components/ui/index.js
+++ b/frontend/app/components/ui/index.js
@@ -56,4 +56,5 @@ export { default as Toggler } from './Toggler';
export { default as Input } from './Input';
export { default as Form } from './Form';
export { default as Modal } from './Modal';
-export { default as Message } from './Message';
\ No newline at end of file
+export { default as Message } from './Message';
+export { default as Popover } from './Popover';
diff --git a/frontend/app/constants/zindex.ts b/frontend/app/constants/zindex.ts
index f5026ea85..f70b77d56 100644
--- a/frontend/app/constants/zindex.ts
+++ b/frontend/app/constants/zindex.ts
@@ -2,11 +2,12 @@ export const INDEXES = {
PLAYER_REQUEST_WINDOW: 10,
BUG_REPORT_PICKER: 19,
BUG_REPORT: 20,
- POPUP_GUIDE_BG: 99998,
- POPUP_GUIDE_BTN: 99999,
-}
+ POPUP_GUIDE_BG: 99997,
+ POPUP_GUIDE_BTN: 99998,
+ TOOLTIP: 99999,
+};
export const getHighest = () => {
- const allIndexes = Object.values(INDEXES)
- return allIndexes[allIndexes.length - 1] + 1
-}
+ const allIndexes = Object.values(INDEXES);
+ return allIndexes[allIndexes.length - 1] + 1;
+};
diff --git a/frontend/app/styles/general.css b/frontend/app/styles/general.css
index 8ddb1f741..84f327edd 100644
--- a/frontend/app/styles/general.css
+++ b/frontend/app/styles/general.css
@@ -334,4 +334,17 @@ p {
#ccc 2px,
#ccc 4px
);
+}
+
+.animate-fade {
+ animation: fade 0.1s cubic-bezier(0.4, 0, 0.6, 1);
+}
+
+@keyframes fade {
+ 100% {
+ opacity: 1;
+ }
+ 0% {
+ opacity: 0;
+ }
}
\ No newline at end of file
diff --git a/frontend/package.json b/frontend/package.json
index 8c41d5409..af78fbdaf 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -17,6 +17,7 @@
"postinstall": "yarn gen:icons && yarn gen:colors"
},
"dependencies": {
+ "@floating-ui/react-dom-interactions": "^0.10.3",
"@sentry/browser": "^5.21.1",
"@svg-maps/world": "^1.0.1",
"@svgr/webpack": "^6.2.1",
@@ -51,6 +52,7 @@
"react-highlight": "^0.14.0",
"react-json-view": "^1.21.3",
"react-lazyload": "^3.2.0",
+ "react-merge-refs": "^2.0.1",
"react-redux": "^5.1.2",
"react-router": "^5.3.3",
"react-router-dom": "^5.3.3",