mirror of
https://github.com/element-plus/element-plus.git
synced 2024-11-27 02:01:15 +08:00
Feat/implementing trigger for popper (#214)
* feat/implementing-trigger-for-popper - Implementing trigger for popper - Refactors against click-outside * feat(popper): implemented trigger for popper
This commit is contained in:
parent
9ca93eaa40
commit
d0b37cdf5f
@ -3,7 +3,7 @@ import { on } from '@element-plus/utils/dom'
|
||||
|
||||
import type { DirectiveBinding, ObjectDirective, ComponentPublicInstance } from 'vue'
|
||||
|
||||
type DocumentHandler = <T extends Event>(mouseup: T, mousedown: T) => void;
|
||||
type DocumentHandler = <T extends MouseEvent>(mouseup: T, mousedown: T) => void;
|
||||
|
||||
type FlushList = Map<
|
||||
HTMLElement,
|
||||
@ -15,11 +15,11 @@ type FlushList = Map<
|
||||
|
||||
const nodeList: FlushList = new Map()
|
||||
|
||||
let startClick: Event
|
||||
let startClick: MouseEvent
|
||||
|
||||
if (!isServer) {
|
||||
on(document, 'mousedown', e => (startClick = e))
|
||||
on(document, 'mouseup', e => {
|
||||
on(document, 'mousedown', (e: MouseEvent) => (startClick = e))
|
||||
on(document, 'mouseup', (e: MouseEvent) => {
|
||||
for (const { documentHandler } of nodeList.values()) {
|
||||
documentHandler(e, startClick)
|
||||
}
|
||||
@ -30,21 +30,44 @@ function createDocumentHandler(
|
||||
el: HTMLElement,
|
||||
binding: DirectiveBinding,
|
||||
): DocumentHandler {
|
||||
let excludes: HTMLElement[] = []
|
||||
if (Array.isArray(binding.arg)) {
|
||||
excludes = binding.arg
|
||||
} else {
|
||||
// due to current implementation on binding type is wrong the type casting is necessary here
|
||||
excludes.push(binding.arg as unknown as HTMLElement)
|
||||
}
|
||||
return function(mouseup, mousedown) {
|
||||
const popperRef = (binding.instance as ComponentPublicInstance<{
|
||||
popperRef: Nullable<HTMLElement>
|
||||
}>).popperRef
|
||||
const mouseUpTarget = mouseup.target as Node
|
||||
const mouseDownTarget = mousedown.target as Node
|
||||
const isBound = !binding || !binding.instance
|
||||
const isTargetExists = !mouseUpTarget || !mouseDownTarget
|
||||
const isContainedByEl = el.contains(mouseUpTarget) || el.contains(mouseDownTarget)
|
||||
const isSelf = el === mouseUpTarget
|
||||
|
||||
const isTargetExcluded =
|
||||
( excludes.length &&
|
||||
excludes.some(item => item?.contains(mouseUpTarget))
|
||||
) || (
|
||||
excludes.length && excludes.includes(mouseDownTarget as HTMLElement)
|
||||
)
|
||||
const isContainedByPopper = (
|
||||
popperRef &&
|
||||
(
|
||||
popperRef.contains(mouseUpTarget) ||
|
||||
popperRef.contains(mouseDownTarget)
|
||||
)
|
||||
)
|
||||
if (
|
||||
!binding ||
|
||||
!binding.instance ||
|
||||
!mouseup.target ||
|
||||
!mousedown.target ||
|
||||
el.contains(mouseup.target as Node) ||
|
||||
el.contains(mousedown.target as Node) ||
|
||||
el === mouseup.target ||
|
||||
(popperRef &&
|
||||
(popperRef.contains(mouseup.target as Node) ||
|
||||
popperRef.contains(mousedown.target as Node)))
|
||||
isBound ||
|
||||
isTargetExists ||
|
||||
isContainedByEl ||
|
||||
isSelf ||
|
||||
isTargetExcluded ||
|
||||
isContainedByPopper
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
@ -3,21 +3,22 @@ import { on, off } from '@element-plus/utils/dom'
|
||||
|
||||
import type { Ref } from 'vue'
|
||||
|
||||
type Events = Array<{
|
||||
export type Event = {
|
||||
name: string
|
||||
handler: (...args: any[]) => any
|
||||
}>
|
||||
}
|
||||
|
||||
export default (el: Ref<HTMLElement>, events: Events) => {
|
||||
events.map(({ name, handler }) => {
|
||||
on(el.value, name, handler)
|
||||
})
|
||||
export default (el: Ref<HTMLElement>, events: Event[]) => {
|
||||
|
||||
watch(el, (_, __, onCleanup) => {
|
||||
onCleanup(() => {
|
||||
watch(el, val => {
|
||||
if (val) {
|
||||
events.map(({ name, handler }) => {
|
||||
on(el.value, name, handler)
|
||||
})
|
||||
} else {
|
||||
events.map(({ name, handler }) => {
|
||||
off(el.value, name, handler)
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -17,6 +17,9 @@ const selector = '[role="tooltip"]'
|
||||
const TEST_TRIGGER = 'test-trigger'
|
||||
const MOUSE_ENTER_EVENT = 'mouseenter'
|
||||
const MOUSE_LEAVE_EVENT = 'mouseleave'
|
||||
const CLICK_EVENT = 'click'
|
||||
const FOCUS_EVENT = 'focus'
|
||||
const BLUR_EVENT = 'blur'
|
||||
const DISPLAY_NONE = 'display: none'
|
||||
|
||||
const Wrapped = (props: UnKnownProps, { slots }) => h('div', h(ElPopper, props, slots))
|
||||
@ -81,7 +84,6 @@ describe('Popper.vue', () => {
|
||||
|
||||
test('append to body', () => {
|
||||
let wrapper = _mount()
|
||||
const selector = '[role="tooltip"]'
|
||||
expect(wrapper.find(selector).exists()).toBe(false)
|
||||
/**
|
||||
* Current layout of `ElPopper`
|
||||
@ -199,4 +201,113 @@ describe('Popper.vue', () => {
|
||||
// the only way to test this is by providing an error handler to catch it
|
||||
expect(errorHandler).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
describe('trigger', () => {
|
||||
|
||||
test('should work with click trigger', async () => {
|
||||
const wrapper = _mount({
|
||||
trigger: ['click'],
|
||||
appendToBody: false,
|
||||
})
|
||||
await nextTick()
|
||||
|
||||
const trigger = wrapper.find(`.${TEST_TRIGGER}`)
|
||||
const popper = wrapper.findComponent(ElPopper)
|
||||
expect(popper.vm.visible).toBe(false)
|
||||
// for now triggering event on element via DOMWrapper is not available so we need to apply
|
||||
// old way
|
||||
await trigger.trigger(CLICK_EVENT)
|
||||
|
||||
expect(popper.vm.visible).toBe(true)
|
||||
|
||||
await trigger.trigger(MOUSE_ENTER_EVENT)
|
||||
expect(popper.vm.visible).toBe(true)
|
||||
await trigger.trigger(FOCUS_EVENT)
|
||||
expect(popper.vm.visible).toBe(true)
|
||||
})
|
||||
|
||||
test('should work with hover trigger', async () => {
|
||||
const wrapper = _mount({
|
||||
trigger: ['hover'],
|
||||
appendToBody: false,
|
||||
})
|
||||
await nextTick()
|
||||
|
||||
const trigger = wrapper.find(`.${TEST_TRIGGER}`)
|
||||
const popper = wrapper.findComponent(ElPopper)
|
||||
expect(popper.vm.visible).toBe(false)
|
||||
// for now triggering event on element via DOMWrapper is not available so we need to apply
|
||||
// old way
|
||||
await trigger.trigger(MOUSE_ENTER_EVENT)
|
||||
|
||||
expect(popper.vm.visible).toBe(true)
|
||||
|
||||
await trigger.trigger(MOUSE_LEAVE_EVENT)
|
||||
expect(popper.vm.visible).toBe(false)
|
||||
|
||||
await trigger.trigger(FOCUS_EVENT)
|
||||
expect(popper.vm.visible).toBe(false)
|
||||
|
||||
await trigger.trigger(CLICK_EVENT)
|
||||
expect(popper.vm.visible).toBe(false)
|
||||
})
|
||||
|
||||
test('should work with focus trigger', async () => {
|
||||
const wrapper = _mount({
|
||||
trigger: [FOCUS_EVENT],
|
||||
appendToBody: false,
|
||||
})
|
||||
await nextTick()
|
||||
|
||||
|
||||
const trigger = wrapper.find(`.${TEST_TRIGGER}`)
|
||||
const popper = wrapper.findComponent(ElPopper)
|
||||
expect(popper.vm.visible).toBe(false)
|
||||
// for now triggering event on element via DOMWrapper is not available so we need to apply
|
||||
// old way
|
||||
await trigger.trigger(FOCUS_EVENT)
|
||||
|
||||
expect(popper.vm.visible).toBe(true)
|
||||
|
||||
await trigger.trigger(BLUR_EVENT)
|
||||
expect(popper.vm.visible).toBe(false)
|
||||
|
||||
await trigger.trigger(MOUSE_ENTER_EVENT)
|
||||
expect(popper.vm.visible).toBe(false)
|
||||
|
||||
await trigger.trigger(CLICK_EVENT)
|
||||
expect(popper.vm.visible).toBe(false)
|
||||
})
|
||||
|
||||
test('combined trigger', async () => {
|
||||
const wrapper = _mount({
|
||||
trigger: [FOCUS_EVENT, CLICK_EVENT, 'hover'],
|
||||
appendToBody: false,
|
||||
})
|
||||
await nextTick()
|
||||
|
||||
const trigger = wrapper.find(`.${TEST_TRIGGER}`)
|
||||
const popper = wrapper.findComponent(ElPopper)
|
||||
expect(popper.vm.visible).toBe(false)
|
||||
// for now triggering event on element via DOMWrapper is not available so we need to apply
|
||||
// old way
|
||||
await trigger.trigger(CLICK_EVENT)
|
||||
|
||||
expect(popper.vm.visible).toBe(true)
|
||||
|
||||
await trigger.trigger(BLUR_EVENT)
|
||||
expect(popper.vm.visible).toBe(false)
|
||||
|
||||
await trigger.trigger(MOUSE_ENTER_EVENT)
|
||||
expect(popper.vm.visible).toBe(true)
|
||||
|
||||
await trigger.trigger(CLICK_EVENT)
|
||||
expect(popper.vm.visible).toBe(false)
|
||||
|
||||
await trigger.trigger(FOCUS_EVENT)
|
||||
expect(popper.vm.visible).toBe(true)
|
||||
|
||||
await trigger.trigger(CLICK_EVENT)
|
||||
expect(popper.vm.visible).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -153,31 +153,7 @@ export default defineComponent({
|
||||
// this is a reference that we need to pass down to child component
|
||||
// to obtain the child instance
|
||||
|
||||
const {
|
||||
arrowRef,
|
||||
clickMask,
|
||||
doDestroy,
|
||||
onShow,
|
||||
onHide,
|
||||
popperInstance,
|
||||
popperId,
|
||||
popperRef,
|
||||
initializePopper,
|
||||
visible,
|
||||
} = usePopper(props as IPopperOptions, ctx)
|
||||
|
||||
return {
|
||||
arrowRef,
|
||||
clickMask,
|
||||
popperId,
|
||||
doDestroy,
|
||||
onShow,
|
||||
onHide,
|
||||
popperRef,
|
||||
popperInstance,
|
||||
initializePopper,
|
||||
visible,
|
||||
}
|
||||
return usePopper(props as IPopperOptions)
|
||||
},
|
||||
deactivated() {
|
||||
this.doDestroy()
|
||||
@ -186,6 +162,7 @@ export default defineComponent({
|
||||
this.initializePopper()
|
||||
},
|
||||
render() {
|
||||
const { $slots } = this
|
||||
const arrow = this.showArrow
|
||||
? h(
|
||||
'div',
|
||||
@ -225,7 +202,7 @@ export default defineComponent({
|
||||
onClick: stop,
|
||||
},
|
||||
[
|
||||
this.$slots.default ? this.$slots.default() : this.content,
|
||||
($slots.default?.()) || this.content,
|
||||
arrow,
|
||||
],
|
||||
),
|
||||
@ -235,11 +212,13 @@ export default defineComponent({
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
const _t = $slots.trigger?.()
|
||||
return h(
|
||||
Fragment,
|
||||
null,
|
||||
[
|
||||
this.$slots.trigger?.(),
|
||||
_t,
|
||||
this.appendToBody
|
||||
? h(
|
||||
Teleport,
|
||||
@ -251,11 +230,10 @@ export default defineComponent({
|
||||
'div',
|
||||
{
|
||||
class: 'el-popper__mask',
|
||||
onClick: this.clickMask,
|
||||
},
|
||||
popper,
|
||||
),
|
||||
[[ClickOutside, () => this.$emit(UPDATE_VALUE_EVENT, false)]],
|
||||
[[ClickOutside, this.onHide, [this.excludes] as any]],
|
||||
),
|
||||
)
|
||||
: popper,
|
||||
@ -268,16 +246,12 @@ export default defineComponent({
|
||||
<style>
|
||||
|
||||
.el-popper__mask {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
position: fixed;
|
||||
z-index: 1000000;
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
bottom: 0px;
|
||||
right: 0px;
|
||||
visibility: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.el-popper {
|
||||
position: absolute;
|
||||
border-radius: 4px;
|
||||
|
1
packages/popper/src/popper.d.ts
vendored
1
packages/popper/src/popper.d.ts
vendored
@ -29,5 +29,6 @@ export type IPopperOptions = {
|
||||
showArrow: boolean
|
||||
strategy: PositioningStrategy
|
||||
tabIndex: string
|
||||
trigger: TriggerType[]
|
||||
value: boolean
|
||||
}
|
||||
|
@ -3,22 +3,23 @@ import { debounce } from 'lodash'
|
||||
import { createPopper } from '@popperjs/core'
|
||||
|
||||
import { generateId } from '@element-plus/utils/util'
|
||||
import { off, addClass } from '@element-plus/utils/dom'
|
||||
import { addClass } from '@element-plus/utils/dom'
|
||||
import throwError from '@element-plus/utils/error'
|
||||
|
||||
import useEvents from '@element-plus/hooks/use-events'
|
||||
import { default as useEvents } from '@element-plus/hooks/use-events'
|
||||
|
||||
import useModifier from './useModifier'
|
||||
|
||||
import type { Ref } from 'vue'
|
||||
import type { SetupContext } from '@vue/runtime-core'
|
||||
import type { IPopperOptions, RefElement, PopperInstance } from './popper'
|
||||
|
||||
export const DEFAULT_TRIGGER = ['click', 'hover', 'focus', 'contextMenu']
|
||||
export const DEFAULT_TRIGGER = ['hover']
|
||||
export const UPDATE_VALUE_EVENT = 'updateValue'
|
||||
|
||||
const clearTimer = (timer: Ref<Nullable<NodeJS.Timeout>>) => {
|
||||
clearTimeout(timer.value)
|
||||
if (timer.value) {
|
||||
clearTimeout(timer.value)
|
||||
}
|
||||
timer.value = null
|
||||
}
|
||||
|
||||
@ -45,9 +46,9 @@ const getTrigger = () => {
|
||||
return trigger
|
||||
}
|
||||
|
||||
export default <T extends IPopperOptions>(props: T, ctx: SetupContext) => {
|
||||
export default <T extends IPopperOptions>(props: T) => {
|
||||
const arrowRef = ref<RefElement>(null)
|
||||
const trigger = ref<RefElement>(null)
|
||||
const triggerRef = ref<RefElement>(null)
|
||||
const exceptionState = ref(false)
|
||||
const popperInstance = ref<Nullable<PopperInstance>>(null)
|
||||
const popperId = ref(`el-popper-${generateId()}`)
|
||||
@ -55,6 +56,8 @@ export default <T extends IPopperOptions>(props: T, ctx: SetupContext) => {
|
||||
const show = ref(false)
|
||||
const timeout = ref<NodeJS.Timeout>(null)
|
||||
const timeoutPending = ref<NodeJS.Timeout>(null)
|
||||
const excludes = computed(() => triggerRef.value)
|
||||
const triggerFocused = ref(false)
|
||||
|
||||
const popperOptions = computed(() => {
|
||||
return {
|
||||
@ -97,17 +100,18 @@ export default <T extends IPopperOptions>(props: T, ctx: SetupContext) => {
|
||||
}
|
||||
}
|
||||
|
||||
const closePopper = debounce(() => {
|
||||
const close = () => {
|
||||
if (props.enterable && exceptionState.value) return
|
||||
clearTimer(timeout)
|
||||
if (timeoutPending.value !== null) {
|
||||
clearTimer(timeoutPending)
|
||||
}
|
||||
clearTimer(timeoutPending)
|
||||
show.value = false
|
||||
if (props.disabled) {
|
||||
doDestroy(true)
|
||||
}
|
||||
}, props.closeDelay)
|
||||
}
|
||||
const closePopper = props.closeDelay
|
||||
? debounce(close, props.closeDelay)
|
||||
: close
|
||||
|
||||
function onShow() {
|
||||
setExpectionState(true)
|
||||
@ -128,7 +132,7 @@ export default <T extends IPopperOptions>(props: T, ctx: SetupContext) => {
|
||||
|
||||
function initializePopper() {
|
||||
const _trigger = getTrigger()
|
||||
trigger.value = _trigger
|
||||
triggerRef.value = _trigger
|
||||
|
||||
popperInstance.value = createPopper(_trigger, popperRef.value,
|
||||
props.popperOptions !== null
|
||||
@ -144,26 +148,6 @@ export default <T extends IPopperOptions>(props: T, ctx: SetupContext) => {
|
||||
_trigger.setAttribute('aria-describedby', popperId.value)
|
||||
_trigger.setAttribute('tabindex', props.tabIndex)
|
||||
addClass(_trigger, props.class)
|
||||
|
||||
const events = [
|
||||
{
|
||||
name: 'mouseenter',
|
||||
handler: onShow,
|
||||
},
|
||||
{
|
||||
name: 'mouseleave',
|
||||
handler: onHide,
|
||||
},
|
||||
{
|
||||
name: 'focus',
|
||||
handler: onShow,
|
||||
},
|
||||
{
|
||||
name: 'blur',
|
||||
handler: onHide,
|
||||
},
|
||||
]
|
||||
useEvents(trigger, events)
|
||||
}
|
||||
|
||||
function doDestroy(forceDestroy: boolean) {
|
||||
@ -172,21 +156,83 @@ export default <T extends IPopperOptions>(props: T, ctx: SetupContext) => {
|
||||
detachPopper()
|
||||
}
|
||||
|
||||
function clickMask() {
|
||||
if (props.manualMode) {
|
||||
ctx.emit(UPDATE_VALUE_EVENT, false)
|
||||
}
|
||||
}
|
||||
|
||||
function detachPopper() {
|
||||
popperInstance.value.destroy()
|
||||
popperInstance.value = null
|
||||
const _trigger = trigger.value
|
||||
off(_trigger, 'mouseenter', onShow)
|
||||
off(_trigger, 'mouseleave', onHide)
|
||||
off(_trigger, 'focus', onShow)
|
||||
off(_trigger, 'blur', onHide)
|
||||
trigger.value = null
|
||||
triggerRef.value = null
|
||||
}
|
||||
|
||||
if (!props.manualMode) {
|
||||
const toggleState = () => {
|
||||
if (visible.value) {
|
||||
onHide()
|
||||
} else {
|
||||
onShow()
|
||||
}
|
||||
}
|
||||
|
||||
const handlePopperEvents = (e: Event) => {
|
||||
e.stopImmediatePropagation()
|
||||
switch (e.type) {
|
||||
case 'click': {
|
||||
if (triggerFocused.value) {
|
||||
// reset previous focus event
|
||||
triggerFocused.value = false
|
||||
}
|
||||
toggleState()
|
||||
break
|
||||
}
|
||||
case 'mouseenter': {
|
||||
onShow()
|
||||
break
|
||||
}
|
||||
case 'mouseleave': {
|
||||
onHide()
|
||||
break
|
||||
}
|
||||
case 'focus': {
|
||||
triggerFocused.value = true
|
||||
onShow()
|
||||
break
|
||||
}
|
||||
case 'blur': {
|
||||
triggerFocused.value = false
|
||||
onHide()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
const events = []
|
||||
const handler = handlePopperEvents
|
||||
if (props.trigger.includes('click')) {
|
||||
events.push({
|
||||
name: 'click',
|
||||
handler,
|
||||
})
|
||||
}
|
||||
|
||||
if (props.trigger.includes('hover')) {
|
||||
events.push({
|
||||
name: 'mouseenter',
|
||||
handler,
|
||||
},
|
||||
{
|
||||
name: 'mouseleave',
|
||||
handler,
|
||||
})
|
||||
}
|
||||
|
||||
if (props.trigger.includes('focus')) {
|
||||
events.push({
|
||||
name: 'focus',
|
||||
handler,
|
||||
},
|
||||
{
|
||||
name: 'blur',
|
||||
handler,
|
||||
})
|
||||
}
|
||||
useEvents(triggerRef, events)
|
||||
}
|
||||
|
||||
watch(popperOptions, val => {
|
||||
@ -219,7 +265,7 @@ export default <T extends IPopperOptions>(props: T, ctx: SetupContext) => {
|
||||
|
||||
onUpdated(() => {
|
||||
const _trigger = getTrigger()
|
||||
if (_trigger !== trigger.value && popperInstance.value) {
|
||||
if (_trigger !== triggerRef.value && popperInstance.value) {
|
||||
detachPopper()
|
||||
}
|
||||
if (popperInstance.value) {
|
||||
@ -230,15 +276,16 @@ export default <T extends IPopperOptions>(props: T, ctx: SetupContext) => {
|
||||
})
|
||||
|
||||
return {
|
||||
clickMask,
|
||||
doDestroy,
|
||||
onShow,
|
||||
onHide,
|
||||
initializePopper,
|
||||
arrowRef,
|
||||
excludes,
|
||||
popperId,
|
||||
popperInstance,
|
||||
popperRef,
|
||||
triggerRef,
|
||||
visible,
|
||||
}
|
||||
}
|
||||
|
@ -15,7 +15,9 @@
|
||||
class="item"
|
||||
effect="dark"
|
||||
content="Top Center prompts info"
|
||||
:append-to-body="false"
|
||||
placement="top"
|
||||
:trigger="['click', 'focus', 'hover']"
|
||||
>
|
||||
<el-button>top</el-button>
|
||||
</el-tooltip>
|
||||
@ -24,6 +26,7 @@
|
||||
effect="dark"
|
||||
content="Top Right prompts info"
|
||||
placement="top-end"
|
||||
:trigger="['click']"
|
||||
>
|
||||
<el-button>top-end</el-button>
|
||||
</el-tooltip>
|
||||
|
@ -87,6 +87,10 @@ export default defineComponent({
|
||||
type: String,
|
||||
default: 'el-fade-in-linear',
|
||||
},
|
||||
trigger: {
|
||||
type: [String, Array] as PropType<string | string[]>,
|
||||
default: () => ['hover'],
|
||||
},
|
||||
visibleArrow: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
@ -124,8 +128,9 @@ export default defineComponent({
|
||||
placement,
|
||||
popperOptions,
|
||||
showAfter,
|
||||
transition,
|
||||
tabindex,
|
||||
transition,
|
||||
trigger,
|
||||
visibleArrow,
|
||||
} = this
|
||||
const popper = h(
|
||||
@ -141,8 +146,9 @@ export default defineComponent({
|
||||
placement,
|
||||
showAfter: openDelay || showAfter, // this is for mapping API due to we decided to rename the current openDelay API to showAfter for better readability,
|
||||
showArrow: visibleArrow,
|
||||
transition,
|
||||
tabIndex: String(tabindex),
|
||||
transition,
|
||||
trigger,
|
||||
popperOptions, // Breakings!: Once popperOptions is provided, the whole popper is under user's control, ElPopper nolonger generates the default options for popper, this is by design if the user wants the full contorl on @PopperJS, read the doc @https://popper.js.org/docs/v2/
|
||||
value: this.modelValue,
|
||||
onUpdateValue,
|
||||
|
Loading…
Reference in New Issue
Block a user