feat: animate widget resize
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user