Skip to content

Commit cc4f6dd

Browse files
Copilotserhalp
andauthored
feat: add comprehensive tooltips to every field in cache analysis run panel (#88)
* Initial plan * Add comprehensive tooltips to cache analysis fields Co-authored-by: serhalp <1377702+serhalp@users.noreply.github.com> * Add comprehensive tooltip tests and finalize implementation Co-authored-by: serhalp <1377702+serhalp@users.noreply.github.com> * fix: update tooltip definitions and add cacheable disclaimer - Fix Netlify Durable cache definition to describe opt-in cache with durable directive - Update URL to point to durable directive documentation section - Improve Netlify-Vary definition with more complete description - Add disclaimer to Cacheable tooltip about incomplete implementation - Update tests to match new tooltip text Co-authored-by: serhalp <1377702+serhalp@users.noreply.github.com> * feat: add keyboard accessibility to tooltips - Add tabindex="0" to all tooltip-bearing elements - Add focus/blur event handlers to match mouse hover behavior - Add visual focus indicators with outline and background color - Add tooltip-trigger class with dotted underline for static labels - Ensure keyboard users can navigate and see tooltips - Improve screen reader support with aria-label attributes Co-authored-by: serhalp <1377702+serhalp@users.noreply.github.com> * Apply suggestions from code review * fix: update Netlify Durable tooltip unit test Update test expectation to match the corrected tooltip text about regional cache sharing Co-authored-by: serhalp <1377702+serhalp@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: serhalp <1377702+serhalp@users.noreply.github.com> Co-authored-by: Philippe Serhal <philippe.serhal@netlify.com>
1 parent e93af17 commit cc4f6dd

File tree

3 files changed

+402
-3
lines changed

3 files changed

+402
-3
lines changed

app/components/CacheAnalysis.vue

Lines changed: 133 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<script setup lang="ts">
22
import { formatDuration, intervalToDuration } from 'date-fns'
3+
import { getFieldTooltip, getCacheNameTooltip, getForwardReasonTooltip, formatTooltip } from '~/utils/tooltips'
34
45
const props = defineProps<{
56
cacheHeaders: Record<string, string>
@@ -61,10 +62,20 @@ onUnmounted(() => {
6162
<template>
6263
<div class="container">
6364
<div>
64-
Served by: <strong>{{ cacheAnalysis.servedBy.source }}</strong>
65+
<span
66+
class="tooltip-trigger"
67+
tabindex="0"
68+
:title="formatTooltip(getFieldTooltip('served-by'))"
69+
:aria-label="`Served by: ${getFieldTooltip('served-by').text}`"
70+
>Served by:</span> <strong>{{ cacheAnalysis.servedBy.source }}</strong>
6571
</div>
6672
<div>
67-
CDN node(s): <code>{{ cacheAnalysis.servedBy.cdnNodes }}</code>
73+
<span
74+
class="tooltip-trigger"
75+
tabindex="0"
76+
:title="formatTooltip(getFieldTooltip('cdn-nodes'))"
77+
:aria-label="`CDN nodes: ${getFieldTooltip('cdn-nodes').text}`"
78+
>CDN node(s):</span> <code>{{ cacheAnalysis.servedBy.cdnNodes }}</code>
6879
</div>
6980

7081
<hr />
@@ -88,17 +99,25 @@ onUnmounted(() => {
8899
<!-- This is a bit of a hack to use the pretty <dt> styling but with sections. -->
89100
<!-- I should probably just do something custom instead. -->
90101
<dt class="cache-heading">
91-
<h4>
102+
<h4
103+
tabindex="0"
104+
class="cache-name-heading"
105+
:title="formatTooltip(getCacheNameTooltip(cacheName))"
106+
>
92107
↳ <em>{{ cacheName }}</em> cache
93108
</h4>
94109
</dt>
95110
<dd />
96111

97112
<dt
98113
class="data-key"
114+
tabindex="0"
99115
:class="{ 'key-highlighted': isKeyHovered(`Hit-${cacheIndex}`) }"
116+
:title="formatTooltip(getFieldTooltip('hit'))"
100117
@mouseenter="handleDataKeyHover(`Hit-${cacheIndex}`, parameters.hit)"
101118
@mouseleave="handleDataKeyLeave"
119+
@focus="handleDataKeyHover(`Hit-${cacheIndex}`, parameters.hit)"
120+
@blur="handleDataKeyLeave"
102121
>
103122
Hit
104123
</dt>
@@ -116,9 +135,13 @@ onUnmounted(() => {
116135
<template v-if="parameters.fwd">
117136
<dt
118137
class="data-key"
138+
tabindex="0"
119139
:class="{ 'key-highlighted': isKeyHovered(`Forwarded because-${cacheIndex}`) }"
140+
:title="formatTooltip(getFieldTooltip('forwarded-because'))"
120141
@mouseenter="handleDataKeyHover(`Forwarded because-${cacheIndex}`, parameters.fwd)"
121142
@mouseleave="handleDataKeyLeave"
143+
@focus="handleDataKeyHover(`Forwarded because-${cacheIndex}`, parameters.fwd)"
144+
@blur="handleDataKeyLeave"
122145
>
123146
Forwarded because
124147
</dt>
@@ -129,6 +152,7 @@ onUnmounted(() => {
129152
'value-matching': isKeyHovered(`Forwarded because-${cacheIndex}`) && isValueMatching(parameters.fwd),
130153
'value-different': isKeyHovered(`Forwarded because-${cacheIndex}`) && !isValueMatching(parameters.fwd),
131154
}"
155+
:title="formatTooltip(getForwardReasonTooltip(parameters.fwd))"
132156
>
133157
{{ parameters.fwd }}
134158
</dd>
@@ -137,9 +161,13 @@ onUnmounted(() => {
137161
<template v-if="parameters['fwd-status']">
138162
<dt
139163
class="data-key"
164+
tabindex="0"
140165
:class="{ 'key-highlighted': isKeyHovered(`Forwarded status-${cacheIndex}`) }"
166+
:title="formatTooltip(getFieldTooltip('forwarded-status'))"
141167
@mouseenter="handleDataKeyHover(`Forwarded status-${cacheIndex}`, parameters['fwd-status'])"
142168
@mouseleave="handleDataKeyLeave"
169+
@focus="handleDataKeyHover(`Forwarded status-${cacheIndex}`, parameters['fwd-status'])"
170+
@blur="handleDataKeyLeave"
143171
>
144172
Forwarded status
145173
</dt>
@@ -158,9 +186,13 @@ onUnmounted(() => {
158186
<template v-if="parameters.ttl">
159187
<dt
160188
class="data-key"
189+
tabindex="0"
161190
:class="{ 'key-highlighted': isKeyHovered(`TTL-${cacheIndex}`) }"
191+
:title="formatTooltip(getFieldTooltip('ttl'))"
162192
@mouseenter="handleDataKeyHover(`TTL-${cacheIndex}`, parameters.ttl)"
163193
@mouseleave="handleDataKeyLeave"
194+
@focus="handleDataKeyHover(`TTL-${cacheIndex}`, parameters.ttl)"
195+
@blur="handleDataKeyLeave"
164196
>
165197
TTL
166198
</dt>
@@ -186,9 +218,13 @@ onUnmounted(() => {
186218
<template v-if="parameters.stored">
187219
<dt
188220
class="data-key"
221+
tabindex="0"
189222
:class="{ 'key-highlighted': isKeyHovered(`Stored the response-${cacheIndex}`) }"
223+
:title="formatTooltip(getFieldTooltip('stored-response'))"
190224
@mouseenter="handleDataKeyHover(`Stored the response-${cacheIndex}`, parameters.stored)"
191225
@mouseleave="handleDataKeyLeave"
226+
@focus="handleDataKeyHover(`Stored the response-${cacheIndex}`, parameters.stored)"
227+
@blur="handleDataKeyLeave"
192228
>
193229
Stored the response
194230
</dt>
@@ -207,9 +243,13 @@ onUnmounted(() => {
207243
<template v-if="parameters.collapsed">
208244
<dt
209245
class="data-key"
246+
tabindex="0"
210247
:class="{ 'key-highlighted': isKeyHovered(`Collapsed w/ other reqs-${cacheIndex}`) }"
248+
:title="formatTooltip(getFieldTooltip('collapsed-requests'))"
211249
@mouseenter="handleDataKeyHover(`Collapsed w/ other reqs-${cacheIndex}`, parameters.collapsed)"
212250
@mouseleave="handleDataKeyLeave"
251+
@focus="handleDataKeyHover(`Collapsed w/ other reqs-${cacheIndex}`, parameters.collapsed)"
252+
@blur="handleDataKeyLeave"
213253
>
214254
Collapsed w/ other reqs
215255
</dt>
@@ -228,9 +268,13 @@ onUnmounted(() => {
228268
<template v-if="parameters.key">
229269
<dt
230270
class="data-key"
271+
tabindex="0"
231272
:class="{ 'key-highlighted': isKeyHovered(`Cache key-${cacheIndex}`) }"
273+
:title="formatTooltip(getFieldTooltip('cache-key'))"
232274
@mouseenter="handleDataKeyHover(`Cache key-${cacheIndex}`, parameters.key)"
233275
@mouseleave="handleDataKeyLeave"
276+
@focus="handleDataKeyHover(`Cache key-${cacheIndex}`, parameters.key)"
277+
@blur="handleDataKeyLeave"
234278
>
235279
Cache key
236280
</dt>
@@ -249,9 +293,13 @@ onUnmounted(() => {
249293
<template v-if="parameters.detail">
250294
<dt
251295
class="data-key"
296+
tabindex="0"
252297
:class="{ 'key-highlighted': isKeyHovered(`Extra details-${cacheIndex}`) }"
298+
:title="formatTooltip(getFieldTooltip('extra-details'))"
253299
@mouseenter="handleDataKeyHover(`Extra details-${cacheIndex}`, parameters.detail)"
254300
@mouseleave="handleDataKeyLeave"
301+
@focus="handleDataKeyHover(`Extra details-${cacheIndex}`, parameters.detail)"
302+
@blur="handleDataKeyLeave"
255303
>
256304
Extra details
257305
</dt>
@@ -279,9 +327,13 @@ onUnmounted(() => {
279327

280328
<dt
281329
class="data-key"
330+
tabindex="0"
282331
:class="{ 'key-highlighted': isKeyHovered('Cacheable') }"
332+
:title="formatTooltip(getFieldTooltip('cacheable'))"
283333
@mouseenter="handleDataKeyHover('Cacheable', cacheAnalysis.cacheControl.isCacheable)"
284334
@mouseleave="handleDataKeyLeave"
335+
@focus="handleDataKeyHover('Cacheable', cacheAnalysis.cacheControl.isCacheable)"
336+
@blur="handleDataKeyLeave"
285337
>
286338
Cacheable
287339
</dt>
@@ -299,9 +351,13 @@ onUnmounted(() => {
299351
<template v-if="cacheAnalysis.cacheControl.age">
300352
<dt
301353
class="data-key"
354+
tabindex="0"
302355
:class="{ 'key-highlighted': isKeyHovered('Age') }"
356+
:title="formatTooltip(getFieldTooltip('age'))"
303357
@mouseenter="handleDataKeyHover('Age', cacheAnalysis.cacheControl.age)"
304358
@mouseleave="handleDataKeyLeave"
359+
@focus="handleDataKeyHover('Age', cacheAnalysis.cacheControl.age)"
360+
@blur="handleDataKeyLeave"
305361
>
306362
Age
307363
</dt>
@@ -327,9 +383,13 @@ onUnmounted(() => {
327383
<template v-if="cacheAnalysis.cacheControl.date">
328384
<dt
329385
class="data-key"
386+
tabindex="0"
330387
:class="{ 'key-highlighted': isKeyHovered('Date') }"
388+
:title="formatTooltip(getFieldTooltip('date'))"
331389
@mouseenter="handleDataKeyHover('Date', cacheAnalysis.cacheControl.date)"
332390
@mouseleave="handleDataKeyLeave"
391+
@focus="handleDataKeyHover('Date', cacheAnalysis.cacheControl.date)"
392+
@blur="handleDataKeyLeave"
333393
>
334394
Date
335395
</dt>
@@ -354,9 +414,13 @@ onUnmounted(() => {
354414
<template v-if="cacheAnalysis.cacheControl.etag">
355415
<dt
356416
class="data-key"
417+
tabindex="0"
357418
:class="{ 'key-highlighted': isKeyHovered('ETag') }"
419+
:title="formatTooltip(getFieldTooltip('etag'))"
358420
@mouseenter="handleDataKeyHover('ETag', cacheAnalysis.cacheControl.etag)"
359421
@mouseleave="handleDataKeyLeave"
422+
@focus="handleDataKeyHover('ETag', cacheAnalysis.cacheControl.etag)"
423+
@blur="handleDataKeyLeave"
360424
>
361425
ETag
362426
</dt>
@@ -375,9 +439,13 @@ onUnmounted(() => {
375439
<template v-if="cacheAnalysis.cacheControl.expiresAt">
376440
<dt
377441
class="data-key"
442+
tabindex="0"
378443
:class="{ 'key-highlighted': isKeyHovered('Expires at') }"
444+
:title="formatTooltip(getFieldTooltip('expires-at'))"
379445
@mouseenter="handleDataKeyHover('Expires at', cacheAnalysis.cacheControl.expiresAt)"
380446
@mouseleave="handleDataKeyLeave"
447+
@focus="handleDataKeyHover('Expires at', cacheAnalysis.cacheControl.expiresAt)"
448+
@blur="handleDataKeyLeave"
381449
>
382450
Expires at
383451
</dt>
@@ -402,9 +470,13 @@ onUnmounted(() => {
402470
<template v-if="cacheAnalysis.cacheControl.ttl">
403471
<dt
404472
class="data-key"
473+
tabindex="0"
405474
:class="{ 'key-highlighted': isKeyHovered('TTL (browser)') }"
475+
:title="formatTooltip(getFieldTooltip('ttl-browser'))"
406476
@mouseenter="handleDataKeyHover('TTL (browser)', cacheAnalysis.cacheControl.ttl)"
407477
@mouseleave="handleDataKeyLeave"
478+
@focus="handleDataKeyHover('TTL (browser)', cacheAnalysis.cacheControl.ttl)"
479+
@blur="handleDataKeyLeave"
408480
>
409481
TTL{{
410482
cacheAnalysis.cacheControl.netlifyCdnTtl
@@ -435,9 +507,13 @@ onUnmounted(() => {
435507
<template v-if="cacheAnalysis.cacheControl.cdnTtl">
436508
<dt
437509
class="data-key"
510+
tabindex="0"
438511
:class="{ 'key-highlighted': isKeyHovered('TTL (CDN)') }"
512+
:title="formatTooltip(getFieldTooltip('ttl-cdn'))"
439513
@mouseenter="handleDataKeyHover('TTL (CDN)', cacheAnalysis.cacheControl.cdnTtl)"
440514
@mouseleave="handleDataKeyLeave"
515+
@focus="handleDataKeyHover('TTL (CDN)', cacheAnalysis.cacheControl.cdnTtl)"
516+
@blur="handleDataKeyLeave"
441517
>
442518
TTL ({{
443519
cacheAnalysis.cacheControl.netlifyCdnTtl
@@ -467,9 +543,13 @@ onUnmounted(() => {
467543
<template v-if="cacheAnalysis.cacheControl.netlifyCdnTtl">
468544
<dt
469545
class="data-key"
546+
tabindex="0"
470547
:class="{ 'key-highlighted': isKeyHovered('TTL (Netlify CDN)') }"
548+
:title="formatTooltip(getFieldTooltip('ttl-netlify-cdn'))"
471549
@mouseenter="handleDataKeyHover('TTL (Netlify CDN)', cacheAnalysis.cacheControl.netlifyCdnTtl)"
472550
@mouseleave="handleDataKeyLeave"
551+
@focus="handleDataKeyHover('TTL (Netlify CDN)', cacheAnalysis.cacheControl.netlifyCdnTtl)"
552+
@blur="handleDataKeyLeave"
473553
>
474554
TTL (Netlify CDN)
475555
</dt>
@@ -495,9 +575,13 @@ onUnmounted(() => {
495575
<template v-if="cacheAnalysis.cacheControl.vary">
496576
<dt
497577
class="data-key"
578+
tabindex="0"
498579
:class="{ 'key-highlighted': isKeyHovered('Vary') }"
580+
:title="formatTooltip(getFieldTooltip('vary'))"
499581
@mouseenter="handleDataKeyHover('Vary', cacheAnalysis.cacheControl.vary)"
500582
@mouseleave="handleDataKeyLeave"
583+
@focus="handleDataKeyHover('Vary', cacheAnalysis.cacheControl.vary)"
584+
@blur="handleDataKeyLeave"
501585
>
502586
Vary
503587
</dt>
@@ -516,9 +600,13 @@ onUnmounted(() => {
516600
<template v-if="cacheAnalysis.cacheControl.netlifyVary">
517601
<dt
518602
class="data-key"
603+
tabindex="0"
519604
:class="{ 'key-highlighted': isKeyHovered('Netlify-Vary') }"
605+
:title="formatTooltip(getFieldTooltip('netlify-vary'))"
520606
@mouseenter="handleDataKeyHover('Netlify-Vary', cacheAnalysis.cacheControl.netlifyVary)"
521607
@mouseleave="handleDataKeyLeave"
608+
@focus="handleDataKeyHover('Netlify-Vary', cacheAnalysis.cacheControl.netlifyVary)"
609+
@blur="handleDataKeyLeave"
522610
>
523611
Netlify-Vary
524612
</dt>
@@ -537,9 +625,13 @@ onUnmounted(() => {
537625
<template v-if="cacheAnalysis.cacheControl.revalidate">
538626
<dt
539627
class="data-key"
628+
tabindex="0"
540629
:class="{ 'key-highlighted': isKeyHovered('Revalidation') }"
630+
:title="formatTooltip(getFieldTooltip('revalidation'))"
541631
@mouseenter="handleDataKeyHover('Revalidation', cacheAnalysis.cacheControl.revalidate)"
542632
@mouseleave="handleDataKeyLeave"
633+
@focus="handleDataKeyHover('Revalidation', cacheAnalysis.cacheControl.revalidate)"
634+
@blur="handleDataKeyLeave"
543635
>
544636
Revalidation
545637
</dt>
@@ -583,6 +675,18 @@ dt.cache-heading h4 {
583675
font-size: 1.1em;
584676
}
585677
678+
dt.cache-heading h4.cache-name-heading {
679+
cursor: help;
680+
display: inline-block;
681+
}
682+
683+
dt.cache-heading h4.cache-name-heading:focus {
684+
outline: 2px solid rgb(59, 130, 246);
685+
outline-offset: 2px;
686+
border-radius: 4px;
687+
background-color: rgba(59, 130, 246, 0.1);
688+
}
689+
586690
/* Default Netlify Examples styles add ": " */
587691
.cache-heading::after {
588692
content: none;
@@ -603,6 +707,12 @@ dd code {
603707
background-color: rgba(59, 130, 246, 0.1);
604708
}
605709
710+
.data-key:focus {
711+
outline: 2px solid rgb(59, 130, 246);
712+
outline-offset: 2px;
713+
background-color: rgba(59, 130, 246, 0.15);
714+
}
715+
606716
.data-key.key-highlighted {
607717
background-color: rgba(59, 130, 246, 0.2);
608718
font-weight: 600;
@@ -631,4 +741,24 @@ dd code {
631741
color: rgb(107, 114, 128);
632742
margin-left: 0.25em;
633743
}
744+
745+
/* Tooltip trigger accessibility styles */
746+
.tooltip-trigger {
747+
cursor: help;
748+
text-decoration: underline;
749+
text-decoration-style: dotted;
750+
text-decoration-color: rgba(107, 114, 128, 0.5);
751+
text-underline-offset: 2px;
752+
}
753+
754+
.tooltip-trigger:focus {
755+
outline: 2px solid rgb(59, 130, 246);
756+
outline-offset: 2px;
757+
border-radius: 2px;
758+
background-color: rgba(59, 130, 246, 0.1);
759+
}
760+
761+
.tooltip-trigger:hover {
762+
text-decoration-color: rgba(107, 114, 128, 0.8);
763+
}
634764
</style>

0 commit comments

Comments
 (0)