Add collapse animation example

This commit is contained in:
2023-07-03 21:30:09 +02:00
parent 933a15b74d
commit 5249384196
2 changed files with 111 additions and 11 deletions

View 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>

View File

@@ -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>