Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
[Typescript] Modified Transition React Component to Support Nested Transitions w/ Tailwind
// Modal Source: https://tailwindui.com/components/application-ui/overlays/modals
import React, { ReactNode } from "react";
import { Transition } from "components/transition";
interface Props {
/** The modal open/close state */
open: boolean;
}
export function ModalExample({ open }: Props) {
return (
<Transition show={open} className="fixed bottom-0 inset-x-0 px-4 pb-4 sm:inset-0 sm:flex sm:items-center sm:justify-center">
<Transition
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
className="fixed inset-0 transition-opacity"
>
<div className="absolute inset-0 bg-gray-800 opacity-75"></div>
</Transition>
<Transition
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
className="bg-white rounded-lg overflow-hidden shadow-xl transform transition-all sm:max-w-lg sm:w-full"
>
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div className="sm:flex sm:items-start">
<div className="mx-auto flex-shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
<svg className="h-6 w-6 text-red-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
</div>
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 className="text-lg leading-6 font-medium text-gray-900" id="modal-headline">
Deactivate account
</h3>
<div className="mt-2">
<p className="text-sm leading-5 text-gray-500">
Are you sure you want to deactivate your account? All of your data will be permanently removed. This action cannot be undone.
</p>
</div>
</div>
</div>
</div>
<div className="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
<span className="flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto">
<button type="button" className="inline-flex justify-center w-full rounded-md border border-transparent px-4 py-2 bg-red-600 text-base leading-6 font-medium text-white shadow-sm hover:bg-red-500 focus:outline-none focus:border-red-700 focus:shadow-outline-red transition ease-in-out duration-150 sm:text-sm sm:leading-5">
Deactivate
</button>
</span>
<span className="mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto">
<button type="button" className="inline-flex justify-center w-full rounded-md border border-gray-300 px-4 py-2 bg-white text-base leading-6 font-medium text-gray-700 shadow-sm hover:text-gray-500 focus:outline-none focus:border-blue-300 focus:shadow-outline-blue transition ease-in-out duration-150 sm:text-sm sm:leading-5">
Cancel
</button>
</span>
</div>
</Transition>
</Transition>
);
}
// JSX Version by Adam Wathan: https://gist.github.com/adamwathan/e0a791aa0419098a7ece70028b2e641e
import React, { ReactNode, useRef, useEffect, useContext } from "react";
import { CSSTransition as ReactCSSTransition } from "react-transition-group";
interface TransitionProps {
show?: boolean;
enter?: string;
enterFrom?: string;
enterTo?: string;
leave?: string;
leaveFrom?: string;
leaveTo?: string;
appear?: string | boolean;
className?: string;
children: ReactNode;
}
interface ParentContextProps {
parent: {
show?: boolean;
appear?: string | boolean;
isInitialRender?: boolean;
};
}
const TransitionContext = React.createContext<ParentContextProps>({
parent: {},
});
function useIsInitialRender() {
const isInitialRender = useRef(true);
useEffect(() => {
isInitialRender.current = false;
}, []);
return isInitialRender.current;
}
function CSSTransition({
show,
enter = "",
enterFrom = "",
enterTo = "",
leave = "",
leaveFrom = "",
leaveTo = "",
appear,
className,
children,
}: TransitionProps) {
const nodeRef = React.useRef<HTMLDivElement>(null);
const enterClasses = enter.split(" ").filter((s) => s.length);
const enterFromClasses = enterFrom.split(" ").filter((s) => s.length);
const enterToClasses = enterTo.split(" ").filter((s) => s.length);
const leaveClasses = leave.split(" ").filter((s) => s.length);
const leaveFromClasses = leaveFrom.split(" ").filter((s) => s.length);
const leaveToClasses = leaveTo.split(" ").filter((s) => s.length);
function addClasses(classes: string[]) {
if (nodeRef.current) nodeRef.current.classList.add(...classes);
}
function removeClasses(classes: string[]) {
if (nodeRef.current) nodeRef.current.classList.remove(...classes);
}
return (
<ReactCSSTransition
appear={appear}
unmountOnExit
in={show}
nodeRef={nodeRef}
addEndListener={(done) => {
nodeRef.current?.addEventListener("transitionend", done, false);
}}
onEnter={() => {
addClasses([...enterClasses, ...enterFromClasses]);
}}
onEntering={() => {
removeClasses(enterFromClasses);
addClasses(enterToClasses);
}}
onEntered={() => {
removeClasses([...enterToClasses, ...enterClasses]);
}}
onExit={() => {
addClasses([...leaveClasses, ...leaveFromClasses]);
}}
onExiting={() => {
removeClasses(leaveFromClasses);
addClasses(leaveToClasses);
}}
onExited={() => {
removeClasses([...leaveToClasses, ...leaveClasses]);
}}
>
<div ref={nodeRef} className={className}>
{children}
</div>
</ReactCSSTransition>
);
}
export function Transition({ show, appear, ...rest }: TransitionProps) {
const { parent } = useContext(TransitionContext);
const isInitialRender = useIsInitialRender();
const isChild = show === undefined;
if (isChild) {
return <CSSTransition appear={parent.appear || !parent.isInitialRender} show={parent.show} {...rest} />;
}
return (
<TransitionContext.Provider
value={{
parent: {
show,
isInitialRender,
appear,
},
}}
>
<CSSTransition appear={appear} show={show} {...rest} />
</TransitionContext.Provider>
);
}
@stevecastaneda

This comment has been minimized.

Copy link
Owner Author

@stevecastaneda stevecastaneda commented Apr 24, 2020

For nested transitions, if you don't pass show it's based on the parent show state. The parent automatically waits for all children to finish transitioning before removing, so it needs no props except show.

@stevecastaneda

This comment has been minimized.

Copy link
Owner Author

@stevecastaneda stevecastaneda commented May 23, 2020

Added some missing declarations that popped up in the latest version of Typescript.

@stevecastaneda

This comment has been minimized.

Copy link
Owner Author

@stevecastaneda stevecastaneda commented Jun 9, 2020

Updated to use react-transition-group 4.4.1. Be sure to update your types as well. This uses a reference instead of the deprecated findDOMNode that throws a warning in TS Strict mode.

@stevecastaneda

This comment has been minimized.

Copy link
Owner Author

@stevecastaneda stevecastaneda commented Jun 9, 2020

Refactored with help of @RobinMalfait. Thanks!

@vincaslt

This comment has been minimized.

Copy link

@vincaslt vincaslt commented Jul 15, 2020

that wrapping div potentially breaks children styles.

@stevecastaneda

This comment has been minimized.

Copy link
Owner Author

@stevecastaneda stevecastaneda commented Jul 15, 2020

that wrapping div potentially breaks children styles.

The TransitionGroup component will take a component={null} prop but I haven't figured out how to get that on the Transition or CSSTransition components. Any ideas there?

I agree, ideally, you wouldn't render any container at all once the transition was complete.

@stevecastaneda

This comment has been minimized.

Copy link
Owner Author

@stevecastaneda stevecastaneda commented Jul 16, 2020

@vincaslt Try out the new version I just posted.

@vincaslt

This comment has been minimized.

Copy link

@vincaslt vincaslt commented Jul 16, 2020

A bit more verbose, but seems to work alright, thanks

@stevecastaneda

This comment has been minimized.

Copy link
Owner Author

@stevecastaneda stevecastaneda commented Jul 16, 2020

@vincaslt Yup, pros and cons but I don't see any other way since they each require their own reference. Open to opinions on how we can improve it.

@vincaslt

This comment has been minimized.

Copy link

@vincaslt vincaslt commented Jul 25, 2020

@stevecastaneda

This comment has been minimized.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment