Skip to content

Commit

Permalink
feat(refactor): Menu refresh with mouse-based position (#1945)
Browse files Browse the repository at this point in the history
  • Loading branch information
Ross Bulat authored Feb 13, 2024
1 parent b168bdd commit bf856d0
Show file tree
Hide file tree
Showing 9 changed files with 156 additions and 161 deletions.
22 changes: 8 additions & 14 deletions src/canvas/PoolMembers/Lists/Member.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
faUnlockAlt,
} from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import type { MouseEvent as ReactMouseEvent } from 'react';
import { useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { useMenu } from 'contexts/Menu';
Expand All @@ -16,18 +17,14 @@ import { useList } from 'library/List/context';
import { Identity } from 'library/ListItem/Labels/Identity';
import { PoolMemberBonded } from 'library/ListItem/Labels/PoolMemberBonded';
import { Select } from 'library/ListItem/Labels/Select';
import {
Labels,
MenuPosition,
Separator,
Wrapper,
} from 'library/ListItem/Wrappers';
import { Labels, Separator, Wrapper } from 'library/ListItem/Wrappers';
import type { AnyJson } from 'types';
import { usePrompt } from 'contexts/Prompt';
import { UnbondMember } from '../Prompts/UnbondMember';
import { WithdrawMember } from '../Prompts/WithdrawMember';
import { motion } from 'framer-motion';
import { useApi } from 'contexts/Api';
import { MenuList } from 'library/Menu/List';

export const Member = ({
who,
Expand All @@ -42,8 +39,8 @@ export const Member = ({
const { activeEra } = useApi();
const { meta } = usePoolMembers();
const { selectActive } = useList();
const { openMenu, open } = useMenu();
const { openPromptWith } = usePrompt();
const { setMenuPosition, setMenuItems, open } = useMenu();
const { activePool, isOwner, isBouncer } = useActivePool();

// Ref for the member container.
Expand Down Expand Up @@ -109,12 +106,10 @@ export const Member = ({
}
}

// configure floating menu
const posRef = useRef(null);
const toggleMenu = () => {
// Handler for opening menu.
const toggleMenu = (ev: ReactMouseEvent<HTMLButtonElement, MouseEvent>) => {
if (!open) {
setMenuItems(menuItems);
setMenuPosition(posRef);
openMenu(ev, <MenuList items={menuItems} />);
}
};

Expand All @@ -135,7 +130,6 @@ export const Member = ({
>
<Wrapper className="member">
<div className="inner canvas">
<MenuPosition ref={posRef} />
<div className="row top">
{selectActive && <Select item={{ address: who }} />}
<Identity address={who} />
Expand All @@ -146,7 +140,7 @@ export const Member = ({
type="button"
className="label"
disabled={!member}
onClick={() => toggleMenu()}
onClick={(ev) => toggleMenu(ev)}
>
<FontAwesomeIcon icon={faBars} />
</button>
Expand Down
15 changes: 7 additions & 8 deletions src/contexts/Menu/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,12 @@
import type { MenuContextInterface } from './types';

export const defaultMenuContext: MenuContextInterface = {
openMenu: () => {},
closeMenu: () => {},
setMenuPosition: (r) => {},
checkMenuPosition: (r) => {},
setMenuItems: (items) => {},
open: 0,
show: 0,
open: false,
show: false,
inner: null,
position: [0, 0],
items: [],
openMenu: (ev, newInner) => {},
closeMenu: () => {},
setMenuInner: (newInner) => {},
checkMenuPosition: (menuRef) => {},
};
97 changes: 50 additions & 47 deletions src/contexts/Menu/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,92 +4,95 @@
import type { ReactNode, RefObject } from 'react';
import { createContext, useContext, useState } from 'react';
import { defaultMenuContext } from './defaults';
import type { MenuContextInterface, MenuItem } from './types';
import type { MenuContextInterface, MenuMouseEvent } from './types';

export const MenuContext =
createContext<MenuContextInterface>(defaultMenuContext);

export const useMenu = () => useContext(MenuContext);

export const MenuProvider = ({ children }: { children: ReactNode }) => {
const [open, setOpen] = useState<number>(0);
const [show, setShow] = useState<number>(0);
const [items, setItems] = useState<MenuItem[]>([]);
// Whether the menu is currently open. This initiates menu state but does not reflect whether the
// menu is being displayed.
const [open, setOpen] = useState<boolean>(false);

// Whether the menu is currently showing.
const [show, setShow] = useState<boolean>(false);

// The components to be displayed in the menu.
const [inner, setInner] = useState<ReactNode>(null);

// The menu position coordinates.
const [position, setPosition] = useState<[number, number]>([0, 0]);

const openMenu = () => {
// Padding from the window edge.
const DocumentPadding = 20;

// Sets the menu position and opens it. Only succeeds if the menu has been instantiated and is not
// currently open.
const openMenu = (ev: MenuMouseEvent, newInner?: ReactNode) => {
if (open) {
return;
}
setOpen(1);
const bodyRect = document.body.getBoundingClientRect();
const x = ev.clientX - bodyRect.left;
const y = ev.clientY - bodyRect.top;

if (newInner) {
setInner(newInner);
}

setPosition([x, y]);
setOpen(true);
};

// Hides the menu and closes.
const closeMenu = () => {
setShow(0);
setTimeout(() => {
setOpen(0);
}, 100);
setShow(false);
setOpen(false);
};

const setMenuPosition = (ref: RefObject<HTMLDivElement>) => {
if (open || !ref?.current) {
return;
}
const bodyRect = document.body.getBoundingClientRect();
const elemRect = ref.current.getBoundingClientRect();

const x = elemRect.left - bodyRect.left;
const y = elemRect.top - bodyRect.top;

setPosition([x, y]);
openMenu();
// Sets the inner JSX of the menu.
const setMenuInner = (newInner: ReactNode) => {
setInner(newInner);
};

// Adjusts menu position and shows the menu.
const checkMenuPosition = (ref: RefObject<HTMLDivElement>) => {
if (!ref?.current) {
return;
}

// Adjust menu position if it is leaking out of the window, otherwise keep it at the current
// position.
const bodyRect = document.body.getBoundingClientRect();
const menuRect = ref.current.getBoundingClientRect();
const hiddenRight = menuRect.right > bodyRect.right;
const hiddenBottom = menuRect.bottom > bodyRect.bottom;

let x = menuRect.left - bodyRect.left;
let y = menuRect.top - bodyRect.top;
const right = menuRect.right;
const bottom = menuRect.bottom;

// small offset from menu start
y -= 10;
const x = hiddenRight
? window.innerWidth - menuRect.width - DocumentPadding
: position[0];

const documentPadding = 20;
const y = hiddenBottom
? window.innerHeight - menuRect.height - DocumentPadding
: position[1];

if (right > bodyRect.right) {
x = bodyRect.right - ref.current.offsetWidth - documentPadding;
}
if (bottom > bodyRect.bottom) {
y = bodyRect.bottom - ref.current.offsetHeight - documentPadding;
}
setPosition([x, y]);
setShow(1);
};

const setMenuItems = (_items: MenuItem[]) => {
setItems(_items);
setShow(true);
};

return (
<MenuContext.Provider
value={{
openMenu,
closeMenu,
setMenuPosition,
checkMenuPosition,
setMenuItems,
open,
show,
inner,
position,
items,
closeMenu,
openMenu,
setMenuInner,
checkMenuPosition,
}}
>
{children}
Expand Down
23 changes: 15 additions & 8 deletions src/contexts/Menu/types.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,29 @@
// Copyright 2023 @paritytech/polkadot-staking-dashboard authors & contributors
// SPDX-License-Identifier: GPL-3.0-only

import type { ReactNode, RefObject } from 'react';
import type {
ReactNode,
RefObject,
MouseEvent as ReactMouseEvent,
} from 'react';

export interface MenuContextInterface {
openMenu: () => void;
open: boolean;
show: boolean;
inner: ReactNode | null;
position: [number, number];
openMenu: (ev: MenuMouseEvent, newInner?: ReactNode) => void;
closeMenu: () => void;
setMenuPosition: (ref: RefObject<HTMLDivElement>) => void;
setMenuInner: (items: ReactNode) => void;
checkMenuPosition: (ref: RefObject<HTMLDivElement>) => void;
setMenuItems: (items: MenuItem[]) => void;
open: number;
show: number;
position: [number, number];
items: MenuItem[];
}

export interface MenuItem {
icon: ReactNode;
title: string;
cb: () => void;
}

export type MenuMouseEvent =
| MouseEvent
| ReactMouseEvent<HTMLButtonElement, MouseEvent>;
9 changes: 0 additions & 9 deletions src/library/ListItem/Wrappers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -289,15 +289,6 @@ export const Separator = styled.div`
opacity: 0.7;
`;

export const MenuPosition = styled.div`
position: absolute;
top: -10px;
right: 10px;
width: 0;
height: 0;
opacity: 0;
`;

export const TooltipTrigger = styled.div`
z-index: 1;
width: 130%;
Expand Down
28 changes: 28 additions & 0 deletions src/library/Menu/List.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { MenuItem } from 'contexts/Menu/types';
import { ItemWrapper } from './Wrappers';
import { useMenu } from 'contexts/Menu';

export const MenuList = ({ items }: { items: MenuItem[] }) => {
const { closeMenu } = useMenu();

return (
<>
{items.map((item, i: number) => {
const { icon, title, cb } = item;

return (
<ItemWrapper
key={`menu_item_${i}`}
onClick={() => {
cb();
closeMenu();
}}
>
{icon}
<div className="title">{title}</div>
</ItemWrapper>
);
})}
</>
);
};
Loading

0 comments on commit bf856d0

Please sign in to comment.