Skip to content

Commit

Permalink
MA-19439: Unification of overhead variations with support for To and …
Browse files Browse the repository at this point in the history
…parameter navigation
  • Loading branch information
pavel-nikitin-2022 committed Oct 1, 2024
1 parent bedc884 commit 1d1a9ef
Show file tree
Hide file tree
Showing 10 changed files with 125 additions and 57 deletions.
2 changes: 1 addition & 1 deletion examples/vk-mini-apps-router-example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"@types/react-dom": "^18.2.7",
"@vkontakte/icons": "^2.77.0",
"@vkontakte/vk-bridge": "latest",
"@vkontakte/vk-mini-apps-router": "1.4.2",
"@vkontakte/vk-mini-apps-router": "1.5.0",
"@vkontakte/vkui": "^6.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Panel, PanelHeader, Header, Button, ButtonGroup, Group, Tabs, TabsItem,
import bridge from '@vkontakte/vk-bridge';
import { GoFunctionProp, NavProp } from '../types';
import { useEnableSwipeBack, useActiveVkuiLocation, RouterLink } from '@vkontakte/vk-mini-apps-router';
import { ALTERNATIVE_PANEL_TABS, HOME_PANEL_MODALS, PERSIK_PANEL_MODALS } from '../routes';
import { ALTERNATIVE_PANEL_TABS, HOME_PANEL_MODALS, PERSIK_PANEL_MODALS, routes } from '../routes';
import { AppMap } from '../appMap/AppMap';

export const Alternative = ({ nav, go }: NavProp & GoFunctionProp) => {
Expand Down Expand Up @@ -58,7 +58,7 @@ export const Alternative = ({ nav, go }: NavProp & GoFunctionProp) => {
</ButtonGroup>
<Spacing size={12}></Spacing>
<InfoRow header="Ссылки">
<RouterLink to="/persik">Можно использовать ссылки (на Персика)</RouterLink>
<RouterLink params={{emotion: 'sad'}} to={{pathname: routes.default_root.default_view.persik_0.path, hash: '10'}}>Можно использовать ссылки (на Персика)</RouterLink>
</InfoRow>
</Group>
}
Expand Down
4 changes: 2 additions & 2 deletions examples/vk-mini-apps-router-example/src/panels/Persik.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,11 @@ export const Persik = (props: NavProp) => {
{emotion !== 'sad' && <FormItem><Button
stretched size="l"
mode="secondary"
onClick={() => routeNavigator.push(persikEmotionPanel, { emotion: 'sad' }, { keepSearchParams: true })}
onClick={() => routeNavigator.push({pathname: persikEmotionPanel.path, hash: 'persik'}, { emotion: 'sad' }, { keepSearchParams: true })}
>А еды нет...</Button></FormItem>}
<FormItem>
<Button stretched size="l" mode="secondary" onClick={() =>
routeNavigator.push(`/persik${emotion ? '/' + emotion : ''}/persik_modal${emotion ? '/sad' : ''}`, { keepSearchParams: true })}>
routeNavigator.push('/persik/:emotion/persik_modal/:emotion', {emotion: emotion || 'fish'}, { keepSearchParams: true })}>
Персик в модалке
</Button>
</FormItem>
Expand Down
4 changes: 2 additions & 2 deletions examples/vk-mini-apps-router-example/src/routes.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import {
createHashParamRouter,
createModal,
createPanel,
createRoot,
createTab,
createView,
RoutesConfig,
RouteLeaf,
createHashRouter
} from '@vkontakte/vk-mini-apps-router';

export const DEFAULT_ROOT = 'default_root';
Expand Down Expand Up @@ -131,7 +131,7 @@ export const hierarchy: RouteLeaf[] = [
},
];

export const router = createHashParamRouter(routes.getRoutes());
export const router = createHashRouter(routes.getRoutes());

// export const router = createHashRouter([
// {
Expand Down
11 changes: 7 additions & 4 deletions src/components/RouterLink.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Link } from '@vkontakte/vkui';
import { useHref } from '../hooks/useHref';
import { RelativeRoutingType, To } from '@remix-run/router';
import { Params, RelativeRoutingType, To } from '@remix-run/router';
import {
AnchorHTMLAttributes,
CSSProperties,
Expand All @@ -9,12 +9,13 @@ import {
MouseEvent as ReactMouseEvent,
} from 'react';
import { useLinkClickHandler } from '../hooks/useLinkClickHandler';
import { Page, PageWithParams } from '../page-types/common';

export interface LinkProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'href'> {
to: To | Page | PageWithParams<string>;
reloadDocument?: boolean;
replace?: boolean;
relative?: RelativeRoutingType;
to: To;
}

export interface RouterLinkProps extends Omit<LinkProps, 'className' | 'style' | 'children'> {
Expand All @@ -23,6 +24,7 @@ export interface RouterLinkProps extends Omit<LinkProps, 'className' | 'style' |
className?: string;
end?: boolean;
style?: CSSProperties;
params?: Params;
}

const ABSOLUTE_URL_REGEX = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;
Expand All @@ -33,7 +35,7 @@ const isBrowser =
typeof window.document.createElement !== 'undefined';

export const RouterLink = forwardRef<HTMLAnchorElement, RouterLinkProps>(function (
{ to, relative, replace, target, reloadDocument, onClick, ...rest }: RouterLinkProps,
{ to, relative, replace, target, reloadDocument, params, onClick, ...rest }: RouterLinkProps,
ref,
) {
// Rendered into <a href> for absolute URLs
Expand All @@ -59,12 +61,13 @@ export const RouterLink = forwardRef<HTMLAnchorElement, RouterLinkProps>(functio
}
}

const href = useHref(to, { relative });
const href = useHref(to, { relative, params });

const internalOnClick = useLinkClickHandler(to, {
replace,
target,
relative,
params,
});

function handleClick(event: ReactMouseEvent<HTMLAnchorElement>) {
Expand Down
18 changes: 14 additions & 4 deletions src/hooks/useHref.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,28 @@
import { Location, RelativeRoutingType, To } from '@remix-run/router';
import { Location, RelativeRoutingType, To, Params } from '@remix-run/router';
import { RouterContext } from '../contexts';
import { useContext } from 'react';
import { useResolvedPath } from './useResolvedPath';
import { getHrefWithoutHash } from '../utils/getHrefWithoutHash';
import { invariant } from '../utils/utils';
import { getPathFromTo, invariant } from '../utils/utils';
import { Page, PageWithParams } from '../page-types/common';

export function useHref(to: To, { relative }: { relative?: RelativeRoutingType } = {}): string {
export function useHref(
to: To | Page | PageWithParams<string>,
{ relative, params }: { relative?: RelativeRoutingType; params?: Params } = {},
): string {
const routeContext = useContext(RouterContext);
invariant(
routeContext,
'You can not use useHref hook outside of RouteContext. Make sure calling it inside RouterProvider.',
);

const { hash, pathname, search } = useResolvedPath(to, { relative });
const path = getPathFromTo({
to,
params,
defaultPathname: routeContext.router.state.location.pathname,
});

const { hash, pathname, search } = useResolvedPath(path, { relative });

const hrefWithoutHash = getHrefWithoutHash();
const href = routeContext.router.createHref({ pathname, search, hash } as Location);
Expand Down
13 changes: 10 additions & 3 deletions src/hooks/useLinkClickHandler.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { createPath, RelativeRoutingType, To } from '@remix-run/router';
import { createPath, Params, RelativeRoutingType, To } from '@remix-run/router';
import { HTMLAttributeAnchorTarget, MouseEvent as ReactMouseEvent, useCallback } from 'react';
import { useLocation, useRouteNavigator } from './hooks';
import { useResolvedPath } from './useResolvedPath';
import { Page, PageWithParams } from '../page-types/common';
import { getPathFromTo } from '../utils';

type LimitedMouseEvent = Pick<MouseEvent, 'button' | 'metaKey' | 'altKey' | 'ctrlKey' | 'shiftKey'>;

Expand All @@ -18,22 +20,27 @@ export function shouldProcessLinkClick(event: LimitedMouseEvent, target?: string
}

export function useLinkClickHandler<E extends Element = HTMLAnchorElement>(
to: To,
to: To | Page | PageWithParams<string>,
{
target,
replace: replaceProp,
preventScrollReset,
relative,
params,
}: {
target?: HTMLAttributeAnchorTarget;
replace?: boolean;
preventScrollReset?: boolean;
relative?: RelativeRoutingType;
params?: Params;
} = {},
): (event: ReactMouseEvent<E>) => void {
const navigator = useRouteNavigator();
const location = useLocation();
const path = useResolvedPath(to, { relative });

const path = useResolvedPath(getPathFromTo({ to, params, defaultPathname: location.pathname }), {
relative,
});

return useCallback(
(event: ReactMouseEvent<E>) => {
Expand Down
68 changes: 48 additions & 20 deletions src/services/DefaultRouteNavigator.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import { BlockerFunction, Params, Router, RouterNavigateOptions } from '@remix-run/router';
import { createKey, fillParamsIntoPath, isModalShown, isPopoutShown } from '../utils/utils';
import { BlockerFunction, Params, Router, RouterNavigateOptions, To } from '@remix-run/router';
import {
createKey,
getParamKeys,
getPathFromTo,
isModalShown,
isPopoutShown,
} from '../utils/utils';
import {
NAVIGATION_BLOCKER_KEY,
STATE_KEY_BLOCK_FORWARD_NAVIGATION,
STATE_KEY_SHOW_MODAL,
STATE_KEY_SHOW_POPOUT,
} from '../const';
import { hasNavigationOptionsKeys, NavigationOptions, RouteNavigator } from './RouteNavigator.type';
import { NavigationOptions, RouteNavigator } from './RouteNavigator.type';
import { buildPanelPathFromModalMatch } from '../utils/buildPanelPathFromModalMatch';
import { InternalRouteConfig, ModalWithRoot } from '../type';
import { Page, PageWithParams } from '../page-types/common';
Expand All @@ -30,28 +36,26 @@ export class DefaultRouteNavigator implements RouteNavigator {
}

public async push(
to: string | Page | PageWithParams<string>,
to: To | Page | PageWithParams<string>,
paramsOrOptions: Params | NavigationOptions = {},
options: NavigationOptions = {},
): Promise<void> {
const paramsAreOptions = hasNavigationOptionsKeys(paramsOrOptions);
const preparedOptions: NavigationOptions = paramsAreOptions ? paramsOrOptions : options;
const { preparedOptions, preparedParams } = this.parseParams(to, paramsOrOptions, options);
const fullOptions = {
...preparedOptions,
replace: Boolean(this.router.state.location.state?.[STATE_KEY_BLOCK_FORWARD_NAVIGATION]),
};
const preparedParams: Params = paramsAreOptions ? {} : (paramsOrOptions as Params);

await this.navigate(to, fullOptions, preparedParams);
}

public async replace(
to: string | Page | PageWithParams<string>,
to: To | Page | PageWithParams<string>,
paramsOrOptions: Params | NavigationOptions = {},
options: NavigationOptions = {},
): Promise<void> {
const paramsAreOptions = hasNavigationOptionsKeys(paramsOrOptions);
const preparedOptions: NavigationOptions = paramsAreOptions ? paramsOrOptions : options;
const preparedParams: Params = paramsAreOptions ? {} : (paramsOrOptions as Params);
const { preparedOptions, preparedParams } = this.parseParams(to, paramsOrOptions, options);

await this.navigate(to, { ...preparedOptions, replace: true }, preparedParams);
}

Expand Down Expand Up @@ -159,21 +163,45 @@ Make sure this route exists or use hideModal with pushPanel set to false.`);
}

private async navigate(
to: string | Page | PageWithParams<string>,
to: To | Page | PageWithParams<string>,
opts?: RouterNavigateOptions & NavigationOptions,
params: Params = {},
): Promise<void> {
// prettier-ignore
let path = typeof to === 'string'
? to
: to.hasParams
? fillParamsIntoPath(to.path, params)
: to.path;

if (opts?.keepSearchParams) {
let path = getPathFromTo({ to, params, defaultPathname: this.router.state.location.pathname });
const newUrl = new URL(path, window.location.origin);

if (opts?.keepSearchParams && !newUrl.search) {
path += this.router.state.location.search;
}

await this.router.navigate(path, opts);
}

private validateOptions({ state, keepSearchParams }: NavigationOptions = {}) {
const invalidState = state && typeof state !== 'object';
const invalidKeepSearchParams = keepSearchParams && typeof keepSearchParams !== 'boolean';

if (invalidState || invalidKeepSearchParams) {
console.warn('Invalid navigate options type');
return {};
}
return { state, keepSearchParams };
}

private parseParams(
to: To | Page | PageWithParams<string>,
paramsOrOptions: Params | NavigationOptions = {},
options: NavigationOptions = {},
) {
const path = typeof to === 'object' ? ('path' in to ? to.path : to.pathname || '') : to;

if (getParamKeys(path).length) {
return {
preparedParams: paramsOrOptions as Params,
preparedOptions: this.validateOptions(options),
};
}

return { preparedParams: {}, preparedOptions: this.validateOptions(paramsOrOptions) };
}
}
20 changes: 6 additions & 14 deletions src/services/RouteNavigator.type.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,25 @@
import { Page, PageWithParams } from '../page-types/common';
import { BlockerFunction, Params } from '@remix-run/router';
import { BlockerFunction, Params, To } from '@remix-run/router';

export interface NavigationOptions {
keepSearchParams?: boolean;
state?: Object;
}

export function hasNavigationOptionsKeys<T extends {}>(object: T): boolean {
const base: Required<NavigationOptions> = {
keepSearchParams: true,
state: {},
};
return Object.keys(object).some((key) => key in base);
state?: Record<string, unknown>;
}

export interface RouteNavigator {
push<T extends string>(
to: PageWithParams<T>,
to: To | PageWithParams<T>,
params: Params<T>,
options?: NavigationOptions,
): Promise<void>;
push(to: string | Page, options?: NavigationOptions): Promise<void>;
push(to: To | Page, options?: NavigationOptions): Promise<void>;

replace<T extends string>(
to: PageWithParams<T>,
to: To | PageWithParams<T>,
params: Params<T>,
options?: NavigationOptions,
): Promise<void>;
replace(to: string | Page, options?: NavigationOptions): Promise<void>;
replace(to: To | Page, options?: NavigationOptions): Promise<void>;

back(to?: number): Promise<void>;

Expand Down
Loading

0 comments on commit 1d1a9ef

Please sign in to comment.