11import Vue from '../../utils/vue'
2- import { modalManager } from './helpers/modal-manager'
3- import { BvModalEvent } from './helpers/bv-modal-event.class'
4- import idMixin from '../../mixins/id'
5- import listenOnRootMixin from '../../mixins/listen-on-root'
6- import normalizeSlotMixin from '../../mixins/normalize-slot'
7- import scopedStyleAttrsMixin from '../../mixins/scoped-style-attrs'
82import BVTransition from '../../utils/bv-transition'
93import KeyCodes from '../../utils/key-codes'
104import observeDom from '../../utils/observe-dom'
11- import { BTransporterSingle } from '../../utils/transporter'
12- import { isBrowser } from '../../utils/env'
13- import { isString } from '../../utils/inspect'
5+ import { arrayIncludes } from '../../utils/array'
146import { getComponentConfig } from '../../utils/config'
7+ import {
8+ contains ,
9+ eventOff ,
10+ eventOn ,
11+ isVisible ,
12+ requestAF ,
13+ select ,
14+ selectAll
15+ } from '../../utils/dom'
16+ import { isBrowser } from '../../utils/env'
1517import { stripTags } from '../../utils/html'
16- import { contains , eventOff , eventOn , isVisible , select , selectAll } from '../../utils/dom'
18+ import { isString , isUndefinedOrNull } from '../../utils/inspect'
19+ import { BTransporterSingle } from '../../utils/transporter'
20+ import idMixin from '../../mixins/id'
21+ import listenOnRootMixin from '../../mixins/listen-on-root'
22+ import normalizeSlotMixin from '../../mixins/normalize-slot'
23+ import scopedStyleAttrsMixin from '../../mixins/scoped-style-attrs'
1724import { BButton } from '../button/button'
1825import { BButtonClose } from '../button/button-close'
26+ import { modalManager } from './helpers/modal-manager'
27+ import { BvModalEvent } from './helpers/bv-modal-event.class'
1928
2029// --- Constants ---
2130
@@ -255,6 +264,14 @@ export const props = {
255264 static : {
256265 type : Boolean ,
257266 default : false
267+ } ,
268+ autoFocusButton : {
269+ type : String ,
270+ default : null ,
271+ validator : val => {
272+ /* istanbul ignore next */
273+ return isUndefinedOrNull ( val ) || arrayIncludes ( [ 'ok' , 'cancel' , 'close' ] , val )
274+ }
258275 }
259276}
260277
@@ -576,10 +593,18 @@ export const BModal = /*#__PURE__*/ Vue.extend({
576593 this . checkModalOverflow ( )
577594 this . isShow = true
578595 this . isTransitioning = false
579- this . $nextTick ( ( ) => {
596+ // We use `requestAF()` to allow transition hooks to complete
597+ // before passing control over to the other handlers
598+ // This will allow users to not have to use `$nextTick()` or `requestAF()`
599+ // when trying to pre-focus an element
600+ requestAF ( ( ) => {
580601 this . emitEvent ( this . buildEvent ( 'shown' ) )
581- this . focusFirst ( )
582602 this . setEnforceFocus ( true )
603+ this . $nextTick ( ( ) => {
604+ // Delayed in a `$nextTick()` to allow users time to pre-focus
605+ // an element if the wish
606+ this . focusFirst ( )
607+ } )
583608 } )
584609 } ,
585610 onBeforeLeave ( ) {
@@ -731,18 +756,32 @@ export const BModal = /*#__PURE__*/ Vue.extend({
731756 focusFirst ( ) {
732757 // Don't try and focus if we are SSR
733758 if ( isBrowser ) {
734- const modal = this . $refs . modal
735- const content = this . $refs . content
736- const activeElement = this . getActiveElement ( )
737- // If the modal contains the activeElement, we don't do anything
738- if ( modal && content && ! ( activeElement && contains ( content , activeElement ) ) ) {
739- // Make sure top of modal is showing (if longer than the viewport)
740- // and focus the modal content wrapper
741- this . $nextTick ( ( ) => {
742- modal . scrollTop = 0
743- content . focus ( )
744- } )
745- }
759+ requestAF ( ( ) => {
760+ const modal = this . $refs . modal
761+ const content = this . $refs . content
762+ const activeElement = this . getActiveElement ( )
763+ // If the modal contains the activeElement, we don't do anything
764+ if ( modal && content && ! ( activeElement && contains ( content , activeElement ) ) ) {
765+ const ok = this . $refs [ 'ok-button' ]
766+ const cancel = this . $refs [ 'cancel-button' ]
767+ const close = this . $refs [ 'close-button' ]
768+ // Focus the appropriate button or modal content wrapper
769+ const autoFocus = this . autoFocusButton
770+ const el =
771+ autoFocus === 'ok' && ok
772+ ? ok . $el || ok
773+ : autoFocus === 'cancel' && cancel
774+ ? cancel . $el || cancel
775+ : autoFocus === 'close' && close
776+ ? close . $el || close
777+ : content
778+ // Make sure top of modal is showing (if longer than the viewport)
779+ if ( el === content ) {
780+ modal . scrollTop = 0
781+ }
782+ attemptFocus ( el )
783+ }
784+ } )
746785 }
747786 } ,
748787 returnFocusTo ( ) {
@@ -777,6 +816,7 @@ export const BModal = /*#__PURE__*/ Vue.extend({
777816 closeButton = h (
778817 BButtonClose ,
779818 {
819+ ref : 'close-button' ,
780820 props : {
781821 disabled : this . isTransitioning ,
782822 ariaLabel : this . headerCloseLabel ,
@@ -840,6 +880,7 @@ export const BModal = /*#__PURE__*/ Vue.extend({
840880 cancelButton = h (
841881 BButton ,
842882 {
883+ ref : 'cancel-button' ,
843884 props : {
844885 variant : this . cancelVariant ,
845886 size : this . buttonSize ,
@@ -857,6 +898,7 @@ export const BModal = /*#__PURE__*/ Vue.extend({
857898 const okButton = h (
858899 BButton ,
859900 {
901+ ref : 'ok-button' ,
860902 props : {
861903 variant : this . okVariant ,
862904 size : this . buttonSize ,
0 commit comments