diff --git a/src/components/Select.svelte b/src/components/Select.svelte
index 1abd7b5e..4d85b69d 100644
--- a/src/components/Select.svelte
+++ b/src/components/Select.svelte
@@ -2,7 +2,8 @@
import IconChevronDown from 'virtual:icons/lucide/chevron-down'
import IconCheck from 'virtual:icons/heroicons-outline/check'
import { clickOutside } from 'svelte-use-click-outside'
- import { fade } from 'svelte/transition'
+ import { onMount, tick } from 'svelte'
+ import { debounce } from 'throttle-debounce'
interface Option {
label: string
@@ -17,19 +18,18 @@
export let icon = null
let open = false
+ let direction = 'down'
+ let checkingDirection = false
+ let element: HTMLDivElement
- $: if (options) {
- setSelected()
- }
+ $: if (selected) value = selected.value
+
+ $: if (options) setSelected()
function setSelected() {
selected = options.find((option) => option.value === (value || ''))
}
- $: if (selected) {
- value = selected.value
- }
-
// Returns the width of a string based on the font size and family
const getTextWidth = Object.assign(
(text: string, font: string) => {
@@ -42,7 +42,7 @@
return metrics.width
} else {
// Return estimate if SSR
- return text.length * 7.75
+ return text.length * 8.3
}
},
// Reuses the same canvas object
@@ -50,44 +50,101 @@
)
const minWidth = Math.max(
- ...options.map((it) => getTextWidth(String(it.label || it.value), '16px Inter'))
+ ...options.map((it) => getTextWidth(String(it.label || it.value), '16px Inter')),
+ ...(!value ? [71] : []) // width of "Choose..." text
)
- let element: HTMLElement
-
function selectOption(option: Option) {
selected = option
open = false
- element.focus()
+ element.blur()
+ }
+
+ // Checks if there is enough room below the element to show the dropdown, if not, show above the element
+ async function checkDirection() {
+ checkingDirection = true
+ await tick()
+ const { bottom } = element.children[0].getBoundingClientRect()
+ const height = (element.children[1] as HTMLDivElement).offsetHeight
+ const windowBottom = window.scrollY + window.innerHeight
+ const spaceBelow = windowBottom - bottom
+ if (spaceBelow < height) {
+ direction = 'up'
+ } else {
+ direction = 'down'
+ }
+ checkingDirection = false
}
function keydown(event: KeyboardEvent) {
- if (event.key === ' ' || event.key === 'Enter') {
- event.preventDefault()
+ const currentIndex = options.indexOf(selected)
+ if (event.key === 'End') {
+ selected = options[options.length - 1]
+ } else if (['ArrowDown', 'ArrowUp'].includes(event.key)) {
+ event.preventDefault()
if (!open) {
open = true
- // Needs delay before trying to move focus
- setTimeout(() => (element.children[1].children[0] as HTMLButtonElement).focus(), 0)
- } else {
- const option = options.find(
- ({ label }) => label === document.activeElement.innerHTML.trim()
- )
- selectOption(option)
+ return
+ }
+
+ if (
+ (event.key === 'ArrowUp' && direction === 'down') ||
+ (event.key === 'ArrowDown' && direction === 'up')
+ ) {
+ if (currentIndex > 0) {
+ selected = options[currentIndex - 1]
+ } else {
+ selected = options[options.length - 1]
+ }
+ } else if (
+ (event.key === 'ArrowDown' && direction === 'down') ||
+ (event.key === 'ArrowUp' && direction === 'up')
+ ) {
+ if (currentIndex < options.length - 1) {
+ selected = options[currentIndex + 1]
+ } else {
+ selected = options[0]
+ }
+ }
+ } else if (event.key === 'Home') {
+ selected = options[0]
+ } else if (event.key === 'Escape') {
+ if (open) {
+ // prevent ESC bubble in this case (interfering with modal closing etc)
+ event.preventDefault()
+ event.stopPropagation()
+
open = false
}
+ } else if (['Enter', ' '].includes(event.key)) {
+ if (!open) {
+ open = true
+ } else {
+ open = false
+ }
+ if (event.key === 'Enter' || event.key === ' ') {
+ event.preventDefault()
+ }
}
}
+
+ onMount(() => {
+ checkDirection()
+
+ const debounced = debounce(100, checkDirection)
+ window.addEventListener('resize', debounced)
+ window.addEventListener('scroll;', debounced)
+ })
{
- open = false
- }}
+ use:clickOutside={() => (open = false)}
bind:this={element}
tabindex="0"
+ on:focus={() => (open = true)}
+ on:blur={() => (open = false)}
on:keydown={keydown}
on:click>
- {#if open}
-
- {#each options as option, index (option.value)}
- {@const isSelected = selected?.value === option.value}
-
- {/each}
-
- {/if}
+
+
+ {#each options as option, index (option.value)}
+ {@const isSelected = selected?.value === option.value}
+
+ {/each}
+