Refactor out circular dependency in Transition components
This was causing problems with Jest even though it was otherwise fine.
This commit is contained in:
@@ -12,7 +12,7 @@
|
|||||||
useNesting,
|
useNesting,
|
||||||
useParentNesting,
|
useParentNesting,
|
||||||
useTransitionContext,
|
useTransitionContext,
|
||||||
} from "./TransitionRoot.svelte";
|
} from "$lib/components/transitions/common.svelte";
|
||||||
import { useId } from "$lib/hooks/use-id";
|
import { useId } from "$lib/hooks/use-id";
|
||||||
import { forwardEventsBuilder } from "$lib/internal/forwardEventsBuilder";
|
import { forwardEventsBuilder } from "$lib/internal/forwardEventsBuilder";
|
||||||
import { get_current_component } from "svelte/internal";
|
import { get_current_component } from "svelte/internal";
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { hasOpenClosed } from "$lib/internal/open-closed";
|
import { hasOpenClosed } from "$lib/internal/open-closed";
|
||||||
import TransitionChild from "./TransitionChild.svelte";
|
import TransitionChild from "./TransitionChild.svelte";
|
||||||
import TransitionRoot, {
|
import TransitionRoot from "./TransitionRoot.svelte";
|
||||||
hasTransitionContext,
|
|
||||||
} from "./TransitionRoot.svelte";
|
|
||||||
import { forwardEventsBuilder } from "$lib/internal/forwardEventsBuilder";
|
import { forwardEventsBuilder } from "$lib/internal/forwardEventsBuilder";
|
||||||
import { get_current_component } from "svelte/internal";
|
import { get_current_component } from "svelte/internal";
|
||||||
import type { SupportedAs } from "$lib/internal/elements";
|
import type { SupportedAs } from "$lib/internal/elements";
|
||||||
import type { HTMLActionArray } from "$lib/hooks/use-actions";
|
import type { HTMLActionArray } from "$lib/hooks/use-actions";
|
||||||
|
import { hasTransitionContext } from "./common.svelte";
|
||||||
const forwardEvents = forwardEventsBuilder(get_current_component(), [
|
const forwardEvents = forwardEventsBuilder(get_current_component(), [
|
||||||
"beforeEnter",
|
"beforeEnter",
|
||||||
"beforeLeave",
|
"beforeLeave",
|
||||||
|
|||||||
@@ -1,126 +1,22 @@
|
|||||||
<script lang="ts" context="module">
|
|
||||||
export enum TreeStates {
|
|
||||||
Visible = "visible",
|
|
||||||
Hidden = "hidden",
|
|
||||||
}
|
|
||||||
|
|
||||||
type ID = ReturnType<typeof useId>;
|
|
||||||
|
|
||||||
export interface NestingContextValues {
|
|
||||||
children: { id: ID; state: TreeStates }[];
|
|
||||||
register: (id: ID) => () => void;
|
|
||||||
unregister: (id: ID, strategy?: RenderStrategy) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TransitionContextValues {
|
|
||||||
show: boolean;
|
|
||||||
appear: boolean;
|
|
||||||
// This is not part of base Headless UI, but we need it because TransitionRoot does not render.
|
|
||||||
// In base Headless UI, for a component with unmount=false, the initial state for the Child is
|
|
||||||
// still "visible". It still works, because the parent still is hidden and has display: none
|
|
||||||
// In our version the parent renders nothing, so we need to send down the correct initial state
|
|
||||||
// ourselves.
|
|
||||||
initialShow: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const TRANSITION_CONTEXT_NAME = "headlessui-transition-context";
|
|
||||||
export const NESTING_CONTEXT_NAME = "headlessui-nesting-context";
|
|
||||||
export function hasTransitionContext() {
|
|
||||||
return getContext(TRANSITION_CONTEXT_NAME) !== undefined;
|
|
||||||
}
|
|
||||||
export function useTransitionContext(): Readable<TransitionContextValues> {
|
|
||||||
let context = getContext(TRANSITION_CONTEXT_NAME) as
|
|
||||||
| Writable<TransitionContextValues>
|
|
||||||
| undefined;
|
|
||||||
if (context === undefined) {
|
|
||||||
throw new Error(
|
|
||||||
"A <TransitionChild /> is used but it is missing a parent <TransitionRoot />."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useParentNesting(): Readable<NestingContextValues> {
|
|
||||||
let context = getContext(NESTING_CONTEXT_NAME) as
|
|
||||||
| Writable<NestingContextValues>
|
|
||||||
| undefined;
|
|
||||||
if (context === undefined) {
|
|
||||||
throw new Error(
|
|
||||||
"A <TransitionChild /> is used but it is missing a parent <TransitionRoot />."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function hasChildren(
|
|
||||||
bag:
|
|
||||||
| NestingContextValues["children"]
|
|
||||||
| { children: NestingContextValues["children"] }
|
|
||||||
): boolean {
|
|
||||||
if ("children" in bag) return hasChildren(bag.children);
|
|
||||||
return bag.filter(({ state }) => state === TreeStates.Visible).length > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useNesting(done?: () => void) {
|
|
||||||
let transitionableChildren: NestingContextValues["children"] = [];
|
|
||||||
|
|
||||||
function unregister(childId: ID, strategy = RenderStrategy.Hidden) {
|
|
||||||
let idx = transitionableChildren.findIndex(({ id }) => id === childId);
|
|
||||||
if (idx === -1) return;
|
|
||||||
|
|
||||||
let hadChildren = hasChildren(transitionableChildren);
|
|
||||||
|
|
||||||
match(strategy, {
|
|
||||||
[RenderStrategy.Unmount]() {
|
|
||||||
transitionableChildren.splice(idx, 1);
|
|
||||||
},
|
|
||||||
[RenderStrategy.Hidden]() {
|
|
||||||
transitionableChildren[idx].state = TreeStates.Hidden;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (hadChildren && !hasChildren(transitionableChildren)) {
|
|
||||||
done?.();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function register(childId: ID) {
|
|
||||||
let child = transitionableChildren.find(({ id }) => id === childId);
|
|
||||||
if (!child) {
|
|
||||||
transitionableChildren.push({
|
|
||||||
id: childId,
|
|
||||||
state: TreeStates.Visible,
|
|
||||||
});
|
|
||||||
} else if (child.state !== TreeStates.Visible) {
|
|
||||||
child.state = TreeStates.Visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => unregister(childId, RenderStrategy.Unmount);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
children: transitionableChildren,
|
|
||||||
register,
|
|
||||||
unregister,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { getContext, onMount, setContext } from "svelte";
|
import { onMount, setContext } from "svelte";
|
||||||
|
import { writable, Writable } from "svelte/store";
|
||||||
import { Readable, writable, Writable } from "svelte/store";
|
|
||||||
import { match } from "$lib/utils/match";
|
import { match } from "$lib/utils/match";
|
||||||
import { State, useOpenClosed } from "$lib/internal/open-closed";
|
import { State, useOpenClosed } from "$lib/internal/open-closed";
|
||||||
import { RenderStrategy } from "$lib/utils/Render.svelte";
|
import TransitionChild from "$lib/components/transitions/TransitionChild.svelte";
|
||||||
import TransitionChild from "./TransitionChild.svelte";
|
|
||||||
import type { useId } from "$lib/hooks/use-id";
|
|
||||||
import { forwardEventsBuilder } from "$lib/internal/forwardEventsBuilder";
|
import { forwardEventsBuilder } from "$lib/internal/forwardEventsBuilder";
|
||||||
import { get_current_component } from "svelte/internal";
|
import { get_current_component } from "svelte/internal";
|
||||||
import type { SupportedAs } from "$lib/internal/elements";
|
import type { SupportedAs } from "$lib/internal/elements";
|
||||||
import type { HTMLActionArray } from "$lib/hooks/use-actions";
|
import type { HTMLActionArray } from "$lib/hooks/use-actions";
|
||||||
|
import {
|
||||||
|
hasChildren,
|
||||||
|
NestingContextValues,
|
||||||
|
NESTING_CONTEXT_NAME,
|
||||||
|
TransitionContextValues,
|
||||||
|
TRANSITION_CONTEXT_NAME,
|
||||||
|
TreeStates,
|
||||||
|
useNesting,
|
||||||
|
} from "./common.svelte";
|
||||||
const forwardEvents = forwardEventsBuilder(get_current_component(), [
|
const forwardEvents = forwardEventsBuilder(get_current_component(), [
|
||||||
"beforeEnter",
|
"beforeEnter",
|
||||||
"beforeLeave",
|
"beforeLeave",
|
||||||
|
|||||||
115
src/lib/components/transitions/common.svelte
Normal file
115
src/lib/components/transitions/common.svelte
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
<script lang="ts" context="module">
|
||||||
|
import type { useId } from "$lib/hooks/use-id";
|
||||||
|
import { match } from "$lib/utils/match";
|
||||||
|
import { RenderStrategy } from "$lib/utils/Render.svelte";
|
||||||
|
import { getContext } from "svelte";
|
||||||
|
import type { Readable, Writable } from "svelte/store";
|
||||||
|
|
||||||
|
export enum TreeStates {
|
||||||
|
Visible = "visible",
|
||||||
|
Hidden = "hidden",
|
||||||
|
}
|
||||||
|
|
||||||
|
type ID = ReturnType<typeof useId>;
|
||||||
|
|
||||||
|
export interface NestingContextValues {
|
||||||
|
children: { id: ID; state: TreeStates }[];
|
||||||
|
register: (id: ID) => () => void;
|
||||||
|
unregister: (id: ID, strategy?: RenderStrategy) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TransitionContextValues {
|
||||||
|
show: boolean;
|
||||||
|
appear: boolean;
|
||||||
|
// This is not part of base Headless UI, but we need it because TransitionRoot does not render.
|
||||||
|
// In base Headless UI, for a component with unmount=false, the initial state for the Child is
|
||||||
|
// still "visible". It still works, because the parent still is hidden and has display: none
|
||||||
|
// In our version the parent renders nothing, so we need to send down the correct initial state
|
||||||
|
// ourselves.
|
||||||
|
initialShow: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TRANSITION_CONTEXT_NAME = "headlessui-transition-context";
|
||||||
|
export const NESTING_CONTEXT_NAME = "headlessui-nesting-context";
|
||||||
|
export function hasTransitionContext() {
|
||||||
|
return getContext(TRANSITION_CONTEXT_NAME) !== undefined;
|
||||||
|
}
|
||||||
|
export function useTransitionContext(): Readable<TransitionContextValues> {
|
||||||
|
let context = getContext(TRANSITION_CONTEXT_NAME) as
|
||||||
|
| Writable<TransitionContextValues>
|
||||||
|
| undefined;
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error(
|
||||||
|
"A <TransitionChild /> is used but it is missing a parent <TransitionRoot />."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useParentNesting(): Readable<NestingContextValues> {
|
||||||
|
let context = getContext(NESTING_CONTEXT_NAME) as
|
||||||
|
| Writable<NestingContextValues>
|
||||||
|
| undefined;
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error(
|
||||||
|
"A <TransitionChild /> is used but it is missing a parent <TransitionRoot />."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasChildren(
|
||||||
|
bag:
|
||||||
|
| NestingContextValues["children"]
|
||||||
|
| { children: NestingContextValues["children"] }
|
||||||
|
): boolean {
|
||||||
|
if ("children" in bag) return hasChildren(bag.children);
|
||||||
|
return bag.filter(({ state }) => state === TreeStates.Visible).length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useNesting(done?: () => void) {
|
||||||
|
let transitionableChildren: NestingContextValues["children"] = [];
|
||||||
|
|
||||||
|
function unregister(childId: ID, strategy = RenderStrategy.Hidden) {
|
||||||
|
let idx = transitionableChildren.findIndex(({ id }) => id === childId);
|
||||||
|
if (idx === -1) return;
|
||||||
|
|
||||||
|
let hadChildren = hasChildren(transitionableChildren);
|
||||||
|
|
||||||
|
match(strategy, {
|
||||||
|
[RenderStrategy.Unmount]() {
|
||||||
|
transitionableChildren.splice(idx, 1);
|
||||||
|
},
|
||||||
|
[RenderStrategy.Hidden]() {
|
||||||
|
transitionableChildren[idx].state = TreeStates.Hidden;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hadChildren && !hasChildren(transitionableChildren)) {
|
||||||
|
done?.();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function register(childId: ID) {
|
||||||
|
let child = transitionableChildren.find(({ id }) => id === childId);
|
||||||
|
if (!child) {
|
||||||
|
transitionableChildren.push({
|
||||||
|
id: childId,
|
||||||
|
state: TreeStates.Visible,
|
||||||
|
});
|
||||||
|
} else if (child.state !== TreeStates.Visible) {
|
||||||
|
child.state = TreeStates.Visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => unregister(childId, RenderStrategy.Unmount);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
children: transitionableChildren,
|
||||||
|
register,
|
||||||
|
unregister,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
</script>
|
||||||
Reference in New Issue
Block a user