feat(web): improve alt text (#7596)

* alt text

* memory lane alt text

* revert sql generator change

* use getAltText

* oops

* handle large number of people in asset

* nit

* add aria-label to search button

* update api

* fixed tests

* fixed typing

* fixed spacing

* fix displaying null
This commit is contained in:
Mert 2024-03-03 16:42:17 -05:00 committed by GitHub
parent 07c926bb12
commit 2fa10a254c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 200 additions and 54 deletions

View file

@ -47,11 +47,11 @@ describe('AlbumCard component', () => {
const detailsText = `${count} items` + (shared ? ' . Shared' : '');
expect(albumImgElement).toHaveAttribute('src');
expect(albumImgElement).toHaveAttribute('alt', album.id);
expect(albumImgElement).toHaveAttribute('alt', album.albumName);
await waitFor(() => expect(albumImgElement).toHaveAttribute('src'));
expect(albumImgElement).toHaveAttribute('alt', album.id);
expect(albumImgElement).toHaveAttribute('alt', album.albumName);
expect(sdkMock.getAssetThumbnail).not.toHaveBeenCalled();
expect(albumNameElement).toHaveTextContent(album.albumName);
@ -74,11 +74,11 @@ describe('AlbumCard component', () => {
const albumImgElement = sut.getByTestId('album-image');
const albumNameElement = sut.getByTestId('album-name');
const albumDetailsElement = sut.getByTestId('album-details');
expect(albumImgElement).toHaveAttribute('alt', album.id);
expect(albumImgElement).toHaveAttribute('alt', album.albumName);
await waitFor(() => expect(albumImgElement).toHaveAttribute('src', thumbnailUrl));
expect(albumImgElement).toHaveAttribute('alt', album.id);
expect(albumImgElement).toHaveAttribute('alt', album.albumName);
expect(sdkMock.getAssetThumbnail).toHaveBeenCalledTimes(1);
expect(sdkMock.getAssetThumbnail).toHaveBeenCalledWith({
id: 'thumbnailIdOne',

View file

@ -72,7 +72,7 @@
<img
loading={preload ? 'eager' : 'lazy'}
src={imageData}
alt={album.id}
alt={album.albumName}
class="z-0 h-full w-full rounded-xl object-cover transition-all duration-300 hover:shadow-lg"
data-testid="album-image"
draggable="false"
@ -82,7 +82,7 @@
loading={preload ? 'eager' : 'lazy'}
src="$lib/assets/no-thumbnail.png"
sizes="min(271px,186px)"
alt={album.id}
alt={album.albumName}
class="z-0 h-full w-full rounded-xl object-cover transition-all duration-300 hover:shadow-lg"
data-testid="album-image"
draggable="false"

View file

@ -195,7 +195,7 @@
<img
class="rounded-lg w-[75px] h-[75px] object-cover"
src={getAssetThumbnailUrl(reaction.assetId, ThumbnailFormat.Webp)}
alt="comment-thumbnail"
alt="Profile picture of {reaction.user.name}, who commented on this asset"
/>
</div>
{/if}
@ -241,7 +241,7 @@
<img
class="rounded-lg w-[75px] h-[75px] object-cover"
src={getAssetThumbnailUrl(reaction.assetId, ThumbnailFormat.Webp)}
alt="like-thumbnail"
alt="Profile picture of {reaction.user.name}, who liked this asset"
/>
</div>
{/if}

View file

@ -616,7 +616,16 @@
{:then component}
<svelte:component
this={component.default}
mapMarkers={[{ lat: latlng.lat, lon: latlng.lng, id: asset.id }]}
mapMarkers={[
{
lat: latlng.lat,
lon: latlng.lng,
id: asset.id,
city: asset.exifInfo?.city ?? null,
state: asset.exifInfo?.state ?? null,
country: asset.exifInfo?.country ?? null,
},
]}
center={latlng}
zoom={15}
simplified

View file

@ -13,6 +13,7 @@
import { fade } from 'svelte/transition';
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
import { NotificationType, notificationController } from '../shared-components/notification/notification';
import { getAltText } from '$lib/utils/thumbnail-util';
export let asset: AssetResponseDto;
export let element: HTMLDivElement | undefined = undefined;
@ -133,7 +134,7 @@
bind:this={$photoViewer}
transition:fade={{ duration: haveFadeTransition ? 150 : 0 }}
src={assetData}
alt={asset.id}
alt={getAltText(asset)}
class="h-full w-full object-contain"
draggable="false"
/>

View file

@ -8,7 +8,7 @@
import Icon from '$lib/components/elements/icon.svelte';
export let url: string;
export let altText: string;
export let altText: string | undefined;
export let title: string | null = null;
export let heightStyle: string | undefined = undefined;
export let widthStyle: string;

View file

@ -3,6 +3,7 @@
import Icon from '$lib/components/elements/icon.svelte';
import { ProjectionType } from '$lib/constants';
import { getAssetFileUrl, getAssetThumbnailUrl, isSharedLink } from '$lib/utils';
import { getAltText } from '$lib/utils/thumbnail-util';
import { timeToSeconds } from '$lib/utils/date-time';
import { AssetTypeEnum, ThumbnailFormat, type AssetResponseDto } from '@immich/sdk';
import {
@ -177,7 +178,7 @@
{#if asset.resized}
<ImageThumbnail
url={getAssetThumbnailUrl(asset.id, format)}
altText={asset.originalFileName}
altText={getAltText(asset)}
widthStyle="{width}px"
heightStyle="{height}px"
thumbhash={asset.thumbhash}

View file

@ -223,14 +223,14 @@
url={selectedPersonToCreate[index] ||
getPeopleThumbnailUrl(selectedPersonToReassign[index]?.id || face.person.id)}
altText={selectedPersonToReassign[index]
? selectedPersonToReassign[index]?.name || ''
? selectedPersonToReassign[index]?.name
: selectedPersonToCreate[index]
? 'new person'
? 'New person'
: getPersonNameWithHiddenValue(face.person?.name, face.person?.isHidden)}
title={selectedPersonToReassign[index]
? selectedPersonToReassign[index]?.name || ''
? selectedPersonToReassign[index]?.name
: selectedPersonToCreate[index]
? 'new person'
? 'New person'
: getPersonNameWithHiddenValue(face.person?.name, face.person?.isHidden)}
widthStyle="90px"
heightStyle="90px"

View file

@ -171,7 +171,7 @@
<img
class="h-full w-full rounded-2xl object-cover"
src={getAssetThumbnailUrl(previousMemory.assets[0].id, ThumbnailFormat.Jpeg)}
alt=""
alt="Previous memory"
draggable="false"
/>
{:else}
@ -179,7 +179,7 @@
class="h-full w-full rounded-2xl object-cover"
src="$lib/assets/no-thumbnail.png"
sizes="min(271px,186px)"
alt=""
alt="Previous memory"
draggable="false"
/>
{/if}
@ -203,7 +203,7 @@
transition:fade
class="h-full w-full rounded-2xl object-contain transition-all"
src={getAssetThumbnailUrl(currentAsset.id, ThumbnailFormat.Jpeg)}
alt=""
alt={currentAsset.exifInfo?.description}
draggable="false"
/>
{/key}
@ -244,7 +244,7 @@
<img
class="h-full w-full rounded-2xl object-cover"
src={getAssetThumbnailUrl(nextMemory.assets[0].id, ThumbnailFormat.Jpeg)}
alt=""
alt="Next memory"
draggable="false"
/>
{:else}
@ -252,7 +252,7 @@
class="h-full w-full rounded-2xl object-cover"
src="$lib/assets/no-thumbnail.png"
sizes="min(271px,186px)"
alt=""
alt="Next memory"
draggable="false"
/>
{/if}

View file

@ -4,6 +4,7 @@
import { AppRoute, QueryParameter } from '$lib/constants';
import { memoryStore } from '$lib/stores/memory.store';
import { getAssetThumbnailUrl } from '$lib/utils';
import { getAltText } from '$lib/utils/thumbnail-util';
import { ThumbnailFormat, getMemoryLane } from '@immich/sdk';
import { mdiChevronLeft, mdiChevronRight } from '@mdi/js';
import { onMount } from 'svelte';
@ -64,7 +65,6 @@
{/if}
</div>
{/if}
<div class="inline-block" bind:offsetWidth={innerWidth}>
{#each $memoryStore as memory, index (memory.title)}
<button
@ -74,7 +74,7 @@
<img
class="h-full w-full rounded-xl object-cover"
src={getAssetThumbnailUrl(memory.assets[0].id, ThumbnailFormat.Jpeg)}
alt={memory.title}
alt={`Memory Lane ${getAltText(memory.assets[0])}`}
draggable="false"
/>
<p class="absolute bottom-2 left-4 z-10 text-lg text-white">{memory.title}</p>

View file

@ -192,7 +192,18 @@
{:then component}
<svelte:component
this={component.default}
mapMarkers={lat && lng && asset ? [{ id: asset.id, lat, lon: lng }] : []}
mapMarkers={lat && lng && asset
? [
{
id: asset.id,
lat,
lon: lng,
city: asset.exifInfo?.city ?? null,
state: asset.exifInfo?.state ?? null,
country: asset.exifInfo?.country ?? null,
},
]
: []}
{zoom}
bind:addClipMapMarker
center={lat && lng ? { lat, lng } : undefined}

View file

@ -88,7 +88,7 @@
}
}
type FeaturePoint = Feature<Point, { id: string }>;
type FeaturePoint = Feature<Point, { id: string; city: string | null; state: string | null; country: string | null }>;
const asFeature = (marker: MapMarkerResponseDto): FeaturePoint => {
return {
@ -96,6 +96,9 @@
geometry: { type: 'Point', coordinates: [marker.lon, marker.lat] },
properties: {
id: marker.id,
city: marker.city,
state: marker.state,
country: marker.country,
},
};
};
@ -107,6 +110,9 @@
lat: coords.lat,
lon: coords.lng,
id: featurePoint.properties.id,
city: featurePoint.properties.city,
state: featurePoint.properties.state,
country: featurePoint.properties.country,
};
};
</script>
@ -178,7 +184,9 @@
<img
src={getAssetThumbnailUrl(feature.properties?.id, undefined)}
class="rounded-full w-[60px] h-[60px] border-2 border-immich-primary shadow-lg hover:border-immich-dark-primary transition-all duration-200 hover:scale-150 object-cover bg-immich-primary"
alt={`Image with id ${feature.properties?.id}`}
alt={feature.properties?.city && feature.properties.country
? `Map marker for images taken in ${feature.properties.city}, ${feature.properties.country}`
: 'Map marker with image'}
/>
{/if}
{#if $$slots.popup}

View file

@ -97,7 +97,7 @@
<div class="absolute inset-y-0 left-0 flex items-center pl-6">
<div class="dark:text-immich-dark-fg/75">
<button class="flex items-center">
<Icon path={mdiMagnify} size="1.5em" />
<Icon ariaLabel="search" path={mdiMagnify} size="1.5em" />
</button>
</div>
</div>
@ -134,7 +134,7 @@
type="reset"
class="rounded-full p-2 hover:bg-immich-primary/5 active:bg-immich-primary/10 dark:text-immich-dark-fg/75 dark:hover:bg-immich-dark-primary/25 dark:active:bg-immich-dark-primary/[.35]"
>
<Icon path={mdiClose} size="1.5em" />
<Icon ariaLabel="clear" path={mdiClose} size="1.5em" />
</button>
</div>
{/if}