From 18c5d44f5fcaa262e0d034823365f0b417874391 Mon Sep 17 00:00:00 2001 From: Alexander Harding Date: Mon, 18 Nov 2024 13:33:54 -0600 Subject: [PATCH 1/9] Add inline portal node support This adds `createHtmlInlinePortalNode` to the public api, which creates a `` instead of `
` wrapper. This is helpful when portalling into phrasing content. For example, placing a portal inside `

` [0] Without this, React will emit hydration warnings. Resolves #44 [0] https://html.spec.whatwg.org/multipage/grouping-content.html#the-p-element --- src/index.tsx | 62 ++++++++++++++++++++++++++--------------- stories/html.stories.js | 19 ++++++++++++- 2 files changed, 58 insertions(+), 23 deletions(-) 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!'; From b1fb2b62998cbb2abf4bdedaeca00498fcca9c44 Mon Sep 17 00:00:00 2001 From: Alexander Harding Date: Mon, 18 Nov 2024 13:44:07 -0600 Subject: [PATCH 2/9] Keep exported types same --- src/index.tsx | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/index.tsx b/src/index.tsx index 8225464..446280e 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -33,19 +33,15 @@ interface PortalNodeBase> { // latest placeholder we replaced. This avoids some race conditions. unmount(expectedPlaceholder?: Node): void; } -export interface HtmlBlockPortalNode = Component> extends PortalNodeBase { +export interface HtmlPortalNode = Component> extends PortalNodeBase { element: HTMLElement; - elementType: typeof ELEMENT_TYPE_HTML_BLOCK; -} -export interface HtmlInlinePortalNode = Component> extends PortalNodeBase { - element: HTMLElement; - elementType: typeof ELEMENT_TYPE_HTML_INLINE; + elementType: typeof ELEMENT_TYPE_HTML_BLOCK | typeof ELEMENT_TYPE_HTML_INLINE; } export interface SvgPortalNode = Component> extends PortalNodeBase { element: SVGElement; elementType: typeof ELEMENT_TYPE_SVG; } -type AnyPortalNode = Component> = HtmlBlockPortalNode | HtmlInlinePortalNode | SvgPortalNode; +type AnyPortalNode = Component> = HtmlPortalNode | SvgPortalNode; const validateElementType = (domElement: Element, elementType: ANY_ELEMENT_TYPE) => { @@ -257,9 +253,9 @@ class OutPortal> extends React.PureComponent = Component>(options?: Options) => HtmlBlockPortalNode; + = Component>(options?: Options) => HtmlPortalNode; const createHtmlInlinePortalNode = createPortalNode.bind(null, ELEMENT_TYPE_HTML_INLINE) as - = Component>(options?: Options) => HtmlInlinePortalNode; + = Component>(options?: Options) => HtmlPortalNode; const createSvgPortalNode = createPortalNode.bind(null, ELEMENT_TYPE_SVG) as = Component>(options?: Options) => SvgPortalNode; From 7335b7d4ec3739c24e9f80b219a287b65c55831f Mon Sep 17 00:00:00 2001 From: Alexander Harding Date: Mon, 18 Nov 2024 13:50:28 -0600 Subject: [PATCH 3/9] Enable noFallthroughCasesInSwitch for switches --- tsconfig.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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" From 6b41a07a8a550f494b0d5585781a426f00c26fba Mon Sep 17 00:00:00 2001 From: Alexander Harding Date: Tue, 19 Nov 2024 12:29:41 -0600 Subject: [PATCH 4/9] Update README --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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])` From 43da1cf7fa889085967f318e407b93d7cf51c042 Mon Sep 17 00:00:00 2001 From: Alexander Harding Date: Sun, 23 Mar 2025 10:34:38 -0500 Subject: [PATCH 5/9] Feedback --- README.md | 18 +++++++++--------- src/index.tsx | 26 +++++++++++++++++--------- stories/html.stories.js | 4 ++-- 3 files changed, 28 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 335f88b..73ad5f0 100644 --- a/README.md +++ b/README.md @@ -115,19 +115,19 @@ How does it work under the hood? This creates a detached DOM node, with a little extra functionality attached to allow transmitting props later on. -This node will contain your portal contents later, within a `
`, and will eventually be attached in the target location. +This node will contain your portal contents later, and will eventually be attached in the target location. -An optional options object parameter can be passed to configure the node. The only supported option is `attributes`: this can be used to set the HTML attributes (style, class, etc.) of the intermediary, like so: +An optional options object parameter can be passed to configure the node. -```javascript -const portalNode = portals.createHtmlPortalNode({ - attributes: { id: "div-1", style: "background-color: #aaf; width: 100px;" } -}); -``` +- `options.containerElement` can be set to `'div'` or `'span'` to contigure the detached DOM node container type. -### `portals.createHtmlInlinePortalNode([options])` +- `options.attributes` can be used to set the HTML attributes (style, class, etc.) of the intermediary, like so: -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). + ```javascript + const portalNode = portals.createHtmlPortalNode({ + attributes: { id: "div-1", style: "background-color: #aaf; width: 100px;" } + }); + ``` ### `portals.createSvgPortalNode([options])` diff --git a/src/index.tsx b/src/index.tsx index 446280e..c60910e 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -8,10 +8,20 @@ const ELEMENT_TYPE_SVG = '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 }; +type BaseOptions = { + attributes?: { [key: string]: string }; }; +type HtmlOptions = BaseOptions & { + containerElement?: typeof ELEMENT_TYPE_HTML_BLOCK | typeof ELEMENT_TYPE_HTML_INLINE; +}; + +type SvgOptions = BaseOptions & { + containerElement?: typeof ELEMENT_TYPE_SVG; +}; + +type Options = HtmlOptions | SvgOptions; + // ReactDOM can handle several different namespaces, but they're not exported publicly // https://github.com/facebook/react/blob/b87aabdfe1b7461e7331abb3601d9e6bb27544bc/packages/react-dom/src/shared/DOMNamespaces.js#L8-L10 const SVG_NAMESPACE = 'http://www.w3.org/2000/svg'; @@ -63,9 +73,10 @@ const validateElementType = (domElement: Element, elementType: ANY_ELEMENT_TYPE) // This is the internal implementation: the public entry points set elementType to an appropriate value const createPortalNode = >( - elementType: ANY_ELEMENT_TYPE, + defaultElementType: ANY_ELEMENT_TYPE, options?: Options ): AnyPortalNode => { + const elementType = options?.containerElement ?? defaultElementType; let initialProps = {} as ComponentProps; let parent: Node | undefined; @@ -85,7 +96,7 @@ const createPortalNode = >( throw new Error(`Invalid element type "${elementType}" for createPortalNode: must be "div", "span" or "svg".`); } - if (options && typeof options === "object") { + if (options && typeof options === "object" && options.attributes) { for (const [key, value] of Object.entries(options.attributes)) { element.setAttribute(key, value); } @@ -253,15 +264,12 @@ class OutPortal> extends React.PureComponent = Component>(options?: Options) => HtmlPortalNode; -const createHtmlInlinePortalNode = createPortalNode.bind(null, ELEMENT_TYPE_HTML_INLINE) as - = Component>(options?: Options) => HtmlPortalNode; + = Component>(options?: HtmlOptions) => HtmlPortalNode; const createSvgPortalNode = createPortalNode.bind(null, ELEMENT_TYPE_SVG) as - = Component>(options?: Options) => SvgPortalNode; + = Component>(options?: SvgOptions) => SvgPortalNode; export { createHtmlPortalNode, - createHtmlInlinePortalNode, createSvgPortalNode, InPortal, OutPortal, diff --git a/stories/html.stories.js b/stories/html.stories.js index 22bfa69..301f22a 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, createHtmlInlinePortalNode, InPortal, OutPortal } from '..'; +import { createHtmlPortalNode, InPortal, OutPortal } from '..'; const Container = (props) =>
@@ -290,7 +290,7 @@ storiesOf('Portals', module) }); }) .add('can render inline portal', () => { - const portalNode = createHtmlInlinePortalNode(); + const portalNode = createHtmlPortalNode({ containerElement: 'span' }); return

From 86fe25fe94dc365eb85217a44001bcac20e1b072 Mon Sep 17 00:00:00 2001 From: Alexander Harding Date: Sun, 23 Mar 2025 10:40:01 -0500 Subject: [PATCH 6/9] readme tweak --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 73ad5f0..6d76dc8 100644 --- a/README.md +++ b/README.md @@ -119,7 +119,7 @@ This node will contain your portal contents later, and will eventually be attach An optional options object parameter can be passed to configure the node. -- `options.containerElement` can be set to `'div'` or `'span'` to contigure the detached DOM node container type. +- `options.containerElement` (default: `div`) can be set to `'span'` to ensure valid HTML (avoid React hydration warnings) when portaling into [phrasing content](https://developer.mozilla.org/en-US/docs/Web/HTML/Content_categories#phrasing_content). - `options.attributes` can be used to set the HTML attributes (style, class, etc.) of the intermediary, like so: From f526aa1895aaf41a0419c34ebf5a6260d68de004 Mon Sep 17 00:00:00 2001 From: Alexander Harding Date: Sun, 23 Mar 2025 10:42:54 -0500 Subject: [PATCH 7/9] restore removed --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 6d76dc8..894e343 100644 --- a/README.md +++ b/README.md @@ -128,6 +128,8 @@ An optional options object parameter can be passed to configure the node. attributes: { id: "div-1", style: "background-color: #aaf; width: 100px;" } }); ``` + + The detached DOM node is also available at `.element`, so you can mutate that directly with the standard DOM APIs if preferred. ### `portals.createSvgPortalNode([options])` From 103ba232f94f7c8e5985ece001ba2934b768a905 Mon Sep 17 00:00:00 2001 From: Alexander Harding Date: Thu, 27 Mar 2025 16:06:31 -0500 Subject: [PATCH 8/9] Allow arbitrary containerElement type --- README.md | 2 +- src/index.tsx | 45 +++++++++++++++++++++-------------------- stories/html.stories.js | 43 ++++++++++++++++++++++++++++++++++++++- 3 files changed, 66 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 894e343..57a0e3e 100644 --- a/README.md +++ b/README.md @@ -133,7 +133,7 @@ An optional options object parameter can be passed to configure the node. ### `portals.createSvgPortalNode([options])` -This creates a detached SVG DOM node. It works identically to the node from `createHtmlPortalNode`, except it will work with SVG elements. Content is placed within a `` instead of a `

`. +This creates a detached SVG DOM node. It works identically to the node from `createHtmlPortalNode`, except it will work with SVG elements. Content is placed within a `` instead of a `
` by default, which can be customized by `options.containerElement`. An error will be thrown if you attempt to use a HTML node for SVG content, or a SVG node for HTML content. diff --git a/src/index.tsx b/src/index.tsx index c60910e..531a573 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -2,22 +2,19 @@ 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_BLOCK = 'div'; -const ELEMENT_TYPE_HTML_INLINE = 'span'; +const ELEMENT_TYPE_HTML = 'html'; const ELEMENT_TYPE_SVG = 'svg'; -type ANY_ELEMENT_TYPE = typeof ELEMENT_TYPE_HTML_BLOCK | typeof ELEMENT_TYPE_HTML_INLINE | typeof ELEMENT_TYPE_SVG; - type BaseOptions = { attributes?: { [key: string]: string }; }; type HtmlOptions = BaseOptions & { - containerElement?: typeof ELEMENT_TYPE_HTML_BLOCK | typeof ELEMENT_TYPE_HTML_INLINE; + containerElement?: keyof HTMLElementTagNameMap; }; type SvgOptions = BaseOptions & { - containerElement?: typeof ELEMENT_TYPE_SVG; + containerElement?: keyof SVGElementTagNameMap; }; type Options = HtmlOptions | SvgOptions; @@ -45,7 +42,7 @@ interface PortalNodeBase> { } export interface HtmlPortalNode = Component> extends PortalNodeBase { element: HTMLElement; - elementType: typeof ELEMENT_TYPE_HTML_BLOCK | typeof ELEMENT_TYPE_HTML_INLINE; + elementType: typeof ELEMENT_TYPE_HTML; } export interface SvgPortalNode = Component> extends PortalNodeBase { element: SVGElement; @@ -54,15 +51,14 @@ export interface SvgPortalNode = Component> extend type AnyPortalNode = Component> = HtmlPortalNode | SvgPortalNode; -const validateElementType = (domElement: Element, elementType: ANY_ELEMENT_TYPE) => { +const validateElementType = (domElement: Element, elementType: typeof ELEMENT_TYPE_HTML | typeof ELEMENT_TYPE_SVG) => { const ownerDocument = (domElement.ownerDocument ?? document) as any; // 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 switch (elementType) { - case ELEMENT_TYPE_HTML_BLOCK: - case ELEMENT_TYPE_HTML_INLINE: + case ELEMENT_TYPE_HTML: return domElement instanceof ownerWindow.HTMLElement; case ELEMENT_TYPE_SVG: return domElement instanceof ownerWindow.SVGElement; @@ -73,10 +69,9 @@ const validateElementType = (domElement: Element, elementType: ANY_ELEMENT_TYPE) // This is the internal implementation: the public entry points set elementType to an appropriate value const createPortalNode = >( - defaultElementType: ANY_ELEMENT_TYPE, + elementType: typeof ELEMENT_TYPE_HTML | typeof ELEMENT_TYPE_SVG, options?: Options ): AnyPortalNode => { - const elementType = options?.containerElement ?? defaultElementType; let initialProps = {} as ComponentProps; let parent: Node | undefined; @@ -85,15 +80,14 @@ const createPortalNode = >( let element; switch (elementType) { - case ELEMENT_TYPE_HTML_BLOCK: - case ELEMENT_TYPE_HTML_INLINE: - element = document.createElement(elementType); + case ELEMENT_TYPE_HTML: + element = document.createElement(options?.containerElement ?? 'div'); break; case ELEMENT_TYPE_SVG: - element = document.createElementNS(SVG_NAMESPACE, 'g'); + element = document.createElementNS(SVG_NAMESPACE, options?.containerElement ?? 'g'); break; default: - throw new Error(`Invalid element type "${elementType}" for createPortalNode: must be "div", "span" or "svg".`); + throw new Error(`Invalid element type "${elementType}" for createPortalNode: must be "html" or "svg".`); } if (options && typeof options === "object" && options.attributes) { @@ -256,14 +250,21 @@ class OutPortal> extends React.PureComponent placeholder: - // - prevents invalid HTML (e.g. inside

) - // - works fine even for SVG. - return ; + const tagName = this.props.node.element.tagName; + + // SVG tagName is lowercase and case sensitive, HTML is uppercase and case insensitive. + // React.createElement expects lowercase first letter to treat as non-component element. + // (Passing uppercase type won't break anything, but React warns otherwise:) + // https://github.com/facebook/react/blob/8039f1b2a05d00437cd29707761aeae098c80adc/CHANGELOG.md?plain=1#L1984 + const type = this.props.node.elementType === ELEMENT_TYPE_HTML + ? tagName.toLowerCase() + : tagName; + + return React.createElement(type, { ref: this.placeholderNode }); } } -const createHtmlPortalNode = createPortalNode.bind(null, ELEMENT_TYPE_HTML_BLOCK) as +const createHtmlPortalNode = createPortalNode.bind(null, ELEMENT_TYPE_HTML) as = Component>(options?: HtmlOptions) => HtmlPortalNode; const createSvgPortalNode = createPortalNode.bind(null, ELEMENT_TYPE_SVG) as = Component>(options?: SvgOptions) => SvgPortalNode; diff --git a/stories/html.stories.js b/stories/html.stories.js index 301f22a..089d4d6 100644 --- a/stories/html.stories.js +++ b/stories/html.stories.js @@ -289,7 +289,7 @@ storiesOf('Portals', module)

}); }) - .add('can render inline portal', () => { + .add('portal container element as span in paragraph', () => { const portalNode = createHtmlPortalNode({ containerElement: 'span' }); return
@@ -306,6 +306,47 @@ storiesOf('Portals', module)

; }) + .add("portal container element as tr", () => { + const portalNode = createHtmlPortalNode({ containerElement: "tr" }); + + return React.createElement(() => { + const [useFirstTable, setUseFirstTable] = React.useState(true); + + return ( +
+ + Cell 1 + Cell 2 + Cell 3 + + + + +
+ + + + + + + {useFirstTable && } +
First Table
+ + + + + + + + {!useFirstTable && } +
Second Table
+
+
+ ); + }); + }) .add('Example from README', () => { const MyExpensiveComponent = () => 'expensive!'; From ead89031c3a1cfa1db36d2bea064f97c038d65d8 Mon Sep 17 00:00:00 2001 From: Alexander Harding Date: Thu, 27 Mar 2025 16:11:39 -0500 Subject: [PATCH 9/9] indent --- stories/html.stories.js | 68 ++++++++++++++++++++--------------------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/stories/html.stories.js b/stories/html.stories.js index 089d4d6..098a9fe 100644 --- a/stories/html.stories.js +++ b/stories/html.stories.js @@ -310,41 +310,41 @@ storiesOf('Portals', module) const portalNode = createHtmlPortalNode({ containerElement: "tr" }); return React.createElement(() => { - const [useFirstTable, setUseFirstTable] = React.useState(true); + const [useFirstTable, setUseFirstTable] = React.useState(true); - return ( -
- - Cell 1 - Cell 2 - Cell 3 - - - - -
- - - - - - - {useFirstTable && } -
First Table
- - - - - - - - {!useFirstTable && } -
Second Table
-
-
- ); + return ( +
+ + Cell 1 + Cell 2 + Cell 3 + + + + +
+ + + + + + + {useFirstTable && } +
First Table
+ + + + + + + + {!useFirstTable && } +
Second Table
+
+
+ ); }); }) .add('Example from README', () => {