Skip to content

Commit 28e0438

Browse files
committed
[IMPL] usePopover hook
1 parent 901d644 commit 28e0438

11 files changed

Lines changed: 230 additions & 51 deletions

File tree

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { CSSProperties, useMemo } from "react"
2+
import { usePopover } from "../../../../../../packages/react-tools/src"
3+
4+
/**
5+
The component uses _usePopover_ hook to show a popover when a button is clicked.
6+
*/
7+
export const UsePopover = () => {
8+
const { Popover, isSupported, isOpen, showPopover} = usePopover({
9+
mode: "auto",
10+
})
11+
12+
const style = useMemo<CSSProperties>(() => ({
13+
width: 300,
14+
height: 200,
15+
position: "absolute"
16+
}), []);
17+
18+
return <div>
19+
<p>Is supported: {isSupported ? "Yes" : "No"}</p>
20+
<button onClick={showPopover} disabled={isOpen}>Open Popover</button>
21+
<Popover style={style}>
22+
<h2>
23+
Popover heading
24+
</h2>
25+
<p>Popover content</p>
26+
</Popover>
27+
</div>
28+
}

apps/react-tools-demo/src/constants/components.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,8 @@ export const COMPONENTS = [
103103
"useSpeechSynthesis",
104104
"useFPS",
105105
"usePIP",
106-
"useDocumentPIP"
106+
"useDocumentPIP",
107+
"usePopover"
107108
]
108109
],
109110
//UTILS
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# usePopover
2+
Hook to use [Popover API](https://developer.mozilla.org/en-US/docs/Web/API/Popover_API).
3+
4+
## Usage
5+
6+
```tsx
7+
export const UsePopover = () => {
8+
const { Popover, isSupported, isOpen, showPopover} = usePopover({
9+
mode: "auto",
10+
})
11+
12+
const style = useMemo<CSSProperties>(() => (isOpen ? {
13+
width: 200,
14+
height: 100,
15+
position: "absolute",
16+
inset: "unset",
17+
top: 10,
18+
margin: 0
19+
} : {}), [isOpen]);
20+
21+
return <div style={{ display: "flex", flexDirection: "column", gap: 20 }}>
22+
<p>Is supported: {isSupported}</p>
23+
<button onClick={showPopover} disabled={isOpen}>Open Popover</button>
24+
<Popover style={style}>
25+
<h2>
26+
Popover heading
27+
</h2>
28+
<p>Popover content</p>
29+
</Popover>
30+
</div>
31+
}
32+
```
33+
34+
> The component uses _usePopover_ hook to show a popover when a button is clicked.
35+
36+
37+
## API
38+
39+
```tsx
40+
usePopover({ mode, onBeforeToggle, onToggle }: UsePopoverProps): UsePopoverResult
41+
```
42+
43+
> ### Params
44+
>
45+
> - __param__: _UsePopoverProps_
46+
object
47+
> - __param.mode__: _"auto"|"manual"_
48+
popover state: __auto__ indicates that popover can be "light dismissed" by selecting outside the popover area, by contrast __manual__ popover must always be explicity hidden.
49+
> - __param.onBeforeToggle?__: _(evt: ToggleEvent) => void_
50+
function that will be executed before popover showed/hidden.
51+
> - __param.onToggle?__: _(evt: ToggleEvent) => void_
52+
function that will be executed when popover has been showed/hidden.
53+
>
54+
55+
> ### Returns
56+
>
57+
> __reuslt__: _UsePopoverResult_
58+
> Object with these properties:
59+
> - __isSupported__: boolean that indicates if Popover API is supported or not.
60+
> - __isSupported__: boolean that indicates if popover is opened or not.
61+
> - __showPopover__: function to show popover.
62+
> - __hidePopover__: function to hide popover.
63+
> - __togglePopover__: function to toggle popover.
64+
> - __Popover__: Component that wraps the element to render in popover. It can be stylized with _className_ and _style_ props.
65+
>

apps/react-tools-demo/src/markdown/usePrevious.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ function UsePrevious() {
88
const [count, setCount] = useState(0);
99
const [previous, toggleTrack] = usePrevious(count);
1010

11+
1112
return (<>
1213
<button onClick={() => setCount((count) => {
1314
const val = count + 1;

packages/react-tools/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,7 @@
106106
- [x] useFPS
107107
- [x] usePIP
108108
- [x] useDocumentPIP
109-
- [ ] useIdleDetection (not work yet. https://developer.mozilla.org/en-US/docs/Web/API/Idle_Detection_API)
110-
- [ ] usePopover (https://developer.mozilla.org/en-US/docs/Web/API/Popover_API)
109+
- [x] usePopover
111110
- [ ] useRemotePlayback (https://developer.mozilla.org/en-US/docs/Web/API/Remote_Playback_API)
112111
- [ ] useSensor (https://developer.mozilla.org/en-US/docs/Web/API/Sensor_APIs)
113112
- [ ] useSerial (https://developer.mozilla.org/en-US/docs/Web/API/Web_Serial_API)
@@ -131,6 +130,7 @@
131130
- [ ] useIndexedDB
132131
- [ ] useWebWorker (https://vueuse.org/core/useWebWorker/)
133132
- [ ] useWebWorkerFn (https://vueuse.org/core/useWebWorkerFn/)
133+
- [ ] useIdleDetection (not work yet. https://developer.mozilla.org/en-US/docs/Web/API/Idle_Detection_API)
134134

135135
- __UTILS__
136136
- [x] isShallowEqual

packages/react-tools/src/hooks/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,4 +83,5 @@ export { useSpeechSynthesis } from './useSpeechSynthesis';
8383
export { useFPS } from './useFPS';
8484
export { usePointerLock } from './usePointerLock';
8585
export { usePIP } from './usePIP';
86-
export { useDocumentPIP } from './useDocumentPIP';
86+
export { useDocumentPIP } from './useDocumentPIP';
87+
export { usePopover } from './usePopover';

packages/react-tools/src/hooks/usePopover.ts

Lines changed: 0 additions & 38 deletions
This file was deleted.
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { ComponentPropsWithRef, useCallback, useMemo, useRef } from "react";
2+
import { useId, useMergedRef, useSyncExternalStore } from "..";
3+
import { UsePopoverProps, UsePopoverResult } from "../models";
4+
5+
/**
6+
* **`usePopover`**: Hook to use [Popover API](https://developer.mozilla.org/en-US/docs/Web/API/Popover_API).
7+
* @param {UsePopoverProps} param - object
8+
* @param {"auto"|"manual"} param.mode - popover state: __auto__ indicates that popover can be "light dismissed" by selecting outside the popover area, by contrast __manual__ popover must always be explicity hidden.
9+
* @param {(evt: ToggleEvent) => void} [param.onBeforeToggle] - function that will be executed before popover showed/hidden.
10+
* @param {(evt: ToggleEvent) => void} [param.onToggle] - function that will be executed when popover has been showed/hidden.
11+
* @returns {UsePopoverResult} reuslt
12+
* Object with these properties:
13+
* - __isSupported__: boolean that indicates if Popover API is supported or not.
14+
* - __isSupported__: boolean that indicates if popover is opened or not.
15+
* - __showPopover__: function to show popover.
16+
* - __hidePopover__: function to hide popover.
17+
* - __togglePopover__: function to toggle popover.
18+
* - __Popover__: Component that wraps the element to render in popover. It can be stylized with _className_ and _style_ props.
19+
*/
20+
function usePopover({ mode, onBeforeToggle, onToggle }: UsePopoverProps): UsePopoverResult {
21+
const isSupported = "showPopover" in document.body;
22+
const id = useId();
23+
const isOpenCached = useRef(false);
24+
const notifyRef = useRef<() => void>();
25+
const onBeforeToggleCb = useCallback((evt: ToggleEvent) => {
26+
!!onBeforeToggle && onBeforeToggle(evt);
27+
}, [onBeforeToggle]);
28+
29+
const onToggleCb = useCallback((evt: ToggleEvent) => {
30+
console.log("onToggle");
31+
32+
isOpenCached.current = evt.newState === "open";
33+
!!onToggle && onToggle(evt);
34+
!!notifyRef.current && notifyRef.current();
35+
}, [onToggle]);
36+
37+
const popoverListenerRef = useCallback((node: HTMLDivElement) => {
38+
if (node) {
39+
node.removeEventListener("beforetoggle", onBeforeToggleCb as EventListenerOrEventListenerObject)
40+
node.removeEventListener("toggle", onToggleCb as EventListenerOrEventListenerObject)
41+
node.addEventListener("beforetoggle", onBeforeToggleCb as EventListenerOrEventListenerObject)
42+
node.addEventListener("toggle", onToggleCb as EventListenerOrEventListenerObject)
43+
}
44+
}, [onBeforeToggleCb, onToggleCb]);
45+
const popoverRef = useRef<HTMLDivElement>(null);
46+
47+
const ref = useMergedRef(popoverRef, popoverListenerRef);
48+
49+
const Popover = useMemo(() => {
50+
return (({ children, ...rest }: ComponentPropsWithRef<"div">) => {
51+
return "showPopover" in document.body && <div id={id} ref={ref} popover={mode} {...rest}>
52+
{children}
53+
</div>
54+
})
55+
}, [id, mode, ref]);
56+
57+
58+
const isOpen = useSyncExternalStore(
59+
useCallback(notify => {
60+
notifyRef.current = notify;
61+
return () => {
62+
notifyRef.current = undefined;
63+
}
64+
}, []),
65+
useCallback(() => isOpenCached.current, [])
66+
);
67+
68+
const showPopover = useCallback(() => {
69+
"showPopover" in document.body && popoverRef.current && popoverRef.current.showPopover();
70+
}, []);
71+
72+
const hidePopover = useCallback(() => {
73+
"showPopover" in document.body && popoverRef.current && popoverRef.current.hidePopover();
74+
}, []);
75+
76+
const togglePopover = useCallback(() => {
77+
"showPopover" in document.body && popoverRef.current && popoverRef.current.togglePopover();
78+
}, []);
79+
80+
if (popoverRef.current) {
81+
setTimeout(() => {
82+
if (popoverRef.current) {
83+
popoverRef.current.removeEventListener("beforetoggle", onBeforeToggleCb as EventListenerOrEventListenerObject)
84+
popoverRef.current.removeEventListener("toggle", onToggleCb as EventListenerOrEventListenerObject)
85+
isOpenCached.current && popoverRef.current?.showPopover();
86+
setTimeout(() => {
87+
popoverRef.current?.addEventListener("beforetoggle", onBeforeToggleCb as EventListenerOrEventListenerObject)
88+
popoverRef.current?.addEventListener("toggle", onToggleCb as EventListenerOrEventListenerObject)
89+
}, 1)
90+
}
91+
}, 1);
92+
}
93+
94+
return {
95+
isSupported,
96+
isOpen,
97+
showPopover,
98+
hidePopover,
99+
togglePopover,
100+
Popover
101+
}
102+
}
103+
104+
export { usePopover };

packages/react-tools/src/index.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,9 @@ export type {
5050
DocumentPIPOptions,
5151
DocumentPictureInPictureEvent,
5252
UseDocumentPIPProps,
53-
UseDocumentPIPResult
53+
UseDocumentPIPResult,
54+
UsePopoverProps,
55+
UsePopoverResult
5456
} from './models'
5557

5658
export {
@@ -139,7 +141,8 @@ export {
139141
useFPS,
140142
usePointerLock,
141143
usePIP,
142-
useDocumentPIP
144+
useDocumentPIP,
145+
usePopover
143146
} from './hooks'
144147

145148
export {

packages/react-tools/src/models/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,4 @@ export type { UseFPSProps, UseFPSResult } from './useFPS.model';
1515
export type { UsePointerLockProps, UsePointerLockResult } from './usePointerLock.model';
1616
export type { UsePIPProps, UsePIPResult } from './usePIP.model';
1717
export type { DocumentPictureInPictureEvent, DocumentPIPOptions, UseDocumentPIPProps, UseDocumentPIPResult } from './useDocumentPIP.model';
18-
export type { UsePopoverProps } from './usePopover.model';
18+
export type { UsePopoverProps, UsePopoverResult } from './usePopover.model';

0 commit comments

Comments
 (0)