Skip to content

Instantly share code, notes, and snippets.

@stevecastaneda
Last active July 26, 2020 20:17
Show Gist options
  • Save stevecastaneda/e71a8465c3f290b98982e5d160260de2 to your computer and use it in GitHub Desktop.
Save stevecastaneda/e71a8465c3f290b98982e5d160260de2 to your computer and use it in GitHub Desktop.
[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
Copy link
Author

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
Copy link
Author

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

@stevecastaneda
Copy link
Author

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
Copy link
Author

Refactored with help of @RobinMalfait. Thanks!

@vincaslt
Copy link

that wrapping div potentially breaks children styles.

@stevecastaneda
Copy link
Author

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
Copy link
Author

@vincaslt Try out the new version I just posted.

@vincaslt
Copy link

vincaslt commented Jul 16, 2020

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

@stevecastaneda
Copy link
Author

@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
Copy link

@stevecastaneda
Copy link
Author

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