Skip to content

Commit 7fbf046

Browse files
refactor: extract tag component (#742)
Co-authored-by: Daniel Roe <daniel@roe.dev>
1 parent e4f19b5 commit 7fbf046

File tree

9 files changed

+107
-47
lines changed

9 files changed

+107
-47
lines changed

app/components/Filter/Chips.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ const emit = defineEmits<{
1414
<template>
1515
<div v-if="chips.length > 0" class="flex flex-wrap items-center gap-2">
1616
<TransitionGroup name="chip">
17-
<span v-for="chip in chips" :key="chip.id" class="tag gap-1">
17+
<TagStatic v-for="chip in chips" :key="chip.id" class="gap-1">
1818
<span class="text-fg-subtle text-xs">{{ chip.label }}:</span>
1919
<span class="max-w-32 truncate">{{
2020
Array.isArray(chip.value) ? chip.value.join(', ') : chip.value
@@ -27,7 +27,7 @@ const emit = defineEmits<{
2727
>
2828
<span class="i-carbon-close w-3 h-3" aria-hidden="true" />
2929
</button>
30-
</span>
30+
</TagStatic>
3131
</TransitionGroup>
3232

3333
<button

app/components/Filter/Panel.vue

Lines changed: 12 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -243,22 +243,17 @@ const hasActiveFilters = computed(() => !!filterSummary.value)
243243
role="radiogroup"
244244
:aria-label="$t('filters.weekly_downloads')"
245245
>
246-
<button
246+
<TagClickable
247247
v-for="range in DOWNLOAD_RANGES"
248248
:key="range.value"
249249
type="button"
250250
role="radio"
251251
:aria-checked="filters.downloadRange === range.value"
252-
class="tag transition-colors duration-200 focus-visible:ring-2 focus-visible:ring-fg focus-visible:ring-offset-1"
253-
:class="
254-
filters.downloadRange === range.value
255-
? 'bg-fg text-bg border-fg hover:text-bg/50'
256-
: ''
257-
"
252+
:status="filters.downloadRange === range.value ? 'active' : 'default'"
258253
@click="emit('update:downloadRange', range.value)"
259254
>
260255
{{ $t(getDownloadRangeLabelKey(range.value)) }}
261-
</button>
256+
</TagClickable>
262257
</div>
263258
</fieldset>
264259

@@ -272,22 +267,17 @@ const hasActiveFilters = computed(() => !!filterSummary.value)
272267
role="radiogroup"
273268
:aria-label="$t('filters.updated_within')"
274269
>
275-
<button
270+
<TagClickable
276271
v-for="option in UPDATED_WITHIN_OPTIONS"
277272
:key="option.value"
278273
type="button"
279274
role="radio"
280275
:aria-checked="filters.updatedWithin === option.value"
281-
class="tag transition-colors duration-200 focus-visible:ring-2 focus-visible:ring-fg focus-visible:ring-offset-1"
282-
:class="
283-
filters.updatedWithin === option.value
284-
? 'bg-fg text-bg border-fg hover:text-bg/70'
285-
: ''
286-
"
276+
:status="filters.updatedWithin === option.value ? 'active' : 'default'"
287277
@click="emit('update:updatedWithin', option.value)"
288278
>
289279
{{ $t(getUpdatedWithinLabelKey(option.value)) }}
290-
</button>
280+
</TagClickable>
291281
</div>
292282
</fieldset>
293283

@@ -300,20 +290,17 @@ const hasActiveFilters = computed(() => !!filterSummary.value)
300290
</span>
301291
</legend>
302292
<div class="flex flex-wrap gap-2" role="radiogroup" :aria-label="$t('filters.security')">
303-
<button
293+
<TagClickable
304294
v-for="security in SECURITY_FILTER_VALUES"
305295
:key="security"
306296
type="button"
307297
role="radio"
308298
disabled
309299
:aria-checked="filters.security === security"
310-
class="tag transition-colors duration-200 opacity-50 cursor-not-allowed focus-visible:ring-2 focus-visible:ring-fg focus-visible:ring-offset-1"
311-
:class="
312-
filters.security === security ? 'bg-fg text-bg border-fg hover:text-bg/70' : ''
313-
"
300+
:status="filters.security === security ? 'active' : 'default'"
314301
>
315302
{{ $t(getSecurityLabelKey(security)) }}
316-
</button>
303+
</TagClickable>
317304
</div>
318305
</fieldset>
319306

@@ -323,19 +310,16 @@ const hasActiveFilters = computed(() => !!filterSummary.value)
323310
{{ $t('filters.keywords') }}
324311
</legend>
325312
<div class="flex flex-wrap gap-1.5" role="group" :aria-label="$t('filters.keywords')">
326-
<button
313+
<TagClickable
327314
v-for="keyword in displayedKeywords"
328315
:key="keyword"
329316
type="button"
330317
:aria-pressed="filters.keywords.includes(keyword)"
331-
class="tag text-xs transition-colors duration-200 focus-visible:ring-2 focus-visible:ring-fg focus-visible:ring-offset-1"
332-
:class="
333-
filters.keywords.includes(keyword) ? 'bg-fg text-bg border-fg hover:text-bg/70' : ''
334-
"
318+
:status="filters.keywords.includes(keyword) ? 'active' : 'default'"
335319
@click="emit('toggleKeyword', keyword)"
336320
>
337321
{{ keyword }}
338-
</button>
322+
</TagClickable>
339323
<button
340324
v-if="hasMoreKeywords"
341325
type="button"

app/components/Package/Card.vue

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -162,20 +162,20 @@ const pkgDescription = useMarkdown(() => ({
162162
:aria-label="$t('package.card.keywords')"
163163
class="relative z-10 flex flex-wrap gap-1.5 mt-3 pt-3 border-t border-border list-none m-0 p-0 pointer-events-none"
164164
>
165-
<button
165+
<TagClickable
166166
v-for="keyword in result.package.keywords.slice(0, 5)"
167167
:key="keyword"
168168
type="button"
169-
class="tag text-xs hover:bg-fg hover:text-bg hover:border-fg transition-colors duration-200 focus-visible:ring-2 focus-visible:ring-fg focus-visible:ring-offset-1 border-solid pointer-events-auto"
170-
:class="{ 'bg-fg text-bg hover:opacity-80': props.filters?.keywords.includes(keyword) }"
169+
class="pointer-events-auto"
170+
:status="props.filters?.keywords.includes(keyword) ? 'active' : 'default'"
171171
:title="`Filter by ${keyword}`"
172172
@click.stop="emit('clickKeyword', keyword)"
173173
>
174174
{{ keyword }}
175-
</button>
175+
</TagClickable>
176176
<span
177177
v-if="result.package.keywords.length > 5"
178-
class="tag text-fg-subtle text-xs border-none bg-transparent pointer-events-auto"
178+
class="text-fg-subtle text-xs pointer-events-auto"
179179
:title="result.package.keywords.slice(5).join(', ')"
180180
>
181181
+{{ result.package.keywords.length - 5 }}

app/components/Package/TableRow.vue

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -123,20 +123,19 @@ const allMaintainersText = computed(() => {
123123
class="flex flex-wrap gap-1"
124124
:aria-label="$t('package.card.keywords')"
125125
>
126-
<button
126+
<TagClickable
127127
v-for="keyword in pkg.keywords.slice(0, 3)"
128128
:key="keyword"
129129
type="button"
130-
class="tag text-xs hover:bg-fg hover:text-bg hover:border-fg transition-colors duration-200 focus-visible:ring-2 focus-visible:ring-fg focus-visible:ring-offset-1 border-solid"
131-
:class="{ 'bg-fg text-bg hover:opacity-80': props.filters?.keywords.includes(keyword) }"
130+
:status="props.filters?.keywords.includes(keyword) ? 'active' : 'default'"
132131
:title="`Filter by ${keyword}`"
133132
@click.stop="emit('clickKeyword', keyword)"
134133
>
135134
{{ keyword }}
136-
</button>
135+
</TagClickable>
137136
<span
138137
v-if="pkg.keywords.length > 3"
139-
class="tag text-fg-subtle text-xs border-none bg-transparent"
138+
class="text-fg-subtle text-xs"
140139
:title="pkg.keywords.slice(3).join(', ')"
141140
>
142141
+{{ pkg.keywords.length - 3 }}

app/components/Tag/Clickable.vue

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<script setup lang="ts">
2+
const props = withDefaults(
3+
defineProps<{ as?: string | Component; status?: 'default' | 'active'; disabled?: boolean }>(),
4+
{
5+
status: 'default',
6+
as: 'button',
7+
},
8+
)
9+
</script>
10+
11+
<template>
12+
<component
13+
:is="props.as"
14+
class="inline-flex items-center px-2 py-0.5 text-xs font-mono border rounded transition-colors duration-200 focus-visible:ring-2 focus-visible:ring-fg focus-visible:ring-offset-1"
15+
:class="{
16+
'bg-bg-muted text-fg-muted border-border hover:(text-fg border-border-hover)':
17+
status === 'default',
18+
'bg-fg text-bg border-fg hover:(text-text-bg/50)': status === 'active',
19+
'opacity-50 cursor-not-allowed': disabled,
20+
}"
21+
:disabled="disabled"
22+
>
23+
<slot />
24+
</component>
25+
</template>

app/components/Tag/Static.vue

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<script setup lang="ts">
2+
const props = withDefaults(defineProps<{ as?: string | Component }>(), { as: 'span' })
3+
</script>
4+
5+
<template>
6+
<component
7+
:is="as"
8+
class="inline-flex items-center px-2 py-0.5 text-xs font-mono text-fg-muted bg-bg-muted border border-border rounded"
9+
>
10+
<slot />
11+
</component>
12+
</template>

app/pages/package/[...package].vue

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { joinURL } from 'ufo'
1111
import { areUrlsEquivalent } from '#shared/utils/url'
1212
import { isEditableElement } from '~/utils/input'
1313
import { formatBytes } from '~/utils/formatters'
14+
import { NuxtLink } from '#components'
1415
1516
definePageMeta({
1617
name: 'package',
@@ -1005,9 +1006,12 @@ defineOgImageComponent('Package', {
10051006
</h2>
10061007
<ul class="flex flex-wrap gap-1.5 list-none m-0 p-0">
10071008
<li v-for="keyword in displayVersion.keywords.slice(0, 15)" :key="keyword">
1008-
<NuxtLink :to="{ name: 'search', query: { q: `keywords:${keyword}` } }" class="tag">
1009+
<TagClickable
1010+
:as="NuxtLink"
1011+
:to="{ name: 'search', query: { q: `keywords:${keyword}` } }"
1012+
>
10091013
{{ keyword }}
1010-
</NuxtLink>
1014+
</TagClickable>
10111015
</li>
10121016
</ul>
10131017
</section>

test/nuxt/a11y.spec.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,8 @@ import {
111111
SettingsAccentColorPicker,
112112
SettingsBgThemePicker,
113113
SettingsToggle,
114+
TagStatic,
115+
TagClickable,
114116
TerminalExecute,
115117
TerminalInstall,
116118
TooltipAnnounce,
@@ -232,6 +234,44 @@ describe('component accessibility audits', () => {
232234
})
233235
})
234236

237+
describe('TagStatic', () => {
238+
it('should have no accessibility violations', async () => {
239+
const component = await mountSuspended(TagStatic, {
240+
slots: { default: 'Tag content' },
241+
})
242+
const results = await runAxe(component)
243+
expect(results.violations).toEqual([])
244+
})
245+
})
246+
247+
describe('TagClickable', () => {
248+
it('should have no accessibility violations', async () => {
249+
const component = await mountSuspended(TagClickable, {
250+
slots: { default: 'Tag content' },
251+
})
252+
const results = await runAxe(component)
253+
expect(results.violations).toEqual([])
254+
})
255+
256+
it('should have no accessibility violationst for active state', async () => {
257+
const component = await mountSuspended(TagClickable, {
258+
props: { status: 'active' },
259+
slots: { default: 'Tag content' },
260+
})
261+
const results = await runAxe(component)
262+
expect(results.violations).toEqual([])
263+
})
264+
265+
it('should have no accessibility violationst for disabled state', async () => {
266+
const component = await mountSuspended(TagClickable, {
267+
props: { disabled: true },
268+
slots: { default: 'Tag content' },
269+
})
270+
const results = await runAxe(component)
271+
expect(results.violations).toEqual([])
272+
})
273+
})
274+
235275
describe('TooltipApp', () => {
236276
it('should have no accessibility violations', async () => {
237277
const component = await mountSuspended(TooltipApp, {

uno.config.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -142,11 +142,7 @@ export default defineConfig({
142142
],
143143
['link-subtle', 'text-fg-muted hover:text-fg transition-colors duration-200 focus-ring'],
144144

145-
// Tags/badges
146-
[
147-
'tag',
148-
'inline-flex items-center px-2 py-0.5 text-xs font-mono text-fg-muted bg-bg-muted border border-border rounded transition-colors duration-200 hover:(text-fg border-border-hover)',
149-
],
145+
// badges
150146
['badge-orange', 'bg-badge-orange/10 text-badge-orange'],
151147
['badge-yellow', 'bg-badge-yellow/10 text-badge-yellow'],
152148
['badge-green', 'bg-badge-green/10 text-badge-green'],

0 commit comments

Comments
 (0)