Skip to content

Commit

Permalink
feat: add useTransitionManagerV2
Browse files Browse the repository at this point in the history
  • Loading branch information
hi-ogawa committed Oct 23, 2023
1 parent 653fedc commit e240a48
Show file tree
Hide file tree
Showing 5 changed files with 112 additions and 5 deletions.
16 changes: 16 additions & 0 deletions packages/app/src/components/stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
Transition,
TransitionV2,
useLaggedBoolean,
useTransitionManagerV2,
} from "@hiogawa/tiny-transition/dist/react";
import { ANTD_VARS } from "@hiogawa/unocss-preset-antd";
import { none, objectKeys, objectPickBy, range } from "@hiogawa/utils";
Expand Down Expand Up @@ -488,6 +489,7 @@ export function StoryModal() {

export function StorySlide() {
const [show, setShow] = React.useState(true);
const manager = useTransitionManagerV2(show, { appear: true });

return (
<div className="flex flex-col items-center gap-3 m-2">
Expand Down Expand Up @@ -522,6 +524,20 @@ export function StorySlide() {
>
<span className="border px-2 py-1">hello from bottom/left</span>
</TransitionV2>
{manager.state && (
<div
ref={manager.ref}
className={cls(
"absolute top-2 left-2 inline-block duration-500 transform",
manager.state === "enterFrom" && "translate-y-[-200%]",
manager.state === "enterTo" && "translate-y-0",
manager.state === "leaveFrom" && "translate-y-0",
manager.state === "leaveTo" && "translate-y-[-200%]"
)}
>
<span className="border px-2 py-1">hello from top/left</span>
</div>
)}
</div>
</section>
</div>
Expand Down
2 changes: 1 addition & 1 deletion packages/tiny-transition/src/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ function onTransitionEnd(el: HTMLElement, callback: () => void) {
}

export function computeTransitionTimeout(el: HTMLElement): number {
// this probably also `forceStyle`
// this probably also flush style/paint because of `getComputedStyle`
const style = getComputedStyle(el);
const [duration, delay] = [
style.transitionDuration,
Expand Down
8 changes: 4 additions & 4 deletions packages/tiny-transition/src/lagged/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
// https://github.com/solidjs-community/solid-primitives/pull/437

// animation in each direction requires two intemediate steps
// false --(true)----> enterFrom --(next frame)--> enterTo ---(timeout)-> true
// <-(timeout)-- leaveTo <-(next frame)-- leaveFrom <--(false)----
// false --(true)----> enterFrom --(mount + next frame)--> enterTo ---(timeout)-> true
// <-(timeout)-- leaveTo <-(next frame)----------- leaveFrom <--(false)----
export type LaggedBooleanState =
| boolean
| "enterFrom"
Expand Down Expand Up @@ -74,7 +74,7 @@ export class LaggedBoolean {
}
}

class AsyncOperation {
export class AsyncOperation {
private disposables = new Set<() => void>();

setTimeout(callback: () => void, ms: number) {
Expand All @@ -93,6 +93,6 @@ class AsyncOperation {
}
}

function forceStyle() {
export function forceStyle() {
typeof document.body.offsetHeight || console.log("unreachable");
}
20 changes: 20 additions & 0 deletions packages/tiny-transition/src/lagged/react.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,26 @@ import {
type LaggedBooleanOptions,
type LaggedBooleanState,
} from "./core";
import { TransitionManagerV2 } from "./v2";

export function useTransitionManagerV2(
value: boolean,
options?: { appear?: boolean }
) {
const [manager] = useState(() => new TransitionManagerV2(value, options));

useEffect(() => {
manager.set(value);
}, [value]);

useSyncExternalStore(
manager.subscribe,
() => manager.state,
() => manager.state
);

return manager;
}

export function useLaggedBoolean(
value: boolean,
Expand Down
71 changes: 71 additions & 0 deletions packages/tiny-transition/src/lagged/v2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { computeTransitionTimeout } from "../core";
import { AsyncOperation, type LaggedBooleanState, forceStyle } from "./core";

export class TransitionManagerV2 {
state: LaggedBooleanState;
private el: HTMLElement | null = null;
private listeners = new Set<() => void>();
private asyncOp = new AsyncOperation();

constructor(value: boolean, options?: { appear?: boolean }) {
this.state = options?.appear ? !value : value;
}

set(value: boolean) {
const isTruthy =
this.state === true ||
this.state === "enterFrom" ||
this.state === "enterTo";
if (value !== isTruthy) {
this.startTransition(value);
}
}

ref = (el: HTMLElement | null) => {
this.el = el;
if (el) {
if (this.state === "enterFrom") {
this.startTransition(true);
}
} else {
this.state = false;
}
};

private startTransition(value: boolean) {
this.asyncOp.dispose();

this.update(value ? "enterFrom" : "leaveFrom");

// delay "enterTo" transition until mount
if (!this.el) {
return;
}

const duration = computeTransitionTimeout(this.el);

this.asyncOp.requestAnimationFrame(() => {
forceStyle(); // `appear` breaks without this. not entirely sure why.
this.update(value ? "enterTo" : "leaveTo");

this.asyncOp.setTimeout(() => {
this.update(value);
}, duration);
});
}

private update(state: LaggedBooleanState) {
if (this.state === state) {
return;
}
this.state = state;
for (const listener of this.listeners) {
listener();
}
}

subscribe = (listener: () => void) => {
this.listeners.add(listener);
return () => this.listeners.delete(listener);
};
}

0 comments on commit e240a48

Please sign in to comment.