Commit a11c5fa3 authored by isstuev's avatar isstuev

mobile menu with animation

parent dcc965c0
<svg viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#top-accounts_svg__a)"> <g clip-path="url(#top-accounts_svg__a)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.585 2.973a3.412 3.412 0 0 0-3.412 3.412v17.23a3.412 3.412 0 0 0 3.412 3.412h17.23a.95.95 0 0 0 0-1.9H7.585a1.512 1.512 0 1 1 0-3.023h17.23a.95.95 0 0 0 .934-1.126.954.954 0 0 0 .016-.176V4.662c0-.933-.756-1.689-1.688-1.689H7.585ZM6.073 6.385c0-.835.677-1.512 1.512-1.512h16.28v15.33H7.585a3.4 3.4 0 0 0-1.512.353V6.385Zm8.897 1.4a3.013 3.013 0 1 0 0 6.026 3.013 3.013 0 0 0 0-6.026Zm-1.024 3.013a1.024 1.024 0 1 1 2.048 0 1.024 1.024 0 0 1-2.048 0Zm1.025 3.097c-2.316 0-4.3 1.303-5.319 3.243a.446.446 0 0 0 .395.654h1.281a.446.446 0 0 0 .366-.19 3.972 3.972 0 0 1 3.277-1.718c1.33 0 2.52.685 3.268 1.723a.446.446 0 0 0 .362.185h1.292a.446.446 0 0 0 .393-.659c-1.01-1.866-2.983-3.238-5.315-3.238Z" fill="#4A5568"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M7.585 2.973a3.412 3.412 0 0 0-3.412 3.412v17.23a3.412 3.412 0 0 0 3.412 3.412h17.23a.95.95 0 0 0 0-1.9H7.585a1.512 1.512 0 1 1 0-3.023h17.23a.95.95 0 0 0 .934-1.126.954.954 0 0 0 .016-.176V4.662c0-.933-.756-1.689-1.688-1.689H7.585ZM6.073 6.385c0-.835.677-1.512 1.512-1.512h16.28v15.33H7.585a3.4 3.4 0 0 0-1.512.353V6.385Zm8.897 1.4a3.013 3.013 0 1 0 0 6.026 3.013 3.013 0 0 0 0-6.026Zm-1.024 3.013a1.024 1.024 0 1 1 2.048 0 1.024 1.024 0 0 1-2.048 0Zm1.025 3.097c-2.316 0-4.3 1.303-5.319 3.243a.446.446 0 0 0 .395.654h1.281a.446.446 0 0 0 .366-.19 3.972 3.972 0 0 1 3.277-1.718c1.33 0 2.52.685 3.268 1.723a.446.446 0 0 0 .362.185h1.292a.446.446 0 0 0 .393-.659c-1.01-1.866-2.983-3.238-5.315-3.238Z" fill="currentColor"/>
</g> </g>
<defs> <defs>
<clipPath id="top-accounts_svg__a"> <clipPath id="top-accounts_svg__a">
......
<svg viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#verified_svg__a)"> <g clip-path="url(#verified_svg__a)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M23.4 15a8.4 8.4 0 1 1-16.8 0 8.4 8.4 0 0 1 16.8 0Zm1.6 0c0 5.523-4.477 10-10 10S5 20.523 5 15 9.477 5 15 5s10 4.477 10 10Zm-5.895-3.706a.916.916 0 1 1 1.295 1.295l-6.022 6.022a1.05 1.05 0 0 1-1.485 0l-3.2-3.199a.915.915 0 0 1 1.296-1.295l2.257 2.258a.55.55 0 0 0 .778 0l5.081-5.081Z" fill="#4A5568"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M23.4 15a8.4 8.4 0 1 1-16.8 0 8.4 8.4 0 0 1 16.8 0Zm1.6 0c0 5.523-4.477 10-10 10S5 20.523 5 15 9.477 5 15 5s10 4.477 10 10Zm-5.895-3.706a.916.916 0 1 1 1.295 1.295l-6.022 6.022a1.05 1.05 0 0 1-1.485 0l-3.2-3.199a.915.915 0 0 1 1.296-1.295l2.257 2.258a.55.55 0 0 0 .778 0l5.081-5.081Z" fill="currentColor"/>
</g> </g>
<defs> <defs>
<clipPath id="verified_svg__a"> <clipPath id="verified_svg__a">
......
*,
*::after,
*::before {
transition-delay: 0s !important;
transition-duration: 0s !important;
animation-delay: -0.0001s !important;
animation-duration: 0s !important;
animation-play-state: paused !important;
}
\ No newline at end of file
import './fonts.css'; import './fonts.css';
import './index.css';
import { beforeMount } from '@playwright/experimental-ct-react/hooks'; import { beforeMount } from '@playwright/experimental-ct-react/hooks';
import _defaultsDeep from 'lodash/defaultsDeep'; import _defaultsDeep from 'lodash/defaultsDeep';
import MockDate from 'mockdate'; import MockDate from 'mockdate';
......
...@@ -58,4 +58,16 @@ test.describe('auth', () => { ...@@ -58,4 +58,16 @@ test.describe('auth', () => {
await component.locator('svg[aria-label="Menu button"]').click(); await component.locator('svg[aria-label="Menu button"]').click();
await expect(page).toHaveScreenshot(); await expect(page).toHaveScreenshot();
}); });
extendedTest('submenu', async({ mount, page }) => {
const component = await mount(
<TestApp>
<Burger/>
</TestApp>,
);
await component.locator('svg[aria-label="Menu button"]').click();
await page.locator('div[aria-label="Blockchain link group"]').click();
await expect(page).toHaveScreenshot();
});
}); });
...@@ -13,7 +13,6 @@ import { ...@@ -13,7 +13,6 @@ import {
import React from 'react'; import React from 'react';
import chevronIcon from 'icons/arrows/east-mini.svg'; import chevronIcon from 'icons/arrows/east-mini.svg';
import useIsMobile from 'lib/hooks/useIsMobile';
import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps'; import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps';
import NavLink from './NavLink'; import NavLink from './NavLink';
...@@ -35,25 +34,23 @@ interface Props { ...@@ -35,25 +34,23 @@ interface Props {
icon: React.FunctionComponent<React.SVGAttributes<SVGElement>>; icon: React.FunctionComponent<React.SVGAttributes<SVGElement>>;
} }
const NavLinkGroup = ({ text, subItems, icon, isCollapsed, isActive }: Props) => { const NavLinkGroupDesktop = ({ text, subItems, icon, isCollapsed, isActive }: Props) => {
const colors = useColors(); const colors = useColors();
const isExpanded = isCollapsed === false; const isExpanded = isCollapsed === false;
const isMobile = useIsMobile();
return ( return (
<Box as="li" listStyleType="none" w="100%"> <Box as="li" listStyleType="none" w="100%">
<Popover <Popover
trigger="hover" trigger="hover"
placement={ isMobile ? 'bottom-end' : 'right-start' } placement="right-start"
isLazy isLazy
> >
<PopoverTrigger> <PopoverTrigger>
<Box <Box
w={{ base: '100%', lg: isExpanded ? '180px' : '60px', xl: isCollapsed ? '60px' : '180px' }} w={{ lg: isExpanded ? '180px' : '60px', xl: isCollapsed ? '60px' : '180px' }}
pl={{ base: 3, lg: isExpanded ? 3 : '15px', xl: isCollapsed ? '15px' : 3 }} pl={{ lg: isExpanded ? 3 : '15px', xl: isCollapsed ? '15px' : 3 }}
pr={{ base: 3, lg: isExpanded ? 0 : '15px', xl: isCollapsed ? '15px' : 0 }} pr={{ lg: isExpanded ? 0 : '15px', xl: isCollapsed ? '15px' : 0 }}
py={ 2.5 } py={ 2.5 }
display="flex" display="flex"
color={ isActive ? colors.text.active : colors.text.default } color={ isActive ? colors.text.active : colors.text.default }
...@@ -71,7 +68,7 @@ const NavLinkGroup = ({ text, subItems, icon, isCollapsed, isActive }: Props) => ...@@ -71,7 +68,7 @@ const NavLinkGroup = ({ text, subItems, icon, isCollapsed, isActive }: Props) =>
variant="inherit" variant="inherit"
fontSize="sm" fontSize="sm"
lineHeight="20px" lineHeight="20px"
opacity={{ base: '1', lg: isExpanded ? '1' : '0', xl: isCollapsed ? '0' : '1' }} opacity={{ lg: isExpanded ? '1' : '0', xl: isCollapsed ? '0' : '1' }}
transitionProperty="opacity" transitionProperty="opacity"
transitionDuration="normal" transitionDuration="normal"
transitionTimingFunction="ease" transitionTimingFunction="ease"
...@@ -85,6 +82,9 @@ const NavLinkGroup = ({ text, subItems, icon, isCollapsed, isActive }: Props) => ...@@ -85,6 +82,9 @@ const NavLinkGroup = ({ text, subItems, icon, isCollapsed, isActive }: Props) =>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent width="auto"> <PopoverContent width="auto">
<PopoverBody p={ 4 }> <PopoverBody p={ 4 }>
<Text variant="secondary" fontSize="sm" mb={ 2 } display={{ lg: isExpanded ? 'none' : 'block', xl: isCollapsed ? 'block' : 'none' }}>
{ text }
</Text>
<VStack spacing={ 1 } alignItems="start"> <VStack spacing={ 1 } alignItems="start">
{ subItems.map(item => <NavLink key={ item.text } { ...item } isCollapsed={ false }/>) } { subItems.map(item => <NavLink key={ item.text } { ...item } isCollapsed={ false }/>) }
</VStack> </VStack>
...@@ -95,4 +95,4 @@ const NavLinkGroup = ({ text, subItems, icon, isCollapsed, isActive }: Props) => ...@@ -95,4 +95,4 @@ const NavLinkGroup = ({ text, subItems, icon, isCollapsed, isActive }: Props) =>
); );
}; };
export default NavLinkGroup; export default NavLinkGroupDesktop;
import {
Icon,
Text,
HStack,
Flex,
Box,
} from '@chakra-ui/react';
import React from 'react';
import chevronIcon from 'icons/arrows/east-mini.svg';
import getDefaultTransitionProps from 'theme/utils/getDefaultTransitionProps';
import useColors from './useColors';
type NavigationLink = {
text: string;
url: string;
icon: React.FunctionComponent<React.SVGAttributes<SVGElement>>;
isNewUi: boolean;
isActive: boolean;
}
interface Props {
isCollapsed?: boolean;
isActive?: boolean;
subItems: Array<NavigationLink>;
text: string;
icon: React.FunctionComponent<React.SVGAttributes<SVGElement>>;
onClick: () => void;
}
const NavLinkGroup = ({ text, icon, isActive, onClick }: Props) => {
const colors = useColors();
return (
<Box as="li" listStyleType="none" w="100%" onClick={ onClick }>
<Box
w="100%"
px={ 3 }
py={ 2.5 }
display="flex"
color={ isActive ? colors.text.active : colors.text.default }
bgColor={ isActive ? colors.bg.active : colors.bg.default }
_hover={{ color: isActive ? colors.text.active : colors.text.hover }}
borderRadius="base"
whiteSpace="nowrap"
aria-label={ `${ text } link group` }
{ ...getDefaultTransitionProps({ transitionProperty: 'width, padding' }) }
>
<Flex justifyContent="space-between" width="100%" alignItems="center" pr={ 1 }>
<HStack spacing={ 3 } overflow="hidden">
<Icon as={ icon } boxSize="30px"/>
<Text
variant="inherit"
fontSize="sm"
lineHeight="20px"
transitionProperty="opacity"
transitionDuration="normal"
transitionTimingFunction="ease"
>
{ text }
</Text>
</HStack>
<Icon as={ chevronIcon } transform="rotate(180deg)" boxSize={ 6 }/>
</Flex>
</Box>
</Box>
);
};
export default NavLinkGroup;
...@@ -70,7 +70,7 @@ test('with tooltips +@desktop-xl -@default', async({ mount, page }) => { ...@@ -70,7 +70,7 @@ test('with tooltips +@desktop-xl -@default', async({ mount, page }) => {
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
test('with submenu +@desktop-xl', async({ mount, page }) => { test('with submenu +@desktop-xl +@dark-mode', async({ mount, page }) => {
const component = await mount( const component = await mount(
<TestApp> <TestApp>
<Flex w="100%" minH="100vh" alignItems="stretch"> <Flex w="100%" minH="100vh" alignItems="stretch">
......
...@@ -12,7 +12,7 @@ import NetworkMenu from 'ui/snippets/networkMenu/NetworkMenu'; ...@@ -12,7 +12,7 @@ import NetworkMenu from 'ui/snippets/networkMenu/NetworkMenu';
import NavFooter from './NavFooter'; import NavFooter from './NavFooter';
import NavLink from './NavLink'; import NavLink from './NavLink';
import NavLinkGroup from './NavLinkGroup'; import NavLinkGroupDesktop from './NavLinkGroupDesktop';
const NavigationDesktop = () => { const NavigationDesktop = () => {
const appProps = useAppContext(); const appProps = useAppContext();
...@@ -81,7 +81,7 @@ const NavigationDesktop = () => { ...@@ -81,7 +81,7 @@ const NavigationDesktop = () => {
<VStack as="ul" spacing="1" alignItems="flex-start"> <VStack as="ul" spacing="1" alignItems="flex-start">
{ mainNavItems.map((item) => { { mainNavItems.map((item) => {
if (item.subItems) { if (item.subItems) {
return <NavLinkGroup key={ item.text } { ...item } isCollapsed={ isCollapsed }/>; return <NavLinkGroupDesktop key={ item.text } { ...item } isCollapsed={ isCollapsed }/>;
} else { } else {
return <NavLink key={ item.text } { ...item } isCollapsed={ isCollapsed }/>; return <NavLink key={ item.text } { ...item } isCollapsed={ isCollapsed }/>;
} }
......
import { Box, VStack } from '@chakra-ui/react'; import { Box, Flex, Text, Icon, VStack, useColorModeValue } from '@chakra-ui/react';
import React from 'react'; import { AnimatePresence, animate, motion, useMotionValue } from 'framer-motion';
import React, { useCallback } from 'react';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import chevronIcon from 'icons/arrows/east-mini.svg';
import * as cookies from 'lib/cookies'; import * as cookies from 'lib/cookies';
import useNavItems from 'lib/hooks/useNavItems'; import useNavItems from 'lib/hooks/useNavItems';
import NavFooter from 'ui/snippets/navigation/NavFooter'; import NavFooter from 'ui/snippets/navigation/NavFooter';
import NavLink from 'ui/snippets/navigation/NavLink'; import NavLink from 'ui/snippets/navigation/NavLink';
import NavLinkGroup from './NavLinkGroup'; import NavLinkGroupMobile from './NavLinkGroupMobile';
const NavigationMobile = () => { const NavigationMobile = () => {
const { mainNavItems, accountNavItems } = useNavItems(); const { mainNavItems, accountNavItems } = useNavItems();
const [ openedGroupIndex, setOpenedGroupIndex ] = React.useState(-1);
const x = useMotionValue(0);
const onGroupItemOpen = (index: number) => () => {
animate(x, -250);
setOpenedGroupIndex(index);
};
const onGroupItemClose = useCallback(() => {
animate(x, 0);
setOpenedGroupIndex(-1);
}, [ x ]);
const isAuth = Boolean(cookies.get(cookies.NAMES.API_TOKEN)); const isAuth = Boolean(cookies.get(cookies.NAMES.API_TOKEN));
const hasAccount = appConfig.isAccountSupported && isAuth; const hasAccount = appConfig.isAccountSupported && isAuth;
const iconColor = useColorModeValue('blue.600', 'blue.300');
return ( return (
<> <>
<Box as="nav" mt={ 6 }> <Box position="relative">
<VStack as="ul" spacing="1" alignItems="flex-start"> <Box
{ mainNavItems.map((item) => { as={ motion.nav }
if (item.subItems) { mt={ 6 }
return <NavLinkGroup key={ item.text } { ...item }/>; style={{ x }}
} else { transitionDuration="100ms"
return <NavLink key={ item.text } { ...item }/>; >
} <VStack
}) } w="100%"
</VStack> as="ul"
</Box> spacing="1"
{ isAuth && ( alignItems="flex-start"
<Box as="nav" mt={ 6 }> >
<VStack as="ul" spacing="1" alignItems="flex-start"> { mainNavItems.map((item, index) => {
{ accountNavItems.map((item) => <NavLink key={ item.text } { ...item }/>) } if (item.subItems) {
return <NavLinkGroupMobile key={ item.text } { ...item } onClick={ onGroupItemOpen(index) }/>;
} else {
return <NavLink key={ item.text } { ...item }/>;
}
}) }
</VStack> </VStack>
</Box> </Box>
) } { isAuth && (
<Box
as={ motion.nav }
mt={ 6 }
style={{ x }}
transitionDuration="100ms"
>
<VStack as="ul" spacing="1" alignItems="flex-start">
{ accountNavItems.map((item) => <NavLink key={ item.text } { ...item }/>) }
</VStack>
</Box>
) }
<AnimatePresence>
{ openedGroupIndex >= 0 && (
<Box
as={ motion.nav }
mt={ 6 }
position="absolute"
top={ 0 }
initial={{ x: 250 }}
animate={{ x: 0 }}
exit={{ x: 250 }}
transitionDuration="100ms"
transitionTimingFunction="linear"
key="sub"
>
<VStack
w="100%"
as="ul"
spacing="1"
alignItems="flex-start"
>
<Flex alignItems="center" px={ 3 } py={ 2.5 } w="100%" h="50px" onClick={ onGroupItemClose }>
<Icon as={ chevronIcon } boxSize={ 6 } mr={ 2 } color={ iconColor }/>
<Text variant="secondary" fontSize="sm">{ mainNavItems[openedGroupIndex].text }</Text>
</Flex>
{ mainNavItems[openedGroupIndex].subItems?.map((item) => <NavLink key={ item.text } { ...item }/>) }
</VStack>
</Box>
) }
</AnimatePresence>
</Box>
<NavFooter hasAccount={ hasAccount }/> <NavFooter hasAccount={ hasAccount }/>
</> </>
); );
......
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