mirror of
https://github.com/luxfi/explore.git
synced 2026-09-24 00:28:18 +00:00
+10
-4
@@ -6,15 +6,15 @@ import type { RouteName } from './routes';
|
||||
|
||||
const PATH_PARAM_REGEXP = /\/\[(\w+)\]/g;
|
||||
|
||||
export function link(routeName: RouteName, urlParams?: Record<string, string | undefined>, queryParams?: Record<string, string>): string {
|
||||
export function link(routeName: RouteName, urlParams?: Record<string, Array<string> | string | undefined>, queryParams?: Record<string, string>): string {
|
||||
const route = ROUTES[routeName];
|
||||
if (!route) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const network = findNetwork({
|
||||
network_type: urlParams?.network_type || '',
|
||||
network_sub_type: urlParams?.network_sub_type,
|
||||
network_type: typeof urlParams?.network_type === 'string' ? urlParams?.network_type : '',
|
||||
network_sub_type: typeof urlParams?.network_sub_type === 'string' ? urlParams?.network_sub_type : undefined,
|
||||
});
|
||||
|
||||
const path = route.pattern.replace(PATH_PARAM_REGEXP, (_, paramName: string) => {
|
||||
@@ -22,7 +22,13 @@ export function link(routeName: RouteName, urlParams?: Record<string, string | u
|
||||
return '';
|
||||
}
|
||||
|
||||
const paramValue = urlParams?.[paramName];
|
||||
let paramValue = urlParams?.[paramName];
|
||||
if (Array.isArray(paramValue)) {
|
||||
// FIXME we don't have yet params as array, but typescript says that we could
|
||||
// dun't know how to manage it, fix me if you find an issue
|
||||
paramValue = paramValue.join(',');
|
||||
}
|
||||
|
||||
return paramValue ? `/${ paramValue }` : '';
|
||||
});
|
||||
|
||||
|
||||
@@ -11,10 +11,6 @@ export default function useLink() {
|
||||
const networkSubType = router.query.network_sub_type;
|
||||
|
||||
return React.useCallback((...args: LinkBuilderParams) => {
|
||||
if (typeof networkType !== 'string' || typeof networkSubType !== 'string') {
|
||||
return '';
|
||||
}
|
||||
|
||||
return link(args[0], { network_type: networkType, network_sub_type: networkSubType, ...args[1] }, args[2]);
|
||||
}, [ networkType, networkSubType ]);
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ const AddressTagsPage: NextPage<Props> = ({ pageParams }: Props) => {
|
||||
return (
|
||||
<>
|
||||
<Head><title>{ title }</title></Head>
|
||||
<PrivateTags tab="address"/>
|
||||
<PrivateTags tab="private_tags_address"/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -19,7 +19,7 @@ const TransactionTagsPage: NextPage<Props> = ({ pageParams }: Props) => {
|
||||
return (
|
||||
<>
|
||||
<Head><title>{ title }</title></Head>
|
||||
<PrivateTags tab="transaction"/>
|
||||
<PrivateTags tab="private_tags_tx"/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -11,7 +11,7 @@ type Props = {
|
||||
|
||||
const TransactionPage: NextPage<Props> = ({ pageParams }: Props) => {
|
||||
return (
|
||||
<TransactionNextPage tab="details" pageParams={ pageParams }/>
|
||||
<TransactionNextPage tab="tx_index" pageParams={ pageParams }/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ type Props = {
|
||||
}
|
||||
|
||||
const TransactionPage: NextPage<Props> = ({ pageParams }: Props) => {
|
||||
return <TransactionNextPage pageParams={ pageParams } tab="internal_txn"/>;
|
||||
return <TransactionNextPage pageParams={ pageParams } tab="tx_internal"/>;
|
||||
};
|
||||
|
||||
export default TransactionPage;
|
||||
|
||||
@@ -10,7 +10,7 @@ type Props = {
|
||||
}
|
||||
|
||||
const TransactionPage: NextPage<Props> = ({ pageParams }: Props) => {
|
||||
return <TransactionNextPage pageParams={ pageParams } tab="logs"/>;
|
||||
return <TransactionNextPage pageParams={ pageParams } tab="tx_logs"/>;
|
||||
};
|
||||
|
||||
export default TransactionPage;
|
||||
|
||||
@@ -10,7 +10,7 @@ type Props = {
|
||||
}
|
||||
|
||||
const TransactionPage: NextPage<Props> = ({ pageParams }: Props) => {
|
||||
return <TransactionNextPage pageParams={ pageParams } tab="raw_trace"/>;
|
||||
return <TransactionNextPage pageParams={ pageParams } tab="tx_raw_trace"/>;
|
||||
};
|
||||
|
||||
export default TransactionPage;
|
||||
|
||||
@@ -10,7 +10,7 @@ type Props = {
|
||||
}
|
||||
|
||||
const TransactionPage: NextPage<Props> = ({ pageParams }: Props) => {
|
||||
return <TransactionNextPage pageParams={ pageParams } tab="state"/>;
|
||||
return <TransactionNextPage pageParams={ pageParams } tab="tx_state"/>;
|
||||
};
|
||||
|
||||
export default TransactionPage;
|
||||
|
||||
@@ -106,15 +106,39 @@ const variantSimple = defineStyle((props) => {
|
||||
};
|
||||
});
|
||||
|
||||
const variantSubtle = defineStyle((props) => {
|
||||
const { colorScheme: c } = props;
|
||||
|
||||
const activeBg = mode(`${ c }.50`, 'gray.800')(props);
|
||||
|
||||
return {
|
||||
bg: 'transparent',
|
||||
color: mode(`${ c }.700`, 'gray.400')(props),
|
||||
_active: {
|
||||
color: mode(`${ c }.700`, 'gray.50')(props),
|
||||
bg: mode(`${ c }.50`, 'gray.800')(props),
|
||||
},
|
||||
_hover: {
|
||||
color: `${ c }.400`,
|
||||
_active: {
|
||||
bg: props.isActive ? activeBg : 'transparent',
|
||||
color: mode(`${ c }.700`, 'gray.50')(props),
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const variants = {
|
||||
solid: variantSolid,
|
||||
outline: variantOutline,
|
||||
simple: variantSimple,
|
||||
subtle: variantSubtle,
|
||||
};
|
||||
|
||||
const baseStyle = defineStyle({
|
||||
fontWeight: 600,
|
||||
borderRadius: 'base',
|
||||
overflow: 'hidden',
|
||||
});
|
||||
|
||||
const sizes = {
|
||||
|
||||
@@ -7,6 +7,7 @@ const global = (props: StyleFunctionProps) => ({
|
||||
body: {
|
||||
bg: mode('white', 'black')(props),
|
||||
...getDefaultTransitionProps(),
|
||||
'-webkit-tap-highlight-color': 'transparent',
|
||||
},
|
||||
form: {
|
||||
w: '100%',
|
||||
|
||||
+1
-1
@@ -5,8 +5,8 @@ import type { AppItemOverview } from 'types/client/apps';
|
||||
|
||||
import { TEMPORARY_DEMO_APPS } from 'data/apps';
|
||||
import AppList from 'ui/apps/AppList';
|
||||
import FilterInput from 'ui/shared/FilterInput';
|
||||
import AppModal from 'ui/apps/AppModal';
|
||||
import FilterInput from 'ui/shared/FilterInput';
|
||||
|
||||
const defaultDisplayedApps = [ ...TEMPORARY_DEMO_APPS ]
|
||||
.sort((a, b) => a.title.localeCompare(b.title));
|
||||
|
||||
+10
-38
@@ -1,56 +1,28 @@
|
||||
import {
|
||||
Box,
|
||||
Tab,
|
||||
Tabs,
|
||||
TabList,
|
||||
TabPanel,
|
||||
TabPanels,
|
||||
} from '@chakra-ui/react';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { Box } from '@chakra-ui/react'; import React from 'react';
|
||||
|
||||
import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
|
||||
|
||||
import useLink from 'lib/link/useLink';
|
||||
import PrivateAddressTags from 'ui/privateTags/PrivateAddressTags';
|
||||
import PrivateTransactionTags from 'ui/privateTags/PrivateTransactionTags';
|
||||
import Page from 'ui/shared/Page';
|
||||
import PageHeader from 'ui/shared/PageHeader';
|
||||
import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
|
||||
|
||||
const TABS = [ 'address', 'transaction' ] as const;
|
||||
|
||||
type TabName = typeof TABS[number];
|
||||
const TABS: Array<RoutedTab> = [
|
||||
{ routeName: 'private_tags_address', title: 'Address', component: <PrivateAddressTags/> },
|
||||
{ routeName: 'private_tags_tx', title: 'Transaction', component: <PrivateTransactionTags/> },
|
||||
];
|
||||
|
||||
type Props = {
|
||||
tab: TabName;
|
||||
tab: RoutedTab['routeName'];
|
||||
}
|
||||
|
||||
const PrivateTags = ({ tab }: Props) => {
|
||||
const [ , setActiveTab ] = useState<TabName>(tab);
|
||||
|
||||
const link = useLink();
|
||||
|
||||
const onChangeTab = useCallback((index: number) => {
|
||||
setActiveTab(TABS[index]);
|
||||
const newUrl = link(TABS[index] === 'address' ? 'private_tags_address' : 'private_tags_tx');
|
||||
history.replaceState(history.state, '', newUrl);
|
||||
}, [ link ]);
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<Box h="100%">
|
||||
<PageHeader text="Private tags"/>
|
||||
<Tabs variant="soft-rounded" colorScheme="blue" isLazy onChange={ onChangeTab } defaultIndex={ TABS.indexOf(tab) }>
|
||||
<TabList marginBottom={{ base: 6, lg: 8 }}>
|
||||
<Tab>Address</Tab>
|
||||
<Tab>Transaction</Tab>
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
<TabPanel padding={ 0 }>
|
||||
<PrivateAddressTags/>
|
||||
</TabPanel>
|
||||
<TabPanel padding={ 0 }>
|
||||
<PrivateTransactionTags/>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
<RoutedTabs tabs={ TABS } defaultActiveTab={ tab }/>
|
||||
</Box>
|
||||
</Page>
|
||||
);
|
||||
|
||||
+11
-46
@@ -1,68 +1,33 @@
|
||||
import {
|
||||
Tab,
|
||||
Tabs,
|
||||
TabList,
|
||||
TabPanel,
|
||||
TabPanels,
|
||||
} from '@chakra-ui/react';
|
||||
import { useRouter } from 'next/router';
|
||||
import React from 'react';
|
||||
|
||||
import type { RouteName } from 'lib/link/routes';
|
||||
import useLink from 'lib/link/useLink';
|
||||
import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
|
||||
|
||||
import Page from 'ui/shared/Page';
|
||||
import PageHeader from 'ui/shared/PageHeader';
|
||||
import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
|
||||
import TxDetails from 'ui/tx/TxDetails';
|
||||
import TxInternals from 'ui/tx/TxInternals';
|
||||
import TxLogs from 'ui/tx/TxLogs';
|
||||
import TxRawTrace from 'ui/tx/TxRawTrace';
|
||||
import TxState from 'ui/tx/TxState';
|
||||
|
||||
interface Tab {
|
||||
type: 'details' | 'internal_txn' | 'logs' | 'raw_trace' | 'state';
|
||||
name: string;
|
||||
path?: string;
|
||||
component?: React.ReactNode;
|
||||
routeName: RouteName;
|
||||
}
|
||||
|
||||
const TABS: Array<Tab> = [
|
||||
{ type: 'details', routeName: 'tx_index', name: 'Details', component: <TxDetails/> },
|
||||
{ type: 'internal_txn', routeName: 'tx_internal', name: 'Internal txn', component: <TxInternals/> },
|
||||
{ type: 'logs', routeName: 'tx_logs', name: 'Logs', component: <TxLogs/> },
|
||||
{ type: 'state', routeName: 'tx_state', name: 'State', component: <TxState/> },
|
||||
{ type: 'raw_trace', routeName: 'tx_raw_trace', name: 'Raw trace', component: <TxRawTrace/> },
|
||||
const TABS: Array<RoutedTab> = [
|
||||
{ routeName: 'tx_index', title: 'Details', component: <TxDetails/> },
|
||||
{ routeName: 'tx_internal', title: 'Internal txn', component: <TxInternals/> },
|
||||
{ routeName: 'tx_logs', title: 'Logs', component: <TxLogs/> },
|
||||
{ routeName: 'tx_state', title: 'State', component: <TxState/> },
|
||||
{ routeName: 'tx_raw_trace', title: 'Raw trace', component: <TxRawTrace/> },
|
||||
];
|
||||
|
||||
export interface Props {
|
||||
tab: Tab['type'];
|
||||
tab: RoutedTab['routeName'];
|
||||
}
|
||||
|
||||
const TransactionPageContent = ({ tab }: Props) => {
|
||||
const [ , setActiveTab ] = React.useState<Tab['type']>(tab);
|
||||
const router = useRouter();
|
||||
const link = useLink();
|
||||
|
||||
const handleTabChange = React.useCallback((index: number) => {
|
||||
const nextTab = TABS[index];
|
||||
setActiveTab(nextTab.type);
|
||||
const newUrl = link(nextTab.routeName, { id: router.query.id as string });
|
||||
window.history.replaceState(history.state, '', newUrl);
|
||||
}, [ setActiveTab, link, router.query.id ]);
|
||||
|
||||
const defaultIndex = TABS.map(({ type }) => type).indexOf(tab);
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<PageHeader text="Transaction details"/>
|
||||
<Tabs variant="soft-rounded" colorScheme="blue" isLazy onChange={ handleTabChange } defaultIndex={ defaultIndex }>
|
||||
<TabList marginBottom={{ base: 6, lg: 8 }} flexWrap="wrap">
|
||||
{ TABS.map((tab) => <Tab key={ tab.type }>{ tab.name }</Tab>) }
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
{ TABS.map((tab) => <TabPanel padding={ 0 } key={ tab.type }>{ tab.component || tab.name }</TabPanel>) }
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
<RoutedTabs tabs={ TABS } defaultActiveTab={ tab }/>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
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 useIsMobile from 'lib/hooks/useIsMobile';
|
||||
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 isMobile = useIsMobile();
|
||||
|
||||
const [ activeTab, setActiveTab ] = React.useState<number>(defaultIndex);
|
||||
const { tabsCut, tabsList, tabsRefs, listRef } = useAdaptiveTabs(tabs, isMobile);
|
||||
|
||||
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 ]);
|
||||
|
||||
return (
|
||||
<Tabs variant="soft-rounded" colorScheme="blue" isLazy onChange={ handleTabChange } index={ activeTab }>
|
||||
<TabList
|
||||
marginBottom={{ base: 6, lg: 8 }}
|
||||
flexWrap="nowrap"
|
||||
whiteSpace="nowrap"
|
||||
ref={ listRef }
|
||||
overflowY="hidden"
|
||||
overflowX={ isMobile ? 'auto' : undefined }
|
||||
overscrollBehaviorX="contain"
|
||||
css={{
|
||||
'scroll-snap-type': 'x mandatory',
|
||||
// hide scrollbar
|
||||
'&::-webkit-scrollbar': { /* Chromiums */
|
||||
display: 'none',
|
||||
},
|
||||
'-ms-overflow-style': 'none', /* IE and Edge */
|
||||
'scrollbar-width': 'none', /* Firefox */
|
||||
}}
|
||||
>
|
||||
{ tabsList.map((tab, index) => {
|
||||
if (!tab.routeName) {
|
||||
return (
|
||||
<RoutedTabsMenu
|
||||
key="menu"
|
||||
tabs={ tabs }
|
||||
activeTab={ tabs[activeTab] }
|
||||
tabsCut={ tabsCut }
|
||||
isActive={ activeTab >= tabsCut }
|
||||
styles={ tabsCut < tabs.length ?
|
||||
// initially our cut is 0 and we don't want to show the menu button too
|
||||
// but we want to keep it in the tabs row so it won't collapse
|
||||
// that's why we only change opacity but not the position itself
|
||||
{ opacity: tabsCut === 0 ? 0 : 1 } :
|
||||
hiddenItemStyles
|
||||
}
|
||||
onItemClick={ handleTabChange }
|
||||
buttonRef={ tabsRefs[index] }
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Tab
|
||||
key={ tab.routeName }
|
||||
ref={ tabsRefs[index] }
|
||||
{ ...(index < tabsCut ? {} : hiddenItemStyles) }
|
||||
scrollSnapAlign="start"
|
||||
>
|
||||
{ tab.title }
|
||||
</Tab>
|
||||
);
|
||||
}) }
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
{ tabsList.map((tab) => <TabPanel padding={ 0 } key={ tab.routeName }>{ tab.component }</TabPanel>) }
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(RoutedTabs);
|
||||
@@ -0,0 +1,68 @@
|
||||
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: (index: number) => void;
|
||||
buttonRef: React.RefObject<HTMLButtonElement>;
|
||||
}
|
||||
|
||||
const RoutedTabsMenu = ({ tabs, tabsCut, isActive, styles, onItemClick, buttonRef, activeTab }: Props) => {
|
||||
const { isOpen, onClose, onOpen } = useDisclosure();
|
||||
|
||||
const handleItemClick = React.useCallback((event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
onClose();
|
||||
const tabIndex = (event.target as HTMLButtonElement).getAttribute('data-index');
|
||||
if (tabIndex) {
|
||||
onItemClick(tabsCut + Number(tabIndex));
|
||||
}
|
||||
}, [ onClose, onItemClick, tabsCut ]);
|
||||
|
||||
return (
|
||||
<Popover isLazy placement="bottom-end" key="more" isOpen={ isOpen } onClose={ onClose } onOpen={ onOpen } closeDelay={ 0 }>
|
||||
<PopoverTrigger>
|
||||
<Button
|
||||
variant="subtle"
|
||||
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={ handleItemClick }
|
||||
isActive={ activeTab.routeName === tab.routeName }
|
||||
justifyContent="left"
|
||||
data-index={ index }
|
||||
>
|
||||
{ tab.title }
|
||||
</Button>
|
||||
)) }
|
||||
</PopoverBody>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(RoutedTabsMenu);
|
||||
@@ -0,0 +1,16 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
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>, disabled?: boolean) {
|
||||
// to avoid flickering we set initial value to 0
|
||||
// so there will be no displayed tabs initially
|
||||
const [ tabsCut, setTabsCut ] = React.useState(disabled ? tabs.length : 0);
|
||||
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 tabsList = React.useMemo(() => {
|
||||
if (disabled) {
|
||||
return tabs;
|
||||
}
|
||||
|
||||
return [ ...tabs, menuButton ];
|
||||
}, [ tabs, disabled ]);
|
||||
|
||||
React.useEffect(() => {
|
||||
!disabled && setTabsRefs(tabsList.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(() => {
|
||||
if (tabsRefs.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const resizeHandler = _debounce(() => {
|
||||
setTabsCut(calculateCut());
|
||||
}, 100);
|
||||
const resizeObserver = new ResizeObserver(resizeHandler);
|
||||
|
||||
resizeObserver.observe(document.body);
|
||||
return function cleanup() {
|
||||
resizeObserver.unobserve(document.body);
|
||||
};
|
||||
}, [ calculateCut, tabsRefs.length ]);
|
||||
|
||||
return React.useMemo(() => {
|
||||
return {
|
||||
tabsCut,
|
||||
tabsList,
|
||||
tabsRefs,
|
||||
listRef,
|
||||
};
|
||||
}, [ tabsList, tabsCut, tabsRefs, listRef ]);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { MenuButton } from './types';
|
||||
|
||||
import { middot } from 'lib/html-entities';
|
||||
|
||||
export const menuButton: MenuButton = {
|
||||
routeName: null,
|
||||
title: `${ middot }${ middot }${ middot }`,
|
||||
component: null,
|
||||
};
|
||||
Reference in New Issue
Block a user