diff --git a/src/components/collapse/README.md b/src/components/collapse/README.md index f11df98a1a2..1523c239335 100644 --- a/src/components/collapse/README.md +++ b/src/components/collapse/README.md @@ -56,6 +56,10 @@ To make the `` show initially, set the `visible` prop: ``` +By default, an initially visible collapse will not animate on mount. To enable the collapse +expanding animation on mount (when `visible` or `v-model` is `true`), set the `appear` prop on +``. + ## `v-model` support The component's collapsed (visible) state can also be set with `v-model` which binds internally to diff --git a/src/components/collapse/collapse.js b/src/components/collapse/collapse.js index 14c464e372e..3e843069dbb 100644 --- a/src/components/collapse/collapse.js +++ b/src/components/collapse/collapse.js @@ -3,15 +3,14 @@ import idMixin from '../../mixins/id' import listenOnRootMixin from '../../mixins/listen-on-root' import normalizeSlotMixin from '../../mixins/normalize-slot' import { isBrowser } from '../../utils/env' +import { BVCollapse } from '../../utils/bv-collapse' import { addClass, hasClass, removeClass, closest, matches, - reflow, getCS, - getBCR, eventOn, eventOff } from '../../utils/dom' @@ -54,6 +53,11 @@ export const BCollapse = /*#__PURE__*/ Vue.extend({ tag: { type: String, default: 'div' + }, + appear: { + // If `true` (and `visible` is `true` on mount), animate initially visible + type: Boolean, + default: false } }, data() { @@ -141,36 +145,26 @@ export const BCollapse = /*#__PURE__*/ Vue.extend({ this.show = !this.show }, onEnter(el) { - el.style.height = 0 - reflow(el) - el.style.height = el.scrollHeight + 'px' this.transitioning = true // This should be moved out so we can add cancellable events this.$emit('show') }, onAfterEnter(el) { - el.style.height = null this.transitioning = false this.$emit('shown') }, onLeave(el) { - el.style.height = 'auto' - el.style.display = 'block' - el.style.height = getBCR(el).height + 'px' - reflow(el) this.transitioning = true - el.style.height = 0 // This should be moved out so we can add cancellable events this.$emit('hide') }, onAfterLeave(el) { - el.style.height = null this.transitioning = false this.$emit('hidden') }, emitState() { this.$emit('input', this.show) - // Let v-b-toggle know the state of this collapse + // Let `v-b-toggle` know the state of this collapse this.$root.$emit(EVENT_STATE, this.safeId(), this.show) if (this.accordion && this.show) { // Tell the other collapses in this accordion to close @@ -184,13 +178,15 @@ export const BCollapse = /*#__PURE__*/ Vue.extend({ this.$root.$emit(EVENT_STATE_SYNC, this.safeId(), this.show) }, checkDisplayBlock() { - // Check to see if the collapse has `display: block !important;` set. - // We can't set `display: none;` directly on this.$el, as it would - // trigger a new transition to start (or cancel a current one). + // Check to see if the collapse has `display: block !important` set + // We can't set `display: none` directly on `this.$el`, as it would + // trigger a new transition to start (or cancel a current one) const restore = hasClass(this.$el, 'show') removeClass(this.$el, 'show') const isBlock = getCS(this.$el).display === 'block' - restore && addClass(this.$el, 'show') + if (restore) { + addClass(this.$el, 'show') + } return isBlock }, clickHandler(evt) { @@ -202,7 +198,7 @@ export const BCollapse = /*#__PURE__*/ Vue.extend({ } if (matches(el, '.nav-link,.dropdown-item') || closest('.nav-link,.dropdown-item', el)) { if (!this.checkDisplayBlock()) { - // Only close the collapse if it is not forced to be 'display: block !important;' + // Only close the collapse if it is not forced to be `display: block !important` this.show = false } } @@ -246,16 +242,9 @@ export const BCollapse = /*#__PURE__*/ Vue.extend({ [this.normalizeSlot('default')] ) return h( - 'transition', + BVCollapse, { - props: { - enterClass: '', - enterActiveClass: 'collapsing', - enterToClass: '', - leaveClass: '', - leaveActiveClass: 'collapsing', - leaveToClass: '' - }, + props: { appear: this.appear }, on: { enter: this.onEnter, afterEnter: this.onAfterEnter, diff --git a/src/components/collapse/package.json b/src/components/collapse/package.json index 9be02e5dcf4..8526099418a 100644 --- a/src/components/collapse/package.json +++ b/src/components/collapse/package.json @@ -33,6 +33,11 @@ { "prop": "visible", "description": "When 'true', expands the collapse" + }, + { + "prop": "appear", + "version": "2.2.0", + "description": "When set, and prop 'visible' is true on mount, will animate on initial mount" } ], "events": [ diff --git a/src/utils/bv-collapse.js b/src/utils/bv-collapse.js new file mode 100644 index 00000000000..41a5c1ea989 --- /dev/null +++ b/src/utils/bv-collapse.js @@ -0,0 +1,79 @@ +// Generic collapse transion helper component +// +// Note: +// Applies the classes `collapse`, `show` and `collapsing` +// during the enter/leave transition phases only +// Although it appears that Vue may be leaving the classes +// in-place after the transition completes +import Vue from './vue' +import { mergeData } from 'vue-functional-data-merge' +import { getBCR, reflow, requestAF } from './dom' + +// Transition event handler helpers +const onEnter = el => { + el.style.height = 0 + // Animaton frame delay neeeded for `appear` to work + requestAF(() => { + reflow(el) + el.style.height = `${el.scrollHeight}px` + }) +} + +const onAfterEnter = el => { + el.style.height = null +} + +const onLeave = el => { + el.style.height = 'auto' + el.style.display = 'block' + el.style.height = `${getBCR(el).height}px` + reflow(el) + el.style.height = 0 +} + +const onAfterLeave = el => { + el.style.height = null +} + +// Default transition props +// `appear` will use the enter classes +const TRANSITION_PROPS = { + css: true, + enterClass: '', + enterActiveClass: 'collapsing', + enterToClass: 'collapse show', + leaveClass: 'collapse show', + leaveActiveClass: 'collapsing', + leaveToClass: 'collapse' +} + +// Default transition handlers +// `appear` will use the enter handlers +const TRANSITION_HANDLERS = { + enter: onEnter, + afterEnter: onAfterEnter, + leave: onLeave, + afterLeave: onAfterLeave +} + +// @vue/component +export const BVCollapse = /*#__PURE__*/ Vue.extend({ + name: 'BVCollapse', + functional: true, + props: { + appear: { + // If `true` (and `visible` is `true` on mount), animate initially visible + type: Boolean, + default: false + } + }, + render(h, { props, data, children }) { + return h( + 'transition', + // We merge in the `appear` prop last + mergeData(data, { props: TRANSITION_PROPS, on: TRANSITION_HANDLERS }, { props }), + // Note: `` supports a single root element only + children + ) + } +})