diff --git a/src/components/modal/helpers/bv-modal.js b/src/components/modal/helpers/bv-modal.js index 31d052b339a..0fd5bc93efb 100644 --- a/src/components/modal/helpers/bv-modal.js +++ b/src/components/modal/helpers/bv-modal.js @@ -3,6 +3,8 @@ import { NAME_MODAL, NAME_MSG_BOX } from '../../../constants/components' import { EVENT_NAME_HIDDEN, EVENT_NAME_HIDE, + EVENT_NAME_SHOW, + EVENT_NAME_TOGGLE, HOOK_EVENT_NAME_BEFORE_DESTROY, HOOK_EVENT_NAME_DESTROYED } from '../../../constants/events' @@ -21,6 +23,7 @@ import { readonlyDescriptor } from '../../../utils/object' import { pluginFactory } from '../../../utils/plugins' +import { pluckProps } from '../../../utils/props' import { warn, warnNotClient, warnNoPromiseSupport } from '../../../utils/warn' import { BModal, props as modalProps } from '../modal' @@ -51,16 +54,6 @@ const propsToSlots = { // --- Helper methods --- -// Method to filter only recognized props that are not undefined -const filterOptions = options => { - return BASE_PROPS.reduce((memo, key) => { - if (!isUndefined(options[key])) { - memo[key] = options[key] - } - return memo - }, {}) -} - // Method to install `$bvModal` VM injection const plugin = Vue => { // Create a private sub-component that extends BModal @@ -116,7 +109,7 @@ const plugin = Vue => { parent: $parent, // Preset the prop values propsData: { - ...filterOptions(getComponentConfig(NAME_MODAL)), + ...pluckProps(BASE_PROPS, getComponentConfig(NAME_MODAL)), // Defaults that user can override hideHeaderClose: true, hideHeader: !(props.title || props.titleHtml), @@ -166,7 +159,7 @@ const plugin = Vue => { // Private utility method to open a user defined message box and returns a promise. // Not to be used directly by consumers, as this method may change calling syntax - const makeMsgBox = ($parent, content, options = {}, resolver = null) => { + const makeMsgBox = ($parent, content, props = {}, resolver = null) => { if ( !content || warnNoPromiseSupport(PROP_NAME) || @@ -176,7 +169,14 @@ const plugin = Vue => { /* istanbul ignore next */ return } - return asyncMsgBox($parent, { ...filterOptions(options), msgBoxContent: content }, resolver) + return asyncMsgBox( + $parent, + { + ...pluckProps(BASE_PROPS, props), + msgBoxContent: content + }, + resolver + ) } // BvModal instance class @@ -193,17 +193,24 @@ const plugin = Vue => { // --- Instance methods --- - // Show modal with the specified ID args are for future use + // Show modal with the specified ID show(id, ...args) { - if (id && this._root) { - this._root.$emit(getRootActionEventName(NAME_MODAL, 'show'), id, ...args) + if (id) { + this._root.$emit(getRootActionEventName(NAME_MODAL, EVENT_NAME_SHOW), id, ...args) } } - // Hide modal with the specified ID args are for future use + // Hide modal with the specified ID hide(id, ...args) { - if (id && this._root) { - this._root.$emit(getRootActionEventName(NAME_MODAL, 'hide'), id, ...args) + if (id) { + this._root.$emit(getRootActionEventName(NAME_MODAL, EVENT_NAME_HIDE), id, ...args) + } + } + + // Toggle modal with the specified ID + toggle(id, ...args) { + if (id) { + this._root.$emit(getRootActionEventName(NAME_MODAL, EVENT_NAME_TOGGLE), id, ...args) } } @@ -212,38 +219,44 @@ const plugin = Vue => { // should have a Polyfill loaded (which they need anyways for IE 11 support) // Open a message box with OK button only and returns a promise - msgBoxOk(message, options = {}) { - // Pick the modal props we support from options - const props = { - ...options, - // Add in overrides and our content prop - okOnly: true, - okDisabled: false, - hideFooter: false, - msgBoxContent: message - } - return makeMsgBox(this._vm, message, props, () => { - // Always resolve to true for OK - return true - }) + msgBoxOk(message, props = {}) { + return makeMsgBox( + this._vm, + message, + { + ...props, + // Add in overrides and our content prop + okOnly: true, + okDisabled: false, + hideFooter: false, + msgBoxContent: message + }, + () => { + // Always resolve to true for OK + return true + } + ) } // Open a message box modal with OK and CANCEL buttons // and returns a promise - msgBoxConfirm(message, options = {}) { - // Set the modal props we support from options - const props = { - ...options, - // Add in overrides and our content prop - okOnly: false, - okDisabled: false, - cancelDisabled: false, - hideFooter: false - } - return makeMsgBox(this._vm, message, props, bvModalEvent => { - const trigger = bvModalEvent.trigger - return trigger === 'ok' ? true : trigger === 'cancel' ? false : null - }) + msgBoxConfirm(message, props = {}) { + return makeMsgBox( + this._vm, + message, + { + ...props, + // Add in overrides and our content prop + okOnly: false, + okDisabled: false, + cancelDisabled: false, + hideFooter: false + }, + bvModalEvent => { + const trigger = bvModalEvent.trigger + return trigger === 'ok' ? true : trigger === 'cancel' ? false : null + } + ) } } diff --git a/src/components/modal/modal.js b/src/components/modal/modal.js index fa8a860ac19..28d57e519a7 100644 --- a/src/components/modal/modal.js +++ b/src/components/modal/modal.js @@ -369,8 +369,8 @@ export const BModal = /*#__PURE__*/ Vue.extend({ // Listen for `bv:modal::show events`, and close ourselves if the // opening modal not us this.listenOnRoot(getRootEventName(NAME_MODAL, EVENT_NAME_SHOW), this.modalListener) - // Initially show modal? - if (this[MODEL_PROP_NAME] === true) { + // Initially show modal + if (this[MODEL_PROP_NAME]) { this.$nextTick(this.show) } }, @@ -384,6 +384,41 @@ export const BModal = /*#__PURE__*/ Vue.extend({ } }, methods: { + // Private method to get the current document active element + getActiveElement() { + // Returning focus to `document.body` may cause unwanted scrolls, + // so we exclude setting focus on body + const activeElement = getActiveElement(IS_BROWSER ? [document.body] : []) + // Preset the fallback return focus value if it is not set + // `document.activeElement` should be the trigger element that was clicked or + // in the case of using the v-model, which ever element has current focus + // Will be overridden by some commands such as toggle, etc. + // Note: On IE 11, `document.activeElement` may be `null` + // So we test it for truthiness first + // https://github.com/bootstrap-vue/bootstrap-vue/issues/3206 + return activeElement && activeElement.focus ? activeElement : null + }, + buildEvent(type, options = {}) { + return new BvModalEvent(type, { + // Default options + cancelable: false, + target: this.$refs.modal || this.$el || null, + relatedTarget: null, + trigger: null, + // Supplied options + ...options, + // Options that can't be overridden + vueTarget: this, + componentId: this.modalId + }) + }, + emitEvent(bvEvent) { + const { type } = bvEvent + // We emit on `$root` first in case a global listener wants to cancel + // the event first before the instance emits its event + this.emitOnRoot(getRootEventName(NAME_MODAL, type), bvEvent, bvEvent.componentId) + this.$emit(type, bvEvent) + }, setObserver(on = false) { this.$_observer && this.$_observer.disconnect() this.$_observer = null @@ -395,32 +430,16 @@ export const BModal = /*#__PURE__*/ Vue.extend({ ) } }, - // Private method to update the v-model + // Private method to update the `v-model` updateModel(value) { if (value !== this[MODEL_PROP_NAME]) { this.$emit(MODEL_EVENT_NAME, value) } }, - // Private method to create a BvModalEvent object - buildEvent(type, options = {}) { - return new BvModalEvent(type, { - // Default options - cancelable: false, - target: this.$refs.modal || this.$el || null, - relatedTarget: null, - trigger: null, - // Supplied options - ...options, - // Options that can't be overridden - vueTarget: this, - componentId: this.modalId - }) - }, - // Public method to show modal show() { + // If already open, or in the process of opening, do nothing + /* istanbul ignore next */ if (this.isVisible || this.isOpening) { - // If already open, or in the process of opening, do nothing - /* istanbul ignore next */ return } /* istanbul ignore next */ @@ -448,10 +467,10 @@ export const BModal = /*#__PURE__*/ Vue.extend({ // Show the modal this.doShow() }, - // Public method to hide modal hide(trigger = '') { + // If already closed, or in the process of closing, do nothing + /* istanbul ignore next */ if (!this.isVisible || this.isClosing) { - /* istanbul ignore next */ return } this.isClosing = true @@ -482,7 +501,6 @@ export const BModal = /*#__PURE__*/ Vue.extend({ // Update the v-model this.updateModel(false) }, - // Public method to toggle modal visibility toggle(triggerEl) { if (triggerEl) { this.$_returnFocus = triggerEl @@ -493,20 +511,6 @@ export const BModal = /*#__PURE__*/ Vue.extend({ this.show() } }, - // Private method to get the current document active element - getActiveElement() { - // Returning focus to `document.body` may cause unwanted scrolls, - // so we exclude setting focus on body - const activeElement = getActiveElement(IS_BROWSER ? [document.body] : []) - // Preset the fallback return focus value if it is not set - // `document.activeElement` should be the trigger element that was clicked or - // in the case of using the v-model, which ever element has current focus - // Will be overridden by some commands such as toggle, etc. - // Note: On IE 11, `document.activeElement` may be `null` - // So we test it for truthiness first - // https://github.com/bootstrap-vue/bootstrap-vue/issues/3206 - return activeElement && activeElement.focus ? activeElement : null - }, // Private method to finish showing modal doShow() { /* istanbul ignore next: commenting out for now until we can test stacking */ @@ -588,13 +592,6 @@ export const BModal = /*#__PURE__*/ Vue.extend({ this.emitEvent(this.buildEvent(EVENT_NAME_HIDDEN)) }) }, - emitEvent(bvEvent) { - const { type } = bvEvent - // We emit on `$root` first in case a global listener wants to cancel - // the event first before the instance emits its event - this.emitOnRoot(getRootEventName(NAME_MODAL, type), bvEvent, bvEvent.componentId) - this.$emit(type, bvEvent) - }, // UI event handlers onDialogMousedown() { // Watch to see if the matching mouseup event occurs outside the dialog diff --git a/src/components/toast/helpers/bv-toast.js b/src/components/toast/helpers/bv-toast.js index 6de91803ae0..9c03e8fcc7f 100644 --- a/src/components/toast/helpers/bv-toast.js +++ b/src/components/toast/helpers/bv-toast.js @@ -8,6 +8,7 @@ import { EVENT_NAME_HIDDEN, EVENT_NAME_HIDE, EVENT_NAME_SHOW, + EVENT_NAME_TOGGLE, HOOK_EVENT_NAME_DESTROYED } from '../../../constants/events' import { concat } from '../../../utils/array' @@ -25,6 +26,7 @@ import { readonlyDescriptor } from '../../../utils/object' import { pluginFactory } from '../../../utils/plugins' +import { pluckProps } from '../../../utils/props' import { warn, warnNotClient } from '../../../utils/warn' import { BToast, props as toastProps } from '../toast' @@ -47,16 +49,6 @@ const propsToSlots = { // --- Helper methods --- -// Method to filter only recognized props that are not undefined -const filterOptions = options => { - return BASE_PROPS.reduce((memo, key) => { - if (!isUndefined(options[key])) { - memo[key] = options[key] - } - return memo - }, {}) -} - // Method to install `$bvToast` VM injection const plugin = Vue => { // Create a private sub-component constructor that @@ -75,16 +67,10 @@ const plugin = Vue => { mounted() { // Self destruct handler const handleDestroy = () => { - // Ensure the toast has been force hidden - this.localShow = false - this.doRender = false this.$nextTick(() => { - this.$nextTick(() => { - // In a `requestAF()` to release control back to application - // and to allow the portal-target time to remove the content - requestAF(() => { - this.$destroy() - }) + // In a `requestAF()` to release control back to application + requestAF(() => { + this.$destroy() }) }) } @@ -95,7 +81,7 @@ const plugin = Vue => { // Self destruct when toaster is destroyed this.listenOnRoot(getRootEventName(NAME_TOASTER, EVENT_NAME_DESTROYED), toaster => { /* istanbul ignore next: hard to test */ - if (toaster === this.toaster) { + if (toaster === this.computedToaster) { handleDestroy() } }) @@ -114,7 +100,7 @@ const plugin = Vue => { // app `$root`, and it ensures `BToast` is destroyed when parent is destroyed parent: $parent, propsData: { - ...filterOptions(getComponentConfig(NAME_TOAST)), + ...pluckProps(BASE_PROPS, getComponentConfig(NAME_TOAST)), // Add in (filtered) user supplied props ...omit(props, keys(propsToSlots)), // Props that can't be overridden @@ -154,26 +140,39 @@ const plugin = Vue => { // --- Public Instance methods --- - // Opens a user defined toast and returns immediately - toast(content, options = {}) { + // Shows a user defined toast and returns immediately + toast(content, props = {}) { + /* istanbul ignore next */ if (!content || warnNotClient(PROP_NAME)) { - /* istanbul ignore next */ return } - makeToast({ ...filterOptions(options), toastContent: content }, this._vm) + makeToast( + { + ...pluckProps(BASE_PROPS, props), + toastContent: content + }, + this._vm + ) } - // shows a `` component with the specified ID + // Show a toast with the specified ID show(id) { if (id) { this._root.$emit(getRootActionEventName(NAME_TOAST, EVENT_NAME_SHOW), id) } } - // Hide a toast with specified ID, or if not ID all toasts + // Hide a toast with specified ID, or if no ID all toasts hide(id = null) { this._root.$emit(getRootActionEventName(NAME_TOAST, EVENT_NAME_HIDE), id) } + + // Toggle a toast with the specified ID + toggle(id) { + if (id) { + this._root.$emit(getRootActionEventName(NAME_TOAST, EVENT_NAME_TOGGLE), id) + } + } } // Add our instance mixin diff --git a/src/components/toast/helpers/bv-toast.spec.js b/src/components/toast/helpers/bv-toast.spec.js index d0a44b23cd5..d480da6af34 100644 --- a/src/components/toast/helpers/bv-toast.spec.js +++ b/src/components/toast/helpers/bv-toast.spec.js @@ -6,7 +6,7 @@ const localVue = createLocalVue() localVue.use(ToastPlugin) describe('$bvToast', () => { - it('$bvToast.show() and $bvToast.hide() works', async () => { + it('`show()` and `hide()` methods work', async () => { const App = { render(h) { return h( @@ -34,6 +34,8 @@ describe('$bvToast', () => { await waitRAF() await waitNT(wrapper.vm) await waitRAF() + await waitNT(wrapper.vm) + await waitRAF() expect(wrapper.vm.$bvToast).toBeDefined() expect(wrapper.vm.$bvToast.show).toBeDefined() @@ -68,7 +70,7 @@ describe('$bvToast', () => { wrapper.destroy() }) - it('$bvModal.toast() works', async () => { + it('`toast()` method works', async () => { const App = { render(h) { return h('div', 'app') diff --git a/src/components/toast/toast.js b/src/components/toast/toast.js index 542a23530e2..951d146f7ac 100644 --- a/src/components/toast/toast.js +++ b/src/components/toast/toast.js @@ -8,6 +8,7 @@ import { EVENT_NAME_HIDE, EVENT_NAME_SHOW, EVENT_NAME_SHOWN, + EVENT_NAME_TOGGLE, EVENT_OPTIONS_NO_CAPTURE } from '../../constants/events' import { @@ -99,16 +100,20 @@ export const BToast = /*#__PURE__*/ Vue.extend({ data() { return { isMounted: false, - doRender: false, - localShow: false, - isTransitioning: false, - isHiding: false, + isHidden: true, // If toast should not be in document + isVisible: false, // Controls toast visible state + isTransitioning: false, // Used for style control + isShowing: false, // To signal that the toast is in the process of showing + isHiding: false, // To signal that the toast is in the process of hiding order: 0, dismissStarted: 0, resumeDismiss: 0 } }, computed: { + toastId() { + return this.safeId() + }, toastClasses() { const { appendToast, variant } = this @@ -120,8 +125,8 @@ export const BToast = /*#__PURE__*/ Vue.extend({ } }, slotScope() { - const { hide } = this - return { hide } + const { hide, isVisible: visible } = this + return { hide, visible } }, computedDuration() { // Minimum supported duration is 1 second @@ -141,18 +146,15 @@ export const BToast = /*#__PURE__*/ Vue.extend({ computedAttrs() { return { ...this.bvAttrs, - id: this.safeId(), + id: this.toastId, tabindex: '0' } } }, watch: { - [MODEL_PROP_NAME](newValue) { - this[newValue ? 'show' : 'hide']() - }, - localShow(newValue) { - if (newValue !== this[MODEL_PROP_NAME]) { - this.$emit(MODEL_EVENT_NAME, newValue) + [MODEL_PROP_NAME](newValue, oldValue) { + if (newValue !== oldValue) { + this[newValue ? 'show' : 'hide']() } }, /* istanbul ignore next */ @@ -162,9 +164,9 @@ export const BToast = /*#__PURE__*/ Vue.extend({ }, /* istanbul ignore next */ static(newValue) { - // If static changes to true, and the toast is showing, + // If static changes to `true`, and the toast is showing, // ensure the toaster target exists - if (newValue && this.localShow) { + if (newValue && this.isVisible) { this.ensureToaster() } } @@ -175,81 +177,46 @@ export const BToast = /*#__PURE__*/ Vue.extend({ }, mounted() { this.isMounted = true - this.$nextTick(() => { - if (this[MODEL_PROP_NAME]) { - requestAF(() => { - this.show() - }) - } - }) - // Listen for global $root show events - this.listenOnRoot(getRootActionEventName(NAME_TOAST, EVENT_NAME_SHOW), id => { - if (id === this.safeId()) { - this.show() - } - }) - // Listen for global $root hide events - this.listenOnRoot(getRootActionEventName(NAME_TOAST, EVENT_NAME_HIDE), id => { - if (!id || id === this.safeId()) { - this.hide() - } - }) + // Listen for events from others to either show or hide ourselves + this.listenOnRoot(getRootActionEventName(NAME_TOAST, EVENT_NAME_SHOW), this.showHandler) + this.listenOnRoot(getRootActionEventName(NAME_TOAST, EVENT_NAME_HIDE), this.hideHandler) + this.listenOnRoot(getRootActionEventName(NAME_TOAST, EVENT_NAME_TOGGLE), this.toggleHandler) // Make sure we hide when toaster is destroyed /* istanbul ignore next: difficult to test */ - this.listenOnRoot(getRootEventName(NAME_TOASTER, EVENT_NAME_DESTROYED), toaster => { - /* istanbul ignore next */ - if (toaster === this.computedToaster) { - this.hide() - } - }) + this.listenOnRoot( + getRootEventName(NAME_TOASTER, EVENT_NAME_DESTROYED), + this.toasterDestroyedHandler + ) + // Initially show toast + if (this[MODEL_PROP_NAME]) { + this.$nextTick(this.show) + } }, beforeDestroy() { this.clearDismissTimer() + if (this.isVisible) { + this.isVisible = false + this.isTransitioning = false + } }, methods: { - show() { - if (!this.localShow) { - this.ensureToaster() - const showEvent = this.buildEvent(EVENT_NAME_SHOW) - this.emitEvent(showEvent) - this.dismissStarted = this.resumeDismiss = 0 - this.order = Date.now() * (this.appendToast ? 1 : -1) - this.isHiding = false - this.doRender = true - this.$nextTick(() => { - // We show the toast after we have rendered the portal and b-toast wrapper - // so that screen readers will properly announce the toast - requestAF(() => { - this.localShow = true - }) - }) - } - }, - hide() { - if (this.localShow) { - const hideEvent = this.buildEvent(EVENT_NAME_HIDE) - this.emitEvent(hideEvent) - this.setHoverHandler(false) - this.dismissStarted = this.resumeDismiss = 0 - this.clearDismissTimer() - this.isHiding = true - requestAF(() => { - this.localShow = false - }) - } - }, buildEvent(type, options = {}) { return new BvEvent(type, { + // Default options cancelable: false, target: this.$el || null, relatedTarget: null, + // Supplied options ...options, + // Options that can't be overridden vueTarget: this, - componentId: this.safeId() + componentId: this.toastId }) }, emitEvent(bvEvent) { const { type } = bvEvent + // We emit on `$root` first in case a global listener wants to cancel + // the event first before the instance emits its event this.emitOnRoot(getRootEventName(NAME_TOAST, type), bvEvent) this.$emit(type, bvEvent) }, @@ -284,9 +251,143 @@ export const BToast = /*#__PURE__*/ Vue.extend({ this.$_dismissTimer = null }, setHoverHandler(on) { - const el = this.$refs['b-toast'] - eventOnOff(on, el, 'mouseenter', this.onPause, EVENT_OPTIONS_NO_CAPTURE) - eventOnOff(on, el, 'mouseleave', this.onUnPause, EVENT_OPTIONS_NO_CAPTURE) + const $el = this.$refs['b-toast'] + eventOnOff(on, $el, 'mouseenter', this.onPause, EVENT_OPTIONS_NO_CAPTURE) + eventOnOff(on, $el, 'mouseleave', this.onUnpause, EVENT_OPTIONS_NO_CAPTURE) + }, + // Private method to update the `v-model` + updateModel(value) { + if (value !== this[MODEL_PROP_NAME]) { + this.$emit(MODEL_EVENT_NAME, value) + } + }, + show() { + console.log('show', { + id: this.toastId, + isVisible: this.isVisible, + isShowing: this.isShowing + }) + // If already shown, or in the process of showing, do nothing + /* istanbul ignore next */ + if (this.isVisible || this.isShowing) { + return + } + // If we are in the process of hiding, wait until hidden before showing + /* istanbul ignore next */ + if (this.isHiding) { + this.$once(EVENT_NAME_HIDDEN, this.show) + return + } + this.isShowing = true + const showEvent = this.buildEvent(EVENT_NAME_SHOW, { cancelable: true }) + this.emitEvent(showEvent) + // Don't show if canceled + if (showEvent.defaultPrevented || this.isVisible) { + this.isShowing = false + // Ensure the `v-model` reflects the current state + this.updateModel(false) + return + } + this.ensureToaster() + this.dismissStarted = this.resumeDismiss = 0 + this.order = Date.now() * (this.appendToast ? 1 : -1) + // Place toast in DOM + this.isHidden = false + console.log('show', { id: this.toastId, isHidden: this.isHidden }) + // We do this in `$nextTick()` to ensure the toast is in DOM first, + // before we show it + this.$nextTick(() => { + requestAF(() => { + this.isVisible = true + this.isShowing = false + // Update the `v-model` + this.updateModel(true) + }) + }) + }, + hide() { + // If already hidden, or in the process of hiding, do nothing + /* istanbul ignore next */ + if (!this.isVisible || this.isHiding) { + return + } + this.isHiding = true + const hideEvent = this.buildEvent(EVENT_NAME_HIDE, { cancelable: true }) + this.emitEvent(hideEvent) + // Hide if not canceled + if (hideEvent.defaultPrevented || !this.isVisible) { + this.isHiding = false + // Ensure the `v-model` reflects the current state + this.updateModel(true) + return + } + this.setHoverHandler(false) + this.dismissStarted = this.resumeDismiss = 0 + this.clearDismissTimer() + // Trigger the hide transition + this.isVisible = false + // Update the v-model + this.updateModel(false) + }, + toggle() { + if (this.isVisible) { + this.hide() + } else { + this.show() + } + }, + // Transition handlers + onBeforeEnter() { + console.log('onBeforeEnter', { id: this.toastId, isVisible: this.isVisible }) + this.isTransitioning = true + }, + onAfterEnter() { + console.log('onAfterEnter', { id: this.toastId, isVisible: this.isVisible }) + this.isTransitioning = false + // We use `requestAF()` to allow transition hooks to complete + // before passing control over to the other handlers + requestAF(() => { + this.emitEvent(this.buildEvent(EVENT_NAME_SHOWN)) + + this.startDismissTimer() + this.setHoverHandler(true) + }) + }, + onBeforeLeave() { + console.log('onBeforeLeave', { id: this.toastId, isVisible: this.isVisible }) + this.isTransitioning = true + }, + onAfterLeave() { + console.log('onAfterLeave', { id: this.toastId, isVisible: this.isVisible }) + this.isTransitioning = false + this.order = 0 + this.resumeDismiss = this.dismissStarted = 0 + this.isHidden = true + this.$nextTick(() => { + this.isHiding = false + this.emitEvent(this.buildEvent(EVENT_NAME_HIDDEN)) + }) + }, + // Root listener handlers + showHandler(id) { + if (id === this.toastId) { + this.show() + } + }, + hideHandler(id) { + if (!id || id === this.toastId) { + this.hide() + } + }, + toggleHandler(id) { + if (id === this.toastId) { + this.toggle() + } + }, + toasterDestroyedHandler(toaster) { + if (toaster === this.computedToaster) { + this.hide() + } }, onPause() { // Determine time remaining, and then pause timer @@ -299,7 +400,7 @@ export const BToast = /*#__PURE__*/ Vue.extend({ this.resumeDismiss = mathMax(this.computedDuration - passed, MIN_DURATION) } }, - onUnPause() { + onUnpause() { // Restart timer with max of time remaining or 1 second if (this.noAutoHide || this.noHoverPause || !this.resumeDismiss) { this.resumeDismiss = this.dismissStarted = 0 @@ -308,37 +409,16 @@ export const BToast = /*#__PURE__*/ Vue.extend({ this.startDismissTimer() }, onLinkClick() { - // We delay the close to allow time for the - // browser to process the link click + // We delay hiding to give the browser time to process the link click this.$nextTick(() => { requestAF(() => { this.hide() }) }) }, - onBeforeEnter() { - this.isTransitioning = true - }, - onAfterEnter() { - this.isTransitioning = false - const hiddenEvent = this.buildEvent(EVENT_NAME_SHOWN) - this.emitEvent(hiddenEvent) - this.startDismissTimer() - this.setHoverHandler(true) - }, - onBeforeLeave() { - this.isTransitioning = true - }, - onAfterLeave() { - this.isTransitioning = false - this.order = 0 - this.resumeDismiss = this.dismissStarted = 0 - const hiddenEvent = this.buildEvent(EVENT_NAME_HIDDEN) - this.emitEvent(hiddenEvent) - this.doRender = false - }, // Render helper for generating the toast makeToast(h) { + console.log('makeToast', { id: this.toastId }) const { title, slotScope } = this const link = isLink(this) const $headerContent = [] @@ -354,6 +434,7 @@ export const BToast = /*#__PURE__*/ Vue.extend({ $headerContent.push( h(BButtonClose, { staticClass: 'ml-auto mb-1', + props: { disabled: this.isTransitioning }, on: { click: () => { this.hide() @@ -367,7 +448,10 @@ export const BToast = /*#__PURE__*/ Vue.extend({ if ($headerContent.length > 0) { $header = h( 'header', - { staticClass: 'toast-header', class: this.headerClass }, + { + staticClass: 'toast-header', + class: this.headerClass + }, $headerContent ) } @@ -397,11 +481,12 @@ export const BToast = /*#__PURE__*/ Vue.extend({ } }, render(h) { - if (!this.doRender || !this.isMounted) { + console.log('render', { id: this.toastId, isMounted: this.isMounted, isHidden: this.isHidden }) + if (!this.isMounted || this.isHidden) { return h() } - const { order, static: isStatic, isHiding, isStatus } = this + const { order, noFade, static: isStatic, isHiding, isStatus } = this const name = `b-toast-${this[COMPONENT_UID_KEY]}` const $toast = h( @@ -413,7 +498,7 @@ export const BToast = /*#__PURE__*/ Vue.extend({ // If scoped styles are applied and the toast is not static, // make sure the scoped style data attribute is applied ...(isStatic ? {} : this.scopedStyleAttrs), - id: this.safeId('_toast_outer'), + id: this.safeId('__BV_toast_outer_'), role: isHiding ? null : isStatus ? 'status' : 'alert', 'aria-live': isHiding ? null : isStatus ? 'polite' : 'assertive', 'aria-atomic': isHiding ? null : 'true' @@ -425,10 +510,10 @@ export const BToast = /*#__PURE__*/ Vue.extend({ h( BVTransition, { - props: { noFade: this.noFade }, + props: { noFade }, on: this.transitionHandlers }, - [this.localShow ? this.makeToast(h) : h()] + [this.isVisible ? this.makeToast(h) : h()] ) ] ) diff --git a/src/components/toast/toast.spec.js b/src/components/toast/toast.spec.js index 63fcaf746dc..63860df9f8a 100644 --- a/src/components/toast/toast.spec.js +++ b/src/components/toast/toast.spec.js @@ -31,6 +31,8 @@ describe('b-toast', () => { await waitRAF() await waitNT(wrapper.vm) await waitRAF() + await waitNT(wrapper.vm) + await waitRAF() expect(wrapper.element.tagName).toBe('DIV') expect(wrapper.classes()).toContain('b-toast') @@ -40,14 +42,14 @@ describe('b-toast', () => { expect(wrapper.attributes('aria-live')).toEqual('assertive') expect(wrapper.attributes('aria-atomic')).toEqual('true') - expect(wrapper.find('.toast').exists()).toBe(true) const $toast = wrapper.find('.toast') + expect($toast.exists()).toBe(true) expect($toast.element.tagName).toBe('DIV') expect($toast.classes()).toContain('toast') expect($toast.attributes('tabindex')).toEqual('0') - expect($toast.find('.toast-header').exists()).toBe(true) const $header = $toast.find('.toast-header') + expect($header.exists()).toBe(true) expect($header.element.tagName).toBe('HEADER') expect($header.classes().length).toBe(1) expect($header.find('strong').exists()).toBe(true) @@ -58,8 +60,8 @@ describe('b-toast', () => { expect($header.find('button').classes()).toContain('ml-auto') expect($header.find('button').classes()).toContain('mb-1') - expect($toast.find('.toast-body').exists()).toBe(true) const $body = $toast.find('.toast-body') + expect($body.exists()).toBe(true) expect($body.element.tagName).toBe('DIV') expect($body.classes().length).toBe(1) expect($body.text()).toEqual('content') @@ -103,6 +105,8 @@ describe('b-toast', () => { await waitRAF() await waitNT(wrapper.vm) await waitRAF() + await waitNT(wrapper.vm) + await waitRAF() expect(wrapper.element.tagName).toBe('DIV') diff --git a/src/utils/props.js b/src/utils/props.js index 26f15df833a..e0ab491b5df 100644 --- a/src/utils/props.js +++ b/src/utils/props.js @@ -63,7 +63,11 @@ export const copyProps = (props, transformFn = identity) => { // that has props that reference the original prop values export const pluckProps = (keysToPluck, objToPluck, transformFn = identity) => (isArray(keysToPluck) ? keysToPluck.slice() : keys(keysToPluck)).reduce((memo, prop) => { - memo[transformFn(prop)] = objToPluck[prop] + const value = objToPluck[prop] + if (!isUndefined(value)) { + memo[transformFn(prop)] = value + } + return memo }, {}) pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy