I have a helper custom component that works as a multiple-choice selection. However, I am facing a problem with the ul list. I need to keep it open when the user selects one or more options. It shouldn't focus out at that time. If outside the component is clicked, it should focusout and close the list. Currently whenever a list is selected the div focuses out. I have tried to patch it but it is a buggy solution, sometimes the focusout event isn't firing. Any sort of suggestion is appreciated.
Thank you!
This is the template part of the component
<template>
<div
class="relative pb-6"
tabindex="0"
:class="{
'has-error': errorText,
success: !errorText
}"
@focusout="handleFocusOut"
>
<label
class="label block pt-0"
>
{{ label }}
</label>
<button
class="btn-primary flex w-full justify-between"
@click.prevent="onClick"
>
<span>
{{ displayedSelectedValue }}
</span>
<span class="ml-auto">
<ChevronUpIcon
v-if="showSelectOptions"
class="h-6 w-6"
/>
<ChevronDownIcon
v-else
class="h-6 w-6"
/>
</span>
</button>
<ul
v-show="showSelectOptions"
class="absolute bottom-16 z-10 max-h-72 w-full space-y-1 overflow-y-scroll
rounded border border-gray-300 bg-white px-4 py-2 shadow-xl"
>
<li
v-for="item in items"
:key="item"
>
<label
:for="item"
class="block cursor-pointer"
>
<input
:id="item"
type="checkbox"
class="cursor-pointer bg-gray-500"
:name="item"
:checked="isItemSelected(item)"
@input="onSelectItem(item)"
>
<span
class="mx-4"
>
{{ item }}
</span>
</label>
</li>
</ul>
<p
class="help-message absolute p-1 text-sm"
>
<template
v-if="errorText"
>
{{ errorText }}
</template>
</p>
</div>
</template>
and this is the script part of the component:
<script lang="ts" setup>
import {
ChevronDownIcon,
ChevronUpIcon,
XIcon,
} from '@heroicons/vue/outline';
import {
ref,
watch,
PropType,
computed,
} from 'vue';
const emits = defineEmits<{
(e: 'blur', value: string[]): void;
(e: 'select', value: string[]): void;
}>();
const props = defineProps({
label: {
type: String,
required: true,
},
multiple: {
type: Boolean,
default: false,
},
value: {
type: Array as PropType<string[]>,
required: true,
},
items: {
type: Array as PropType<string[]>,
required: true,
},
errorText: {
type: String,
default: '',
},
});
watch(() => props.value, () => {
selectedValue.value = props.value;
});
const showSelectOptions = ref(false);
const selectedValue = ref(props.value);
const isListClicked = ref(false);
const displayedSelectedValue = computed(() => selectedValue.value.join(', '));
let inputBlurTimeoutId: number | undefined;
function isItemSelected(item: string) {
return selectedValue.value.includes(item);
}
function onClick(e) {
if (inputBlurTimeoutId) {
window.clearTimeout(inputBlurTimeoutId);
}
showSelectOptions.value = !showSelectOptions.value;
}
function handleFocusOut() {
console.log('focus out');
if (inputBlurTimeoutId) {
window.clearTimeout(inputBlurTimeoutId);
}
inputBlurTimeoutId = window.setTimeout(() => {
if (isListClicked.value) {
isListClicked.value = false;
return;
}
showSelectOptions.value = false;
}, 1 * 500);
}
function onSelectItem(item: string) {
isListClicked.value = true;
const itemIndex = selectedValue.value.indexOf(item);
if (itemIndex !== -1) {
selectedValue.value.splice(itemIndex, 1);
} else {
selectedValue.value.push(item);
}
emits('select', selectedValue.value);
}
</script>