Skip to content

Commit 1ab5a16

Browse files
committed
refactor(link-base): update size and tests
1 parent f8ab6cb commit 1ab5a16

3 files changed

Lines changed: 131 additions & 45 deletions

File tree

app/components/Link/Base.vue

Lines changed: 56 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,11 @@ const props = withDefaults(
77
/** Disabled links will be displayed as plain text */
88
disabled?: boolean
99
/**
10-
* `type` should never be used, because this will always be a link.
11-
* */
12-
type?: never
13-
variant?: 'button-primary' | 'button-secondary' | 'link'
14-
size?: 'small' | 'medium'
15-
block?: boolean
10+
* Controls whether the link is styled as text or as a button.
11+
*/
12+
type?: 'button' | 'link'
13+
size?: 'xs' | 'sm' | 'md' | 'lg'
14+
inline?: boolean
1615
1716
ariaKeyshortcuts?: string
1817
@@ -37,7 +36,7 @@ const props = withDefaults(
3736
noUnderline?: boolean
3837
} & NuxtLinkProps
3938
>(),
40-
{ variant: 'link', size: 'medium' },
39+
{ type: 'link', size: 'md', inline: true },
4140
)
4241
4342
const isLinkExternal = computed(
@@ -50,47 +49,66 @@ const isLinkAnchor = computed(
5049
() => !!props.to && typeof props.to === 'string' && props.to.startsWith('#'),
5150
)
5251
53-
/** size is only applicable for button like links */
54-
const isLink = computed(() => props.variant === 'link')
55-
const isButton = computed(() => !isLink.value)
56-
const isButtonSmall = computed(() => props.size === 'small' && !isLink.value)
57-
const isButtonMedium = computed(() => props.size === 'medium' && !isLink.value)
52+
const isLink = computed(() => props.type === 'link')
53+
const isButton = computed(() => props.type === 'button')
54+
const sizeClass = computed(() => {
55+
if (isButton.value) {
56+
switch (props.size) {
57+
case 'xs':
58+
return 'text-xs px-2 py-0.5'
59+
case 'sm':
60+
return 'text-sm px-4 py-2'
61+
case 'md':
62+
return 'text-base px-5 py-2.5'
63+
case 'lg':
64+
return 'text-lg px-6 py-3'
65+
}
66+
}
67+
68+
switch (props.size) {
69+
case 'xs':
70+
return 'text-xs'
71+
case 'sm':
72+
return 'text-sm'
73+
case 'md':
74+
return 'text-base'
75+
case 'lg':
76+
return 'text-lg'
77+
}
78+
})
5879
</script>
5980

6081
<template>
6182
<span
6283
v-if="disabled"
63-
:class="{
64-
'flex': block,
65-
'inline-flex': !block,
66-
'opacity-50 gap-x-1 items-center justify-center font-mono border border-transparent rounded-md':
67-
isButton,
68-
'text-sm px-4 py-2': isButtonMedium,
69-
'text-xs px-2 py-0.5': isButtonSmall,
70-
'text-bg bg-fg': variant === 'button-primary',
71-
'bg-transparent text-fg': variant === 'button-secondary',
72-
}"
84+
:class="[
85+
inline ? 'inline-flex' : 'flex',
86+
sizeClass,
87+
{
88+
'opacity-50 gap-x-1 items-center justify-center font-mono border border-transparent rounded-md':
89+
isButton,
90+
'bg-transparent text-fg': isButton,
91+
},
92+
]"
7393
><slot
7494
/></span>
7595
<NuxtLink
7696
v-else
7797
class="group/link gap-x-1 items-center"
78-
:class="{
79-
'flex': block,
80-
'inline-flex': !block,
81-
'underline-offset-[0.2rem] underline decoration-1 decoration-fg/30':
82-
!isLinkAnchor && isLink && !noUnderline,
83-
'justify-start font-mono text-fg hover:(decoration-accent text-accent) focus-visible:(decoration-accent text-accent) transition-colors duration-200':
84-
isLink,
85-
'justify-center font-mono border border-border rounded-md transition-all duration-200':
86-
isButton,
87-
'text-sm px-4 py-2': isButtonMedium,
88-
'text-xs px-2 py-0.5': isButtonSmall,
89-
'bg-transparent text-fg hover:(bg-fg/10 text-accent) focus-visible:(bg-fg/10 text-accent) aria-[current=true]:(bg-fg/10 text-accent border-fg/20 hover:enabled:(bg-fg/20 text-fg/50))':
90-
variant === 'button-secondary',
91-
'text-bg bg-fg hover:(bg-fg/50 text-accent) focus-visible:(bg-fg/50) aria-current:(bg-fg text-bg border-fg hover:enabled:(text-bg/50))':
92-
variant === 'button-primary',
93-
}"
98+
:class="[
99+
inline ? 'inline-flex' : 'flex',
100+
sizeClass,
101+
{
102+
'underline-offset-[0.2rem] underline decoration-1 decoration-fg/30':
103+
!isLinkAnchor && isLink && !noUnderline,
104+
'justify-start font-mono text-fg hover:(decoration-accent text-accent) focus-visible:(decoration-accent text-accent) transition-colors duration-200':
105+
isLink,
106+
'justify-center font-mono border border-border rounded-md transition-all duration-200':
107+
isButton,
108+
'bg-transparent text-fg hover:(bg-fg/10 text-accent) focus-visible:(bg-fg/10 text-accent) aria-[current=true]:(bg-fg/10 text-accent border-fg/20 hover:enabled:(bg-fg/20 text-fg/50))':
109+
isButton,
110+
},
111+
]"
94112
:to="to"
95113
:aria-keyshortcuts="ariaKeyshortcuts"
96114
:target="isLinkExternal ? '_blank' : undefined"

test/nuxt/a11y.spec.ts

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -380,7 +380,8 @@ describe('component accessibility audits', () => {
380380

381381
it("should have no accessibility violations when it's the current link", async () => {
382382
const component = await mountSuspended(LinkBase, {
383-
props: { to: 'http://example.com', current: true },
383+
props: { to: 'http://example.com' },
384+
attrs: { 'aria-current': 'page' },
384385
slots: { default: 'Button link content' },
385386
})
386387
const results = await runAxe(component)
@@ -396,18 +397,18 @@ describe('component accessibility audits', () => {
396397
expect(results.violations).toEqual([])
397398
})
398399

399-
it('should have no accessibility violations as secondary button', async () => {
400+
it('should have no accessibility violations as button link', async () => {
400401
const component = await mountSuspended(LinkBase, {
401-
props: { to: 'http://example.com', disabled: true, variant: 'button-secondary' },
402+
props: { to: 'http://example.com', disabled: true, type: 'button' },
402403
slots: { default: 'Button link content' },
403404
})
404405
const results = await runAxe(component)
405406
expect(results.violations).toEqual([])
406407
})
407408

408-
it('should have no accessibility violations as primary button', async () => {
409+
it('should have no accessibility violations as button link (large)', async () => {
409410
const component = await mountSuspended(LinkBase, {
410-
props: { to: 'http://example.com', disabled: true, variant: 'button-primary' },
411+
props: { to: 'http://example.com', disabled: true, type: 'button', size: 'lg' },
411412
slots: { default: 'Button link content' },
412413
})
413414
const results = await runAxe(component)
@@ -419,8 +420,22 @@ describe('component accessibility audits', () => {
419420
props: {
420421
to: 'http://example.com',
421422
disabled: true,
422-
variant: 'button-secondary',
423-
size: 'small',
423+
type: 'button',
424+
size: 'sm',
425+
},
426+
slots: { default: 'Button link content' },
427+
})
428+
const results = await runAxe(component)
429+
expect(results.violations).toEqual([])
430+
})
431+
432+
it('should have no accessibility violations as extra-small button', async () => {
433+
const component = await mountSuspended(LinkBase, {
434+
props: {
435+
to: 'http://example.com',
436+
disabled: true,
437+
type: 'button',
438+
size: 'xs',
424439
},
425440
slots: { default: 'Button link content' },
426441
})
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { describe, expect, it } from 'vitest'
2+
import { mountSuspended } from '@nuxt/test-utils/runtime'
3+
import LinkBase from '~/components/Link/Base.vue'
4+
5+
describe('LinkBase', () => {
6+
it('renders a default text link with inline layout and medium size', async () => {
7+
const wrapper = await mountSuspended(LinkBase, {
8+
props: { to: '/about' },
9+
slots: { default: 'About' },
10+
})
11+
12+
const link = wrapper.get('a')
13+
expect(link.classes()).toContain('inline-flex')
14+
expect(link.classes()).toContain('text-base')
15+
expect(link.classes()).toContain('underline')
16+
})
17+
18+
it('uses flex layout and small text size when inline is false', async () => {
19+
const wrapper = await mountSuspended(LinkBase, {
20+
props: { to: '/settings', size: 'sm', inline: false },
21+
slots: { default: 'Settings' },
22+
})
23+
24+
const link = wrapper.get('a')
25+
expect(link.classes()).toContain('flex')
26+
expect(link.classes()).toContain('text-sm')
27+
})
28+
29+
it('applies extra-small text size for links', async () => {
30+
const wrapper = await mountSuspended(LinkBase, {
31+
props: { to: '/compare', size: 'xs' },
32+
slots: { default: 'Compare' },
33+
})
34+
35+
const link = wrapper.get('a')
36+
expect(link.classes()).toContain('text-xs')
37+
})
38+
39+
it('styles button links with size classes and no underline', async () => {
40+
const wrapper = await mountSuspended(LinkBase, {
41+
props: { to: '/compare', type: 'button', size: 'lg' },
42+
slots: { default: 'Compare' },
43+
})
44+
45+
const link = wrapper.get('a')
46+
expect(link.classes()).toContain('border')
47+
expect(link.classes()).toContain('rounded-md')
48+
expect(link.classes()).toContain('text-lg')
49+
expect(link.classes()).toContain('px-6')
50+
expect(link.classes()).toContain('py-3')
51+
expect(link.classes()).not.toContain('underline')
52+
})
53+
})

0 commit comments

Comments
 (0)