diff --git a/src/popover/demos/enUS/placement.demo.md b/src/popover/demos/enUS/placement.demo.md index 1321be1b4..1920e05a9 100644 --- a/src/popover/demos/enUS/placement.demo.md +++ b/src/popover/demos/enUS/placement.demo.md @@ -97,6 +97,10 @@ ``` +```script +console.log('wow') +``` + ```css .popover-grid { display: grid; diff --git a/src/popover/index.js b/src/popover/index.js deleted file mode 100644 index 760b2e681..000000000 --- a/src/popover/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default as NPopover } from './src/Popover' diff --git a/src/popover/index.ts b/src/popover/index.ts new file mode 100644 index 000000000..716a1510b --- /dev/null +++ b/src/popover/index.ts @@ -0,0 +1 @@ +export { default as NPopover, popoverProps } from './src/Popover' diff --git a/src/popover/src/Popover.js b/src/popover/src/Popover.js deleted file mode 100644 index 24e196507..000000000 --- a/src/popover/src/Popover.js +++ /dev/null @@ -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 - ) - ] - } - }) - } -}) diff --git a/src/popover/src/Popover.ts b/src/popover/src/Popover.ts new file mode 100644 index 000000000..d1d0aeb02 --- /dev/null +++ b/src/popover/src/Popover.ts @@ -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, + 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(null) + const hideTimerIdRef = ref(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( + '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 + ) + ] + } + }) + } +}) diff --git a/src/popover/src/PopoverBody.js b/src/popover/src/PopoverBody.js deleted file mode 100644 index b27ccf710..000000000 --- a/src/popover/src/PopoverBody.js +++ /dev/null @@ -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 - } - } - ) - } -}) diff --git a/src/popover/src/PopoverBody.ts b/src/popover/src/PopoverBody.ts new file mode 100644 index 000000000..71899766e --- /dev/null +++ b/src/popover/src/PopoverBody.ts @@ -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, + 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( + 'Popover', + 'Popover', + style, + popoverLight, + props + ) + const followerRef = ref(null) + const NPopover = inject('NPopover') as PopoverInjection + const followerEnabledRef = ref(props.show) + const directivesRef = computed(() => { + 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 + } + } + ) + } +}) diff --git a/src/popover/src/styles/index.cssr.js b/src/popover/src/styles/index.cssr.ts similarity index 63% rename from src/popover/src/styles/index.cssr.js rename to src/popover/src/styles/index.cssr.ts index 0fe98d26c..095829411 100644 --- a/src/popover/src/styles/index.cssr.js +++ b/src/popover/src/styles/index.cssr.ts @@ -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)] + ) + ] + ) ]) } diff --git a/src/popover/styles/_common.js b/src/popover/styles/_common.ts similarity index 100% rename from src/popover/styles/_common.js rename to src/popover/styles/_common.ts diff --git a/src/popover/styles/dark.js b/src/popover/styles/dark.ts similarity index 74% rename from src/popover/styles/dark.js rename to src/popover/styles/dark.ts index 049610648..2dc010141 100644 --- a/src/popover/styles/dark.js +++ b/src/popover/styles/dark.ts @@ -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, diff --git a/src/popover/styles/index.js b/src/popover/styles/index.js deleted file mode 100644 index 17a000255..000000000 --- a/src/popover/styles/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export { default as popoverDark } from './dark.js' -export { default as popoverLight } from './light.js' diff --git a/src/popover/styles/index.ts b/src/popover/styles/index.ts new file mode 100644 index 000000000..a24b98e5d --- /dev/null +++ b/src/popover/styles/index.ts @@ -0,0 +1,2 @@ +export { default as popoverDark } from './dark' +export { default as popoverLight } from './light' diff --git a/src/popover/styles/light.js b/src/popover/styles/light.ts similarity index 66% rename from src/popover/styles/light.js rename to src/popover/styles/light.ts index fd4f0dd88..838afab9c 100644 --- a/src/popover/styles/light.js +++ b/src/popover/styles/light.ts @@ -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