Skip to content

Commit c0e5c30

Browse files
committed
feat( headless): add component p-popover
1 parent 9d1d87c commit c0e5c30

File tree

11 files changed

+969
-4
lines changed

11 files changed

+969
-4
lines changed

.nvmrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
20
1+
18

src/.vitepress/config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,10 @@ export default defineConfig({
396396
text: 'Dropzone',
397397
link: '/components/dropzone/',
398398
},
399+
{
400+
text: 'Popover',
401+
link: '/components/popover/',
402+
},
399403
{
400404
text: 'Spread',
401405
link: '/components/spread/',

src/.vitepress/theme/components/NuxtLink.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727

2828
<script lang="ts">
2929
import type { PropType } from 'vue-demi'
30+
import { hasProtocol } from 'ufo'
3031
import {
3132
computed,
3233
defineComponent,
@@ -50,7 +51,7 @@ export default defineComponent({
5051
setup (props) {
5152
const isExternalLink = computed(() => {
5253
return typeof props.href === 'string'
53-
&& (props.href.startsWith('http') || props.href.startsWith('#'))
54+
&& (hasProtocol(props.href) || props.href.startsWith('#'))
5455
})
5556
5657
return { isExternalLink }
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { fireEvent, render } from '@testing-library/vue'
2+
import pPopover from './Popover.vue'
3+
4+
it('should able to show tooltip over activator element', async () => {
5+
const screen = render({
6+
components: { pPopover },
7+
template : `
8+
<p-popover>
9+
<template #activator="{ toggle }">
10+
<a href="javascript:void" data-testid="activator" @click="toggle">
11+
Remove?
12+
</a>
13+
</template>
14+
<template #default>
15+
<div class="flex items-center space-x-2">
16+
<span>Are you sure?</span>
17+
<p-button data-testid="confirm" @click="popoverPromise.finish(true)">
18+
Yes
19+
</p-button>
20+
<p-button data-testid="cancel" @click="popoverPromise.finish(false)">
21+
Cancel
22+
</p-button>
23+
</div>
24+
</template>
25+
</p-popover>
26+
`,
27+
setup () {
28+
return {}
29+
},
30+
})
31+
32+
const popover = screen.queryByTestId('popover')
33+
const tooltip = screen.queryByTestId('popover-tooltip')
34+
const activator = screen.queryByTestId('activator')
35+
36+
expect(popover).toBeInTheDocument()
37+
expect(tooltip).not.toBeVisible()
38+
39+
await fireEvent.click(activator)
40+
41+
expect(tooltip).toBeVisible()
42+
43+
await fireEvent.click(activator)
44+
45+
expect(tooltip).not.toBeVisible()
46+
})

src/components/popover/Popover.vue

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
<template>
2+
<div
3+
ref="root"
4+
data-testid="popover"
5+
class="popover">
6+
<slot
7+
name="activator"
8+
:is-show="isShow"
9+
:toggle="toggle"
10+
:show="show"
11+
:hide="hide" />
12+
13+
<p-tooltip
14+
ref="tooltip"
15+
v-model="isShow"
16+
class="popover__tooltip"
17+
data-testid="popover-tooltip"
18+
:color="color"
19+
:placement="placement"
20+
:target="root"
21+
@show="$emit('show')"
22+
@hide="$emit('hide')">
23+
<slot
24+
:is-show="isShow"
25+
:toggle="toggle"
26+
:show="show"
27+
:hide="hide" />
28+
</p-tooltip>
29+
</div>
30+
</template>
31+
32+
<script lang="ts" setup>
33+
import type { PropType, VNode } from 'vue-demi'
34+
import type { Placement } from '@floating-ui/dom'
35+
import type { ColorVariant } from '../tooltip'
36+
import {
37+
ref,
38+
provide,
39+
computed,
40+
} from 'vue-demi'
41+
import pTooltip from '../tooltip/Tooltip.vue'
42+
import { useVModel } from '../input'
43+
import { onClickOutside } from '@vueuse/core'
44+
import { POPOVER_CONTEXT } from '.'
45+
46+
const props = defineProps({
47+
modelValue: {
48+
type : Boolean,
49+
default: false,
50+
},
51+
color: {
52+
type : String as PropType<ColorVariant>,
53+
default: 'white',
54+
},
55+
placement: {
56+
type : String as PropType<Placement>,
57+
default: 'top',
58+
},
59+
disabled: {
60+
type : Boolean,
61+
default: false,
62+
},
63+
})
64+
65+
const root = ref<HTMLDivElement>()
66+
const tooltip = ref<InstanceType<typeof pTooltip>>()
67+
const isShow = useVModel(props)
68+
69+
const tooltipEl = computed<HTMLElement>(() => tooltip.value.$el)
70+
71+
onClickOutside(tooltipEl, () => {
72+
if (isShow.value) {
73+
// Add little delay too prevent race condition with v-model changing
74+
setTimeout(() => {
75+
hide()
76+
})
77+
}
78+
}, { ignore: [root] })
79+
80+
function toggle () {
81+
if (!props.disabled) {
82+
if (isShow.value)
83+
hide()
84+
else
85+
show()
86+
}
87+
}
88+
89+
function show () {
90+
if (!props.disabled)
91+
isShow.value = true
92+
}
93+
94+
function hide () {
95+
if (!props.disabled)
96+
isShow.value = false
97+
}
98+
99+
defineEmits<{
100+
'show': [],
101+
'hide': [],
102+
'update:modelValue': [boolean],
103+
}>()
104+
105+
defineSlots<{
106+
'activator'(props: { isShow: boolean, show: () => void, hide: () => void, toggle: () => void }): VNode[],
107+
'default'(props: { isShow: boolean, show: () => void, hide: () => void, toggle: () => void }): VNode[],
108+
}>()
109+
110+
defineExpose({
111+
isShow,
112+
show,
113+
hide,
114+
toggle,
115+
})
116+
117+
provide(POPOVER_CONTEXT, {
118+
isShow,
119+
show,
120+
hide,
121+
toggle,
122+
})
123+
</script>
124+
125+
<style lang="postcss">
126+
.popover {
127+
& > &__tooltip.tooltip {
128+
@apply text-base;
129+
}
130+
}
131+
</style>

0 commit comments

Comments
 (0)