diff --git a/ui/shared/Tabs/AdaptiveTabsList.tsx b/ui/shared/Tabs/AdaptiveTabsList.tsx new file mode 100644 index 0000000000..5f5ff81f1f --- /dev/null +++ b/ui/shared/Tabs/AdaptiveTabsList.tsx @@ -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 ( + + { tabsList.map((tab, index) => { + if (!tab.id) { + return ( + = 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 ( + + { typeof tab.title === 'function' ? tab.title() : tab.title } + + + ); + }) } + { + props.rightSlot && tabsCut > 0 ? + { props.rightSlot } : + null + } + + ); +}; + +export default React.memo(AdaptiveTabsList); diff --git a/ui/shared/Tabs/TabCounter.tsx b/ui/shared/Tabs/TabCounter.tsx index 139e98a4b3..691af5ba70 100644 --- a/ui/shared/Tabs/TabCounter.tsx +++ b/ui/shared/Tabs/TabCounter.tsx @@ -1,15 +1,15 @@ -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'); @@ -17,21 +17,14 @@ const TabCounter = ({ count, parentClassName }: Props) => { return null; } - const sx: SystemStyleObject = { - [`.${ parentClassName }:hover &`]: { color: 'inherit' }, - }; - return ( - 0 ? 'text_secondary' : zeroCountColor } ml={ 1 } - sx={ sx } - transitionProperty="color" - transitionDuration="normal" - transitionTimingFunction="ease" + { ...getDefaultTransitionProps() } > { count > COUNTER_OVERLOAD ? `${ COUNTER_OVERLOAD }+` : count } - + ); }; diff --git a/ui/shared/Tabs/TabsMenu.tsx b/ui/shared/Tabs/TabsMenu.tsx index 57b4b5bcd9..36fd2fd81a 100644 --- a/ui/shared/Tabs/TabsMenu.tsx +++ b/ui/shared/Tabs/TabsMenu.tsx @@ -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; activeTab?: TabItem; @@ -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 } - + )) } diff --git a/ui/shared/Tabs/TabsWithScroll.tsx b/ui/shared/Tabs/TabsWithScroll.tsx index 9c19ffa598..a4a5fc4bb1 100644 --- a/ui/shared/Tabs/TabsWithScroll.tsx +++ b/ui/shared/Tabs/TabsWithScroll.tsx @@ -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; lazyBehavior?: LazyMode; tabListProps?: ChakraProps | (({ isSticky, activeTabIndex }: { isSticky: boolean; activeTabIndex: number }) => ChakraProps); @@ -57,19 +38,15 @@ const TabsWithScroll = ({ className, ...themeProps }: Props) => { - const scrollDirection = useScrollDirection(); const [ activeTabIndex, setActiveTabIndex ] = useState(defaultTabIndex || 0); - const isMobile = useIsMobile(); + const [ screenWidth, setScreenWidth ] = React.useState(0); + const tabsRef = useRef(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 ]); @@ -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
{ tabs[0].component }
; @@ -115,77 +86,20 @@ const TabsWithScroll = ({ ref={ tabsRef } lazyBehavior={ lazyBehavior } > - - { tabsList.map((tab, index) => { - if (!tab.id) { - return ( - = 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 ( - - { typeof tab.title === 'function' ? tab.title() : tab.title } - - - ); - }) } - { rightSlot && tabsCut > 0 ? { rightSlot } : null } - + { tabsList.map((tab) => { tab.component }) } diff --git a/ui/shared/Tabs/useAdaptiveTabs.tsx b/ui/shared/Tabs/useAdaptiveTabs.tsx index f9d8ca7fb9..4b4deea826 100644 --- a/ui/shared/Tabs/useAdaptiveTabs.tsx +++ b/ui/shared/Tabs/useAdaptiveTabs.tsx @@ -1,4 +1,3 @@ -import _debounce from 'lodash/debounce'; import React from 'react'; import type { MenuButton, RoutedTab } from './types'; @@ -28,7 +27,7 @@ export default function useAdaptiveTabs(tabs: Array, dis if (result.visibleNum < index) { // 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; } @@ -62,22 +61,6 @@ export default function useAdaptiveTabs(tabs: Array, dis } }, [ 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 { tabsCut, diff --git a/ui/shared/Tabs/useScrollToActiveTab.tsx b/ui/shared/Tabs/useScrollToActiveTab.tsx new file mode 100644 index 0000000000..39cd0e491b --- /dev/null +++ b/ui/shared/Tabs/useScrollToActiveTab.tsx @@ -0,0 +1,31 @@ +import React from 'react'; + +interface Props { + activeTabIndex: number; + tabsRefs: Array>; + listRef: React.RefObject; + 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 ]); +} diff --git a/ui/shared/layout/__screenshots__/Layout.pw.tsx_default_base-view-mobile-1.png b/ui/shared/layout/__screenshots__/Layout.pw.tsx_default_base-view-mobile-1.png index b138a5892e..0f7f7e0759 100644 Binary files a/ui/shared/layout/__screenshots__/Layout.pw.tsx_default_base-view-mobile-1.png and b/ui/shared/layout/__screenshots__/Layout.pw.tsx_default_base-view-mobile-1.png differ diff --git a/ui/shared/layout/__screenshots__/LayoutError.pw.tsx_default_base-view-mobile-1.png b/ui/shared/layout/__screenshots__/LayoutError.pw.tsx_default_base-view-mobile-1.png index ce15bd1e57..1a7a26cf30 100644 Binary files a/ui/shared/layout/__screenshots__/LayoutError.pw.tsx_default_base-view-mobile-1.png and b/ui/shared/layout/__screenshots__/LayoutError.pw.tsx_default_base-view-mobile-1.png differ diff --git a/ui/shared/layout/__screenshots__/LayoutHome.pw.tsx_default_base-view-mobile-1.png b/ui/shared/layout/__screenshots__/LayoutHome.pw.tsx_default_base-view-mobile-1.png index 3cb2e73e65..39a96a84a9 100644 Binary files a/ui/shared/layout/__screenshots__/LayoutHome.pw.tsx_default_base-view-mobile-1.png and b/ui/shared/layout/__screenshots__/LayoutHome.pw.tsx_default_base-view-mobile-1.png differ diff --git a/ui/snippets/footer/Footer.tsx b/ui/snippets/footer/Footer.tsx index 8272d93d10..e6982c9c35 100644 --- a/ui/snippets/footer/Footer.tsx +++ b/ui/snippets/footer/Footer.tsx @@ -107,7 +107,8 @@ const Footer = () => { return ( { { config.UI.footer.links && Blockscout } diff --git a/ui/snippets/footer/__screenshots__/Footer.pw.tsx_dark-color-mode_with-custom-links-2-cols-base-view-dark-mode-mobile-1.png b/ui/snippets/footer/__screenshots__/Footer.pw.tsx_dark-color-mode_with-custom-links-2-cols-base-view-dark-mode-mobile-1.png index 37b01d108f..ff1d4ea7ac 100644 Binary files a/ui/snippets/footer/__screenshots__/Footer.pw.tsx_dark-color-mode_with-custom-links-2-cols-base-view-dark-mode-mobile-1.png and b/ui/snippets/footer/__screenshots__/Footer.pw.tsx_dark-color-mode_with-custom-links-2-cols-base-view-dark-mode-mobile-1.png differ diff --git a/ui/snippets/footer/__screenshots__/Footer.pw.tsx_dark-color-mode_with-custom-links-4-cols-mobile-dark-mode-1.png b/ui/snippets/footer/__screenshots__/Footer.pw.tsx_dark-color-mode_with-custom-links-4-cols-mobile-dark-mode-1.png index a00c14b91e..53b597a4f1 100644 Binary files a/ui/snippets/footer/__screenshots__/Footer.pw.tsx_dark-color-mode_with-custom-links-4-cols-mobile-dark-mode-1.png and b/ui/snippets/footer/__screenshots__/Footer.pw.tsx_dark-color-mode_with-custom-links-4-cols-mobile-dark-mode-1.png differ diff --git a/ui/snippets/footer/__screenshots__/Footer.pw.tsx_dark-color-mode_without-custom-links-base-view-dark-mode-mobile-1.png b/ui/snippets/footer/__screenshots__/Footer.pw.tsx_dark-color-mode_without-custom-links-base-view-dark-mode-mobile-1.png index f4e3281c2f..850931017d 100644 Binary files a/ui/snippets/footer/__screenshots__/Footer.pw.tsx_dark-color-mode_without-custom-links-base-view-dark-mode-mobile-1.png and b/ui/snippets/footer/__screenshots__/Footer.pw.tsx_dark-color-mode_without-custom-links-base-view-dark-mode-mobile-1.png differ diff --git a/ui/snippets/footer/__screenshots__/Footer.pw.tsx_dark-color-mode_without-custom-links-with-indexing-alert-dark-mode-mobile-1.png b/ui/snippets/footer/__screenshots__/Footer.pw.tsx_dark-color-mode_without-custom-links-with-indexing-alert-dark-mode-mobile-1.png index 503426ee65..4e4f626c76 100644 Binary files a/ui/snippets/footer/__screenshots__/Footer.pw.tsx_dark-color-mode_without-custom-links-with-indexing-alert-dark-mode-mobile-1.png and b/ui/snippets/footer/__screenshots__/Footer.pw.tsx_dark-color-mode_without-custom-links-with-indexing-alert-dark-mode-mobile-1.png differ diff --git a/ui/snippets/footer/__screenshots__/Footer.pw.tsx_default_with-custom-links-2-cols-base-view-dark-mode-mobile-1.png b/ui/snippets/footer/__screenshots__/Footer.pw.tsx_default_with-custom-links-2-cols-base-view-dark-mode-mobile-1.png index 35a8c563eb..dcb30d18df 100644 Binary files a/ui/snippets/footer/__screenshots__/Footer.pw.tsx_default_with-custom-links-2-cols-base-view-dark-mode-mobile-1.png and b/ui/snippets/footer/__screenshots__/Footer.pw.tsx_default_with-custom-links-2-cols-base-view-dark-mode-mobile-1.png differ diff --git a/ui/snippets/footer/__screenshots__/Footer.pw.tsx_default_with-custom-links-4-cols-mobile-dark-mode-1.png b/ui/snippets/footer/__screenshots__/Footer.pw.tsx_default_with-custom-links-4-cols-mobile-dark-mode-1.png index 605dad1ea1..1e2bab604a 100644 Binary files a/ui/snippets/footer/__screenshots__/Footer.pw.tsx_default_with-custom-links-4-cols-mobile-dark-mode-1.png and b/ui/snippets/footer/__screenshots__/Footer.pw.tsx_default_with-custom-links-4-cols-mobile-dark-mode-1.png differ diff --git a/ui/snippets/footer/__screenshots__/Footer.pw.tsx_default_with-custom-links-4-cols-screen-xl-1.png b/ui/snippets/footer/__screenshots__/Footer.pw.tsx_default_with-custom-links-4-cols-screen-xl-1.png index d156bb1403..04bef25558 100644 Binary files a/ui/snippets/footer/__screenshots__/Footer.pw.tsx_default_with-custom-links-4-cols-screen-xl-1.png and b/ui/snippets/footer/__screenshots__/Footer.pw.tsx_default_with-custom-links-4-cols-screen-xl-1.png differ diff --git a/ui/snippets/footer/__screenshots__/Footer.pw.tsx_default_without-custom-links-base-view-dark-mode-mobile-1.png b/ui/snippets/footer/__screenshots__/Footer.pw.tsx_default_without-custom-links-base-view-dark-mode-mobile-1.png index 301fa2fadc..0fa981b4de 100644 Binary files a/ui/snippets/footer/__screenshots__/Footer.pw.tsx_default_without-custom-links-base-view-dark-mode-mobile-1.png and b/ui/snippets/footer/__screenshots__/Footer.pw.tsx_default_without-custom-links-base-view-dark-mode-mobile-1.png differ diff --git a/ui/snippets/footer/__screenshots__/Footer.pw.tsx_default_without-custom-links-with-indexing-alert-dark-mode-mobile-1.png b/ui/snippets/footer/__screenshots__/Footer.pw.tsx_default_without-custom-links-with-indexing-alert-dark-mode-mobile-1.png index 4be98fd4f8..6254c00c48 100644 Binary files a/ui/snippets/footer/__screenshots__/Footer.pw.tsx_default_without-custom-links-with-indexing-alert-dark-mode-mobile-1.png and b/ui/snippets/footer/__screenshots__/Footer.pw.tsx_default_without-custom-links-with-indexing-alert-dark-mode-mobile-1.png differ