Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat(Popup): support plugin usage #2219

Merged
merged 3 commits into from
Mar 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
28 changes: 28 additions & 0 deletions src/popup/_example/plugin.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<template>
<t-space>
<t-button variant="outline" @click="handleElement1" class="trigger-element1">已渲染的节点1</t-button>
<t-button variant="outline" @click="handleElement2" class="trigger-element2">已渲染的节点2</t-button>
</t-space>
</template>

<script lang="jsx">
export default {
methods: {
handleElement1() {
this.$popup('.trigger-element1', '渲染文本内容', {
placement: 'bottom',
showArrow: true,
trigger: 'hover',
destroyOnClose: true,
});
},
handleElement2() {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
this.$popup('.trigger-element2', (h) => <div>渲染的是DOM节点</div>, {
placement: 'top',
trigger: 'click',
});
},
},
};
</script>
8 changes: 4 additions & 4 deletions src/popup/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import './style';

export type PopupProps = TdPopupProps;
export * from './type';
export * from './plugin';

export const Popup = withInstall(mapProps(
['visible'],
{ model: { prop: 'visible', event: 'visible-change' } },
)(_Popup));
export const Popup = withInstall(
mapProps(['visible'], { model: { prop: 'visible', event: 'visible-change' } })(_Popup),
);

export default Popup;
210 changes: 210 additions & 0 deletions src/popup/plugin.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
import Vue, { VNode } from 'vue';
import { createPopper, Instance } from '@popperjs/core';
import {
getAttach, once, on, off,
} from '../utils/dom';
import props from './props';
import { getClassPrefixMixins } from '../config-provider/config-receiver';
import { renderTNodeJSX } from '../utils/render-tnode';
import { getPopperPlacement, triggers } from './utils';
import mixins from '../utils/mixins';

import type { TNode, ClassName } from '../common';
import type { TdPopupProps } from './type';

export interface PopupPluginApi {
config: TdPopupProps;
}
const classPrefixMixins = getClassPrefixMixins('popup');

let popperInstance: Instance;
let overlayInstance: HTMLElement;
let timeout: NodeJS.Timeout;
let triggerEl: HTMLElement;

const triggerType = (triggerProps: string): Record<(typeof triggers)[number], boolean> => triggers.reduce(
(map, trigger) => ({
...map,
[trigger]: triggerProps.includes(trigger),
}),
{} as any,
);

const Overlay = mixins(classPrefixMixins).extend({
name: 'TPopupOverlay',
data() {
return {
visibleState: false,
contentClicked: false,
};
},
props: {
...props,
triggerEl: HTMLElement,
},
computed: {
hasTrigger(): Record<(typeof triggers)[number], boolean> {
return triggerType(this.trigger);
},
overlayClasses(): ClassName {
return [
`${this.componentName}__content`,
{
[`${this.componentName}__content--text`]: this.content === 'string',
[`${this.componentName}__content--arrow`]: this.showArrow,
[this.commonStatusClassName.disabled]: this.disabled,
},
this.overlayInnerClassName,
];
},
},

methods: {
handleDocumentClick(e: Event): void {
if (triggerEl?.contains(e.target as Node)) return;
if (this.contentClicked) {
setTimeout(() => {
this.contentClicked = false;
});
} else {
if (this.destroyOnClose) {
this.visibleState = false;
}
popperInstance?.destroy();
popperInstance = null;
triggerEl = null;
}
},
handleMouseLeave(): void {
if (this.destroyOnClose) {
this.visibleState = false;
}
popperInstance?.destroy();
popperInstance = null;
},
handleMouseEnter(): void {
clearTimeout(timeout);
},
},
created() {
this.visibleState = true;
},
mounted() {
setTimeout(() => {
on(document, 'click', this.handleDocumentClick);
});
},
beforeDestroy() {
off(document, 'click', this.handleDocumentClick);
},
render(h): VNode {
const content = renderTNodeJSX(this, 'content');

const hidePopup = this.hideEmptyPopup && ['', undefined, null].includes(content);
const {
handleMouseLeave, handleMouseEnter, visibleState, hasTrigger,
} = this;
const renderNode = h(
'div',
{
class: [this.componentName, this.overlayClassName],
ref: 'popper',
style: [
hidePopup && { visibility: 'hidden', pointerEvents: 'none' },
{ zIndex: this.zIndex },
this.overlayStyle,
],
on: {
mousedown: () => {
this.contentClicked = true;
},
...(hasTrigger.hover && {
mouseenter: handleMouseEnter,
mouseleave: handleMouseLeave,
}),
},
},
[
h(
'div',
{
ref: 'overlay',
class: this.overlayClasses,
style: this.overlayInnerStyle,
},
[content, this.showArrow && h('div', { class: `${this.componentName}__arrow` })],
),
],
);
return visibleState ? (
<transition slot="content" name={`${this.componentName}--animation`} appear>
{renderNode}
</transition>
) : null;
},
});

const removeOverlayInstance = () => {
if (overlayInstance) {
overlayInstance.remove();
overlayInstance = null;
}
if (popperInstance) {
popperInstance.destroy();
popperInstance = null;
}
};

const triggerPopupPlugin = (trigger: string, content: TNode, popupProps: TdPopupProps) => {
const hasTrigger = triggerType(popupProps.trigger || 'hover');
const currentTriggerEl = getAttach(trigger);
if (triggerEl && hasTrigger.click) {
return;
}
triggerEl = currentTriggerEl;
removeOverlayInstance();

let attach = getAttach(popupProps.attach);

const delay = [].concat(popupProps.delay ?? [250, 150]);
const closeDelay = delay[1] ?? delay[0];
if (attach === document.body) {
// don't allow mount on body directly
const popupDom = document.createElement('div');
document.body.appendChild(popupDom);
attach = popupDom;
}

overlayInstance = new Overlay({
propsData: {
...popupProps,
content,
triggerEl,
},
}).$mount(attach).$el as HTMLElement;

if (hasTrigger.hover) {
const mouseoutEvent = () => {
timeout = setTimeout(removeOverlayInstance, closeDelay);
};
once(triggerEl, 'mouseleave', mouseoutEvent);
} else if (hasTrigger.focus) {
const focusoutEvent = () => {
timeout = setTimeout(removeOverlayInstance, closeDelay);
};
once(triggerEl, 'focusout', focusoutEvent);
}

popperInstance = createPopper(triggerEl, overlayInstance, {
placement: getPopperPlacement(popupProps.placement as TdPopupProps['placement']),
...popupProps.popperOptions,
});
};

Vue.prototype.$popup = triggerPopupPlugin;

declare module 'vue/types/vue' {
interface Vue {
$popup: PopupPluginApi;
}
}
10 changes: 10 additions & 0 deletions src/popup/popup.en-US.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,13 @@ name | params | description
scroll | `(context: { e: WheelEvent })` | \-
scroll-to-bottom | `(context: { e: WheelEvent })` | \-
visible-change | `(visible: boolean, context: PopupVisibleChangeContext)` | [see more ts definition](https://github.com/Tencent/tdesign-vue/tree/develop/src/popup/type.ts)。<br/>`interface PopupVisibleChangeContext { e?: PopupTriggerEvent; trigger?: PopupTriggerSource }`<br/><br/>`type PopupTriggerEvent = MouseEvent \| FocusEvent \| KeyboardEvent`<br/><br/>`type PopupTriggerSource = 'document' \| 'trigger-element-click' \| 'trigger-element-hover' \| 'trigger-element-blur' \| 'trigger-element-focus' \| 'trigger-element-mousedown' \| 'context-menu' \| 'keydown-esc'`<br/>

### PopupPlugin

support `this.$popup`

name | params | default | description
-- | -- | -- | --
content | String / Slot / Function | - | required。Typescript:`string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-vue/blob/develop/src/common.ts)
popupProps | Object | - | \-
triggerElement | String | - | required
18 changes: 18 additions & 0 deletions src/popup/popup.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
:: BASE_DOC ::

### 通过插件方式调用Popup

通过插件方式调用Popup,用于将Popup渲染在已有节点的场景,同时该方式不论如何调用都只会挂载在一个节点上,用于减少页面上的Popup的渲染节点。

使用方式:`this.$popup(triggerElement, content, popupProps)`

{{ plugin }}

## API
### Popup Props

Expand Down Expand Up @@ -35,3 +43,13 @@ onVisibleChange | Function | | TS 类型:`(visible: boolean, context: PopupVi
scroll | `(context: { e: WheelEvent })` | 下拉选项滚动事件
scroll-to-bottom | `(context: { e: WheelEvent })` | 下拉滚动触底事件,常用于滚动到底执行具体业务逻辑
visible-change | `(visible: boolean, context: PopupVisibleChangeContext)` | 当浮层隐藏或显示时触发,`trigger=document` 表示点击非浮层元素触发;`trigger=context-menu` 表示右击触发。[详细类型定义](https://github.com/Tencent/tdesign-vue/tree/develop/src/popup/type.ts)。<br/>`interface PopupVisibleChangeContext { e?: PopupTriggerEvent; trigger?: PopupTriggerSource }`<br/><br/>`type PopupTriggerEvent = MouseEvent \| FocusEvent \| KeyboardEvent`<br/><br/>`type PopupTriggerSource = 'document' \| 'trigger-element-click' \| 'trigger-element-hover' \| 'trigger-element-blur' \| 'trigger-element-focus' \| 'trigger-element-mousedown' \| 'context-menu' \| 'keydown-esc'`<br/>

### PopupPlugin

同时也支持 `this.$popup`。

参数名称 | 参数类型 | 参数默认值 | 参数说明
-- | -- | -- | --
content | String / Slot / Function | - | 必需。气泡框的内容。TS 类型:`string \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-vue/blob/develop/src/common.ts)
popupProps | Object | - | 透传气泡框/浮层的属性
triggerElement | String | - | 必需。触发气泡框/浮层的元素,传入选择器即可
29 changes: 3 additions & 26 deletions src/popup/popup.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { VNodeDirective } from 'vue';
import { createPopper, Placement } from '@popperjs/core';
import { createPopper } from '@popperjs/core';
import { on, off, once } from '../utils/dom';
import { renderTNodeJSX, renderContent } from '../utils/render-tnode';
import { getIEVersion } from '../utils/helper';
Expand All @@ -10,32 +10,12 @@ import Container from './container';
import { getClassPrefixMixins } from '../config-provider/config-receiver';
import mixins from '../utils/mixins';
import { emitEvent } from '../utils/event';
import { getPopperPlacement, attachListeners, triggers } from './utils';

const classPrefixMixins = getClassPrefixMixins('popup');

const triggers = ['click', 'hover', 'focus', 'context-menu'] as const;
const injectionKey = '__T_POPUP';

function getPopperPlacement(placement: TdPopupProps['placement']) {
return placement.replace(/-(left|top)$/, '-start').replace(/-(right|bottom)$/, '-end') as Placement;
}

function attachListeners(elm: Element) {
const offs: Array<() => void> = [];
return {
add<K extends keyof HTMLElementEventMap>(type: K, listener: (ev: HTMLElementEventMap[K]) => void) {
on(elm, type, listener);
offs.push(() => {
off(elm, type, listener);
});
},
clean() {
offs.forEach((handler) => handler?.());
offs.length = 0;
},
};
}

export default mixins(classPrefixMixins).extend({
name: 'TPopup',

Expand Down Expand Up @@ -88,7 +68,7 @@ export default mixins(classPrefixMixins).extend({
this.overlayInnerClassName,
];
},
hasTrigger(): Record<typeof triggers[number], boolean> {
hasTrigger(): Record<(typeof triggers)[number], boolean> {
return triggers.reduce(
(map, trigger) => ({
...map,
Expand Down Expand Up @@ -201,7 +181,6 @@ export default mixins(classPrefixMixins).extend({
this.popper.update();
return;
}

this.popper = createPopper(triggerEl, popperEl, {
modifiers:
getIEVersion() > 9
Expand Down Expand Up @@ -380,7 +359,6 @@ export default mixins(classPrefixMixins).extend({
const ref = renderContent(this, 'default', 'triggerElement');
const content = renderTNodeJSX(this, 'content');
const hidePopup = this.hideEmptyPopup && ['', undefined, null].includes(content);

const overlay = visible || !destroyOnClose
? h(
'div',
Expand Down Expand Up @@ -429,7 +407,6 @@ export default mixins(classPrefixMixins).extend({
],
)
: null;

return (
<Container
ref="container"
Expand Down
2 changes: 1 addition & 1 deletion src/popup/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export default {
},
/** 浮层出现位置 */
placement: {
type: String,
type: String as PropType<TdPopupProps['placement']>,
default: 'top',
},
/** popper 初始化配置,详情参考 https://popper.js.org/docs/ */
Expand Down
2 changes: 2 additions & 0 deletions src/popup/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

import { TNode, ClassName, Styles, AttachNode } from '../common';

export type PopupMethod = (triggerElement: string, content: string | TNode, popupProps?: object) => void;

export interface TdPopupProps {
/**
* 制定挂载节点。数据类型为 String 时,会被当作选择器处理,进行节点查询。示例:'body' 或 () => document.body
Expand Down