Skip to content

Commit 382b440

Browse files
feat(components): [Popover] implements
1 parent e037390 commit 382b440

37 files changed

+1571
-33
lines changed

packages/vue-primitives/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@
8585
},
8686
"dependencies": {
8787
"@floating-ui/dom": "^1.6.10",
88+
"@floating-ui/utils": "^0.2.7",
8889
"@floating-ui/vue": "^1.1.4",
8990
"aria-hidden": "^1.2.4"
9091
},

packages/vue-primitives/src/dismissable-layer/DismissableLayer.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script setup lang="ts">
2-
import { computed, onBeforeUnmount, shallowRef, useAttrs, watch, watchEffect } from 'vue'
2+
import { computed, onBeforeUnmount, shallowRef, useAttrs, watchEffect } from 'vue'
33
import { Primitive } from '../primitive/index.ts'
44
import { isFunction } from '../utils/is.ts'
55
import { composeEventHandlers, forwardRef } from '../utils/vue.ts'

packages/vue-primitives/src/dismissable-layer/utils.ts

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,9 @@ export function usePointerdownOutside(
4545
const target = event.target as HTMLElement
4646

4747
// TODO: wip
48-
// if (isLayerExist(node.value, target)) {
49-
// isPointerInsideDOMTree = false
50-
// return
51-
// }
48+
if (!isPointerInsideDOMTree && isInsideDOMTree(node.value, target)) {
49+
isPointerInsideDOMTree = true
50+
}
5251

5352
if (target && !isPointerInsideDOMTree) {
5453
const eventDetail = { originalEvent: event }
@@ -143,11 +142,15 @@ export function useFocusOutside(
143142
const ownerDocument = nodeVal.ownerDocument
144143

145144
async function handleFocus(event: FocusEvent) {
146-
// await nextTick()
145+
await nextTick()
146+
147+
if (!node.value)
148+
return
147149

148150
// TODO: wip
149-
// if (!node.value || isLayerExist(node.value, event.target as HTMLElement))
150-
// return
151+
if (!isFocusInsideDOMTree && isInsideDOMTree(node.value, event.target as HTMLElement)) {
152+
isFocusInsideDOMTree = true
153+
}
151154

152155
if (event.target && !isFocusInsideDOMTree) {
153156
const eventDetail = { originalEvent: event }
@@ -163,23 +166,23 @@ export function useFocusOutside(
163166
return ret
164167
}
165168

166-
function isLayerExist(layerElement: HTMLElement, targetElement: HTMLElement) {
167-
const targetLayer = targetElement.closest<HTMLElement>('[data-dismissable-layer]')
169+
function isInsideDOMTree(node: HTMLElement, targetElement: HTMLElement) {
170+
const mainLayer = node.dataset.dismissableLayer === '' ? node : node.querySelector<HTMLElement>('[data-dismissable-layer]')
168171

169-
if (!targetLayer)
172+
if (!mainLayer)
170173
return false
171174

172-
const mainLayer = layerElement.dataset.dismissableLayer === '' ? layerElement : layerElement.querySelector<HTMLElement>('[data-dismissable-layer]')
175+
const targetLayer = targetElement.closest<HTMLElement>('[data-dismissable-layer]')
173176

174-
if (!mainLayer)
177+
if (!targetLayer)
175178
return false
176179

177180
if (mainLayer === targetLayer)
178181
return true
179182

180-
const nodeList = Array.from(layerElement.ownerDocument.querySelectorAll<HTMLElement>('[data-dismissable-layer]'))
183+
const layerList = Array.from(node.ownerDocument.querySelectorAll<HTMLElement>('[data-dismissable-layer]'))
181184

182-
if (nodeList.indexOf(mainLayer) < nodeList.indexOf(targetLayer))
185+
if (layerList.indexOf(mainLayer) < layerList.indexOf(targetLayer))
183186
return true
184187

185188
return false

packages/vue-primitives/src/floating/useFloating.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import {
77
toValue,
88
watch,
99
watchEffect,
10-
watchSyncEffect,
1110
} from 'vue'
1211

1312
import { useRef } from '../hooks/useRef.ts'
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import type { Ref } from 'vue'
2+
import { type MutableRefObject, createContext } from '../hooks/index.ts'
3+
4+
export interface PopoverProps {
5+
id?: string
6+
open?: boolean
7+
defaultOpen?: boolean
8+
modal?: boolean
9+
}
10+
11+
// eslint-disable-next-line ts/consistent-type-definitions
12+
export type PopoverEmits = {
13+
/**
14+
* Event handler called when the open state of the popover changes.
15+
*/
16+
'update:open': [value: boolean]
17+
}
18+
19+
export interface PopoverContextValue {
20+
triggerRef: MutableRefObject<HTMLButtonElement | undefined>
21+
contentId?: string
22+
open: Ref<boolean>
23+
onOpenChange: (open: boolean) => void
24+
onOpenToggle: () => void
25+
hasCustomAnchor: Ref<boolean>
26+
onCustomAnchorAdd: () => void
27+
onCustomAnchorRemove: () => void
28+
modal: () => boolean
29+
}
30+
31+
export const [providePopoverContext, usePopoverContext] = createContext<PopoverContextValue>('Poppover')
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<script setup lang="ts">
2+
import { shallowRef } from 'vue'
3+
import { useControllableState, useRef } from '../hooks/index.ts'
4+
import { Popper } from '../popper/index.ts'
5+
import { type PopoverEmits, type PopoverProps, providePopoverContext } from './Popover.ts'
6+
7+
defineOptions({
8+
name: 'Popover',
9+
})
10+
11+
const props = withDefaults(defineProps<PopoverProps>(), {
12+
open: undefined,
13+
defaultOpen: false,
14+
modal: false,
15+
})
16+
const emit = defineEmits<PopoverEmits>()
17+
18+
const triggerRef = useRef<HTMLButtonElement>()
19+
const hasCustomAnchor = shallowRef(false)
20+
21+
const open = useControllableState(props, v => emit('update:open', v), 'open', props.defaultOpen)
22+
23+
providePopoverContext({
24+
triggerRef,
25+
contentId: props.id,
26+
open,
27+
onOpenChange(value) {
28+
open.value = value
29+
},
30+
onOpenToggle() {
31+
open.value = !open.value
32+
},
33+
hasCustomAnchor,
34+
onCustomAnchorAdd() {
35+
hasCustomAnchor.value = true
36+
},
37+
onCustomAnchorRemove() {
38+
hasCustomAnchor.value = false
39+
},
40+
modal() {
41+
return props.modal
42+
},
43+
})
44+
</script>
45+
46+
<template>
47+
<Popper>
48+
<slot />
49+
</Popper>
50+
</template>
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import type { PopperAnchorProps } from '../popper/index.ts'
2+
3+
export interface PopoverAnchorProps extends PopperAnchorProps {}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<script setup lang="ts">
2+
import { onBeforeUnmount, onMounted } from 'vue'
3+
import { PopperAnchor } from '../popper/index.ts'
4+
import { usePopoverContext } from './Popover.ts'
5+
import type { PopoverAnchorProps } from './PopoverAnchor.ts'
6+
7+
defineOptions({
8+
name: 'PopoverAnchor',
9+
})
10+
11+
const props = defineProps<PopoverAnchorProps>()
12+
13+
const context = usePopoverContext('PopoverAnchor')
14+
15+
onMounted(() => {
16+
context.onCustomAnchorAdd()
17+
})
18+
19+
onBeforeUnmount(() => {
20+
context.onCustomAnchorRemove()
21+
})
22+
</script>
23+
24+
<template>
25+
<PopperAnchor
26+
v-bind="props"
27+
>
28+
<slot />
29+
</PopperAnchor>
30+
</template>
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import type { PrimitiveProps } from '../primitive/index.ts'
2+
3+
export interface PopoverCloseProps extends PrimitiveProps {}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<script setup lang="ts">
2+
import { useAttrs } from 'vue'
3+
import Primitive from '../primitive/Primitive.vue'
4+
import { composeEventHandlers } from '../utils/vue.ts'
5+
import { isFunction } from '../utils/is.ts'
6+
import { usePopoverContext } from './Popover.ts'
7+
import type { PopoverCloseProps } from './PopoverClose.ts'
8+
9+
defineOptions({
10+
name: 'PopoverClose',
11+
inheritAttrs: false,
12+
})
13+
14+
withDefaults(defineProps<PopoverCloseProps>(), {
15+
as: 'button',
16+
})
17+
const attrs = useAttrs()
18+
19+
const context = usePopoverContext('PopoverClose')
20+
21+
const onClick = composeEventHandlers((event) => {
22+
if (isFunction(attrs.onClick))
23+
attrs.onClick(event)
24+
}, () => {
25+
context.onOpenChange(false)
26+
})
27+
</script>
28+
29+
<template>
30+
<Primitive
31+
:as="as"
32+
:as-child="asChild"
33+
type="button"
34+
v-bind="{
35+
...attrs,
36+
onClick,
37+
}"
38+
>
39+
<slot />
40+
</Primitive>
41+
</template>

0 commit comments

Comments
 (0)