react-slide-routes
Version:
The easiest way to slide React routes
139 lines (130 loc) • 4.92 kB
JavaScript
import { __rest } from 'tslib';
import { jsx } from '@emotion/react/jsx-runtime';
import { Children, createRef, useRef, useCallback, cloneElement, useMemo, isValidElement, useContext } from 'react';
import { createRoutesFromElements, useLocation, useRoutes, Route, UNSAFE_RouteContext, matchRoutes } from 'react-router';
import { TransitionGroup, CSSTransition } from 'react-transition-group';
import { css } from '@emotion/react';
const getTransformStyles = (transformFn, max) => `
// back
& > .back-enter {
transform: ${transformFn}(-${max});
}
& > .back-enter-active {
transform: ${transformFn}(0);
}
& > .back-exit {
transform: ${transformFn}(0);
}
& > .back-exit-active {
transform: ${transformFn}(${max});
}
// forward
& > .forward-enter {
transform: ${transformFn}(${max});
}
& > .forward-enter-active {
transform: ${transformFn}(0);
}
& > .forward-exit {
transform: ${transformFn}(0);
}
& > .forward-exit-active {
transform: ${transformFn}(-${max});
}
`;
const getTransitionGroupCss = (duration, timing, direction) => css `
display: grid;
& > .item {
grid-area: 1 / 1 / 2 / 2;
&:not(:only-child) {
&.${direction}-enter-active, &.${direction}-exit-active {
transition: transform ${duration}ms ${timing};
}
}
}
&.slide {
overflow: hidden;
${getTransformStyles('translateX', '100%')}
}
&.vertical-slide {
overflow: hidden;
${getTransformStyles('translateY', '100%')}
}
&.rotate {
perspective: 2000px;
& > .item {
backface-visibility: hidden;
}
${getTransformStyles('rotateY', '180deg')}
}
`;
const isRouteElement = (element) => {
return isValidElement(element) && element.type === Route;
};
// from useRoutes:
// https://github.com/remix-run/react-router/blob/f3d3e05ec00c6950720930beaf74fecbaf9dc5b6/packages/react-router/lib/hooks.tsx#L302
const useNextPath = (pathname = '') => {
const { matches: parentMatches } = useContext(UNSAFE_RouteContext);
const routeMatch = parentMatches[parentMatches.length - 1];
const parentPathnameBase = routeMatch ? routeMatch.pathnameBase : '/';
return parentPathnameBase === '/'
? pathname
: pathname.slice(parentPathnameBase.length) || '/';
};
const getMatch = (routes, pathname) => {
const matches = matchRoutes(routes, pathname);
if (matches === null) {
throw new Error(`Route ${pathname} does not match`);
}
const index = routes.findIndex((route) => {
return matches.some((match) => match.route === route);
});
return { index, route: routes[index] };
};
const SlideRoutes = (props) => {
const { animation = 'slide', duration = 200, timing = 'ease', destroy = true, compare, children, } = props;
// routes
const routeElements = Children.map(children, (child) => {
if (!isRouteElement(child)) {
return child;
}
const _a = child.props, { element } = _a, restProps = __rest(_a, ["element"]);
if (!element) {
return child;
}
const nodeRef = createRef();
const newElement = (jsx("div", { className: "item", ref: nodeRef, children: element }));
return Object.assign(Object.assign({}, child), { props: Object.assign(Object.assign({}, restProps), { element: newElement }) });
});
const routes = createRoutesFromElements(routeElements);
if (compare) {
routes.sort(compare);
}
const location = useLocation();
const routeList = useRoutes(routes, location);
// direction
const nextPath = useNextPath(location.pathname);
const prevPath = useRef(null);
const direction = useRef('undirected');
const nextMatch = getMatch(routes, nextPath);
if (prevPath.current && prevPath.current !== nextPath) {
const prevMatch = getMatch(routes, prevPath.current);
const indexDiff = nextMatch.index - prevMatch.index;
if (indexDiff > 0) {
direction.current = 'forward';
}
else if (indexDiff < 0) {
direction.current = 'back';
}
else if (indexDiff === 0) {
direction.current = 'undirected';
}
}
prevPath.current = nextPath;
// props
const childFactory = useCallback((child) => cloneElement(child, { classNames: direction.current }), []);
const cssTransitionProps = useMemo(() => (destroy ? { timeout: duration } : { addEndListener() { } }), [destroy, duration]);
const nextEl = nextMatch.route.element;
return (jsx(TransitionGroup, { className: `slide-routes ${animation}`, childFactory: childFactory, css: getTransitionGroupCss(duration, timing, direction.current), children: jsx(CSSTransition, Object.assign({ nodeRef: nextEl.props.ref || nextEl.ref }, cssTransitionProps, { children: routeList }), nextMatch.route.path || nextMatch.index) }));
};
export { SlideRoutes as default };