Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
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
2 changes: 1 addition & 1 deletion assets/images/coins.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions assets/images/credit-card-multiple.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion assets/images/multifactorAuthentication/fingerprint.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions assets/images/paycheck.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions assets/images/user-arrow-left.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,9 @@ const CONST = {
POPOVER_DATE_RANGE_WIDTH: 672,
POPOVER_DATE_MAX_HEIGHT: 366,
POPOVER_DATE_MIN_HEIGHT: 322,
ADVANCED_FILTERS_POPOVER_HEIGHT: 520,
ADVANCED_FILTERS_POPOVER_WIDTH: 582,
ADVANCED_FILTERS_CONTENT_WIDTH: 331,
TOOLTIP_ANIMATION_DURATION: 500,
DROPDOWN_SCROLL_THRESHOLD: 5,
// Multiplier for gyroscope animation in order to make it a bit more subtle
Expand Down Expand Up @@ -9309,6 +9312,7 @@ const CONST = {
ADVANCED_FILTER_ITEM: 'Search-AdvancedFilterItem',
SAVE_SEARCH_BUTTON: 'Search-SaveSearchButton',
VIEW_RESULTS_BUTTON: 'Search-ViewResultsButton',
CLEAR_FILTERS_BUTTON: 'Search-ClearFiltersButton',
ACTION_CELL_VIEW: 'Search-ActionCellView',
ACTION_CELL_PAY: 'Search-ActionCellPay',
ACTION_CELL_ACTION: 'Search-ActionCellAction',
Expand Down
4 changes: 4 additions & 0 deletions src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -754,6 +754,10 @@ const ROUTES = {
return `${baseRoute}/${subPage}` as const;
},
},
SEARCH_ADVANCED_FILTERS_CONTENT: {
route: 'search/filters/:filterKey',
getRoute: (filterKey: SearchFilterKey | UserFriendlyKey) => `search/filters/${filterKey}` as const,
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For mobile view, any reason for not using existing legacy filter pages?

App/src/ROUTES.ts

Lines 747 to 756 in 52e1696

SEARCH_ADVANCED_FILTERS: {
route: 'search/filters/:filterKey?/:subPage?',
getRoute: (filterKey?: SearchFilterKey | UserFriendlyKey, subPage?: string) => {
const baseRoute = `search/filters/${filterKey ?? ''}` as const;
if (!subPage || !filterKey) {
return baseRoute;
}
return `${baseRoute}/${subPage}` as const;
},
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As we're going with navigation path, why not just reorder filter menu items in old filters page but completely refactored?

SEARCH_REPORT: {
route: 'search/view/:reportID/:reportActionID?',
getRoute: ({reportID, reportActionID, backTo}: {reportID: string | undefined; reportActionID?: string; backTo?: string}) => {
Expand Down
1 change: 1 addition & 0 deletions src/SCREENS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ const SCREENS = {
COLUMNS_RHP: 'Search_Columns_RHP',
REPORT_VERIFY_ACCOUNT: 'Search_Report_Verify_Account',
ADVANCED_FILTERS_RHP: 'Search_Advanced_Filters_RHP',
ADVANCED_FILTERS_CONTENT_RHP: 'Search_Advanced_Filters_Content_RHP',
ADVANCED_FILTERS_TYPE_RHP: 'Search_Advanced_Filters_Type_RHP',
ADVANCED_FILTERS_STATUS_RHP: 'Search_Advanced_Filters_Status_RHP',
ADVANCED_FILTERS_DATE_RHP: 'Search_Advanced_Filters_Date_RHP',
Expand Down
6 changes: 6 additions & 0 deletions src/components/Icon/chunks/expensify-icons.chunk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ import Copy from '@assets/images/copy.svg';
import CreditCardExclamation from '@assets/images/credit-card-exclamation.svg';
import CreditCardHourglass from '@assets/images/credit-card-hourglass.svg';
import CreditCardLock from '@assets/images/credit-card-lock.svg';
import CreditCardMultiple from '@assets/images/credit-card-multiple.svg';
import CreditCardWithPlaneHourglass from '@assets/images/credit-card-with-plane-hourglass.svg';
import CreditCardWithPlane from '@assets/images/credit-card-with-plane.svg';
import CreditCard from '@assets/images/creditcard.svg';
Expand Down Expand Up @@ -186,6 +187,7 @@ import OfflineCloud from '@assets/images/offline-cloud.svg';
import Offline from '@assets/images/offline.svg';
import Paperclip from '@assets/images/paperclip.svg';
import Pause from '@assets/images/pause.svg';
import Paycheck from '@assets/images/paycheck.svg';
import Pencil from '@assets/images/pencil.svg';
import Percent from '@assets/images/percent.svg';
import Phone from '@assets/images/phone.svg';
Expand Down Expand Up @@ -249,6 +251,7 @@ import TreasureChest from '@assets/images/treasure-chest.svg';
import Unlock from '@assets/images/unlock.svg';
import UploadAlt from '@assets/images/upload-alt.svg';
import Upload from '@assets/images/upload.svg';
import UserArrowLeft from '@assets/images/user-arrow-left.svg';
import UserCheck from '@assets/images/user-check.svg';
import UserEye from '@assets/images/user-eye.svg';
import UserLock from '@assets/images/user-lock.svg';
Expand Down Expand Up @@ -326,6 +329,7 @@ const Expensicons = {
ConnectionComplete,
Copy,
CreditCard,
CreditCardMultiple,
Crop,
CreditCardHourglass,
CreditCardExclamation,
Expand Down Expand Up @@ -427,6 +431,7 @@ const Expensicons = {
OdometerEnd,
Paperclip,
Pause,
Paycheck,
Pencil,
Percent,
Phone,
Expand Down Expand Up @@ -472,6 +477,7 @@ const Expensicons = {
Upload,
UploadAlt,
User,
UserArrowLeft,
UserCheck,
Users,
VideoSlash,
Expand Down
11 changes: 11 additions & 0 deletions src/components/SafeTriangle/index.native.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type SafeTriangleProps from './types';

/**
* A component that provides a "safe triangle" wrapper.
* On native platforms, hover interactions are not applicable, so this is a no-op wrapper.
*/
function SafeTriangle({children}: SafeTriangleProps) {
return children;
}

export default SafeTriangle;
194 changes: 194 additions & 0 deletions src/components/SafeTriangle/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import React, {useEffect, useRef, useState} from 'react';
import {View} from 'react-native';
import {Polygon, Svg} from 'react-native-svg';
import useThemeStyles from '@hooks/useThemeStyles';
import {isMobile} from '@libs/Browser';
import htmlDivElementRef from '@src/types/utils/htmlDivElementRef';
import type SafeTriangleProps from './types';

type Point = [number, number];

type SafeTriangleOverlayProps = {
submenuRef: React.RefObject<View | null>;
containerRef: React.RefObject<View | null>;
};

type Rect = {
top: number;
left: number;
width: number;
height: number;
};

/** Time in ms before the safe triangle is cleared after cursor stops moving toward submenu */
const SAFE_TRIANGLE_CLEAR_DELAY_MS = 50;

const OFFSET = 2;

function isPointInPolygon(point: Point, polygon: Point[]) {
const [x, y] = point;
let isInside = false;
const length = polygon.length;
for (let i = 0, j = length - 1; i < length; j = i++) {
const [xi, yi] = polygon.at(i) ?? [0, 0];
const [xj, yj] = polygon.at(j) ?? [0, 0];
const intersect = yi >= y !== yj >= y && x <= ((xj - xi) * (y - yi)) / (yj - yi) + xi;
if (intersect) {
isInside = !isInside;
}
}
return isInside;
}

function SafeTriangleOverlay({submenuRef, containerRef}: SafeTriangleOverlayProps) {
const styles = useThemeStyles();

const [points, setPoints] = useState<string | null>(null);
const [svgRect, setSvgRect] = useState<Rect | null>(null);

const apexRef = useRef<Point | null>(null);
const lastCursorPosition = useRef<Point | null>(null);
const lastCursorTime = useRef<number>(0);
const timeoutRef = useRef<NodeJS.Timeout | undefined>(undefined);

const getCursorSpeed = (x: number, y: number): number | null => {
const currentTime = performance.now();
const elapsedTime = currentTime - lastCursorTime.current;

if (lastCursorPosition.current === null || elapsedTime === 0) {
lastCursorPosition.current = [x, y];
lastCursorTime.current = currentTime;
return null;
}

const deltaX = x - lastCursorPosition.current[0];
const deltaY = y - lastCursorPosition.current[1];
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
const speed = distance / elapsedTime; // px / ms

lastCursorPosition.current = [x, y];
lastCursorTime.current = currentTime;

return speed;
};

const clearTriangle = () => {
setPoints(null);
setSvgRect(null);
apexRef.current = null;
};

const onMouseMove = (event: MouseEvent) => {
clearTimeout(timeoutRef.current);

const {clientX, clientY} = event;
const speed = getCursorSpeed(clientX, clientY);

if (!submenuRef.current) {
clearTriangle();
return;
}

const rect = submenuRef.current.getBoundingClientRect();
if (!rect) {
clearTriangle();
return;
}

// If speed is slow, update the apex to the current cursor position
if (speed === null || speed < 0.1) {
apexRef.current = [clientX, clientY];
}

const [x, y] = apexRef.current ?? [clientX, clientY];

// Create a polygon from apex to the submenu's left edge
const cursorPoint: Point = [0, y - rect.top];
// We subtract OFFSET from x-coordinates to account for the offset in the container's left style
const topLeftSubMenuPoint: Point = [rect.left - x - OFFSET, 0];
const bottomLeftSubMenuPoint: Point = [rect.left - x - OFFSET, rect.bottom - rect.top];

const polygon = [cursorPoint, topLeftSubMenuPoint, bottomLeftSubMenuPoint];

// Check if the current mouse position is within the safe triangle
// The polygon points are relative to [x + OFFSET, rect.top], so we adjust the mouse position accordingly
const isSafe = isPointInPolygon([clientX - x + OFFSET, clientY - rect.top], polygon);

if (isSafe) {
const pointsString = polygon.map((p) => p.join(',')).join(' ');
setPoints(pointsString);
setSvgRect({
top: rect.top,
left: x + OFFSET,
height: rect.height,
width: rect.left - x - OFFSET,
});
timeoutRef.current = setTimeout(clearTriangle, SAFE_TRIANGLE_CLEAR_DELAY_MS);
} else {
clearTriangle();
}
};

useEffect(() => {
const container = htmlDivElementRef(containerRef).current;
if (!container) {
return;
}

container.addEventListener('mousemove', onMouseMove, true);

return () => {
container.removeEventListener('mousemove', onMouseMove, true);
clearTimeout(timeoutRef.current);
};
}, [onMouseMove, containerRef]);

if (!points || !svgRect) {
return null;
}

return (
<Svg
style={[styles.pFixed, styles.cursorPointer, svgRect, {zIndex: 1000}]}
Comment thread
bernhardoj marked this conversation as resolved.
width={svgRect.width}
height={svgRect.height}
onWheel={clearTriangle}
pointerEvents="none"
>
<Polygon
points={points}
fill="transparent"
pointerEvents="auto"
/>
</Svg>
);
}

/**
* A component that creates a "safe triangle" area between the cursor and a submenu.
* This prevents the submenu from switching when the user moves the cursor
* diagonally towards the submenu by rendering an invisible SVG overlay.
*/
function SafeTriangle({submenuRef, children}: SafeTriangleProps) {
const styles = useThemeStyles();
const containerRef = useRef<View>(null);

if (isMobile()) {
return children;
}

return (
<View
ref={containerRef}
style={styles.flex1}
>
{children}
<SafeTriangleOverlay
containerRef={containerRef}
submenuRef={submenuRef}
/>
</View>
);
}

export default SafeTriangle;
8 changes: 8 additions & 0 deletions src/components/SafeTriangle/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type {View} from 'react-native';

type SafeTriangleProps = {
submenuRef: React.RefObject<View | null>;
children: React.ReactNode;
};

export default SafeTriangleProps;
Loading
Loading