Disclosure state primitive
Implements controlled/uncontrolled open-close state for disclosure components (Dialog, Popover, Tooltip, Collapsible, etc.) wrapping createControllableSignal with open/close/toggle convenience methods.
This commit is contained in:
parent
512eba474a
commit
ed13193430
37
packages/core/src/primitives/create-disclosure-state.ts
Normal file
37
packages/core/src/primitives/create-disclosure-state.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import type { Accessor } from "solid-js";
|
||||||
|
import { createControllableSignal } from "./create-controllable-signal";
|
||||||
|
|
||||||
|
export interface CreateDisclosureStateOptions {
|
||||||
|
open?: boolean;
|
||||||
|
defaultOpen?: boolean;
|
||||||
|
onOpenChange?: (open: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DisclosureState {
|
||||||
|
isOpen: Accessor<boolean>;
|
||||||
|
open: () => void;
|
||||||
|
close: () => void;
|
||||||
|
toggle: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared open/close state for all disclosure components (Dialog, Popover,
|
||||||
|
* Tooltip, Collapsible, etc.). Wraps createControllableSignal with
|
||||||
|
* convenience open/close/toggle methods.
|
||||||
|
*/
|
||||||
|
export function createDisclosureState(
|
||||||
|
options: CreateDisclosureStateOptions,
|
||||||
|
): DisclosureState {
|
||||||
|
const [isOpen, setIsOpen] = createControllableSignal<boolean>({
|
||||||
|
value: () => options.open,
|
||||||
|
defaultValue: () => options.defaultOpen ?? false,
|
||||||
|
onChange: options.onOpenChange,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
isOpen,
|
||||||
|
open: () => setIsOpen(true),
|
||||||
|
close: () => setIsOpen(false),
|
||||||
|
toggle: () => setIsOpen(!isOpen()),
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -0,0 +1,76 @@
|
|||||||
|
import { createRoot } from "solid-js";
|
||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
import { createDisclosureState } from "../../src/primitives/create-disclosure-state";
|
||||||
|
|
||||||
|
describe("createDisclosureState", () => {
|
||||||
|
it("starts closed by default", () => {
|
||||||
|
createRoot((dispose) => {
|
||||||
|
const state = createDisclosureState({});
|
||||||
|
expect(state.isOpen()).toBe(false);
|
||||||
|
dispose();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("respects defaultOpen", () => {
|
||||||
|
createRoot((dispose) => {
|
||||||
|
const state = createDisclosureState({ defaultOpen: true });
|
||||||
|
expect(state.isOpen()).toBe(true);
|
||||||
|
dispose();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("open() sets state to true", () => {
|
||||||
|
createRoot((dispose) => {
|
||||||
|
const state = createDisclosureState({});
|
||||||
|
state.open();
|
||||||
|
expect(state.isOpen()).toBe(true);
|
||||||
|
dispose();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("close() sets state to false", () => {
|
||||||
|
createRoot((dispose) => {
|
||||||
|
const state = createDisclosureState({ defaultOpen: true });
|
||||||
|
state.close();
|
||||||
|
expect(state.isOpen()).toBe(false);
|
||||||
|
dispose();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("toggle() flips state", () => {
|
||||||
|
createRoot((dispose) => {
|
||||||
|
const state = createDisclosureState({});
|
||||||
|
state.toggle();
|
||||||
|
expect(state.isOpen()).toBe(true);
|
||||||
|
state.toggle();
|
||||||
|
expect(state.isOpen()).toBe(false);
|
||||||
|
dispose();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calls onOpenChange when state changes", () => {
|
||||||
|
createRoot((dispose) => {
|
||||||
|
const onChange = vi.fn();
|
||||||
|
const state = createDisclosureState({ onOpenChange: onChange });
|
||||||
|
state.open();
|
||||||
|
expect(onChange).toHaveBeenCalledWith(true);
|
||||||
|
state.close();
|
||||||
|
expect(onChange).toHaveBeenCalledWith(false);
|
||||||
|
dispose();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("respects controlled open prop", () => {
|
||||||
|
createRoot((dispose) => {
|
||||||
|
const state = createDisclosureState({ open: true });
|
||||||
|
expect(state.isOpen()).toBe(true);
|
||||||
|
// Calling close() fires onChange but does not change internal state
|
||||||
|
const onChange = vi.fn();
|
||||||
|
const controlled = createDisclosureState({ open: true, onOpenChange: onChange });
|
||||||
|
controlled.close();
|
||||||
|
expect(controlled.isOpen()).toBe(true); // still controlled
|
||||||
|
expect(onChange).toHaveBeenCalledWith(false);
|
||||||
|
dispose();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
x
Reference in New Issue
Block a user