🌐 AI搜索 & 代理 主页
Skip to content

Commit 6f2827e

Browse files
tmorehousejacobmllr95
authored andcommitted
feat(modal): add prop for auto focusing one of the built in-buttons once shown (closes #3945) (#3979)
* feat(modal): add prop for auto focusing one of the built in buttons on shown * Update modal.js * Update modal.js * Update modal.js * Update modal.js * Update modal.js * Update modal.js * Update modal.js * Update modal.js * Update modal.js * Update modal.js * Update modal.spec.js * Update modal.spec.js * Update modal.spec.js * Update README.md * Update README.md * Update modal.js * Update modal.js
1 parent 7418f08 commit 6f2827e

File tree

3 files changed

+90
-27
lines changed

3 files changed

+90
-27
lines changed

src/components/modal/README.md

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -999,9 +999,18 @@ focus a form control when the modal opens. Note that the `autofocus` prop will n
999999
`b-modal` if the `static` prop is used without the `lazy` prop set, as `autofocus` happens when the
10001000
`b-form-*` controls are _mounted in the DOM_.
10011001

1002-
**Note:** it is **not recommended** to autofocus an input inside a modal for accessibility reasons,
1003-
as screen reader users will not know the context of where the input is. It is best to let
1004-
`<b-modal>` focus the modal's container and then allow the user to tab into the input.
1002+
If you want to auto focus one of the _built-in_ modal buttons (`ok`, `cancel` or the header `close`
1003+
button, you can set the prop `auto-focus-button` to one of the values `'ok'`, `'cancel'` or
1004+
`'close'` and `<b-modal>` will focus the specified button if it exists. This feature is also
1005+
available for modal message boxes.
1006+
1007+
<p class="alert alert-warning">
1008+
<strong>Note:</strong> it is <strong>not recommended</strong> to autofocus an input or control
1009+
inside of a modal for accessibility reasons, as screen reader users will not know the context of
1010+
where the input is (the announcement of the modal may not be spoken). It is best to let
1011+
<code>&lt;b-modal&gt;</code> focus the modal's container, allowing the modal information to be
1012+
spoken to the user, and then allow the user to tab into the input.
1013+
</p>
10051014

10061015
### Returning focus to the triggering element
10071016

src/components/modal/modal.js

Lines changed: 66 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,30 @@
11
import 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'
82
import BVTransition from '../../utils/bv-transition'
93
import KeyCodes from '../../utils/key-codes'
104
import 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'
146
import { 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'
1517
import { 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'
1724
import { BButton } from '../button/button'
1825
import { 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,

src/components/modal/modal.spec.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1049,7 +1049,9 @@ describe('modal', () => {
10491049
await waitNT(wrapper.vm)
10501050
await waitRAF()
10511051
await waitNT(wrapper.vm)
1052+
await waitRAF()
10521053
await waitNT(wrapper.vm)
1054+
await waitRAF()
10531055

10541056
// Modal should now be open
10551057
expect($modal.element.style.display).toEqual('block')
@@ -1065,7 +1067,9 @@ describe('modal', () => {
10651067
await waitNT(wrapper.vm)
10661068
await waitRAF()
10671069
await waitNT(wrapper.vm)
1070+
await waitRAF()
10681071
await waitNT(wrapper.vm)
1072+
await waitRAF()
10691073

10701074
// Modal should now be closed
10711075
expect($modal.element.style.display).toEqual('none')
@@ -1103,7 +1107,9 @@ describe('modal', () => {
11031107
await waitNT(wrapper.vm)
11041108
await waitRAF()
11051109
await waitNT(wrapper.vm)
1110+
await waitRAF()
11061111
await waitNT(wrapper.vm)
1112+
await waitRAF()
11071113

11081114
const $button = wrapper.find('button.trigger')
11091115
expect($button.exists()).toBe(true)
@@ -1131,7 +1137,9 @@ describe('modal', () => {
11311137
await waitNT(wrapper.vm)
11321138
await waitRAF()
11331139
await waitNT(wrapper.vm)
1140+
await waitRAF()
11341141
await waitNT(wrapper.vm)
1142+
await waitRAF()
11351143

11361144
// Modal should now be open
11371145
expect($modal.element.style.display).toEqual('block')
@@ -1148,7 +1156,9 @@ describe('modal', () => {
11481156
await waitNT(wrapper.vm)
11491157
await waitRAF()
11501158
await waitNT(wrapper.vm)
1159+
await waitRAF()
11511160
await waitNT(wrapper.vm)
1161+
await waitRAF()
11521162

11531163
// Modal should now be closed
11541164
expect($modal.element.style.display).toEqual('none')
@@ -1181,7 +1191,9 @@ describe('modal', () => {
11811191
await waitNT(wrapper.vm)
11821192
await waitRAF()
11831193
await waitNT(wrapper.vm)
1194+
await waitRAF()
11841195
await waitNT(wrapper.vm)
1196+
await waitRAF()
11851197

11861198
const $button = wrapper.find('button.trigger')
11871199
expect($button.exists()).toBe(true)

0 commit comments

Comments
 (0)