1
0
mirror of https://github.com/immich-app/immich.git synced 2024-12-25 10:43:13 +02:00

feat(wip): add Combobox component for timezone picker (#6154)

* add initial Combobox

* add basic input to Combobox

* add search functionality

* adjust styling

* add Combobox icon and adjust styling

* styling

* refactored

* refactored

* better display of timezone

* fix: clicks

* fix: eslint

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
This commit is contained in:
Michael Lyon 2024-01-27 11:36:40 -07:00 committed by GitHub
parent 64ab09bbb6
commit c4b8c853bc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 111 additions and 75 deletions

View File

@ -10,48 +10,51 @@
import { createEventDispatcher } from 'svelte';
import { DateTime } from 'luxon';
import ConfirmDialogue from './confirm-dialogue.svelte';
import Dropdown from '../elements/dropdown.svelte';
import Combobox from './combobox.svelte';
export let initialDate: DateTime = DateTime.now();
interface ZoneOption {
zone: string;
offset: string;
}
type ZoneOption = {
/**
* Timezone name
*
* e.g. Europe/Berlin
*/
label: string;
/**
* Timezone offset
*
* e.g. UTC+01:00
*/
value: string;
};
const timezones: ZoneOption[] = Intl.supportedValuesOf('timeZone').map((zone: string) => ({
zone,
offset: 'UTC' + DateTime.local({ zone }).toFormat('ZZ'),
label: zone + ` (${DateTime.local({ zone }).toFormat('ZZ')})`,
value: 'UTC' + DateTime.local({ zone }).toFormat('ZZ'),
}));
const initialOption = timezones.find((item) => item.offset === 'UTC' + initialDate.toFormat('ZZ'));
const initialOption = timezones.find((item) => item.value === 'UTC' + initialDate.toFormat('ZZ'));
let selectedOption = {
label: initialOption?.label || '',
value: initialOption?.value || '',
};
let selectedDate = initialDate.toFormat("yyyy-MM-dd'T'HH:mm");
let selectedTimezone = initialOption?.offset || null;
let disabled = false;
let searchQuery = '';
let filteredTimezones: ZoneOption[] = timezones;
const updateSearchQuery = (event: Event) => {
searchQuery = (event.target as HTMLInputElement).value;
filterTimezones();
};
const filterTimezones = () => {
filteredTimezones = timezones.filter((timezone) => timezone.zone.toLowerCase().includes(searchQuery.toLowerCase()));
};
const dispatch = createEventDispatcher<{
cancel: void;
confirm: string;
}>();
const handleCancel = () => dispatch('cancel');
const handleConfirm = () => {
let date = DateTime.fromISO(selectedDate);
if (selectedTimezone != null) {
date = date.setZone(selectedTimezone, { keepLocalTime: true }); // Keep local time if not it's really confusing
}
date = date.setZone(selectedOption.value, { keepLocalTime: true }); // Keep local time if not it's really confusing
const value = date.toISO();
if (value) {
@ -65,34 +68,6 @@
event.stopPropagation();
}
};
let isDropdownOpen = false;
let isSearching = false;
const onSearchFocused = () => {
isSearching = true;
openDropdown();
};
const onSearchBlurred = () => {
isSearching = false;
closeDropdown();
};
const openDropdown = () => {
isDropdownOpen = true;
};
const closeDropdown = () => {
isDropdownOpen = false;
};
const handleSelectTz = (item: ZoneOption) => {
selectedTimezone = item.offset;
closeDropdown();
};
</script>
<div role="presentation" on:keydown={handleKeydown}>
@ -118,29 +93,7 @@
</div>
<div class="flex flex-col w-full mt-2">
<label for="timezone">Timezone</label>
<div class="relative">
<input
class="text-sm my-4 w-full bg-gray-200 p-3 rounded-lg dark:text-white dark:bg-gray-600"
id="timezoneSearch"
type="text"
placeholder="Search timezone..."
bind:value={searchQuery}
on:input={updateSearchQuery}
on:focus={onSearchFocused}
on:blur={onSearchBlurred}
/>
<Dropdown
class="h-[400px]"
selectedOption={initialOption}
options={filteredTimezones}
render={(item) => (item ? `${item.zone} (${item.offset})` : '(not selected)')}
on:select={({ detail: item }) => handleSelectTz(item)}
controlable={true}
bind:showMenu={isDropdownOpen}
on:click-outside={isSearching ? null : closeDropdown}
/>
</div>
<Combobox bind:selectedOption options={timezones} placeholder="Search timezone..." />
</div>
</div>
</ConfirmDialogue>

View File

@ -0,0 +1,83 @@
<script lang="ts" context="module">
// Necessary for eslint
/* eslint-disable @typescript-eslint/no-explicit-any */
type T = any;
</script>
<script lang="ts" generics="T">
import Icon from '$lib/components/elements/icon.svelte';
import { clickOutside } from '$lib/utils/click-outside';
import { mdiMagnify, mdiUnfoldMoreHorizontal } from '@mdi/js';
type ComboBoxOption = {
label: string;
value: T;
};
export let options: ComboBoxOption[] = [];
export let selectedOption: ComboBoxOption;
export let placeholder = '';
let isOpen = false;
let searchQuery = '';
$: filteredOptions = options.filter((option) => option.label.toLowerCase().includes(searchQuery.toLowerCase()));
let handleClick = () => {
searchQuery = '';
isOpen = !isOpen;
};
let handleOutClick = () => {
searchQuery = '';
isOpen = false;
};
let handleSelect = (option: ComboBoxOption) => {
selectedOption = option;
isOpen = false;
};
</script>
<div class="relative" use:clickOutside on:outclick={handleOutClick}>
<button
class="text-sm text-left w-full bg-gray-200 p-3 rounded-lg dark:text-white dark:bg-gray-600 dark:hover:bg-gray-500 transition-all"
on:click={handleClick}
>{selectedOption.label}
<div class="absolute right-0 top-0 h-full flex px-4 justify-center items-center content-between">
<Icon path={mdiUnfoldMoreHorizontal} />
</div>
</button>
{#if isOpen}
<div
class="absolute w-full top-full mt-2 bg-white dark:bg-gray-800 rounded-lg border border-gray-300 dark:border-gray-900"
>
<div class="relative border-b flex">
<div class="absolute inset-y-0 left-0 flex items-center pl-3">
<div class="dark:text-immich-dark-fg/75">
<button class="flex items-center">
<Icon path={mdiMagnify} />
</button>
</div>
</div>
<!-- svelte-ignore a11y-autofocus -->
<input bind:value={searchQuery} autofocus {placeholder} class="ml-9 grow bg-transparent py-2" />
</div>
<div class="h-64 overflow-y-auto">
{#each filteredOptions as option (option.label)}
<button
class="block text-left w-full px-4 py-2 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700 transition-all
${option.label === selectedOption.label ? 'bg-gray-300 dark:bg-gray-600' : ''}
"
class:bg-gray-300={option.label === selectedOption.label}
on:click={() => handleSelect(option)}
>
{option.label}
</button>
{/each}
</div>
</div>
{/if}
</div>