[READ-ONLY] a fast, modern browser for the npm registry
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor: extract tag component (#742)

Co-authored-by: Daniel Roe <daniel@roe.dev>

authored by

Marcus Blättermann
Daniel Roe
and committed by
GitHub
7fbf0469 e4f19b52

+107 -47
+2 -2
app/components/Filter/Chips.vue
··· 14 14 <template> 15 15 <div v-if="chips.length > 0" class="flex flex-wrap items-center gap-2"> 16 16 <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"> 18 18 <span class="text-fg-subtle text-xs">{{ chip.label }}:</span> 19 19 <span class="max-w-32 truncate">{{ 20 20 Array.isArray(chip.value) ? chip.value.join(', ') : chip.value ··· 27 27 > 28 28 <span class="i-carbon-close w-3 h-3" aria-hidden="true" /> 29 29 </button> 30 - </span> 30 + </TagStatic> 31 31 </TransitionGroup> 32 32 33 33 <button
+12 -28
app/components/Filter/Panel.vue
··· 243 243 role="radiogroup" 244 244 :aria-label="$t('filters.weekly_downloads')" 245 245 > 246 - <button 246 + <TagClickable 247 247 v-for="range in DOWNLOAD_RANGES" 248 248 :key="range.value" 249 249 type="button" 250 250 role="radio" 251 251 :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'" 258 253 @click="emit('update:downloadRange', range.value)" 259 254 > 260 255 {{ $t(getDownloadRangeLabelKey(range.value)) }} 261 - </button> 256 + </TagClickable> 262 257 </div> 263 258 </fieldset> 264 259 ··· 272 267 role="radiogroup" 273 268 :aria-label="$t('filters.updated_within')" 274 269 > 275 - <button 270 + <TagClickable 276 271 v-for="option in UPDATED_WITHIN_OPTIONS" 277 272 :key="option.value" 278 273 type="button" 279 274 role="radio" 280 275 :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'" 287 277 @click="emit('update:updatedWithin', option.value)" 288 278 > 289 279 {{ $t(getUpdatedWithinLabelKey(option.value)) }} 290 - </button> 280 + </TagClickable> 291 281 </div> 292 282 </fieldset> 293 283 ··· 300 290 </span> 301 291 </legend> 302 292 <div class="flex flex-wrap gap-2" role="radiogroup" :aria-label="$t('filters.security')"> 303 - <button 293 + <TagClickable 304 294 v-for="security in SECURITY_FILTER_VALUES" 305 295 :key="security" 306 296 type="button" 307 297 role="radio" 308 298 disabled 309 299 :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'" 314 301 > 315 302 {{ $t(getSecurityLabelKey(security)) }} 316 - </button> 303 + </TagClickable> 317 304 </div> 318 305 </fieldset> 319 306 ··· 323 310 {{ $t('filters.keywords') }} 324 311 </legend> 325 312 <div class="flex flex-wrap gap-1.5" role="group" :aria-label="$t('filters.keywords')"> 326 - <button 313 + <TagClickable 327 314 v-for="keyword in displayedKeywords" 328 315 :key="keyword" 329 316 type="button" 330 317 :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'" 335 319 @click="emit('toggleKeyword', keyword)" 336 320 > 337 321 {{ keyword }} 338 - </button> 322 + </TagClickable> 339 323 <button 340 324 v-if="hasMoreKeywords" 341 325 type="button"
+5 -5
app/components/Package/Card.vue
··· 162 162 :aria-label="$t('package.card.keywords')" 163 163 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" 164 164 > 165 - <button 165 + <TagClickable 166 166 v-for="keyword in result.package.keywords.slice(0, 5)" 167 167 :key="keyword" 168 168 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'" 171 171 :title="`Filter by ${keyword}`" 172 172 @click.stop="emit('clickKeyword', keyword)" 173 173 > 174 174 {{ keyword }} 175 - </button> 175 + </TagClickable> 176 176 <span 177 177 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" 179 179 :title="result.package.keywords.slice(5).join(', ')" 180 180 > 181 181 +{{ result.package.keywords.length - 5 }}
+4 -5
app/components/Package/TableRow.vue
··· 123 123 class="flex flex-wrap gap-1" 124 124 :aria-label="$t('package.card.keywords')" 125 125 > 126 - <button 126 + <TagClickable 127 127 v-for="keyword in pkg.keywords.slice(0, 3)" 128 128 :key="keyword" 129 129 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'" 132 131 :title="`Filter by ${keyword}`" 133 132 @click.stop="emit('clickKeyword', keyword)" 134 133 > 135 134 {{ keyword }} 136 - </button> 135 + </TagClickable> 137 136 <span 138 137 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" 140 139 :title="pkg.keywords.slice(3).join(', ')" 141 140 > 142 141 +{{ pkg.keywords.length - 3 }}
+25
app/components/Tag/Clickable.vue
··· 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>
+12
app/components/Tag/Static.vue
··· 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>
+6 -2
app/pages/package/[...package].vue
··· 11 11 import { areUrlsEquivalent } from '#shared/utils/url' 12 12 import { isEditableElement } from '~/utils/input' 13 13 import { formatBytes } from '~/utils/formatters' 14 + import { NuxtLink } from '#components' 14 15 15 16 definePageMeta({ 16 17 name: 'package', ··· 1005 1006 </h2> 1006 1007 <ul class="flex flex-wrap gap-1.5 list-none m-0 p-0"> 1007 1008 <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 + > 1009 1013 {{ keyword }} 1010 - </NuxtLink> 1014 + </TagClickable> 1011 1015 </li> 1012 1016 </ul> 1013 1017 </section>
+40
test/nuxt/a11y.spec.ts
··· 111 111 SettingsAccentColorPicker, 112 112 SettingsBgThemePicker, 113 113 SettingsToggle, 114 + TagStatic, 115 + TagClickable, 114 116 TerminalExecute, 115 117 TerminalInstall, 116 118 TooltipAnnounce, ··· 226 228 const component = await mountSuspended(BaseCard, { 227 229 props: { isExactMatch: true }, 228 230 slots: { default: '<p>Exact match content</p>' }, 231 + }) 232 + const results = await runAxe(component) 233 + expect(results.violations).toEqual([]) 234 + }) 235 + }) 236 + 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' }, 229 269 }) 230 270 const results = await runAxe(component) 231 271 expect(results.violations).toEqual([])
+1 -5
uno.config.ts
··· 142 142 ], 143 143 ['link-subtle', 'text-fg-muted hover:text-fg transition-colors duration-200 focus-ring'], 144 144 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 150 146 ['badge-orange', 'bg-badge-orange/10 text-badge-orange'], 151 147 ['badge-yellow', 'bg-badge-yellow/10 text-badge-yellow'], 152 148 ['badge-green', 'bg-badge-green/10 text-badge-green'],