refactor(popover): ts

This commit is contained in:
07akioni 2021-01-15 00:53:40 +08:00
parent defbb58129
commit 88db9bf48f
13 changed files with 842 additions and 684 deletions

View File

@ -97,6 +97,10 @@
</div>
```
```script
console.log('wow')
```
```css
.popover-grid {
display: grid;

View File

@ -1 +0,0 @@
export { default as NPopover } from './src/Popover'

1
src/popover/index.ts Normal file
View File

@ -0,0 +1 @@
export { default as NPopover, popoverProps } from './src/Popover'

View File

@ -1,310 +0,0 @@
import { h, ref, computed, watch, createTextVNode, defineComponent } from 'vue'
import { VBinder, VTarget } from 'vueuc'
import { useMergedState, useCompitable, useIsMounted, useMemo } from 'vooks'
import { call, keep, warn } from '../../_utils'
import { useTheme } from '../../_mixins'
import NPopoverBody from './PopoverBody'
const bodyPropKeys = Object.keys(NPopoverBody.props)
function appendEvents (vNode, events) {
Object.entries(events).forEach(([key, handler]) => {
if (!vNode.props) vNode.props = {}
const originalHandler = vNode.props[key]
if (!originalHandler) vNode.props[key] = handler
else {
vNode.props[key] = (...args) => {
originalHandler(...args)
handler()
}
}
})
}
function getFirstSlotVNode (slots, slotName = 'default') {
let slot = slots[slotName]
if (!slot) {
warn('getFirstSlotVNode', `slot[${slotName}] is empty`)
}
slot = slot()
// vue will normalize the slot, so slot must be an array
if (slot.length === 1) {
return slot[0]
} else {
warn('getFirstSlotVNode', `slot[${slotName}] should have exactly one child`)
return null
}
}
const textVNodeType = createTextVNode('').type
export default defineComponent({
name: 'Popover',
provide () {
return {
NPopover: this
}
},
inheritAttrs: false,
props: {
...useTheme.props,
show: {
type: Boolean,
default: undefined
},
defaultShow: {
type: Boolean,
default: false
},
showArrow: {
type: Boolean,
default: true
},
trigger: {
validator (value) {
return ['hover', 'click'].includes(value)
},
default: undefined
},
delay: {
type: Number,
default: 200
},
duration: {
type: Number,
default: 200
},
raw: {
type: Boolean,
default: false
},
placement: {
type: String,
default: 'bottom'
},
x: {
type: Number,
default: undefined
},
y: {
type: Number,
default: undefined
},
disabled: {
type: Boolean,
default: false
},
displayDirective: {
type: String,
default: 'if'
},
arrowStyle: {
type: Object,
default: undefined
},
filp: {
type: Boolean,
default: true
},
animated: {
type: Boolean,
default: true
},
// private
padded: {
type: Boolean,
default: true
},
shadow: {
type: Boolean,
default: true
},
// events
// eslint-disable-next-line vue/prop-name-casing
'onUpdate:show': {
type: Function,
default: undefined
},
// deprecated
onShow: {
validator () {
warn(
'popover',
'`on-show` is deprecated, please use `on-update:show` instead.'
)
return true
},
default: undefined
},
onHide: {
validator () {
warn(
'popover',
'`on-hide` is deprecated, please use `on-update:show` instead.'
)
return true
},
default: undefined
},
arrow: {
type: Boolean,
default: undefined
}
},
setup (props) {
// setup show
const controlledShowRef = computed(() => props.show)
const uncontrolledShowRef = ref(props.defaultShow)
const mergedShowWithoutDisabledRef = useMergedState(
controlledShowRef,
uncontrolledShowRef
)
const mergedShowRef = computed(() => {
return props.disabled ? false : mergedShowWithoutDisabledRef.value
})
// setup show-arrow
const compatibleShowArrowRef = useCompitable(props, ['arrow', 'showArrow'])
watch(mergedShowRef, (value) => {
if (props.showWatcher) {
props.showWatcher(value)
}
})
return {
isMounted: useIsMounted(),
positionManually: useMemo(() => {
return props.x !== undefined && props.y !== undefined
}),
// if to show popover body
uncontrolledShow: uncontrolledShowRef,
mergedShow: mergedShowRef,
compatibleShowArrow: compatibleShowArrowRef
}
},
data () {
return {
showTimerId: null,
hideTimerId: null,
triggerVNode: null,
bodyInstance: null
}
},
methods: {
doUpdateShow (value) {
const { 'onUpdate:show': onUpdateShow, onShow, onHide } = this
this.uncontrolledShow = value
if (onUpdateShow) {
call(onUpdateShow, value)
}
if (value && onShow) {
call(onShow, true)
}
if (value && onHide) {
call(onHide, false)
}
},
syncPosition () {
if (this.bodyInstance) {
this.bodyInstance.syncPosition()
}
},
getTriggerElement () {
return this.triggerVNode.el
},
clearTimer () {
const { showTimerId, hideTimerId } = this
if (showTimerId) {
window.clearTimeout(showTimerId)
this.showTimerId = null
}
if (hideTimerId) {
window.clearTimeout(hideTimerId)
this.hideTimerId = null
}
},
handleMouseEnter () {
if (this.trigger === 'hover' && !this.disabled) {
this.clearTimer()
if (this.mergedShow) return
this.showTimerId = window.setTimeout(() => {
this.doUpdateShow(true)
this.showTimerId = null
}, this.delay)
}
},
handleMouseLeave () {
if (this.trigger === 'hover' && !this.disabled) {
this.clearTimer()
if (!this.mergedShow) return
this.hideTimerId = window.setTimeout(() => {
this.doUpdateShow(false)
this.hideTimerId = null
}, this.duration)
}
},
// will be called in popover-content
handleMouseMoveOutside (e) {
this.handleMouseLeave(e)
},
// will be called in popover-content
handleClickOutside () {
if (!this.mergedShow) return
if (this.trigger === 'click') {
this.clearTimer()
this.doUpdateShow(false)
}
},
handleClick () {
if (this.trigger === 'click' && !this.disabled) {
this.clearTimer()
const nextShow = !this.mergedShow
this.doUpdateShow(nextShow)
}
},
setShow (value) {
this.uncontrolledShow = value
}
},
render () {
const { positionManually } = this
const slots = { ...this.$slots }
let triggerVNode
if (!positionManually) {
if (slots.activator) {
triggerVNode = getFirstSlotVNode(slots, 'activator')
} else {
triggerVNode = getFirstSlotVNode(slots, 'trigger')
}
triggerVNode =
triggerVNode.type === textVNodeType
? h('span', [triggerVNode])
: triggerVNode
appendEvents(triggerVNode, {
onClick: this.handleClick,
onMouseEnter: this.handleMouseEnter,
onMouseLeave: this.handleMouseLeave
})
this.triggerVNode = triggerVNode
}
return h(VBinder, null, {
default: () => {
return [
positionManually
? null
: h(VTarget, null, {
default: () => triggerVNode
}),
h(
NPopoverBody,
keep(this.$props, bodyPropKeys, {
...this.$attrs,
show: this.mergedShow
}),
slots
)
]
}
})
}
})

371
src/popover/src/Popover.ts Normal file
View File

@ -0,0 +1,371 @@
import {
h,
ref,
reactive,
computed,
createTextVNode,
defineComponent,
PropType,
VNode,
Slots,
provide
} from 'vue'
import { VBinder, VTarget, FollowerPlacement } from 'vueuc'
import { useMergedState, useCompitable, useIsMounted, useMemo } from 'vooks'
import { call, keep, warn } from '../../_utils'
import { useTheme } from '../../_mixins'
import NPopoverBody, { popoverBodyProps } from './PopoverBody'
const bodyPropKeys = Object.keys(
popoverBodyProps
) as (keyof typeof popoverBodyProps)[]
function appendEvents (
vNode: VNode,
events: {
onClick: (e: MouseEvent) => void
onMouseEnter: (e: MouseEvent) => void
onMouseLeave: (e: MouseEvent) => void
}
): void {
Object.entries(events).forEach(([key, handler]) => {
if (!vNode.props) vNode.props = {}
const originalHandler = vNode.props[key]
if (!originalHandler) vNode.props[key] = handler
else {
vNode.props[key] = (...args: unknown[]) => {
originalHandler(...args)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
;(handler as any)(...args)
}
}
})
}
function getFirstSlotVNode (slots: Slots, slotName = 'default'): VNode | null {
const slot = slots[slotName]
if (!slot) {
warn('getFirstSlotVNode', `slot[${slotName}] is empty`)
return null
}
const slotContent = slot()
// vue will normalize the slot, so slot must be an array
if (slotContent.length === 1) {
return slotContent[0]
} else {
warn('getFirstSlotVNode', `slot[${slotName}] should have exactly one child`)
return null
}
}
const textVNodeType = createTextVNode('').type
type BodyInstance = { syncPosition: () => void; [key: string]: unknown }
export interface PopoverInjection {
handleMouseLeave: (e: MouseEvent) => void
handleMouseEnter: (e: MouseEvent) => void
handleMouseMoveOutside: (e: MouseEvent) => void
handleClickOutside: (e: MouseEvent) => void
getTriggerElement: () => HTMLElement
setBodyInstance: (value: BodyInstance | null) => void
isMounted: boolean
}
export const popoverProps = {
...useTheme.props,
show: {
type: Boolean,
default: undefined
},
defaultShow: {
type: Boolean,
default: false
},
showArrow: {
type: Boolean,
default: true
},
trigger: {
type: String as PropType<'hover' | 'click'>,
default: undefined
},
delay: {
type: Number,
default: 200
},
duration: {
type: Number,
default: 200
},
raw: {
type: Boolean,
default: false
},
placement: {
type: String as PropType<FollowerPlacement>,
default: 'bottom'
},
x: {
type: Number,
default: undefined
},
y: {
type: Number,
default: undefined
},
disabled: {
type: Boolean,
default: false
},
displayDirective: {
type: String,
default: 'if'
},
arrowStyle: {
type: Object,
default: undefined
},
filp: {
type: Boolean,
default: true
},
animated: {
type: Boolean,
default: true
},
// private
padded: {
type: Boolean,
default: true
},
shadow: {
type: Boolean,
default: true
},
// events
// eslint-disable-next-line vue/prop-name-casing
'onUpdate:show': {
type: Function,
default: undefined
},
// deprecated
onShow: {
type: Function,
validator: (): boolean => {
warn(
'popover',
'`on-show` is deprecated, please use `on-update:show` instead.'
)
return true
},
default: undefined
},
onHide: {
type: Function,
validator: (): boolean => {
warn(
'popover',
'`on-hide` is deprecated, please use `on-update:show` instead.'
)
return true
},
default: undefined
},
arrow: {
type: Boolean,
default: undefined
},
width: {
type: Number,
default: undefined
},
minWidth: {
type: Number,
default: undefined
},
maxWidth: {
type: Number,
default: undefined
}
}
export default defineComponent({
name: 'Popover',
inheritAttrs: false,
props: popoverProps,
setup (props) {
const isMountedRef = useIsMounted()
// setup show
const controlledShowRef = computed(() => props.show)
const uncontrolledShowRef = ref(props.defaultShow)
const mergedShowWithoutDisabledRef = useMergedState(
controlledShowRef,
uncontrolledShowRef
)
const mergedShowRef = computed(() => {
return props.disabled ? false : mergedShowWithoutDisabledRef.value
})
// setup show-arrow
const compatibleShowArrowRef = useCompitable(props, ['arrow', 'showArrow'])
// trigger
let triggerVNode = null as VNode | null
// bodyInstance
let bodyInstance = null as BodyInstance | null
const showTimerIdRef = ref<number | null>(null)
const hideTimerIdRef = ref<number | null>(null)
// methods
function doUpdateShow (value: boolean) {
const { 'onUpdate:show': onUpdateShow, onShow, onHide } = props
uncontrolledShowRef.value = value
if (onUpdateShow) {
call(onUpdateShow, value)
}
if (value && onShow) {
call(onShow, true)
}
if (value && onHide) {
call(onHide, false)
}
}
function syncPosition () {
if (bodyInstance) {
bodyInstance.syncPosition()
}
}
function clearTimer () {
const { value: showTimerId } = showTimerIdRef
const { value: hideTimerId } = hideTimerIdRef
if (showTimerId) {
window.clearTimeout(showTimerId)
showTimerIdRef.value = null
}
if (hideTimerId) {
window.clearTimeout(hideTimerId)
hideTimerIdRef.value = null
}
}
function handleMouseEnter () {
if (props.trigger === 'hover' && !props.disabled) {
clearTimer()
if (mergedShowRef.value) return
showTimerIdRef.value = window.setTimeout(() => {
doUpdateShow(true)
showTimerIdRef.value = null
}, props.delay)
}
}
function handleMouseLeave () {
if (props.trigger === 'hover' && !props.disabled) {
clearTimer()
if (!mergedShowRef.value) return
hideTimerIdRef.value = window.setTimeout(() => {
doUpdateShow(false)
hideTimerIdRef.value = null
}, props.duration)
}
}
// will be called in popover-content
function handleMouseMoveOutside () {
handleMouseLeave()
}
// will be called in popover-content
function handleClickOutside () {
if (!mergedShowRef.value) return
if (props.trigger === 'click') {
clearTimer()
doUpdateShow(false)
}
}
function handleClick () {
if (props.trigger === 'click' && !props.disabled) {
clearTimer()
const nextShow = !mergedShowRef.value
doUpdateShow(nextShow)
}
}
function setShow (value: boolean) {
uncontrolledShowRef.value = value
}
function getTriggerElement () {
return triggerVNode?.el as HTMLElement
}
function setBodyInstance (value: BodyInstance | null): void {
bodyInstance = value
}
provide<PopoverInjection>(
'NPopover',
reactive({
getTriggerElement,
handleMouseEnter,
handleMouseLeave,
handleClickOutside,
handleMouseMoveOutside,
setBodyInstance,
isMounted: isMountedRef
})
)
return {
positionManually: useMemo(() => {
return props.x !== undefined && props.y !== undefined
}),
// if to show popover body
uncontrolledShow: uncontrolledShowRef,
mergedShow: mergedShowRef,
compatibleShowArrow: compatibleShowArrowRef,
setShow,
handleClick,
handleMouseEnter,
handleMouseLeave,
setTriggerVNode (v: VNode | null) {
triggerVNode = v
},
syncPosition
}
},
render () {
const { positionManually } = this
const slots = { ...this.$slots }
let triggerVNode: VNode | null
if (!positionManually) {
if (slots.activator) {
triggerVNode = getFirstSlotVNode(slots, 'activator')
} else {
triggerVNode = getFirstSlotVNode(slots, 'trigger')
}
if (triggerVNode) {
triggerVNode =
triggerVNode.type === textVNodeType
? h('span', [triggerVNode])
: triggerVNode
appendEvents(triggerVNode, {
onClick: this.handleClick,
onMouseEnter: this.handleMouseEnter,
onMouseLeave: this.handleMouseLeave
})
}
this.setTriggerVNode(triggerVNode)
}
return h(VBinder, null, {
default: () => {
return [
positionManually
? null
: h(VTarget, null, {
default: () => triggerVNode
}),
h(
NPopoverBody,
keep(this.$props, bodyPropKeys, {
...this.$attrs,
show: this.mergedShow
}),
slots
)
]
}
})
}
})

View File

@ -1,304 +0,0 @@
import {
h,
vShow,
withDirectives,
Transition,
ref,
defineComponent,
computed,
mergeProps,
inject
} from 'vue'
import { VFollower } from 'vueuc'
import { clickoutside, mousemoveoutside } from 'vdirs'
import { useTheme, useConfig } from '../../_mixins'
import { formatLength, useAdjustedTo, getSlot } from '../../_utils'
import { popoverLight } from '../styles'
import style from './styles/index.cssr.js'
export default defineComponent({
name: 'PopoverBody',
inheritAttrs: false,
props: {
...useTheme.props,
show: {
type: Boolean,
default: undefined
},
trigger: {
type: String,
default: undefined
},
showArrow: {
type: Boolean,
default: undefined
},
delay: {
type: Number,
default: undefined
},
duration: {
type: Number,
default: undefined
},
raw: {
type: Boolean,
default: undefined
},
arrowStyle: {
type: Object,
default: undefined
},
displayDirective: {
type: String,
default: undefined
},
x: {
type: Number,
default: undefined
},
y: {
type: Number,
default: undefined
},
filp: {
type: Boolean,
default: undefined
},
placement: {
type: String,
default: undefined
},
// private
shadow: {
type: Boolean,
default: undefined
},
padded: {
type: Boolean,
default: undefined
},
// deprecated
width: {
type: Number,
default: undefined
},
minWidth: {
type: Number,
default: undefined
},
maxWidth: {
type: Number,
default: undefined
},
animated: {
type: Boolean,
default: undefined
}
},
setup (props) {
const themeRef = useTheme('Popover', 'Popover', style, popoverLight, props)
const NPopover = inject('NPopover')
return {
...useConfig(props),
NPopover,
adjustedTo: useAdjustedTo(props),
followerEnabled: ref(props.show),
followerRef: ref(null),
cssVars: computed(() => {
const {
common: {
cubicBezierEaseInOut,
cubicBezierEaseIn,
cubicBezierEaseOut
},
self: {
space,
spaceArrow,
padding,
fontSize,
textColor,
color,
boxShadow,
borderRadius,
arrowHeight,
arrowOffset,
arrowOffsetVertical
}
} = themeRef.value
return {
'--box-shadow': boxShadow,
'--bezier': cubicBezierEaseInOut,
'--bezier-ease-in': cubicBezierEaseIn,
'--bezier-ease-out': cubicBezierEaseOut,
'--font-size': fontSize,
'--text-color': textColor,
'--color': color,
'--border-radius': borderRadius,
'--arrow-height': arrowHeight,
'--arrow-offset': arrowOffset,
'--arrow-offset-vertical': arrowOffsetVertical,
'--padding': padding,
'--space': space,
'--space-arrow': spaceArrow
}
})
}
},
computed: {
useVShow () {
return this.displayDirective === 'show'
},
directives () {
const { trigger } = this
const directives = []
if (trigger === 'click') {
directives.push([clickoutside, this.handleClickOutside])
}
if (trigger === 'hover') {
directives.push([mousemoveoutside, this.handleMouseMoveOutside])
}
if (this.useVShow) directives.push([vShow, this.show])
return directives
},
style () {
return [
{
width: formatLength(this.width),
maxWidth: formatLength(this.maxWidth),
minWidth: formatLength(this.minWidth)
},
this.cssVars
]
}
},
watch: {
show (value) {
if (value) this.followerEnabled = true
else {
if (!this.animated) {
this.followerEnabled = false
}
}
}
},
created () {
this.NPopover.bodyInstance = this
},
beforeUnmount () {
this.NPopover.bodyInstance = null
},
methods: {
syncPosition () {
this.followerRef.syncPosition()
},
handleMouseEnter (e) {
if (this.trigger === 'hover') {
this.NPopover.handleMouseEnter(e)
}
},
handleMouseLeave (e) {
if (this.trigger === 'hover') {
this.NPopover.handleMouseLeave(e)
}
},
handleMouseMoveOutside (e) {
if (
this.trigger === 'hover' &&
!this.getTriggerElement().contains(e.target)
) {
this.NPopover.handleMouseMoveOutside(e)
}
},
handleClickOutside (e) {
if (
this.trigger === 'click' &&
!this.getTriggerElement().contains(e.target)
) {
this.NPopover.handleClickOutside()
}
},
getTriggerElement () {
return this.NPopover.getTriggerElement()
}
},
render () {
const { animated } = this
const contentNode =
this.useVShow || this.show
? withDirectives(
h(
'div',
mergeProps(
{
class: [
'n-popover',
{
'n-popover--no-arrow': !this.showArrow,
'n-popover--shadow': this.shadow,
'n-popover--padded': this.padded,
'n-popover--raw': this.raw
}
],
ref: 'body',
style: this.style,
onMouseEnter: this.handleMouseEnter,
onMouseLeave: this.handleMouseLeave
},
this.$attrs
),
[
getSlot(this),
this.showArrow
? h(
'div',
{
class: 'n-popover-arrow-wrapper'
},
[
h('div', {
class: 'n-popover-arrow',
style: this.arrowStyle
})
]
)
: null
]
),
this.directives
)
: null
return h(
VFollower,
{
show: this.show,
enabled: this.followerEnabled,
to: this.adjustedTo,
x: this.x,
y: this.y,
placement: this.placement,
containerClass: this.namespace,
ref: 'followerRef'
},
{
default: () => {
return animated
? h(
Transition,
{
name: 'popover-transition',
appear: this.NPopover.isMounted,
onAfterLeave: () => {
this.followerEnabled = false
}
},
{
default: () => contentNode
}
)
: contentNode
}
}
)
}
})

View File

@ -0,0 +1,313 @@
import {
h,
vShow,
withDirectives,
Transition,
ref,
defineComponent,
computed,
mergeProps,
inject,
onBeforeUnmount,
DirectiveArguments,
PropType,
watch,
toRef
} from 'vue'
import { VFollower, FollowerPlacement, FollowerRef } from 'vueuc'
import { clickoutside, mousemoveoutside } from 'vdirs'
import { useTheme, useConfig } from '../../_mixins'
import { formatLength, useAdjustedTo, getSlot } from '../../_utils'
import { popoverLight } from '../styles'
import style from './styles/index.cssr'
import { PopoverThemeVars } from '../styles/light'
import { PopoverInjection } from './Popover'
export const popoverBodyProps = {
...useTheme.props,
show: {
type: Boolean,
default: undefined
},
trigger: {
type: String,
default: undefined
},
showArrow: {
type: Boolean,
default: undefined
},
delay: {
type: Number,
default: undefined
},
duration: {
type: Number,
default: undefined
},
raw: {
type: Boolean,
default: undefined
},
arrowStyle: {
type: Object,
default: undefined
},
displayDirective: {
type: String,
default: undefined
},
x: {
type: Number,
default: undefined
},
y: {
type: Number,
default: undefined
},
filp: {
type: Boolean,
default: undefined
},
placement: {
type: String as PropType<FollowerPlacement>,
default: undefined
},
// private
shadow: {
type: Boolean,
default: undefined
},
padded: {
type: Boolean,
default: undefined
},
animated: {
type: Boolean,
default: undefined
},
// deprecated
width: {
type: Number,
default: undefined
},
minWidth: {
type: Number,
default: undefined
},
maxWidth: {
type: Number,
default: undefined
}
}
export default defineComponent({
name: 'PopoverBody',
inheritAttrs: false,
props: popoverBodyProps,
setup (props) {
const themeRef = useTheme<PopoverThemeVars>(
'Popover',
'Popover',
style,
popoverLight,
props
)
const followerRef = ref<FollowerRef | null>(null)
const NPopover = inject<PopoverInjection>('NPopover') as PopoverInjection
const followerEnabledRef = ref(props.show)
const directivesRef = computed<DirectiveArguments>(() => {
const { trigger } = props
const directives = []
if (trigger === 'click') {
directives.push([clickoutside, handleClickOutside])
}
if (trigger === 'hover') {
directives.push([mousemoveoutside, handleMouseMoveOutside])
}
if (props.displayDirective === 'show') { directives.push([vShow, props.show]) }
return directives as DirectiveArguments
})
const styleRef = computed(() => {
return [
{
width: formatLength(props.width),
maxWidth: formatLength(props.maxWidth),
minWidth: formatLength(props.minWidth)
},
cssVarsRef.value
]
})
const cssVarsRef = computed(() => {
const {
common: { cubicBezierEaseInOut, cubicBezierEaseIn, cubicBezierEaseOut },
self: {
space,
spaceArrow,
padding,
fontSize,
textColor,
color,
boxShadow,
borderRadius,
arrowHeight,
arrowOffset,
arrowOffsetVertical
}
} = themeRef.value
return {
'--box-shadow': boxShadow,
'--bezier': cubicBezierEaseInOut,
'--bezier-ease-in': cubicBezierEaseIn,
'--bezier-ease-out': cubicBezierEaseOut,
'--font-size': fontSize,
'--text-color': textColor,
'--color': color,
'--border-radius': borderRadius,
'--arrow-height': arrowHeight,
'--arrow-offset': arrowOffset,
'--arrow-offset-vertical': arrowOffsetVertical,
'--padding': padding,
'--space': space,
'--space-arrow': spaceArrow
}
})
NPopover.setBodyInstance({
syncPosition
})
onBeforeUnmount(() => {
NPopover.setBodyInstance(null)
})
watch(toRef(props, 'show'), (value) => {
if (value) followerEnabledRef.value = true
else {
if (!props.animated) {
followerEnabledRef.value = false
}
}
})
function syncPosition () {
// eslint-disable-next-line no-unused-expressions
followerRef.value?.syncPosition()
}
function handleMouseEnter (e: MouseEvent) {
if (props.trigger === 'hover') {
NPopover.handleMouseEnter(e)
}
}
function handleMouseLeave (e: MouseEvent) {
if (props.trigger === 'hover') {
NPopover.handleMouseLeave(e)
}
}
function handleMouseMoveOutside (e: MouseEvent) {
if (
props.trigger === 'hover' &&
!getTriggerElement().contains(e.target as Node)
) {
NPopover.handleMouseMoveOutside(e)
}
}
function handleClickOutside (e: MouseEvent) {
if (
props.trigger === 'click' &&
!getTriggerElement().contains(e.target as Node)
) {
NPopover.handleClickOutside(e)
}
}
function getTriggerElement () {
return NPopover.getTriggerElement()
}
return {
...useConfig(props),
NPopover,
followerRef,
adjustedTo: useAdjustedTo(props),
followerEnabled: followerEnabledRef,
style: styleRef,
directives: directivesRef,
handleMouseEnter,
handleMouseLeave
}
},
render () {
const { animated } = this
const contentNode =
this.displayDirective === 'show' || this.show
? withDirectives(
h(
'div',
mergeProps(
{
class: [
'n-popover',
{
'n-popover--no-arrow': !this.showArrow,
'n-popover--shadow': this.shadow,
'n-popover--padded': this.padded,
'n-popover--raw': this.raw
}
],
ref: 'body',
style: this.style,
onMouseEnter: this.handleMouseEnter,
onMouseLeave: this.handleMouseLeave
},
this.$attrs
),
[
getSlot(this),
this.showArrow
? h(
'div',
{
class: 'n-popover-arrow-wrapper'
},
[
h('div', {
class: 'n-popover-arrow',
style: this.arrowStyle
})
]
)
: null
]
),
this.directives
)
: null
return h(
VFollower,
{
show: this.show,
enabled: this.followerEnabled,
to: this.adjustedTo,
x: this.x,
y: this.y,
placement: this.placement,
containerClass: this.namespace,
ref: 'followerRef'
},
{
default: () => {
return animated
? h(
Transition,
{
name: 'popover-transition',
appear: this.NPopover.isMounted,
onAfterLeave: () => {
this.followerEnabled = false
}
},
{
default: () => contentNode
}
)
: contentNode
}
}
)
}
})

View File

@ -1,3 +1,4 @@
import { FollowerPlacement } from 'vueuc'
import { c, cB, cM, cNotM } from '../../../_utils/cssr'
const oppositePlacement = {
@ -22,7 +23,9 @@ const oppositePlacement = {
// --space
// --space-arrow
export default c([
cB('popover', `
cB(
'popover',
`
transition:
background-color .3s var(--bezier),
color .3s var(--bezier);
@ -30,40 +33,62 @@ export default c([
position: relative;
font-size: var(--font-size);
color: var(--text-color);
`, [
// body transition
c('&.popover-transition-enter-from, &.popover-transition-leave-to', `
`,
[
// body transition
c(
'&.popover-transition-enter-from, &.popover-transition-leave-to',
`
opacity: 0;
transform: scale(.85);
`),
c('&.popover-transition-enter-to, &.popover-transition-leave-from', `
`
),
c(
'&.popover-transition-enter-to, &.popover-transition-leave-from',
`
transform: scale(1);
opacity: 1;
`),
c('&.popover-transition-enter-active', `
`
),
c(
'&.popover-transition-enter-active',
`
transition:
opacity .15s var(--bezier-ease-out),
transform .15s var(--bezier-ease-out);
`),
c('&.popover-transition-leave-active', `
`
),
c(
'&.popover-transition-leave-active',
`
transition:
opacity .15s var(--bezier-ease-in),
transform .15s var(--bezier-ease-in);
`),
cNotM('raw', `
`
),
cNotM(
'raw',
`
background-color: var(--color);
border-radius: var(--border-radius);
`, [
cM('padded', {
padding: 'var(--padding)'
})
]),
cB('popover-arrow-wrapper', `
`,
[
cM('padded', {
padding: 'var(--padding)'
})
]
),
cB(
'popover-arrow-wrapper',
`
position: absolute;
overflow: hidden;
pointer-events: none;
`, [
cB('popover-arrow', `
`,
[
cB(
'popover-arrow',
`
transition: background-color .3s var(--bezier);
position: absolute;
display: block;
@ -73,83 +98,134 @@ export default c([
transform: rotate(45deg);
background-color: var(--color);
pointer-events: all;
`)
]),
cM('shadow', {
boxShadow: 'var(--box-shadow)'
})
]),
placementStyle('top-start', `
`
)
]
),
cM('shadow', {
boxShadow: 'var(--box-shadow)'
})
]
),
placementStyle(
'top-start',
`
top: calc(-0.707 * var(--arrow-height));
left: var(--arrow-offset);
`),
placementStyle('top', `
`
),
placementStyle(
'top',
`
top: calc(-0.707 * var(--arrow-height));
transform: translateX(calc(-0.707 * var(--arrow-height))) rotate(45deg);
left: 50%;
`),
placementStyle('top-end', `
`
),
placementStyle(
'top-end',
`
top: calc(-0.707 * var(--arrow-height));
right: var(--arrow-offset);
`),
placementStyle('bottom-start', `
`
),
placementStyle(
'bottom-start',
`
bottom: calc(-0.707 * var(--arrow-height));
left: var(--arrow-offset);
`),
placementStyle('bottom', `
`
),
placementStyle(
'bottom',
`
bottom: calc(-0.707 * var(--arrow-height));
transform: translateX(calc(-0.707 * var(--arrow-height))) rotate(45deg);
left: 50%;
`),
placementStyle('bottom-end', `
`
),
placementStyle(
'bottom-end',
`
bottom: calc(-0.707 * var(--arrow-height));
right: var(--arrow-offset);
`),
placementStyle('left-start', `
`
),
placementStyle(
'left-start',
`
left: calc(-0.707 * var(--arrow-height));
top: var(--arrow-offset-vertical);
`),
placementStyle('left', `
`
),
placementStyle(
'left',
`
left: calc(-0.707 * var(--arrow-height));
transform: translateY(calc(-0.707 * var(--arrow-height))) rotate(45deg);
top: 50%;
`),
placementStyle('left-end', `
`
),
placementStyle(
'left-end',
`
left: calc(-0.707 * var(--arrow-height));
bottom: var(--arrow-offset-vertical);
`),
placementStyle('right-start', `
`
),
placementStyle(
'right-start',
`
right: calc(-0.707 * var(--arrow-height));
top: var(--arrow-offset-vertical);
`),
placementStyle('right', `
`
),
placementStyle(
'right',
`
right: calc(-0.707 * var(--arrow-height));
transform: translateY(calc(-0.707 * var(--arrow-height))) rotate(45deg);
top: 50%;
`),
placementStyle('right-end', `
`
),
placementStyle(
'right-end',
`
right: calc(-0.707 * var(--arrow-height));
bottom: var(--arrow-offset-vertical);
`)
`
)
])
function placementStyle (
placement,
arrowStyleLiteral
placement: FollowerPlacement,
arrowStyleLiteral: string
) {
const position = placement.split('-')[0]
const position = placement.split('-')[0] as
| 'top'
| 'right'
| 'bottom'
| 'left'
const sizeStyle = ['top', 'bottom'].includes(position)
? 'height: var(--space-arrow);'
: 'width: var(--space-arrow);'
return c(`[v-placement="${placement}"]`, [
cB('popover', `
cB(
'popover',
`
margin-${oppositePlacement[position]}: var(--space-arrow);
`, [
cM('no-arrow', `
`,
[
cM(
'no-arrow',
`
margin-${position}: var(--space);
margin-${oppositePlacement[position]}: var(--space);
`),
cB('popover-arrow-wrapper', `
`
),
cB(
'popover-arrow-wrapper',
`
right: 0;
left: 0;
top: 0;
@ -157,9 +233,10 @@ function placementStyle (
${position}: 100%;
${oppositePlacement[position]}: auto;
${sizeStyle}
`, [
cB('popover-arrow', arrowStyleLiteral)
])
])
`,
[cB('popover-arrow', arrowStyleLiteral)]
)
]
)
])
}

View File

@ -1,10 +1,12 @@
import commonVariables from './_common'
import { commonDark } from '../../_styles/new-common'
import type { ThemeCommonVars } from '../../_styles/new-common'
import commonVariables from './_common'
import { PopoverThemeVars } from './light'
export default {
name: 'Popover',
common: commonDark,
self (vars) {
self (vars: ThemeCommonVars): PopoverThemeVars {
const {
popoverColor,
textColor2Overlay,

View File

@ -1,2 +0,0 @@
export { default as popoverDark } from './dark.js'
export { default as popoverLight } from './light.js'

View File

@ -0,0 +1,2 @@
export { default as popoverDark } from './dark'
export { default as popoverLight } from './light'

View File

@ -1,10 +1,11 @@
import commonVariables from './_common'
import { commonLight } from '../../_styles/new-common'
import type { ThemeCommonVars } from '../../_styles/new-common'
import commonVariables from './_common'
export default {
const popoverLight = {
name: 'Popover',
common: commonLight,
self (vars) {
self (vars: ThemeCommonVars) {
const {
boxShadow2,
popoverColor,
@ -22,3 +23,7 @@ export default {
}
}
}
export default popoverLight
export type PopoverThemeVars = ReturnType<typeof popoverLight.self>