Commit f1374b45 authored by tom goriunov's avatar tom goriunov Committed by GitHub

fix adaptive tabs and footer layout (#1302)

* fix tab list resize issue

* footer layout adjustments for md screens

* fix ts
parent 5f937f79
import type { StyleProps, ThemingProps } from '@chakra-ui/react';
import { Box, Tab, TabList, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
import { useScrollDirection } from 'lib/contexts/scrollDirection';
import useIsMobile from 'lib/hooks/useIsMobile';
import useIsSticky from 'lib/hooks/useIsSticky';
import TabCounter from './TabCounter';
import TabsMenu from './TabsMenu';
import type { Props as TabsProps } from './TabsWithScroll';
import useAdaptiveTabs from './useAdaptiveTabs';
import useScrollToActiveTab from './useScrollToActiveTab';
import { menuButton } from './utils';
const hiddenItemStyles: StyleProps = {
position: 'absolute',
top: '-9999px',
left: '-9999px',
visibility: 'hidden',
};
interface Props extends TabsProps {
activeTabIndex: number;
onItemClick: (index: number) => void;
themeProps: ThemingProps<'Tabs'>;
}
const AdaptiveTabsList = (props: Props) => {
const scrollDirection = useScrollDirection();
const listBgColor = useColorModeValue('white', 'black');
const isMobile = useIsMobile();
const tabsList = React.useMemo(() => {
return [ ...props.tabs, menuButton ];
}, [ props.tabs ]);
const { tabsCut, tabsRefs, listRef, rightSlotRef } = useAdaptiveTabs(tabsList, isMobile);
const isSticky = useIsSticky(listRef, 5, props.stickyEnabled);
useScrollToActiveTab({ activeTabIndex: props.activeTabIndex, listRef, tabsRefs, isMobile });
return (
<TabList
marginBottom={{ base: 6, lg: 8 }}
mx={{ base: '-16px', lg: 'unset' }}
px={{ base: '16px', lg: 'unset' }}
flexWrap="nowrap"
whiteSpace="nowrap"
ref={ listRef }
overflowX={{ base: 'auto', lg: 'initial' }}
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 */
}}
bgColor={ listBgColor }
transitionProperty="top,box-shadow,background-color,color"
transitionDuration="normal"
transitionTimingFunction="ease"
{
...(props.stickyEnabled ? {
position: 'sticky',
boxShadow: { base: isSticky ? 'md' : 'none', lg: 'none' },
top: { base: scrollDirection === 'down' ? `0px` : `106px`, lg: 0 },
zIndex: { base: 'sticky2', lg: 'docked' },
} : { })
}
{
...(typeof props.tabListProps === 'function' ?
props.tabListProps({ isSticky, activeTabIndex: props.activeTabIndex }) :
props.tabListProps)
}
>
{ tabsList.map((tab, index) => {
if (!tab.id) {
return (
<TabsMenu
key="menu"
tabs={ props.tabs }
activeTab={ props.tabs[props.activeTabIndex] }
tabsCut={ tabsCut }
isActive={ props.activeTabIndex >= tabsCut }
styles={ tabsCut < props.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={ props.onItemClick }
buttonRef={ tabsRefs[index] }
size={ props.themeProps.size || 'md' }
/>
);
}
return (
<Tab
key={ tab.id }
ref={ tabsRefs[index] }
{ ...(index < tabsCut ? {} : hiddenItemStyles) }
scrollSnapAlign="start"
flexShrink={ 0 }
sx={{
'&:hover span': {
color: 'inherit',
},
}}
>
{ typeof tab.title === 'function' ? tab.title() : tab.title }
<TabCounter count={ tab.count }/>
</Tab>
);
}) }
{
props.rightSlot && tabsCut > 0 ?
<Box ref={ rightSlotRef } ml="auto" { ...props.rightSlotProps }> { props.rightSlot } </Box> :
null
}
</TabList>
);
};
export default React.memo(AdaptiveTabsList);
import type { SystemStyleObject } from '@chakra-ui/react'; import { chakra, useColorModeValue } from '@chakra-ui/react';
import { Text, useColorModeValue } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps';
const COUNTER_OVERLOAD = 50; const COUNTER_OVERLOAD = 50;
type Props = { type Props = {
count?: number | null; count?: number | null;
parentClassName: string;
} }
const TabCounter = ({ count, parentClassName }: Props) => { const TabCounter = ({ count }: Props) => {
const zeroCountColor = useColorModeValue('blackAlpha.400', 'whiteAlpha.400'); const zeroCountColor = useColorModeValue('blackAlpha.400', 'whiteAlpha.400');
...@@ -17,21 +17,14 @@ const TabCounter = ({ count, parentClassName }: Props) => { ...@@ -17,21 +17,14 @@ const TabCounter = ({ count, parentClassName }: Props) => {
return null; return null;
} }
const sx: SystemStyleObject = {
[`.${ parentClassName }:hover &`]: { color: 'inherit' },
};
return ( return (
<Text <chakra.span
color={ count > 0 ? 'text_secondary' : zeroCountColor } color={ count > 0 ? 'text_secondary' : zeroCountColor }
ml={ 1 } ml={ 1 }
sx={ sx } { ...getDefaultTransitionProps() }
transitionProperty="color"
transitionDuration="normal"
transitionTimingFunction="ease"
> >
{ count > COUNTER_OVERLOAD ? `${ COUNTER_OVERLOAD }+` : count } { count > COUNTER_OVERLOAD ? `${ COUNTER_OVERLOAD }+` : count }
</Text> </chakra.span>
); );
}; };
......
...@@ -15,8 +15,6 @@ import type { MenuButton, TabItem } from './types'; ...@@ -15,8 +15,6 @@ import type { MenuButton, TabItem } from './types';
import TabCounter from './TabCounter'; import TabCounter from './TabCounter';
import { menuButton } from './utils'; import { menuButton } from './utils';
const BUTTON_CLASSNAME = 'button-item';
interface Props { interface Props {
tabs: Array<TabItem | MenuButton>; tabs: Array<TabItem | MenuButton>;
activeTab?: TabItem; activeTab?: TabItem;
...@@ -62,10 +60,14 @@ const TabsMenu = ({ tabs, tabsCut, isActive, styles, onItemClick, buttonRef, act ...@@ -62,10 +60,14 @@ const TabsMenu = ({ tabs, tabsCut, isActive, styles, onItemClick, buttonRef, act
isActive={ activeTab ? activeTab.id === tab.id : false } isActive={ activeTab ? activeTab.id === tab.id : false }
justifyContent="left" justifyContent="left"
data-index={ index } data-index={ index }
className={ BUTTON_CLASSNAME } sx={{
'&:hover span': {
color: 'inherit',
},
}}
> >
{ typeof tab.title === 'function' ? tab.title() : tab.title } { typeof tab.title === 'function' ? tab.title() : tab.title }
<TabCounter count={ tab.count } parentClassName={ BUTTON_CLASSNAME }/> <TabCounter count={ tab.count }/>
</Button> </Button>
)) } )) }
</PopoverBody> </PopoverBody>
......
import type { LazyMode } from '@chakra-ui/lazy-utils'; import type { LazyMode } from '@chakra-ui/lazy-utils';
import type { ChakraProps, ThemingProps } from '@chakra-ui/react'; import type { ChakraProps, ThemingProps } from '@chakra-ui/react';
import { import {
Tab,
Tabs, Tabs,
TabList,
TabPanel, TabPanel,
TabPanels, TabPanels,
Box,
useColorModeValue,
chakra, chakra,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import type { StyleProps } from '@chakra-ui/styled-system'; import _debounce from 'lodash/debounce';
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import type { TabItem } from './types'; import type { TabItem } from './types';
import { useScrollDirection } from 'lib/contexts/scrollDirection'; import AdaptiveTabsList from './AdaptiveTabsList';
import useIsMobile from 'lib/hooks/useIsMobile';
import useIsSticky from 'lib/hooks/useIsSticky';
import TabCounter from './TabCounter';
import TabsMenu from './TabsMenu';
import useAdaptiveTabs from './useAdaptiveTabs';
import { menuButton } from './utils'; import { menuButton } from './utils';
const TAB_CLASSNAME = 'tab-item'; export interface Props extends ThemingProps<'Tabs'> {
const hiddenItemStyles: StyleProps = {
position: 'absolute',
top: '-9999px',
left: '-9999px',
visibility: 'hidden',
};
interface Props extends ThemingProps<'Tabs'> {
tabs: Array<TabItem>; tabs: Array<TabItem>;
lazyBehavior?: LazyMode; lazyBehavior?: LazyMode;
tabListProps?: ChakraProps | (({ isSticky, activeTabIndex }: { isSticky: boolean; activeTabIndex: number }) => ChakraProps); tabListProps?: ChakraProps | (({ isSticky, activeTabIndex }: { isSticky: boolean; activeTabIndex: number }) => ChakraProps);
...@@ -57,19 +38,15 @@ const TabsWithScroll = ({ ...@@ -57,19 +38,15 @@ const TabsWithScroll = ({
className, className,
...themeProps ...themeProps
}: Props) => { }: Props) => {
const scrollDirection = useScrollDirection();
const [ activeTabIndex, setActiveTabIndex ] = useState<number>(defaultTabIndex || 0); const [ activeTabIndex, setActiveTabIndex ] = useState<number>(defaultTabIndex || 0);
const isMobile = useIsMobile(); const [ screenWidth, setScreenWidth ] = React.useState(0);
const tabsRef = useRef<HTMLDivElement>(null); const tabsRef = useRef<HTMLDivElement>(null);
const tabsList = React.useMemo(() => { const tabsList = React.useMemo(() => {
return [ ...tabs, menuButton ]; return [ ...tabs, menuButton ];
}, [ tabs ]); }, [ tabs ]);
const { tabsCut, tabsRefs, listRef, rightSlotRef } = useAdaptiveTabs(tabsList, isMobile);
const isSticky = useIsSticky(listRef, 5, stickyEnabled);
const listBgColor = useColorModeValue('white', 'black');
const handleTabChange = React.useCallback((index: number) => { const handleTabChange = React.useCallback((index: number) => {
onTabChange ? onTabChange(index) : setActiveTabIndex(index); onTabChange ? onTabChange(index) : setActiveTabIndex(index);
}, [ onTabChange ]); }, [ onTabChange ]);
...@@ -80,23 +57,17 @@ const TabsWithScroll = ({ ...@@ -80,23 +57,17 @@ const TabsWithScroll = ({
} }
}, [ defaultTabIndex ]); }, [ defaultTabIndex ]);
useEffect(() => { React.useEffect(() => {
if (activeTabIndex < tabs.length && isMobile) { const resizeHandler = _debounce(() => {
window.setTimeout(() => { setScreenWidth(window.innerWidth);
const activeTabRef = tabsRefs[activeTabIndex]; }, 100);
if (activeTabRef.current && listRef.current) { const resizeObserver = new ResizeObserver(resizeHandler);
const activeTabRect = activeTabRef.current.getBoundingClientRect();
listRef.current.scrollTo({ resizeObserver.observe(document.body);
left: activeTabRect.left + listRef.current.scrollLeft - 16, return function cleanup() {
behavior: 'smooth', resizeObserver.unobserve(document.body);
}); };
} }, []);
// have to wait until DOM is updated and all styles to tabs is applied
}, 300);
}
// run only when tab index or device type is updated
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ activeTabIndex, isMobile ]);
if (tabs.length === 1) { if (tabs.length === 1) {
return <div>{ tabs[0].component }</div>; return <div>{ tabs[0].component }</div>;
...@@ -115,77 +86,20 @@ const TabsWithScroll = ({ ...@@ -115,77 +86,20 @@ const TabsWithScroll = ({
ref={ tabsRef } ref={ tabsRef }
lazyBehavior={ lazyBehavior } lazyBehavior={ lazyBehavior }
> >
<TabList <AdaptiveTabsList
marginBottom={{ base: 6, lg: 8 }} // the easiest and most readable way to achieve correct tab's cut recalculation when screen is resized
mx={{ base: '-16px', lg: 'unset' }} // is to do full re-render of the tabs list
px={{ base: '16px', lg: 'unset' }} // so we use screenWidth as a key for the TabsList component
flexWrap="nowrap" key={ screenWidth }
whiteSpace="nowrap" tabs={ tabs }
ref={ listRef } tabListProps={ tabListProps }
overflowX={{ base: 'auto', lg: 'initial' }} rightSlot={ rightSlot }
overscrollBehaviorX="contain" rightSlotProps={ rightSlotProps }
css={{ stickyEnabled={ stickyEnabled }
'scroll-snap-type': 'x mandatory', activeTabIndex={ activeTabIndex }
// hide scrollbar onItemClick={ handleTabChange }
'&::-webkit-scrollbar': { /* Chromiums */ themeProps={ themeProps }
display: 'none', />
},
'-ms-overflow-style': 'none', /* IE and Edge */
'scrollbar-width': 'none', /* Firefox */
}}
bgColor={ listBgColor }
transitionProperty="top,box-shadow,background-color,color"
transitionDuration="normal"
transitionTimingFunction="ease"
{
...(stickyEnabled ? {
position: 'sticky',
boxShadow: { base: isSticky ? 'md' : 'none', lg: 'none' },
top: { base: scrollDirection === 'down' ? `0px` : `106px`, lg: 0 },
zIndex: { base: 'sticky2', lg: 'docked' },
} : { })
}
{ ...(typeof tabListProps === 'function' ? tabListProps({ isSticky, activeTabIndex }) : tabListProps) }
>
{ tabsList.map((tab, index) => {
if (!tab.id) {
return (
<TabsMenu
key="menu"
tabs={ tabs }
activeTab={ tabs[activeTabIndex] }
tabsCut={ tabsCut }
isActive={ activeTabIndex >= 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] }
size={ themeProps.size || 'md' }
/>
);
}
return (
<Tab
key={ tab.id }
ref={ tabsRefs[index] }
{ ...(index < tabsCut ? {} : hiddenItemStyles) }
scrollSnapAlign="start"
flexShrink={ 0 }
className={ TAB_CLASSNAME }
>
{ typeof tab.title === 'function' ? tab.title() : tab.title }
<TabCounter count={ tab.count } parentClassName={ TAB_CLASSNAME }/>
</Tab>
);
}) }
{ rightSlot && tabsCut > 0 ? <Box ref={ rightSlotRef } ml="auto" { ...rightSlotProps }> { rightSlot } </Box> : null }
</TabList>
<TabPanels> <TabPanels>
{ tabsList.map((tab) => <TabPanel padding={ 0 } key={ tab.id }>{ tab.component }</TabPanel>) } { tabsList.map((tab) => <TabPanel padding={ 0 } key={ tab.id }>{ tab.component }</TabPanel>) }
</TabPanels> </TabPanels>
......
import _debounce from 'lodash/debounce';
import React from 'react'; import React from 'react';
import type { MenuButton, RoutedTab } from './types'; import type { MenuButton, RoutedTab } from './types';
...@@ -28,7 +27,7 @@ export default function useAdaptiveTabs(tabs: Array<RoutedTab | MenuButton>, dis ...@@ -28,7 +27,7 @@ export default function useAdaptiveTabs(tabs: Array<RoutedTab | MenuButton>, dis
if (result.visibleNum < index) { if (result.visibleNum < index) {
// means that we haven't increased visibleNum on the previous iteration, so there is no space left // means that we haven't increased visibleNum on the previous iteration, so there is no space left
// we skip now till the rest of the loop // we skip now till the end of the loop
return result; return result;
} }
...@@ -62,22 +61,6 @@ export default function useAdaptiveTabs(tabs: Array<RoutedTab | MenuButton>, dis ...@@ -62,22 +61,6 @@ export default function useAdaptiveTabs(tabs: Array<RoutedTab | MenuButton>, dis
} }
}, [ calculateCut, disabled, tabsRefs ]); }, [ calculateCut, disabled, tabsRefs ]);
React.useEffect(() => {
if (tabsRefs.length === 0 || disabled) {
return;
}
const resizeHandler = _debounce(() => {
setTabsCut(calculateCut());
}, 100);
const resizeObserver = new ResizeObserver(resizeHandler);
resizeObserver.observe(document.body);
return function cleanup() {
resizeObserver.unobserve(document.body);
};
}, [ calculateCut, disabled, tabsRefs.length ]);
return React.useMemo(() => { return React.useMemo(() => {
return { return {
tabsCut, tabsCut,
......
import React from 'react';
interface Props {
activeTabIndex: number;
tabsRefs: Array<React.RefObject<HTMLButtonElement>>;
listRef: React.RefObject<HTMLDivElement>;
isMobile?: boolean;
}
export default function useScrollToActiveTab({ activeTabIndex, tabsRefs, listRef, isMobile }: Props) {
React.useEffect(() => {
if (activeTabIndex < tabsRefs.length && isMobile) {
window.setTimeout(() => {
const activeTabRef = tabsRefs[activeTabIndex];
if (activeTabRef.current && listRef.current) {
const activeTabRect = activeTabRef.current.getBoundingClientRect();
listRef.current.scrollTo({
left: activeTabRect.left + listRef.current.scrollLeft - 16,
behavior: 'smooth',
});
}
// have to wait until DOM is updated and all styles to tabs is applied
}, 300);
}
// run only when tab index or device type is changed
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ activeTabIndex, isMobile ]);
}
...@@ -107,7 +107,8 @@ const Footer = () => { ...@@ -107,7 +107,8 @@ const Footer = () => {
return ( return (
<Flex <Flex
direction={{ base: 'column', lg: 'row' }} direction={{ base: 'column', lg: 'row' }}
p={{ base: 4, lg: 9 }} px={{ base: 4, lg: 12 }}
py={{ base: 4, lg: 9 }}
borderTop="1px solid" borderTop="1px solid"
borderColor="divider" borderColor="divider"
as="footer" as="footer"
...@@ -149,8 +150,20 @@ const Footer = () => { ...@@ -149,8 +150,20 @@ const Footer = () => {
{ config.UI.footer.links && <Text fontWeight={ 500 } mb={ 3 }>Blockscout</Text> } { config.UI.footer.links && <Text fontWeight={ 500 } mb={ 3 }>Blockscout</Text> }
<Grid <Grid
gap={ 1 } gap={ 1 }
gridTemplateColumns={ config.UI.footer.links ? '160px' : { base: 'repeat(auto-fill, 160px)', lg: 'repeat(4, 160px)' } } gridTemplateColumns={
gridTemplateRows={{ base: 'auto', lg: config.UI.footer.links ? 'auto' : 'repeat(2, auto)' }} config.UI.footer.links ?
'160px' :
{
base: 'repeat(auto-fill, 160px)',
lg: 'repeat(2, 160px)',
xl: 'repeat(4, 160px)',
}
}
gridTemplateRows={{
base: 'auto',
lg: config.UI.footer.links ? 'auto' : 'repeat(4, auto)',
xl: config.UI.footer.links ? 'auto' : 'repeat(2, auto)',
}}
gridAutoFlow={{ base: 'row', lg: config.UI.footer.links ? 'row' : 'column' }} gridAutoFlow={{ base: 'row', lg: config.UI.footer.links ? 'row' : 'column' }}
mt={{ base: 0, lg: config.UI.footer.links ? 0 : '100px' }} mt={{ base: 0, lg: config.UI.footer.links ? 0 : '100px' }}
> >
......
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