diff --git a/devui/popover/index.ts b/devui/popover/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..8325a36c012a26349dc8ea457c7281712437fb8c --- /dev/null +++ b/devui/popover/index.ts @@ -0,0 +1,17 @@ +import type { App } from 'vue' +import Popover from './src/popover' + +Popover.install = function (app: App): void { + app.component(Popover.name, Popover) +} + +export { Popover } + +export default { + title: 'Popover 悬浮提示', + category: '反馈', + status: '开发中', // TODO: 组件若开发完成则填入"已完成",并删除该注释 + install(app: App): void { + app.use(Popover as any); + } +} diff --git a/devui/popover/src/popover.scss b/devui/popover/src/popover.scss new file mode 100644 index 0000000000000000000000000000000000000000..60f8e1d335f7ad328ba68cfecd0e0c2dea8ed063 --- /dev/null +++ b/devui/popover/src/popover.scss @@ -0,0 +1,323 @@ +@import '../../style/theme/color'; +@import '../../style//theme//corner'; +@import '../../style//theme/font'; +@import '../../style//theme//animation'; + +@mixin some-display { + display: inline-flex; +} + +.devui-popover { + --devui-popover-margin: -8px; + --devui-popover-width: -6px; + --devui-popover-offset: 8px; + + position: relative; + @include some-display; + + &::after { + content: ''; + width: 12px; + display: none; + height: 12px; + transform: rotate(45deg); + position: absolute; + z-index: 1060; + background-color: $devui-feedback-overlay-bg; + } + + &.devui-popover-isVisible { + .devui-popover-content { + @include some-display; + } + + &::after { + @include some-display; + } + } + + .devui-popover-content { + position: absolute; + display: none; + padding: 5px 14px; + align-items: center; + flex-wrap: wrap; + white-space: nowrap; + border-radius: $devui-border-radius-feedback; + color: $devui-feedback-overlay-text; + background-color: $devui-feedback-overlay-bg; + font-size: $devui-font-size-sm; + + &.is-icon { + flex-wrap: nowrap; + } + + .devui-popover-icon { + margin-right: 5px; + } + } +} + +//动画效果 +.devui-popover { + &.devui-popover-animation { + $devui-popover-animation-wait:0s; + @mixin some-animation { + animation: some-animation 0.2s $devui-animation-linear $devui-popover-animation-wait 1 forwards; + } + + .devui-popover-content { + opacity: 0; + @include some-animation; + } + + &::after { + $devui-popover-animation-wait:0.1s; + + opacity: 0; + @include some-animation; + } + } +} + +@keyframes some-animation { + 0% { + @include some-display; + } + + 100% { + opacity: 1; + } +} + +//left等方向 +.devui-popover { + @mixin left-postion--content { + left: var(--devui-popover-margin); + top: 0; + transform: translate(-100%, 0); + } + @mixin left-postion--after { + left: var(--devui-popover-margin); + top: 50%; + margin-top: var(--devui-popover-width); + margin-left: var(--devui-popover-width); + } + + &.left { + .devui-popover-content { + @include left-postion--content(); + + top: 50%; + transform: translate(-100%, -50%); + } + + &::after { + @include left-postion--after; + } + } + + &.left-top { + .devui-popover-content { + @include left-postion--content; + } + + &::after { + @include left-postion--after; + + top: var(--devui-popover-offset); + margin-top: auto; + } + } + + &.left-bottom { + .devui-popover-content { + @include left-postion--content; + + top: auto; + bottom: 0; + } + + &::after { + @include left-postion--after; + + bottom: var(--devui-popover-offset); + margin-top: auto; + } + } +} + +//top等方向 +.devui-popover { + @mixin top-postion--content { + top: var(--devui-popover-margin); + left: 0; + transform: translate(0, -100%); + } + @mixin top-postion--after { + top: var(--devui-popover-margin); + margin-top: var(--devui-popover-width); + margin-left: var(--devui-popover-width); + } + + &.top { + .devui-popover-content { + @include top-postion--content; + + left: 50%; + transform: translate(-50%, -100%); + } + + &::after { + left: 50%; + @include top-postion--after; + } + } + + &.top-left { + .devui-popover-content { + @include top-postion--content; + } + + &::after { + @include top-postion--after; + + left: var(--devui-popover-offset); + margin-left: auto; + margin-right: var(--devui-popover-width); + } + } + + &.top-right { + .devui-popover-content { + @include top-postion--content; + + left: auto; + right: 0; + } + + &::after { + @include top-postion--after; + + right: var(--devui-popover-offset); + } + } +} + +//right等方向 +.devui-popover { + @mixin right-postion--content { + right: var(--devui-popover-margin); + top: 50%; + transform: translate(100%, 0); + } + @mixin right-postion--after { + right: var(--devui-popover-margin); + margin-right: var(--devui-popover-width); + top: var(--devui-popover-offset); + } + + &.right { + .devui-popover-content { + @include right-postion--content; + + transform: translate(100%, -50%); + } + + &::after { + @include right-postion--after; + + top: 50%; + margin-top: var(--devui-popover-width); + } + } + + &.right-top { + .devui-popover-content { + @include right-postion--content; + + top: 0; + } + + &::after { + @include right-postion--after; + } + } + + &.right-bottom { + .devui-popover-content { + @include right-postion--content; + + bottom: 0; + top: auto; + } + + &::after { + @include right-postion--after; + + bottom: var(--devui-popover-offset); + } + } +} + +//bottom等方向 +.devui-popover { + @mixin bottom-postion--content { + left: 0; + bottom: 0; + margin-bottom: var(--devui-popover-width); + transform: translate(0, 100%); + } + @mixin bottom-postion--after { + left: var(--devui-popover-offset); + bottom: var(--devui-popover-margin); + margin-bottom: var(--devui-popover-width); + margin-right: var(--devui-popover-width); + } + + &.bottom { + .devui-popover-content { + @include bottom-postion--content; + + left: 50%; + bottom: 0; + transform: translate(-50%, 100%); + } + + &::after { + @include bottom-postion--after; + + left: 50%; + margin-right: auto; + margin-left: var(--devui-popover-width); + } + } + + &.bottom-left { + .devui-popover-content { + @include bottom-postion--content; + } + + &::after { + @include bottom-postion--after; + } + } + + &.bottom-right { + .devui-popover-content { + @include bottom-postion--content; + + left: auto; + right: 0; + } + + &::after { + @include bottom-postion--after; + + left: auto; + margin-right: auto; + right: var(--devui-popover-offset); + margin-left: var(--devui-popover-width); + } + } +} diff --git a/devui/popover/src/popover.tsx b/devui/popover/src/popover.tsx new file mode 100644 index 0000000000000000000000000000000000000000..9fbd12ca37fc6c2f7baf8f0fec25b79c698b773b --- /dev/null +++ b/devui/popover/src/popover.tsx @@ -0,0 +1,121 @@ +import { defineComponent, toRefs, ref, CSSProperties, reactive } from 'vue' +import clickoutsideDirective from '../../shared/devui-directive/clickoutside' +import './popover.scss' + +type positionType = 'top' | 'right' | 'bottom' | 'left' +type triggerType = 'click' | 'hover' +type popType = 'success' | 'error' | 'warning' | 'info' | 'default' +const popTypeClass = { + success: { name: 'right-o', color: 'rgb(61, 204, 166)' }, + error: { name: 'error-o', color: 'rgb(249, 95, 91)' }, + info: { name: 'info-o', color: 'rgb(81, 112, 255)' }, + warning: { name: 'warning-o', color: 'rgb(254, 204, 85)' }, + default: {} +} + +export default defineComponent({ + name: 'DPopover', + directives: { + clickoutside: clickoutsideDirective + }, + + props: { + visible: { + type: Boolean, + default: false + }, + position: { + type: String as () => positionType, + default: 'bottom' + }, + content: { + type: String, + default: 'default' + }, + + trigger: { + type: String as () => triggerType, + default: 'click', + }, + + zIndex: { + type: Number as () => CSSProperties, + default: 1060 + }, + popType: { + type: String as () => popType, + default: 'default' + }, + showAnimation: { + type: Boolean, + default: true + }, + mouseEnterDelay: { + type: Number, + default: 150 + }, + mouseLeaveDelay: { + type: Number, + default: 100 + } + }, + + setup(props, ctx) { + let enter = null + let leave = null + const visible = ref(props.visible); + const { position, content, zIndex, trigger, popType, mouseEnterDelay, mouseLeaveDelay, showAnimation } = toRefs(props); + const isClick = trigger.value === 'click' + const iconType = reactive(popTypeClass[popType.value]) + const event = function () { + if (visible.value) { + visible.value = false; + return + } + visible.value = true + } + const onClick = isClick ? event : null; + const onMouseenter = isClick ? null : () => { + enter && clearTimeout(enter); + enter = setTimeout(() => { + visible.value = true + }, mouseEnterDelay.value) + } + const onMouseleave = isClick ? null : () => { + leave && clearTimeout(leave) + leave = setTimeout(() => { + visible.value = false + }, mouseLeaveDelay.value) + } + const hiddenContext = function () { + visible.value = false + } + + + return () => { + const { slots } = ctx; + const style: CSSProperties = { + zIndex: zIndex.value + } + + return ( +
+
+ {slots.reference?.()} +
+
+ {iconType.name && } + {slots.content?.() || {content.value}} +
+
+ ) + } + }, +}) diff --git a/sites/.vitepress/config/sidebar.ts b/sites/.vitepress/config/sidebar.ts index 7c9f0f769a5d71664eea48998892d7ab61e698e1..4f7412cc6325386ddba9749ebe3ade67946030e5 100644 --- a/sites/.vitepress/config/sidebar.ts +++ b/sites/.vitepress/config/sidebar.ts @@ -37,7 +37,7 @@ const sidebar = { { text: 'Loading 加载提示', link: '/components/loading/', status: '已完成' }, { text: 'Mention 提及', link: '/components/mention/' }, { text: 'Modal 模态弹窗', link: '/components/modal/' }, - { text: 'Popover 悬浮提示', link: '/components/popover/' }, + { text: 'Popover 悬浮提示', link: '/components/popover/', status: "开发中" }, { text: 'ReadTip 阅读提示', link: '/components/read-tip/' }, { text: 'Toast 全局通知', link: '/components/toast/', status: '已完成' }, { text: 'Tooltip 提示', link: '/components/tooltip/' }, diff --git a/sites/components/popover/index.md b/sites/components/popover/index.md new file mode 100644 index 0000000000000000000000000000000000000000..d6abd794a2b2748e988bbec201e4452ab4761b06 --- /dev/null +++ b/sites/components/popover/index.md @@ -0,0 +1,386 @@ +# Popover 悬浮提示 +简单的文字提示气泡框。 + +### 何时使用 +用来通知用户非关键性问题或提示某控件处于某特殊情况。 + +### 基本用法 +当Popover弹出时,会基于`reference`插槽的内容进行定位。 + +
+ + + + + + + + + + + + + + + + + + +
+ + +```html + + + + + + + + + + + + + + + + + + + + + + +``` + +### 自定义内容 +自定义`reference`插槽的内容与弹出提示内容。 + +
+ + + +
+ +```html +
+ + + +
+``` + +### 弹出位置 +总共支持12个弹出位置。 + +
+ + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + +
+ +```html +
+ + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + +
+``` + +### 延时触发 +仅需要在 trigger 为 hover 的时候,鼠标移入的时长超过 [mouseEnterDelay] 毫秒之后才会触发,以防止用户无意划过导致的闪现,默认值是150毫秒;鼠标移出之后,再经过[mouseLeaveDelay]毫秒后,Popover组件才会隐藏,默认值是100毫秒。 + +
+ + + + + + + + +
+ +```html + + + + + + + + +``` + +### dPopover 参数 + +| 参数 | 类型 | 默认值 | 描述 | 跳转Demo | +| ---- | ---- | ---- | ---- | ---- | +| content | `string` | defalut |可选,弹出框的显示内容或 | [自定义内容](#自定义内容) | +| visible | `boolean` | false | 可选,弹框的初始化弹出状态 | [基本用法](#基本用法) | +| trigger | `string` | click | 可选,弹框触发方式 | [基本用法](#基本用法) | +| popType | `string` | default | 可选,弹出框类型,样式不同 | [基本用法](#基本用法) | +| zIndex | `number` | 1060 | 可选,z-index值用于手动控制层高 | [基本用法](#基本用法) | +| positionType | `string` | bottom | 可选,控制弹框出现 的方向 | [弹出位置](#弹出位置) | +| showAnimation | `boolean` | true | 可选,是否显示动画 | [基本用法](#基本用法) | +| mouseEnterDelay | `number` | 150 | 可选,仅需要在 trigger 为 hover 的时候,设置鼠标移入后延时多少才显示 Popover,单位是 `ms` | [延时触发](#延时触发) | +| mouseLeaveDelay | `number` | 100 | 可选,仅需要在 trigger 为 hover 的时候,设置鼠标移出后延时多少才隐藏 popover,单位是 `ms` | [延时触发](#延时触发) | +