I have a custom dropup component that I am building and I find myself running into some very weird issues with it. The biggest one being the blur event, which doesnt seem to be firing when I click outside of the input.
Basically I just want the options elements to hide on blur, if there is no localValue
then reset the component back to the Initial View and if there IS a localValue
set it to the Selected State, keeping the value in there.
I'm really not sure where I am going wrong here.
---Designs---
Initial State
Toggled State
Selected State
I've made a CodeSandbox so you can see this thing live.
I would love if anyone has any ideas on how I can fix this and get the blur event happening properly.
Any advice would be greatly appreciated!
Cheers!
NOTE
I've tried putting the blur event on the input itself, since it makes sense I think that when clicking outside of the input, it should close everything. optionsAreVisible.value = true
does work to close it, but I dont think just blurring the input is the way to go here. I think I still want to have the blur on the entire element.
Extra Issues:
Is my
:key="buttonKey"
andbuttonKey.value++
combination to reset the component the proper way to do this? It seems hacky and a bit wrong to me, but I am not totally sure!How to get the Initial State and Selected State to be the same size? The Selected State has a few more elements in it, but the design has them as the same width and I've been having trouble with getting that to work properly without hardcoding widths (which I dont want to do)
Not sure if this is a bug with Codesandbox, but
:class="[localValue === '' ? '_hide' : '', '_input-active-delete']"
line doesnt seem to be working so I have to do a v-if, which is not what my actually codebase uses. ANy idea what could be going on there?
SelectModelInput
<template>
<div :key="buttonKey">
<div
style="position: relative; width: 100%"
tabindex="-1"
@blur="() => closeDropdown()"
>
<div class="_input" @click="() => toggleDropup()">
<p
v-if="selectModelVersionIsVisible"
class="_input-active-select-model"
>
Select Model
</p>
<div v-if="!selectModelVersionIsVisible" class="_input-active">
<p class="_input-active-op-real">OP-REAL</p>
<input
v-model="localValue"
:class="[
localValue !== '' && !optionsAreVisible ? '_bg-change' : '',
'_input-active-input',
]"
type="text"
placeholder="Type to Search"
/>
<button
v-if="localValue"
:class="[localValue === '' ? '_hide' : '', '_input-active-delete']"
@click="() => clearLocalVal()"
>
<svg
style="width: 18px"
xmlns="http://www.w3.org/2000/svg"
width="100%"
height="100%"
viewBox="0 0 24 24"
fill="none"
stroke="#A5B0CB"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="feather feather-x"
>
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
</div>
<Transition name="fade">
<div
v-if="optionsAreVisible && filteredOptions.length !== 0"
class="_options"
>
<a
v-for="option in filteredOptions"
:key="option.model"
class="_option"
>
<div class="_option-inner" @click="selectOption(option.model)">
<p class="_option-inner-model">{{ option.model }}</p>
<p class="_option-inner-date">{{ option.date }}</p>
</div>
</a>
</div>
</Transition>
</div>
</div>
</template>
<script>
import { computed, defineComponent, ref, watch } from "vue";
export default defineComponent({
emit: ["input"],
props: {
value: { type: String },
models: {
type: Array,
default: () => [
{ model: "Test v1", date: "Jul.5.2022" },
{ model: "Test v2", date: "Jul.6.2022" },
{ model: "Test v3", date: "Jul.8.2022" },
],
},
},
setup(props, { emit }) {
const localValue = ref(props.value);
watch(localValue, (newVal) => {
emit("input", newVal);
});
const buttonKey = ref(0);
const optionsAreVisible = ref(false);
const selectModelVersionIsVisible = ref(true);
const options = ref(props.models);
const filteredOptions = computed(() => {
const filteredOptions = options.value.filter((option) =>
option.model.toLowerCase().includes(localValue.value?.toLowerCase())
);
return filteredOptions;
});
function toggleDropup() {
console.log("toggle Dropup");
optionsAreVisible.value = true;
selectModelVersionIsVisible.value = false;
}
function selectOption(option) {
console.log(`option → `, option);
localValue.value = option;
optionsAreVisible.value = false;
selectModelVersionIsVisible.value = false;
}
function closeDropdown() {
console.log(`localValue → `, localValue.value);
console.log("ping");
optionsAreVisible.value = true
}
function clearLocalVal() {
buttonKey.value++;
selectModelVersionIsVisible.value = true;
optionsAreVisible.value = false;
localValue.value = "";
}
return {
options,
optionsAreVisible,
selectModelVersionIsVisible,
toggleDropup,
filteredOptions,
localValue,
selectOption,
clearLocalVal,
buttonKey,
closeDropdown,
};
},
});
</script>
<style lang="sass" scoped>
button
background: none
border: none
p
padding: 0
margin: 0
::placeholder
font-style: italic
line-height: 0
color: #A5B0CB
._input
all: unset
position: relative
display: flex
justify-content: center
align-items: center
min-height: 2rem
min-width: 212px
background: #161a24
border-radius: 1rem
&:hover,
&:focus
box-shadow: 0 0 0 1px #3867D0
&-active
display: flex
justify-content: center
align-items: center
// padding-right: 2rem
&-select-model
// padding: 6px 0
color: #A5B0CB
font-size: 1rem
font-weight: 300
&-op-real
display: flex
justify-content: center
align-items: center
height: 100%
border-top-left-radius: 1rem
border-bottom-left-radius: 1rem
padding: 0 10px
background: #202634
font-size: 12px
font-weight: bold
letter-spacing: 0.96px
padding: 8px 10px
user-select: none
&-input
all: unset
font-size: 14px
text-align: center
color: #A5B0CB
padding: 6px 0px
padding-right: 2rem
flex: 1
border-top-right-radius: 1rem
border-bottom-right-radius: 1rem
&:focus::placeholder
color: transparent
&-delete
position: absolute
bottom: 50%
transform: translateY(50%)
right: 0
display: flex
justify-content: center
padding: 0
padding-right: 8px
._bg-change
background-color: #202634
._hide
visibility: hidden
._options
position: absolute
bottom: 100%
left: 30%
z-index: 100
display: flex
flex-direction: column
list-style-type: none
padding: 0.5rem 1rem
gap: 0.25rem
background-color: #2C2B30
border: 1px solid #727275
border-radius: 0.5rem
._option
background-color: transparent
font-size: 0.875rem
line-height: 1.25rem
font-weight: 400
white-space: nowrap
width: 100%
&-inner
width: 100%
display: flex
justify-content: space-between
padding: 0.25rem
cursor: pointer
&:hover
background-color: #3582F5
border-radius: 0.25rem
&-model
font-size: 14px
color: #fff
margin-right: 2.5rem
&-date
font-size: 12px
color: #B2B4B9
.fade-enter-active,
.fade-leave-active
transition: opacity 300ms
.fade-enter,
.fade-leave-to
opacity: 0
</style>
App.vue
<template>
<div class="_wrapper">
<SelectModelInput
v-model="text"
:models="modelsFromDev"
/>
<br />
<span style="color: white">{{ text }}</span>
</div>
</template>
<script>
import { defineComponent, ref } from "vue";
import SelectModelInput from "./components/SelectModelInput.vue";
export default defineComponent({
setup() {
const text = ref("");
const modelsFromDev = [
{ model: "Test v4", date: "Jul.5.2022" },
{ model: "Test v5", date: "Jul.6.2022" },
{ model: "Test v6", date: "Jul.8.2022" },
];
return { text, modelsFromDev };
},
components: { SelectModelInput },
});
</script>
<style lang="sass" scoped>
body
padding: 0
margin: 0
._wrapper
color: white
display: flex
flex-direction: column
justify-content: center
align-items: center
width: 100vw
height: 100vh
background: #283044
</style>