feat(Link)!: rename from LinkCustom and add exact-query / exact-hash props

This commit is contained in:
Benjamin Canac
2023-07-30 19:46:27 +02:00
parent a9300db91e
commit cefe5a76e0
10 changed files with 72 additions and 29 deletions

View File

@@ -0,0 +1,24 @@
---
title: 'Link'
description: Render a NuxtLink but with superpowers.
links:
- label: GitHub
icon: i-simple-icons-github
to: https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/elements/Link.vue
navigation:
badge: Edge
---
## Usage
The Link component is a wrapper around [`<NuxtLink>`](https://nuxt.com/docs/api/components/nuxt-link) through the [custom](https://router.vuejs.org/api/interfaces/RouterLinkProps.html#Properties-custom) prop that provides a few extra props:
- `inactive-class` prop to set a class when the link is inactive, `active-class` is used when active.
- `exact` prop to style with `active-class` when the link is active and the route is exactly the same as the current route.
- `exact-query` and `exact-hash` props to style with `active-class` when the link is active and the query or hash is exactly the same as the current query or hash.
The incentive behind this is to provide the same API as NuxtLink back in Nuxt 2 / Vue 2. You can read more about it in the Vue Router [migration from Vue 2](https://router.vuejs.org/guide/migration/#removal-of-the-exact-prop-in-router-link) guide.
It also renders an `<a>` tag when a `to` prop is provided, otherwise it renders a `<button>` tag.
It is used underneath by the [Button](/elements/button), [Dropdown](/elements/dropdown) and [VerticalNavigation](/navigation/vertical-navigation) components.

View File

@@ -1,5 +1,5 @@
<template>
<ULinkCustom :type="type" :disabled="disabled || loading" :class="buttonClass">
<ULink :type="type" :disabled="disabled || loading" :class="buttonClass">
<slot name="leading" :disabled="disabled" :loading="loading">
<UIcon v-if="isLeading && leadingIconName" :name="leadingIconName" :class="leadingIconClass" aria-hidden="true" />
</slot>
@@ -13,7 +13,7 @@
<slot name="trailing" :disabled="disabled" :loading="loading">
<UIcon v-if="isTrailing && trailingIconName" :name="trailingIconName" :class="trailingIconClass" aria-hidden="true" />
</slot>
</ULinkCustom>
</ULink>
</template>
<script lang="ts">
@@ -21,7 +21,7 @@ import { computed, defineComponent, useSlots } from 'vue'
import type { PropType } from 'vue'
import { defu } from 'defu'
import UIcon from '../elements/Icon.vue'
import ULinkCustom from '../elements/LinkCustom.vue'
import ULink from '../elements/Link.vue'
import { classNames } from '../../utils'
import { useAppConfig } from '#imports'
// TODO: Remove
@@ -33,7 +33,7 @@ import appConfig from '#build/app.config'
export default defineComponent({
components: {
UIcon,
ULinkCustom
ULink
},
props: {
type: {

View File

@@ -20,7 +20,7 @@
<HMenuItems :class="[ui.base, ui.divide, ui.ring, ui.rounded, ui.shadow, ui.background, ui.height]" static>
<div v-for="(subItems, index) of items" :key="index" :class="ui.padding">
<HMenuItem v-for="(item, subIndex) of subItems" :key="subIndex" v-slot="{ active, disabled: itemDisabled }" :disabled="item.disabled">
<ULinkCustom
<ULink
v-bind="omit(item, ['label', 'slot', 'icon', 'iconClass', 'avatar', 'shortcuts', 'disabled', 'click'])"
:class="[ui.item.base, ui.item.padding, ui.item.size, ui.item.rounded, active ? ui.item.active : ui.item.inactive, itemDisabled && ui.item.disabled]"
@click="item.click"
@@ -35,7 +35,7 @@
<UKbd v-for="shortcut of item.shortcuts" :key="shortcut">{{ shortcut }}</UKbd>
</span>
</slot>
</ULinkCustom>
</ULink>
</HMenuItem>
</div>
</HMenuItems>
@@ -53,7 +53,7 @@ import { omit } from 'lodash-es'
import UIcon from '../elements/Icon.vue'
import UAvatar from '../elements/Avatar.vue'
import UKbd from '../elements/Kbd.vue'
import ULinkCustom from '../elements/LinkCustom.vue'
import ULink from '../elements/Link.vue'
import { usePopper } from '../../composables/usePopper'
import type { DropdownItem } from '../../types/dropdown'
import type { PopperOptions } from '../../types'
@@ -73,7 +73,7 @@ export default defineComponent({
UIcon,
UAvatar,
UKbd,
ULinkCustom
ULink
},
props: {
items: {

View File

@@ -1,10 +1,10 @@
<template>
<button v-if="!to" :type="type" :disabled="disabled" v-bind="$attrs" :class="inactiveClass">
<button v-if="!to" v-bind="$attrs" :class="inactiveClass">
<slot />
</button>
<NuxtLink
v-else
v-slot="{ href, target, rel, navigate, isActive, isExactActive, isExternal }"
v-slot="{ route, href, target, rel, navigate, isActive, isExactActive, isExternal }"
v-bind="$props"
custom
>
@@ -13,7 +13,7 @@
:href="href"
:rel="rel"
:target="target"
:class="resolveLinkClass({ isActive, isExactActive })"
:class="resolveLinkClass(route, { isActive, isExactActive })"
@click="(e) => !isExternal && navigate(e)"
>
<slot v-bind="{ isActive: exact ? isExactActive : isActive }" />
@@ -22,6 +22,7 @@
</template>
<script lang="ts">
import { isEqual } from 'lodash-es'
import { defineComponent } from 'vue'
import { NuxtLink } from '#components'
@@ -29,25 +30,32 @@ export default defineComponent({
inheritAttrs: false,
props: {
...NuxtLink.props,
type: {
type: String,
default: null
},
disabled: {
type: Boolean,
default: null
},
exact: {
type: Boolean,
default: false
},
exactQuery: {
type: Boolean,
default: false
},
exactHash: {
type: Boolean,
default: false
},
inactiveClass: {
type: String,
default: undefined
}
},
setup (props) {
function resolveLinkClass ({ isActive, isExactActive }: { isActive: boolean, isExactActive: boolean }) {
function resolveLinkClass (route, { isActive, isExactActive }: { isActive: boolean, isExactActive: boolean }) {
if (props.exactQuery && !isEqual(route.query, useRoute().query)) {
return props.inactiveClass
}
if (props.exactHash && route.hash !== useRoute().hash) {
return props.inactiveClass
}
if (props.exact && isExactActive) {
return props.activeClass
}

View File

@@ -1,6 +1,6 @@
<template>
<nav :class="ui.wrapper">
<ULinkCustom
<ULink
v-for="(link, index) of links"
v-slot="{ isActive }"
:key="index"
@@ -33,7 +33,7 @@
{{ link.badge }}
</span>
</slot>
</ULinkCustom>
</ULink>
</nav>
</template>
@@ -44,7 +44,7 @@ import { defu } from 'defu'
import { omit } from 'lodash-es'
import UIcon from '../elements/Icon.vue'
import UAvatar from '../elements/Avatar.vue'
import ULinkCustom from '../elements/LinkCustom.vue'
import ULink from '../elements/Link.vue'
import type { VerticalNavigationLink } from '../../types/vertical-navigation'
import { useAppConfig } from '#imports'
// TODO: Remove
@@ -57,7 +57,7 @@ export default defineComponent({
components: {
UIcon,
UAvatar,
ULinkCustom
ULink
},
props: {
links: {

View File

@@ -1,4 +1,6 @@
export interface Button {
import type { Link } from './link'
export interface Button extends Link {
type?: string
block?: boolean
label?: string

View File

@@ -1,7 +1,7 @@
import type { NuxtLinkProps } from '#app'
import type { Link } from './link'
import type { Avatar } from './avatar'
export interface DropdownItem extends NuxtLinkProps {
export interface DropdownItem extends Link {
label: string
slot?: string
icon?: string

View File

@@ -4,6 +4,7 @@ export * from './button'
export * from './clipboard'
export * from './command-palette'
export * from './dropdown'
export * from './link'
export * from './notification'
export * from './popper'
export * from './tabs'

8
src/runtime/types/link.d.ts vendored Normal file
View File

@@ -0,0 +1,8 @@
import type { NuxtLinkProps } from '#app'
export interface Link extends NuxtLinkProps {
exact?: boolean
exactQuery?: boolean
exactMatch?: boolean
inactiveClass?: string
}

View File

@@ -1,7 +1,7 @@
import type { NuxtLinkProps } from '#app'
import type { Link } from './link'
import type { Avatar } from './avatar'
export interface VerticalNavigationLink extends NuxtLinkProps {
export interface VerticalNavigationLink extends Link {
label: string
icon?: string
iconClass?: string