Skip to content

Commit

Permalink
Portale element to any referenced DOM element (#707)
Browse files Browse the repository at this point in the history
Co-authored-by: Andrey Myssak <[email protected]>
Signed-off-by: Sergey Myssak <[email protected]>
  • Loading branch information
SergeyMyssak and andreymyssak committed Jul 1, 2023
1 parent 54d5011 commit 9a7d3e8
Show file tree
Hide file tree
Showing 10 changed files with 176 additions and 68 deletions.
1 change: 1 addition & 0 deletions scripts/jest/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
},
"setupFiles": [
"<rootDir>/scripts/jest/setup/enzyme.js",
"<rootDir>/scripts/jest/setup/html_element.js",
"<rootDir>/scripts/jest/setup/throw_on_console_error.js"
],
"setupFilesAfterEnv": [
Expand Down
24 changes: 24 additions & 0 deletions scripts/jest/setup/html_element.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

if (typeof window !== 'undefined') {
HTMLElement.prototype.insertAdjacentElement = function (position, element) {
switch (position) {
case 'beforebegin':
this.parentNode.insertBefore(element, this);
break;
case 'afterend':
if (this.nextSibling) {
this.parentNode.insertBefore(element, this.nextSibling);
} else {
this.parentNode.appendChild(element);
}
break;
// add other cases if needed
default:
throw new Error(`Unsupported position: ${position}`);
}
};
}
63 changes: 42 additions & 21 deletions src/components/bottom_bar/__snapshots__/bottom_bar.test.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,48 @@ exports[`OuiBottomBar props bodyClassName is rendered 1`] = `
</section>
`;

exports[`OuiBottomBar props insert root prop is altered 1`] = `
Array [
<section
aria-label="Page level controls"
class="ouiBottomBar ouiBottomBar--fixed ouiBottomBar--paddingMedium"
>
<h2
class="ouiScreenReaderOnly"
>
Page level controls
</h2>
</section>,
<p
aria-live="assertive"
class="ouiScreenReaderOnly"
>
There is a new region landmark with page level controls at the end of the document.
</p>,
]
`;

exports[`OuiBottomBar props insert sibling prop is altered 1`] = `
Array [
<section
aria-label="Page level controls"
class="ouiBottomBar ouiBottomBar--fixed ouiBottomBar--paddingMedium"
>
<h2
class="ouiScreenReaderOnly"
>
Page level controls
</h2>
</section>,
<p
aria-live="assertive"
class="ouiScreenReaderOnly"
>
There is a new region landmark with page level controls at the end of the document.
</p>,
]
`;

exports[`OuiBottomBar props landmarkHeading 1`] = `
Array [
<section
Expand Down Expand Up @@ -268,24 +310,3 @@ Array [
</p>,
]
`;

exports[`OuiBottomBar props usePortal can be false 1`] = `
Array [
<section
aria-label="Page level controls"
class="ouiBottomBar ouiBottomBar--fixed ouiBottomBar--paddingMedium"
>
<h2
class="ouiScreenReaderOnly"
>
Page level controls
</h2>
</section>,
<p
aria-live="assertive"
class="ouiScreenReaderOnly"
>
There is a new region landmark with page level controls at the end of the document.
</p>,
]
`;
19 changes: 17 additions & 2 deletions src/components/bottom_bar/bottom_bar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,23 @@ describe('OuiBottomBar', () => {
expect(component).toMatchSnapshot();
});

test('usePortal can be false', () => {
const component = render(<OuiBottomBar usePortal={false} />);
test('insert root prop is altered', () => {
const component = render(
<OuiBottomBar insert={{ root: document.getElementById('main')! }} />
);

expect(component).toMatchSnapshot();
});

test('insert sibling prop is altered', () => {
const component = render(
<OuiBottomBar
insert={{
sibling: document.getElementById('main')!,
position: 'after',
}}
/>
);

expect(component).toMatchSnapshot();
});
Expand Down
22 changes: 12 additions & 10 deletions src/components/bottom_bar/bottom_bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ import { OuiScreenReaderOnly } from '../accessibility';
import { CommonProps, ExclusiveUnion } from '../common';
import { OuiI18n } from '../i18n';
import { useResizeObserver } from '../observer/resize_observer';
import { OuiPortal } from '../portal';
import { OuiPortal, OuiPortalInsert } from '../portal';

type BottomBarPaddingSize = 'none' | 's' | 'm' | 'l';

Expand All @@ -59,26 +59,27 @@ export const POSITIONS = ['static', 'fixed', 'sticky'] as const;
export type _BottomBarPosition = typeof POSITIONS[number];

type _BottomBarExclusivePositions = ExclusiveUnion<
{ position?: 'static' | 'sticky' },
{
position?: 'fixed';
/**
* Whether to wrap in an OuiPortal which appends the component to the body element.
* Whether to wrap in OuiPortal. Can be configured using "insert" prop.
* Only works if `position` is `fixed`.
*/
usePortal?: boolean;
/**
* Whether the component should apply padding on the document body element to afford for its own displacement height.
* Only works if `usePortal` is true and `position` is `fixed`.
* Configuration for placing children in the DOM. By default, attaches children to the body element.
* Only works if `position` is `fixed` and `usePortal` is true.
*/
affordForDisplacement?: boolean;
},
{
insert?: OuiPortalInsert;
/**
* How to position the bottom bar against its parent.
* Whether the component should apply padding on the document body element to afford for its own displacement height.
* Only works if `position` is `fixed` and `usePortal` is true.
*/
position: 'static' | 'sticky';
affordForDisplacement?: boolean;
}
>;

export type OuiBottomBarProps = CommonProps &
HTMLAttributes<HTMLElement> &
_BottomBarExclusivePositions & {
Expand Down Expand Up @@ -132,6 +133,7 @@ export const OuiBottomBar = forwardRef<
bodyClassName,
landmarkHeading,
usePortal = true,
insert,
left,
right,
bottom,
Expand Down Expand Up @@ -230,7 +232,7 @@ export const OuiBottomBar = forwardRef<
</>
);

return usePortal ? <OuiPortal>{bar}</OuiPortal> : bar;
return usePortal ? <OuiPortal insert={insert}>{bar}</OuiPortal> : bar;
}
);

Expand Down
7 changes: 2 additions & 5 deletions src/components/popover/popover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ import { OuiScreenReaderOnly } from '../accessibility';

import { OuiPanel, PanelPaddingSize, OuiPanelProps } from '../panel';

import { OuiPortal } from '../portal';
import { OuiPortal, OuiPortalInsert } from '../portal';

import { OuiMutationObserver } from '../observer/mutation_observer';

Expand Down Expand Up @@ -141,10 +141,7 @@ export interface OuiPopoverProps {
* Passed directly to OuiPortal for DOM positioning. Both properties are
* required if prop is specified
*/
insert?: {
sibling: HTMLElement;
position: 'before' | 'after';
};
insert?: OuiPortalInsert;
/**
* Visibility state of the popover
*/
Expand Down
26 changes: 12 additions & 14 deletions src/components/portal/__snapshots__/portal.test.tsx.snap
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`OuiPortal is rendered 1`] = `
<div>
<OuiPortal>
<Portal
containerInfo={
<div>
Content
</div>
}
>
Content
</Portal>
</OuiPortal>
</div>
exports[`OuiPortal should render OuiPortal 1`] = `
<OuiPortal>
<Portal
containerInfo={
<div>
Content
</div>
}
>
Content
</Portal>
</OuiPortal>
`;
2 changes: 1 addition & 1 deletion src/components/portal/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,4 @@
* under the License.
*/

export { OuiPortal, OuiPortalProps } from './portal';
export { OuiPortal, OuiPortalProps, OuiPortalInsert } from './portal';
48 changes: 42 additions & 6 deletions src/components/portal/portal.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,49 @@ import { mount } from 'enzyme';
import { OuiPortal } from './portal';

describe('OuiPortal', () => {
test('is rendered', () => {
const component = mount(
<div>
<OuiPortal>Content</OuiPortal>
</div>
);
afterEach(() => {
document.body.innerHTML = '';
});

it('should render OuiPortal', () => {
const component = mount(<OuiPortal>Content</OuiPortal>);

expect(component).toMatchSnapshot();
});

it('should attach Content to body', () => {
mount(<OuiPortal>Content</OuiPortal>);

expect(document.body.innerHTML).toEqual('<div>Content</div>');
});

it('should attach Content inside an element', () => {
const container = document.createElement('div');
container.setAttribute('id', 'container');
document.body.appendChild(container);
document.body.appendChild(document.createElement('div'));

mount(<OuiPortal insert={{ root: container }}>Content</OuiPortal>);

expect(document.body.innerHTML).toEqual(
'<div id="container">Content</div><div></div>'
);
});

it('should attach Content before an element', () => {
const container = document.createElement('div');
container.setAttribute('id', 'container');
document.body.appendChild(container);

mount(
<OuiPortal insert={{ sibling: container, position: 'before' }}>
Content
</OuiPortal>,
{ attachTo: document.body }
);

expect(document.body.innerHTML).toEqual(
'<div>Content</div><div id="container"></div>'
);
});
});
32 changes: 23 additions & 9 deletions src/components/portal/portal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@

import { Component, ReactNode } from 'react';
import { createPortal } from 'react-dom';
import { keysOf } from '../common';
import { ExclusiveUnion, keysOf } from '../common';

interface InsertPositionsMap {
after: InsertPosition;
Expand All @@ -52,31 +52,45 @@ export const INSERT_POSITIONS: OuiPortalInsertPosition[] = keysOf(
);

type OuiPortalInsertPosition = keyof typeof insertPositions;
export type OuiPortalInsert = ExclusiveUnion<
{ root?: HTMLElement },
{ sibling: HTMLElement; position: OuiPortalInsertPosition }
>;

export interface OuiPortalProps {
/**
* ReactNode to render as this component's content
*/
children: ReactNode;
insert?: { sibling: HTMLElement; position: OuiPortalInsertPosition };
portalRef?: (ref: HTMLDivElement | null) => void;
insert?: OuiPortalInsert;
portalRef?: (ref: HTMLElement | null) => void;
}

export class OuiPortal extends Component<OuiPortalProps> {
portalNode: HTMLDivElement;
portalNode: HTMLElement;
constructor(props: OuiPortalProps) {
super(props);

const { insert } = this.props;

this.portalNode = document.createElement('div');

// no insertion defined, append to body
if (insert == null) {
// no insertion defined, append to body
document.body.appendChild(this.portalNode);
} else {
// inserting before or after an element
const { sibling, position } = insert;
return;
}

const { root, sibling, position } = insert;

// inserting within an element
if (root) {
this.portalNode = root;
return;
}

// inserting before or after an element
if (sibling && position) {
sibling.insertAdjacentElement(insertPositions[position], this.portalNode);
}
}
Expand All @@ -92,7 +106,7 @@ export class OuiPortal extends Component<OuiPortalProps> {
this.updatePortalRef(null);
}

updatePortalRef(ref: HTMLDivElement | null) {
updatePortalRef(ref: HTMLElement | null) {
if (this.props.portalRef) {
this.props.portalRef(ref);
}
Expand Down

0 comments on commit 9a7d3e8

Please sign in to comment.