Commit bf49b2b5 authored by tom's avatar tom

refactoring

parent 0b3fc463
import { Box } from '@chakra-ui/react'; import React from 'react'; import { Box } from '@chakra-ui/react'; import React from 'react';
import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
import PrivateAddressTags from 'ui/privateTags/PrivateAddressTags'; import PrivateAddressTags from 'ui/privateTags/PrivateAddressTags';
import PrivateTransactionTags from 'ui/privateTags/PrivateTransactionTags'; import PrivateTransactionTags from 'ui/privateTags/PrivateTransactionTags';
import Page from 'ui/shared/Page'; import Page from 'ui/shared/Page';
import PageHeader from 'ui/shared/PageHeader'; import PageHeader from 'ui/shared/PageHeader';
import type { RoutedTab } from 'ui/shared/RoutedTabs'; import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
import RoutedTabs from 'ui/shared/RoutedTabs';
const TABS: Array<RoutedTab> = [ const TABS: Array<RoutedTab> = [
{ routeName: 'private_tags_address', title: 'Address', component: <PrivateAddressTags/> }, { routeName: 'private_tags_address', title: 'Address', component: <PrivateAddressTags/> },
......
import React from 'react'; import React from 'react';
import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
import Page from 'ui/shared/Page'; import Page from 'ui/shared/Page';
import PageHeader from 'ui/shared/PageHeader'; import PageHeader from 'ui/shared/PageHeader';
import type { RoutedTab } from 'ui/shared/RoutedTabs'; import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
import RoutedTabs from 'ui/shared/RoutedTabs';
import TxDetails from 'ui/tx/TxDetails'; import TxDetails from 'ui/tx/TxDetails';
import TxInternals from 'ui/tx/TxInternals'; import TxInternals from 'ui/tx/TxInternals';
import TxLogs from 'ui/tx/TxLogs'; import TxLogs from 'ui/tx/TxLogs';
......
import {
Tab,
Tabs,
TabList,
TabPanel,
TabPanels,
Button,
Popover,
PopoverTrigger,
PopoverContent,
PopoverBody,
useDisclosure,
} from '@chakra-ui/react';
import type { StyleProps } from '@chakra-ui/styled-system';
import _debounce from 'lodash/debounce';
import { useRouter } from 'next/router';
import React from 'react';
import { middot } from 'lib/html-entities';
import { link } from 'lib/link/link';
import type { RouteName } from 'lib/link/routes';
export interface RoutedTab {
// for simplicity we use routeName as an id
// if we migrate to non-Next.js router that should be revised
// id: string;
routeName: RouteName | null;
title: string;
component: React.ReactNode;
}
const menuButton: RoutedTab = {
routeName: null,
title: `${ middot }${ middot }${ middot }`,
component: null,
};
const hiddenItemStyles: StyleProps = {
position: 'absolute',
top: '-9999px',
left: '-9999px',
visibility: 'hidden',
};
interface Props {
tabs: Array<RoutedTab>;
defaultActiveTab: RoutedTab['routeName'];
}
const RoutedTabs = ({ tabs, defaultActiveTab }: Props) => {
const defaultIndex = tabs.findIndex(({ routeName }) => routeName === defaultActiveTab);
const [ tabsNum, setTabsNum ] = React.useState(tabs.length);
const [ activeTab, setActiveTab ] = React.useState<number>(defaultIndex);
const { isOpen: isMenuOpen, onToggle: onMenuToggle } = useDisclosure();
const [ tabsRefs, setTabsRefs ] = React.useState<Array<React.RefObject<HTMLButtonElement>>>([]);
const menuRef = React.useRef<HTMLDivElement>(null);
const router = useRouter();
const displayedTabs = (() => {
return [ ...tabs, menuButton ];
})();
React.useEffect(() => {
setTabsRefs(displayedTabs.map((_, index) => tabsRefs[index] || React.createRef()));
// imitate componentDidMount
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const calculateCut = React.useCallback(() => {
const listWidth = menuRef.current?.getBoundingClientRect().width;
const tabWidths = tabsRefs.map((tab) => tab.current?.getBoundingClientRect().width);
const menuWidth = tabWidths.at(-1);
if (!listWidth || !menuWidth) {
return tabs.length;
}
const { visibleTabNum } = tabWidths.slice(0, -1).reduce((result, item, index) => {
if (!item) {
return result;
}
if (result.accWidth + item <= listWidth - menuWidth) {
return { visibleTabNum: result.visibleTabNum + 1, accWidth: result.accWidth + item };
}
if (result.accWidth + item <= listWidth && index === tabWidths.length - 2) {
return { visibleTabNum: result.visibleTabNum + 1, accWidth: result.accWidth + item };
}
return result;
}, { visibleTabNum: 0, accWidth: 0 });
return visibleTabNum;
}, [ tabs.length, tabsRefs ]);
React.useEffect(() => {
if (tabsRefs.length > 0) {
setTabsNum(calculateCut());
}
}, [ calculateCut, tabsRefs ]);
React.useEffect(() => {
const resizeHandler = _debounce(() => {
setTabsNum(calculateCut());
}, 100);
const resizeObserver = new ResizeObserver(resizeHandler);
resizeObserver.observe(document.body);
return function cleanup() {
resizeObserver.unobserve(document.body);
};
}, [ calculateCut ]);
const handleTabChange = React.useCallback((index: number) => {
const nextTab = displayedTabs[index];
if (nextTab.routeName) {
const newUrl = link(nextTab.routeName, router.query);
router.push(newUrl, undefined, { shallow: true });
}
setActiveTab(index);
}, [ displayedTabs, router ]);
const handleItemInMenuClick = React.useCallback((event: React.MouseEvent<HTMLButtonElement>) => {
const tabIndex = (event.target as HTMLButtonElement).getAttribute('data-index');
if (tabIndex) {
handleTabChange(tabsNum + Number(tabIndex));
}
}, [ handleTabChange, tabsNum ]);
return (
<Tabs variant="soft-rounded" colorScheme="blue" isLazy onChange={ handleTabChange } index={ activeTab }>
<TabList marginBottom={{ base: 6, lg: 8 }} flexWrap="nowrap" whiteSpace="nowrap" ref={ menuRef }>
{ displayedTabs.map((tab, index) => {
if (!tab.routeName) {
return (
<Popover isLazy placement="bottom-end" key="more">
<PopoverTrigger>
<Button
variant="subtle"
onClick={ onMenuToggle }
isActive={ isMenuOpen || activeTab >= tabsNum }
ref={ tabsRefs[index] }
{ ...(tabsNum < tabs.length ? {} : hiddenItemStyles) }
>
{ menuButton.title }
</Button>
</PopoverTrigger>
<PopoverContent w="auto">
<PopoverBody display="flex" flexDir="column">
{ displayedTabs.slice(tabsNum, -1).map((tab, index) => (
<Button
key={ tab.routeName }
variant="subtle"
onClick={ handleItemInMenuClick }
isActive={ displayedTabs[activeTab].routeName === tab.routeName }
justifyContent="left"
data-index={ index }
>
{ tab.title }
</Button>
)) }
</PopoverBody>
</PopoverContent>
</Popover>
);
}
return (
<Tab
key={ tab.routeName }
ref={ tabsRefs[index] }
{ ...(index < tabsNum ? {} : hiddenItemStyles) }
>
{ tab.title }
</Tab>
);
}) }
</TabList>
<TabPanels>
{ displayedTabs.map((tab) => <TabPanel padding={ 0 } key={ tab.routeName }>{ tab.component }</TabPanel>) }
</TabPanels>
</Tabs>
);
};
export default React.memo(RoutedTabs);
import {
Tab,
Tabs,
TabList,
TabPanel,
TabPanels,
} from '@chakra-ui/react';
import type { StyleProps } from '@chakra-ui/styled-system';
import { useRouter } from 'next/router';
import React from 'react';
import type { RoutedTab } from './types';
import { link } from 'lib/link/link';
import RoutedTabsMenu from './RoutedTabsMenu';
import useAdaptiveTabs from './useAdaptiveTabs';
const hiddenItemStyles: StyleProps = {
position: 'absolute',
top: '-9999px',
left: '-9999px',
visibility: 'hidden',
};
interface Props {
tabs: Array<RoutedTab>;
defaultActiveTab: RoutedTab['routeName'];
}
const RoutedTabs = ({ tabs, defaultActiveTab }: Props) => {
const defaultIndex = tabs.findIndex(({ routeName }) => routeName === defaultActiveTab);
const [ activeTab, setActiveTab ] = React.useState<number>(defaultIndex);
const { tabsCut, tabsWithMenu, tabsRefs, listRef } = useAdaptiveTabs(tabs);
const router = useRouter();
const handleTabChange = React.useCallback((index: number) => {
const nextTab = tabs[index];
if (nextTab.routeName) {
const newUrl = link(nextTab.routeName, router.query);
router.push(newUrl, undefined, { shallow: true });
}
setActiveTab(index);
}, [ tabs, router ]);
const handleItemInMenuClick = React.useCallback((event: React.MouseEvent<HTMLButtonElement>) => {
const tabIndex = (event.target as HTMLButtonElement).getAttribute('data-index');
if (tabIndex) {
handleTabChange(tabsCut + Number(tabIndex));
}
}, [ handleTabChange, tabsCut ]);
return (
<Tabs variant="soft-rounded" colorScheme="blue" isLazy onChange={ handleTabChange } index={ activeTab }>
<TabList marginBottom={{ base: 6, lg: 8 }} flexWrap="nowrap" whiteSpace="nowrap" ref={ listRef }>
{ tabsWithMenu.map((tab, index) => {
if (!tab.routeName) {
return (
<RoutedTabsMenu
key="menu"
tabs={ tabs }
activeTab={ tabs[activeTab] }
tabsCut={ tabsCut }
isActive={ activeTab >= tabsCut }
styles={ tabsCut < tabs.length ? {} : hiddenItemStyles }
onItemClick={ handleItemInMenuClick }
buttonRef={ tabsRefs[index] }
/>
);
}
return (
<Tab
key={ tab.routeName }
ref={ tabsRefs[index] }
{ ...(index < tabsCut ? {} : hiddenItemStyles) }
>
{ tab.title }
</Tab>
);
}) }
</TabList>
<TabPanels>
{ tabsWithMenu.map((tab) => <TabPanel padding={ 0 } key={ tab.routeName }>{ tab.component }</TabPanel>) }
</TabPanels>
</Tabs>
);
};
export default React.memo(RoutedTabs);
import { Popover,
PopoverTrigger,
PopoverContent,
PopoverBody,
Button,
useDisclosure,
} from '@chakra-ui/react';
import type { StyleProps } from '@chakra-ui/styled-system';
import React from 'react';
import type { MenuButton, RoutedTab } from './types';
import { menuButton } from './utils';
interface Props {
tabs: Array<RoutedTab | MenuButton>;
activeTab: RoutedTab;
tabsCut: number;
isActive: boolean;
styles?: StyleProps;
onItemClick: (event: React.MouseEvent<HTMLButtonElement>) => void;
buttonRef: React.RefObject<HTMLButtonElement>;
}
const RoutedTabsMenu = ({ tabs, tabsCut, isActive, styles, onItemClick, buttonRef, activeTab }: Props) => {
const { isOpen, onToggle } = useDisclosure();
return (
<Popover isLazy placement="bottom-end" key="more">
<PopoverTrigger>
<Button
variant="subtle"
onClick={ onToggle }
isActive={ isOpen || isActive }
ref={ buttonRef }
{ ...styles }
>
{ menuButton.title }
</Button>
</PopoverTrigger>
<PopoverContent w="auto">
<PopoverBody display="flex" flexDir="column">
{ tabs.slice(tabsCut).map((tab, index) => (
<Button
key={ tab.routeName }
variant="subtle"
onClick={ onItemClick }
isActive={ activeTab.routeName === tab.routeName }
justifyContent="left"
data-index={ index }
>
{ tab.title }
</Button>
)) }
</PopoverBody>
</PopoverContent>
</Popover>
);
};
export default React.memo(RoutedTabsMenu);
import type { RouteName } from 'lib/link/routes';
export interface RoutedTab {
// for simplicity we use routeName as an id
// if we migrate to non-Next.js router that should be revised
// id: string;
routeName: RouteName | null;
title: string;
component: React.ReactNode;
}
export interface MenuButton {
routeName: null;
title: string;
component: null;
}
import _debounce from 'lodash/debounce';
import React from 'react';
import type { RoutedTab } from './types';
import { menuButton } from './utils';
export default function useAdaptiveTabs(tabs: Array<RoutedTab>) {
const [ tabsCut, setTabsCut ] = React.useState(tabs.length);
const [ tabsRefs, setTabsRefs ] = React.useState<Array<React.RefObject<HTMLButtonElement>>>([]);
const listRef = React.useRef<HTMLDivElement>(null);
const calculateCut = React.useCallback(() => {
const listWidth = listRef.current?.getBoundingClientRect().width;
const tabWidths = tabsRefs.map((tab) => tab.current?.getBoundingClientRect().width);
const menuWidth = tabWidths.at(-1);
if (!listWidth || !menuWidth) {
return tabs.length;
}
const { visibleNum } = tabWidths.slice(0, -1).reduce((result, item, index) => {
if (!item) {
return result;
}
if (result.accWidth + item <= listWidth - menuWidth) {
return { visibleNum: result.visibleNum + 1, accWidth: result.accWidth + item };
}
if (result.accWidth + item <= listWidth && index === tabWidths.length - 2) {
return { visibleNum: result.visibleNum + 1, accWidth: result.accWidth + item };
}
return result;
}, { visibleNum: 0, accWidth: 0 });
return visibleNum;
}, [ tabs.length, tabsRefs ]);
const tabsWithMenu = React.useMemo(() => {
return [ ...tabs, menuButton ];
}, [ tabs ]);
React.useEffect(() => {
setTabsRefs(tabsWithMenu.map((_, index) => tabsRefs[index] || React.createRef()));
// imitate componentDidMount
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
React.useEffect(() => {
if (tabsRefs.length > 0) {
setTabsCut(calculateCut());
}
}, [ calculateCut, tabsRefs ]);
React.useEffect(() => {
const resizeHandler = _debounce(() => {
setTabsCut(calculateCut());
}, 100);
const resizeObserver = new ResizeObserver(resizeHandler);
resizeObserver.observe(document.body);
return function cleanup() {
resizeObserver.unobserve(document.body);
};
}, [ calculateCut ]);
return React.useMemo(() => {
return {
tabsCut,
tabsWithMenu,
tabsRefs,
listRef,
};
}, [ tabsWithMenu, tabsCut, tabsRefs, listRef ]);
}
import type { MenuButton } from './types';
import { middot } from 'lib/html-entities';
export const menuButton: MenuButton = {
routeName: null,
title: `${ middot }${ middot }${ middot }`,
component: null,
};
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment