Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 87 additions & 1 deletion src/components/portal/portal.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@
/// <reference types="../../../cypress/support" />

import React, { useState } from 'react';
import { EuiPortal, EuiPortalProps } from './index';

import { EuiProvider } from '../provider';

import { EuiPortal, EuiPortalProps } from './portal';

describe('EuiPortal', () => {
describe('insertion', () => {
Expand Down Expand Up @@ -124,5 +127,88 @@ describe('EuiPortal', () => {
});
});
});

describe('`insert` inherited from EuiProvider.componentDefaults', () => {
it('allows configuring the default `insert` for all EuiPortal components', () => {
const Wrapper = () => {
const [siblingRef, setSiblingRef] = useState<HTMLElement | null>(
null
);
return (
<>
<div id="sibling" ref={setSiblingRef} />
{siblingRef && (
<EuiProvider
componentDefaults={{
EuiPortal: {
insert: { sibling: siblingRef, position: 'before' },
},
}}
>
<EuiPortal>Hello</EuiPortal>
<EuiPortal>World</EuiPortal>
</EuiProvider>
)}
</>
);
};
cy.realMount(<Wrapper />);

// verify all portal elements were appended before the sibling
cy.get('div[data-euiportal]').then((portals) => {
cy.get('div#sibling').then((siblings) => {
expect(portals).to.have.lengthOf(2);
expect(siblings).to.have.lengthOf(1);
const beforeSibling = siblings.get(0).previousElementSibling;
expect(beforeSibling).to.equal(portals.get(1));
expect(beforeSibling?.previousElementSibling).to.equal(
portals.get(0)
);
});
});
});

it('still allows overriding defaults via component props', () => {
const Wrapper = () => {
const [siblingRef, setSiblingRef] = useState<HTMLElement | null>(
null
);
return (
<>
<div id="sibling" ref={setSiblingRef} />
{siblingRef && (
<EuiProvider
componentDefaults={{
EuiPortal: {
insert: { sibling: siblingRef, position: 'before' },
},
}}
>
<EuiPortal>Hello</EuiPortal>
<EuiPortal
insert={{ sibling: siblingRef, position: 'after' }}
>
World
</EuiPortal>
</EuiProvider>
)}
</>
);
};
cy.realMount(<Wrapper />);

// verify portal elements were appended before and after the sibling
cy.get('div[data-euiportal]').then((portals) => {
cy.get('div#sibling').then((siblings) => {
expect(portals).to.have.lengthOf(2);
expect(siblings).to.have.lengthOf(1);
expect(siblings.get(0).previousElementSibling).to.equal(
portals.get(0)
);
expect(siblings.get(0).nextElementSibling).to.equal(portals.get(1));
});
});
});
});
});
});
38 changes: 21 additions & 17 deletions src/components/portal/portal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,27 +11,19 @@
* into portals.
*/

import { Component, ReactNode } from 'react';
import React, { Component, FunctionComponent, ReactNode } from 'react';
import { createPortal } from 'react-dom';

import { EuiNestedThemeContext } from '../../services';
import { keysOf } from '../common';
import { useEuiComponentDefaults } from '../provider/component_defaults';

interface InsertPositionsMap {
after: InsertPosition;
before: InsertPosition;
}

export const insertPositions: InsertPositionsMap = {
const INSERT_POSITIONS = ['after', 'before'] as const;
type EuiPortalInsertPosition = (typeof INSERT_POSITIONS)[number];
const insertPositions: Record<EuiPortalInsertPosition, InsertPosition> = {
after: 'afterend',
before: 'beforebegin',
};

type EuiPortalInsertPosition = keyof typeof insertPositions;

export const INSERT_POSITIONS: EuiPortalInsertPosition[] =
keysOf(insertPositions);

export interface EuiPortalProps {
/**
* ReactNode to render as this component's content
Expand All @@ -41,14 +33,26 @@ export interface EuiPortalProps {
* If not specified, `EuiPortal` will insert itself
* into the end of the `document.body` by default
*/
insert?: { sibling: HTMLElement; position: 'before' | 'after' };
insert?: { sibling: HTMLElement; position: EuiPortalInsertPosition };
/**
* Optional ref callback
*/
portalRef?: (ref: HTMLDivElement | null) => void;
}

export class EuiPortal extends Component<EuiPortalProps> {
export const EuiPortal: FunctionComponent<EuiPortalProps> = ({
children,
...props
}) => {
const { EuiPortal: defaults } = useEuiComponentDefaults();
return (
<EuiPortalClass {...defaults} {...props}>
{children}
</EuiPortalClass>
);
};

export class EuiPortalClass extends Component<EuiPortalProps> {
static contextType = EuiNestedThemeContext;

portalNode: HTMLDivElement | null = null;
Expand Down Expand Up @@ -85,7 +89,7 @@ export class EuiPortal extends Component<EuiPortalProps> {
}

// Set the inherited color of the portal based on the wrapping EuiThemeProvider
setThemeColor() {
private setThemeColor() {
if (this.portalNode && this.context) {
const { hasDifferentColorFromGlobalTheme, colorClassName } = this.context;

Expand All @@ -95,7 +99,7 @@ export class EuiPortal extends Component<EuiPortalProps> {
}
}

updatePortalRef(ref: HTMLDivElement | null) {
private updatePortalRef(ref: HTMLDivElement | null) {
if (this.props.portalRef) {
this.props.portalRef(ref);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,5 @@ describe('EuiComponentDefaultsProvider', () => {
// NOTE: Components are in charge of their own testing to ensure that the props
// coming from `useEuiComponentDefaults()` were properly applied. This file
// is simply a very light wrapper that carries prop data.
// @see `src/components/portal/portal.spec.tsx` as an example
});
1 change: 1 addition & 0 deletions upcoming_changelogs/6941.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- `EuiPortal`'s `insert` prop can now be configured globally via `EuiProvider.componentDefaults`