-
Notifications
You must be signed in to change notification settings - Fork 11
Expand file tree
/
Copy pathuseMenuPositioner.ts
More file actions
89 lines (77 loc) · 3.3 KB
/
useMenuPositioner.ts
File metadata and controls
89 lines (77 loc) · 3.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
import { useEffect, useState, useRef } from 'react';
import { MenuPositionEnum } from '../constants';
import useCallbackRef from './useCallbackRef';
import useUpdateEffect from './useUpdateEffect';
import { calculateMenuTop, menuFitsBelowControl, scrollMenuIntoViewOnOpen } from '../utils';
import type { RefObject } from 'react';
import type { CallbackFunction } from '../types';
/**
* useMenuPositioner hook
*
* Handle calculating and maintaining the menuHeight used by react-window.
* Handle scroll animation and callback execution when menuOpen = true.
* Handle resetting menuHeight back to the menuHeightDefault and callback execution when menuOpen = false.
* Use ref to track if the menuHeight was resized, and if so, set the menu height back to default (avoids uncessary renders) with call to setMenuHeight.
* Handle determining where to place the menu in relation to control - when menuPosition = 'top' or menuPosition = 'bottom' and there is not sufficient space below control, place on top.
*/
const useMenuPositioner = (
menuRef: RefObject<HTMLElement | null>,
controlRef: RefObject<HTMLElement | null>,
menuOpen: boolean,
menuPosition: MenuPositionEnum,
menuHeightDefault: number,
menuSize: number,
isMenuPortaled: boolean,
onMenuOpen?: CallbackFunction,
onMenuClose?: CallbackFunction,
menuScrollDuration?: number,
scrollMenuIntoView?: boolean
): [string | undefined, number] => {
const resetMenuHeightRef = useRef<boolean>(false);
const shouldScrollRef = useRef<boolean>(!isMenuPortaled);
const onMenuOpenRef = useCallbackRef(onMenuOpen);
const onMenuCloseRef = useCallbackRef(onMenuClose);
const [menuHeight, setMenuHeight] = useState<number>(menuHeightDefault);
const [isMenuTopPosition, setIsMenuTopPosition] = useState<boolean>(false);
useEffect(() => {
shouldScrollRef.current = !isMenuTopPosition && !isMenuPortaled;
}, [isMenuTopPosition, isMenuPortaled]);
useEffect(() => {
const isTopPos =
menuPosition === MenuPositionEnum.TOP ||
(menuPosition === MenuPositionEnum.AUTO && !menuFitsBelowControl(menuRef.current));
setIsMenuTopPosition(isTopPos);
}, [menuRef, menuPosition]);
useUpdateEffect(() => {
if (menuOpen) {
const handleOnMenuOpen = (availableSpace?: number): void => {
onMenuOpenRef();
if (availableSpace) {
resetMenuHeightRef.current = true;
setMenuHeight(availableSpace);
}
};
shouldScrollRef.current
? scrollMenuIntoViewOnOpen(
menuRef.current,
menuScrollDuration,
scrollMenuIntoView,
handleOnMenuOpen
)
: handleOnMenuOpen();
} else {
onMenuCloseRef();
if (resetMenuHeightRef.current) {
resetMenuHeightRef.current = false;
setMenuHeight(menuHeightDefault);
}
}
}, [menuRef, menuOpen, menuHeightDefault, scrollMenuIntoView, menuScrollDuration, onMenuCloseRef, onMenuOpenRef]);
// Calculated menu height passed react-window; calculate MenuWrapper <div /> 'top' style prop if menu is positioned above control
const menuHeightCalc = Math.min(menuHeight, menuSize);
const menuStyleTop = isMenuTopPosition
? calculateMenuTop(menuHeightCalc, menuRef.current, controlRef.current)
: undefined;
return [menuStyleTop, menuHeightCalc];
};
export default useMenuPositioner;