Skip to content

Commit c1a5564

Browse files
committed
feat(Popup): always render Popup in FloatingTree
1 parent f6b7f8e commit c1a5564

File tree

2 files changed

+148
-116
lines changed

2 files changed

+148
-116
lines changed

src/components/Popup/Popup.tsx

+76-63
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,16 @@ import * as React from 'react';
55
import {
66
FloatingFocusManager,
77
FloatingNode,
8+
FloatingTree,
89
arrow,
910
autoUpdate,
1011
offset as floatingOffset,
1112
limitShift,
1213
shift,
1314
useDismiss,
1415
useFloating,
16+
useFloatingNodeId,
17+
useFloatingParentNodeId,
1518
useInteractions,
1619
useRole,
1720
useTransitionStatus,
@@ -74,8 +77,6 @@ export interface PopupProps extends DOMProps, AriaLabelingProps, QAProps {
7477
floatingMiddlewares?: Middleware[];
7578
/** Floating UI context to provide interactions */
7679
floatingContext?: FloatingRootContext<ReferenceType>;
77-
/** Floating UI node id */
78-
floatingNodeId?: string;
7980
/** Additional floating element props to provide interactions */
8081
floatingInteractions?: ElementProps[];
8182
/** React ref floating element is attached to */
@@ -148,7 +149,7 @@ export interface PopupProps extends DOMProps, AriaLabelingProps, QAProps {
148149

149150
const b = block('popup');
150151

151-
export function Popup({
152+
function PopupComponent({
152153
keepMounted = false,
153154
hasArrow = false,
154155
open = false,
@@ -160,7 +161,6 @@ export function Popup({
160161
anchorRef,
161162
floatingMiddlewares,
162163
floatingContext,
163-
floatingNodeId,
164164
floatingInteractions,
165165
floatingRef,
166166
floatingStyles: floatingStylesProp,
@@ -225,6 +225,8 @@ export function Popup({
225225
[onOpenChange, onClose, onEscapeKeyDown, onOutsideClick],
226226
);
227227

228+
const floatingNodeId = useFloatingNodeId();
229+
228230
const {
229231
refs,
230232
elements,
@@ -320,70 +322,81 @@ export function Popup({
320322
}
321323
}
322324

323-
const content =
324-
isMounted || keepMounted ? (
325-
<Portal disablePortal={disablePortal}>
326-
<FloatingFocusManager
327-
context={context}
328-
disabled={!isMounted}
329-
modal={modal}
330-
initialFocus={initialFocus}
331-
returnFocus={returnFocus}
332-
closeOnFocusOut={!disableFocusOut}
333-
visuallyHiddenDismiss={disableVisuallyHiddenDismiss ? false : i18n('close')}
334-
guards={modal || !disablePortal}
335-
order={focusOrder}
336-
>
337-
<div
338-
ref={handleFloatingRef}
339-
className={floatingClassName}
340-
style={{
341-
position: 'absolute',
342-
top: 0,
343-
left: 0,
344-
zIndex,
345-
width: 'max-content',
346-
pointerEvents: isMounted ? 'auto' : 'none',
347-
outline: 'none',
348-
...floatingStyles,
349-
...floatingStylesProp,
350-
}}
351-
data-floating-ui-placement={finalPlacement}
352-
data-floating-ui-status={status}
353-
aria-modal={modal && isMounted ? true : undefined}
354-
{...getFloatingProps({
355-
onTransitionEnd: handleTransitionEnd,
356-
})}
325+
return (
326+
<FloatingNode id={floatingNodeId}>
327+
{isMounted || keepMounted ? (
328+
<Portal disablePortal={disablePortal}>
329+
<FloatingFocusManager
330+
context={context}
331+
disabled={!isMounted}
332+
modal={modal}
333+
initialFocus={initialFocus}
334+
returnFocus={returnFocus}
335+
closeOnFocusOut={!disableFocusOut}
336+
visuallyHiddenDismiss={disableVisuallyHiddenDismiss ? false : i18n('close')}
337+
guards={modal || !disablePortal}
338+
order={focusOrder}
357339
>
358340
<div
359-
ref={contentRef}
360-
className={b(
361-
{
362-
open: isMounted,
363-
'disable-transition': disableTransition,
364-
},
365-
className,
366-
)}
367-
style={style}
368-
data-qa={qa}
369-
{...filterDOMProps(restProps)}
341+
ref={handleFloatingRef}
342+
className={floatingClassName}
343+
style={{
344+
position: 'absolute',
345+
top: 0,
346+
left: 0,
347+
zIndex,
348+
width: 'max-content',
349+
pointerEvents: isMounted ? 'auto' : 'none',
350+
outline: 'none',
351+
...floatingStyles,
352+
...floatingStylesProp,
353+
}}
354+
data-floating-ui-placement={finalPlacement}
355+
data-floating-ui-status={status}
356+
aria-modal={modal && isMounted ? true : undefined}
357+
{...getFloatingProps({
358+
onTransitionEnd: handleTransitionEnd,
359+
})}
370360
>
371-
{hasArrow && (
372-
<PopupArrow
373-
ref={setArrowElement}
374-
styles={middlewareData.arrowStyles}
375-
/>
376-
)}
377-
{children}
361+
<div
362+
ref={contentRef}
363+
className={b(
364+
{
365+
open: isMounted,
366+
'disable-transition': disableTransition,
367+
},
368+
className,
369+
)}
370+
style={style}
371+
data-qa={qa}
372+
{...filterDOMProps(restProps)}
373+
>
374+
{hasArrow && (
375+
<PopupArrow
376+
ref={setArrowElement}
377+
styles={middlewareData.arrowStyles}
378+
/>
379+
)}
380+
{children}
381+
</div>
378382
</div>
379-
</div>
380-
</FloatingFocusManager>
381-
</Portal>
382-
) : null;
383+
</FloatingFocusManager>
384+
</Portal>
385+
) : null}
386+
</FloatingNode>
387+
);
388+
}
389+
390+
export function Popup(props: PopupProps) {
391+
const parentId = useFloatingParentNodeId();
383392

384-
if (floatingNodeId) {
385-
return <FloatingNode id={floatingNodeId}>{content}</FloatingNode>;
393+
if (parentId === null) {
394+
return (
395+
<FloatingTree>
396+
<PopupComponent {...props} />
397+
</FloatingTree>
398+
);
386399
}
387400

388-
return content;
401+
return <PopupComponent {...props} />;
389402
}

src/components/lab/Menu/Menu.tsx

+72-53
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,12 @@ import {
44
Composite,
55
CompositeItem,
66
FloatingList,
7-
FloatingTree,
87
flip,
98
offset,
109
safePolygon,
1110
shift,
1211
useClick,
1312
useDismiss,
14-
useFloatingNodeId,
1513
useFloatingParentNodeId,
1614
useFloatingRootContext,
1715
useFloatingTree,
@@ -39,7 +37,65 @@ import './Menu.scss';
3937

4038
const b = block('menu2');
4139

42-
function MenuComponent({
40+
// The component is needed to run submenu logic hooks.
41+
// We get <nodeId> of the Popup using "useFloatingParentNodeId" here
42+
// and <parentId> from using "useFloatingParentNodeId" outside the Popup.
43+
function MenuPopupContent({
44+
open,
45+
onRequestClose,
46+
parentId,
47+
children,
48+
className,
49+
style,
50+
qa,
51+
}: Pick<MenuProps, 'children' | 'className' | 'style' | 'qa'> & {
52+
open: boolean;
53+
onRequestClose: () => void;
54+
parentId: string | null;
55+
}) {
56+
const tree = useFloatingTree();
57+
const nodeId = useFloatingParentNodeId();
58+
59+
React.useEffect(() => {
60+
if (!tree) return;
61+
62+
function handleTreeClick() {
63+
// Closing only the root Menu so the closing animation runs once for all menus due to shared portal container
64+
if (!parentId) {
65+
onRequestClose();
66+
}
67+
}
68+
69+
function handleSubMenuOpen(event: {nodeId: string; parentId: string}) {
70+
// Closing on sibling submenu open
71+
if (event.nodeId !== nodeId && event.parentId === parentId) {
72+
onRequestClose();
73+
}
74+
}
75+
76+
tree.events.on('click', handleTreeClick);
77+
tree.events.on('menuopen', handleSubMenuOpen);
78+
79+
return () => {
80+
tree.events.off('click', handleTreeClick);
81+
tree.events.off('menuopen', handleSubMenuOpen);
82+
};
83+
}, [onRequestClose, tree, nodeId, parentId]);
84+
85+
React.useEffect(() => {
86+
if (open && tree) {
87+
tree.events.emit('menuopen', {parentId, nodeId});
88+
}
89+
}, [open, tree, nodeId, parentId]);
90+
91+
return (
92+
<div className={b(null, className)} style={style} data-qa={qa}>
93+
{children}
94+
</div>
95+
);
96+
}
97+
98+
export function Menu({
4399
trigger,
44100
inline = false,
45101
defaultOpen,
@@ -63,8 +119,6 @@ function MenuComponent({
63119
const itemsRef = React.useRef<Array<HTMLElement | null>>([]);
64120
const parentMenu = React.useContext(MenuContext);
65121

66-
const tree = useFloatingTree();
67-
const nodeId = useFloatingNodeId();
68122
const parentId = useFloatingParentNodeId();
69123
const isNested = Boolean(parentId);
70124

@@ -114,6 +168,10 @@ function MenuComponent({
114168
? trigger(anchorProps, anchorRef)
115169
: null;
116170

171+
const handleContentRequestClose = React.useCallback(() => {
172+
setIsOpen(false);
173+
}, [setIsOpen]);
174+
117175
const getItemPropsInline = React.useCallback(
118176
(userProps?: React.HTMLAttributes<HTMLElement>) => {
119177
const handleItemPointerEnter = (event: React.PointerEvent<HTMLElement>) => {
@@ -163,37 +221,6 @@ function MenuComponent({
163221
[parentMenu, inline, size, activeIndex, getItemPropsInline, getItemProps],
164222
);
165223

166-
React.useEffect(() => {
167-
if (!tree) return;
168-
169-
function handleTreeClick() {
170-
// Closing only the root Menu so the closing animation runs once for all menus due to shared portal container
171-
if (!parentId) {
172-
setIsOpen(false);
173-
}
174-
}
175-
176-
function handleSubMenuOpen(event: {nodeId: string; parentId: string}) {
177-
if (event.nodeId !== nodeId && event.parentId === parentId) {
178-
setIsOpen(false);
179-
}
180-
}
181-
182-
tree.events.on('click', handleTreeClick);
183-
tree.events.on('menuopen', handleSubMenuOpen);
184-
185-
return () => {
186-
tree.events.off('click', handleTreeClick);
187-
tree.events.off('menuopen', handleSubMenuOpen);
188-
};
189-
}, [setIsOpen, tree, nodeId, parentId]);
190-
191-
React.useEffect(() => {
192-
if (isOpen && tree) {
193-
tree.events.emit('menuopen', {parentId, nodeId});
194-
}
195-
}, [isOpen, tree, nodeId, parentId]);
196-
197224
React.useEffect(() => {
198225
if (!anchorNode) {
199226
if (trigger) {
@@ -251,37 +278,29 @@ function MenuComponent({
251278
placement={isNested ? `${isRTL ? 'left' : 'right'}-start` : placement}
252279
disablePortal={isNested}
253280
floatingContext={floatingContext}
254-
floatingNodeId={nodeId}
255281
floatingRef={setFloatingElement}
256282
floatingMiddlewares={middlewares}
257283
floatingInteractions={interactions}
258284
>
259285
<MenuContext.Provider value={contextValue}>
260286
<FloatingList elementsRef={itemsRef}>
261-
<div className={b(null, className)} style={style} data-qa={qa}>
287+
<MenuPopupContent
288+
open={isOpen}
289+
onRequestClose={handleContentRequestClose}
290+
parentId={parentId}
291+
className={className}
292+
style={style}
293+
qa={qa}
294+
>
262295
{children}
263-
</div>
296+
</MenuPopupContent>
264297
</FloatingList>
265298
</MenuContext.Provider>
266299
</Popup>
267300
</React.Fragment>
268301
);
269302
}
270303

271-
export function Menu(props: MenuProps) {
272-
const parentId = useFloatingParentNodeId();
273-
274-
if (!props.inline && parentId === null) {
275-
return (
276-
<FloatingTree>
277-
<MenuComponent {...props} />
278-
</FloatingTree>
279-
);
280-
}
281-
282-
return <MenuComponent {...props} />;
283-
}
284-
285304
Menu.displayName = 'Menu';
286305

287306
Menu.Trigger = MenuTrigger;

0 commit comments

Comments
 (0)