mirror of
				https://github.com/immich-app/immich.git
				synced 2025-10-31 02:39:03 -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 | ||||
|         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: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}"> | ||||
|           <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 --> | ||||
|     <div class="flex flex-wrap gap-[2px]"> | ||||
|       {#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} | ||||
|     </div> | ||||
|   </section> | ||||
|  | ||||
| @ -646,7 +646,7 @@ | ||||
|                   ? 'bg-transparent border-2 border-white' | ||||
|                   : 'bg-gray-700/40'} inline-block hover:bg-transparent" | ||||
|                 asset={stackedAsset} | ||||
|                 on:click={() => { | ||||
|                 onClick={() => { | ||||
|                   asset = stackedAsset; | ||||
|                   preloadAssets = index + 1 >= $stackAssetsStore.length ? [] : [$stackAssetsStore[index + 1]]; | ||||
|                 }} | ||||
|  | ||||
| @ -21,9 +21,9 @@ | ||||
|   import { fade } from 'svelte/transition'; | ||||
|   import ImageThumbnail from './image-thumbnail.svelte'; | ||||
|   import VideoThumbnail from './video-thumbnail.svelte'; | ||||
|   import { shortcut } from '$lib/utils/shortcut'; | ||||
| 
 | ||||
|   const dispatch = createEventDispatcher<{ | ||||
|     click: { asset: AssetResponseDto }; | ||||
|     select: { asset: AssetResponseDto }; | ||||
|     'mouse-event': { isMouseOver: boolean; selectedGroupIndex: number }; | ||||
|   }>(); | ||||
| @ -40,12 +40,13 @@ | ||||
|   export let readonly = false; | ||||
|   export let showArchiveIcon = false; | ||||
|   export let showStackedIcon = true; | ||||
|   export let intersecting = false; | ||||
|   export let onClick: ((asset: AssetResponseDto) => void) | undefined = undefined; | ||||
| 
 | ||||
|   let className = ''; | ||||
|   export { className as class }; | ||||
| 
 | ||||
|   let mouseOver = false; | ||||
|   $: clickable = !disabled && onClick; | ||||
| 
 | ||||
|   $: dispatch('mouse-event', { isMouseOver: mouseOver, selectedGroupIndex: groupIndex }); | ||||
| 
 | ||||
| @ -62,14 +63,8 @@ | ||||
|   })(); | ||||
| 
 | ||||
|   const thumbnailClickedHandler = () => { | ||||
|     if (!disabled) { | ||||
|       dispatch('click', { asset }); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   const thumbnailKeyDownHandler = (e: KeyboardEvent) => { | ||||
|     if (e.key === 'Enter') { | ||||
|       thumbnailClickedHandler(); | ||||
|     if (clickable) { | ||||
|       onClick?.(asset); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
| @ -89,20 +84,22 @@ | ||||
|   }; | ||||
| </script> | ||||
| 
 | ||||
| <IntersectionObserver once={false} on:intersected bind:intersecting> | ||||
|   <!-- svelte-ignore a11y-no-static-element-interactions --> | ||||
| <IntersectionObserver once={false} on:intersected let:intersecting> | ||||
|   <!-- svelte-ignore a11y-no-noninteractive-tabindex --> | ||||
|   <div | ||||
|     style:width="{width}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-immich-primary/20 dark:bg-immich-dark-primary/20'}" | ||||
|     class:cursor-not-allowed={disabled} | ||||
|     class:hover:cursor-pointer={!disabled} | ||||
|     class:hover:cursor-pointer={clickable} | ||||
|     on:mouseenter={onMouseEnter} | ||||
|     on:mouseleave={onMouseLeave} | ||||
|     role={clickable ? 'button' : undefined} | ||||
|     tabindex={clickable ? 0 : undefined} | ||||
|     on:click={thumbnailClickedHandler} | ||||
|     on:keydown={thumbnailKeyDownHandler} | ||||
|     use:shortcut={{ shortcut: { key: 'Enter' }, onShortcut: thumbnailClickedHandler }} | ||||
|   > | ||||
|     {#if intersecting} | ||||
|       <div class="absolute z-20 h-full w-full {className}"> | ||||
| @ -140,6 +137,11 @@ | ||||
|           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 --> | ||||
|         {#if !isSharedLink() && asset.isFavorite} | ||||
|           <div class="absolute bottom-2 left-2 z-10"> | ||||
|  | ||||
| @ -178,7 +178,7 @@ | ||||
|               {showArchiveIcon} | ||||
|               {asset} | ||||
|               {groupIndex} | ||||
|               on:click={() => assetClickHandler(asset, groupAssets, groupTitle)} | ||||
|               onClick={() => assetClickHandler(asset, groupAssets, groupTitle)} | ||||
|               on:select={() => assetSelectHandler(asset, groupAssets, groupTitle)} | ||||
|               on:mouse-event={() => assetMouseEventHandler(groupTitle, asset)} | ||||
|               selected={$selectedAssets.has(asset) || $assetStore.albumAssets.has(asset.id)} | ||||
|  | ||||
| @ -8,7 +8,7 @@ | ||||
|   import { isSearchEnabled } from '$lib/stores/search.store'; | ||||
|   import { featureFlags } from '$lib/stores/server-config.store'; | ||||
|   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 type { AlbumResponseDto, AssetResponseDto } from '@immich/sdk'; | ||||
|   import { DateTime } from 'luxon'; | ||||
| @ -202,24 +202,24 @@ | ||||
| 
 | ||||
|   let shiftKeyIsDown = false; | ||||
| 
 | ||||
|   const onKeyDown = (e: KeyboardEvent) => { | ||||
|   const onKeyDown = (event: KeyboardEvent) => { | ||||
|     if ($isSearchEnabled) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     if (e.key == 'Shift') { | ||||
|       e.preventDefault(); | ||||
|     if (matchesShortcut(event, { key: 'Shift' })) { | ||||
|       event.preventDefault(); | ||||
|       shiftKeyIsDown = true; | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   const onKeyUp = (e: KeyboardEvent) => { | ||||
|   const onKeyUp = (event: KeyboardEvent) => { | ||||
|     if ($isSearchEnabled) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     if (e.key == 'Shift') { | ||||
|       e.preventDefault(); | ||||
|     if (matchesShortcut(event, { key: 'Shift' })) { | ||||
|       event.preventDefault(); | ||||
|       shiftKeyIsDown = false; | ||||
|     } | ||||
|   }; | ||||
|  | ||||
| @ -26,17 +26,14 @@ | ||||
|   let currentViewAssetIndex = 0; | ||||
|   $: isMultiSelectionMode = selectedAssets.size > 0; | ||||
| 
 | ||||
|   const viewAssetHandler = (event: CustomEvent) => { | ||||
|     const { asset }: { asset: AssetResponseDto } = event.detail; | ||||
| 
 | ||||
|   const viewAssetHandler = (asset: AssetResponseDto) => { | ||||
|     currentViewAssetIndex = assets.findIndex((a) => a.id == asset.id); | ||||
|     selectedAsset = assets[currentViewAssetIndex]; | ||||
|     $showAssetViewer = true; | ||||
|     updateAssetState(selectedAsset.id, false); | ||||
|   }; | ||||
| 
 | ||||
|   const selectAssetHandler = (event: CustomEvent) => { | ||||
|     const { asset }: { asset: AssetResponseDto } = event.detail; | ||||
|   const selectAssetHandler = (asset: AssetResponseDto) => { | ||||
|     let temporary = new Set(selectedAssets); | ||||
| 
 | ||||
|     if (selectedAssets.has(asset)) { | ||||
| @ -123,8 +120,8 @@ | ||||
|         <Thumbnail | ||||
|           {asset} | ||||
|           readonly={disableAssetSelect} | ||||
|           on:click={(e) => (isMultiSelectionMode ? selectAssetHandler(e) : viewAssetHandler(e))} | ||||
|           on:select={selectAssetHandler} | ||||
|           onClick={(e) => (isMultiSelectionMode ? selectAssetHandler(e) : viewAssetHandler(e))} | ||||
|           on:select={(e) => selectAssetHandler(e.detail.asset)} | ||||
|           on:intersected={(event) => | ||||
|             i === Math.max(1, assets.length - 7) ? dispatch('intersected', event.detail) : undefined} | ||||
|           selected={selectedAssets.has(asset)} | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user