11'use client' ;
22
33import * as React from 'react' ;
4- import {
5- useControllableState ,
6- useEventCallback ,
7- useOnClickOutside ,
8- useOnScrollOutside ,
9- elementContains ,
10- useTimeout ,
11- } from '@fluentui/react-utilities' ;
4+ import { useOnClickOutside , useOnScrollOutside , elementContains } from '@fluentui/react-utilities' ;
125import { useFluent_unstable as useFluent } from '@fluentui/react-shared-contexts' ;
13- import { usePositioning , resolvePositioningShorthand } from '../../hooks' ;
14- import type { PopoverProps , PopoverState , PopoverContextValue , OpenPopoverEvents , PopoverType } from './Popover.types' ;
6+ import type { PopoverProps , PopoverState } from './Popover.types' ;
7+ import { usePopoverBase , ensureNativePopoverShown } from './usePopoverBase' ;
8+
9+ export { usePopoverContextValues } from './usePopoverBase' ;
10+
11+ /**
12+ * Returns the state for a Popover component.
13+ *
14+ * Renders the surface with `popover="manual"`, leaving dismiss behaviour
15+ * (click-outside, scroll, Escape) under React's control. Open state is
16+ * the React state — the surface is in the top layer for painting only,
17+ * never owns visibility decisions.
18+ */
19+ export const usePopover = ( props : PopoverProps ) : PopoverState => {
20+ const base = usePopoverBase ( props ) ;
21+ const { open, setOpen, triggerRef, contentRef, inline, openOnContext, closeOnScroll, closeOnIframeFocus } = base ;
1522
16- const SUPPORTS_POPOVER_OPEN_SELECTOR =
17- typeof CSS !== 'undefined' && typeof CSS . supports === 'function' && CSS . supports ( 'selector(:popover-open)' ) ;
18-
19- type ToggleEventLike = Event & { newState ?: 'open' | 'closed' } ;
20-
21- function useInternalPopover ( props : PopoverProps , popoverType : PopoverType ) : PopoverState {
22- const {
23- openOnHover = false ,
24- openOnContext = false ,
25- mouseLeaveDelay = 500 ,
26- withArrow = false ,
27- disableAutoFocus = false ,
28- closeOnScroll = false ,
29- closeOnIframeFocus = true ,
30- inline = false ,
31- mountNode,
32- } = props ;
33-
34- const [ open , setOpenState ] = useControllableState ( {
35- state : props . open ,
36- defaultState : props . defaultOpen ,
37- initialState : false ,
38- } ) ;
39-
40- const [ contextTarget , setContextTarget ] = React . useState < { x : number ; y : number } | undefined > ( undefined ) ;
4123 const { targetDocument } = useFluent ( ) ;
4224
43- const onOpenChange = useEventCallback ( ( e : OpenPopoverEvents , shouldOpen : boolean ) => {
44- props . onOpenChange ?.( e , { event : e , type : e . type , open : shouldOpen } ) ;
45- } ) ;
46-
47- const [ setOpenTimeout , clearOpenTimeout ] = useTimeout ( ) ;
48-
49- const setOpen = useEventCallback ( ( e : OpenPopoverEvents , shouldOpen : boolean ) => {
50- clearOpenTimeout ( ) ;
51-
52- if ( shouldOpen && e . type === 'contextmenu' ) {
53- const mouseEvent = e as React . MouseEvent < HTMLElement > ;
54- setContextTarget ( { x : mouseEvent . clientX , y : mouseEvent . clientY } ) ;
55- }
56-
57- if ( ! shouldOpen ) {
58- setContextTarget ( undefined ) ;
59- }
60-
61- if ( e . type === 'mouseleave' ) {
62- setOpenTimeout ( ( ) => {
63- setOpenState ( shouldOpen ) ;
64- onOpenChange ( e , shouldOpen ) ;
65- } , mouseLeaveDelay ) ;
66- } else {
67- setOpenState ( shouldOpen ) ;
68- onOpenChange ( e , shouldOpen ) ;
69- }
70- } ) ;
71-
72- const toggleOpen = React . useCallback (
73- ( e : OpenPopoverEvents ) => {
74- setOpen ( e , ! open ) ;
75- } ,
76- [ setOpen , open ] ,
77- ) ;
78-
79- const triggerRef = React . useRef < HTMLElement > ( null ) ;
80- const contentRef = React . useRef < HTMLElement > ( null ) ;
81- const arrowRef = React . useRef < HTMLDivElement > ( null ) ;
82-
83- const positioning = usePositioning ( resolvePositioningShorthand ( props . positioning ) ) ;
84-
85- const isAutoMode = popoverType === 'auto' && ! inline ;
86-
8725 useOnClickOutside ( {
8826 contains : elementContains ,
8927 element : targetDocument ,
9028 callback : ev => setOpen ( ev , false ) ,
9129 refs : [ triggerRef , contentRef ] ,
92- disabled : ! open || isAutoMode ,
30+ disabled : ! open ,
9331 disabledFocusOnIframe : ! closeOnIframeFocus ,
9432 } ) ;
9533
@@ -98,142 +36,18 @@ function useInternalPopover(props: PopoverProps, popoverType: PopoverType): Popo
9836 element : targetDocument ,
9937 callback : ev => setOpen ( ev , false ) ,
10038 refs : [ triggerRef , contentRef ] ,
101- disabled : ! open || ! ( openOnContext || closeOnScroll ) || isAutoMode ,
39+ disabled : ! open || ! ( openOnContext || closeOnScroll ) ,
10240 } ) ;
10341
104- // Mirror the browser-driven toggle events into React state when in auto mode.
105- // Covers Escape, click-outside, and the popover-stack dismissal that happens
106- // when an unrelated `popover="auto"` opens. Skip the no-op transition the
107- // browser fires for our own `showPopover()` call (newState='open' while
108- // React already has `open=true`).
109- const onSurfaceToggle = useEventCallback ( ( event : Event ) => {
110- const toggle = event as ToggleEventLike ;
111- const nextOpen = toggle . newState === 'open' ;
112- if ( nextOpen === open ) {
113- return ;
114- }
115- setOpenState ( nextOpen ) ;
116- props . onOpenChange ?.( event , { event, type : event . type , open : nextOpen } ) ;
117- } ) ;
118-
119- // The surface is unmounted while closed (`state.open ? popoverSurface : null`),
120- // so this effect must re-run when `open` flips so we attach `showPopover()`
121- // and the `toggle` listener to the freshly-mounted surface element.
12242 React . useEffect ( ( ) => {
12343 const surface = contentRef . current ;
12444
12545 if ( ! surface || inline || ! open ) {
12646 return ;
12747 }
12848
129- if ( typeof surface . showPopover !== 'function' ) {
130- return ;
131- }
132-
133- if ( ! surface . hasAttribute ( 'popover' ) || surface . getAttribute ( 'popover' ) !== popoverType ) {
134- surface . setAttribute ( 'popover' , popoverType ) ;
135- }
136-
137- if ( ! ( SUPPORTS_POPOVER_OPEN_SELECTOR && surface . matches ( ':popover-open' ) ) ) {
138- surface . showPopover ( ) ;
139- }
140-
141- if ( popoverType !== 'auto' ) {
142- return ;
143- }
144-
145- surface . addEventListener ( 'toggle' , onSurfaceToggle ) ;
146- return ( ) => surface . removeEventListener ( 'toggle' , onSurfaceToggle ) ;
147- } , [ open , inline , popoverType , onSurfaceToggle ] ) ;
148-
149- const children = React . Children . toArray ( props . children ) as React . ReactElement [ ] ;
150-
151- if ( process . env . NODE_ENV !== 'production' ) {
152- if ( children . length === 0 ) {
153- // eslint-disable-next-line no-console
154- console . warn ( 'Popover must contain at least one child' ) ;
155- }
156-
157- if ( children . length > 2 ) {
158- // eslint-disable-next-line no-console
159- console . warn ( 'Popover must contain at most two children' ) ;
160- }
161- }
162-
163- let popoverTrigger : React . ReactElement | undefined ;
164- let popoverSurface : React . ReactElement | undefined ;
165-
166- if ( children . length === 2 ) {
167- popoverTrigger = children [ 0 ] ;
168- popoverSurface = children [ 1 ] ;
169- } else if ( children . length === 1 ) {
170- popoverSurface = children [ 0 ] ;
171- }
172-
173- return {
174- open,
175- setOpen,
176- toggleOpen,
177- triggerRef,
178- contentRef,
179- arrowRef,
180- popoverTrigger,
181- popoverSurface,
182- openOnHover,
183- openOnContext,
184- withArrow,
185- disableAutoFocus,
186- inline,
187- mountNode,
188- onOpenChange : props . onOpenChange ,
189- contextTarget,
190- setContextTarget,
191- positioning,
192- popoverType,
193- } ;
194- }
195-
196- export const usePopover = ( props : PopoverProps ) : PopoverState => useInternalPopover ( props , 'manual' ) ;
197-
198- export const usePopoverAuto = ( props : PopoverProps ) : PopoverState => useInternalPopover ( props , 'auto' ) ;
199-
200- export const usePopoverContextValues = ( state : PopoverState ) : { popover : PopoverContextValue } => {
201- const {
202- open,
203- setOpen,
204- toggleOpen,
205- triggerRef,
206- contentRef,
207- arrowRef,
208- openOnHover,
209- openOnContext,
210- disableAutoFocus,
211- withArrow,
212- inline,
213- mountNode,
214- positioning,
215- popoverType,
216- } = state ;
49+ ensureNativePopoverShown ( surface , 'manual' ) ;
50+ } , [ open , inline , contentRef ] ) ;
21751
218- return {
219- popover : {
220- open,
221- setOpen,
222- toggleOpen,
223- triggerRef,
224- contentRef,
225- arrowRef,
226- openOnHover,
227- openOnContext,
228- disableAutoFocus,
229- withArrow,
230- inline,
231- mountNode,
232- popoverType,
233- positioning : {
234- targetRef : positioning . targetRef ,
235- containerRef : positioning . containerRef ,
236- } ,
237- } ,
238- } ;
52+ return { ...base , popoverType : 'manual' } ;
23953} ;
0 commit comments