Skip to content

feat(b-form-tags): adds focusin & focusout to wrapper and prevents firing multiple focus/blur events #6395

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 19 commits into from
Feb 3, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/components/form-input/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@
"events": [
{
"event": "blur",
"description": "Emitted after the input looses focus",
"description": "Emitted after the input loses focus",
"args": [
{
"arg": "event",
Expand Down
52 changes: 45 additions & 7 deletions src/components/form-tags/form-tags.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@
// Based loosely on https://adamwathan.me/renderless-components-in-vuejs/
import { Vue } from '../../vue'
import { NAME_FORM_TAGS } from '../../constants/components'
import { EVENT_NAME_TAG_STATE, EVENT_OPTIONS_PASSIVE } from '../../constants/events'
import {
EVENT_NAME_BLUR,
EVENT_NAME_FOCUS,
EVENT_NAME_FOCUSIN,
EVENT_NAME_FOCUSOUT,
EVENT_NAME_TAG_STATE,
EVENT_OPTIONS_PASSIVE
} from '../../constants/events'
import { CODE_BACKSPACE, CODE_DELETE, CODE_ENTER } from '../../constants/key-codes'
import {
PROP_TYPE_ARRAY,
Expand All @@ -24,7 +31,7 @@ import { identity } from '../../utils/identity'
import { isEvent, isNumber, isString } from '../../utils/inspect'
import { looseEqual } from '../../utils/loose-equal'
import { makeModelMixin } from '../../utils/model'
import { pick, sortKeys } from '../../utils/object'
import { omit, pick, sortKeys } from '../../utils/object'
import { hasPropFunction, makeProp, makePropsConfigurable } from '../../utils/props'
import { escapeRegExp, toString, trim, trimLeft } from '../../utils/string'
import { formControlMixin, props as formControlProps } from '../../mixins/form-control'
Expand Down Expand Up @@ -154,7 +161,8 @@ export const BFormTags = /*#__PURE__*/ Vue.extend({
// Tags that were removed
removedTags: [],
// Populated when tags are parsed
tagsState: cleanTagsState()
tagsState: cleanTagsState(),
focusState: null
}
},
computed: {
Expand All @@ -180,9 +188,11 @@ export const BFormTags = /*#__PURE__*/ Vue.extend({
},
computedInputHandlers() {
return {
...this.bvListeners,
input: this.onInputInput,
...omit(this.bvListeners, [EVENT_NAME_FOCUSIN, EVENT_NAME_FOCUSOUT]),
blur: this.onInputBlur,
change: this.onInputChange,
focus: this.onInputFocus,
input: this.onInputInput,
keydown: this.onInputKeydown,
reset: this.reset
}
Expand Down Expand Up @@ -411,11 +421,39 @@ export const BFormTags = /*#__PURE__*/ Vue.extend({
})
}
},
onFocusin() {
onInputFocus(event) {
if (this.focusState !== 'out') {
this.focusState = 'in'
this.$nextTick(() => {
requestAF(() => {
if (this.hasFocus) {
this.$emit(EVENT_NAME_FOCUS, event)
this.focusState = null
}
})
})
}
},
onInputBlur(event) {
if (this.focusState !== 'in') {
this.focusState = 'out'
this.$nextTick(() => {
requestAF(() => {
if (!this.hasFocus) {
this.$emit(EVENT_NAME_BLUR, event)
this.focusState = null
}
})
})
}
},
onFocusin(event) {
this.hasFocus = true
this.$emit(EVENT_NAME_FOCUSIN, event)
},
onFocusout() {
onFocusout(event) {
this.hasFocus = false
this.$emit(EVENT_NAME_FOCUSOUT, event)
},
handleAutofocus() {
this.$nextTick(() => {
Expand Down
39 changes: 38 additions & 1 deletion src/components/form-tags/form-tags.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -843,7 +843,7 @@ describe('form-tags', () => {
wrapper.destroy()
})

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

$input.trigger('focus')
$input.trigger('focusin')

await waitNT(wrapper.vm)
await waitRAF()

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

wrapper.destroy()
})

it('emits focusin and focusout when internal focus changes', async () => {
const onFocusIn = jest.fn()
const onFocusOut = jest.fn()
const wrapper = mount(BFormTags, {
propsData: {
value: ['apple', 'orange']
},
listeners: {
focusin: onFocusIn,
focusout: onFocusOut
}
})

expect(onFocusIn).not.toHaveBeenCalled()
expect(onFocusOut).not.toHaveBeenCalled()

const $input = wrapper.find('input')
const $tag = wrapper.find('.b-form-tag')

$input.trigger('focusin')

await waitNT(wrapper.vm)
await waitRAF()

expect(onFocusIn).toHaveBeenCalledTimes(1)
expect(onFocusOut).not.toHaveBeenCalled()

$tag.trigger('focusin')
$input.trigger('focusout')
await waitNT(wrapper.vm)
await waitRAF()

expect(onFocusIn).toHaveBeenCalledTimes(2)
expect(onFocusOut).toHaveBeenCalledTimes(1)
})
})
44 changes: 44 additions & 0 deletions src/components/form-tags/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,50 @@
}
],
"events": [
{
"event": "blur",
"description": "Emitted when component loses focus",
"args": [
{
"arg": "event",
"type": "FocusEvent",
"description": "Native blur event (before any formatting)"
}
]
},
{
"event": "focus",
"description": "Emitted when component gains focus",
"args": [
{
"arg": "event",
"type": "FocusEvent",
"description": "Native focus event (before any formatting)"
}
]
},
{
"event": "focusin",
"description": "Emitted when internal elements of component gain focus.",
"args": [
{
"arg": "event",
"type": "FocusEvent",
"description": "Native focusin event (before any formatting)"
}
]
},
{
"event": "focusout",
"description": "Emitted when internal elements of component lose focus.",
"args": [
{
"arg": "event",
"type": "FocusEvent",
"description": "Native focusout event (before any formatting)"
}
]
},
{
"event": "input",
"description": "Emitted when the tags changes. Updates the v-model",
Expand Down
2 changes: 1 addition & 1 deletion src/components/form-textarea/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@
"events": [
{
"event": "blur",
"description": "Emitted after the textarea looses focus",
"description": "Emitted after the textarea loses focus",
"args": [
{
"arg": "event",
Expand Down
2 changes: 1 addition & 1 deletion src/components/tooltip/helpers/bv-tooltip.js
Original file line number Diff line number Diff line change
Expand Up @@ -705,7 +705,7 @@ export const BVTooltip = /*#__PURE__*/ Vue.extend({
eventOn(el, 'focusin', this.handleEvent, EVENT_OPTIONS_NO_CAPTURE)
eventOn(el, 'focusout', this.handleEvent, EVENT_OPTIONS_NO_CAPTURE)
} else if (trigger === 'blur') {
// Used to close $tip when element looses focus
// Used to close $tip when element loses focus
/* istanbul ignore next */
eventOn(el, 'focusout', this.handleEvent, EVENT_OPTIONS_NO_CAPTURE)
} else if (trigger === 'hover') {
Expand Down
1 change: 1 addition & 0 deletions src/constants/events.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export const EVENT_NAME_ENABLE = 'enable'
export const EVENT_NAME_ENABLED = 'enabled'
export const EVENT_NAME_FILTERED = 'filtered'
export const EVENT_NAME_FIRST = 'first'
export const EVENT_NAME_FOCUS = 'focus'
export const EVENT_NAME_FOCUSIN = 'focusin'
export const EVENT_NAME_FOCUSOUT = 'focusout'
export const EVENT_NAME_HEAD_CLICKED = 'head-clicked'
Expand Down