Refactor out circular dependency in Transition components

This was causing problems with Jest even though it was otherwise fine.
This commit is contained in:
Ryan Gossiaux
2021-12-26 11:49:04 -08:00
parent 3801d35d0f
commit 39dda7eb76
4 changed files with 130 additions and 120 deletions

View File

@@ -12,7 +12,7 @@
useNesting,
useParentNesting,
useTransitionContext,
} from "./TransitionRoot.svelte";
} from "$lib/components/transitions/common.svelte";
import { useId } from "$lib/hooks/use-id";
import { forwardEventsBuilder } from "$lib/internal/forwardEventsBuilder";
import { get_current_component } from "svelte/internal";

View File

@@ -1,13 +1,12 @@
<script lang="ts">
import { hasOpenClosed } from "$lib/internal/open-closed";
import TransitionChild from "./TransitionChild.svelte";
import TransitionRoot, {
hasTransitionContext,
} from "./TransitionRoot.svelte";
import TransitionRoot from "./TransitionRoot.svelte";
import { forwardEventsBuilder } from "$lib/internal/forwardEventsBuilder";
import { get_current_component } from "svelte/internal";
import type { SupportedAs } from "$lib/internal/elements";
import type { HTMLActionArray } from "$lib/hooks/use-actions";
import { hasTransitionContext } from "./common.svelte";
const forwardEvents = forwardEventsBuilder(get_current_component(), [
"beforeEnter",
"beforeLeave",

View File

@@ -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">
import { getContext, onMount, setContext } from "svelte";
import { Readable, writable, Writable } from "svelte/store";
import { onMount, setContext } from "svelte";
import { writable, Writable } from "svelte/store";
import { match } from "$lib/utils/match";
import { State, useOpenClosed } from "$lib/internal/open-closed";
import { RenderStrategy } from "$lib/utils/Render.svelte";
import TransitionChild from "./TransitionChild.svelte";
import type { useId } from "$lib/hooks/use-id";
import TransitionChild from "$lib/components/transitions/TransitionChild.svelte";
import { forwardEventsBuilder } from "$lib/internal/forwardEventsBuilder";
import { get_current_component } from "svelte/internal";
import type { SupportedAs } from "$lib/internal/elements";
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(), [
"beforeEnter",
"beforeLeave",

View 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>