From cac29bac0df9112cec1d4c66af82dd343081e08a Mon Sep 17 00:00:00 2001
From: ben-basten <45583362+ben-basten@users.noreply.github.com>
Date: Thu, 19 Sep 2024 23:22:05 -0400
Subject: [PATCH] feat: zooming and virtual keyboard working for iPadOS/Safari

---
 .../shared-components/combobox.svelte         | 39 ++++++++++++-------
 1 file changed, 25 insertions(+), 14 deletions(-)

diff --git a/web/src/lib/components/shared-components/combobox.svelte b/web/src/lib/components/shared-components/combobox.svelte
index a565268dc7..6aa97bfdd2 100644
--- a/web/src/lib/components/shared-components/combobox.svelte
+++ b/web/src/lib/components/shared-components/combobox.svelte
@@ -21,7 +21,7 @@
   import { fly } from 'svelte/transition';
   import Icon from '$lib/components/elements/icon.svelte';
   import { mdiMagnify, mdiUnfoldMoreHorizontal, mdiClose } from '@mdi/js';
-  import { createEventDispatcher, onDestroy, onMount, tick } from 'svelte';
+  import { createEventDispatcher, onMount, tick } from 'svelte';
   import type { FormEventHandler } from 'svelte/elements';
   import { shortcuts } from '$lib/actions/shortcut';
   import { focusOutside } from '$lib/actions/focus-outside';
@@ -52,6 +52,7 @@
   let selectedIndex: number | undefined;
   let optionRefs: HTMLElement[] = [];
   let input: HTMLInputElement;
+  let dropdown: HTMLUListElement;
   let bounds: DOMRect | undefined;
   let scrollableAncestor: Element | null;
   let dropdownDirection: 'bottom' | 'top' = 'bottom';
@@ -81,11 +82,15 @@
     observer.observe(input);
     scrollableAncestor = input.closest('.overflow-y-auto, .overflow-y-scroll');
     scrollableAncestor?.addEventListener('scroll', onPositionChange);
-  });
+    window.visualViewport?.addEventListener('resize', onPositionChange);
+    window.visualViewport?.addEventListener('scroll', onPositionChange);
 
-  onDestroy(() => {
-    scrollableAncestor?.removeEventListener('scroll', onPositionChange);
-    observer.disconnect();
+    return () => {
+      observer.disconnect();
+      scrollableAncestor?.removeEventListener('scroll', onPositionChange);
+      window.visualViewport?.removeEventListener('resize', onPositionChange);
+      window.visualViewport?.removeEventListener('scroll', onPositionChange);
+    };
   });
 
   const dispatch = createEventDispatcher<{
@@ -155,21 +160,28 @@
       return undefined;
     }
 
-    const viewportHeight = window.innerHeight;
+    const vv = window.visualViewport;
+    const viewportHeight = vv?.height || 0;
+    const left = boundary.left + (vv?.offsetLeft || 0);
+    const offsetTop = vv?.offsetTop || 0;
 
     if (dropdownDirection === 'top') {
+      const dropdownHeight = dropdown?.clientHeight || 0;
+      const availableHeight = boundary.top - dropdownOffset;
+      const adjustTop = Math.max(availableHeight - dropdownHeight, 0);
       return {
-        bottom: `${viewportHeight - boundary.top}px`,
-        left: `${boundary.left}px`,
+        top: `${dropdownOffset + offsetTop + adjustTop}px`,
+        left: `${left}px`,
         width: `${boundary.width}px`,
-        maxHeight: maxHeight(boundary.top - dropdownOffset),
+        maxHeight: maxHeight(availableHeight),
       };
     }
 
+    const top = boundary.bottom + offsetTop;
     const availableHeight = viewportHeight - boundary.bottom;
     return {
-      top: `${boundary.bottom}px`,
-      left: `${boundary.left}px`,
+      top: `${top}px`,
+      left: `${left}px`,
       width: `${boundary.width}px`,
       maxHeight: maxHeight(availableHeight - dropdownOffset),
     };
@@ -191,7 +203,7 @@
       return 'bottom';
     }
 
-    const viewportHeight = window.innerHeight;
+    const viewportHeight = window.visualViewport?.height || 0;
     const heightBelow = viewportHeight - boundary.bottom;
     const heightAbove = boundary.top;
 
@@ -201,7 +213,6 @@
   const getInputPosition = () => input?.getBoundingClientRect();
 </script>
 
-<svelte:window on:resize={onPositionChange} />
 <label class="immich-form-label" class:sr-only={hideLabel} for={inputId}>{label}</label>
 <div
   class="relative w-full dark:text-gray-300 text-gray-700 text-base"
@@ -308,11 +319,11 @@
     class:shadow={dropdownDirection === 'bottom'}
     class:border={isOpen}
     style:top={position?.top}
-    style:bottom={position?.bottom}
     style:left={position?.left}
     style:width={position?.width}
     style:max-height={position?.maxHeight}
     tabindex="-1"
+    bind:this={dropdown}
   >
     {#if isOpen}
       {#if filteredOptions.length === 0}