Skip to content

Commit 2cd4e86

Browse files
Merge pull request #2404 from dxc-technology/jialecl/portal-check
[patch] Making sure that the popover container is ready
2 parents a874796 + 7086be9 commit 2cd4e86

6 files changed

Lines changed: 188 additions & 152 deletions

File tree

packages/lib/src/base-menu/GroupItem.tsx

Lines changed: 62 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useContext, useId } from "react";
1+
import { useContext, useEffect, useId, useState } from "react";
22
import DxcIcon from "../icon/Icon";
33
import SubMenu from "./SubMenu";
44
import ItemAction from "./ItemAction";
@@ -10,14 +10,17 @@ import BaseMenuContext from "./BaseMenuContext";
1010

1111
const GroupItem = ({ items, ...props }: GroupItemProps) => {
1212
const groupMenuId = `group-menu-${useId()}`;
13-
14-
const NavigationTreeId = `sidenav-${useId()}`;
13+
const navigationTreeId = `sidenav-${useId()}`;
1514
const contextValue = useContext(BaseMenuContext) ?? {};
1615
const { groupSelected, isOpen, toggleOpen, hasPopOver, isHorizontal } = useGroupItem(
1716
items,
1817
contextValue,
1918
props.defaultOpen
2019
);
20+
const [portalContainer, setPortalContainer] = useState<HTMLElement | null>(null);
21+
useEffect(() => {
22+
setPortalContainer(document?.getElementById(`${navigationTreeId}-portal`));
23+
}, []);
2124

2225
return hasPopOver ? (
2326
<>
@@ -38,60 +41,64 @@ const GroupItem = ({ items, ...props }: GroupItemProps) => {
3841
{...props}
3942
/>
4043
</Popover.Trigger>
41-
<Popover.Portal container={document.getElementById(`${NavigationTreeId}-portal`)}>
42-
<BaseMenuContext.Provider value={{ ...contextValue, displayGroupLines: false, hasPopOver: false }}>
43-
<Popover.Content
44-
aria-label="Group details"
45-
onKeyDown={(event) => {
46-
if (event.key === "Escape") {
47-
toggleOpen();
48-
}
49-
}}
50-
align="start"
51-
side={isHorizontal ? "bottom" : "right"}
52-
style={{
53-
zIndex: "var(--z-contextualmenu)",
54-
padding: "var(--spacing-padding-xs)",
55-
boxShadow: "var(--shadow-100)",
56-
backgroundColor: "var(--color-bg-neutral-lightest)",
57-
borderRadius: "var(--border-radius-m)",
58-
...(isHorizontal
59-
? {}
60-
: {
61-
display: "flex",
62-
flexDirection: "column",
63-
gap: "var(--spacing-gap-xxs)",
64-
}),
65-
}}
66-
sideOffset={isHorizontal ? 16 : 0}
67-
onInteractOutside={() => toggleOpen()}
68-
>
69-
{!isHorizontal && props.depthLevel === 0 && (
70-
<ItemAction
71-
aria-controls={isOpen ? groupMenuId : undefined}
72-
aria-expanded={isOpen ? true : undefined}
73-
aria-pressed={groupSelected && !isOpen}
74-
collapseIcon={isOpen ? <DxcIcon icon="filled_expand_less" /> : <DxcIcon icon="filled_expand_more" />}
75-
onClick={() => toggleOpen()}
76-
selected={groupSelected && !isOpen}
77-
{...props}
78-
icon={undefined}
79-
/>
80-
)}
81-
<SubMenu id={groupMenuId} depthLevel={props.depthLevel} isPopOver={true}>
82-
{items.map((item, index) => (
83-
<MenuItem
84-
item={item}
85-
depthLevel={isHorizontal ? props.depthLevel : props.depthLevel + 1}
86-
key={`${item.label}-${index}`}
44+
{portalContainer && (
45+
<Popover.Portal container={portalContainer}>
46+
<BaseMenuContext.Provider value={{ ...contextValue, displayGroupLines: false, hasPopOver: false }}>
47+
<Popover.Content
48+
aria-label="Group details"
49+
onKeyDown={(event) => {
50+
if (event.key === "Escape") {
51+
toggleOpen();
52+
}
53+
}}
54+
align="start"
55+
side={isHorizontal ? "bottom" : "right"}
56+
style={{
57+
zIndex: "var(--z-contextualmenu)",
58+
padding: "var(--spacing-padding-xs)",
59+
boxShadow: "var(--shadow-100)",
60+
backgroundColor: "var(--color-bg-neutral-lightest)",
61+
borderRadius: "var(--border-radius-m)",
62+
...(isHorizontal
63+
? {}
64+
: {
65+
display: "flex",
66+
flexDirection: "column",
67+
gap: "var(--spacing-gap-xxs)",
68+
}),
69+
}}
70+
sideOffset={isHorizontal ? 16 : 0}
71+
onInteractOutside={() => toggleOpen()}
72+
>
73+
{!isHorizontal && props.depthLevel === 0 && (
74+
<ItemAction
75+
aria-controls={isOpen ? groupMenuId : undefined}
76+
aria-expanded={isOpen ? true : undefined}
77+
aria-pressed={groupSelected && !isOpen}
78+
collapseIcon={
79+
isOpen ? <DxcIcon icon="filled_expand_less" /> : <DxcIcon icon="filled_expand_more" />
80+
}
81+
onClick={() => toggleOpen()}
82+
selected={groupSelected && !isOpen}
83+
{...props}
84+
icon={undefined}
8785
/>
88-
))}
89-
</SubMenu>
90-
</Popover.Content>
91-
</BaseMenuContext.Provider>
92-
</Popover.Portal>
86+
)}
87+
<SubMenu id={groupMenuId} depthLevel={props.depthLevel} isPopOver={true}>
88+
{items.map((item, index) => (
89+
<MenuItem
90+
item={item}
91+
depthLevel={isHorizontal ? props.depthLevel : props.depthLevel + 1}
92+
key={`${item.label}-${index}`}
93+
/>
94+
))}
95+
</SubMenu>
96+
</Popover.Content>
97+
</BaseMenuContext.Provider>
98+
</Popover.Portal>
99+
)}
93100
</Popover.Root>
94-
<div id={`${NavigationTreeId}-portal`} style={{ position: "absolute" }} />
101+
<div id={`${navigationTreeId}-portal`} style={{ position: "absolute" }} />
95102
</>
96103
) : (
97104
<>

packages/lib/src/date-input/DateInput.tsx

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,8 @@ const DxcDateInput = forwardRef<RefType, DateInputPropsType>(
150150
: null
151151
);
152152
const [sideOffset, setSideOffset] = useState(SIDEOFFSET);
153+
const [portalContainer, setPortalContainer] = useState<HTMLElement | null>(null);
154+
153155
const translatedLabels = useContext(HalstackLanguageContext);
154156
const dateRef = useRef<HTMLDivElement | null>(null);
155157
const popoverContentRef = useRef<HTMLDivElement | null>(null);
@@ -258,6 +260,9 @@ const DxcDateInput = forwardRef<RefType, DateInputPropsType>(
258260
closeCalendar();
259261
}
260262
};
263+
useEffect(() => {
264+
setPortalContainer(document?.getElementById(`${calendarId}-portal`));
265+
}, []);
261266

262267
useEffect(() => {
263268
window.addEventListener("scroll", adjustSideOffset);
@@ -327,20 +332,23 @@ const DxcDateInput = forwardRef<RefType, DateInputPropsType>(
327332
ariaLabel={ariaLabel}
328333
/>
329334
</Popover.Trigger>
330-
<Popover.Portal container={document.getElementById(`${calendarId}-portal`)}>
331-
<StyledPopoverContent
332-
sideOffset={sideOffset}
333-
align="end"
334-
aria-modal
335-
onBlur={handleDatePickerOnBlur}
336-
onKeyDown={handleDatePickerEscKeydown}
337-
ref={popoverContentRef}
338-
>
339-
<DatePicker id={calendarId} onDateSelect={handleCalendarOnClick} date={dayjsDate} />
340-
</StyledPopoverContent>
341-
</Popover.Portal>
335+
{portalContainer && (
336+
<Popover.Portal container={portalContainer}>
337+
<StyledPopoverContent
338+
sideOffset={sideOffset}
339+
align="end"
340+
aria-modal
341+
onBlur={handleDatePickerOnBlur}
342+
onKeyDown={handleDatePickerEscKeydown}
343+
ref={popoverContentRef}
344+
>
345+
<DatePicker id={calendarId} onDateSelect={handleCalendarOnClick} date={dayjsDate} />
346+
</StyledPopoverContent>
347+
</Popover.Portal>
348+
)}
342349
</Popover.Root>
343350
</DateInputContainer>
351+
344352
<div id={`${calendarId}-portal`} style={{ position: "absolute" }} />
345353
</>
346354
);

packages/lib/src/dropdown/Dropdown.tsx

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as Popover from "@radix-ui/react-popover";
2-
import { FocusEvent, KeyboardEvent, useCallback, useId, useLayoutEffect, useRef, useState } from "react";
2+
import { FocusEvent, KeyboardEvent, useCallback, useEffect, useId, useLayoutEffect, useRef, useState } from "react";
33
import styled from "@emotion/styled";
44
import { getMargin } from "../common/utils";
55
import { spaces } from "../common/variables";
@@ -131,6 +131,10 @@ const DxcDropdown = ({
131131
const menuId = `menu-${id}`;
132132
const [isOpen, changeIsOpen] = useState(false);
133133
const [visualFocusIndex, setVisualFocusIndex] = useState(0);
134+
const [portalContainer, setPortalContainer] = useState<HTMLElement | null>(null);
135+
useEffect(() => {
136+
setPortalContainer(document?.getElementById(`${id}-portal`));
137+
}, []);
134138

135139
const triggerRef = useRef<HTMLButtonElement | null>(null);
136140
const menuRef = useRef<HTMLUListElement | null>(null);
@@ -300,23 +304,26 @@ const DxcDropdown = ({
300304
</DropdownTrigger>
301305
</Popover.Trigger>
302306
</Tooltip>
303-
<Popover.Portal container={document.getElementById(`${id}-portal`)}>
304-
<Popover.Content aria-label="Dropdown options" asChild sideOffset={1}>
305-
<DropdownMenu
306-
id={menuId}
307-
dropdownTriggerId={triggerId}
308-
options={options}
309-
iconsPosition={optionsIconPosition}
310-
visualFocusIndex={visualFocusIndex}
311-
menuItemOnClick={handleMenuItemOnClick}
312-
onKeyDown={handleMenuOnKeyDown}
313-
styles={{ width }}
314-
ref={menuRef}
315-
/>
316-
</Popover.Content>
317-
</Popover.Portal>
307+
{portalContainer && (
308+
<Popover.Portal container={portalContainer}>
309+
<Popover.Content aria-label="Dropdown options" asChild sideOffset={1}>
310+
<DropdownMenu
311+
id={menuId}
312+
dropdownTriggerId={triggerId}
313+
options={options}
314+
iconsPosition={optionsIconPosition}
315+
visualFocusIndex={visualFocusIndex}
316+
menuItemOnClick={handleMenuItemOnClick}
317+
onKeyDown={handleMenuOnKeyDown}
318+
styles={{ width }}
319+
ref={menuRef}
320+
/>
321+
</Popover.Content>
322+
</Popover.Portal>
323+
)}
318324
</Popover.Root>
319325
</DropdownContainer>
326+
320327
<div id={`${id}-portal`} style={{ position: "absolute" }} />
321328
</>
322329
);

packages/lib/src/select/Select.tsx

Lines changed: 43 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
MouseEvent,
88
useCallback,
99
useContext,
10+
useEffect,
1011
useId,
1112
useMemo,
1213
useRef,
@@ -210,6 +211,10 @@ const DxcSelect = forwardRef<RefType, SelectPropsType>(
210211
const [isOpen, changeIsOpen] = useState(false);
211212
const [searchValue, setSearchValue] = useState("");
212213
const [visualFocusIndex, changeVisualFocusIndex] = useState(-1);
214+
const [portalContainer, setPortalContainer] = useState<HTMLElement | null>(null);
215+
useEffect(() => {
216+
setPortalContainer(document?.getElementById(`${id}-portal`));
217+
}, []);
213218

214219
const selectRef = useRef<HTMLDivElement | null>(null);
215220
const selectSearchInputRef = useRef<HTMLInputElement | null>(null);
@@ -596,44 +601,47 @@ const DxcSelect = forwardRef<RefType, SelectPropsType>(
596601
</DxcFlex>
597602
</Select>
598603
</Popover.Trigger>
599-
<Popover.Portal container={document.getElementById(`${id}-portal`)}>
600-
<Popover.Content
601-
aria-label="Select options"
602-
onCloseAutoFocus={(event) => {
603-
// Avoid select to lose focus when the list is closed
604-
event.preventDefault();
605-
}}
606-
onOpenAutoFocus={(event) => {
607-
// Avoid select to lose focus when the list is opened
608-
event.preventDefault();
609-
}}
610-
sideOffset={4}
611-
style={{ zIndex: "var(--z-dropdown)" }}
612-
>
613-
<Listbox
614-
ariaLabelledBy={labelId}
615-
currentValue={value ?? innerValue}
616-
enableSelectAll={enableSelectAll}
617-
handleOptionOnClick={handleOptionOnClick}
618-
handleGroupOnClick={handleSelectAllGroup}
619-
handleSelectAllOnClick={handleSelectAllOnClick}
620-
virtualizedHeight={virtualizedHeight}
621-
id={listboxId}
622-
lastOptionIndex={lastOptionIndex}
623-
multiple={multiple}
624-
optional={optional}
625-
optionalItem={optionalItem}
626-
options={searchable ? filteredOptions : options}
627-
searchable={searchable}
628-
selectionType={selectionType}
629-
styles={{ width }}
630-
visualFocusIndex={visualFocusIndex}
631-
/>
632-
</Popover.Content>
633-
</Popover.Portal>
604+
{portalContainer && (
605+
<Popover.Portal container={portalContainer}>
606+
<Popover.Content
607+
aria-label="Select options"
608+
onCloseAutoFocus={(event) => {
609+
// Avoid select to lose focus when the list is closed
610+
event.preventDefault();
611+
}}
612+
onOpenAutoFocus={(event) => {
613+
// Avoid select to lose focus when the list is opened
614+
event.preventDefault();
615+
}}
616+
sideOffset={4}
617+
style={{ zIndex: "var(--z-dropdown)" }}
618+
>
619+
<Listbox
620+
ariaLabelledBy={labelId}
621+
currentValue={value ?? innerValue}
622+
enableSelectAll={enableSelectAll}
623+
handleOptionOnClick={handleOptionOnClick}
624+
handleGroupOnClick={handleSelectAllGroup}
625+
handleSelectAllOnClick={handleSelectAllOnClick}
626+
virtualizedHeight={virtualizedHeight}
627+
id={listboxId}
628+
lastOptionIndex={lastOptionIndex}
629+
multiple={multiple}
630+
optional={optional}
631+
optionalItem={optionalItem}
632+
options={searchable ? filteredOptions : options}
633+
searchable={searchable}
634+
selectionType={selectionType}
635+
styles={{ width }}
636+
visualFocusIndex={visualFocusIndex}
637+
/>
638+
</Popover.Content>
639+
</Popover.Portal>
640+
)}
634641
</Popover.Root>
635642
{!disabled && typeof error === "string" && <ErrorMessage error={error} id={errorId} />}
636643
</SelectContainer>
644+
637645
<div id={`${id}-portal`} style={{ position: "absolute" }} />
638646
</>
639647
);

0 commit comments

Comments
 (0)