Commit 4221b04b authored by tom goriunov's avatar tom goriunov Committed by GitHub

Limit page content width (#2197)

* limit content width for horizontal menu layout

* limit content width for vertical menu layout

* limit amout of tabs in routed tabs skeleton

* fix width of error screen

* add NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL L2 review envs

* add tests

* fixes

* fix tests

* move content wrapper to render fixture

* update screenshots and fix tests
parent fb6b1b1b
......@@ -67,6 +67,7 @@ frontend:
NEXT_PUBLIC_METADATA_SERVICE_API_HOST: https://metadata.services.blockscout.com
NEXT_PUBLIC_ROLLUP_TYPE: optimistic
NEXT_PUBLIC_ROLLUP_L1_BASE_URL: https://eth.blockscout.com
NEXT_PUBLIC_ROLLUP_L2_WITHDRAWAL_URL: https://app.optimism.io/bridge/withdraw
NEXT_PUBLIC_GRAPHIQL_TRANSACTION: 0x4a0ed8ddf751a7cb5297f827699117b0f6d21a0b2907594d300dc9fed75c7e62
NEXT_PUBLIC_USE_NEXT_JS_PROXY: true
NEXT_PUBLIC_NAVIGATION_LAYOUT: horizontal
......
import type { ChakraProps } from '@chakra-ui/react';
import { type ChakraProps } from '@chakra-ui/react';
import { GrowthBookProvider } from '@growthbook/growthbook-react';
import * as Sentry from '@sentry/react';
import { QueryClientProvider } from '@tanstack/react-query';
......@@ -19,6 +19,7 @@ import useLoadFeatures from 'lib/growthbook/useLoadFeatures';
import useNotifyOnNavigation from 'lib/hooks/useNotifyOnNavigation';
import { SocketProvider } from 'lib/socket/context';
import AppErrorBoundary from 'ui/shared/AppError/AppErrorBoundary';
import AppErrorGlobalContainer from 'ui/shared/AppError/AppErrorGlobalContainer';
import GoogleAnalytics from 'ui/shared/GoogleAnalytics';
import Layout from 'ui/shared/layout/Layout';
import Web3ModalProvider from 'ui/shared/Web3ModalProvider';
......@@ -38,7 +39,7 @@ const ERROR_SCREEN_STYLES: ChakraProps = {
justifyContent: 'center',
width: 'fit-content',
maxW: '800px',
margin: '0 auto',
margin: { base: '0 auto', lg: '0 auto' },
p: { base: 4, lg: 0 },
};
......@@ -60,6 +61,7 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
<AppErrorBoundary
{ ...ERROR_SCREEN_STYLES }
onError={ handleError }
Container={ AppErrorGlobalContainer }
>
<Web3ModalProvider>
<AppContextProvider pageProps={ pageProps }>
......
import { Box, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
interface Props {
children?: React.ReactNode;
}
const ContentWrapper = ({ children }: Props) => {
const bgColor = useColorModeValue('white', 'black');
return <Box bgColor={ bgColor }>{ children }</Box>;
};
export default React.memo(ContentWrapper);
......@@ -6,6 +6,7 @@ import React from 'react';
import type { JsonObject } from '@playwright/experimental-ct-core/types/component';
import ContentWrapper from 'playwright/ContentWrapper';
import type { Props as TestAppProps } from 'playwright/TestApp';
import TestApp from 'playwright/TestApp';
......@@ -27,7 +28,7 @@ export type RenderFixture = (component: JSX.Element, options?: Options, props?:
const fixture: TestFixture<RenderFixture, { mount: Mount }> = async({ mount }, use) => {
await use((component, options, props) => {
return mount(
<TestApp { ...props }>{ component }</TestApp>,
<TestApp { ...props }><ContentWrapper>{ component }</ContentWrapper></TestApp>,
options,
);
});
......
......@@ -7,7 +7,7 @@ import getDefaultTransitionProps from './utils/getDefaultTransitionProps';
const global = (props: StyleFunctionProps) => ({
body: {
bg: mode('white', 'black')(props),
bg: mode('gray.100', '#3A4957')(props),
...getDefaultTransitionProps(),
'-webkit-tap-highlight-color': 'transparent',
'font-variant-ligatures': 'no-contextual',
......
......@@ -7,11 +7,13 @@ import { test, expect } from 'playwright/lib';
import NameDomain from './NameDomain';
test('details tab', async({ render, mockTextAd, mockApiResponse }) => {
test('details tab', async({ render, mockTextAd, mockApiResponse, mockAssetResponse }) => {
await mockTextAd();
await mockApiResponse('domain_info', ensDomainMock.ensDomainA, {
pathParams: { chainId: config.chain.id, name: ensDomainMock.ensDomainA.name },
});
await mockAssetResponse(ensDomainMock.ensDomainA.protocol?.icon_url as string, './playwright/mocks/image_s.jpg');
const component = await render(
<NameDomain/>,
{ hooksConfig: {
......@@ -24,7 +26,7 @@ test('details tab', async({ render, mockTextAd, mockApiResponse }) => {
await expect(component).toHaveScreenshot();
});
test('history tab +@mobile', async({ render, mockTextAd, mockApiResponse }) => {
test('history tab +@mobile', async({ render, mockTextAd, mockApiResponse, mockAssetResponse }) => {
await mockTextAd();
await mockApiResponse('domain_info', ensDomainMock.ensDomainA, {
pathParams: { chainId: config.chain.id, name: ensDomainMock.ensDomainA.name },
......@@ -37,6 +39,7 @@ test('history tab +@mobile', async({ render, mockTextAd, mockApiResponse }) => {
}, {
pathParams: { chainId: config.chain.id, name: ensDomainMock.ensDomainA.name },
});
await mockAssetResponse(ensDomainMock.ensDomainA.protocol?.icon_url as string, './playwright/mocks/image_s.jpg');
const component = await render(
<NameDomain/>,
{ hooksConfig: {
......
......@@ -9,13 +9,18 @@ interface Props {
className?: string;
children: React.ReactNode;
onError?: (error: Error) => void;
Container?: React.FC<{ children: React.ReactNode }>;
}
const AppErrorBoundary = ({ className, children, onError }: Props) => {
const AppErrorBoundary = ({ className, children, onError, Container }: Props) => {
const renderErrorScreen = React.useCallback((error?: Error) => {
return <AppError error={ error } className={ className }/>;
}, [ className ]);
const content = <AppError error={ error } className={ className }/>;
if (Container) {
return <Container>{ content }</Container>;
}
return content;
}, [ className, Container ]);
return (
<ErrorBoundary renderErrorScreen={ renderErrorScreen } onError={ onError }>
......
import { Box, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
interface Props {
children: React.ReactNode;
}
const AppErrorGlobalContainer = ({ children }: Props) => {
const bgColor = useColorModeValue('white', 'black');
return <Box bgColor={ bgColor }>{ children }</Box>;
};
export default React.memo(AppErrorGlobalContainer);
......@@ -80,7 +80,7 @@ const AdaptiveTabsList = (props: Props) => {
props.tabListProps)
}
>
{ tabsList.map((tab, index) => {
{ tabsList.slice(0, props.isLoading ? 5 : Infinity).map((tab, index) => {
if (!tab.id) {
if (props.isLoading) {
return null;
......
......@@ -99,7 +99,7 @@ const TabsWithScroll = ({
// - tabs list is changed when API data is loaded
// is to do full re-render of the tabs list
// so we use screenWidth + tabIds as a key for the TabsList component
key={ screenWidth + '_' + tabsList.map((tab) => tab.id).join(':') }
key={ isLoading + '_' + screenWidth + '_' + tabsList.map((tab) => tab.id).join(':') }
tabs={ tabs }
tabListProps={ tabListProps }
rightSlot={ rightSlot }
......
......@@ -160,6 +160,7 @@ const AddressEntry = (props: EntityProps) => {
onMouseEnter={ context?.onMouseEnter }
onMouseLeave={ context?.onMouseLeave }
position="relative"
zIndex={ 0 }
>
<Icon { ...partsProps } color={ props.iconColor }/>
<Link { ...linkProps }>
......
......@@ -2,6 +2,7 @@ import React from 'react';
import { indexingStatus } from 'mocks/stats/index';
import { test, expect } from 'playwright/lib';
import * as pwConfig from 'playwright/utils/config';
import Layout from './Layout';
......@@ -16,3 +17,20 @@ test('base view +@mobile', async({ render, mockEnvs, mockApiResponse }) => {
const component = await render(<Layout>Page Content</Layout>);
await expect(component).toHaveScreenshot();
});
test.describe('xl screen', () => {
test.use({ viewport: pwConfig.viewport.xl });
test('vertical navigation', async({ render }) => {
const component = await render(<Layout>Page Content</Layout>);
await expect(component).toHaveScreenshot();
});
test('horizontal navigation', async({ render, mockEnvs }) => {
await mockEnvs([
[ 'NEXT_PUBLIC_NAVIGATION_LAYOUT', 'horizontal' ],
]);
const component = await render(<Layout>Page Content</Layout>);
await expect(component).toHaveScreenshot();
});
});
import { Box, chakra } from '@chakra-ui/react';
import { Box, chakra, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
import config from 'configs/app';
import { CONTENT_MAX_WIDTH } from '../utils';
interface Props {
children: React.ReactNode;
className?: string;
}
const Container = ({ children, className }: Props) => {
const bgColor = useColorModeValue('white', 'black');
return (
<Box className={ className } minWidth={{ base: '100vw', lg: 'fit-content' }}>
<Box
className={ className }
minWidth={{ base: '100vw', lg: 'fit-content' }}
maxW={ config.UI.navigation.layout === 'horizontal' ? undefined : `${ CONTENT_MAX_WIDTH }px` }
m="0 auto"
bgColor={ bgColor }
>
{ children }
</Box>
);
......
......@@ -3,6 +3,8 @@ import React from 'react';
import config from 'configs/app';
import { CONTENT_MAX_WIDTH } from '../utils';
interface Props {
children: React.ReactNode;
className?: string;
......@@ -16,6 +18,8 @@ const MainArea = ({ children, className }: Props) => {
<Flex
className={ className }
w="100%"
maxW={ `${ CONTENT_MAX_WIDTH }px` }
m="0 auto"
minH={{
base: `calc(100vh - ${ TOP_BAR_HEIGHT }px)`,
lg: `calc(100vh - ${ TOP_BAR_HEIGHT + HORIZONTAL_NAV_BAR_HEIGHT }px)`,
......
......@@ -14,7 +14,7 @@ const MainColumn = ({ children, className }: Props) => {
className={ className }
flexDir="column"
flexGrow={ 1 }
w={{ base: '100%', lg: 'auto' }}
w={{ base: '100%', lg: config.UI.navigation.layout === 'horizontal' ? '100%' : 'auto' }}
paddingX={{ base: 3, lg: config.UI.navigation.layout === 'horizontal' ? 6 : 12 }}
paddingTop={{ base: `${ 12 + 52 }px`, lg: 6 }} // 12px is top padding of content area, 52px is search bar height
paddingBottom={ 8 }
......
import config from 'configs/app';
export const CONTENT_MAX_WIDTH = config.UI.navigation.layout === 'horizontal' ? 1440 : 1512;
import type { GridProps } from '@chakra-ui/react';
import type { GridProps, HTMLChakraProps } from '@chakra-ui/react';
import { Box, Grid, Flex, Text, Link, VStack, Skeleton, useColorModeValue } from '@chakra-ui/react';
import { useQuery } from '@tanstack/react-query';
import React from 'react';
......@@ -12,6 +12,7 @@ import useFetch from 'lib/hooks/useFetch';
import useIssueUrl from 'lib/hooks/useIssueUrl';
import { copy } from 'lib/html-entities';
import IconSvg from 'ui/shared/IconSvg';
import { CONTENT_MAX_WIDTH } from 'ui/shared/layout/utils';
import NetworkAddToWallet from 'ui/shared/NetworkAddToWallet';
import FooterLinkItem from './FooterLinkItem';
......@@ -154,89 +155,98 @@ const Footer = () => {
);
}, [ apiVersionUrl, backendVersionData?.backend_version, frontendLink, logoColor ]);
const containerProps: GridProps = {
const containerProps: HTMLChakraProps<'div'> = {
as: 'footer',
borderTopWidth: '1px',
borderTopColor: 'solid',
};
const contentProps: GridProps = {
px: { base: 4, lg: config.UI.navigation.layout === 'horizontal' ? 6 : 12 },
py: { base: 4, lg: 8 },
borderTop: '1px solid',
borderColor: 'divider',
gridTemplateColumns: { base: '1fr', lg: 'minmax(auto, 470px) 1fr' },
columnGap: { lg: '32px', xl: '100px' },
maxW: `${ CONTENT_MAX_WIDTH }px`,
m: '0 auto',
};
if (config.UI.footer.links) {
return (
<Grid { ...containerProps }>
<div>
{ renderNetworkInfo() }
{ renderProjectInfo() }
</div>
<Grid
gap={{ base: 6, lg: colNum === MAX_LINKS_COLUMNS + 1 ? 2 : 8, xl: 12 }}
gridTemplateColumns={{
base: 'repeat(auto-fill, 160px)',
lg: `repeat(${ colNum }, 135px)`,
xl: `repeat(${ colNum }, 160px)`,
}}
justifyContent={{ lg: 'flex-end' }}
mt={{ base: 8, lg: 0 }}
>
{
([
{ title: 'Blockscout', links: BLOCKSCOUT_LINKS },
...(linksData || []),
])
.slice(0, colNum)
.map(linkGroup => (
<Box key={ linkGroup.title }>
<Skeleton fontWeight={ 500 } mb={ 3 } display="inline-block" isLoaded={ !isPlaceholderData }>{ linkGroup.title }</Skeleton>
<VStack spacing={ 1 } alignItems="start">
{ linkGroup.links.map(link => <FooterLinkItem { ...link } key={ link.text } isLoading={ isPlaceholderData }/>) }
</VStack>
</Box>
))
}
<Box { ...containerProps }>
<Grid { ...contentProps }>
<div>
{ renderNetworkInfo() }
{ renderProjectInfo() }
</div>
<Grid
gap={{ base: 6, lg: colNum === MAX_LINKS_COLUMNS + 1 ? 2 : 8, xl: 12 }}
gridTemplateColumns={{
base: 'repeat(auto-fill, 160px)',
lg: `repeat(${ colNum }, 135px)`,
xl: `repeat(${ colNum }, 160px)`,
}}
justifyContent={{ lg: 'flex-end' }}
mt={{ base: 8, lg: 0 }}
>
{
([
{ title: 'Blockscout', links: BLOCKSCOUT_LINKS },
...(linksData || []),
])
.slice(0, colNum)
.map(linkGroup => (
<Box key={ linkGroup.title }>
<Skeleton fontWeight={ 500 } mb={ 3 } display="inline-block" isLoaded={ !isPlaceholderData }>{ linkGroup.title }</Skeleton>
<VStack spacing={ 1 } alignItems="start">
{ linkGroup.links.map(link => <FooterLinkItem { ...link } key={ link.text } isLoading={ isPlaceholderData }/>) }
</VStack>
</Box>
))
}
</Grid>
</Grid>
</Grid>
</Box>
);
}
return (
<Grid
{ ...containerProps }
gridTemplateAreas={{
lg: `
<Box { ...containerProps }>
<Grid
{ ...contentProps }
gridTemplateAreas={{
lg: `
"network links-top"
"info links-bottom"
`,
}}
>
{ renderNetworkInfo({ lg: 'network' }) }
{ renderProjectInfo({ lg: 'info' }) }
<Grid
gridArea={{ lg: 'links-bottom' }}
gap={ 1 }
gridTemplateColumns={{
base: 'repeat(auto-fill, 160px)',
lg: 'repeat(3, 160px)',
xl: 'repeat(4, 160px)',
}}
gridTemplateRows={{
base: 'auto',
lg: 'repeat(3, auto)',
xl: 'repeat(2, auto)',
}}
gridAutoFlow={{ base: 'row', lg: 'column' }}
alignContent="start"
justifyContent={{ lg: 'flex-end' }}
mt={{ base: 8, lg: 0 }}
>
{ BLOCKSCOUT_LINKS.map(link => <FooterLinkItem { ...link } key={ link.text }/>) }
{ renderNetworkInfo({ lg: 'network' }) }
{ renderProjectInfo({ lg: 'info' }) }
<Grid
gridArea={{ lg: 'links-bottom' }}
gap={ 1 }
gridTemplateColumns={{
base: 'repeat(auto-fill, 160px)',
lg: 'repeat(3, 160px)',
xl: 'repeat(4, 160px)',
}}
gridTemplateRows={{
base: 'auto',
lg: 'repeat(3, auto)',
xl: 'repeat(2, auto)',
}}
gridAutoFlow={{ base: 'row', lg: 'column' }}
alignContent="start"
justifyContent={{ lg: 'flex-end' }}
mt={{ base: 8, lg: 0 }}
>
{ BLOCKSCOUT_LINKS.map(link => <FooterLinkItem { ...link } key={ link.text }/>) }
</Grid>
</Grid>
</Grid>
</Box>
);
};
......
import { chakra, Flex } from '@chakra-ui/react';
import { Box, chakra, Flex } from '@chakra-ui/react';
import React from 'react';
import config from 'configs/app';
import useNavItems, { isGroupItem } from 'lib/hooks/useNavItems';
import { CONTENT_MAX_WIDTH } from 'ui/shared/layout/utils';
import NetworkLogo from 'ui/snippets/networkMenu/NetworkLogo';
import ProfileMenuDesktop from 'ui/snippets/profileMenu/ProfileMenuDesktop';
import WalletMenuDesktop from 'ui/snippets/walletMenu/WalletMenuDesktop';
......@@ -15,30 +16,32 @@ const NavigationDesktop = () => {
const { mainNavItems } = useNavItems();
return (
<Flex
display={{ base: 'none', lg: 'flex' }}
alignItems="center"
px={ 6 }
py={ 2 }
borderBottomWidth="1px"
borderColor="divider"
>
<NetworkLogo isCollapsed={ false } w={{ lg: 'auto' }} maxW="120px"/>
<TestnetBadge ml={ 3 }/>
<chakra.nav ml="auto" mr={ config.features.account.isEnabled || config.features.blockchainInteraction.isEnabled ? 8 : 0 }>
<Flex as="ul" columnGap={ 3 }>
{ mainNavItems.map((item) => {
if (isGroupItem(item)) {
return <NavLinkGroup key={ item.text } item={ item }/>;
} else {
return <NavLink key={ item.text } item={ item } noIcon py={ 1.5 } w="fit-content"/>;
}
}) }
</Flex>
</chakra.nav>
{ config.features.account.isEnabled && <ProfileMenuDesktop buttonBoxSize="32px"/> }
{ config.features.blockchainInteraction.isEnabled && <WalletMenuDesktop size="sm"/> }
</Flex>
<Box borderColor="divider" borderBottomWidth="1px">
<Flex
display={{ base: 'none', lg: 'flex' }}
alignItems="center"
px={ 6 }
py={ 2 }
maxW={ `${ CONTENT_MAX_WIDTH }px` }
m="0 auto"
>
<NetworkLogo isCollapsed={ false } w={{ lg: 'auto' }} maxW="120px"/>
<TestnetBadge ml={ 3 }/>
<chakra.nav ml="auto" mr={ config.features.account.isEnabled || config.features.blockchainInteraction.isEnabled ? 8 : 0 }>
<Flex as="ul" columnGap={ 3 }>
{ mainNavItems.map((item) => {
if (isGroupItem(item)) {
return <NavLinkGroup key={ item.text } item={ item }/>;
} else {
return <NavLink key={ item.text } item={ item } noIcon py={ 1.5 } w="fit-content"/>;
}
}) }
</Flex>
</chakra.nav>
{ config.features.account.isEnabled && <ProfileMenuDesktop buttonBoxSize="32px"/> }
{ config.features.blockchainInteraction.isEnabled && <WalletMenuDesktop size="sm"/> }
</Flex>
</Box>
);
};
......
......@@ -2,6 +2,7 @@ import { Flex, Divider, useColorModeValue, Box } from '@chakra-ui/react';
import React from 'react';
import config from 'configs/app';
import { CONTENT_MAX_WIDTH } from 'ui/shared/layout/utils';
import DeFiDropdown from './DeFiDropdown';
import NetworkMenu from './NetworkMenu';
......@@ -12,30 +13,33 @@ const TopBar = () => {
const bgColor = useColorModeValue('gray.50', 'whiteAlpha.100');
return (
<Flex
py={ 2 }
px={{ base: 3, lg: 6 }}
bgColor={ bgColor }
justifyContent="space-between"
alignItems="center"
>
<TopBarStats/>
<Flex alignItems="center">
{ config.features.deFiDropdown.isEnabled && (
<>
<DeFiDropdown/>
<Divider mr={ 3 } ml={{ base: 2, sm: 3 }} height={ 4 } orientation="vertical"/>
</>
) }
<Settings/>
{ config.UI.navigation.layout === 'horizontal' && Boolean(config.UI.navigation.featuredNetworks) && (
<Box display={{ base: 'none', lg: 'flex' }}>
<Divider mx={ 3 } height={ 4 } orientation="vertical"/>
<NetworkMenu/>
</Box>
) }
<Box bgColor={ bgColor }>
<Flex
py={ 2 }
px={{ base: 3, lg: 6 }}
maxW={ `${ CONTENT_MAX_WIDTH }px` }
m="0 auto"
justifyContent="space-between"
alignItems="center"
>
<TopBarStats/>
<Flex alignItems="center">
{ config.features.deFiDropdown.isEnabled && (
<>
<DeFiDropdown/>
<Divider mr={ 3 } ml={{ base: 2, sm: 3 }} height={ 4 } orientation="vertical"/>
</>
) }
<Settings/>
{ config.UI.navigation.layout === 'horizontal' && Boolean(config.UI.navigation.featuredNetworks) && (
<Box display={{ base: 'none', lg: 'flex' }}>
<Divider mx={ 3 } height={ 4 } orientation="vertical"/>
<NetworkMenu/>
</Box>
) }
</Flex>
</Flex>
</Flex>
</Box>
);
};
......
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