Skip to content

Commit

Permalink
[EuiPortal] Allow insert prop to be configured via `EuiProvider.com…
Browse files Browse the repository at this point in the history
…ponentDefaults` (elastic#6941)

* [EuiPortal] Add global settings for Portal-Based Components

* changelog

* Remove `PortalProvider` usage in favor of `EuiComponentDefaultsProvider`

Update EuiPortal component

- update class naming to match other instances in EUI for consistency (e.g. EuiAccordionClass)

- update function component typing and syntax

- minor object spread syntax nit

* Add EuiPortal tests for provided `insert` behavior

- has to be Cypress, as our current version of jest/jsdom does not support `insertAdjacentElement`

+ clarify in comments of the new provider test how individual component tests should be handled

* changelog

* Clean up types

- not super necessary to have a separate file for it, these types are only used within EuiPortal

- use `as const` array and typeof instead of `keyof`

- simplify insert positions map typing

---------

Co-authored-by: y1j2x34 <[email protected]>
  • Loading branch information
cee-chen and y1j2x34 committed Jul 26, 2023
1 parent ff8bbf6 commit e9e96e0
Show file tree
Hide file tree
Showing 4 changed files with 101 additions and 19 deletions.
80 changes: 78 additions & 2 deletions src/components/portal/portal.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@
/// <reference types="cypress-real-events" />
/// <reference types="../../../cypress/support" />

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

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

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

describe('`insert` inherited from EuiProvider.componentDefaults', () => {
const sibling = document.createElement('div');
sibling.id = 'sibling';

const Wrapper: FunctionComponent = ({ children }) => {
const [mounted, setMounted] = useState(false);

useEffect(() => {
document.body.appendChild(sibling);
setMounted(true);
}, []);

return <>{mounted && children}</>;
};

it('allows configuring the default `insert` for all EuiPortal components', () => {
cy.mount(
<Wrapper>
<EuiPortal>Hello</EuiPortal>
<EuiPortal>World</EuiPortal>
</Wrapper>,
{
providerProps: {
componentDefaults: {
EuiPortal: { insert: { sibling, position: 'before' } },
},
},
}
);

// 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', () => {
cy.mount(
<Wrapper>
<EuiPortal>Hello</EuiPortal>
<EuiPortal insert={{ sibling: sibling, position: 'after' }}>
World
</EuiPortal>
</Wrapper>,
{
providerProps: {
componentDefaults: {
EuiPortal: { insert: { sibling: sibling, position: 'before' } },
},
},
}
);

// 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`

0 comments on commit e9e96e0

Please sign in to comment.