Skip to content

Commit 4f5ce2f

Browse files
committed
feat(utils): implemented a reusable hover mode API
1 parent cff46c4 commit 4f5ce2f

File tree

7 files changed

+890
-0
lines changed

7 files changed

+890
-0
lines changed
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import React, {
2+
ReactElement,
3+
ReactNode,
4+
useCallback,
5+
useEffect,
6+
useMemo,
7+
useRef,
8+
useState,
9+
} from "react";
10+
11+
import { useOnUnmount } from "../useOnUnmount";
12+
import {
13+
DEFAULT_HOVER_MODE_DEACTIVATION_TIME,
14+
DEFAULT_HOVER_MODE_VISIBLE_IN_TIME,
15+
} from "./constants";
16+
import {
17+
HoverModeContext,
18+
HoverModeContextProvider,
19+
} from "./useHoverModeContext";
20+
21+
/** @remarks \@since 2.8.0 */
22+
export interface HoverModeConfiguration {
23+
/**
24+
* Boolean if the hover mode functionality should be disabled.
25+
*
26+
* @defaultValue `false`
27+
*/
28+
disabled?: boolean;
29+
30+
/**
31+
* The amount of time (in ms) the user must hover an element before the hover
32+
* mode is enabled and the visibility is set to `true`.
33+
*
34+
* @defaultValue {@link DEFAULT_HOVER_MODE_VISIBLE_IN_TIME}
35+
*/
36+
defaultVisibleInTime?: number;
37+
38+
/**
39+
* The amount of time (in ms) the user must not hover any element connected to
40+
* the hover mode.
41+
*
42+
* @defaultValue {@link DEFAULT_HOVER_MODE_DEACTIVATION_TIME}
43+
*/
44+
deactivateTime?: number;
45+
}
46+
47+
/** @remarks \@since 2.8.0 */
48+
export interface HoverModeProviderProps extends HoverModeConfiguration {
49+
children: ReactNode;
50+
}
51+
52+
/**
53+
* This component should normally be mounted near the root of your app to enable
54+
* hover mode for child components. However, it can also be used at other levels
55+
* if hover mode functionality should not carry over between two different parts
56+
* of the screen.
57+
*
58+
* @example
59+
* Separating Hover Mode
60+
* ```tsx
61+
* export default function Example(): ReactElement {
62+
* return (
63+
* <>
64+
* <HoverModeProvider>
65+
* <HeaderActions />
66+
* </HoverModeProvider>
67+
* <HoverModeProvider>
68+
* <MainContent />
69+
* </HoverModeProvider>
70+
* </>
71+
* );
72+
* }
73+
* ```
74+
*
75+
* @remarks \@since 2.8.0
76+
*/
77+
export function HoverModeProvider({
78+
children,
79+
disabled = false,
80+
defaultVisibleInTime = DEFAULT_HOVER_MODE_VISIBLE_IN_TIME,
81+
deactivateTime = DEFAULT_HOVER_MODE_DEACTIVATION_TIME,
82+
}: HoverModeProviderProps): ReactElement {
83+
const [visibleInTime, setVisibleInTime] = useState(defaultVisibleInTime);
84+
const timeoutRef = useRef<number>();
85+
const enableHoverMode = useCallback(() => {
86+
if (disabled) {
87+
return;
88+
}
89+
90+
window.clearTimeout(timeoutRef.current);
91+
setVisibleInTime(0);
92+
}, [disabled]);
93+
const disableHoverMode = useCallback(() => {
94+
window.clearTimeout(timeoutRef.current);
95+
setVisibleInTime(defaultVisibleInTime);
96+
}, [defaultVisibleInTime]);
97+
98+
const startDisableTimer = useCallback(() => {
99+
window.clearTimeout(timeoutRef.current);
100+
timeoutRef.current = window.setTimeout(() => {
101+
setVisibleInTime(defaultVisibleInTime);
102+
}, deactivateTime);
103+
}, [defaultVisibleInTime, deactivateTime]);
104+
105+
useEffect(() => {
106+
if (disabled) {
107+
window.clearTimeout(timeoutRef.current);
108+
setVisibleInTime(defaultVisibleInTime);
109+
}
110+
}, [disabled, defaultVisibleInTime]);
111+
112+
useOnUnmount(() => {
113+
window.clearTimeout(timeoutRef.current);
114+
});
115+
116+
const context = useMemo<HoverModeContext>(
117+
() => ({
118+
visibleInTime,
119+
enableHoverMode,
120+
disableHoverMode,
121+
startDisableTimer,
122+
}),
123+
[disableHoverMode, enableHoverMode, startDisableTimer, visibleInTime]
124+
);
125+
126+
return (
127+
<HoverModeContextProvider value={context}>
128+
{children}
129+
</HoverModeContextProvider>
130+
);
131+
}
132+
133+
/* istanbul ignore next */
134+
if (process.env.NODE_ENV !== "production") {
135+
try {
136+
const PropTypes = require("prop-types");
137+
138+
HoverModeProvider.propTypes = {
139+
disabled: PropTypes.bool,
140+
defaultVisibleInTime: PropTypes.number,
141+
deactivateTime: PropTypes.number,
142+
children: PropTypes.node.isRequired,
143+
};
144+
} catch (e) {}
145+
}

0 commit comments

Comments
 (0)