Skip to content

Commit 44e558f

Browse files
feat(b-form-tags): adds focusin & focusout to wrapper and prevents firing multiple focus/blur events (#6395)
* adds focus and blur events to b-form-tags * fixed clicks near margin of wrapper not focusing input * fixed merging error * Update form-tags.js * Update form-tags.js * Update _form-tags.scss * Update form-tags.js * Update form-tags.js * Update form-tags.js * patched focus & blur on input + focusin & focusout on wrapper; test w.i.p. * attached focusin and focusout to wrapper * updated test * Update events.js * Update form-tags.js * updated docs * Update package.json Co-authored-by: Jacob Müller <jacob.mueller.elz@gmail.com>
1 parent f485b81 commit 44e558f

File tree

7 files changed

+131
-11
lines changed

7 files changed

+131
-11
lines changed

src/components/form-input/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@
7777
"events": [
7878
{
7979
"event": "blur",
80-
"description": "Emitted after the input looses focus",
80+
"description": "Emitted after the input loses focus",
8181
"args": [
8282
{
8383
"arg": "event",

src/components/form-tags/form-tags.js

+45-7
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,14 @@
22
// Based loosely on https://adamwathan.me/renderless-components-in-vuejs/
33
import { Vue } from '../../vue'
44
import { NAME_FORM_TAGS } from '../../constants/components'
5-
import { EVENT_NAME_TAG_STATE, EVENT_OPTIONS_PASSIVE } from '../../constants/events'
5+
import {
6+
EVENT_NAME_BLUR,
7+
EVENT_NAME_FOCUS,
8+
EVENT_NAME_FOCUSIN,
9+
EVENT_NAME_FOCUSOUT,
10+
EVENT_NAME_TAG_STATE,
11+
EVENT_OPTIONS_PASSIVE
12+
} from '../../constants/events'
613
import { CODE_BACKSPACE, CODE_DELETE, CODE_ENTER } from '../../constants/key-codes'
714
import {
815
PROP_TYPE_ARRAY,
@@ -24,7 +31,7 @@ import { identity } from '../../utils/identity'
2431
import { isEvent, isNumber, isString } from '../../utils/inspect'
2532
import { looseEqual } from '../../utils/loose-equal'
2633
import { makeModelMixin } from '../../utils/model'
27-
import { pick, sortKeys } from '../../utils/object'
34+
import { omit, pick, sortKeys } from '../../utils/object'
2835
import { hasPropFunction, makeProp, makePropsConfigurable } from '../../utils/props'
2936
import { escapeRegExp, toString, trim, trimLeft } from '../../utils/string'
3037
import { formControlMixin, props as formControlProps } from '../../mixins/form-control'
@@ -154,7 +161,8 @@ export const BFormTags = /*#__PURE__*/ Vue.extend({
154161
// Tags that were removed
155162
removedTags: [],
156163
// Populated when tags are parsed
157-
tagsState: cleanTagsState()
164+
tagsState: cleanTagsState(),
165+
focusState: null
158166
}
159167
},
160168
computed: {
@@ -180,9 +188,11 @@ export const BFormTags = /*#__PURE__*/ Vue.extend({
180188
},
181189
computedInputHandlers() {
182190
return {
183-
...this.bvListeners,
184-
input: this.onInputInput,
191+
...omit(this.bvListeners, [EVENT_NAME_FOCUSIN, EVENT_NAME_FOCUSOUT]),
192+
blur: this.onInputBlur,
185193
change: this.onInputChange,
194+
focus: this.onInputFocus,
195+
input: this.onInputInput,
186196
keydown: this.onInputKeydown,
187197
reset: this.reset
188198
}
@@ -411,11 +421,39 @@ export const BFormTags = /*#__PURE__*/ Vue.extend({
411421
})
412422
}
413423
},
414-
onFocusin() {
424+
onInputFocus(event) {
425+
if (this.focusState !== 'out') {
426+
this.focusState = 'in'
427+
this.$nextTick(() => {
428+
requestAF(() => {
429+
if (this.hasFocus) {
430+
this.$emit(EVENT_NAME_FOCUS, event)
431+
this.focusState = null
432+
}
433+
})
434+
})
435+
}
436+
},
437+
onInputBlur(event) {
438+
if (this.focusState !== 'in') {
439+
this.focusState = 'out'
440+
this.$nextTick(() => {
441+
requestAF(() => {
442+
if (!this.hasFocus) {
443+
this.$emit(EVENT_NAME_BLUR, event)
444+
this.focusState = null
445+
}
446+
})
447+
})
448+
}
449+
},
450+
onFocusin(event) {
415451
this.hasFocus = true
452+
this.$emit(EVENT_NAME_FOCUSIN, event)
416453
},
417-
onFocusout() {
454+
onFocusout(event) {
418455
this.hasFocus = false
456+
this.$emit(EVENT_NAME_FOCUSOUT, event)
419457
},
420458
handleAutofocus() {
421459
this.$nextTick(() => {

src/components/form-tags/form-tags.spec.js

+38-1
Original file line numberDiff line numberDiff line change
@@ -843,7 +843,7 @@ describe('form-tags', () => {
843843
wrapper.destroy()
844844
})
845845

846-
it('emits focus and blur events', async () => {
846+
it('emits focus and blur events when wrapper gains/loses focus', async () => {
847847
const onFocus = jest.fn()
848848
const onBlur = jest.fn()
849849
const wrapper = mount(BFormTags, {
@@ -864,6 +864,7 @@ describe('form-tags', () => {
864864

865865
$input.trigger('focus')
866866
$input.trigger('focusin')
867+
867868
await waitNT(wrapper.vm)
868869
await waitRAF()
869870

@@ -879,4 +880,40 @@ describe('form-tags', () => {
879880

880881
wrapper.destroy()
881882
})
883+
884+
it('emits focusin and focusout when internal focus changes', async () => {
885+
const onFocusIn = jest.fn()
886+
const onFocusOut = jest.fn()
887+
const wrapper = mount(BFormTags, {
888+
propsData: {
889+
value: ['apple', 'orange']
890+
},
891+
listeners: {
892+
focusin: onFocusIn,
893+
focusout: onFocusOut
894+
}
895+
})
896+
897+
expect(onFocusIn).not.toHaveBeenCalled()
898+
expect(onFocusOut).not.toHaveBeenCalled()
899+
900+
const $input = wrapper.find('input')
901+
const $tag = wrapper.find('.b-form-tag')
902+
903+
$input.trigger('focusin')
904+
905+
await waitNT(wrapper.vm)
906+
await waitRAF()
907+
908+
expect(onFocusIn).toHaveBeenCalledTimes(1)
909+
expect(onFocusOut).not.toHaveBeenCalled()
910+
911+
$tag.trigger('focusin')
912+
$input.trigger('focusout')
913+
await waitNT(wrapper.vm)
914+
await waitRAF()
915+
916+
expect(onFocusIn).toHaveBeenCalledTimes(2)
917+
expect(onFocusOut).toHaveBeenCalledTimes(1)
918+
})
882919
})

src/components/form-tags/package.json

+44
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,50 @@
130130
}
131131
],
132132
"events": [
133+
{
134+
"event": "blur",
135+
"description": "Emitted when component loses focus",
136+
"args": [
137+
{
138+
"arg": "event",
139+
"type": "FocusEvent",
140+
"description": "Native blur event (before any formatting)"
141+
}
142+
]
143+
},
144+
{
145+
"event": "focus",
146+
"description": "Emitted when component gains focus",
147+
"args": [
148+
{
149+
"arg": "event",
150+
"type": "FocusEvent",
151+
"description": "Native focus event (before any formatting)"
152+
}
153+
]
154+
},
155+
{
156+
"event": "focusin",
157+
"description": "Emitted when internal elements of component gain focus.",
158+
"args": [
159+
{
160+
"arg": "event",
161+
"type": "FocusEvent",
162+
"description": "Native focusin event (before any formatting)"
163+
}
164+
]
165+
},
166+
{
167+
"event": "focusout",
168+
"description": "Emitted when internal elements of component lose focus.",
169+
"args": [
170+
{
171+
"arg": "event",
172+
"type": "FocusEvent",
173+
"description": "Native focusout event (before any formatting)"
174+
}
175+
]
176+
},
133177
{
134178
"event": "input",
135179
"description": "Emitted when the tags changes. Updates the v-model",

src/components/form-textarea/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@
7373
"events": [
7474
{
7575
"event": "blur",
76-
"description": "Emitted after the textarea looses focus",
76+
"description": "Emitted after the textarea loses focus",
7777
"args": [
7878
{
7979
"arg": "event",

src/components/tooltip/helpers/bv-tooltip.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -705,7 +705,7 @@ export const BVTooltip = /*#__PURE__*/ Vue.extend({
705705
eventOn(el, 'focusin', this.handleEvent, EVENT_OPTIONS_NO_CAPTURE)
706706
eventOn(el, 'focusout', this.handleEvent, EVENT_OPTIONS_NO_CAPTURE)
707707
} else if (trigger === 'blur') {
708-
// Used to close $tip when element looses focus
708+
// Used to close $tip when element loses focus
709709
/* istanbul ignore next */
710710
eventOn(el, 'focusout', this.handleEvent, EVENT_OPTIONS_NO_CAPTURE)
711711
} else if (trigger === 'hover') {

src/constants/events.js

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export const EVENT_NAME_ENABLE = 'enable'
1616
export const EVENT_NAME_ENABLED = 'enabled'
1717
export const EVENT_NAME_FILTERED = 'filtered'
1818
export const EVENT_NAME_FIRST = 'first'
19+
export const EVENT_NAME_FOCUS = 'focus'
1920
export const EVENT_NAME_FOCUSIN = 'focusin'
2021
export const EVENT_NAME_FOCUSOUT = 'focusout'
2122
export const EVENT_NAME_HEAD_CLICKED = 'head-clicked'

0 commit comments

Comments
 (0)