fix(popover): can't be nested with other pop-able components, closes #872

This commit is contained in:
07akioni 2021-10-05 23:53:28 +08:00
parent 587ea809bb
commit 9b059bcd5d
9 changed files with 173 additions and 69 deletions

View File

@ -29,6 +29,7 @@
- Fix `n-date-picker`'s `date` type of `action` validate error. - Fix `n-date-picker`'s `date` type of `action` validate error.
- Fix `n-data-table` throws error when using `selection` and `summary` together, closes [#1276](https://github.com/TuSimple/naive-ui/issues/1276). - Fix `n-data-table` throws error when using `selection` and `summary` together, closes [#1276](https://github.com/TuSimple/naive-ui/issues/1276).
- Fix `n-data-table` selection column's width is collapsed when it is set to fixed, closes [#1283](https://github.com/TuSimple/naive-ui/issues/1283). - Fix `n-data-table` selection column's width is collapsed when it is set to fixed, closes [#1283](https://github.com/TuSimple/naive-ui/issues/1283).
- Fix `n-popconfirm` can't be nested in `n-tooltip`, closes [#872](https://github.com/TuSimple/naive-ui/issues/872).
## 2.19.3 (2021-09-28) ## 2.19.3 (2021-09-28)

View File

@ -29,6 +29,7 @@
- 修复 `n-date-picker``date` 类型的 `action` 验证错误 - 修复 `n-date-picker``date` 类型的 `action` 验证错误
- 修复 `n-data-table``selection``summary` 一起使用时报错,关闭 [#1276](https://github.com/TuSimple/naive-ui/issues/1276) - 修复 `n-data-table``selection``summary` 一起使用时报错,关闭 [#1276](https://github.com/TuSimple/naive-ui/issues/1276)
- 修复 `n-data-table` 勾选列的宽度在设为 fixed 时候塌陷,关闭 [#1283](https://github.com/TuSimple/naive-ui/issues/1283) - 修复 `n-data-table` 勾选列的宽度在设为 fixed 时候塌陷,关闭 [#1283](https://github.com/TuSimple/naive-ui/issues/1283)
- 修复 `n-popconfirm` 不能被嵌套于 `n-tooltip` 内,关闭 [#872](https://github.com/TuSimple/naive-ui/issues/872).
## 2.19.3 (2021-09-28) ## 2.19.3 (2021-09-28)

View File

@ -134,7 +134,7 @@
"vdirs": "^0.1.4", "vdirs": "^0.1.4",
"vfonts": "^0.1.0", "vfonts": "^0.1.0",
"vooks": "^0.2.6", "vooks": "^0.2.6",
"vueuc": "^0.4.12" "vueuc": "^0.4.13"
}, },
"sideEffects": false, "sideEffects": false,
"homepage": "https://www.naiveui.com", "homepage": "https://www.naiveui.com",

View File

@ -37,6 +37,7 @@ export type PopconfirmProps = ExtractPublicPropTypes<typeof popconfirmProps>
export default defineComponent({ export default defineComponent({
name: 'Popconfirm', name: 'Popconfirm',
props: popconfirmProps, props: popconfirmProps,
__popover__: true,
setup (props) { setup (props) {
const { mergedClsPrefixRef } = useConfig() const { mergedClsPrefixRef } = useConfig()
const themeRef = useTheme( const themeRef = useTheme(

View File

@ -20,6 +20,7 @@ manual-position
header header
hoist-debug hoist-debug
nested-debug nested-debug
nested2-debug
``` ```
## API ## API

View File

@ -0,0 +1,27 @@
# Nested Debug
```html
<n-tooltip placement="bottom">
<template #trigger>
<n-popover trigger="click">
<template #trigger>
<n-button>Test</n-button>
</template>
Popover
</n-popover>
</template>
Tooltip
</n-tooltip>
<n-tooltip trigger="hover">
<template #trigger>
<n-popconfirm>
<template #trigger>
<n-button>tooltip内嵌popconfirm</n-button>
</template>
一切都将一去杳然,任何人都无法将其捕获。
</n-popconfirm>
</template>
如果它长得像鸭子,走起来像鸭子,叫起来也像鸭子,那它一定是个鸭子。
</n-tooltip>
```

View File

@ -14,7 +14,7 @@ import {
cloneVNode, cloneVNode,
watchEffect watchEffect
} from 'vue' } from 'vue'
import { VBinder, VTarget, FollowerPlacement } from 'vueuc' import { VBinder, VTarget, FollowerPlacement, BinderInst } from 'vueuc'
import { useMergedState, useCompitable, useIsMounted, useMemo } from 'vooks' import { useMergedState, useCompitable, useIsMounted, useMemo } from 'vooks'
import { call, keep, getFirstSlotVNode, warnOnce } from '../../_utils' import { call, keep, getFirstSlotVNode, warnOnce } from '../../_utils'
import type { import type {
@ -36,19 +36,22 @@ const triggerEventMap = {
focus: ['onFocus', 'onBlur'], focus: ['onFocus', 'onBlur'],
click: ['onClick'], click: ['onClick'],
hover: ['onMouseenter', 'onMouseleave'], hover: ['onMouseenter', 'onMouseleave'],
manual: [] manual: [],
nested: ['onFocus', 'onBlur', 'onMouseenter', 'onMouseleave', 'onClick']
} as const } as const
export interface TriggerEventHandlers {
onClick: (e: MouseEvent) => void
onMouseenter: (e: MouseEvent) => void
onMouseleave: (e: MouseEvent) => void
onFocus: (e: FocusEvent) => void
onBlur: (e: FocusEvent) => void
}
function appendEvents ( function appendEvents (
vNode: VNode, vNode: VNode,
trigger: PopoverTrigger, trigger: PopoverTrigger | 'nested',
events: { events: TriggerEventHandlers
onClick: (e: MouseEvent) => void
onMouseenter: (e: MouseEvent) => void
onMouseleave: (e: MouseEvent) => void
onFocus: (e: FocusEvent) => void
onBlur: (e: FocusEvent) => void
}
): void { ): void {
triggerEventMap[trigger].forEach((eventName) => { triggerEventMap[trigger].forEach((eventName) => {
if (!vNode.props) vNode.props = {} if (!vNode.props) vNode.props = {}
@ -151,6 +154,11 @@ export const popoverBaseProps = {
MaybeArray<(value: boolean) => void> MaybeArray<(value: boolean) => void>
>, >,
zIndex: Number, zIndex: Number,
internalSyncTargetWithParent: Boolean,
internalInheritedEventHandlers: {
type: Array as PropType<TriggerEventHandlers[]>,
default: () => []
},
/** @deprecated */ /** @deprecated */
onShow: [Function, Array] as PropType< onShow: [Function, Array] as PropType<
MaybeArray<(value: boolean) => void> | undefined MaybeArray<(value: boolean) => void> | undefined
@ -183,6 +191,7 @@ export default defineComponent({
name: 'Popover', name: 'Popover',
inheritAttrs: false, inheritAttrs: false,
props: popoverProps, props: popoverProps,
__popover__: true,
setup (props) { setup (props) {
if (__DEV__) { if (__DEV__) {
watchEffect(() => { watchEffect(() => {
@ -219,6 +228,7 @@ export default defineComponent({
}) })
} }
const isMountedRef = useIsMounted() const isMountedRef = useIsMounted()
const binderInstRef = ref<BinderInst | null>(null)
// setup show // setup show
const controlledShowRef = computed(() => props.show) const controlledShowRef = computed(() => props.show)
const uncontrolledShowRef = ref(props.defaultShow) const uncontrolledShowRef = ref(props.defaultShow)
@ -246,8 +256,6 @@ export default defineComponent({
if (props.overlap) return false if (props.overlap) return false
return compatibleShowArrowRef.value return compatibleShowArrowRef.value
}) })
// trigger
let triggerVNode: VNode | null = null
// bodyInstance // bodyInstance
let bodyInstance: BodyInstance | null = null let bodyInstance: BodyInstance | null = null
const showTimerIdRef = ref<number | null>(null) const showTimerIdRef = ref<number | null>(null)
@ -372,7 +380,7 @@ export default defineComponent({
uncontrolledShowRef.value = value uncontrolledShowRef.value = value
} }
function getTriggerElement (): HTMLElement { function getTriggerElement (): HTMLElement {
return triggerVNode?.el as HTMLElement return binderInstRef.value?.targetRef as HTMLElement
} }
function setBodyInstance (value: BodyInstance | null): void { function setBodyInstance (value: BodyInstance | null): void {
bodyInstance = value bodyInstance = value
@ -391,6 +399,7 @@ export default defineComponent({
internalRenderBodyRef: toRef(props, 'internalRenderBody') internalRenderBodyRef: toRef(props, 'internalRenderBody')
}) })
return { return {
binderInstRef,
positionManually: positionManuallyRef, positionManually: positionManuallyRef,
mergedShowConsideringDisabledProp: mergedShowConsideringDisabledPropRef, mergedShowConsideringDisabledProp: mergedShowConsideringDisabledPropRef,
// if to show popover body // if to show popover body
@ -403,65 +412,127 @@ export default defineComponent({
handleMouseLeave, handleMouseLeave,
handleFocus, handleFocus,
handleBlur, handleBlur,
setTriggerVNode (v: VNode | null) {
triggerVNode = v
},
syncPosition syncPosition
} }
}, },
render () { render () {
return h(VBinder, null, { const { positionManually, $slots: slots } = this
default: () => { let triggerVNode: VNode | null
const { positionManually, $slots: slots } = this let popoverInside = false
let triggerVNode: VNode | null if (!positionManually) {
if (!positionManually) { if (slots.activator) {
if (slots.activator) { triggerVNode = getFirstSlotVNode(slots, 'activator')
triggerVNode = getFirstSlotVNode(slots, 'activator') } else {
} else { triggerVNode = getFirstSlotVNode(slots, 'trigger')
triggerVNode = getFirstSlotVNode(slots, 'trigger')
}
if (triggerVNode) {
triggerVNode = cloneVNode(triggerVNode)
triggerVNode =
triggerVNode.type === textVNodeType
? h('span', [triggerVNode])
: triggerVNode
appendEvents(
triggerVNode,
positionManually ? 'manual' : this.trigger,
{
onClick: this.handleClick,
onMouseenter: this.handleMouseEnter,
onMouseleave: this.handleMouseLeave,
onFocus: this.handleFocus,
onBlur: this.handleBlur
}
)
}
this.setTriggerVNode(triggerVNode)
}
// We need to subscribe it. Sometimes rerender won't ge triggered.
// `mergedShowConsideringDisabledProp` is not the final disabled status.
// In ellpisis it's dynamic.
void this.mergedShowConsideringDisabledProp
const mergedShow = this.getMergedShow()
return [
positionManually
? null
: h(VTarget, null, {
default: () => triggerVNode
}),
h(
NPopoverBody,
keep(this.$props, bodyPropKeys, {
...this.$attrs,
showArrow: this.mergedShowArrow,
show: mergedShow
}),
slots
)
]
} }
}) if (triggerVNode) {
triggerVNode = cloneVNode(triggerVNode)
triggerVNode =
triggerVNode.type === textVNodeType
? h('span', [triggerVNode])
: triggerVNode
const handlers = {
onClick: this.handleClick,
onMouseenter: this.handleMouseEnter,
onMouseleave: this.handleMouseLeave,
onFocus: this.handleFocus,
onBlur: this.handleBlur
}
if ((triggerVNode.type as any)?.__popover__) {
popoverInside = true
// We assume that there's no DOM event handlers on popover element
if (!triggerVNode.props) {
triggerVNode.props = {
internalSyncTargetWithParent: true,
internalInheritedEventHandlers: []
}
}
triggerVNode.props.internalSyncTargetWithParent = true
if (!triggerVNode.props.internalInheritedEventHandlers) {
triggerVNode.props.internalInheritedEventHandlers = [handlers]
} else {
triggerVNode.props.internalInheritedEventHandlers = [
handlers,
...triggerVNode.props.internalInheritedEventHandlers
]
}
} else {
const { internalInheritedEventHandlers } = this
const ascendantAndCurrentHandlers: TriggerEventHandlers[] = [
handlers,
...internalInheritedEventHandlers
]
const mergedHandlers: TriggerEventHandlers = {
onBlur: (e: FocusEvent) => {
ascendantAndCurrentHandlers.forEach((_handlers) => {
_handlers.onBlur(e)
})
},
onFocus: (e: FocusEvent) => {
ascendantAndCurrentHandlers.forEach((_handlers) => {
_handlers.onBlur(e)
})
},
onClick: (e: MouseEvent) => {
ascendantAndCurrentHandlers.forEach((_handlers) => {
_handlers.onClick(e)
})
},
onMouseenter: (e: MouseEvent) => {
ascendantAndCurrentHandlers.forEach((_handlers) => {
_handlers.onMouseenter(e)
})
},
onMouseleave: (e: MouseEvent) => {
ascendantAndCurrentHandlers.forEach((_handlers) => {
_handlers.onMouseleave(e)
})
}
}
appendEvents(
triggerVNode,
internalInheritedEventHandlers
? 'nested'
: positionManually
? 'manual'
: this.trigger,
mergedHandlers
)
}
}
}
return (
<VBinder
ref="binderInstRef"
syncTarget={!popoverInside}
syncTargetWithParent={this.internalSyncTargetWithParent}
>
{{
default: () => {
// We need to subscribe it. Sometimes rerender won't ge triggered.
// `mergedShowConsideringDisabledProp` is not the final disabled status.
// In ellpisis it's dynamic.
void this.mergedShowConsideringDisabledProp
const mergedShow = this.getMergedShow()
return [
positionManually
? null
: h(VTarget, null, {
default: () => triggerVNode
}),
h(
NPopoverBody,
keep(this.$props, bodyPropKeys, {
...this.$attrs,
showArrow: this.mergedShowArrow,
show: mergedShow
}),
slots
)
]
}
}}
</VBinder>
)
} }
}) })

View File

@ -32,6 +32,7 @@ export type PopselectProps = ExtractPublicPropTypes<typeof popselectProps>
export default defineComponent({ export default defineComponent({
name: 'Popselect', name: 'Popselect',
props: popselectProps, props: popselectProps,
__popover__: true,
setup (props) { setup (props) {
const themeRef = useTheme( const themeRef = useTheme(
'Popselect', 'Popselect',

View File

@ -21,6 +21,7 @@ export type TooltipProps = ExtractPublicPropTypes<typeof tooltipProps>
export default defineComponent({ export default defineComponent({
name: 'Tooltip', name: 'Tooltip',
props: tooltipProps, props: tooltipProps,
__popover__: true,
setup (props) { setup (props) {
const themeRef = useTheme( const themeRef = useTheme(
'Tooltip', 'Tooltip',