diff --git a/src/Sticky.stories.tsx b/src/Sticky.stories.tsx index a8c520c..5f09740 100644 --- a/src/Sticky.stories.tsx +++ b/src/Sticky.stories.tsx @@ -1,5 +1,6 @@ import { action } from "@storybook/addon-actions"; import { Meta, Story } from "@storybook/react"; +import { createPortal } from "react-dom"; import React, { ComponentPropsWithoutRef, CSSProperties, @@ -7,6 +8,7 @@ import React, { forwardRef, PropsWithChildren, useCallback, + useEffect, useRef, useState, } from "react"; @@ -27,6 +29,7 @@ import { useStickyOffsetCalculator, } from "./index"; import { ScrollContext, useScrollElement } from "./scroll"; +import { useIsomorphicLayoutEffect } from "./util"; // tslint:disable-next-line:no-object-literal-type-assertion export default { @@ -527,3 +530,138 @@ PositionAbsoluteContainer.argTypes = { type: "boolean", }, }; + +function Drawer({ behavior1, onClose, children, right, left }: any) { + return ( +
+
} + > + +

Title

+ +
+
+

{children}

+ +
+ + + ); +} + +interface IPortal { + /** Custom DOM node to render the portal in */ + node?: HTMLDivElement; + /** Set a custom id for the portal node */ + id?: string; + children?: React.ReactNode; +} + +const Portal: React.FC = ({ children }) => { + const [defaultNode, setDefaultNode] = useState(); + const portalId = "portal0"; + + useEffect(() => { + const portalDiv = + typeof window === "undefined" ? undefined : document.createElement("div"); + + if (portalDiv) portalDiv.id = portalId; + + setDefaultNode(portalDiv); + }, [portalId]); + + useIsomorphicLayoutEffect(() => { + if (!defaultNode) return; + + document.body.appendChild(defaultNode); + return () => { + /** Query the element to remove, in case it was modified externally. */ + const portal = document.getElementById(portalId); + if (portal) { + document.body.removeChild(portal); + } + }; + }, [defaultNode, portalId]); + + if (!defaultNode) { + return null; + } + + return createPortal(children, defaultNode); +}; + +function InPortal({ id, children }: any) { + const [hasMounted, setHasMounted] = React.useState(false); + React.useEffect(() => { + setHasMounted(true); + }, []); + if (!hasMounted) { + return null; + } + return createPortal(children, document.querySelector(`#${id}`)!); +} +export const MountingAndUnmountingDrawer: Story = ({ + behavior1, +}) => { + const [isOpen, setOpen] = useState(false); + + return ( +
+ +
+ {isOpen && ( + + setOpen(false)} behavior1={behavior1}> + WORKS + + + )} + {isOpen && ( + + setOpen(false)} + behavior1={behavior1} + BROKEN + > + + )} +
+ ); +}; + +MountingAndUnmountingDrawer.argTypes = { + behavior1: behaviorControl("Behavior 1"), +}; diff --git a/src/components.ts b/src/components.ts index 3ed32cc..ee3adf6 100644 --- a/src/components.ts +++ b/src/components.ts @@ -15,6 +15,7 @@ import { useMemo, useRef, } from "react"; +import { useIsomorphicLayoutEffect } from "./util"; import { elementRootOffset, ICssStyleData, @@ -114,6 +115,7 @@ export const Sticky: FC> = memo( }) => { const { baseZIndex } = useContext(StickyConfigContext); const behaviorState = useRef({}); + const didApplyBehaviorRef = useRef(false); const placeholderRef = useRef(); let ref: RefObject; const handle: IStickyHandle = { @@ -143,6 +145,7 @@ export const Sticky: FC> = memo( } placeholder.style.height = wrapper.offsetHeight + "px"; wrapper.style.width = placeholder.offsetWidth + "px"; + didApplyBehaviorRef.current = true; }, }; @@ -153,6 +156,16 @@ export const Sticky: FC> = memo( // We are not running in a scroll container. Just show the content. return createElement(Fragment, {}, children); } + // eslint-disable-next-line react-hooks/rules-of-hooks + useIsomorphicLayoutEffect(() => { + // Set wrapper style in a layout effect for compatibility with SSR. But only if the true behavior hasn't been applied yet. + if (ref.current && !didApplyBehaviorRef.current) { + const element = ref.current; + Object.entries(wrapperStyle).forEach(([k, v]) => { + element.style.setProperty(k, v); + }); + } + }, [didApplyBehaviorRef, ref]); return createElement( Fragment,