mirror of
				https://github.com/immich-app/immich.git
				synced 2025-10-31 10:49:11 -04: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
					
				| @ -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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user