Skip to content

Commit 03c5524

Browse files
authored
feat(hovercard): DP-175404 expose functions to control hide/show (#1113)
1 parent 311ee69 commit 03c5524

File tree

5 files changed

+212
-0
lines changed

5 files changed

+212
-0
lines changed

packages/dialtone-vue/.storybook/main.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ const config = {
3434
css: {
3535
devSourcemap: true,
3636
},
37+
optimizeDeps: {
38+
include: ['react-dom/client'],
39+
},
3740
});
3841
},
3942

packages/dialtone-vue/components/hovercard/hovercard.stories.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import DtHovercard from './hovercard.vue';
33
import DtHovercardDefaultTemplate from './hovercard_default.story.vue';
44
import DtHovercardManyTemplate from './hovercard_many.story.vue';
55
import DtHovercardWithInputTemplate from './hovercard_with_input.story.vue';
6+
import DtHovercardExternalAnchorTemplate from './hovercard_external_anchor.story.vue';
67
import { createTemplateFromVueFile } from '@/common/storybook_utils';
78
import { action } from 'storybook/actions';
89
import {
@@ -212,3 +213,30 @@ export const WithInput = {
212213
})],
213214
args: { ...Default.args, offset: [0, 5] },
214215
};
216+
217+
const ExternalAnchorTemplate = (args, { argTypes }) => createTemplateFromVueFile(
218+
args,
219+
argTypes,
220+
DtHovercardExternalAnchorTemplate,
221+
);
222+
export const ExternalAnchor = {
223+
render: ExternalAnchorTemplate,
224+
decorators: [() => ({
225+
template: `<dt-stack direction="row" justify="center" align="center" class="d-h464">
226+
<div class="d-w332">
227+
<story />
228+
</div>
229+
</dt-stack>`,
230+
})],
231+
args: { ...Default.args },
232+
parameters: {
233+
docs: {
234+
description: {
235+
story: 'Demonstrates using <code>externalAnchorElement</code> with the exposed <code>show()</code>/<code>hide()</code> API. ' +
236+
'This pattern is used when the anchor lives outside the hovercard\'s DOM scope (e.g. inside a Shadow DOM), ' +
237+
'so the hovercard cannot detect hover events automatically. ' +
238+
'The parent listens for hover events and calls <code>show()</code>/<code>hide()</code> directly on the hovercard ref.',
239+
},
240+
},
241+
},
242+
};

packages/dialtone-vue/components/hovercard/hovercard.test.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,58 @@ describe('DtHovercard Tests', () => {
160160
});
161161
});
162162

163+
describe('With externalAnchorElement — show/hide API Tests', () => {
164+
let externalAnchorWrapper;
165+
166+
beforeEach(() => {
167+
vi.useFakeTimers();
168+
169+
const externalEl = document.createElement('span');
170+
document.body.appendChild(externalEl);
171+
172+
externalAnchorWrapper = mount(DtHovercard, {
173+
props: {
174+
...baseProps,
175+
id: 'hovercard-external',
176+
externalAnchorElement: externalEl,
177+
},
178+
slots: { content: MOCK_DEFAULT_SLOT_MESSAGE },
179+
global: { stubs: { transition: false } },
180+
attachTo: document.body,
181+
});
182+
});
183+
184+
afterEach(() => {
185+
externalAnchorWrapper.unmount();
186+
});
187+
188+
describe('When show() is called', () => {
189+
beforeEach(() => {
190+
externalAnchorWrapper.vm.show();
191+
vi.runAllTimers();
192+
});
193+
194+
it('should show the hovercard after the enter delay', () => {
195+
content = getHovercardContent();
196+
expect(content.textContent).toBe(MOCK_DEFAULT_SLOT_MESSAGE);
197+
});
198+
});
199+
200+
describe('When hide() is called after show()', () => {
201+
beforeEach(() => {
202+
externalAnchorWrapper.vm.show();
203+
vi.runAllTimers();
204+
externalAnchorWrapper.vm.hide();
205+
vi.runAllTimers();
206+
});
207+
208+
it('should hide the hovercard after the leave delay', () => {
209+
content = getHovercardContent();
210+
expect(content).toBeNull();
211+
});
212+
});
213+
});
214+
163215
describe('Accessibility Tests', () => {
164216
describe('When hovercard is open', () => {
165217
beforeEach(async () => {

packages/dialtone-vue/components/hovercard/hovercard.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,8 @@ watch(() => props.open, (open) => {
262262
hovercardOpen.value = open;
263263
}, { immediate: true });
264264
265+
defineExpose({ show: onMouseEnter, hide: onMouseLeave });
266+
265267
function setInTimer () {
266268
if (props.open === null) {
267269
clearTimeout(outTimer.value);
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
<!-- eslint-disable vuejs-accessibility/no-static-element-interactions -->
2+
<!-- eslint-disable vuejs-accessibility/mouse-events-have-key-events -->
3+
<template>
4+
<p class="d-body--md">
5+
Send a message to
6+
<span
7+
ref="mentionEl"
8+
class="d-fw-bold d-fc-primary d-c-pointer"
9+
@mouseenter="onMentionEnter"
10+
@mouseleave="onMentionLeave"
11+
>@Jaqueline Nackos</span>
12+
to get started.
13+
</p>
14+
15+
<dt-hovercard
16+
:id="$attrs.id"
17+
ref="hovercard"
18+
:placement="$attrs.placement"
19+
:content-class="$attrs.contentClass"
20+
:fallback-placements="$attrs.fallbackPlacements"
21+
:padding="$attrs.padding"
22+
:transition="$attrs.transition"
23+
:offset="$attrs.offset"
24+
:header-class="$attrs.headerClass"
25+
:footer-class="$attrs.footerClass"
26+
:append-to="$attrs.appendTo"
27+
:enter-delay="$attrs.enterDelay"
28+
:leave-delay="$attrs.leaveDelay"
29+
:external-anchor-element="mentionElement"
30+
@opened="$attrs.onOpened"
31+
>
32+
<template #content>
33+
<dt-stack
34+
direction="column"
35+
gap="400"
36+
>
37+
<dt-stack
38+
direction="row"
39+
gap="300"
40+
>
41+
<dt-avatar
42+
full-name="Jaqueline Nackos"
43+
:image-src="defaultImage"
44+
image-alt="Person avatar"
45+
seed="JN"
46+
size="md"
47+
presence="busy"
48+
/>
49+
<dt-stack
50+
direction="column"
51+
gap="0"
52+
>
53+
<p class="d-headline--md-compact">
54+
Jaqueline Nackos
55+
</p>
56+
<p class="d-body--sm d-fc-tertiary">
57+
<span class="d-fc-critical">In a meeting</span> • Working from SF
58+
</p>
59+
</dt-stack>
60+
</dt-stack>
61+
<dt-stack
62+
direction="column"
63+
gap="300"
64+
class="d-fc-secondary"
65+
>
66+
<p>
67+
Vice President of Sales Enablement Aerolabs
68+
</p>
69+
<dt-stack
70+
direction="row"
71+
gap="300"
72+
>
73+
<dt-icon
74+
name="clock-4"
75+
size="200"
76+
/>
77+
<p class="d-body--sm">
78+
<b>Local Time:</b> 12:32 PM
79+
</p>
80+
</dt-stack>
81+
</dt-stack>
82+
<dt-stack
83+
direction="row"
84+
gap="400"
85+
>
86+
<dt-button
87+
width="var(--dt-size-100-percent)"
88+
importance="outlined"
89+
kind="muted"
90+
>
91+
Call
92+
</dt-button>
93+
<dt-button
94+
width="var(--dt-size-100-percent)"
95+
importance="outlined"
96+
kind="muted"
97+
>
98+
Message
99+
</dt-button>
100+
</dt-stack>
101+
</dt-stack>
102+
</template>
103+
</dt-hovercard>
104+
</template>
105+
106+
<script setup>
107+
import { ref } from 'vue';
108+
import DtHovercard from './hovercard.vue';
109+
import defaultImage from '@/common/assets/avatar2.png';
110+
import DtStack from '../stack/stack.vue';
111+
import DtIcon from '../icon/icon.vue';
112+
import DtButton from '../button/button.vue';
113+
import DtAvatar from '../avatar/avatar.vue';
114+
115+
const hovercard = ref(null);
116+
const mentionEl = ref(null);
117+
const mentionElement = ref(null);
118+
119+
function onMentionEnter () {
120+
mentionElement.value = mentionEl.value;
121+
hovercard.value?.show();
122+
}
123+
124+
function onMentionLeave () {
125+
hovercard.value?.hide();
126+
}
127+
</script>

0 commit comments

Comments
 (0)