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">
|
<script setup lang="ts">
|
||||||
import { computed, ref, watch } from "vue";
|
import { computed, ref, watch } from "vue";
|
||||||
import TextInput from "./TextInput.vue";
|
import TextInput from "./TextInput.vue";
|
||||||
|
import CollapseAnimator from "../Animators/CollapseAnimator.vue";
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
@@ -26,12 +27,12 @@ const onModelValueChange = (value: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const hasFocus = ref(false);
|
const hasFocus = ref(false);
|
||||||
const showSuggestions = ref(false);
|
const delayedSuggestionsFocus = ref(false);
|
||||||
let timeOutId = 0;
|
let timeOutId = 0;
|
||||||
|
|
||||||
const onFocus = () => {
|
const onFocus = () => {
|
||||||
hasFocus.value = true;
|
hasFocus.value = true;
|
||||||
showSuggestions.value = true;
|
delayedSuggestionsFocus.value = true;
|
||||||
|
|
||||||
clearTimeout(timeOutId);
|
clearTimeout(timeOutId);
|
||||||
};
|
};
|
||||||
@@ -41,7 +42,7 @@ const onBlur = () => {
|
|||||||
|
|
||||||
// Hide the suggestions later to give the click handlers time to react
|
// Hide the suggestions later to give the click handlers time to react
|
||||||
timeOutId = setTimeout(() => {
|
timeOutId = setTimeout(() => {
|
||||||
showSuggestions.value = false;
|
delayedSuggestionsFocus.value = false;
|
||||||
}, 100);
|
}, 100);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -54,6 +55,15 @@ const onButtonClick = (value: string) => {
|
|||||||
const hasMatch = computed<boolean>(() =>
|
const hasMatch = computed<boolean>(() =>
|
||||||
props.options.includes(props.modelValue)
|
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:
|
// 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 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
|
// - 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>
|
<span style="margin-left: 5px" v-show="hasMatch">✅</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<CollapseAnimator :collapsed="!showOptions">
|
||||||
style="display: flex; flex-direction: column; gap: 10px; margin-top: 5px"
|
<div
|
||||||
v-if="showSuggestions && (!hasMatch || props.alwaysShowSuggestions)"
|
style="
|
||||||
>
|
display: flex;
|
||||||
<button v-for="option in props.options" @click="onButtonClick(option)">
|
flex-direction: column;
|
||||||
{{ option }}
|
gap: 10px;
|
||||||
</button>
|
margin-top: 5px;
|
||||||
</div>
|
"
|
||||||
|
>
|
||||||
|
<button v-for="option in props.options" @click="onButtonClick(option)">
|
||||||
|
{{ option }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</CollapseAnimator>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user