Skip to content
Snippets Groups Projects
Unverified Commit a3995582 authored by Joel Jeremy Marquez's avatar Joel Jeremy Marquez Committed by GitHub
Browse files

Swipe up mobile navbar (#1758)


* Swipe up mobile navbar to reveal more menus

* Release notes

* Revert navbar button height to 70

* Reduce debounce value

* More tabs coming soon

* VRT update

* VRT update

* Fix: e2e tests

* Smoother mobile navtabs

---------

Co-authored-by: default avatarMatiss Janis Aboltins <matiss@mja.lv>
parent 5008eb02
No related branches found
No related tags found
No related merge requests found
Showing
with 209 additions and 25 deletions
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
......@@ -30,9 +30,17 @@ export class MobileNavigation {
}
async goToSettingsPage() {
await this.dragNavbarUp();
const link = this.page.getByRole('link', { name: 'Settings' });
await link.click();
return new SettingsPage(this.page);
}
async dragNavbarUp() {
await this.page
.getByRole('navigation')
.dragTo(this.page.getByTestId('budget-table'));
}
}
......@@ -28,6 +28,7 @@
"@types/react-router-dom": "^5.3.3",
"@types/uuid": "^9.0.2",
"@types/webpack-bundle-analyzer": "^4.6.0",
"@use-gesture/react": "^10.3.0",
"chokidar": "^3.5.3",
"cross-env": "^7.0.3",
"date-fns": "^2.29.3",
......
......@@ -29,7 +29,7 @@ export default function ScrollProvider({ children }: ScrollProviderProps) {
setIsBottomReached(
e.target?.scrollHeight - e.target?.scrollTop <= e.target?.clientHeight,
);
}, 20);
}, 10);
window.addEventListener('scroll', listenToScroll, {
capture: true,
......
import React, { type ComponentType, useMemo } from 'react';
import React, { type ComponentType, useEffect } from 'react';
import { NavLink } from 'react-router-dom';
import { useSpring, animated, config } from 'react-spring';
import { useDrag } from '@use-gesture/react';
import usePrevious from '../../hooks/usePrevious';
import Add from '../../icons/v1/Add';
import Cog from '../../icons/v1/Cog';
import PiggyBank from '../../icons/v1/PiggyBank';
import StoreFront from '../../icons/v1/StoreFront';
import Tuning from '../../icons/v1/Tuning';
import Wallet from '../../icons/v1/Wallet';
import Calendar from '../../icons/v2/Calendar';
import { useResponsive } from '../../ResponsiveProvider';
import { theme, styles } from '../../style';
import { theme, styles, type CSSProperties } from '../../style';
import View from '../common/View';
import { useScroll } from '../ScrollProvider';
const height = 70;
const ROW_HEIGHT = 70;
const COLUMN_COUNT = 3;
export default function MobileNavTabs() {
const { isNarrowWidth } = useResponsive();
const { scrollY, isBottomReached } = useScroll();
const { scrollY } = useScroll();
const navTabStyle = {
flex: `1 1 ${100 / COLUMN_COUNT}%`,
height: ROW_HEIGHT,
padding: 10,
};
const navTabs = [
{
name: 'Budget',
path: '/budget',
style: navTabStyle,
icon: Wallet,
},
{
name: 'Transaction',
path: '/transactions/new',
style: navTabStyle,
icon: Add,
},
{
name: 'Accounts',
path: '/accounts',
style: navTabStyle,
icon: PiggyBank,
},
{
name: 'Schedules (Soon)',
path: '/schedules/soon',
style: navTabStyle,
icon: Calendar,
},
{
name: 'Payees (Soon)',
path: '/payees/soon',
style: navTabStyle,
icon: StoreFront,
},
{
name: 'Rules (Soon)',
path: '/rules/soon',
style: navTabStyle,
icon: Tuning,
},
{
name: 'Settings',
path: '/settings',
style: navTabStyle,
icon: Cog,
},
].map(tab => <NavTab key={tab.path} {...tab} />);
const bufferTabsCount = COLUMN_COUNT - (navTabs.length % COLUMN_COUNT);
const bufferTabs = Array.from({ length: bufferTabsCount }).map((_, idx) => (
<div key={idx} style={navTabStyle} />
));
const totalHeight = ROW_HEIGHT * COLUMN_COUNT;
const openY = 0;
const closeY = totalHeight - ROW_HEIGHT;
const hiddenY = totalHeight;
const [{ y }, api] = useSpring(() => ({ y: totalHeight }));
const open = ({ canceled }) => {
// when cancel is true, it means that the user passed the upwards threshold
// so we change the spring config to create a nice wobbly effect
api.start({
y: openY,
immediate: false,
config: canceled ? config.wobbly : config.stiff,
});
};
const close = (velocity = 0) => {
api.start({
y: closeY,
immediate: false,
config: { ...config.stiff, velocity },
});
};
const hide = (velocity = 0) => {
api.start({
y: hiddenY,
immediate: false,
config: { ...config.stiff, velocity },
});
};
const previousScrollY = usePrevious(scrollY);
const isVisible = useMemo(
() =>
previousScrollY === undefined ||
(!isBottomReached && previousScrollY > scrollY) ||
previousScrollY < 0,
[scrollY],
useEffect(() => {
if (
scrollY &&
previousScrollY &&
scrollY > previousScrollY &&
previousScrollY !== 0
) {
hide();
} else {
close();
}
}, [scrollY]);
const bind = useDrag(
({
last,
velocity: [, vy],
direction: [, dy],
offset: [, oy],
cancel,
canceled,
}) => {
// if the user drags up passed a threshold, then we cancel
// the drag so that the sheet resets to its open position
if (oy < 0) {
cancel();
}
// when the user releases the sheet, we check whether it passed
// the threshold for it to close, or if we reset it to its open position
if (last) {
if (oy > ROW_HEIGHT * 0.5 || (vy > 0.5 && dy > 0)) {
close(vy);
} else {
open({ canceled });
}
} else {
// when the user keeps dragging, we just move the sheet according to
// the cursor position
api.start({ y: oy, immediate: true });
}
},
{
from: () => [0, y.get()],
filterTaps: true,
bounds: { top: -totalHeight, bottom: totalHeight - ROW_HEIGHT },
axis: 'y',
rubberband: true,
},
);
return (
<div
<animated.div
role="navigation"
{...bind()}
style={{
y,
touchAction: 'pan-x',
backgroundColor: theme.mobileNavBackground,
borderTop: `1px solid ${theme.menuBorder}`,
...styles.shadow,
display: isNarrowWidth ? 'flex' : 'none',
height,
justifyContent: 'space-around',
paddingTop: 10,
paddingBottom: 10,
height: totalHeight,
width: '100%',
position: 'fixed',
zIndex: 100,
bottom: isVisible ? 0 : -height,
transition: 'bottom 0.2s ease-out',
bottom: 0,
...(!isNarrowWidth && { display: 'none' }),
}}
>
<NavTab name="Budget" path="/budget" icon={Wallet} />
<NavTab name="Accounts" path="/accounts" icon={PiggyBank} />
<NavTab name="Transaction" path="/transactions/new" icon={Add} />
<NavTab name="Settings" path="/settings" icon={Cog} />
</div>
<View
style={{
flexDirection: 'row',
flexWrap: 'wrap',
height: totalHeight,
width: '100%',
}}
>
{[navTabs, bufferTabs]}
</View>
</animated.div>
);
}
......@@ -60,9 +207,10 @@ type NavTabProps = {
name: string;
path: string;
icon: ComponentType<NavTabIconProps>;
style?: CSSProperties;
};
function NavTab({ icon: TabIcon, name, path }: NavTabProps) {
function NavTab({ icon: TabIcon, name, path, style }: NavTabProps) {
return (
<NavLink
to={path}
......@@ -73,6 +221,8 @@ function NavTab({ icon: TabIcon, name, path }: NavTabProps) {
display: 'flex',
flexDirection: 'column',
textDecoration: 'none',
textAlign: 'center',
...style,
})}
>
<TabIcon width={22} height={22} />
......
---
category: Enhancements
authors: [joel-jeremy]
---
Swipe up mobile navbar to reveal more menus.
......@@ -74,6 +74,7 @@ __metadata:
"@types/react-router-dom": ^5.3.3
"@types/uuid": ^9.0.2
"@types/webpack-bundle-analyzer": ^4.6.0
"@use-gesture/react": ^10.3.0
chokidar: ^3.5.3
cross-env: ^7.0.3
date-fns: ^2.29.3
......@@ -4993,6 +4994,24 @@ __metadata:
languageName: node
linkType: hard
 
"@use-gesture/core@npm:10.3.0":
version: 10.3.0
resolution: "@use-gesture/core@npm:10.3.0"
checksum: cd6782b0cf61ae2306ecee4bd3c30942251427c142e3fd3584778d86e1a93b27e087033246700b54c4ad7063aa78747dc793f0dbb7434925c306215fb18dee82
languageName: node
linkType: hard
"@use-gesture/react@npm:^10.3.0":
version: 10.3.0
resolution: "@use-gesture/react@npm:10.3.0"
dependencies:
"@use-gesture/core": 10.3.0
peerDependencies:
react: ">= 16.8.0"
checksum: d43a2296e536ea8e4885ca082b7c554eabb0e19bb7f89b5db96e0511712c849db879de64c2746c94e3c9a5032e8918c90ace67fc023c754034d75b2ea3b727c4
languageName: node
linkType: hard
"@webassemblyjs/ast@npm:1.11.5, @webassemblyjs/ast@npm:^1.11.5":
version: 1.11.5
resolution: "@webassemblyjs/ast@npm:1.11.5"
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment