diff --git a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-checks-that-settings-page-can-be-opened-1-chromium-linux.png b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-checks-that-settings-page-can-be-opened-1-chromium-linux.png index 66afa1a7866e980ade58f21f743bc12fa116eb61..6ca4a16dd3ddfdcdc661b5c236dbeb3228d210ca 100644 Binary files a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-checks-that-settings-page-can-be-opened-1-chromium-linux.png and b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-checks-that-settings-page-can-be-opened-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-checks-that-settings-page-can-be-opened-2-chromium-linux.png b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-checks-that-settings-page-can-be-opened-2-chromium-linux.png index 25eeeab52be1ef25e9626a989347c9e571371981..f52c2aec886feb2cba0de7bd923258c6693fe9a5 100644 Binary files a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-checks-that-settings-page-can-be-opened-2-chromium-linux.png and b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-checks-that-settings-page-can-be-opened-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-checks-that-settings-page-can-be-opened-3-chromium-linux.png b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-checks-that-settings-page-can-be-opened-3-chromium-linux.png index 46bf1b79121d31376fe0539a4e2e25801afbf614..b5934abb60d91677a97bc5bbd86cc7b509c97706 100644 Binary files a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-checks-that-settings-page-can-be-opened-3-chromium-linux.png and b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-checks-that-settings-page-can-be-opened-3-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-checks-that-settings-page-can-be-opened-4-chromium-linux.png b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-checks-that-settings-page-can-be-opened-4-chromium-linux.png index 3de1d448773b8cd5c37165660a859bbfe74783a9..2f8703bb8a4f47f6b45d635ed0093d7f04de3db5 100644 Binary files a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-checks-that-settings-page-can-be-opened-4-chromium-linux.png and b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-checks-that-settings-page-can-be-opened-4-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-loads-the-budget-page-with-budgeted-amounts-1-chromium-linux.png b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-loads-the-budget-page-with-budgeted-amounts-1-chromium-linux.png index 90efef73ea04b2aba2e2b4e25423809be6b80284..c8f465f9d28a425c75f19d366905ad921c390f4b 100644 Binary files a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-loads-the-budget-page-with-budgeted-amounts-1-chromium-linux.png and b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-loads-the-budget-page-with-budgeted-amounts-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-loads-the-budget-page-with-budgeted-amounts-2-chromium-linux.png b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-loads-the-budget-page-with-budgeted-amounts-2-chromium-linux.png index ae75ed2b3f1318cee0cd58dd7da4078db5f370e5..5540f29045f94c988a22ca8ca947490a21c14087 100644 Binary files a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-loads-the-budget-page-with-budgeted-amounts-2-chromium-linux.png and b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-loads-the-budget-page-with-budgeted-amounts-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-opens-the-accounts-page-and-asserts-on-balances-1-chromium-linux.png b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-opens-the-accounts-page-and-asserts-on-balances-1-chromium-linux.png index 43a9bdd4c2b0eb842d475b8fd95a07d507188b4f..0019449738afb4e2e00ebf52867e24174d1af811 100644 Binary files a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-opens-the-accounts-page-and-asserts-on-balances-1-chromium-linux.png and b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-opens-the-accounts-page-and-asserts-on-balances-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-opens-the-accounts-page-and-asserts-on-balances-2-chromium-linux.png b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-opens-the-accounts-page-and-asserts-on-balances-2-chromium-linux.png index de102f9e230f67f796662a43533d9fa8af136dd8..0cef62b3eda58234da6c26e3da4d5dd27a7a72b8 100644 Binary files a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-opens-the-accounts-page-and-asserts-on-balances-2-chromium-linux.png and b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-opens-the-accounts-page-and-asserts-on-balances-2-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/page-models/mobile-navigation.js b/packages/desktop-client/e2e/page-models/mobile-navigation.js index eeabd4b4c0dae071b6445c893f201b860662664c..ebda4cbb777da27e3a7df0311821451c98b36c81 100644 --- a/packages/desktop-client/e2e/page-models/mobile-navigation.js +++ b/packages/desktop-client/e2e/page-models/mobile-navigation.js @@ -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')); + } } diff --git a/packages/desktop-client/package.json b/packages/desktop-client/package.json index 921766ab9e23c2e1c86f34d798992f9db504b7ce..7d242da4567472964489b58b016f171f30fb77a6 100644 --- a/packages/desktop-client/package.json +++ b/packages/desktop-client/package.json @@ -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", diff --git a/packages/desktop-client/src/components/ScrollProvider.tsx b/packages/desktop-client/src/components/ScrollProvider.tsx index 06e59fb60e168f4d6b20a61f2472e79a451e4b1b..45c39e0ac8e08efe8d3d26850798d8995b968a2a 100644 --- a/packages/desktop-client/src/components/ScrollProvider.tsx +++ b/packages/desktop-client/src/components/ScrollProvider.tsx @@ -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, diff --git a/packages/desktop-client/src/components/mobile/MobileNavTabs.tsx b/packages/desktop-client/src/components/mobile/MobileNavTabs.tsx index afa3c89b73ac4aaa709152b1c6e3043c19f299a4..454351e11c3c863d44d374443e955e64f95849b9 100644 --- a/packages/desktop-client/src/components/mobile/MobileNavTabs.tsx +++ b/packages/desktop-client/src/components/mobile/MobileNavTabs.tsx @@ -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> ); } @@ -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} /> diff --git a/upcoming-release-notes/1758.md b/upcoming-release-notes/1758.md new file mode 100644 index 0000000000000000000000000000000000000000..6c53c11f4a13b8e38c4f5ebb34d7a811c5fbfdde --- /dev/null +++ b/upcoming-release-notes/1758.md @@ -0,0 +1,6 @@ +--- +category: Enhancements +authors: [joel-jeremy] +--- + +Swipe up mobile navbar to reveal more menus. diff --git a/yarn.lock b/yarn.lock index 84e07d99b375a76df8515d566ab29599d4171018..0c751260680c1176a77bf62156366ebc2bbe68c8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"