Skip to content

Commit

Permalink
Bytter ut FloatingPortal med egen implementasjon (#2697)
Browse files Browse the repository at this point in the history
* 🎉 Egen Portal-komponent

* 🎨 Portal og Overlay exportert i root

* ♻️ Tooltip bruker egen Portal

* 📝 Changeset

* 🔥 Fjernet ID på Portal-element

* 🎉 Ny type WithAsChild

* ♻️ Mer robust sjekk for conditional elements i WithAsChild

* 📝 WithAsChild JSDOC

* 📝 WithAsChild JSDOC 2

* Update .changeset/lovely-points-heal.md

Co-authored-by: Halvor Haugan <[email protected]>

* ♻️ Fikset kommentarer fra git-pr

---------

Co-authored-by: Halvor Haugan <[email protected]>
  • Loading branch information
KenAJoh and HalvorHaugan authored Feb 6, 2024
1 parent 8556695 commit f44ff44
Show file tree
Hide file tree
Showing 9 changed files with 185 additions and 10 deletions.
5 changes: 5 additions & 0 deletions .changeset/lovely-points-heal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@navikt/ds-react": minor
---

Portal: Ny komponent `Portal` som lar deg enkelt bruke `createPortal`, også på serversiden
1 change: 1 addition & 0 deletions @navikt/core/react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export * from "./expansion-card";
export * from "./form";
export * from "./grid";
export * from "./guide-panel";
export * from "./overlays";
export * from "./help-text";
export * from "./internal-header";
export * from "./link";
Expand Down
2 changes: 2 additions & 0 deletions @navikt/core/react/src/overlays/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { Portal } from "./portal/Portal";
export type { PortalProps } from "./portal/Portal";
99 changes: 99 additions & 0 deletions @navikt/core/react/src/overlays/portal/Portal.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import React from "react";
import { Box } from "../../layout/box";
import { Provider } from "../../provider";
import { Portal } from "./Portal";

export default {
title: "Utilities/Portal",
parameters: {
chromatic: { disable: true },
},
};

export const Default = () => {
return (
<Box background="surface-neutral-subtle" border>
<h1>In regular DOM tree</h1>
<p>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Temporibus
necessitatibus quis esse nesciunt est velit voluptatibus. Distinctio eum
commodi tempora unde. Nulla vel tempora incidunt? Voluptatem molestias
impedit commodi. Tenetur!
</p>
<Portal>
<h1>Inside Portal to different DOM tree</h1>
<p>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Temporibus
necessitatibus quis esse nesciunt est velit voluptatibus. Distinctio
eum commodi tempora unde. Nulla vel tempora incidunt? Voluptatem
molestias impedit commodi. Tenetur!
</p>
</Portal>
</Box>
);
};

export const CustomPortalRoot = () => {
const [portalContainer, setPortalContainer] =
React.useState<HTMLDivElement | null>(null);

return (
<Box background="surface-neutral-subtle">
<Box background="surface-alt-1-subtle">
<h1>Tree A</h1>
<Portal rootElement={portalContainer}>
<p>This is mounted to Tree B, while created inside Tree A</p>
</Portal>
</Box>
<Box background="surface-alt-3-subtle" ref={setPortalContainer}>
<h1>Tree B</h1>
</Box>
</Box>
);
};

export const CustomPortalRootFromProvider = () => {
const [portalContainer, setPortalContainer] =
React.useState<HTMLDivElement>();

return (
<Provider rootElement={portalContainer}>
<Box background="surface-neutral-subtle">
<Box background="surface-alt-1-subtle">
<h1>Tree A</h1>
<Portal>
<p>This is mounted to Tree B, while created inside Tree A</p>
</Portal>
</Box>
<Box background="surface-alt-3-subtle" ref={setPortalContainer}>
<h1>Tree B</h1>
</Box>
</Box>
</Provider>
);
};

export const AsChild = () => {
return (
<Box background="surface-neutral-subtle" border>
<h1>In regular DOM tree</h1>
<p>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Temporibus
necessitatibus quis esse nesciunt est velit voluptatibus. Distinctio eum
commodi tempora unde. Nulla vel tempora incidunt? Voluptatem molestias
impedit commodi. Tenetur!
</p>
<Portal asChild>
<div data-this-is-the-child>
<h1>Inside Portal to different DOM tree</h1>
<p>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Temporibus
necessitatibus quis esse nesciunt est velit voluptatibus. Distinctio
eum commodi tempora unde. Nulla vel tempora incidunt? Voluptatem
molestias impedit commodi. Tenetur!
</p>
</div>
</Portal>
</Box>
);
};
32 changes: 32 additions & 0 deletions @navikt/core/react/src/overlays/portal/Portal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React, { HTMLAttributes, forwardRef } from "react";
import ReactDOM from "react-dom";
import { useProvider } from "../../provider";
import { Slot } from "../../util/Slot";
import { AsChildProps } from "../../util/types";

interface PortalBaseProps extends HTMLAttributes<HTMLDivElement> {
/**
* An optional container where the portaled content should be appended.
*/
rootElement?: HTMLElement | null;
}

export type PortalProps = PortalBaseProps & AsChildProps;

export const Portal = forwardRef<HTMLDivElement, PortalProps>(
({ rootElement, asChild, ...rest }, ref) => {
const contextRoot = useProvider()?.rootElement;
const root = rootElement ?? contextRoot ?? globalThis?.document?.body;

const Component = asChild ? Slot : "div";

return root
? ReactDOM.createPortal(
<Component ref={ref} data-aksel-portal="" {...rest} />,
root,
)
: null;
},
);

export default Portal;
12 changes: 4 additions & 8 deletions @navikt/core/react/src/tooltip/Tooltip.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import {
FloatingPortal,
autoUpdate,
arrow as flArrow,
flip,
Expand All @@ -21,7 +20,7 @@ import React, {
useRef,
} from "react";
import { ModalContext } from "../modal/ModalContext";
import { useProvider } from "../provider";
import Portal from "../overlays/portal/Portal";
import { Detail } from "../typography";
import { useId } from "../util/hooks";
import { useControllableState } from "../util/hooks/useControllableState";
Expand Down Expand Up @@ -123,10 +122,7 @@ export const Tooltip = forwardRef<HTMLDivElement, TooltipProps>(

const arrowRef = useRef<HTMLDivElement | null>(null);
const modalContext = useContext(ModalContext);
const providerRootElement = useProvider()?.rootElement;
const rootElement = modalContext
? modalContext.ref.current
: providerRootElement;
const rootElement = modalContext ? modalContext.ref.current : undefined;

const {
x,
Expand Down Expand Up @@ -199,7 +195,7 @@ export const Tooltip = forwardRef<HTMLDivElement, TooltipProps>(
: children?.props["aria-describedby"],
}),
)}
<FloatingPortal root={rootElement}>
<Portal rootElement={rootElement}>
{_open && (
<div
{...getFloatingProps({
Expand Down Expand Up @@ -253,7 +249,7 @@ export const Tooltip = forwardRef<HTMLDivElement, TooltipProps>(
)}
</div>
)}
</FloatingPortal>
</Portal>
</>
);
},
Expand Down
6 changes: 4 additions & 2 deletions @navikt/core/react/src/util/Slot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@ export const Slot = React.forwardRef<HTMLElement, SlotProps>(
}

if (React.Children.count(children) > 1) {
console.error(
const error = new Error(
"Aksel: Components using 'asChild' expects to recieve a single React element child.",
);
return React.Children.only(null);
error.name = "SlotError";
Error.captureStackTrace?.(error, Slot);
throw error;
}

return null;
Expand Down
37 changes: 37 additions & 0 deletions @navikt/core/react/src/util/types/AsChildProps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
export type AsChildProps =
| {
children: React.ReactElement | false | null;
/**
* Renders the component and its child as a single element,
* merging the props of the component with the props of the child.
*
* @example
* ```tsx
* <Component asChild data-prop>
* <ChildComponent data-child />
* </Component>
*
* // Renders
* <MergedComponent data-prop data-child />
* ```
*/
asChild: true;
}
| {
children: React.ReactNode;
/**
* Renders the component and its child as a single element,
* merging the props of the component with the props of the child.
*
* @example
* ```tsx
* <Component asChild data-prop>
* <ChildComponent data-child />
* </Component>
*
* // Renders
* <MergedComponent data-prop data-child />
* ```
*/
asChild?: false;
};
1 change: 1 addition & 0 deletions @navikt/core/react/src/util/types/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export type { OverridableComponent } from "./OverridableComponent";
export type { AsChildProps } from "./AsChildProps";

0 comments on commit f44ff44

Please sign in to comment.