声明:本文为稀土掘金技术社区首å?‘ç¾çº¦æ–‡ç« ,30天内ç¦?æ¢è½¬è½½ï¼Œ30天å?ŽæœªèŽ·æŽˆæ?ƒç¦?æ¢è½¬è½½ï¼Œä¾µæ?ƒå¿…究ï¼?
�言
承接上文,我们已ç»?知é?“äº†æ— å¤´ç»„ä»¶åº“ Headless UI
是什么,以å?Šæœ‰ä»€ä¹ˆæ ·çš„作用,包括怎么去实现一个最基本 Headless UI
æ— å¤´ç»„ä»¶åº“æ¡†æž¶ï¼›
å¦‚æžœä½ è¿˜ä¸?知é?“也没关系,看看《Headless UI》这个å…?费的专æ ?就行了;
æ£æ‰€è°“å??而论é?“ã€?夸夸其谈ã€?纸上谈兵的人比比皆是,我们è¦?å?šçš„ä¸?ä»…ä»…è¦?能论其é?“ã€?夸其谈,也è¦?行胜于言,更è¦?实战,真æ£å?šåˆ°çŸ¥è¡Œå?ˆä¸€ã€‚
那么接下æ?¥ï¼Œå°±æ?¥åˆ°äº†ç¬¬äºŒç¯‡çš„敲代ç ?实战环节 ~
一�分�对比
1)分æž?ä¼ ç»Ÿ UI 组件库的 Popover 组件需è¦?什么?
我相信大家多多少少都使用过 Ant-Design
�Element
�Vant
ç‰ç‰ä¼ 统 UI 组件库ä¸çš„å…¶ä¸ä¸€ç§?ï¼Œè€Œä¸”å‡ ä¹Žéƒ½ä½¿ç”¨è¿‡äº†å…¶ä¸çš„ Popover 组件
功能,那么我们分�一下他们的共�点:
1.1 ä¼ ç»Ÿ UI 组件库的 Popover 组件基本使用:
1. Element:
2. Ant-Design:
1.2 其它更多功能:
- 触å?‘æ–¹å¼?:点击ã€?è?šç„¦ã€?悬浮ç‰ç‰
- �置显示
- 自定义内容
我相信这是大部分 Popover ç»„ä»¶æ‰€éœ€çš„åŠŸèƒ½ç‚¹ï¼Œå¦‚æžœä½ èƒ½å…¨éƒ¨å®žçŽ°ï¼Œé‚£åŸºæœ¬å°±ç®—æ˜¯ä¸€ä¸ªå?ˆæ ¼çš„组件了。
我们å?¯ä»¥æ ¹æ?®æ¤æ?¥æŠ½ç¦»å…¶ä¸çš„æ ·å¼?,æ?¥å®žçŽ°ä¸€ä¸ªä»…需è¦?äº¤äº’é€»è¾‘çš„æ— å¤´ Popover 组件;
2)分æž?ä¸€ä¸ªæ— å¤´ç»„ä»¶åº“ Popover 组件需è¦?什么?
其实,一个å?ªæœ‰äº¤äº’é€»è¾‘çš„æ— å¤´ Popover ç»„ä»¶ä¸Žä¼ ç»Ÿç»„ä»¶çš„åŠŸèƒ½æ˜¯å¤§å·®ä¸?差的,一些基本使用ç‰ç‰éƒ½æ˜¯å¿…é¡»è¦?具备的;
å?ªæ˜¯åœ¨æ ·å¼?è¿™å?—我们ä¸?需è¦?下更多的功夫;
那么我们实现的功能点就主�包括以下:
- 基本使用
- 触�方�
- �置显示
- 自定义内容
分�完之�,那我们开始实战 ~
二�实战之「基本使用�
å› ä¸ºæˆ‘ä»¬åœ¨ä¸Šä¸€ç¯‡æ–‡ç« ä¸å·²ç»?æ?建好了基本项目框架的架å?,所以我们基于æ¤æ?¥è¿›è¡Œå¼€å?‘å?§ï¼?
1)创建
# �到 package/vue 目录下
cd package/vue
# 新建 Popover 目录
mkdir src/Popover
2)实现基本 PopoverRoot
æ ¹ç»„ä»¶
2.1. 基本�置
新建 PopoverRoot.vue
文件
<script lang="ts">
import type { Ref } from 'vue'
import { createContext } from '@yi-ui/shared'
// 暴露的三个�数
export interface PopoverRootProps {
/**
* 打开状�,当它最�被渲染时。当您�需�控制其打开状�时使用。
*/
defaultOpen?: boolean
/**
* 控制当�组件的打开状�
*/
open?: boolean
/**
* popover的模å¼?。当设置为true时,将ç¦?ç”¨ä¸Žå¤–éƒ¨å…ƒç´ çš„äº¤äº’ï¼Œå¹¶ä¸”å?ªæœ‰å¼¹å‡ºå¼?的内容对å±?幕阅读器å?¯è§?。
*
* @defaultValue false
*/
modal?: boolean
}
// 暴露的事件
export type PopoverRootEmits = {
/**
* 打开状�事件回调
*/
'update:open': [value: boolean]
}
// ç»„ä»¶ä¸Šä¸‹æ–‡ä¼ é€’æ—¶æ‰€éœ€çš„å?‚æ•°
export interface PopoverRootContext {
triggerElement: Ref<HTMLElement | undefined>
contentId: string
open: Ref<boolean>
modal: Ref<boolean>
onOpenChange(value: boolean): void
onOpenToggle(): void
hasCustomAnchor: Ref<boolean>
}
// ä¼ é€’ç»„ä»¶ä¸Šä¸‹æ–‡
export const [injectPopoverRootContext, providePopoverRootContext]
= createContext<PopoverRootContext>('PopoverRoot')
</script>
这段代ç ?很简,我相信大家基本都看得懂,å?ªæ˜¯å…¶ä¸æœ‰ä¸€ä¸ª createContext函数
的出处需�解释一下;
createContext函数
是一个写在yi-ui/shared
å?包ä¸çš„公共方法;主è¦?作用就是让整个组件上下文建立è?”系,方便整个父å?组件的è?”调;
2.2 æ ¸å¿ƒä»£ç ?
è€?规矩,先看代ç ?
<script setup lang="ts">
import { ref, toRefs } from 'vue'
import { useVModel } from '@vueuse/core'
import { PopperRoot } from '../Popper'
// 设置组件的默认属性值
const props = withDefaults(defineProps<PopoverRootProps>(), {
defaultOpen: false,
open: undefined,
modal: false,
})
// 事件
const emit = defineEmits<PopoverRootEmits>()
const { modal } = toRefs(props)
// vueuse 的一个��绑定 hook
// vue 3.4+ �以使用 defineModel:https://cn.vuejs.org/api/sfc-script-setup.html#definemodel
const open = useVModel(props, 'open', emit, {
defaultValue: props.defaultOpen,
passive: (props.open === undefined) as false,
}) as Ref<boolean>
const triggerElement = ref<HTMLElement>()
const hasCustomAnchor = ref(false)
// 暴露给å?组件的属性与方法
providePopoverRootContext({
contentId: '',
modal,
open,
onOpenChange: (value) => {
open.value = value
},
onOpenToggle: () => {
open.value = !open.value
},
triggerElement,
hasCustomAnchor,
})
</script>
<template>
<PopperRoot>
<slot />
</PopperRoot>
</template>
其实整个代ç ?也是很简å?•çš„ï¼Œå¤§å®¶å‡ ä¹Žéƒ½èƒ½çœ‹çš„æ‡‚ï¼Œè¯ä¹‰åŒ–是很明显的;
å…¶ä¸å?¯èƒ½æœ‰ç–‘问的地方,应该就å?ªæœ‰ PopperRoot
了;
PopperRoot
是一个基于@floating-ui/vue
库实现的基本组件;主è¦?ä½œç”¨æ˜¯ä¸ºæµ®åŠ¨å…ƒç´ æ??供锚点定ä½?,并且将其ä½?置定ä½?在当å‰?å?‚è€ƒå…ƒç´ æ—?边的库;
å‡ ä¹Žç»?大多数的组件库都有使用到类似的库,这里就ä¸?展开讲了,大家明白就行了 ~
到这里这个最基本的 PopoverRoot
æ ¹ç»„ä»¶å°±å®žçŽ°å®Œæ¯•äº†ï¼Œæ˜¯ä¸?是很简å?•å‘¢ï¼Œå¯¹çš„,就是这么简å?•ï¼Œå¦‚æžœä½ è¿˜æ²¡çœ‹æ‡‚ï¼Œå»ºè®®å¤šçœ‹å‡ é??哦~
2.3 导出
新建 index.ts
export {
default as PopoverRoot,
type PopoverRootProps,
type PopoverRootEmits,
} from './PopoverRoot.vue'
到这基本就完�了;
那么既然开å?‘完毕了,那么咱们下一æ¥å°±æ˜¯å¼€å§‹åŽ»è°ƒè¯•äº†
2.4 playground 调试
基本使用:
进入目录:
cd playground/vue3
新增 Popover.vue 组件:
<template>
<PopoverRoot :default-open="true">
<div>Your popover content here</div>
</PopoverRoot>
</template>
<script setup lang="ts">
import { PopoverRoot } from '@yi-ui/vue';
</script>
<style scoped>
</style>
渲染效果:
结��数使用:
<template>
<PopoverRoot v-model:open="toggleState" >
<!-- PopoverTrigger -->
<button @click="handleVisible">点击显示/��</button>
<!-- PopoverContent -->
<ul v-if="toggleState">
<li>popover content: 11111</li>
<li>popover content: 22222</li>
<li>popover content: 33333</li>
<li>popover content: 44444</li>
<li>popover content: 55555</li>
</ul>
</PopoverRoot>
</template>
<script setup lang="ts">
import { PopoverRoot } from '@yi-ui/vue';
import { ref } from 'vue';
const toggleState = ref(false);
const handleVisible = () => {
toggleState.value = !toggleState.value;
};
</script>
我们能看到点击按钮会显示与��:
其实代ç ?ä¸çš„实现与现在的 PopoverRoot æ ¹ç»„ä»¶
关�性�是很大;
所以接下�我们��的就是把这个 PopoverTrigger 组件
和 PopoverContent 组件
嵌入到 PopoverRoot æ ¹ç»„ä»¶
ä¸åŽ»ï¼Œè¿™æ ·ä½¿ç”¨çš„时候æ‰?会事å?ŠåŠŸå€?ï¼›
3)实现 PopoverTrigger å?组件
3.1 功能介�
PopoverTrigger
å?组件的功能其实很简å?•ï¼Œé‚£å°±æ˜¯åˆ‡æ?¢å¼¹å‡º Popover
的一个触�器功能,默认情况下,它将 PopoverContent
å?组件(也就是主è¦?的内容部分)定ä½?在当å‰?触å?‘器上。
3.2 æ ¸å¿ƒä»£ç ?
<script lang="ts">
import type { PrimitiveProps } from '@/Primitive'
import { useForwardExpose } from '@yi-ui/shared'
export interface PopoverTriggerProps extends PrimitiveProps {}
</script>
<script setup lang="ts">
import { onMounted } from 'vue'
import { injectPopoverRootContext } from './PopoverRoot.vue'
import { Primitive } from '@/Primitive'
import { PopperAnchor } from '@/Popper'
const props = withDefaults(defineProps<PopoverTriggerProps>(), {
as: 'button',
})
const rootContext = injectPopoverRootContext()
const { forwardRef, currentElement: triggerElement } = useForwardExpose()
onMounted(() => {
rootContext.triggerElement.value = triggerElement.value
})
</script>
<template>
<component
:is="rootContext.hasCustomAnchor.value ? Primitive : PopperAnchor"
as-child
>
<Primitive
:ref="forwardRef"
:type="as === 'button' ? 'button' : undefined"
aria-haspopup="dialog"
:aria-expanded="rootContext.open.value"
:aria-controls="rootContext.contentId"
:data-state="rootContext.open.value ? 'open' : 'closed'"
:as="as"
:as-child="props.asChild"
@click="rootContext.onOpenToggle"
>
<slot />
</Primitive>
</component>
</template>
其实它的代ç ?å°±å?ªéœ€è¦?这么多,我们简å?•è§£é‡Šä¸€ä¸‹å…¶ä¸çš„æ ¸å¿ƒå‡½æ•°ï¼š
useForwardExpose:使其�以处�自己的 Ref;
Primitiveï¼šä¸€ä¸ªé€šç”¨çš„åŸºæœ¬å…ƒç´ ç»„ä»¶ï¼›
PopperAnchor:瞄点定ä½?å…ƒç´ ï¼›
3.3 导出
export {
default as PopoverTrigger,
type PopoverTriggerProps,
} from './PopoverTrigger.vue'
3.4 playground 调试
编辑 Popover.vue 组件:
<script setup lang="ts">
import { PopoverRoot, PopoverTrigger } from '@yi-ui/vue'
import { ref } from 'vue'
const toggleState = ref(false)
// function handleVisible() {
// toggleState.value = !toggleState.value
// }
</script>
<template>
<PopoverRoot v-model:open="toggleState">
<!-- PopoverTrigger -->
<!-- <button @click="handleVisible">点击显示/��</button> -->
<PopoverTrigger>
点击显示/��
</PopoverTrigger>
<!-- PopoverContent -->
<ul v-if="toggleState">
<li>popover content: 11111</li>
<li>popover content: 22222</li>
<li>popover content: 33333</li>
<li>popover content: 44444</li>
<li>popover content: 55555</li>
</ul>
</PopoverRoot>
</template>
æ ¹æ?®ä¸Žä¸Šé?¢ PopoverRootæ ¹ç»„ä»¶
调试的对比,我们很明显的少了 handleVisible 函数
ï¼Œè¿™æ˜¯å› ä¸ºæˆ‘ä»¬å·²ç»?在 PopoverTrigger
å?组件的 onOpenToggle
ä¸å®žçŽ°äº†ã€‚
好了,到这里我们已ç»?有了触å?‘器 PopoverTrigger å?组件
,接下æ?¥å°±æ˜¯è¦?实现 PopoverContent å?组件
主�内容了。
4)实现 PopoverContent å?组件
4.1 功能介�
PopoverContent å?组件
主è¦?就是在 PopoverTrigger å?组件
触�器在弹出窗�时的组件内容。
4.2 æ ¸å¿ƒä»£ç ?
è€?规矩,先看代ç ?
<script lang="ts">
export interface PopoverContentProps {
forceMount?: boolean
}
</script>
<script setup lang="ts">
import { injectPopoverRootContext } from './PopoverRoot.vue'
import { useForwardExpose } from '@yi-ui/shared'
import { Presence } from '@/Presence'
const rootContext = injectPopoverRootContext()
const { forwardRef } = useForwardExpose()
</script>
<template>
<Presence :ref="forwardRef" :present="rootContext.open.value">
<slot />
</Presence>
</template>
其实一看,PopoverContent å?组件
实现起�是�是贼简�;
是的,的确贼简�;
我相信上é?¢çš„代ç ?,大家也就对 Presence
会比较陌生,咱们简�介�下:
主�作用就是有�件的显示和��当�的组件,就类似于 vue 的 v-if 功能。
4.3 导出
新建 index.ts
export {
default as PopoverContent,
type PopoverContentProps,
} from './PopoverContent.vue'
4.4 playground 调试
编辑 Popover.vue 组件:
<script setup lang="ts">
import { PopoverRoot, PopoverTrigger, PopoverContent } from '@yi-ui/vue'
</script>
<template>
<PopoverRoot>
<PopoverTrigger>
点击显示/��
</PopoverTrigger>
<PopoverContent>
<ul>
<li>popover content: 11111</li>
<li>popover content: 22222</li>
<li>popover content: 33333</li>
<li>popover content: 44444</li>
<li>popover content: 55555</li>
</ul>
</PopoverContent>
</PopoverRoot>
</template>
æ ¹æ?®ä¸Žä¸Šé?¢ PopoverRootæ ¹ç»„ä»¶
调试的对比,我们�少了 toggleState
æ•°æ?®ï¼Œè¿™æ˜¯å› 为我们已ç»?用 rootContext.open
�进行关�了。
到这里我们一个最最最基本的 Popover 组件
就已�实现了,但是我们从上�分�得�的,一个完整的 Popover 组件
离�开四个大的功能:
- 基本使用
- 触�方�
- �置显示
- 自定义内容
显然我们已�实现了基本使用和触�方�功能,那么接下就是�置显示和自定义内容了
三�实战之「�置显示�
1)�考&分�
æ ¹æ?®æˆ‘们上é?¢å®žçŽ°çš„基本使用,咱们æ€?考一下,ä½?置显示我们应该在哪个组件ä¸å®žçŽ°å‘¢ï¼Ÿ
很明显还是在 PopoverContent å?组件
ä¸ï¼Œæ¯•ç«Ÿå’±ä»¬çŽ°å®žä¸»è¦?内容就是它;
在上�,咱们讲过了 @floating-ui/vue
库是干嘛的了,之所以用到它,还有一个就是,它能自定义�置;
接下�咱们就基于 @floating-ui/vue
库�实现� ~
2)@floating-ui/vue
的简�实用
2.1 安装
pnpm install @floating-ui/vue
2.2 基本使用
<template>
<button ref="reference">å?‚è€ƒå…ƒç´ </button>
<ul ref="floating" :style="floatingStyles">
<li>111æµ®åŠ¨å…ƒç´ å†…å®¹</li>
<li>222æµ®åŠ¨å…ƒç´ å†…å®¹</li>
<li>333æµ®åŠ¨å…ƒç´ å†…å®¹</li>
<li>444æµ®åŠ¨å…ƒç´ å†…å®¹</li>
<li>555æµ®åŠ¨å…ƒç´ å†…å®¹</li>
</ul>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { useFloating } from '@floating-ui/vue';
// 用于定ä½?çš„å?‚考(或锚点)元ç´
const reference = ref(null);
// 用于浮动的元ç´
const floating = ref(null);
// ç”¨äºŽæŽ§åˆ¶æµ®åŠ¨å…ƒç´ çš„æ ·å¼?
const { floatingStyles } = useFloating(reference, floating);
</script>
渲染效果如下:
默认情况下,
æµ®åŠ¨å…ƒç´ å†…å®¹
将定ä½?在å?‚考元ç´
的底部ä¸å¿ƒï¼Œæ‰€ä»¥çœ‹åˆ°æ•ˆæžœæ˜¯è¿™æ ·çš„。 那么接下æ?¥ï¼Œå°±æ˜¯è¦?更改ä½?置的进阶使用了。
2.3 进阶使用
<template>
<div class="content">
<div>
<button ref="reference">å?‚è€ƒå…ƒç´ å?‘下</button>
<ul ref="floating" :style="floatingStyles">
<li>æµ®åŠ¨å…ƒç´ å†…å®¹</li>
<li>æµ®åŠ¨å…ƒç´ å†…å®¹</li>
<li>æµ®åŠ¨å…ƒç´ å†…å®¹</li>
<li>æµ®åŠ¨å…ƒç´ å†…å®¹</li>
<li>æµ®åŠ¨å…ƒç´ å†…å®¹</li>
</ul>
</div>
<div>
<button ref="referenceTop">å?‚è€ƒå…ƒç´ å?‘上</button>
<ul ref="floatingTop" :style="floatingTopStyles">
<li>æµ®åŠ¨å…ƒç´ å†…å®¹</li>
<li>æµ®åŠ¨å…ƒç´ å†…å®¹</li>
<li>æµ®åŠ¨å…ƒç´ å†…å®¹</li>
<li>æµ®åŠ¨å…ƒç´ å†…å®¹</li>
<li>æµ®åŠ¨å…ƒç´ å†…å®¹</li>
</ul>
</div>
<div>
<button ref="referenceLeft">å?‚è€ƒå…ƒç´ å?‘å·¦</button>
<ul ref="floatingLeft" :style="floatingLeftStyles">
<li>æµ®åŠ¨å…ƒç´ å†…å®¹</li>
<li>æµ®åŠ¨å…ƒç´ å†…å®¹</li>
<li>æµ®åŠ¨å…ƒç´ å†…å®¹</li>
<li>æµ®åŠ¨å…ƒç´ å†…å®¹</li>
<li>æµ®åŠ¨å…ƒç´ å†…å®¹</li>
</ul>
</div>
<div>
<button ref="referenceRight">å?‚è€ƒå…ƒç´ å?‘å?³</button>
<ul ref="floatingRight" :style="floatingRightStyles">
<li>æµ®åŠ¨å…ƒç´ å†…å®¹</li>
<li>æµ®åŠ¨å…ƒç´ å†…å®¹</li>
<li>æµ®åŠ¨å…ƒç´ å†…å®¹</li>
<li>æµ®åŠ¨å…ƒç´ å†…å®¹</li>
<li>æµ®åŠ¨å…ƒç´ å†…å®¹</li>
</ul>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { useFloating, shift, flip, offset } from '@floating-ui/vue';
// 用于定ä½?çš„å?‚考(或锚点)元ç´
const reference = ref(null);
const referenceTop = ref(null);
const referenceLeft = ref(null);
const referenceRight = ref(null);
// 用于浮动的元ç´
const floating = ref(null);
const floatingTop = ref(null);
const floatingLeft = ref(null);
const floatingRight = ref(null);
// ä¸é—´ä»¶
const middleware = [shift(), flip(), offset(10)];
// ç”¨äºŽæŽ§åˆ¶æµ®åŠ¨å…ƒç´ çš„æ ·å¼?
const { floatingStyles } = useFloating(reference, floating, {
// 指定�始化浮动�置
placement: "bottom",
middleware,
});
const { floatingStyles: floatingTopStyles } = useFloating(referenceTop, floatingTop, {
// 指定�始化浮动�置
placement: "top",
middleware,
});
const { floatingStyles: floatingLeftStyles } = useFloating(referenceLeft, floatingLeft, {
// 指定�始化浮动�置
placement: "left",
middleware,
});
const { floatingStyles: floatingRightStyles } = useFloating(referenceRight, floatingRight, {
// 指定�始化浮动�置
placement: "right",
middleware,
});
</script>
<style scoped>
.content {
display: flex;
width: 100vw;
height: 1000px;
padding-top: 300px;
}
.content > div {
width: 20%;
height: 500px;
}
ul, li {
list-style: none;
margin: 0;
padding: 0;
}
ul {
border: 1px solid #ccc;
text-align: center;
}
</style>
咱们直接看渲染结果:
到这里,咱们已ç»?熟悉了@floating-ui/vue 的基本使用了,当然还有超多å¤?æ?‚çš„API 使用,这里就ä¸?赘述了,感兴趣的å?Œå¦å?¯ä»¥è‡ªè¡Œäº†è§£å®˜æ–¹æ–‡æ¡£ï¼šä¼ é€?门。
那么接下æ?¥å°±æ˜¯å¦‚ä½•ä»£å…¥åˆ°æˆ‘ä»¬çš„æ— å¤´ç»„ä»¶åº“ä¸åŽ»äº†ã€‚
3)�装 Popper 共用组件
如果直接按照上é?¢çš„使用,我想这ä¸?是一个å?ˆæ ¼çš„ç»„ä»¶åº“ï¼Œå› ä¸ºå’±ä»¬è€ƒé‡?的还有很多,比如代ç ?解耦ã€?代ç ?å¤?用ç‰ç‰ã€‚
所以咱们需è¦?æ ¹æ?®ä¸Šé?¢æ?¥ç®€å?•å°?装一个 Popper 共用组件
。
该组件包括:
PopperRoot
: æ ¹ç»„ä»¶PopperContent
:主�内容组件PopperArrow
:ç®å¤´PopperAnchor
:触�器
å…¶ä¸æ ¸å¿ƒçš„ PopperContent
代ç ?(抽å?–å…¶ä¸éƒ¨åˆ†ï¼Œä¸»è¦?为了实现ä½?置显示):
<script lang="ts">
import type {
Placement,
} from '@floating-ui/vue'
import { createContext, useForwardExpose } from '@yi-ui/shared'
import type {
Align,
Side,
} from './utils'
export const PopperContentPropsDefaultValue = {
side: 'bottom' as Side,
sideOffset: 0,
align: 'center' as Align,
alignOffset: 0,
}
export interface PopperContentProps {
/**
* ä½?ç½®
*
* @defaultValue "top"
*/
side?: Side
/**
* �离触�器的�离
*
* @defaultValue 0
*/
sideOffset?: number
/**
* 对�方�相对于触�器
*
* @defaultValue "center"
*/
align?: Align
/**
* å??移é‡?
*
* @defaultValue 0
*/
alignOffset?: number
}
export interface PopperContentContext {}
export const [injectPopperContentContext, providePopperContentContext]
= createContext<PopperContentContext>('PopperContent')
</script>
<script setup lang="ts">
import { computed, ref } from 'vue'
import {
useFloating,
} from '@floating-ui/vue'
import { injectPopperRootContext } from './PopperRoot.vue'
import {
Primitive,
} from '../Primitive'
const props = withDefaults(defineProps<PopperContentProps>(), {
...PopperContentPropsDefaultValue,
})
const rootContext = injectPopperRootContext()
const { forwardRef } = useForwardExpose()
const floatingRef = ref<HTMLElement>()
const desiredPlacement = computed(
() =>
(props.side
+ (props.align !== 'center' ? `-${props.align}` : '')) as Placement,
)
const { floatingStyles } = useFloating(
rootContext.anchor,
floatingRef,
{
strategy: 'fixed',
placement: desiredPlacement,
},
)
</script>
<template>
<div
ref="floatingRef"
:style="{
...floatingStyles,
}"
>
<Primitive
:ref="forwardRef"
>
<slot />
</Primitive>
</div>
</template>
我相信通过上�的 @floating-ui/vue
的简å?•å®žç”¨ï¼Œå¤§å®¶éƒ½èƒ½çœ‹çš„懂其ä¸çš„代ç ?ï¼›
还有更多的细节和相关组件这里就��赘述介�了,当然一个完整的 Popper 共用组件
è¿œä¸?æ¢äºŽæ¤ï¼Œæ„Ÿå…´è¶£çš„å?Œå¦å?¯ä»¥çœ‹çœ‹æº?代ç ?ï¼šä¼ é€?é—¨
4)完善 PopoverContent å?组件
æ ¹æ?®ä¸Šé?¢çš„å·²ç»?实现了的 PopoverContent å?组件
�完善
<script lang="ts">
import type { PopperContentProps } from '@/Popper'
import type { PrimitiveProps } from '@/Primitive'
export interface PopoverContentProps extends PopperContentProps, PrimitiveProps {
forceMount?: boolean
}
</script>
<script setup lang="ts">
import { injectPopoverRootContext } from './PopoverRoot.vue'
import { useForwardExpose, useForwardPropsEmits } from '@yi-ui/shared'
import { PopperContent } from '@/Popper'
import { Presence } from '@/Presence'
const props = defineProps<PopperContentProps>()
const emits = defineEmits()
const rootContext = injectPopoverRootContext()
const forwarded = useForwardPropsEmits(props, emits)
const { forwardRef } = useForwardExpose()
</script>
<template>
<Presence :ref="forwardRef" :present="rootContext.open.value">
<PopperContent
v-bind="forwarded"
:ref="forwardRef"
>
<slot />
</PopperContent>
</Presence>
</template>
æ˜¯çš„ï¼Œè¿™æ ·å°±å®žçŽ°äº†ï¼ŒåŸºæœ¬çš„ä½?置了
5)playground 调�
5.1 vue 项目ä¸è°ƒè¯•
<script setup lang="ts">
import { PopoverContent, PopoverRoot, PopoverTrigger } from '@yi-ui/vue'
</script>
<template>
<PopoverRoot>
<PopoverTrigger>
点击显示/��左边的内容
</PopoverTrigger>
<PopoverContent side="left">
<ul>
<li>popover content: 11111</li>
<li>popover content: 22222</li>
<li>popover content: 33333</li>
<li>popover content: 44444</li>
<li>popover content: 55555</li>
</ul>
</PopoverContent>
</PopoverRoot>
<PopoverRoot>
<PopoverTrigger>
点击显示/���边的内容
</PopoverTrigger>
<PopoverContent side="right">
<ul>
<li>popover content: 11111</li>
<li>popover content: 22222</li>
<li>popover content: 33333</li>
<li>popover content: 44444</li>
<li>popover content: 55555</li>
</ul>
</PopoverContent>
</PopoverRoot>
</template>
5.2 渲染效果
四�实战之「自定义内容�
自定义内容,其实在上é?¢å°±å·²ç»?实现了,å?ªæ˜¯æ²¡æœ‰å…·ä½“çš„ demo å±•ç¤ºï¼Œå’±ä»¬æ ¹æ?®ä¸Šé?¢å®žçŽ°äº†çš„æ?¥è‡ªå®šä¹‰ä¸€ä¸‹å†…容
1)demo 展示:
<script setup lang="ts">
import { PopoverContent, PopoverRoot, PopoverTrigger } from '@yi-ui/vue'
</script>
<template>
<PopoverRoot>
<PopoverTrigger>
切�县市
</PopoverTrigger>
<PopoverContent>
<ul>
<li>北京市</li>
<li>上海市</li>
<li>广州市</li>
<li>深圳市</li>
</ul>
</PopoverContent>
</PopoverRoot>
</template>
<style scoped>
ul, li {
list-style: none;
margin: 0;
padding: 0;
}
ul {
border: 1px solid #ccc;
text-align: center;
width: 100px;
background: #ffffff;
border-radius: 4px;
box-shadow: 0px 3px 16px 2px rgba(0,0,0,0.04), 0px 7px 14px 1px rgba(0,0,0,0.08), 0px 5px 5px -3px rgba(0,0,0,0.08);
}
ul li {
height: 40px;
line-height: 40px;
font-size: 14px;
cursor: pointer;
color: #333333;
}
li:hover {
background: #f2f8ff;
color: #006aff;
}
</style>
2)渲染效果:
五��结
å¦‚æžœä½ æœ‰å®žçŽ°è¿‡ä¸€ä¸ªä¼ ç»Ÿç»„ä»¶åº“ï¼Œæˆ‘ç›¸ä¿¡è¿™ä¸ªæ— å¤´ç»„ä»¶åº“çš„å®žçŽ°æ–¹å¼?è‚¯å®šä¼šè®©ä½ çœ¼å‰?一亮;
å› ä¸ºå®ƒçš„ç¡®å°‘äº†å¾ˆå¤šä¸œè¥¿ï¼Œå°±å?•ç‹¬æ‹¿ style æ ·å¼?æ?¥è¯´ï¼Œä½ ä¸?ä»…è¦?æŠŠä¸€äº›æ ·å¼?抽离出æ?¥ï¼Œè¿˜è¦?写æˆ?å?¯å…±ç”¨çš„æ ·å¼?,怎么æ?¶å¿ƒæ€Žä¹ˆæ?¥ï¼›
�说了,说多了都是泪~
当然我们一个完整的 Popover æ— å¤´ç»„ä»¶
ç»?ä¸?ä»…é™?于æ¤ï¼Œä¸ºäº†æ›´å¥½çš„çª?出其功能的自定义,还有许多组件与功能å°?装,这里就ä¸?æ˜¯ä¸€ä¸¤ç¯‡æ–‡ç« èƒ½è®²è§£çš„æ¸…æ¥šçš„ï¼Œå¤§å®¶å?ªè¦?知é?“其基本实现方å¼?与概念å?³å?¯ã€‚
åŽŸæœ¬æƒ³åœ¨è¿™ç¯‡æ–‡ç« ä¸æŠŠæ–‡æ¡£å’Œå?•å…ƒæµ‹è¯•éƒ½è®²è§£äº†çš„,但是写ç?€å?‘现越写越多,内容越æ?¥è¶Šå¤šï¼Œæ¯•ç«Ÿ 4 å?ƒå¤šå—å·²ç»?够看了。
还未完善的有:
- 文档撰写
- �元测试
- 支� Nuxt 调试
- 打包构建
所以å?šäº†ä¸€ä¸ªåŠŸèƒ½åˆ†å‰²å?–èˆ?ï¼Œè¿™ç¯‡ä»¥æ ¸å¿ƒä»£ç ?为主;
下一篇�讲解文档编写和�元测试的讲解,敬请期待~
总结
当å‰?主è¦?实现了一个最基本的 Popover æ— å¤´ç»„ä»¶
,包括其ä¸çš„ä¸€äº›æ ¸å¿ƒçŸ¥è¯†ç‚¹ï¼Œæˆ‘ç›¸ä¿¡æ— è®ºä½ æ˜¯è¦?å?šä¸€ä¸ªæ— 头组件库
è¿˜æ˜¯ä¼ ç»Ÿç»„ä»¶åº“
这都会对�益匪浅的。
Headless UI å¾€æœŸç›¸å…³æ–‡ç« ï¼š
- 在 2023 年屌爆了一整年的 shadcn/ui 用的 Headless UI 到底是何方神圣?
- 实战开始 🚀 在 React å’Œ Vue3 ä¸ä½¿ç”¨ Headless UI æ— å¤´ç»„ä»¶åº“
- æ— å¤´ç»„ä»¶åº“æ—¢ç„¶è¿™ä¹ˆç?« 🔥 那么我们自己手动实现一个æ?¥å®Œæˆ?所谓的 KPI å?§
如果想跟我一起讨论技术å?¹æ°´æ‘¸é±¼ï¼Œ æ¬¢è¿ŽåŠ å…¥å‰?端å¦ä¹ 群è?Š
如果扫ç ?人数满了,å?¯ä»¥æ‰«ç ?æ·»åŠ ä¸ªäºº vx æ‹‰ä½ ï¼šJeddyGong
感谢大家的支æŒ?,ç ?å—实在ä¸?易,其ä¸å¦‚è‹¥æœ‰é”™è¯¯ï¼Œæœ›æŒ‡å‡ºï¼Œå¦‚æžœæ‚¨è§‰å¾—æ–‡ç« ä¸?错,记得 ç‚¹èµžå…³æ³¨åŠ æ”¶è—?
哦 ~
关注我,带您一起��端 ~