Skip to content

Fix/portal broken repro #32

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 138 additions & 0 deletions src/Sticky.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { action } from "@storybook/addon-actions";
import { Meta, Story } from "@storybook/react";
import { createPortal } from "react-dom";
import React, {
ComponentPropsWithoutRef,
CSSProperties,
FC,
forwardRef,
PropsWithChildren,
useCallback,
useEffect,
useRef,
useState,
} from "react";
Expand All @@ -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 {
Expand Down Expand Up @@ -527,3 +530,138 @@ PositionAbsoluteContainer.argTypes = {
type: "boolean",
},
};

function Drawer({ behavior1, onClose, children, right, left }: any) {
return (
<div
style={{
height: "100%",
background: "yellow",
position: "fixed",
top: 0,
left,
right,
width: 400,
}}
>
<StickyScrollContainer
element={<div style={{ height: "inherit", overflow: "scroll" }}></div>}
>
<Sticky behavior={behavior1}>
<h2>Title</h2>
<button onClick={onClose}>Close drawer</button>
</Sticky>
<div>
<h1>{children}</h1>
<ul>
<li>
Morbi in sem quis dui placerat ornare. Pellentesque odio nisi,
euismod in, pharetra a, ultricies in, diam. Sed arcu. Cras
consequat.
</li>
<li>
Praesent dapibus, neque id cursus faucibus, tortor neque egestas
augue, eu vulputate magna eros eu erat. Aliquam erat volutpat. Nam
dui mi, tincidunt quis, accumsan porttitor, facilisis luctus,
metus.
</li>
<li>
Phasellus ultrices nulla quis nibh. Quisque a lectus. Donec
consectetuer ligula vulputate sem tristique cursus. Nam nulla
quam, gravida non, commodo a, sodales sit amet, nisi.
</li>
<li>
Pellentesque fermentum dolor. Aliquam quam lectus, facilisis
auctor, ultrices ut, elementum vulputate, nunc.
</li>
</ul>
</div>
</StickyScrollContainer>
</div>
);
}

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<IPortal> = ({ children }) => {
const [defaultNode, setDefaultNode] = useState<HTMLElement>();
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<IStickyContentProps> = ({
behavior1,
}) => {
const [isOpen, setOpen] = useState(false);

return (
<div>
<button onClick={() => setOpen(true)}>Open drawer</button>
<div id="drawer"></div>
{isOpen && (
<InPortal id="drawer">
<Drawer left={0} onClose={() => setOpen(false)} behavior1={behavior1}>
WORKS
</Drawer>
</InPortal>
)}
{isOpen && (
<Portal>
<Drawer
right={0}
onClose={() => setOpen(false)}
behavior1={behavior1}
BROKEN
></Drawer>
</Portal>
)}
</div>
);
};

MountingAndUnmountingDrawer.argTypes = {
behavior1: behaviorControl("Behavior 1"),
};
13 changes: 13 additions & 0 deletions src/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
useMemo,
useRef,
} from "react";
import { useIsomorphicLayoutEffect } from "./util";
import {
elementRootOffset,
ICssStyleData,
Expand Down Expand Up @@ -114,6 +115,7 @@ export const Sticky: FC<PropsWithChildren<IStickyProps>> = memo(
}) => {
const { baseZIndex } = useContext(StickyConfigContext);
const behaviorState = useRef<any>({});
const didApplyBehaviorRef = useRef(false);
const placeholderRef = useRef<HTMLElement>();
let ref: RefObject<HTMLElement>;
const handle: IStickyHandle = {
Expand Down Expand Up @@ -143,6 +145,7 @@ export const Sticky: FC<PropsWithChildren<IStickyProps>> = memo(
}
placeholder.style.height = wrapper.offsetHeight + "px";
wrapper.style.width = placeholder.offsetWidth + "px";
didApplyBehaviorRef.current = true;
},
};

Expand All @@ -153,6 +156,16 @@ export const Sticky: FC<PropsWithChildren<IStickyProps>> = 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,
Expand Down