VisuallyHidden and Portal utilities

This commit is contained in:
Mats Bosson 2026-03-29 03:01:52 +07:00
parent b291ceab50
commit 697e80ef72
5 changed files with 86 additions and 0 deletions

View File

@ -0,0 +1,2 @@
export { Portal } from "./portal";
export type { PortalProps } from "./portal";

View File

@ -0,0 +1,25 @@
import type { JSX } from "solid-js";
import { isServer } from "solid-js/web";
import { Portal as SolidPortal } from "solid-js/web";
export interface PortalProps {
/** Target container. Defaults to document.body. */
target?: Element | null;
children: JSX.Element;
}
/**
* SSR-safe portal. During SSR, renders content inline.
* On the client, moves content to the target container.
*/
export function Portal(props: PortalProps): JSX.Element {
if (isServer) {
return <>{props.children}</>;
}
return (
<SolidPortal mount={props.target ?? document.body}>
{props.children}
</SolidPortal>
);
}

View File

@ -0,0 +1,2 @@
export { VisuallyHidden } from "./visually-hidden";
export type { VisuallyHiddenProps } from "./visually-hidden";

View File

@ -0,0 +1,34 @@
import type { JSX } from "solid-js";
import { splitProps } from "solid-js";
export interface VisuallyHiddenProps extends JSX.HTMLAttributes<HTMLSpanElement> {
children?: JSX.Element;
}
const visuallyHiddenStyle: JSX.CSSProperties = {
position: "absolute",
border: "0",
width: "1px",
height: "1px",
padding: "0",
margin: "-1px",
overflow: "hidden",
clip: "rect(0, 0, 0, 0)",
"white-space": "nowrap",
"word-wrap": "normal",
};
/**
* Renders content visually hidden but accessible to screen readers.
*/
export function VisuallyHidden(props: VisuallyHiddenProps): JSX.Element {
const [local, rest] = splitProps(props, ["children", "style"]);
return (
<span
style={{ ...visuallyHiddenStyle, ...(local.style as JSX.CSSProperties | undefined) }}
{...rest}
>
{local.children}
</span>
);
}

View File

@ -0,0 +1,23 @@
import { render } from "@solidjs/testing-library";
import { describe, expect, it } from "vitest";
import { Portal } from "../../src/utilities/portal/portal";
describe("Portal", () => {
it("renders children into document.body by default", () => {
render(() => <Portal><div data-testid="portal-content">hello</div></Portal>);
// Content should be in document.body, not the render container
expect(document.body.querySelector("[data-testid='portal-content']")).toBeTruthy();
});
it("renders children into a custom target", () => {
const target = document.createElement("div");
document.body.appendChild(target);
render(() => (
<Portal target={target}>
<div data-testid="custom-portal">hello</div>
</Portal>
));
expect(target.querySelector("[data-testid='custom-portal']")).toBeTruthy();
document.body.removeChild(target);
});
});