File tree Expand file tree Collapse file tree
Expand file tree Collapse file tree Original file line number Diff line number Diff line change 1+ <script setup lang="ts">
2+ const props = defineProps <{
3+ /** Tooltip text */
4+ text: string
5+ /** Position: 'top' | 'bottom' | 'left' | 'right' */
6+ position? : ' top' | ' bottom' | ' left' | ' right'
7+ /** is tooltip visible */
8+ isVisible: boolean
9+ }>()
10+ </script >
11+
12+ <template >
13+ <BaseTooltip :text :isVisible :position :tooltip-attr =" { 'aria-live': 'polite' }"
14+ ><slot
15+ /></BaseTooltip >
16+ </template >
Original file line number Diff line number Diff line change @@ -9,15 +9,6 @@ const props = defineProps<{
99const isVisible = shallowRef (false )
1010const tooltipId = useId ()
1111
12- const positionClasses: Record <string , string > = {
13- top: ' bottom-full inset-is-1/2 -translate-x-1/2 mb-1' ,
14- bottom: ' top-full inset-is-0 mt-1' ,
15- left: ' inset-ie-full top-1/2 -translate-y-1/2 me-2' ,
16- right: ' inset-is-full top-1/2 -translate-y-1/2 ms-2' ,
17- }
18-
19- const tooltipPosition = computed (() => positionClasses [props .position || ' bottom' ])
20-
2112function show() {
2213 isVisible .value = true
2314}
@@ -28,31 +19,16 @@ function hide() {
2819 </script >
2920
3021<template >
31- <div
32- class =" relative inline-flex"
33- :aria-describedby =" isVisible ? tooltipId : undefined"
22+ <BaseTooltip
23+ :text
24+ :isVisible
25+ :position
26+ :tooltip-attr =" { role: 'tooltip', id: tooltipId }"
3427 @mouseenter =" show"
3528 @mouseleave =" hide"
3629 @focusin =" show"
3730 @focusout =" hide"
38- >
39- <slot />
40-
41- <Transition
42- enter-active-class =" transition-opacity duration-150 motion-reduce:transition-none"
43- leave-active-class =" transition-opacity duration-100 motion-reduce:transition-none"
44- enter-from-class =" opacity-0"
45- leave-to-class =" opacity-0"
46- >
47- <div
48- v-if =" isVisible"
49- :id =" tooltipId"
50- role =" tooltip"
51- class =" absolute px-2 py-1 font-mono text-xs text-fg bg-bg-elevated border border-border rounded shadow-lg whitespace-nowrap z-[100] pointer-events-none"
52- :class =" tooltipPosition"
53- >
54- {{ text }}
55- </div >
56- </Transition >
57- </div >
31+ :aria-describedby =" isVisible ? tooltipId : undefined"
32+ ><slot
33+ /></BaseTooltip >
5834</template >
Original file line number Diff line number Diff line change 1+ <script setup lang="ts">
2+ import type { HTMLAttributes } from ' vue'
3+
4+ const props = defineProps <{
5+ /** Tooltip text */
6+ text: string
7+ /** Position: 'top' | 'bottom' | 'left' | 'right' */
8+ position? : ' top' | ' bottom' | ' left' | ' right'
9+ /** is tooltip visible */
10+ isVisible: boolean
11+ /** attributes for tooltip element */
12+ tooltipAttr? : HTMLAttributes
13+ }>()
14+
15+ const positionClasses: Record <string , string > = {
16+ top: ' bottom-full inset-is-1/2 -translate-x-1/2 mb-1' ,
17+ bottom: ' top-full inset-is-0 mt-1' ,
18+ left: ' inset-ie-full top-1/2 -translate-y-1/2 me-2' ,
19+ right: ' inset-is-full top-1/2 -translate-y-1/2 ms-2' ,
20+ }
21+
22+ const tooltipPosition = computed (() => positionClasses [props .position || ' bottom' ])
23+ </script >
24+
25+ <template >
26+ <div class =" relative inline-flex" >
27+ <slot />
28+
29+ <Transition
30+ enter-active-class =" transition-opacity duration-150 motion-reduce:transition-none"
31+ leave-active-class =" transition-opacity duration-100 motion-reduce:transition-none"
32+ enter-from-class =" opacity-0"
33+ leave-to-class =" opacity-0"
34+ >
35+ <div
36+ v-if =" props.isVisible"
37+ class =" absolute px-2 py-1 font-mono text-xs text-fg bg-bg-elevated border border-border rounded shadow-lg whitespace-nowrap z-[100] pointer-events-none"
38+ :class =" tooltipPosition"
39+ v-bind =" tooltipAttr"
40+ >
41+ {{ text }}
42+ </div >
43+ </Transition >
44+ </div >
45+ </template >
Original file line number Diff line number Diff line change @@ -86,6 +86,12 @@ const displayVersion = computed(() => {
8686 return pkg .value .versions [latestTag ] ?? null
8787})
8888
89+ // copy package name
90+ const { copied : copiedPkgName, copy : copyPkgName } = useClipboard ({
91+ source: packageName ,
92+ copiedDuring: 2000 ,
93+ })
94+
8995// Fetch dependency analysis (lazy, client-side)
9096// This is the same composable used by PackageVulnerabilityTree and PackageDeprecatedTree
9197const {
@@ -388,9 +394,19 @@ function handleClick(event: MouseEvent) {
388394 :to =" { name: 'org', params: { org: orgName } }"
389395 class =" text-fg-muted hover:text-fg transition-colors duration-200"
390396 >@{{ orgName }}</NuxtLink
391- ><span v-if =" orgName" >/</span
392- >{{ orgName ? pkg.name.replace(`@${orgName}/`, '') : pkg.name }}
397+ ><span v-if =" orgName" >/</span >
398+ <AnnounceTooltip :text =" $t('common.copied')" :isVisible =" copiedPkgName" >
399+ <button
400+ @click =" copyPkgName()"
401+ aria-describedby =" copy-pkg-name"
402+ class =" cursor-copy ms-1 mt-1 active:scale-95 transition-transform"
403+ >
404+ {{ orgName ? pkg.name.replace(`@${orgName}/`, '') : pkg.name }}
405+ </button >
406+ </AnnounceTooltip >
393407 </h1 >
408+
409+ <span id =" copy-pkg-name" class =" sr-only" >{{ $t('package.copy_name') }}</span >
394410 <span
395411 v-if =" displayVersion"
396412 class =" inline-flex items-baseline gap-1.5 font-mono text-base sm:text-lg text-fg-muted shrink-0"
Original file line number Diff line number Diff line change @@ -418,7 +418,7 @@ defineOgImageComponent('Default', {
418418 <button
419419 v-if =" selectedLines"
420420 type =" button"
421- class =" px-2 py-1 font-mono text-xs text-fg-muted bg-bg-subtle border border-border rounded hover:text-fg hover:border-border-hover transition-colors"
421+ class =" px-2 py-1 font-mono text-xs text-fg-muted bg-bg-subtle border border-border rounded hover:text-fg hover:border-border-hover transition-colors active:scale-95 "
422422 @click =" copyPermalinkUrl"
423423 >
424424 {{ permalinkCopied ? $t('common.copied') : $t('code.copy_link') }}
Original file line number Diff line number Diff line change 111111 "verified_provenance" : " Verified provenance" ,
112112 "view_permalink" : " View permalink for this version" ,
113113 "navigation" : " Package" ,
114+ "copy_name" : " Copy package name" ,
114115 "deprecation" : {
115116 "package" : " This package has been deprecated." ,
116117 "version" : " This version has been deprecated." ,
Original file line number Diff line number Diff line change 111111 "verified_provenance" : " Verified provenance" ,
112112 "view_permalink" : " View permalink for this version" ,
113113 "navigation" : " Package" ,
114+ "copy_name" : " Copy package name" ,
114115 "deprecation" : {
115116 "package" : " This package has been deprecated." ,
116117 "version" : " This version has been deprecated." ,
Original file line number Diff line number Diff line change @@ -56,6 +56,7 @@ import DateTime from '~/components/DateTime.vue'
5656import AppHeader from '~/components/AppHeader.vue'
5757import AppFooter from '~/components/AppFooter.vue'
5858import AppTooltip from '~/components/AppTooltip.vue'
59+ import AnnounceTooltip from '~/components/AnnounceTooltip.vue'
5960import LoadingSpinner from '~/components/LoadingSpinner.vue'
6061import JsrBadge from '~/components/JsrBadge.vue'
6162import ProvenanceBadge from '~/components/ProvenanceBadge.vue'
@@ -194,6 +195,17 @@ describe('component accessibility audits', () => {
194195 } )
195196 } )
196197
198+ describe ( 'AnnounceTooltip' , ( ) => {
199+ it ( 'should have no accessibility violations' , async ( ) => {
200+ const component = await mountSuspended ( AnnounceTooltip , {
201+ props : { text : 'Tooltip content' , isVisible : true } ,
202+ slots : { default : '<button>Trigger</button>' } ,
203+ } )
204+ const results = await runAxe ( component )
205+ expect ( results . violations ) . toEqual ( [ ] )
206+ } )
207+ } )
208+
197209 describe ( 'LoadingSpinner' , ( ) => {
198210 it ( 'should have no accessibility violations' , async ( ) => {
199211 const component = await mountSuspended ( LoadingSpinner )
You can’t perform that action at this time.
0 commit comments