Skip to content

Commit

Permalink
feat(v-collapsible): new collapsible component
Browse files Browse the repository at this point in the history
  • Loading branch information
gravitano committed Sep 14, 2021
1 parent ac8f451 commit bef4440
Show file tree
Hide file tree
Showing 3 changed files with 296 additions and 0 deletions.
108 changes: 108 additions & 0 deletions src/components/VCollapsible/VCollapse.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
<template>
<div :class="{'navbar-collapse': navbar}">
<slot />
</div>
</template>

<script>
const props = {
duration: {
type: [Number, Object],
default: 300,
},
transition: {
type: String,
default: 'ease-in-out',
},
show: Boolean,
navbar: Boolean,
};
export default {
name: 'VCollapse',
props,
emits: ['finish'],
data() {
return {
collapsing: false,
heightWatcher: null,
visible: this.show,
el: null,
};
},
computed: {
toggleTime() {
return (
(this.visible ? this.duration.show : this.duration.hide) ||
this.duration
);
},
},
watch: {
show(val) {
this.visible = val;
},
visible(val) {
if (this.toggleTime) {
this.collapseController(val);
} else {
this.reset();
}
},
},
mounted() {
this.$el.style.display = this.visible ? '' : 'none';
},
beforeUnmount() {
clearTimeout(this.heightWatcher);
},
methods: {
collapseController(val) {
if (this.collapsing === false) {
val ? this.toggle(true) : this.toggle(false);
this.setFinishTimer(this.toggleTime);
} else {
this.setTransition();
this.turn();
const height = Number(this.collapsing.slice(0, -2));
const current = this.$el.offsetHeight;
const time = (val ? height - current : current) / height;
this.setFinishTimer(this.toggleTime * time);
}
},
turn() {
if (this.visible) {
this.$el.style.height = this.collapsing;
} else {
this.$el.style.height = 0;
}
},
toggle(val) {
this.$el.style.display = '';
this.collapsing = this.$el.scrollHeight + 'px';
this.$el.style.height = val ? 0 : this.$el.scrollHeight + 'px';
this.$el.style.overflow = 'hidden';
this.setTransition();
const self = this;
setTimeout(() => {
self.$el.style.height = val ? this.collapsing : 0;
}, 0);
},
setTransition() {
this.$el.style.transition = `all ${this.toggleTime}ms ${this.transition}`;
},
setFinishTimer(duration) {
clearTimeout(this.heightWatcher);
this.heightWatcher = setTimeout(() => this.reset(), duration);
},
reset() {
this.collapsing = false;
this.$el.style.display = this.visible ? '' : 'none';
this.$el.style.height = '';
this.$el.style.overflow = '';
this.$el.style.transition = '';
this.$emit('finish', this.visible);
},
},
};
</script>
72 changes: 72 additions & 0 deletions src/components/VCollapsible/VCollapsible.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import {ref} from 'vue';
import MyCollapsible from './VCollapsible.vue';

export default {
title: 'Components/Collapsible',
component: MyCollapsible,
args: {
modelValue: false,
title: 'Title',
headerClass: 'font-bold',
activeClass: '',
inactiveClass: '',
wrapperClass: 'mb-5',
activatorClass: '',
panelClass: 'px-4 pb-4',
},
};

const Template = (args) => ({
// Components used in your story `template` are defined in the `components` object
components: {MyCollapsible},
// The story's `args` need to be mapped into the template through the `setup()` method
setup() {
const isOpen = ref(false);

return {args, isOpen};
},
// And then the `args` are bound to your component with `v-bind="args"`
template: `<MyCollapsible v-model="isOpen" v-bind='args'>
Anim eiusmod ea nostrud do incididunt consequat duis adipisicing reprehenderit nisi. Minim mollit eiusmod incididunt fugiat ipsum velit ut consequat est consectetur adipisicing. Nulla duis anim velit magna aute nisi elit nulla deserunt. Fugiat consequat ut magna eiusmod sit incididunt qui. Incididunt velit fugiat sunt sint amet magna est laborum excepteur. Aute aliqua nisi est nulla voluptate enim qui amet labore et consectetur. Est pariatur qui amet cupidatat magna est adipisicing quis ea ea.
</MyCollapsible>
`,
});

export const Default = Template.bind({});
Default.args = {};

export const AutoOpen = Template.bind({});
AutoOpen.args = {
modelValue: true,
};

export const CustomClasses = Template.bind({});
CustomClasses.args = {
headerClass: '',
activeClass: 'font-bold text-red-500 bg-red-200 rounded-t-lg',
inactiveClass: 'text-red-500 bg-red-50',
wrapperClass: 'rounded-lg',
activatorClass: 'hover:bg-red-200 hover:text-red-500',
panelClass: 'border p-4 rounded-b-lg',
};

export const Group = (args) => ({
// Components used in your story `template` are defined in the `components` object
components: {MyCollapsible},
// The story's `args` need to be mapped into the template through the `setup()` method
setup() {
return {args};
},
// And then the `args` are bound to your component with `v-bind="args"`
template: `<MyCollapsible v-for="i in 5" :key="i" v-bind='args'>
Anim eiusmod ea nostrud do incididunt consequat duis adipisicing reprehenderit nisi. Minim mollit eiusmod incididunt fugiat ipsum velit ut consequat est consectetur adipisicing. Nulla duis anim velit magna aute nisi elit nulla deserunt. Fugiat consequat ut magna eiusmod sit incididunt qui. Incididunt velit fugiat sunt sint amet magna est laborum excepteur. Aute aliqua nisi est nulla voluptate enim qui amet labore et consectetur. Est pariatur qui amet cupidatat magna est adipisicing quis ea ea.
</MyCollapsible>`,
});

// export const Collapsible = (args) => ({
// components: {MyCollapsible},
// setup() {
// return {args};
// },
// template: `<div class="container mx-auto"><MyCollapsible v-bind="args" /></div>`,
// });
116 changes: 116 additions & 0 deletions src/components/VCollapsible/VCollapsible.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
<template>
<div>
<div
class="
py-3
w-full
flex
justify-between
items-center
px-4
gap-x-4
transition
duration-300
group
cursor-pointer
font-bold
"
:class="[activatorClass, isOpen ? activeClass : inactiveClass]"
@click="toggle"
>
<slot name="header">
{{ title }}
</slot>

<slot name="icon">
<ChevronDownIcon
class="w-5 h-5"
:class="[isOpen ? 'transform rotate-180' : '']"
/>
</slot>
</div>
<v-collapse :show="isOpen">
<div :class="panelClass">
<slot />
</div>
</v-collapse>
</div>
</template>

<script setup lang="ts">
import {ref, watch, toRefs} from 'vue';
import {ChevronDownIcon} from '@heroicons/vue/outline';
import VCollapse from './VCollapse.vue';
const props = defineProps({
modelValue: {
type: Boolean,
default: false,
},
defaultOpen: {
type: Boolean,
default: false,
},
title: {
type: String,
default: '',
},
headerClass: {
type: String,
default: 'font-bold',
},
activeClass: {
type: String,
default: '',
},
wrapperClass: {
type: String,
default: 'mb-5',
},
inactiveClass: {
type: String,
default: '',
},
activatorClass: {
type: String,
default: '',
},
panelClass: {
type: String,
default: 'px-4 pb-4',
},
});
const {
modelValue,
defaultOpen,
title,
headerClass,
activeClass,
wrapperClass,
inactiveClass,
activatorClass,
panelClass,
} = toRefs(props);
const emit = defineEmits(['update:modelValue']);
const panel = ref(null);
const isOpen = ref(modelValue.value);
const toggle = () => (isOpen.value = !isOpen.value);
watch(
modelValue,
(value) => {
isOpen.value = value;
},
{immediate: true},
);
watch(isOpen, (value) => {
emit('update:modelValue', value);
});
</script>

<style scoped></style>

0 comments on commit bef4440

Please sign in to comment.