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>
);
}
@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