Skip to content
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

Bytter ut FloatingPortal med egen implementasjon #2697

Merged
merged 11 commits into from
Feb 6, 2024
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";
34 changes: 17 additions & 17 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3449,7 +3449,7 @@ __metadata:
languageName: node
linkType: hard

"@navikt/aksel-icons@^5.17.2, @navikt/aksel-icons@workspace:@navikt/aksel-icons":
"@navikt/aksel-icons@^5.17.4, @navikt/aksel-icons@workspace:@navikt/aksel-icons":
version: 0.0.0-use.local
resolution: "@navikt/aksel-icons@workspace:@navikt/aksel-icons"
dependencies:
Expand All @@ -3476,8 +3476,8 @@ __metadata:
version: 0.0.0-use.local
resolution: "@navikt/aksel-stylelint@workspace:@navikt/aksel-stylelint"
dependencies:
"@navikt/ds-css": ^5.17.2
"@navikt/ds-tokens": ^5.17.2
"@navikt/ds-css": ^5.17.4
"@navikt/ds-tokens": ^5.17.4
"@types/jest": ^29.0.0
concurrently: 7.2.1
copyfiles: 2.4.1
Expand All @@ -3495,7 +3495,7 @@ __metadata:
version: 0.0.0-use.local
resolution: "@navikt/aksel@workspace:@navikt/aksel"
dependencies:
"@navikt/ds-css": 5.17.2
"@navikt/ds-css": 5.17.4
"@types/inquirer": ^9.0.3
"@types/jest": ^29.0.0
axios: 1.6.0
Expand All @@ -3519,11 +3519,11 @@ __metadata:
languageName: unknown
linkType: soft

"@navikt/ds-css@*, @navikt/[email protected].2, @navikt/ds-css@^5.17.2, @navikt/ds-css@^5.9.2, @navikt/ds-css@workspace:@navikt/core/css":
"@navikt/ds-css@*, @navikt/[email protected].4, @navikt/ds-css@^5.17.4, @navikt/ds-css@^5.9.2, @navikt/ds-css@workspace:@navikt/core/css":
version: 0.0.0-use.local
resolution: "@navikt/ds-css@workspace:@navikt/core/css"
dependencies:
"@navikt/ds-tokens": ^5.17.2
"@navikt/ds-tokens": ^5.17.4
cssnano: 6.0.0
fast-glob: 3.2.11
lodash: 4.17.21
Expand All @@ -3536,13 +3536,13 @@ __metadata:
languageName: unknown
linkType: soft

"@navikt/ds-react@*, @navikt/ds-react@^5.17.2, @navikt/ds-react@^5.9.2, @navikt/ds-react@workspace:@navikt/core/react":
"@navikt/ds-react@*, @navikt/ds-react@^5.17.4, @navikt/ds-react@^5.9.2, @navikt/ds-react@workspace:@navikt/core/react":
version: 0.0.0-use.local
resolution: "@navikt/ds-react@workspace:@navikt/core/react"
dependencies:
"@floating-ui/react": 0.25.4
"@navikt/aksel-icons": ^5.17.2
"@navikt/ds-tokens": ^5.17.2
"@navikt/aksel-icons": ^5.17.4
"@navikt/ds-tokens": ^5.17.4
"@radix-ui/react-tabs": 1.0.0
"@radix-ui/react-toggle-group": 1.0.0
"@testing-library/dom": 8.13.0
Expand Down Expand Up @@ -3576,11 +3576,11 @@ __metadata:
languageName: unknown
linkType: soft

"@navikt/ds-tailwind@^5.17.2, @navikt/ds-tailwind@workspace:@navikt/core/tailwind":
"@navikt/ds-tailwind@^5.17.4, @navikt/ds-tailwind@workspace:@navikt/core/tailwind":
version: 0.0.0-use.local
resolution: "@navikt/ds-tailwind@workspace:@navikt/core/tailwind"
dependencies:
"@navikt/ds-tokens": ^5.17.2
"@navikt/ds-tokens": ^5.17.4
"@types/jest": ^29.0.0
color: 4.2.3
jest: ^29.0.0
Expand All @@ -3592,7 +3592,7 @@ __metadata:
languageName: unknown
linkType: soft

"@navikt/ds-tokens@^5.17.2, @navikt/ds-tokens@workspace:@navikt/core/tokens":
"@navikt/ds-tokens@^5.17.4, @navikt/ds-tokens@workspace:@navikt/core/tokens":
version: 0.0.0-use.local
resolution: "@navikt/ds-tokens@workspace:@navikt/core/tokens"
dependencies:
Expand Down Expand Up @@ -8842,11 +8842,11 @@ __metadata:
version: 0.0.0-use.local
resolution: "aksel.nav.no@workspace:aksel.nav.no"
dependencies:
"@navikt/aksel-icons": ^5.17.2
"@navikt/ds-css": ^5.17.2
"@navikt/ds-react": ^5.17.2
"@navikt/ds-tailwind": ^5.17.2
"@navikt/ds-tokens": ^5.17.2
"@navikt/aksel-icons": ^5.17.4
"@navikt/ds-css": ^5.17.4
"@navikt/ds-react": ^5.17.4
"@navikt/ds-tailwind": ^5.17.4
"@navikt/ds-tokens": ^5.17.4
languageName: unknown
linkType: soft

Expand Down
Loading