feat: animate widget resize

This commit is contained in:
Shivam Mishra
2026-06-10 15:47:19 +05:30
parent 63a14f7d06
commit c5b13c8d45
4 changed files with 58 additions and 14 deletions
+4 -3
View File
@@ -12,8 +12,9 @@ export const SDK_CSS = `
overflow: hidden !important;
position: fixed !important;
transition: opacity 0.2s linear, transform 0.25s linear,
width 0.3s cubic-bezier(0.4, 0, 0.2, 1),
height 0.3s cubic-bezier(0.4, 0, 0.2, 1);
width 0.2s cubic-bezier(0.4, 0, 0.2, 1),
height 0.2s cubic-bezier(0.4, 0, 0.2, 1),
max-height 0.2s cubic-bezier(0.4, 0, 0.2, 1);
z-index: 2147483000 !important;
}
@@ -293,7 +294,7 @@ export const SDK_CSS = `
.woot-widget-holder.has-article-view {
width: min(640px, max(0px, -20px + 100dvw)) !important;
height: calc(100% - 125px) !important;
max-height: none !important;
max-height: calc(100% - 125px) !important;
}
}
+25 -3
View File
@@ -16,7 +16,12 @@ import {
ON_AGENT_MESSAGE_RECEIVED,
ON_CAMPAIGN_MESSAGE_CLICK,
ON_UNREAD_MESSAGE_CLICK,
ON_ARTICLE_VIEW_RESIZING,
} from './constants/widgetBusEvents';
// Keep in sync with the widget holder width/height transition in sdk.js. The
// article view is masked for this long so the iframe can reflow off-screen.
const ARTICLE_VIEW_RESIZE_DURATION = 200;
import { useDarkMode } from 'widget/composables/useDarkMode';
import { useRouter } from 'vue-router';
import { useAvailability } from 'widget/composables/useAvailability';
@@ -44,6 +49,7 @@ export default {
return {
isMobile: false,
campaignsSnoozedTill: undefined,
isArticleView: false,
};
},
computed: {
@@ -87,6 +93,7 @@ export default {
// Here we just collapse when leaving the article view, since the iframe is
// torn down then and can no longer signal.
if (this.isIFrame && previousRouteName === 'article-viewer') {
this.isArticleView = false;
IFrameHelper.sendMessage({ event: 'collapseWidget' });
}
},
@@ -193,6 +200,23 @@ export default {
query: { link, v: Date.now() },
});
},
setArticleView(isArticle) {
// Only resize when the page type actually changes, so navigating between
// two articles (or two listing pages) doesn't trigger a needless mask.
if (!this.isIFrame || isArticle === this.isArticleView) return;
this.isArticleView = isArticle;
// Mask the iframe while the widget resizes so its text reflow happens
// off-screen, then reveal it once the size transition has settled.
emitter.emit(ON_ARTICLE_VIEW_RESIZING, true);
IFrameHelper.sendMessage({
event: isArticle ? 'expandWidget' : 'collapseWidget',
});
setTimeout(
() => emitter.emit(ON_ARTICLE_VIEW_RESIZING, false),
ARTICLE_VIEW_RESIZE_DURATION
);
},
registerUnreadEvents() {
emitter.on(ON_AGENT_MESSAGE_RECEIVED, () => {
const { name: routeName } = this.$route;
@@ -352,9 +376,7 @@ export default {
} else if (message.event === 'open-article') {
this.openArticle(message.slug);
} else if (message.event === 'portalPageLoaded') {
IFrameHelper.sendMessage({
event: message.isArticle ? 'expandWidget' : 'collapseWidget',
});
this.setArticleView(message.isArticle);
} else if (message.event === 'toggle-open') {
this.$store.dispatch('appConfig/toggleWidgetOpen', message.isOpen);
@@ -2,3 +2,4 @@ export const ON_AGENT_MESSAGE_RECEIVED = 'ON_AGENT_MESSAGE_RECEIVED';
export const ON_UNREAD_MESSAGE_CLICK = 'ON_UNREAD_MESSAGE_CLICK';
export const ON_CAMPAIGN_MESSAGE_CLICK = 'ON_CAMPAIGN_MESSAGE_CLICK';
export const ON_CONVERSATION_CREATED = 'ON_CONVERSATION_CREATED';
export const ON_ARTICLE_VIEW_RESIZING = 'ON_ARTICLE_VIEW_RESIZING';
+28 -8
View File
@@ -1,21 +1,41 @@
<script>
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import { useRoute } from 'vue-router';
import { emitter } from 'shared/helpers/mitt';
import { ON_ARTICLE_VIEW_RESIZING } from 'widget/constants/widgetBusEvents';
import IframeLoader from 'shared/components/IframeLoader.vue';
export default {
name: 'ArticleViewer',
components: {
IframeLoader,
},
const route = useRoute();
// Masks the article while the widget resizes (see App.vue#setArticleView) so the
// iframe's text reflow happens off-screen instead of shifting in front of the user.
const isResizing = ref(false);
const setResizing = value => {
isResizing.value = value;
};
onMounted(() => emitter.on(ON_ARTICLE_VIEW_RESIZING, setResizing));
onBeforeUnmount(() => emitter.off(ON_ARTICLE_VIEW_RESIZING, setResizing));
</script>
<template>
<div class="bg-white h-full">
<div class="bg-white dark:bg-slate-900 h-full relative">
<!--
Key by fullPath (not just the link) so the iframe remounts on every
navigation here, including re-opening the same article via the SDK after
the iframe was browsed to another help-center page. See App.vue#openArticle.
-->
<IframeLoader :key="$route.fullPath" :url="$route.query.link" />
<IframeLoader :key="route.fullPath" :url="route.query.link" />
<!--
Cover the article instantly while the widget resizes, then fade it out once
the size transition settles. The asymmetric class (no transition on the way
in, transition on the way out) keeps the cover from revealing the reflow.
-->
<div
class="absolute inset-0 bg-white dark:bg-slate-900 pointer-events-none"
:class="
isResizing ? 'opacity-100' : 'opacity-0 transition-opacity duration-200'
"
/>
</div>
</template>