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

Swipe up mobile navbar #1758

Merged
merged 9 commits into from
Nov 13, 2023
Merged
Changes from 1 commit
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
Next Next commit
Swipe up mobile navbar to reveal more menus
joel-jeremy committed Nov 12, 2023
commit 6a36587753bf64ed3652df2f499934365f587be3
1 change: 1 addition & 0 deletions packages/desktop-client/package.json
Original file line number Diff line number Diff line change
@@ -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",
164 changes: 139 additions & 25 deletions packages/desktop-client/src/components/mobile/MobileNavTabs.tsx
Original file line number Diff line number Diff line change
@@ -1,53 +1,165 @@
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 Wallet from '../../icons/v1/Wallet';
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;

export default function MobileNavTabs() {
const { isNarrowWidth } = useResponsive();
const { scrollY, isBottomReached } = useScroll();
const { scrollY } = useScroll();

const totalRowCount = 2;
const rowHeight = 60;
const totalHeight = rowHeight * totalRowCount;
const openY = 0;
const closeY = rowHeight;
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 && 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 < -rowHeight) {
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 > rowHeight * 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: rowHeight * 0.5, bottom: totalHeight * 0.5 },
axis: 'y',
rubberband: true,
},
);

const navTabStyle = {
flex: '1 1 33%',
height: rowHeight,
padding: 10,
};

return (
<div
<animated.div
{...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,
display: isNarrowWidth ? 'flex' : '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%',
}}
>
<NavTab
style={navTabStyle}
name="Budget"
path="/budget"
icon={Wallet}
/>
<NavTab
style={navTabStyle}
name="Accounts"
path="/accounts"
icon={PiggyBank}
/>
<NavTab
style={navTabStyle}
name="Transaction"
path="/transactions/new"
icon={Add}
/>
<NavTab
style={navTabStyle}
name="Settings"
path="/settings"
icon={Cog}
/>
<div style={navTabStyle} />
<div style={navTabStyle} />
</View>
</animated.div>
);
}

@@ -60,9 +172,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 +186,7 @@ function NavTab({ icon: TabIcon, name, path }: NavTabProps) {
display: 'flex',
flexDirection: 'column',
textDecoration: 'none',
...style,
})}
>
<TabIcon width={22} height={22} />
19 changes: 19 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
@@ -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"