Repository: nuxt/nuxt
File: packages/nuxt/src/app/components/nuxt-link.ts (TypeScript)

Code before:
          ref: el,
          href: href.value || null, // converts `""` to `null` to prevent the attribute from being added as empty (`href=""`)
          rel,
          target,
          onClick: (event) => {
            if (isExternal.value || hasTarget.value) {
              return
            }

            event.preventDefault()

            return props.replace
              ? router.replace(href.value)
              : router.push(href.value)
          },
        }, slots.default?.())
      }
    },
  }) as unknown as (new<CustomProp extends boolean = false>(props: NuxtLinkProps<CustomProp> & VNodeProps & AllowedComponentProps & Omit<AnchorHTMLAttributes, keyof NuxtLinkProps<CustomProp>>) => InstanceType<DefineSetupFnComponent<
    NuxtLinkProps<CustomProp> & VNodeProps & AllowedComponentProps & Omit<AnchorHTMLAttributes, keyof NuxtLinkProps<CustomProp>>,
    [],
    SlotsType<NuxtLinkSlots<CustomProp>>
  >>) & Record<string, any>
}

export default defineNuxtLink(nuxtLinkDefaults)

// -- NuxtLink utils --
function applyTrailingSlashBehavior (to: string, trailingSlash: NuxtLinkOptions['trailingSlash']): string {
  const normalizeFn = trailingSlash === 'append' ? withTrailingSlash : withoutTrailingSlash
  // Until https://github.com/unjs/ufo/issues/189 is resolved
  const hasProtocolDifferentFromHttp = hasProtocol(to) && !to.startsWith('http')
  if (hasProtocolDifferentFromHttp) {
    return to
  }
  return normalizeFn(to, true)
}

// --- Prefetching utils ---
type CallbackFn = () => void
type ObserveFn = (element: Element, callback: CallbackFn) => () => void

function useObserver (): { observe: ObserveFn } | undefined {
  if (import.meta.server) { return }

  const nuxtApp = useNuxtApp()
  if (nuxtApp._observer) {
    return nuxtApp._observer
  }

  let observer: IntersectionObserver | null = null

Review comment (suggestion):
@danielroe I was actually trying to solve a related issue last week. I dont think this fully solves the issue. I think we need at least something like this:


```suggestion
                el?.focus({preventScroll: true})
                el?.scrollIntoView()
```

This focuses the element (if it is focusable) and scrolls to it, even if it is not focusable. And `scrollIntoView` respects  scroll margins and scroll paddings (like the default browser behavior).

While investigating this is stumbled upon this discussion on vue-router. https://github.com/vuejs/router/issues/1189 + this project of anthony https://github.com/antfu/vue-router-better-scroller (trying to solve related issues). I think we should fix this in the long term in Vue Router.

Code after:
          ref: el,
          href: href.value || null, // converts `""` to `null` to prevent the attribute from being added as empty (`href=""`)
          rel,
          target,
          onClick: async (event) => {
            if (isExternal.value || hasTarget.value) {
              return
            }

            event.preventDefault()

            try {
              return await (props.replace ? router.replace(href.value) : router.push(href.value))
            } finally {
              // Focus the target element for hash links to restore accessibility behavior
              // that was prevented by event.preventDefault()
              if (import.meta.client && isHashLinkWithoutHashMode(to.value)) {
                const rawHash = (to.value as string).slice(1)
                let hash = rawHash
                try {
                  hash = decodeURIComponent(rawHash)
                } catch {
                  // ignore errors
                }
                const el = document.getElementById(hash)
                el?.focus()
              }
            }
          },
        }, slots.default?.())
      }
    },
  }) as unknown as (new<CustomProp extends boolean = false>(props: NuxtLinkProps<CustomProp> & VNodeProps & AllowedComponentProps & Omit<AnchorHTMLAttributes, keyof NuxtLinkProps<CustomProp>>) => InstanceType<DefineSetupFnComponent<
    NuxtLinkProps<CustomProp> & VNodeProps & AllowedComponentProps & Omit<AnchorHTMLAttributes, keyof NuxtLinkProps<CustomProp>>,
    [],
    SlotsType<NuxtLinkSlots<CustomProp>>
  >>) & Record<string, any>
}

export default defineNuxtLink(nuxtLinkDefaults)

// -- NuxtLink utils --
function applyTrailingSlashBehavior (to: string, trailingSlash: NuxtLinkOptions['trailingSlash']): string {
  const normalizeFn = trailingSlash === 'append' ? withTrailingSlash : withoutTrailingSlash
  // Until https://github.com/unjs/ufo/issues/189 is resolved
  const hasProtocolDifferentFromHttp = hasProtocol(to) && !to.startsWith('http')
  if (hasProtocolDifferentFromHttp) {
    return to
  }
  return normalizeFn(to, true)
}