diff --git a/README.md b/README.md index 0d49163..335f88b 100644 --- a/README.md +++ b/README.md @@ -125,7 +125,9 @@ const portalNode = portals.createHtmlPortalNode({ }); ``` -The div's DOM node is also available at `.element`, so you can mutate that directly with the standard DOM APIs if preferred. +### `portals.createHtmlInlinePortalNode([options])` + +Same as `portal.createHtmlPortalNode`, except it uses `` instead of `
`. This can be helpful to avoid invalid HTML when portalling videos, etc into [phrasing content](https://developer.mozilla.org/en-US/docs/Web/HTML/Content_categories#phrasing_content), which is invalid HTML markup and triggers [React validateDOMNesting](https://www.dhiwise.com/post/mastering-validatedomnesting-best-practices). ### `portals.createSvgPortalNode([options])` diff --git a/src/index.tsx b/src/index.tsx index 7c2fa0e..446280e 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 }; @@ -34,7 +35,7 @@ interface PortalNodeBase> { } export interface HtmlPortalNode = Component> extends PortalNodeBase { element: HTMLElement; - elementType: typeof ELEMENT_TYPE_HTML; + elementType: typeof ELEMENT_TYPE_HTML_BLOCK | typeof ELEMENT_TYPE_HTML_INLINE; } export interface SvgPortalNode = Component> extends PortalNodeBase { element: SVGElement; @@ -48,13 +49,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 +72,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 +195,7 @@ type OutPortalProps> = { class OutPortal> extends React.PureComponent> { - private placeholderNode = React.createRef(); + private placeholderNode = React.createRef(); private currentPortalNode?: AnyPortalNode; constructor(props: OutPortalProps) { @@ -236,18 +245,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 +const createHtmlPortalNode = createPortalNode.bind(null, ELEMENT_TYPE_HTML_BLOCK) as + = Component>(options?: Options) => HtmlPortalNode; +const createHtmlInlinePortalNode = createPortalNode.bind(null, ELEMENT_TYPE_HTML_INLINE) as = Component>(options?: Options) => HtmlPortalNode; 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!'; diff --git a/tsconfig.json b/tsconfig.json index e0bcc89..e8280bb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,7 +8,8 @@ "strict": true, "rootDir": "./src", "declaration": true, - "declarationDir": "./dist" + "declarationDir": "./dist", + "noFallthroughCasesInSwitch": true }, "include": [ "./src/**/*.tsx"