fix(web): shared link expiration date accessibility (#12060)

- use native select - shows focus, automatically has keyboard
  navigation, accessible for screen readers
- remove DropdownButton component
- fix dropdown styling in Safari
This commit is contained in:
Ben 2024-08-26 21:05:23 -04:00 committed by GitHub
parent 6b6d2a6621
commit 9894b9513b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 46 additions and 113 deletions

View File

@ -8,7 +8,6 @@
import { SharedLinkType, createSharedLink, updateSharedLink, type SharedLinkResponseDto } from '@immich/sdk';
import { mdiContentCopy, mdiLink } from '@mdi/js';
import { createEventDispatcher } from 'svelte';
import DropdownButton, { type DropDownOption } from '../dropdown-button.svelte';
import { NotificationType, notificationController } from '../notification/notification';
import SettingInputField, { SettingInputFieldType } from '../settings/setting-input-field.svelte';
import SettingSwitch from '../settings/setting-switch.svelte';
@ -16,6 +15,7 @@
import { t } from 'svelte-i18n';
import { locale } from '$lib/stores/preferences.store';
import { DateTime, Duration } from 'luxon';
import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
export let onClose: () => void;
export let albumId: string | undefined = undefined;
@ -27,7 +27,7 @@
let allowDownload = true;
let allowUpload = false;
let showMetadata = true;
let expirationOption: DropDownOption<number> | undefined;
let expirationOption: number = 0;
let password = '';
let shouldChangeExpirationTime = false;
let enablePassword = false;
@ -48,14 +48,12 @@
];
$: relativeTime = new Intl.RelativeTimeFormat($locale);
$: expiredDateOption = [
{ label: $t('never'), value: 0 },
...expirationOptions.map(
([value, unit]): DropDownOption<number> => ({
label: relativeTime.format(value, unit),
value: Duration.fromObject({ [unit]: value }).toMillis(),
}),
),
$: expiredDateOptions = [
{ text: $t('never'), value: 0 },
...expirationOptions.map(([value, unit]) => ({
text: relativeTime.format(value, unit),
value: Duration.fromObject({ [unit]: value }).toMillis(),
})),
];
$: shareType = albumId ? SharedLinkType.Album : SharedLinkType.Individual;
@ -82,8 +80,7 @@
}
const handleCreateSharedLink = async () => {
const expirationDate =
expirationOption && expirationOption.value > 0 ? DateTime.now().plus(expirationOption.value).toISO() : undefined;
const expirationDate = expirationOption > 0 ? DateTime.now().plus(expirationOption).toISO() : undefined;
try {
const data = await createSharedLink({
@ -112,8 +109,7 @@
}
try {
const expirationDate =
expirationOption && expirationOption.value > 0 ? DateTime.now().plus(expirationOption.value).toISO() : null;
const expirationDate = expirationOption > 0 ? DateTime.now().plus(expirationOption).toISO() : null;
await updateSharedLink({
id: editingLink.id,
@ -212,19 +208,18 @@
<SettingSwitch bind:checked={allowUpload} title={$t('allow_public_user_to_upload')} />
</div>
<div class="text-sm">
{#if editingLink}
<p class="immich-form-label my-2">
<SettingSwitch bind:checked={shouldChangeExpirationTime} title={$t('change_expiration_time')} />
</p>
{:else}
<p class="immich-form-label my-2">{$t('expire_after')}</p>
{/if}
<DropdownButton
options={expiredDateOption}
bind:selected={expirationOption}
{#if editingLink}
<div class="my-3">
<SettingSwitch bind:checked={shouldChangeExpirationTime} title={$t('change_expiration_time')} />
</div>
{/if}
<div class="mt-3">
<SettingSelect
bind:value={expirationOption}
options={expiredDateOptions}
label={$t('expire_after')}
disabled={editingLink && !shouldChangeExpirationTime}
number={true}
/>
</div>
</div>

View File

@ -1,74 +0,0 @@
<script lang="ts" context="module">
export type DropDownOption<T = unknown> = {
label: string;
value: T;
};
</script>
<script lang="ts">
export let options: DropDownOption[];
export let selected = options.at(0);
export let disabled = false;
export let isOpen = false;
const toggle = () => (isOpen = !isOpen);
</script>
<div id="immich-dropdown" class="relative">
<button
type="button"
{disabled}
on:click={toggle}
aria-expanded={isOpen}
class="flex w-full place-items-center justify-between rounded-lg bg-gray-200 p-2 disabled:cursor-not-allowed disabled:bg-gray-600 dark:bg-gray-600 dark:disabled:bg-gray-300"
>
{#if selected}
<div>
{selected.label}
</div>
{/if}
<div>
<svg
style="tran"
width="20"
height="20"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path d="M19 9l-7 7-7-7" />
</svg>
</div>
</button>
{#if isOpen}
<div class="absolute mt-2 flex w-full flex-col">
{#each options as option}
<button
type="button"
on:click={() => {
selected = option;
isOpen = false;
}}
class="flex w-full bg-gray-200 p-2 transition-all hover:bg-gray-300 dark:bg-gray-500 dark:hover:bg-gray-700"
>
{option.label}
</button>
{/each}
</div>
{/if}
</div>
<style>
svg {
transition: transform 0.2s ease-in;
}
[aria-expanded='true'] svg {
transform: rotate(0.5turn);
}
</style>

View File

@ -3,6 +3,8 @@
import { fly } from 'svelte/transition';
import { createEventDispatcher } from 'svelte';
import { t } from 'svelte-i18n';
import Icon from '$lib/components/elements/icon.svelte';
import { mdiChevronDown } from '@mdi/js';
export let value: string | number;
export let options: { value: string | number; text: string }[];
@ -46,17 +48,27 @@
</p>
{/if}
<select
class="immich-form-input w-full pb-2"
{disabled}
aria-describedby={desc ? `${name}-desc` : undefined}
{name}
id="{name}-select"
bind:value
on:change={handleChange}
>
{#each options as option}
<option value={option.value}>{option.text}</option>
{/each}
</select>
<div class="grid">
<Icon
path={mdiChevronDown}
size={'1.2em'}
ariaHidden={true}
class="pointer-events-none right-1 relative col-start-1 row-start-1 self-center justify-self-end {disabled
? 'text-immich-bg'
: 'text-immich-fg dark:text-immich-bg'}"
/>
<select
class="immich-form-input w-full appearance-none row-start-1 col-start-1 !pr-6"
{disabled}
aria-describedby={desc ? `${name}-desc` : undefined}
{name}
id="{name}-select"
bind:value
on:change={handleChange}
>
{#each options as option}
<option value={option.value}>{option.text}</option>
{/each}
</select>
</div>
</div>