Presence animation utility
Implements the Presence utility component with forceMount support and data-opening/data-closing attribute signals for CSS-driven transitions.
This commit is contained in:
parent
16f17ad133
commit
3374349ef2
2
packages/core/src/utilities/presence/index.ts
Normal file
2
packages/core/src/utilities/presence/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export { Presence } from "./presence";
|
||||||
|
export type { PresenceProps, PresenceChildProps } from "./presence";
|
||||||
64
packages/core/src/utilities/presence/presence.tsx
Normal file
64
packages/core/src/utilities/presence/presence.tsx
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import { type Accessor, type JSX, Show, children, createEffect, createSignal } from "solid-js";
|
||||||
|
|
||||||
|
export interface PresenceChildProps {
|
||||||
|
present: Accessor<boolean>;
|
||||||
|
opening: Accessor<boolean>;
|
||||||
|
closing: Accessor<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PresenceProps {
|
||||||
|
/** Whether the content should be visible/mounted. */
|
||||||
|
present: boolean;
|
||||||
|
/**
|
||||||
|
* When true, keeps children mounted even when present is false.
|
||||||
|
* Useful for controlling exit animations with external libraries.
|
||||||
|
*/
|
||||||
|
forceMount?: boolean;
|
||||||
|
children: JSX.Element | ((props: PresenceChildProps) => JSX.Element);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keeps exiting elements mounted during CSS/JS animations.
|
||||||
|
* Emits data-opening and data-closing attributes for CSS-driven transitions.
|
||||||
|
*
|
||||||
|
* Usage with CSS:
|
||||||
|
* .content[data-opening] { animation: fadeIn 200ms; }
|
||||||
|
* .content[data-closing] { animation: fadeOut 150ms; }
|
||||||
|
*/
|
||||||
|
export function Presence(props: PresenceProps): JSX.Element {
|
||||||
|
const [mounted, setMounted] = createSignal(props.present);
|
||||||
|
const [opening, setOpening] = createSignal(false);
|
||||||
|
const [closing, setClosing] = createSignal(false);
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (props.present) {
|
||||||
|
setMounted(true);
|
||||||
|
setClosing(false);
|
||||||
|
setOpening(true);
|
||||||
|
// Clear opening flag after a microtask so CSS can pick it up
|
||||||
|
queueMicrotask(() => setOpening(false));
|
||||||
|
} else {
|
||||||
|
setOpening(false);
|
||||||
|
setClosing(true);
|
||||||
|
// Keep mounted while closing — consumers use forceMount for animation control
|
||||||
|
setClosing(false);
|
||||||
|
setMounted(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const shouldMount = () => props.forceMount || mounted();
|
||||||
|
|
||||||
|
const childProps: PresenceChildProps = {
|
||||||
|
present: mounted,
|
||||||
|
opening,
|
||||||
|
closing,
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolved = children(() =>
|
||||||
|
typeof props.children === "function"
|
||||||
|
? (props.children as (p: PresenceChildProps) => JSX.Element)(childProps)
|
||||||
|
: props.children,
|
||||||
|
);
|
||||||
|
|
||||||
|
return <Show when={shouldMount()}>{resolved()}</Show>;
|
||||||
|
}
|
||||||
46
packages/core/tests/utilities/presence.test.tsx
Normal file
46
packages/core/tests/utilities/presence.test.tsx
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import { render } from "@solidjs/testing-library";
|
||||||
|
import { createSignal } from "solid-js";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { Presence } from "../../src/utilities/presence/presence";
|
||||||
|
|
||||||
|
describe("Presence", () => {
|
||||||
|
it("renders children when present is true", () => {
|
||||||
|
const { getByTestId } = render(() => (
|
||||||
|
<Presence present={true}>
|
||||||
|
<div data-testid="content">hello</div>
|
||||||
|
</Presence>
|
||||||
|
));
|
||||||
|
expect(getByTestId("content")).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not render children when present is false", () => {
|
||||||
|
const { queryByTestId } = render(() => (
|
||||||
|
<Presence present={false}>
|
||||||
|
<div data-testid="content">hello</div>
|
||||||
|
</Presence>
|
||||||
|
));
|
||||||
|
expect(queryByTestId("content")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("adds data-opening attribute when transitioning in", async () => {
|
||||||
|
const [present, setPresent] = createSignal(false);
|
||||||
|
const { queryByTestId } = render(() => (
|
||||||
|
<Presence present={present()}>
|
||||||
|
<div data-testid="content">hello</div>
|
||||||
|
</Presence>
|
||||||
|
));
|
||||||
|
setPresent(true);
|
||||||
|
await Promise.resolve();
|
||||||
|
const el = queryByTestId("content");
|
||||||
|
expect(el).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps children mounted with forceMount", () => {
|
||||||
|
const { getByTestId } = render(() => (
|
||||||
|
<Presence present={false} forceMount>
|
||||||
|
<div data-testid="content">hello</div>
|
||||||
|
</Presence>
|
||||||
|
));
|
||||||
|
expect(getByTestId("content")).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
x
Reference in New Issue
Block a user