Add collapse animation example
This commit is contained in:
84
src/components/Animators/CollapseAnimator.vue
Normal file
84
src/components/Animators/CollapseAnimator.vue
Normal file
@@ -0,0 +1,84 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, watch } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
collapsed: boolean;
|
||||
}>();
|
||||
|
||||
const growElementId = Math.round(Math.random() * 100000).toString();
|
||||
const initialGrowHeight = props.collapsed ? "0px" : "auto";
|
||||
const transitionRule = "height 0.5s ease-in-out";
|
||||
|
||||
function onTransitionEnd() {
|
||||
// Check the prop, in case the user is toggeling repeatedly before
|
||||
// the transition can finish
|
||||
if (!props.collapsed) {
|
||||
const growElement = getGrowElement();
|
||||
// Set the height to auto to allow for height changes post
|
||||
// transition, else the element would be stuck at the set height
|
||||
// until the next collapse -> unfold
|
||||
growElement.style.height = "auto";
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getGrowElement().addEventListener("transitionend", onTransitionEnd);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
getGrowElement().removeEventListener("transitionend", onTransitionEnd);
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.collapsed,
|
||||
(value, previousValue) => {
|
||||
if (value === previousValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
const growElement = getGrowElement();
|
||||
|
||||
if (previousValue) {
|
||||
growElement.style.height = "0px";
|
||||
growElement.style.transition = transitionRule;
|
||||
|
||||
requestAnimationFrame(function () {
|
||||
growElement.style.height = `${growElement.scrollHeight}px`;
|
||||
});
|
||||
} else {
|
||||
// Disable the transition to avoid triggering instant transitions
|
||||
growElement.style.transition = "";
|
||||
growElement.style.height = `${growElement.scrollHeight}px`;
|
||||
|
||||
requestAnimationFrame(function () {
|
||||
growElement.style.transition = transitionRule;
|
||||
requestAnimationFrame(function () {
|
||||
growElement.style.height = "0px";
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
function getGrowElement(): HTMLElement {
|
||||
const el = document.getElementById(growElementId);
|
||||
if (el == null) {
|
||||
throw new Error("Grow element does not exist");
|
||||
}
|
||||
|
||||
return el;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :id="growElementId" class="grow">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.grow {
|
||||
height: v-bind(initialGrowHeight);
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from "vue";
|
||||
import TextInput from "./TextInput.vue";
|
||||
import CollapseAnimator from "../Animators/CollapseAnimator.vue";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
@@ -26,12 +27,12 @@ const onModelValueChange = (value: string) => {
|
||||
};
|
||||
|
||||
const hasFocus = ref(false);
|
||||
const showSuggestions = ref(false);
|
||||
const delayedSuggestionsFocus = ref(false);
|
||||
let timeOutId = 0;
|
||||
|
||||
const onFocus = () => {
|
||||
hasFocus.value = true;
|
||||
showSuggestions.value = true;
|
||||
delayedSuggestionsFocus.value = true;
|
||||
|
||||
clearTimeout(timeOutId);
|
||||
};
|
||||
@@ -41,7 +42,7 @@ const onBlur = () => {
|
||||
|
||||
// Hide the suggestions later to give the click handlers time to react
|
||||
timeOutId = setTimeout(() => {
|
||||
showSuggestions.value = false;
|
||||
delayedSuggestionsFocus.value = false;
|
||||
}, 100);
|
||||
};
|
||||
|
||||
@@ -54,6 +55,15 @@ const onButtonClick = (value: string) => {
|
||||
const hasMatch = computed<boolean>(() =>
|
||||
props.options.includes(props.modelValue)
|
||||
);
|
||||
|
||||
const showOptions = computed<boolean>(() => {
|
||||
return (
|
||||
delayedSuggestionsFocus.value &&
|
||||
props.options.length > 1 &&
|
||||
(!hasMatch.value || props.alwaysShowSuggestions)
|
||||
);
|
||||
});
|
||||
|
||||
// If we would simply emit an auto complete event on update model value it would:
|
||||
// - not allow the parent to reject the new value without causing inconsistent state
|
||||
// - not handle the case where the parent is the one changing the value, forcing it to implement the autocomplete logic itself as well if needed
|
||||
@@ -81,13 +91,19 @@ watch(
|
||||
<span style="margin-left: 5px" v-show="hasMatch">✅</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style="display: flex; flex-direction: column; gap: 10px; margin-top: 5px"
|
||||
v-if="showSuggestions && (!hasMatch || props.alwaysShowSuggestions)"
|
||||
>
|
||||
<button v-for="option in props.options" @click="onButtonClick(option)">
|
||||
{{ option }}
|
||||
</button>
|
||||
</div>
|
||||
<CollapseAnimator :collapsed="!showOptions">
|
||||
<div
|
||||
style="
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin-top: 5px;
|
||||
"
|
||||
>
|
||||
<button v-for="option in props.options" @click="onButtonClick(option)">
|
||||
{{ option }}
|
||||
</button>
|
||||
</div>
|
||||
</CollapseAnimator>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user