UNPKG

vuetify

Version:

Vue Material Component Framework

332 lines (306 loc) 8.86 kB
// Styles import './VDialog.sass' // Components import { VThemeProvider } from '../VThemeProvider' // Mixins import Activatable from '../../mixins/activatable' import Dependent from '../../mixins/dependent' import Detachable from '../../mixins/detachable' import Overlayable from '../../mixins/overlayable' import Returnable from '../../mixins/returnable' import Stackable from '../../mixins/stackable' import Toggleable from '../../mixins/toggleable' // Directives import ClickOutside from '../../directives/click-outside' // Helpers import mixins from '../../util/mixins' import { removed } from '../../util/console' import { convertToUnit, keyCodes, } from '../../util/helpers' // Types import { VNode, VNodeData } from 'vue' const baseMixins = mixins( Activatable, Dependent, Detachable, Overlayable, Returnable, Stackable, Toggleable ) /* @vue/component */ export default baseMixins.extend({ name: 'v-dialog', directives: { ClickOutside }, props: { dark: Boolean, disabled: Boolean, fullscreen: Boolean, light: Boolean, maxWidth: { type: [String, Number], default: 'none', }, noClickAnimation: Boolean, origin: { type: String, default: 'center center', }, persistent: Boolean, retainFocus: { type: Boolean, default: true, }, scrollable: Boolean, transition: { type: [String, Boolean], default: 'dialog-transition', }, width: { type: [String, Number], default: 'auto', }, }, data () { return { activatedBy: null as EventTarget | null, animate: false, animateTimeout: -1, isActive: !!this.value, stackMinZIndex: 200, previousActiveElement: null as HTMLElement | null, } }, computed: { classes (): object { return { [(`v-dialog ${this.contentClass}`).trim()]: true, 'v-dialog--active': this.isActive, 'v-dialog--persistent': this.persistent, 'v-dialog--fullscreen': this.fullscreen, 'v-dialog--scrollable': this.scrollable, 'v-dialog--animated': this.animate, } }, contentClasses (): object { return { 'v-dialog__content': true, 'v-dialog__content--active': this.isActive, } }, hasActivator (): boolean { return Boolean( !!this.$slots.activator || !!this.$scopedSlots.activator ) }, }, watch: { isActive (val) { if (val) { this.show() this.hideScroll() } else { this.removeOverlay() this.unbind() this.previousActiveElement?.focus() } }, fullscreen (val) { if (!this.isActive) return if (val) { this.hideScroll() this.removeOverlay(false) } else { this.showScroll() this.genOverlay() } }, }, created () { /* istanbul ignore next */ if (this.$attrs.hasOwnProperty('full-width')) { removed('full-width', this) } }, beforeMount () { this.$nextTick(() => { this.isBooted = this.isActive this.isActive && this.show() }) }, beforeDestroy () { if (typeof window !== 'undefined') this.unbind() }, methods: { animateClick () { this.animate = false // Needed for when clicking very fast // outside of the dialog this.$nextTick(() => { this.animate = true window.clearTimeout(this.animateTimeout) this.animateTimeout = window.setTimeout(() => (this.animate = false), 150) }) }, closeConditional (e: Event) { const target = e.target as HTMLElement // Ignore the click if the dialog is closed or destroyed, // if it was on an element inside the content, // if it was dragged onto the overlay (#6969), // or if this isn't the topmost dialog (#9907) return !( this._isDestroyed || !this.isActive || this.$refs.content.contains(target) || (this.overlay && target && !this.overlay.$el.contains(target)) ) && this.activeZIndex >= this.getMaxZIndex() }, hideScroll () { if (this.fullscreen) { document.documentElement.classList.add('overflow-y-hidden') } else { Overlayable.options.methods.hideScroll.call(this) } }, show () { !this.fullscreen && !this.hideOverlay && this.genOverlay() // Double nextTick to wait for lazy content to be generated this.$nextTick(() => { this.$nextTick(() => { this.previousActiveElement = document.activeElement as HTMLElement this.$refs.content.focus() this.bind() }) }) }, bind () { window.addEventListener('focusin', this.onFocusin) }, unbind () { window.removeEventListener('focusin', this.onFocusin) }, onClickOutside (e: Event) { this.$emit('click:outside', e) if (this.persistent) { this.noClickAnimation || this.animateClick() } else { this.isActive = false } }, onKeydown (e: KeyboardEvent) { if (e.keyCode === keyCodes.esc && !this.getOpenDependents().length) { if (!this.persistent) { this.isActive = false const activator = this.getActivator() this.$nextTick(() => activator && (activator as HTMLElement).focus()) } else if (!this.noClickAnimation) { this.animateClick() } } this.$emit('keydown', e) }, // On focus change, wrap focus to stay inside the dialog // https://github.com/vuetifyjs/vuetify/issues/6892 onFocusin (e: Event) { if (!e || !this.retainFocus) return const target = e.target as HTMLElement if ( !!target && // It isn't the document or the dialog body ![document, this.$refs.content].includes(target) && // It isn't inside the dialog body !this.$refs.content.contains(target) && // We're the topmost dialog this.activeZIndex >= this.getMaxZIndex() && // It isn't inside a dependent element (like a menu) !this.getOpenDependentElements().some(el => el.contains(target)) // So we must have focused something outside the dialog and its children ) { // Find and focus the first available element inside the dialog const focusable = this.$refs.content.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])') const el = [...focusable].find(el => !el.hasAttribute('disabled')) as HTMLElement | undefined el && el.focus() } }, genContent () { return this.showLazyContent(() => [ this.$createElement(VThemeProvider, { props: { root: true, light: this.light, dark: this.dark, }, }, [ this.$createElement('div', { class: this.contentClasses, attrs: { role: 'document', tabindex: this.isActive ? 0 : undefined, ...this.getScopeIdAttrs(), }, on: { keydown: this.onKeydown }, style: { zIndex: this.activeZIndex }, ref: 'content', }, [this.genTransition()]), ]), ]) }, genTransition () { const content = this.genInnerContent() if (!this.transition) return content return this.$createElement('transition', { props: { name: this.transition, origin: this.origin, appear: true, }, }, [content]) }, genInnerContent () { const data: VNodeData = { class: this.classes, ref: 'dialog', directives: [ { name: 'click-outside', value: { handler: this.onClickOutside, closeConditional: this.closeConditional, include: this.getOpenDependentElements, }, }, { name: 'show', value: this.isActive }, ], style: { transformOrigin: this.origin, }, } if (!this.fullscreen) { data.style = { ...data.style as object, maxWidth: this.maxWidth === 'none' ? undefined : convertToUnit(this.maxWidth), width: this.width === 'auto' ? undefined : convertToUnit(this.width), } } return this.$createElement('div', data, this.getContentSlot()) }, }, render (h): VNode { return h('div', { staticClass: 'v-dialog__container', class: { 'v-dialog__container--attached': this.attach === '' || this.attach === true || this.attach === 'attach', }, attrs: { role: 'dialog' }, }, [ this.genActivator(), this.genContent(), ]) }, })