mirror of
https://github.com/immich-app/immich.git
synced 2025-01-06 03:46:47 +01:00
fix(web): improve focus and shortcuts (#7983)
* fix(web): improve focus and shortcuts * fix shiftKeyIsDown
This commit is contained in:
parent
a46366d336
commit
029dd99ae0
7 changed files with 31 additions and 34 deletions
|
@ -35,8 +35,6 @@
|
||||||
<tr
|
<tr
|
||||||
class="flex h-[50px] w-full place-items-center border-[3px] border-transparent p-2 text-center odd:bg-immich-gray even:bg-immich-bg hover:cursor-pointer hover:border-immich-primary/75 odd:dark:bg-immich-dark-gray/75 even:dark:bg-immich-dark-gray/50 dark:hover:border-immich-dark-primary/75 md:p-5"
|
class="flex h-[50px] w-full place-items-center border-[3px] border-transparent p-2 text-center odd:bg-immich-gray even:bg-immich-bg hover:cursor-pointer hover:border-immich-primary/75 odd:dark:bg-immich-dark-gray/75 even:dark:bg-immich-dark-gray/50 dark:hover:border-immich-dark-primary/75 md:p-5"
|
||||||
on:click={() => goto(`${AppRoute.ALBUMS}/${album.id}`)}
|
on:click={() => goto(`${AppRoute.ALBUMS}/${album.id}`)}
|
||||||
on:keydown={(event) => event.key === 'Enter' && goto(`${AppRoute.ALBUMS}/${album.id}`)}
|
|
||||||
tabindex="0"
|
|
||||||
>
|
>
|
||||||
<a data-sveltekit-preload-data="hover" class="flex w-full" href="{AppRoute.ALBUMS}/{album.id}">
|
<a data-sveltekit-preload-data="hover" class="flex w-full" href="{AppRoute.ALBUMS}/{album.id}">
|
||||||
<td class="text-md text-ellipsis text-left w-8/12 sm:w-4/12 md:w-4/12 xl:w-[30%] 2xl:w-[40%]"
|
<td class="text-md text-ellipsis text-left w-8/12 sm:w-4/12 md:w-4/12 xl:w-[30%] 2xl:w-[40%]"
|
||||||
|
|
|
@ -45,7 +45,7 @@
|
||||||
<!-- Image grid -->
|
<!-- Image grid -->
|
||||||
<div class="flex flex-wrap gap-[2px]">
|
<div class="flex flex-wrap gap-[2px]">
|
||||||
{#each album.assets as asset (asset.id)}
|
{#each album.assets as asset (asset.id)}
|
||||||
<Thumbnail {asset} on:click={() => (selectedThumbnail = asset)} selected={isSelected(asset.id)} />
|
<Thumbnail {asset} onClick={() => (selectedThumbnail = asset)} selected={isSelected(asset.id)} />
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
@ -646,7 +646,7 @@
|
||||||
? 'bg-transparent border-2 border-white'
|
? 'bg-transparent border-2 border-white'
|
||||||
: 'bg-gray-700/40'} inline-block hover:bg-transparent"
|
: 'bg-gray-700/40'} inline-block hover:bg-transparent"
|
||||||
asset={stackedAsset}
|
asset={stackedAsset}
|
||||||
on:click={() => {
|
onClick={() => {
|
||||||
asset = stackedAsset;
|
asset = stackedAsset;
|
||||||
preloadAssets = index + 1 >= $stackAssetsStore.length ? [] : [$stackAssetsStore[index + 1]];
|
preloadAssets = index + 1 >= $stackAssetsStore.length ? [] : [$stackAssetsStore[index + 1]];
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -21,9 +21,9 @@
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
import ImageThumbnail from './image-thumbnail.svelte';
|
import ImageThumbnail from './image-thumbnail.svelte';
|
||||||
import VideoThumbnail from './video-thumbnail.svelte';
|
import VideoThumbnail from './video-thumbnail.svelte';
|
||||||
|
import { shortcut } from '$lib/utils/shortcut';
|
||||||
|
|
||||||
const dispatch = createEventDispatcher<{
|
const dispatch = createEventDispatcher<{
|
||||||
click: { asset: AssetResponseDto };
|
|
||||||
select: { asset: AssetResponseDto };
|
select: { asset: AssetResponseDto };
|
||||||
'mouse-event': { isMouseOver: boolean; selectedGroupIndex: number };
|
'mouse-event': { isMouseOver: boolean; selectedGroupIndex: number };
|
||||||
}>();
|
}>();
|
||||||
|
@ -40,12 +40,13 @@
|
||||||
export let readonly = false;
|
export let readonly = false;
|
||||||
export let showArchiveIcon = false;
|
export let showArchiveIcon = false;
|
||||||
export let showStackedIcon = true;
|
export let showStackedIcon = true;
|
||||||
export let intersecting = false;
|
export let onClick: ((asset: AssetResponseDto) => void) | undefined = undefined;
|
||||||
|
|
||||||
let className = '';
|
let className = '';
|
||||||
export { className as class };
|
export { className as class };
|
||||||
|
|
||||||
let mouseOver = false;
|
let mouseOver = false;
|
||||||
|
$: clickable = !disabled && onClick;
|
||||||
|
|
||||||
$: dispatch('mouse-event', { isMouseOver: mouseOver, selectedGroupIndex: groupIndex });
|
$: dispatch('mouse-event', { isMouseOver: mouseOver, selectedGroupIndex: groupIndex });
|
||||||
|
|
||||||
|
@ -62,14 +63,8 @@
|
||||||
})();
|
})();
|
||||||
|
|
||||||
const thumbnailClickedHandler = () => {
|
const thumbnailClickedHandler = () => {
|
||||||
if (!disabled) {
|
if (clickable) {
|
||||||
dispatch('click', { asset });
|
onClick?.(asset);
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const thumbnailKeyDownHandler = (e: KeyboardEvent) => {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
thumbnailClickedHandler();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -89,20 +84,22 @@
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<IntersectionObserver once={false} on:intersected bind:intersecting>
|
<IntersectionObserver once={false} on:intersected let:intersecting>
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
||||||
<div
|
<div
|
||||||
style:width="{width}px"
|
style:width="{width}px"
|
||||||
style:height="{height}px"
|
style:height="{height}px"
|
||||||
class="group relative overflow-hidden {disabled
|
class="group focus-visible:outline-none relative overflow-hidden {disabled
|
||||||
? 'bg-gray-300'
|
? 'bg-gray-300'
|
||||||
: 'bg-immich-primary/20 dark:bg-immich-dark-primary/20'}"
|
: 'bg-immich-primary/20 dark:bg-immich-dark-primary/20'}"
|
||||||
class:cursor-not-allowed={disabled}
|
class:cursor-not-allowed={disabled}
|
||||||
class:hover:cursor-pointer={!disabled}
|
class:hover:cursor-pointer={clickable}
|
||||||
on:mouseenter={onMouseEnter}
|
on:mouseenter={onMouseEnter}
|
||||||
on:mouseleave={onMouseLeave}
|
on:mouseleave={onMouseLeave}
|
||||||
|
role={clickable ? 'button' : undefined}
|
||||||
|
tabindex={clickable ? 0 : undefined}
|
||||||
on:click={thumbnailClickedHandler}
|
on:click={thumbnailClickedHandler}
|
||||||
on:keydown={thumbnailKeyDownHandler}
|
use:shortcut={{ shortcut: { key: 'Enter' }, onShortcut: thumbnailClickedHandler }}
|
||||||
>
|
>
|
||||||
{#if intersecting}
|
{#if intersecting}
|
||||||
<div class="absolute z-20 h-full w-full {className}">
|
<div class="absolute z-20 h-full w-full {className}">
|
||||||
|
@ -140,6 +137,11 @@
|
||||||
class:rounded-xl={selected}
|
class:rounded-xl={selected}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- Outline on focus -->
|
||||||
|
<div
|
||||||
|
class="absolute size-full group-focus-visible:outline outline-4 -outline-offset-4 outline-immich-primary"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- Favorite asset star -->
|
<!-- Favorite asset star -->
|
||||||
{#if !isSharedLink() && asset.isFavorite}
|
{#if !isSharedLink() && asset.isFavorite}
|
||||||
<div class="absolute bottom-2 left-2 z-10">
|
<div class="absolute bottom-2 left-2 z-10">
|
||||||
|
|
|
@ -178,7 +178,7 @@
|
||||||
{showArchiveIcon}
|
{showArchiveIcon}
|
||||||
{asset}
|
{asset}
|
||||||
{groupIndex}
|
{groupIndex}
|
||||||
on:click={() => assetClickHandler(asset, groupAssets, groupTitle)}
|
onClick={() => assetClickHandler(asset, groupAssets, groupTitle)}
|
||||||
on:select={() => assetSelectHandler(asset, groupAssets, groupTitle)}
|
on:select={() => assetSelectHandler(asset, groupAssets, groupTitle)}
|
||||||
on:mouse-event={() => assetMouseEventHandler(groupTitle, asset)}
|
on:mouse-event={() => assetMouseEventHandler(groupTitle, asset)}
|
||||||
selected={$selectedAssets.has(asset) || $assetStore.albumAssets.has(asset.id)}
|
selected={$selectedAssets.has(asset) || $assetStore.albumAssets.has(asset.id)}
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
import { isSearchEnabled } from '$lib/stores/search.store';
|
import { isSearchEnabled } from '$lib/stores/search.store';
|
||||||
import { featureFlags } from '$lib/stores/server-config.store';
|
import { featureFlags } from '$lib/stores/server-config.store';
|
||||||
import { deleteAssets } from '$lib/utils/actions';
|
import { deleteAssets } from '$lib/utils/actions';
|
||||||
import { shortcuts, type ShortcutOptions } from '$lib/utils/shortcut';
|
import { shortcuts, type ShortcutOptions, matchesShortcut } from '$lib/utils/shortcut';
|
||||||
import { formatGroupTitle, splitBucketIntoDateGroups } from '$lib/utils/timeline-util';
|
import { formatGroupTitle, splitBucketIntoDateGroups } from '$lib/utils/timeline-util';
|
||||||
import type { AlbumResponseDto, AssetResponseDto } from '@immich/sdk';
|
import type { AlbumResponseDto, AssetResponseDto } from '@immich/sdk';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
|
@ -202,24 +202,24 @@
|
||||||
|
|
||||||
let shiftKeyIsDown = false;
|
let shiftKeyIsDown = false;
|
||||||
|
|
||||||
const onKeyDown = (e: KeyboardEvent) => {
|
const onKeyDown = (event: KeyboardEvent) => {
|
||||||
if ($isSearchEnabled) {
|
if ($isSearchEnabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.key == 'Shift') {
|
if (matchesShortcut(event, { key: 'Shift' })) {
|
||||||
e.preventDefault();
|
event.preventDefault();
|
||||||
shiftKeyIsDown = true;
|
shiftKeyIsDown = true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onKeyUp = (e: KeyboardEvent) => {
|
const onKeyUp = (event: KeyboardEvent) => {
|
||||||
if ($isSearchEnabled) {
|
if ($isSearchEnabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.key == 'Shift') {
|
if (matchesShortcut(event, { key: 'Shift' })) {
|
||||||
e.preventDefault();
|
event.preventDefault();
|
||||||
shiftKeyIsDown = false;
|
shiftKeyIsDown = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -26,17 +26,14 @@
|
||||||
let currentViewAssetIndex = 0;
|
let currentViewAssetIndex = 0;
|
||||||
$: isMultiSelectionMode = selectedAssets.size > 0;
|
$: isMultiSelectionMode = selectedAssets.size > 0;
|
||||||
|
|
||||||
const viewAssetHandler = (event: CustomEvent) => {
|
const viewAssetHandler = (asset: AssetResponseDto) => {
|
||||||
const { asset }: { asset: AssetResponseDto } = event.detail;
|
|
||||||
|
|
||||||
currentViewAssetIndex = assets.findIndex((a) => a.id == asset.id);
|
currentViewAssetIndex = assets.findIndex((a) => a.id == asset.id);
|
||||||
selectedAsset = assets[currentViewAssetIndex];
|
selectedAsset = assets[currentViewAssetIndex];
|
||||||
$showAssetViewer = true;
|
$showAssetViewer = true;
|
||||||
updateAssetState(selectedAsset.id, false);
|
updateAssetState(selectedAsset.id, false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectAssetHandler = (event: CustomEvent) => {
|
const selectAssetHandler = (asset: AssetResponseDto) => {
|
||||||
const { asset }: { asset: AssetResponseDto } = event.detail;
|
|
||||||
let temporary = new Set(selectedAssets);
|
let temporary = new Set(selectedAssets);
|
||||||
|
|
||||||
if (selectedAssets.has(asset)) {
|
if (selectedAssets.has(asset)) {
|
||||||
|
@ -123,8 +120,8 @@
|
||||||
<Thumbnail
|
<Thumbnail
|
||||||
{asset}
|
{asset}
|
||||||
readonly={disableAssetSelect}
|
readonly={disableAssetSelect}
|
||||||
on:click={(e) => (isMultiSelectionMode ? selectAssetHandler(e) : viewAssetHandler(e))}
|
onClick={(e) => (isMultiSelectionMode ? selectAssetHandler(e) : viewAssetHandler(e))}
|
||||||
on:select={selectAssetHandler}
|
on:select={(e) => selectAssetHandler(e.detail.asset)}
|
||||||
on:intersected={(event) =>
|
on:intersected={(event) =>
|
||||||
i === Math.max(1, assets.length - 7) ? dispatch('intersected', event.detail) : undefined}
|
i === Math.max(1, assets.length - 7) ? dispatch('intersected', event.detail) : undefined}
|
||||||
selected={selectedAssets.has(asset)}
|
selected={selectedAssets.has(asset)}
|
||||||
|
|
Loading…
Reference in a new issue