Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
e89a2ab
feat: add navigation-api router
userquin Sep 12, 2025
166f710
chore: fix build error and remove triple slash reference
userquin Sep 12, 2025
57e4f54
chore: fix createHref
userquin Sep 12, 2025
a869c14
chore: fix intercept handler path
userquin Sep 12, 2025
61bece3
chore: move beforeRouteLeave guards from intercept handler to navigate
userquin Sep 12, 2025
8fd37aa
Revert "chore: move beforeRouteLeave guards from intercept handler to…
userquin Sep 12, 2025
68743f3
chore: refactor logic and navigation guards promises logic
userquin Sep 12, 2025
0a73c18
chore: remove spanish comment
userquin Sep 12, 2025
33709ab
chore: fix navigation guards promises args
userquin Sep 12, 2025
58055b3
chore: try handling history nav. in the listener
userquin Sep 12, 2025
97e3796
chore: revert prevent default (not working)
userquin Sep 12, 2025
2e8af8f
chore: remove navigation success and error listeners
userquin Sep 12, 2025
2973232
chore: add isRevertingNavigation guard
userquin Sep 12, 2025
2d21d50
chore: don't call finalizeNavigation on error
userquin Sep 12, 2025
9772a2e
chore: add initial bootstrap
userquin Sep 12, 2025
1618c6c
chore: fix build error
userquin Sep 12, 2025
fd0ff06
chore: add back finalizeNavigation with failure on error
userquin Sep 12, 2025
2578738
chore: change error logic
userquin Sep 13, 2025
88dd20b
chore: update navigation error and initial navigation logic
userquin Sep 13, 2025
7895f77
chore: revert plugin boostrap
userquin Sep 13, 2025
2b6df78
chore: change bootstrap logic
userquin Sep 13, 2025
34478a7
chore: add back and forward info to the navigation info
userquin Sep 13, 2025
be8a320
chore: fire initial navigation to load components
userquin Sep 13, 2025
c5d77ae
chore: update resolveNavigationGuards logic
userquin Sep 13, 2025
50a675a
chore: remove functional functions
userquin Sep 13, 2025
92d9812
chore: revert guard changes
userquin Sep 14, 2025
44d4ae9
chore: change boostrap logic
userquin Sep 14, 2025
ebaf406
chore: .
userquin Sep 14, 2025
e06f13c
chore: abort logic on navigation duplicated
userquin Sep 14, 2025
dd56ef0
chore: change logic
userquin Sep 14, 2025
aa89041
chore: fix to at intercept
userquin Sep 14, 2025
eac6b63
chore: add missing leaving routes guards
userquin Sep 14, 2025
bd15fd3
chore: add back navigation info to guards
userquin Sep 14, 2025
cec1347
chore: provide navigation info to guards wrap call
userquin Sep 14, 2025
83f47c7
chore: add info as last parameter at extractComponentsGuards
userquin Sep 14, 2025
912be85
chore: use runGuardQueue logic from router at nav. api router
userquin Sep 14, 2025
43b2aab
chore: expose navigation api abort signal for advance usage
userquin Sep 15, 2025
1b99c9b
chore: don't show navigation cancellation error at `handleCurrentEntr…
userquin Sep 15, 2025
04352df
chore: check guards parameters call to fix current browser tests
userquin Sep 15, 2025
7c54940
chore: don't provide info to `runWithContext` call
userquin Sep 15, 2025
afdb6e0
feat: add native view transition built-in support
userquin Sep 15, 2025
538ce7b
chore: add routes utils and expose new stuff
userquin Sep 15, 2025
bbd2bbd
chore: fix types
userquin Sep 15, 2025
cecb1ed
chore: .
userquin Sep 15, 2025
19847b2
chore: add return type to enableViewTransition
userquin Sep 15, 2025
d39977c
chore: fix import at new nav. api router
userquin Sep 15, 2025
43e1672
chore: fix imports at client-router
userquin Sep 15, 2025
29fdfb4
chore: refactor logic to detect view transition
userquin Sep 15, 2025
ce33564
chore: change symbol for transition mode
userquin Sep 15, 2025
51fbf74
chore: update TS (5.8.2), api extractor (7.52.13), tsdoc (0.28.13) ad…
userquin Sep 15, 2025
c3c58d3
chore: check for window
userquin Sep 15, 2025
f16ded6
chore: inject transition mode correctly at router view
userquin Sep 15, 2025
8d541a3
chore: add transitionMode to RouterView definition
userquin Sep 15, 2025
f577bbf
chore: check transition mode before enabling view transition
userquin Sep 15, 2025
b53dfc9
chore: use isReady to register native view transition at legacy router
userquin Sep 15, 2025
591be56
chore: change isChangingPage and legacy router registration
userquin Sep 15, 2025
9367a97
chore: update `beforeResolve` guards
userquin Sep 15, 2025
ce59ba7
chore: update `beforeResolve` guard at nav. api router
userquin Sep 15, 2025
8d3ce6a
chore: change viewTransition option
userquin Sep 15, 2025
b0cf9f8
chore: cleanup
userquin Sep 15, 2025
f3ab6cc
chore: add focus and scroll management options
userquin Sep 17, 2025
98ee9f4
chore: remove back and forward buttons check from view transition logic
userquin Sep 17, 2025
0674d04
chore: simplify view transition hooks
userquin Sep 17, 2025
a5a2a88
chore: remove transition error race when using back or forward browse…
userquin Sep 17, 2025
b74d264
chore: cleanup handleNavigate
userquin Sep 17, 2025
e968961
chore: fix Windows dts build
userquin Sep 17, 2025
d4fa249
chore: add `name` to router to allow adding helpers for legacy router
userquin Sep 17, 2025
8a35345
chore: add focus management "polyfill" to legacy router
userquin Sep 17, 2025
ad1e5d3
chore: add microtask before handling focus
userquin Sep 17, 2025
d282525
chore: add enable automatic scroll management to legacy router
userquin Sep 17, 2025
11632e0
chore: rename client router to modern router
userquin Sep 17, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"postinstall": "simple-git-hooks"
},
"devDependencies": {
"@types/dom-navigation": "^1.0.6",
"@vitest/coverage-v8": "^2.1.9",
"@vitest/ui": "^2.1.9",
"brotli": "^1.3.3",
Expand All @@ -44,9 +45,9 @@
"prettier": "^3.5.3",
"semver": "^7.7.1",
"simple-git-hooks": "^2.13.0",
"typedoc": "^0.26.11",
"typedoc-plugin-markdown": "^4.2.10",
"typescript": "~5.6.3",
"typedoc": "^0.28.13",
"typedoc-plugin-markdown": "^4.8.1",
"typescript": "~5.8.2",
"vitest": "^2.1.9"
},
"simple-git-hooks": {
Expand Down
24 changes: 24 additions & 0 deletions packages/router/add-dts-module-augmentation.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import * as fs from 'node:fs/promises'

async function patchVueRouterDts() {
const content = await fs.readFile('./src/globalExtensions.ts', {
encoding: 'utf-8',
})
const moduleAugmentationIdx = content.indexOf('/**')
if (moduleAugmentationIdx === -1) {
throw new Error(
'Cannot find module augmentation in globalExtensions.ts, first /** comment is expected to start module augmentation'
)
}
const targetContent = await fs.readFile('./dist/vue-router.d.ts', {
encoding: 'utf-8',
})
await fs.writeFile(
'./dist/vue-router.d.ts',
`${targetContent}
${content.slice(moduleAugmentationIdx)}`,
{ encoding: 'utf8' }
)
}

patchVueRouterDts()
5 changes: 3 additions & 2 deletions packages/router/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@
"dev": "vitest --ui",
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 1",
"build": "rimraf dist && rollup -c rollup.config.mjs",
"build:dts": "api-extractor run --local --verbose && tail -n +10 src/globalExtensions.ts >> dist/vue-router.d.ts",
"build:dts": "api-extractor run --local --verbose && node ./add-dts-module-augmentation.mjs",
"build:playground": "vue-tsc --noEmit && vite build --config playground/vite.config.ts",
"build:e2e": "vue-tsc --noEmit && vite build --config e2e/vite.config.mjs",
"build:size": "pnpm run build && rollup -c size-checks/rollup.config.mjs",
Expand All @@ -117,12 +117,13 @@
"@vue/devtools-api": "^6.6.4"
},
"devDependencies": {
"@microsoft/api-extractor": "^7.48.0",
"@microsoft/api-extractor": "^7.52.13",
"@rollup/plugin-alias": "^5.1.1",
"@rollup/plugin-commonjs": "^25.0.8",
"@rollup/plugin-node-resolve": "^15.3.1",
"@rollup/plugin-replace": "^5.0.7",
"@rollup/plugin-terser": "^0.4.4",
"@types/dom-navigation": "^1.0.6",
"@types/jsdom": "^21.1.7",
"@types/nightwatch": "^2.3.32",
"@vitejs/plugin-vue": "^5.2.3",
Expand Down
17 changes: 14 additions & 3 deletions packages/router/src/RouterView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
import { assign, isArray, isBrowser } from './utils'
import { warn } from './warning'
import { isSameRouteRecord } from './location'
import { TransitionMode, transitionModeKey } from './transition'

export interface RouterViewProps {
name?: string
Expand Down Expand Up @@ -62,6 +63,7 @@ export const RouterViewImpl = /*#__PURE__*/ defineComponent({
__DEV__ && warnDeprecatedUsage()

const injectedRoute = inject(routerViewLocationKey)!
const transitionMode = inject(transitionModeKey, 'auto')!
const routeToDisplay = computed<RouteLocationNormalizedLoaded>(
() => props.route || injectedRoute.value
)
Expand Down Expand Up @@ -145,7 +147,11 @@ export const RouterViewImpl = /*#__PURE__*/ defineComponent({
matchedRoute && matchedRoute.components![currentName]

if (!ViewComponent) {
return normalizeSlot(slots.default, { Component: ViewComponent, route })
return normalizeSlot(slots.default, {
Component: ViewComponent,
route,
transitionMode,
})
}

// props from route configuration
Expand Down Expand Up @@ -199,8 +205,11 @@ export const RouterViewImpl = /*#__PURE__*/ defineComponent({
return (
// pass the vnode to the slot as a prop.
// h and <component :is="..."> both accept vnodes
normalizeSlot(slots.default, { Component: component, route }) ||
component
normalizeSlot(slots.default, {
Component: component,
route,
transitionMode,
}) || component
)
}
},
Expand Down Expand Up @@ -228,9 +237,11 @@ export const RouterView = RouterViewImpl as unknown as {
default?: ({
Component,
route,
transitionMode,
}: {
Component: VNode
route: RouteLocationNormalizedLoaded
transitionMode: TransitionMode
}) => VNode[]
}
}
Expand Down
109 changes: 109 additions & 0 deletions packages/router/src/focus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import type { RouteLocationNormalized } from './typed-routes'
import type { Router, RouterOptions } from './router'
import { nextTick } from 'vue'

export function enableFocusManagement(router: Router) {
// navigation-api router will handle this for us
if (router.name !== 'legacy') {
return
}

const { handleFocus, clearFocusTimeout } = createFocusManagementHandler()

const unregisterBeforeEach = router.beforeEach(() => {
clearFocusTimeout()
})

const unregister = router.afterEach(async to => {
const focusManagement =
to.meta.focusManagement ?? router.options.focusManagement

// user wants manual focus
if (focusManagement === false) return

let selector = '[autofocus], body'

if (focusManagement === true) {
selector = '[autofocus],h1,main,body'
} else if (
typeof focusManagement === 'string' &&
focusManagement.length > 0
) {
selector = focusManagement
}

// ensure DOM is updated, enqueuing a microtask before handling focus
await nextTick()

handleFocus(selector)
})

return () => {
clearFocusTimeout()
unregisterBeforeEach()
unregister()
}
}

export function prepareFocusReset(
to: RouteLocationNormalized,
routerFocusManagement?: RouterOptions['focusManagement']
) {
let focusReset: 'after-transition' | 'manual' = 'after-transition'
let selector: string | undefined

const focusManagement = to.meta.focusManagement ?? routerFocusManagement
if (focusManagement === false) {
focusReset = 'manual'
}
if (focusManagement === true) {
focusReset = 'manual'
selector = '[autofocus],h1,main,body'
} else if (typeof focusManagement === 'string') {
focusReset = 'manual'
selector = focusManagement || '[autofocus],h1,main,body'
}

return [focusReset, selector] as const
}

export function createFocusManagementHandler() {
let timeoutId: ReturnType<typeof setTimeout> | undefined

return {
handleFocus: (selector: string) => {
clearTimeout(timeoutId)
requestAnimationFrame(() => {
timeoutId = handleFocusManagement(selector)
})
},
clearFocusTimeout: () => {
clearTimeout(timeoutId)
},
}
}

function handleFocusManagement(
selector: string
): ReturnType<typeof setTimeout> {
return setTimeout(() => {
const target = document.querySelector<HTMLElement>(selector)
if (!target) return
target.focus({ preventScroll: true })
if (document.activeElement === target) return
// element has tabindex already, likely not focusable
// because of some other reason, bail out
if (target.hasAttribute('tabindex')) return
const restoreTabindex = () => {
target.removeAttribute('tabindex')
target.removeEventListener('blur', restoreTabindex)
}
// temporarily make the target element focusable
target.setAttribute('tabindex', '-1')
target.addEventListener('blur', restoreTabindex)
// try to focus again
target.focus({ preventScroll: true })
// remove tabindex and event listener if focus still not worked
if (document.activeElement !== target) restoreTabindex()
}, 150) // screen readers may need more time to react
}
40 changes: 40 additions & 0 deletions packages/router/src/history/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,50 @@ export enum NavigationDirection {
unknown = '',
}

export interface NavigationApiEvent {
readonly navigationType: 'reload' | 'push' | 'replace' | 'traverse'
readonly canIntercept: boolean
readonly userInitiated: boolean
readonly hashChange: boolean
readonly hasUAVisualTransition: boolean
readonly destination: {
readonly url: string
readonly key: string | null
readonly id: string | null
readonly index: number
readonly sameDocument: boolean
getState(): unknown
}
readonly signal: AbortSignal
readonly formData: FormData | null
readonly downloadRequest: string | null
readonly info?: unknown

scroll(): void
}

export interface NavigationInformation {
type: NavigationType
direction: NavigationDirection
delta: number
/**
* True if the navigation was triggered by the browser back button.
*
* Note: available only with the new Navigation API Router.
*/
isBackBrowserButton?: boolean
/**
* True if the navigation was triggered by the browser forward button.
*
* Note: available only with the new Navigation API Router.
*/
isForwardBrowserButton?: boolean
/**
* The native Navigation API Event.
*
* Note: available only with the new Navigation API Router.
*/
navigationApiEvent?: NavigationApiEvent
}

export interface NavigationCallback {
Expand Down
7 changes: 7 additions & 0 deletions packages/router/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,13 @@ export type {
} from './typed-routes'

export { createRouter } from './router'
export { createNavigationApiRouter } from './navigation-api'
export type { Router, RouterOptions, RouterScrollBehavior } from './router'
export type { RouterApiOptions } from './navigation-api'
export type { TransitionMode, RouterViewTransition } from './transition'
export type { ModernRouterOptions } from './modern-router-factory'
export { injectTransitionMode, transitionModeKey } from './transition'
export { createModernRouter } from './modern-router-factory'

export { NavigationFailureType, isNavigationFailure } from './errors'
export type {
Expand All @@ -160,6 +166,7 @@ export type {
UseLinkReturn,
} from './RouterLink'
export { RouterView } from './RouterView'
export { isChangingPage } from './utils/routes'
export type { RouterViewProps } from './RouterView'

export type { TypesConfig } from './config'
Expand Down
55 changes: 55 additions & 0 deletions packages/router/src/modern-router-factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import type { Router } from './router'
import type { RouterApiOptions } from './navigation-api'
import type { TransitionMode } from './transition'
import { createNavigationApiRouter } from './navigation-api'
import { isBrowser } from './utils'

export interface ModernRouterOptions {
/**
* Factory function that creates a legacy router instance.
* Typically: () =&gt; createRouter({@ history: createWebHistory(), routes })
*/
legacy: {
factory: (transitionMode: TransitionMode) => Router
}
/**
* Options for the new Navigation API based router.
* If provided and the browser supports it, this will be used.
*/
navigationApi?: {
options: RouterApiOptions
}
/**
* Enable Native View Transitions.
*
* @default undefined
*/
viewTransition?: boolean
}

export function createModernRouter(options: ModernRouterOptions): Router {
let transitionMode: TransitionMode = 'auto'

if (
options?.viewTransition &&
typeof document !== 'undefined' &&
!!document.startViewTransition
) {
transitionMode = 'view-transition'
}

const useNavigationApi =
options.navigationApi &&
isBrowser &&
typeof window !== 'undefined' &&
window.navigation

if (useNavigationApi) {
return createNavigationApiRouter(
options.navigationApi!.options,
transitionMode
)
} else {
return options.legacy.factory(transitionMode)
}
}
Loading
Loading