fix(Link): ensure consistency across Nuxt, Vue and Inertia · nuxt/ui@a9ed10d
@@ -29,6 +29,10 @@ export interface LinkProps extends Partial<Omit<RouterLinkProps, 'custom'>>, /**
2929 * A rel attribute value to apply on the link. Defaults to "noopener noreferrer" for external links.
3030 */
3131 rel?: 'noopener' | 'noreferrer' | 'nofollow' | 'sponsored' | 'ugc' | (string & {}) | null
32+ /**
33+ * If set to true, no rel attribute will be added to the link
34+ */
35+ noRel?: boolean
3236 /**
3337 * The type of the button when not a link.
3438 * @defaultValue 'button'
@@ -66,6 +70,7 @@ import { hasProtocol } from 'ufo'
6670import { useRoute, RouterLink } from 'vue-router'
6771import { useAppConfig } from '#imports'
6872import { tv } from '../../utils/tv'
73+import { mergeClasses } from '../../utils'
6974import { isPartiallyEqual } from '../../utils/link'
7075import ULinkBase from '../../components/LinkBase.vue'
7176@@ -75,25 +80,23 @@ const props = withDefaults(defineProps<LinkProps>(), {
7580 as: 'button',
7681 type: 'button',
7782 ariaCurrentValue: 'page',
78- active: undefined,
79- activeClass: '',
80- inactiveClass: ''
83+ active: undefined
8184})
8285defineSlots<LinkSlots>()
83868487const route = useRoute()
85888689const appConfig = useAppConfig() as Link['AppConfig']
879088-const routerLinkProps = useForwardProps(reactiveOmit(props, 'as', 'type', 'disabled', 'active', 'exact', 'exactQuery', 'exactHash', 'activeClass', 'inactiveClass', 'to', 'href', 'raw', 'custom', 'class'))
91+const routerLinkProps = useForwardProps(reactiveOmit(props, 'as', 'type', 'disabled', 'active', 'exact', 'exactQuery', 'exactHash', 'activeClass', 'inactiveClass', 'to', 'href', 'raw', 'custom', 'class', 'noRel'))
89929093const ui = computed(() => tv({
9194 extend: tv(theme),
9295 ...defu({
9396 variants: {
9497 active: {
95- true: props.activeClass,
96- false: props.inactiveClass
98+ true: mergeClasses(appConfig.ui?.link?.variants?.active?.true, props.activeClass),
99+ false: mergeClasses(appConfig.ui?.link?.variants?.active?.false, props.inactiveClass)
97100 }
98101 }
99102 }, appConfig.ui?.link || {})
@@ -113,6 +116,27 @@ const isExternal = computed(() => {
113116 return typeof to.value === 'string' && hasProtocol(to.value, { acceptRelative: true })
114117})
115118119+const hasTarget = computed(() => !!props.target && props.target !== '_self')
120+121+const rel = computed(() => {
122+ // If noRel is explicitly set, return null
123+ if (props.noRel) {
124+ return null
125+ }
126+127+ // If rel is explicitly set, use it
128+ if (props.rel !== undefined) {
129+ return props.rel || null
130+ }
131+132+ // Default to "noopener noreferrer" for external links or links with target
133+ if (isExternal.value || hasTarget.value) {
134+ return 'noopener noreferrer'
135+ }
136+137+ return null
138+})
139+116140function isLinkActive({ route: linkRoute, isActive, isExactActive }: any) {
117141 if (props.active !== undefined) {
118142 return props.active
@@ -168,6 +192,9 @@ function resolveLinkClass({ route, isActive, isExactActive }: any = {}) {
168192 disabled,
169193 href,
170194 navigate,
195+ rel,
196+ target,
197+ isExternal,
171198 active: isLinkActive({ route: linkRoute, isActive, isExactActive })
172199 }"
173200 />
@@ -181,7 +208,10 @@ function resolveLinkClass({ route, isActive, isExactActive }: any = {}) {
181208 type,
182209 disabled,
183210 href,
184- navigate
211+ navigate,
212+ rel,
213+ target,
214+ isExternal
185215 }"
186216:class="resolveLinkClass({ route: linkRoute, isActive, isExactActive })"
187217>
@@ -199,7 +229,8 @@ function resolveLinkClass({ route, isActive, isExactActive }: any = {}) {
199229 type,
200230 disabled,
201231 href: to,
202- target: isExternal ? '_blank' : undefined,
232+ rel,
233+ target,
203234 active,
204235 isExternal
205236 }"
@@ -213,7 +244,8 @@ function resolveLinkClass({ route, isActive, isExactActive }: any = {}) {
213244 type,
214245 disabled,
215246 href: (to as string),
216- target: isExternal ? '_blank' : undefined,
247+ rel,
248+ target,
217249 isExternal
218250 }"
219251:class="resolveLinkClass()"