Skip to content

Commit 7e19fbb

Browse files
authored
refactor(AnalyticalTable): remove unnecessary portalContainer prop (#6039)
Additionally, this PR removes all mentions of `createPortal` or `portalContainer`. BREAKING CHANGE: `portalContainer` has been removed as it's no longer needed due to the [Popover API](https://developer.mozilla.org/en-US/docs/Web/API/Popover_API).
1 parent b908544 commit 7e19fbb

21 files changed

+82
-255
lines changed

docs/MigrationGuide.mdx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,10 @@ function MyComponent() {
477477
- `alwaysShowSubComponent` has been removed, please use `subComponentsBehavior` with `AnalyticalTableSubComponentsBehavior.Visibe` instead.
478478
- The default value of `sortable` (`true`) has been removed. To allow sorting in your table please set `sorting` to `true` yourself.
479479

480+
**Removed Props:**
481+
482+
- `portalContainer` has been removed as it's no longer needed due to the [Popover API](https://developer.mozilla.org/en-US/docs/Web/API/Popover_API) used in the `Popover` ui5 web component.
483+
480484
**Renamed Enums:**
481485

482486
Only the names of the following enums have changed, so it's sufficient to replace them with the new name.

docs/knowledge-base/Portals.mdx

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,25 @@
11
import { Meta } from '@storybook/addon-docs';
22
import { Footer, TableOfContent } from '@sb/components';
3+
import MessageStripDesign from '@ui5/webcomponents/dist/types/MessageStripDesign.js';
4+
import { MessageStrip } from '@ui5/webcomponents-react';
35

4-
<Meta title="Working with Portals" />
6+
<Meta title="Working with Portals (v1)" />
57

6-
# Working with Portals
8+
# Working with Portals in v1
9+
10+
<MessageStrip
11+
hideCloseButton
12+
design={MessageStripDesign.Critical}
13+
children={
14+
<>
15+
Since v2, it's not necessary to use portals anymore, as all popovers and dialogs are now using the{' '}
16+
<ui5-link target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/Popover_API">
17+
Popover API
18+
</ui5-link>
19+
.
20+
</>
21+
}
22+
/>
723

824
This entry explains why portals are used in UI5 Web Components for React components and what you need to consider when using them.
925

packages/main/src/components/AnalyticalTable/ColumnHeader/ColumnHeaderContainer.tsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ interface ColumnHeaderContainerProps {
1313
onGroupByChanged: (e: CustomEvent<{ column?: Record<string, unknown>; isGrouped?: boolean }>) => void;
1414
resizeInfo: Record<string, unknown>;
1515
isRtl: boolean;
16-
portalContainer: Element;
1716
columnVirtualizer: Virtualizer<DivWithCustomScrollProp, Element>;
1817
uniqueId: string;
1918
showVerticalEndBorder: boolean;
@@ -27,7 +26,6 @@ export const ColumnHeaderContainer = forwardRef<HTMLDivElement, ColumnHeaderCont
2726
onGroupByChanged,
2827
resizeInfo,
2928
isRtl,
30-
portalContainer,
3129
columnVirtualizer,
3230
uniqueId,
3331
showVerticalEndBorder
@@ -86,7 +84,6 @@ export const ColumnHeaderContainer = forwardRef<HTMLDivElement, ColumnHeaderCont
8684
virtualColumn={virtualColumn}
8785
columnVirtualizer={columnVirtualizer}
8886
isRtl={isRtl}
89-
portalContainer={portalContainer}
9087
>
9188
{column.render('Header')}
9289
</ColumnHeader>

packages/main/src/components/AnalyticalTable/ColumnHeader/ColumnHeaderModal.tsx

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,8 @@ import iconSortDescending from '@ui5/webcomponents-icons/dist/sort-descending.js
99
import { enrichEventWithDetails, useI18nBundle, useStylesheet } from '@ui5/webcomponents-react-base';
1010
import type { MutableRefObject } from 'react';
1111
import { useEffect, useRef } from 'react';
12-
import { createPortal } from 'react-dom';
1312
import { FlexBoxAlignItems, TextAlign } from '../../../enums/index.js';
1413
import { CLEAR_SORTING, GROUP, SORT_ASCENDING, SORT_DESCENDING, UNGROUP } from '../../../i18n/i18n-defaults.js';
15-
import { useCanRenderPortal } from '../../../internal/ssr.js';
1614
import { stopPropagation } from '../../../internal/stopPropagation.js';
1715
import { getUi5TagWithSuffix } from '../../../internal/utils.js';
1816
import { Icon } from '../../../webComponents/Icon/index.js';
@@ -31,13 +29,12 @@ export interface ColumnHeaderModalProperties {
3129
onGroupBy?: (e: CustomEvent<{ column: unknown; isGrouped: boolean }>) => void;
3230
open: boolean;
3331
setPopoverOpen: (open: boolean) => void;
34-
portalContainer: Element;
3532
isRtl: boolean;
3633
openerRef: MutableRefObject<HTMLDivElement>;
3734
}
3835

3936
export const ColumnHeaderModal = (props: ColumnHeaderModalProperties) => {
40-
const { column, onSort, onGroupBy, open, setPopoverOpen, portalContainer, isRtl, openerRef } = props;
37+
const { column, onSort, onGroupBy, open, setPopoverOpen, isRtl, openerRef } = props;
4138
useStylesheet(styleData, ColumnHeaderModal.displayName);
4239
const showFilter = column.canFilter;
4340
const showGroup = column.canGroupBy;
@@ -150,21 +147,15 @@ export const ColumnHeaderModal = (props: ColumnHeaderModalProperties) => {
150147
}
151148
};
152149

153-
const canRenderPortal = useCanRenderPortal();
154-
155150
useEffect(() => {
156151
if (open && ref.current && openerRef.current) {
157152
void customElements.whenDefined(getUi5TagWithSuffix('ui5-popover')).then(() => {
158153
ref.current.opener = openerRef.current;
159154
});
160155
}
161-
}, [open, canRenderPortal]);
162-
163-
if (!canRenderPortal) {
164-
return null;
165-
}
156+
}, [open]);
166157

167-
return createPortal(
158+
return (
168159
<Popover
169160
open={open}
170161
hideArrow
@@ -218,8 +209,7 @@ export const ColumnHeaderModal = (props: ColumnHeaderModalProperties) => {
218209
</ListItemStandard>
219210
)}
220211
</List>
221-
</Popover>,
222-
portalContainer ?? document.body
212+
</Popover>
223213
);
224214
};
225215
ColumnHeaderModal.displayName = 'ColumnHeaderModal';

packages/main/src/components/AnalyticalTable/ColumnHeader/index.tsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ export interface ColumnHeaderProps {
3737
columnVirtualizer: Virtualizer<DivWithCustomScrollProp, Element>;
3838
isRtl: boolean;
3939
children: ReactNode | ReactNode[];
40-
portalContainer: Element;
4140
columnId?: string;
4241
showVerticalEndBorder: boolean;
4342

@@ -81,7 +80,6 @@ export const ColumnHeader = (props: ColumnHeaderProps) => {
8180
visibleColumnIndex,
8281
onClick,
8382
onKeyDown,
84-
portalContainer,
8583
isFiltered,
8684
title,
8785
'aria-label': ariaLabel,
@@ -239,7 +237,6 @@ export const ColumnHeader = (props: ColumnHeaderProps) => {
239237
openerRef={columnHeaderRef}
240238
open={popoverOpen}
241239
setPopoverOpen={setPopoverOpen}
242-
portalContainer={portalContainer}
243240
/>
244241
)}
245242
</div>

packages/main/src/components/AnalyticalTable/Recipes.mdx

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,6 @@ Below you can find two examples of typical pain points:
237237
In general, we recommend mounting modals like Dialogs or Popovers outside the `AnalyticalTable`.
238238
Still, it's possible to also render them inside a custom cell, but there are some things to consider:
239239

240-
- Use `createPortal` to render the modal outside the `AnalyticalTable` to prevent visual issues.
241240
- Use conditional rendering to prevent the modal from being rendered for every cell.
242241
- Call `event.stopPropagation()` in the respective handler of the event if you're facing issues with focus handling (`onFocus`), keyboard navigation (i.a. `onKeyDown`), etc.
243242

@@ -254,18 +253,15 @@ const columns = [
254253
return (
255254
<>
256255
<Button onClick={toggle}>Open</Button>
257-
{createPortal(
258-
<Dialog
259-
open={open}
260-
initialFocus={uniqueId}
261-
onFocus={(e) => {
262-
e.stopPropagation();
263-
}}
264-
>
265-
<Input placeholder="Focus me!" id={uniqueId} />
266-
</Dialog>,
267-
document.body
268-
)}
256+
<Dialog
257+
open={open}
258+
initialFocus={uniqueId}
259+
onFocus={(e) => {
260+
e.stopPropagation();
261+
}}
262+
>
263+
<Input placeholder="Focus me!" id={uniqueId} />
264+
</Dialog>
269265
</>
270266
);
271267
}

packages/main/src/components/AnalyticalTable/VerticalResizer.tsx

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
import { useStylesheet, useI18nBundle } from '@ui5/webcomponents-react-base';
22
import type { MutableRefObject } from 'react';
33
import { useCallback, useEffect, useRef, useState } from 'react';
4-
import { createPortal } from 'react-dom';
54
import { DRAG_TO_RESIZE } from '../../i18n/i18n-defaults.js';
6-
import { useCanRenderPortal } from '../../internal/ssr.js';
75
import { classNames, styleData } from './VerticalResizer.module.css.js';
86

97
interface VerticalResizerProps {
@@ -13,7 +11,6 @@ interface VerticalResizerProps {
1311
internalRowHeight: number;
1412
hasPopInColumns: boolean;
1513
popInRowHeight: number;
16-
portalContainer: Element;
1714
rowsLength: number;
1815
visibleRows: number;
1916
handleOnLoadMore: (e: Event) => void;
@@ -34,7 +31,6 @@ export const VerticalResizer = (props: VerticalResizerProps) => {
3431
internalRowHeight,
3532
hasPopInColumns,
3633
popInRowHeight,
37-
portalContainer,
3834
rowsLength,
3935
visibleRows,
4036
handleOnLoadMore
@@ -140,11 +136,6 @@ export const VerticalResizer = (props: VerticalResizerProps) => {
140136
isInitial.current = false;
141137
}, [rowsLength, visibleRows]);
142138

143-
const canRenderPortal = useCanRenderPortal();
144-
if (!canRenderPortal) {
145-
return null;
146-
}
147-
148139
return (
149140
<div
150141
className={classNames.container}
@@ -154,15 +145,12 @@ export const VerticalResizer = (props: VerticalResizerProps) => {
154145
role="separator"
155146
title={i18nBundle.getText(DRAG_TO_RESIZE)}
156147
>
157-
{resizerPosition &&
158-
isDragging &&
159-
createPortal(
160-
<div
161-
className={classNames.resizer}
162-
style={{ top: resizerPosition.top, left: resizerPosition.left, width: resizerPosition.width }}
163-
/>,
164-
portalContainer ?? document.body
165-
)}
148+
{resizerPosition && isDragging && (
149+
<div
150+
className={classNames.resizer}
151+
style={{ top: resizerPosition.top, left: resizerPosition.left, width: resizerPosition.width }}
152+
/>
153+
)}
166154
</div>
167155
);
168156
};

packages/main/src/components/AnalyticalTable/index.tsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,6 @@ const AnalyticalTable = forwardRef<AnalyticalTableDomRef, AnalyticalTablePropTyp
139139
noDataText,
140140
overscanCount,
141141
overscanCountHorizontal = 5,
142-
portalContainer,
143142
retainColumnWidth,
144143
reactTableOptions,
145144
renderRowSubComponent,
@@ -768,7 +767,6 @@ const AnalyticalTable = forwardRef<AnalyticalTableDomRef, AnalyticalTablePropTyp
768767
onSort={onSort}
769768
onGroupByChanged={onGroupByChanged}
770769
isRtl={isRtl}
771-
portalContainer={portalContainer}
772770
columnVirtualizer={columnVirtualizer}
773771
uniqueId={uniqueId}
774772
showVerticalEndBorder={showVerticalEndBorder}
@@ -851,7 +849,6 @@ const AnalyticalTable = forwardRef<AnalyticalTableDomRef, AnalyticalTablePropTyp
851849
dispatch={dispatch}
852850
extensionsHeight={extensionsHeight}
853851
internalRowHeight={internalRowHeight}
854-
portalContainer={portalContainer}
855852
rowsLength={rows.length}
856853
visibleRows={internalVisibleRowCount}
857854
handleOnLoadMore={handleOnLoadMore}

packages/main/src/components/AnalyticalTable/types/index.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -615,14 +615,6 @@ export interface AnalyticalTablePropTypes extends Omit<CommonProps, 'title'> {
615615
* @since 1.19.0
616616
*/
617617
subComponentsBehavior?: AnalyticalTableSubComponentsBehavior | keyof typeof AnalyticalTableSubComponentsBehavior;
618-
/**
619-
* Defines where modals and other elements which should be mounted outside the DOM hierarchy are rendered into via `React.createPortal`.
620-
*
621-
* You can find out more about this [here](https://sap.github.io/ui5-webcomponents-react/?path=/docs/knowledge-base-working-with-portals--page).
622-
*
623-
* Defaults to: `document.body`
624-
*/
625-
portalContainer?: Element;
626618

627619
// events
628620
/**

packages/main/src/components/MessageBox/MessageBox.mdx

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -77,34 +77,6 @@ const MessageBoxComponent = () => {
7777
};
7878
```
7979

80-
## Using MessageBoxes inside other components
81-
82-
`MessageBoxes` are often used within other components, when opened this could sometimes have unwanted side effects.
83-
In this case, we recommend using [createPortal](https://reactjs.org/docs/portals.html) to mount the `MessageBox` outside of the DOM hierarchy of the parent component.
84-
85-
```JSX
86-
const MessageBoxComponent = () => {
87-
const [open, setOpen] = useState(false);
88-
const onButtonClick = () => {
89-
setOpen(true);
90-
};
91-
const handleClose = () => {
92-
setOpen(false);
93-
};
94-
return (
95-
<>
96-
<Button onClick={onButtonClick}>Open Messagebox</Button>
97-
{createPortal(
98-
<MessageBox open={open} onClose={handleClose}>
99-
Content
100-
</MessageBox>,
101-
document.body
102-
)}
103-
</>
104-
);
105-
};
106-
```
107-
10880
# More Examples
10981

11082
<br />

packages/main/src/components/MessageBox/MessageBox.stories.tsx

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,14 @@
11
import { isChromatic } from '@sb/utils';
22
import type { Meta, StoryObj } from '@storybook/react';
3-
import { forwardRef, useEffect, useState } from 'react';
4-
import { createPortal } from 'react-dom';
3+
import { useEffect, useState } from 'react';
54
import { MessageBoxAction } from '../../enums/MessageBoxAction';
65
import { MessageBoxType } from '../../enums/MessageBoxType';
7-
import type { DialogDomRef } from '../../webComponents';
86
import { Button } from '../../webComponents/Button/index';
9-
import type { MessageBoxPropTypes } from './index.js';
10-
import { MessageBox as OriginalMessageBox } from './index.js';
11-
12-
// todo remove once portals are supported inline, or popovers are supported w/o having to mount them to the body
13-
const MessageBox = forwardRef<DialogDomRef, MessageBoxPropTypes>((args, ref) =>
14-
createPortal(<OriginalMessageBox {...args} ref={ref} />, document.body)
15-
);
16-
MessageBox.displayName = 'MessageBox';
7+
import { MessageBox } from './index.js';
178

189
const meta = {
1910
title: 'Modals & Popovers / MessageBox',
20-
component: OriginalMessageBox,
11+
component: MessageBox,
2112
argTypes: {
2213
header: {
2314
control: { disable: true }
@@ -38,7 +29,7 @@ const meta = {
3829
chromatic: { delay: 1000 }
3930
},
4031
tags: ['package:@ui5/webcomponents', 'cem-module:Dialog']
41-
} satisfies Meta<typeof OriginalMessageBox>;
32+
} satisfies Meta<typeof MessageBox>;
4233

4334
export default meta;
4435
type Story = StoryObj<typeof meta>;

packages/main/src/components/MessageBox/index.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,6 @@ const getActions = (actions, type): (string | ReactElement<ButtonPropTypes>)[] =
134134

135135
/**
136136
* The `MessageBox` component provides easier methods to create a `Dialog`, such as standard alerts, confirmation dialogs, or arbitrary message dialogs.
137-
* For convenience, it also provides an `open` prop, so it is not necessary to attach a `ref` to open the `MessageBox`.
138137
*/
139138
const MessageBox = forwardRef<DialogDomRef, MessageBoxPropTypes>((props, ref) => {
140139
const {

0 commit comments

Comments
 (0)