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