Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Router: Support RegExp in Route component #23292

Merged
merged 8 commits into from
Jul 4, 2023
94 changes: 69 additions & 25 deletions code/lib/router/src/router.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { global } from '@storybook/global';
import React, { useCallback } from 'react';
import type { ReactNode } from 'react';
import type { ReactNode, ReactElement } from 'react';

import * as R from 'react-router-dom';
import { ToggleVisibility } from './visibility';
Expand All @@ -16,18 +16,37 @@ interface MatchingData {
interface LocationProps {
children: (renderData: RenderData) => any;
}
interface MatchProps {

interface MatchPropsStartsWith {
path: string;
startsWith: boolean;
startsWith: true;
children: (matchingData: MatchingData) => ReactNode;
}
interface MatchPropsDefault {
path: string | RegExp;
startsWith: false;
children: (matchingData: MatchingData) => ReactNode;
}
interface RouteProps {

interface RoutePropsStartsWith {
path: string;
startsWith?: boolean;
startsWith: true;
hideOnly?: boolean;
children: ReactNode;
}
interface RoutePropsDefault {
path: string | RegExp;
startsWith?: false;
hideOnly?: boolean;
children: ReactNode;
}

const isRoutePropsDefault = (
props: RoutePropsDefault | RoutePropsStartsWith
): props is RoutePropsDefault => {
return !props.startsWith;
};

const getBase = () => `${document.location.pathname}?`;

// const queryNavigate: NavigateFn = (to: string | number, options?: NavigateOptions<{}>) =>
Expand Down Expand Up @@ -86,30 +105,55 @@ Location.displayName = 'QueryLocation';
// A render-prop component for rendering when a certain path is hit.
// It's immensely similar to `Location` but it receives an addition data property: `match`.
// match has a truthy value when the path is hit.
export const Match = ({ children, path: targetPath, startsWith = false }: MatchProps) => (
<Location>
{({ path: urlPath, ...rest }) =>
children({
match: getMatch(urlPath, targetPath, startsWith),
...rest,
})
}
</Location>
);
function Match(props: MatchPropsStartsWith): ReactElement;
function Match(props: MatchPropsDefault): ReactElement;
function Match({
children,
path: targetPath,
startsWith = false,
}: MatchPropsStartsWith | MatchPropsDefault) {
return (
<Location>
{({ path: urlPath, ...rest }) =>
children({
match: getMatch(urlPath, targetPath, startsWith),
...rest,
})
}
</Location>
);
}
Match.displayName = 'QueryMatch';

// A component to conditionally render children based on matching a target path
export const Route = ({ path, children, startsWith = false, hideOnly = false }: RouteProps) => (
<Match path={path} startsWith={startsWith}>
{({ match }) => {
if (hideOnly) {
return <ToggleVisibility hidden={!match}>{children}</ToggleVisibility>;
}
return match ? children : null;
}}
</Match>
);
function Route(props: RoutePropsDefault): ReactElement;
function Route(props: RoutePropsStartsWith): ReactElement;
function Route(input: RoutePropsDefault | RoutePropsStartsWith) {
let propsA: RoutePropsStartsWith = input as RoutePropsStartsWith;
let propsB: RoutePropsDefault = input as RoutePropsDefault;

if (isRoutePropsDefault(input)) {
propsB = { ...input };
propsB.startsWith = false;
} else {
propsA = input;
}

const { children, hideOnly = false } = propsA || propsB;
return (
<Match {...(propsA || propsB)}>
ndelangen marked this conversation as resolved.
Show resolved Hide resolved
{({ match }) => {
if (hideOnly) {
return <ToggleVisibility hidden={!match}>{children}</ToggleVisibility>;
}
return match ? children : null;
}}
</Match>
);
}
Route.displayName = 'Route';

export { Route, Match };

export const LocationProvider: typeof R.BrowserRouter = (...args) => R.BrowserRouter(...args);
export const BaseLocationProvider: typeof R.Router = (...args) => R.Router(...args);
5 changes: 4 additions & 1 deletion code/lib/router/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,8 +158,11 @@ export const stringifyQuery = (query: Query) =>
type Match = { path: string };

export const getMatch = memoize(1000)(
(current: string, target: string, startsWith = true): Match | null => {
(current: string, target: string | RegExp, startsWith = true): Match | null => {
if (startsWith) {
if (typeof target !== 'string') {
throw new Error('startsWith only works with string targets');
}
const startsWithTarget = current && current.startsWith(target);
if (startsWithTarget) {
return { path: current };
Expand Down