Add explicit public image loading and unavailable states, accessible selected-thumbnail state, and Storybook coverage.
This commit is contained in:
@@ -24,3 +24,10 @@ export const BrokenImageFallback = {
|
|||||||
images: [{ id: 'broken', src: '/fixtures/missing-image.webp', alt: 'Unavailable collectible image' }],
|
images: [{ id: 'broken', src: '/fixtures/missing-image.webp', alt: 'Unavailable collectible image' }],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const LoadingImage = {
|
||||||
|
args: {
|
||||||
|
images: [{ id: 'loading', src: '/fixtures/slow-image.webp', alt: 'Collectible image loading' }],
|
||||||
|
},
|
||||||
|
parameters: { chromatic: { delay: 1000 } },
|
||||||
|
};
|
||||||
@@ -1,25 +1,47 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
images: { type: Array, required: true },
|
images: { type: Array, required: true },
|
||||||
});
|
});
|
||||||
const selectedIndex = ref(0);
|
const selectedIndex = ref(0);
|
||||||
const failedImages = ref(new Set());
|
const failedImages = ref(new Set());
|
||||||
|
const loadingImages = ref(new Set());
|
||||||
|
|
||||||
|
function imageKey(image) {
|
||||||
|
return image?.id ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function markLoading(image) {
|
||||||
|
if (!image?.src && !image?.url) return;
|
||||||
|
loadingImages.value = new Set(loadingImages.value).add(imageKey(image));
|
||||||
|
}
|
||||||
|
|
||||||
function markFailed(imageId) {
|
function markFailed(imageId) {
|
||||||
failedImages.value = new Set(failedImages.value).add(imageId);
|
failedImages.value = new Set(failedImages.value).add(imageId);
|
||||||
|
const nextLoadingImages = new Set(loadingImages.value);
|
||||||
|
nextLoadingImages.delete(imageId);
|
||||||
|
loadingImages.value = nextLoadingImages;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function markLoaded(imageId) {
|
||||||
|
const nextLoadingImages = new Set(loadingImages.value);
|
||||||
|
nextLoadingImages.delete(imageId);
|
||||||
|
loadingImages.value = nextLoadingImages;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => markLoading(props.images[selectedIndex.value]));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="image-gallery">
|
<div class="image-gallery">
|
||||||
<div class="image-gallery__stage">
|
<div class="image-gallery__stage">
|
||||||
<img v-if="(props.images[selectedIndex]?.src ?? props.images[selectedIndex]?.url) && !failedImages.has(props.images[selectedIndex].id)" :src="props.images[selectedIndex].src ?? props.images[selectedIndex].url" :alt="props.images[selectedIndex].altText ?? props.images[selectedIndex].alt" @error="markFailed(props.images[selectedIndex].id)" />
|
<img v-if="(props.images[selectedIndex]?.src ?? props.images[selectedIndex]?.url) && !failedImages.has(props.images[selectedIndex].id)" :src="props.images[selectedIndex].src ?? props.images[selectedIndex].url" :alt="props.images[selectedIndex].altText ?? props.images[selectedIndex].alt" @load="markLoaded(props.images[selectedIndex].id)" @error="markFailed(props.images[selectedIndex].id)" />
|
||||||
<span v-else class="image-gallery__placeholder" role="img" :aria-label="props.images[selectedIndex]?.src ? 'Collectible image unavailable' : 'No collectible image available'">{{ props.images[selectedIndex]?.src ? 'IMAGE UNAVAILABLE' : (props.images[selectedIndex]?.label ?? 'IMAGE') }}</span>
|
<span v-if="loadingImages.has(imageKey(props.images[selectedIndex]))" class="image-gallery__placeholder" role="status" aria-label="Loading collectible image">Loading image...</span>
|
||||||
|
<span v-else-if="!((props.images[selectedIndex]?.src ?? props.images[selectedIndex]?.url) && !failedImages.has(props.images[selectedIndex].id))" class="image-gallery__placeholder" role="img" :aria-label="props.images[selectedIndex]?.src ? 'Collectible image unavailable' : 'No collectible image available'">{{ props.images[selectedIndex]?.src ? 'IMAGE UNAVAILABLE' : (props.images[selectedIndex]?.label ?? 'IMAGE') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="props.images.length > 1" class="image-gallery__controls" aria-label="Gallery images">
|
<div v-if="props.images.length > 1" class="image-gallery__controls" aria-label="Gallery images">
|
||||||
<button v-for="(image, index) in props.images" :key="image.id" :class="{ 'image-gallery__thumb--selected': index === selectedIndex }" type="button" :aria-label="`Show image ${index + 1}: ${image.alt}`" @click="selectedIndex = index">
|
<button v-for="(image, index) in props.images" :key="image.id" :class="{ 'image-gallery__thumb--selected': index === selectedIndex }" type="button" :aria-label="`Show image ${index + 1}: ${image.alt}`" :aria-current="index === selectedIndex ? 'true' : undefined" @click="selectedIndex = index; markLoading(image)">
|
||||||
{{ index + 1 }}
|
{{ index + 1 }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user