11<template >
22 <div class =" container" >
3- <ul v-once class =" project-feed" >
3+ <ul class =" project-feed" >
44 <li
55 v-for =" (project, index) in list"
66 :key =" project.name"
2020 :target =" project.url?.startsWith('http') ? '_blank' : undefined"
2121 class =" project-row__link"
2222 >
23- <h3 class =" project-row__title" >{{ project.name }}</h3 >
23+ <component :is = " headingLevel " class =" project-row__title" >{{ project.name }}</component >
2424 </component >
2525 </div >
2626
2727 <p class =" project-row__description" >{{ project.description }}</p >
2828 </div >
2929
3030 <div v-if =" project.image" class =" lightbox" >
31- <input :id =" `zoom-${index}`" type =" checkbox" class =" lightbox__toggle" aria-label =" Open preview" />
32- <label :for =" `zoom-${index}`" class =" lightbox__trigger" >
31+ <button
32+ class =" lightbox__trigger"
33+ :aria-label =" `Open ${project.name} preview`"
34+ @click =" openLightbox(project)"
35+ >
3336 <img
3437 :src =" project.image"
3538 :alt =" `${project.name} preview`"
3639 class =" thumb"
37- :loading =" index <= props.preloadProjectImages ? 'eager' : 'lazy'"
40+ :loading =" index < props.preloadProjectImages ? 'eager' : 'lazy'"
3841 />
39- </label >
40- <label :for =" `zoom-${index}`" class =" lightbox__overlay" >
41- <img :src =" project.image" alt =" " class =" full-view" />
42- </label >
42+ </button >
4343 </div >
4444 </li >
4545 </ul >
46+
47+ <dialog
48+ ref =" dialogEl"
49+ class =" lightbox-dialog"
50+ aria-label =" Project image preview"
51+ @click.self =" closeLightbox"
52+ >
53+ <button class =" lightbox-dialog__close" aria-label =" Close preview" @click =" closeLightbox" >
54+ <span aria-hidden =" true" >× ; </span >
55+ </button >
56+ <img
57+ v-if =" activeProject"
58+ :src =" activeProject.image"
59+ :alt =" `${activeProject.name} preview`"
60+ class =" lightbox-dialog__img"
61+ />
62+ </dialog >
4663 </div >
4764</template >
4865
@@ -58,9 +75,31 @@ interface Project {
5875const props = withDefaults (defineProps <{
5976 limit? : number ;
6077 preloadProjectImages? : number ;
78+ headingLevel? : ' h2' | ' h3' ;
6179}>(), {
6280 limit: undefined ,
63- preloadProjectImages: 0
81+ preloadProjectImages: 0 ,
82+ headingLevel: ' h2'
83+ })
84+
85+ const dialogEl = ref <HTMLDialogElement | null >(null )
86+ const activeProject = ref <Project | null >(null )
87+
88+ function openLightbox(project : Project ): void {
89+ activeProject .value = project
90+ nextTick (() => {
91+ dialogEl .value ?.showModal ()
92+ })
93+ }
94+
95+ function closeLightbox(): void {
96+ dialogEl .value ?.close ()
97+ }
98+
99+ onMounted (() => {
100+ dialogEl .value ?.addEventListener (' close' , () => {
101+ activeProject .value = null
102+ })
64103})
65104
66105const projects: Project [] = [{
@@ -248,14 +287,16 @@ $row-padding-h: 1rem;
248287
249288.lightbox {
250289 flex-shrink : 0 ;
251-
252- & __toggle { display : none ; }
253290
254291 & __trigger {
255292 display : block ;
256293 width : 8.875rem ;
257294 height : 5rem ;
258295 cursor : zoom-in ;
296+ padding : 0 ;
297+ background : none ;
298+ border : none ;
299+
259300 @media (min-width : $responsive-standard-tablet ) { width : 11.125rem ; height : 6.25rem ; }
260301
261302 .thumb {
@@ -264,39 +305,55 @@ $row-padding-h: 1rem;
264305 object-fit : cover ;
265306 border-radius : 0.25rem ;
266307 border : 1px solid var (--color-fg1 );
267- transition : transform 0.2s ease , opacity 0.3s ease , filter 0.3s ease ;
268- & :hover { transform : scale (1.02 ); }
308+
309+ @media (prefers-reduced-motion : no- preference) {
310+ transition : transform 0.2s ease , opacity 0.3s ease , filter 0.3s ease ;
311+ & :hover { transform : scale (1.02 ); }
312+ }
269313 }
270314 }
315+ }
271316
272- & __overlay {
273- position : fixed ;
274- inset : 0 ;
275- background : rgba (0 , 0 , 0 , 0.92 );
276- display : flex ;
277- align-items : center ;
278- justify-content : center ;
279- z-index : 10000 ;
280- cursor : zoom-out ;
281- opacity : 0 ;
282- pointer-events : none ;
283- transition : opacity 0.2s ease ;
284- padding : 1.5rem ;
285-
286- .full-view {
287- max-width : 100% ;
288- max-height : 100% ;
289- border-radius : 0.25rem ;
290- transform : scale (0.98 );
291- transition : transform 0.2s ease ;
292- filter : none !important ;
317+ .lightbox-dialog {
318+ padding : 0 ;
319+ background : rgba (0 , 0 , 0 , 0.92 );
320+ border : none ;
321+ border-radius : 0.5rem ;
322+ max-width : min (90vw , 72rem );
323+ max-height : 90vh ;
324+ width : fit-content ;
325+
326+ & ::backdrop {
327+ background : rgba (0 , 0 , 0 , 0.75 );
328+ }
329+
330+ & __close {
331+ position : absolute ;
332+ top : 0.75rem ;
333+ right : 0.75rem ;
334+ background : rgba (255 , 255 , 255 , 0.15 );
335+ border : 1px solid rgba (255 , 255 , 255 , 0.4 );
336+ color : #fff ;
337+ border-radius : 50% ;
338+ width : 2.25rem ;
339+ height : 2.25rem ;
340+ font-size : 1.5rem ;
341+ line-height : 1 ;
342+ cursor : pointer ;
343+ display : grid ;
344+ place-items : center ;
345+
346+ & :hover ,
347+ & :focus {
348+ background : rgba (255 , 255 , 255 , 0.3 );
293349 }
294350 }
295351
296- & __toggle :checked ~ & __overlay {
297- opacity : 1 ;
298- pointer-events : auto ;
299- .full-view { transform : scale (1 ); }
352+ & __img {
353+ display : block ;
354+ max-width : 100% ;
355+ max-height : 90vh ;
356+ border-radius : 0.5rem ;
300357 }
301358}
302- </style >
359+ </style >
0 commit comments