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
Show file tree
Hide file tree
Changes from all commits
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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions packages/desktop-client/e2e/page-models/mobile-navigation.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
}
}
1 change: 1 addition & 0 deletions packages/desktop-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion packages/desktop-client/src/components/ScrollProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
198 changes: 174 additions & 24 deletions packages/desktop-client/src/components/mobile/MobileNavTabs.tsx
Original file line number Diff line number Diff line change
@@ -1,53 +1,200 @@
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>
);
}

Expand All @@ -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}
Expand All @@ -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} />
Expand Down
6 changes: 6 additions & 0 deletions upcoming-release-notes/1758.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
category: Enhancements
authors: [joel-jeremy]
---

Swipe up mobile navbar to reveal more menus.
19 changes: 19 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down
Loading