-
Notifications
You must be signed in to change notification settings - Fork 3.9k
Update advanced filters button to open filters popover #90029
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
Changes from all commits
e871e30
08de1f9
c9f0ff0
4f046a1
6494bbb
39408c1
c41f25c
d2c8363
ac1f36f
005706c
60da789
cd2962e
578ce2a
6c04b5c
9f1cf0d
57420fd
c01415f
de7eb78
6910745
99f43cf
39cfc19
f5f0c76
8d68975
248b146
2ba302f
f34f94c
d931fd0
507924a
aedd907
d379f35
882d363
1e7a9ec
72a6a00
d2b80a3
b1a34bb
6fe496d
4098768
f9f4743
4ca947e
ec83484
74ba7eb
baf24bc
bf8a94b
d3b85d0
128fd4c
05a448f
582228b
8cc6c14
14e44f1
f1fccd3
05a1774
6895eea
a2f027d
85d10a1
c8f5afc
c15ebac
ace4315
aab7585
e612a42
fb91c05
46d86b4
e092b58
2b5d723
607de86
44f06b2
86ff850
e734cdd
275a525
6066b6b
c592175
f330856
198669c
b49328e
6502c3e
7b363cf
6d247c5
4458bd5
ca0f6a0
bcd8722
f9a84cd
6060d98
4770830
ef26ce2
5d9570f
1c1596e
2561596
ebdf655
be9bcf1
3af9c38
5a05803
5d726a6
aaf94a0
e890eec
987b427
3106286
793ab44
ad4e4bf
5a29379
344efe8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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; |
| 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) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ❌ CONSISTENCY-2 (docs)
Extract it to a named constant: /** Cursor speed threshold (px/ms) below which the safe triangle apex is updated */
const SLOW_CURSOR_SPEED_THRESHOLD = 0.1;
// usage
if (speed === null || speed < SLOW_CURSOR_SPEED_THRESHOLD) {Reviewed at: c9f0ff0 | Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency. |
||
| 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}]} | ||
| 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) { | ||
|
bernhardoj marked this conversation as resolved.
|
||
| 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; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| import type {View} from 'react-native'; | ||
|
bernhardoj marked this conversation as resolved.
|
||
|
|
||
| type SafeTriangleProps = { | ||
| submenuRef: React.RefObject<View | null>; | ||
| children: React.ReactNode; | ||
| }; | ||
|
|
||
| export default SafeTriangleProps; | ||
Uh oh!
There was an error while loading. Please reload this page.