diff --git a/src/index.tsx b/src/index.tsx index 7c2fa0e..8225464 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -2,10 +2,11 @@ import * as React from 'react'; import * as ReactDOM from 'react-dom'; // Internally, the portalNode must be for either HTML or SVG elements -const ELEMENT_TYPE_HTML = 'html'; +const ELEMENT_TYPE_HTML_BLOCK = 'div'; +const ELEMENT_TYPE_HTML_INLINE = 'span'; const ELEMENT_TYPE_SVG = 'svg'; -type ANY_ELEMENT_TYPE = typeof ELEMENT_TYPE_HTML | typeof ELEMENT_TYPE_SVG; +type ANY_ELEMENT_TYPE = typeof ELEMENT_TYPE_HTML_BLOCK | typeof ELEMENT_TYPE_HTML_INLINE | typeof ELEMENT_TYPE_SVG; type Options = { attributes: { [key: string]: string }; @@ -32,15 +33,19 @@ interface PortalNodeBase> { // latest placeholder we replaced. This avoids some race conditions. unmount(expectedPlaceholder?: Node): void; } -export interface HtmlPortalNode = Component> extends PortalNodeBase { +export interface HtmlBlockPortalNode = Component> extends PortalNodeBase { element: HTMLElement; - elementType: typeof ELEMENT_TYPE_HTML; + elementType: typeof ELEMENT_TYPE_HTML_BLOCK; +} +export interface HtmlInlinePortalNode = Component> extends PortalNodeBase { + element: HTMLElement; + elementType: typeof ELEMENT_TYPE_HTML_INLINE; } export interface SvgPortalNode = Component> extends PortalNodeBase { element: SVGElement; elementType: typeof ELEMENT_TYPE_SVG; } -type AnyPortalNode = Component> = HtmlPortalNode | SvgPortalNode; +type AnyPortalNode = Component> = HtmlBlockPortalNode | HtmlInlinePortalNode | SvgPortalNode; const validateElementType = (domElement: Element, elementType: ANY_ELEMENT_TYPE) => { @@ -48,13 +53,16 @@ const validateElementType = (domElement: Element, elementType: ANY_ELEMENT_TYPE) // Cast document to `any` because Typescript doesn't know about the legacy `Document.parentWindow` field, and also // doesn't believe `Window.HTMLElement`/`Window.SVGElement` can be used in instanceof tests. const ownerWindow = ownerDocument.defaultView ?? ownerDocument.parentWindow ?? window; // `parentWindow` for IE8 and earlier - if (elementType === ELEMENT_TYPE_HTML) { - return domElement instanceof ownerWindow.HTMLElement; - } - if (elementType === ELEMENT_TYPE_SVG) { - return domElement instanceof ownerWindow.SVGElement; + + switch (elementType) { + case ELEMENT_TYPE_HTML_BLOCK: + case ELEMENT_TYPE_HTML_INLINE: + return domElement instanceof ownerWindow.HTMLElement; + case ELEMENT_TYPE_SVG: + return domElement instanceof ownerWindow.SVGElement; + default: + throw new Error(`Unrecognized element type "${elementType}" for validateElementType.`); } - throw new Error(`Unrecognized element type "${elementType}" for validateElementType.`); }; // This is the internal implementation: the public entry points set elementType to an appropriate value @@ -68,12 +76,17 @@ const createPortalNode = >( let lastPlaceholder: Node | undefined; let element; - if (elementType === ELEMENT_TYPE_HTML) { - element= document.createElement('div'); - } else if (elementType === ELEMENT_TYPE_SVG){ - element= document.createElementNS(SVG_NAMESPACE, 'g'); - } else { - throw new Error(`Invalid element type "${elementType}" for createPortalNode: must be "html" or "svg".`); + + switch (elementType) { + case ELEMENT_TYPE_HTML_BLOCK: + case ELEMENT_TYPE_HTML_INLINE: + element = document.createElement(elementType); + break; + case ELEMENT_TYPE_SVG: + element = document.createElementNS(SVG_NAMESPACE, 'g'); + break; + default: + throw new Error(`Invalid element type "${elementType}" for createPortalNode: must be "div", "span" or "svg".`); } if (options && typeof options === "object") { @@ -186,7 +199,7 @@ type OutPortalProps> = { class OutPortal> extends React.PureComponent> { - private placeholderNode = React.createRef(); + private placeholderNode = React.createRef(); private currentPortalNode?: AnyPortalNode; constructor(props: OutPortalProps) { @@ -236,18 +249,23 @@ class OutPortal> extends React.PureComponent placeholder works fine even for SVG. - return
; + // A placeholder: + // - prevents invalid HTML (e.g. inside

) + // - works fine even for SVG. + return ; } } -const createHtmlPortalNode = createPortalNode.bind(null, ELEMENT_TYPE_HTML) as - = Component>(options?: Options) => HtmlPortalNode; +const createHtmlPortalNode = createPortalNode.bind(null, ELEMENT_TYPE_HTML_BLOCK) as + = Component>(options?: Options) => HtmlBlockPortalNode; +const createHtmlInlinePortalNode = createPortalNode.bind(null, ELEMENT_TYPE_HTML_INLINE) as + = Component>(options?: Options) => HtmlInlinePortalNode; const createSvgPortalNode = createPortalNode.bind(null, ELEMENT_TYPE_SVG) as = Component>(options?: Options) => SvgPortalNode; export { createHtmlPortalNode, + createHtmlInlinePortalNode, createSvgPortalNode, InPortal, OutPortal, diff --git a/stories/html.stories.js b/stories/html.stories.js index 1bf2986..22bfa69 100644 --- a/stories/html.stories.js +++ b/stories/html.stories.js @@ -2,7 +2,7 @@ import React from 'react'; import { storiesOf } from '@storybook/react'; -import { createHtmlPortalNode, createSvgPortalNode, InPortal, OutPortal } from '..'; +import { createHtmlPortalNode, createHtmlInlinePortalNode, InPortal, OutPortal } from '..'; const Container = (props) =>

@@ -289,6 +289,23 @@ storiesOf('Portals', module)
}); }) + .add('can render inline portal', () => { + const portalNode = createHtmlInlinePortalNode(); + + return
+

+ Portal defined here: + + Hi! + +

+ +

+ Portal renders here: + +

+
; + }) .add('Example from README', () => { const MyExpensiveComponent = () => 'expensive!';