uui-core/src/services/routing/Router6AdaptedRouter.ts (117 lines of code) (raw):

import { IRouterContext } from '../../types/contexts'; import { Link } from '../../types/objects'; import { queryToSearch } from '../../helpers/queryToSearch'; import { searchToQuery } from '../../helpers/searchToQuery'; import { Action, BlockerFunction, IRouter6, Location, To } from './interfaces/IRouter6'; export type { IRouter6 }; type History4Action = 'PUSH' | 'POP' | 'REPLACE'; function mapRouter6ActionToHistory4Action(src: Action): History4Action { if (src === Action.Pop) { return 'POP'; } else if (src === Action.Push) { return 'PUSH'; } return 'REPLACE'; } /** * Adds exactly 1 beforeunload event. It is no-op is such event is already added. * It's needed to make react-router 6 compatible with "history.block". */ export function getBeforeUnloadSingletone() { const BeforeUnloadEventType = 'beforeunload'; let _unblockFn: () => void; return { ensureBlock: () => { if (_unblockFn) { return; } function promptBeforeUnload(event: BeforeUnloadEvent) { // According to the specification, to show the confirmation dialog an event handler should call preventDefault() on the event. event.preventDefault(); event.returnValue = ''; } window.addEventListener(BeforeUnloadEventType, promptBeforeUnload); _unblockFn = function unblockFn() { window.removeEventListener(BeforeUnloadEventType, promptBeforeUnload); }; }, unblock: () => { if (_unblockFn) { _unblockFn(); _unblockFn = undefined; } }, }; } export function getRouter6BlockFn(router: IRouter6) { let blockerKeyIndex = 0; // uui- prefix looks sufficient so that we don't interfere with any internal blockers of router 6. const nextBlockerKey = () => `uui-${++blockerKeyIndex}`; const beforeUnloadSingletone = getBeforeUnloadSingletone(); return function router6BlockFn(blockerFunction: (location: Location, action: History4Action) => void) { const _blockerFn: BlockerFunction = (params) => { blockerFunction(params.nextLocation, mapRouter6ActionToHistory4Action(params.historyAction)); return true; // true means - block navigation. // we want to be compatible with the history 5 behavior for "block" as much as possible. // I.e.: keep navigation blocked until unblock function is explicitly invoked. // https://github.com/remix-run/history/blob/dev/docs/blocking-transitions.md }; const key = nextBlockerKey(); router.getBlocker(key, _blockerFn); beforeUnloadSingletone.ensureBlock(); return function unblock() { router.deleteBlocker(key); if (router.state.blockers.size === 0) { // keep this singletone as long as there are any blockers beforeUnloadSingletone.unblock(); } }; }; } function locationToLink(loc: Location): Link { return { ...loc, query: searchToQuery(loc.search), }; } const withFallback = <T>(v: T, fallback: string = '') => typeof v !== 'undefined' ? v : fallback; function linkToRouter6Dest(link: Link): { to: To, state?: any } { return { to: { hash: withFallback(link.hash), search: queryToSearch(link.query) || link.search, pathname: withFallback(link.pathname), }, state: link.state, }; } function linkToLocation(link: Link): Location { return { hash: withFallback(link.hash), search: queryToSearch(link.query), pathname: withFallback(link.pathname), key: withFallback(link.key), state: link.state, }; } /** * * NOTE: Next methods/properties of the router are marked as PRIVATE - DO NOT USE in the https://github.com/remix-run/react-router/blob/main/packages/router/router.ts#L57 * - state * - getBlocker * - subscribe * - deleteBlocker * - createHref * So we should be extra careful if decide to use another version of the react-router. * Though, it's OK to use such API, because there are no alternatives yet (see https://github.com/remix-run/react-router/issues/9422) * * IMPORTANT: As of now, it was tested only using react-router 6.14.0 */ export class Router6AdaptedRouter implements IRouterContext { constructor(private router6: IRouter6) {} public getCurrentLink(): Link { return locationToLink(this.router6.state.location); } public redirect(link: Link | string): void { // NOTE: navigate is async in the router 6 if (typeof link === 'string') { this.router6.navigate(link); } else { const { to, state } = linkToRouter6Dest(link); this.router6.navigate(to, { state }); } } public transfer(link: Link): void { // NOTE: it's async in the router 6 const { to, state } = linkToRouter6Dest(link); this.router6.navigate(to, { state, replace: true }); } public isActive(link: Link): boolean { const current = this.getCurrentLink(); return current.pathname === link.pathname; } public createHref(link: Link): string { return this.router6.createHref(linkToLocation(link)); } public listen(listener: (link: Link) => void) { return this.router6.subscribe((rState) => { listener(rState.location); }); } public block: (listener: (link: Link) => void) => () => void = getRouter6BlockFn(this.router6); }