Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix adaptive tabs and footer layout #1302

Merged
merged 3 commits into from
Oct 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 130 additions & 0 deletions ui/shared/Tabs/AdaptiveTabsList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
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);
21 changes: 7 additions & 14 deletions ui/shared/Tabs/TabCounter.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,30 @@
import type { SystemStyleObject } from '@chakra-ui/react';
import { Text, useColorModeValue } from '@chakra-ui/react';
import { chakra, useColorModeValue } from '@chakra-ui/react';
import React from 'react';

import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps';

const COUNTER_OVERLOAD = 50;

type Props = {
count?: number | null;
parentClassName: string;
}

const TabCounter = ({ count, parentClassName }: Props) => {
const TabCounter = ({ count }: Props) => {

const zeroCountColor = useColorModeValue('blackAlpha.400', 'whiteAlpha.400');

if (count === undefined || count === null) {
return null;
}

const sx: SystemStyleObject = {
[`.${ parentClassName }:hover &`]: { color: 'inherit' },
};

return (
<Text
<chakra.span
color={ count > 0 ? 'text_secondary' : zeroCountColor }
ml={ 1 }
sx={ sx }
transitionProperty="color"
transitionDuration="normal"
transitionTimingFunction="ease"
{ ...getDefaultTransitionProps() }
>
{ count > COUNTER_OVERLOAD ? `${ COUNTER_OVERLOAD }+` : count }
</Text>
</chakra.span>
);
};

Expand Down
10 changes: 6 additions & 4 deletions ui/shared/Tabs/TabsMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ import type { MenuButton, TabItem } from './types';
import TabCounter from './TabCounter';
import { menuButton } from './utils';

const BUTTON_CLASSNAME = 'button-item';

interface Props {
tabs: Array<TabItem | MenuButton>;
activeTab?: TabItem;
Expand Down Expand Up @@ -62,10 +60,14 @@ const TabsMenu = ({ tabs, tabsCut, isActive, styles, onItemClick, buttonRef, act
isActive={ activeTab ? activeTab.id === tab.id : false }
justifyContent="left"
data-index={ index }
className={ BUTTON_CLASSNAME }
sx={{
'&:hover span': {
color: 'inherit',
},
}}
>
{ typeof tab.title === 'function' ? tab.title() : tab.title }
<TabCounter count={ tab.count } parentClassName={ BUTTON_CLASSNAME }/>
<TabCounter count={ tab.count }/>
</Button>
)) }
</PopoverBody>
Expand Down
146 changes: 30 additions & 116 deletions ui/shared/Tabs/TabsWithScroll.tsx
Original file line number Diff line number Diff line change
@@ -1,39 +1,20 @@
import type { LazyMode } from '@chakra-ui/lazy-utils';
import type { ChakraProps, ThemingProps } from '@chakra-ui/react';
import {
Tab,
Tabs,
TabList,
TabPanel,
TabPanels,
Box,
useColorModeValue,
chakra,
} 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 type { TabItem } from './types';

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 useAdaptiveTabs from './useAdaptiveTabs';
import AdaptiveTabsList from './AdaptiveTabsList';
import { menuButton } from './utils';

const TAB_CLASSNAME = 'tab-item';

const hiddenItemStyles: StyleProps = {
position: 'absolute',
top: '-9999px',
left: '-9999px',
visibility: 'hidden',
};

interface Props extends ThemingProps<'Tabs'> {
export interface Props extends ThemingProps<'Tabs'> {
tabs: Array<TabItem>;
lazyBehavior?: LazyMode;
tabListProps?: ChakraProps | (({ isSticky, activeTabIndex }: { isSticky: boolean; activeTabIndex: number }) => ChakraProps);
Expand All @@ -57,19 +38,15 @@ const TabsWithScroll = ({
className,
...themeProps
}: Props) => {
const scrollDirection = useScrollDirection();
const [ activeTabIndex, setActiveTabIndex ] = useState<number>(defaultTabIndex || 0);
const isMobile = useIsMobile();
const [ screenWidth, setScreenWidth ] = React.useState(0);

const tabsRef = useRef<HTMLDivElement>(null);

const tabsList = React.useMemo(() => {
return [ ...tabs, menuButton ];
}, [ 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) => {
onTabChange ? onTabChange(index) : setActiveTabIndex(index);
}, [ onTabChange ]);
Expand All @@ -80,23 +57,17 @@ const TabsWithScroll = ({
}
}, [ defaultTabIndex ]);

useEffect(() => {
if (activeTabIndex < tabs.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 updated
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ activeTabIndex, isMobile ]);
React.useEffect(() => {
const resizeHandler = _debounce(() => {
setScreenWidth(window.innerWidth);
}, 100);
const resizeObserver = new ResizeObserver(resizeHandler);

resizeObserver.observe(document.body);
return function cleanup() {
resizeObserver.unobserve(document.body);
};
}, []);

if (tabs.length === 1) {
return <div>{ tabs[0].component }</div>;
Expand All @@ -115,77 +86,20 @@ const TabsWithScroll = ({
ref={ tabsRef }
lazyBehavior={ lazyBehavior }
>
<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"
{
...(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>
<AdaptiveTabsList
// the easiest and most readable way to achieve correct tab's cut recalculation when screen is resized
// is to do full re-render of the tabs list
// so we use screenWidth as a key for the TabsList component
key={ screenWidth }
tabs={ tabs }
tabListProps={ tabListProps }
rightSlot={ rightSlot }
rightSlotProps={ rightSlotProps }
stickyEnabled={ stickyEnabled }
activeTabIndex={ activeTabIndex }
onItemClick={ handleTabChange }
themeProps={ themeProps }
/>
<TabPanels>
{ tabsList.map((tab) => <TabPanel padding={ 0 } key={ tab.id }>{ tab.component }</TabPanel>) }
</TabPanels>
Expand Down
Loading
Loading