2
0

🚸 Improve auto scroll behavior

Only trigger when the last element is entirely visible
This commit is contained in:
Baptiste Arnaud
2024-04-18 12:11:28 +02:00
parent 3ca1a2f0d6
commit 5aad10e937
13 changed files with 44 additions and 40 deletions

View File

@ -1,6 +1,6 @@
{
"name": "@typebot.io/js",
"version": "0.2.75",
"version": "0.2.76",
"description": "Javascript library to display typebots on your website",
"type": "module",
"main": "dist/index.js",

View File

@ -22,7 +22,7 @@ type Props = Pick<ContinueChatResponse, 'messages' | 'input'> & {
streamingMessageId: ChatChunkType['streamingMessageId']
isTransitionDisabled?: boolean
onNewBubbleDisplayed: (blockId: string) => Promise<void>
onScrollToBottom: (top?: number) => void
onScrollToBottom: (ref?: HTMLDivElement, offset?: number) => void
onSubmit: (input?: string) => void
onSkip: () => void
onAllBubblesDisplayed: () => void
@ -33,19 +33,17 @@ export const ChatChunk = (props: Props) => {
const [displayedMessageIndex, setDisplayedMessageIndex] = createSignal(
props.isTransitionDisabled ? props.messages.length : 0
)
const [lastBubbleOffsetTop, setLastBubbleOffsetTop] = createSignal<number>()
const [lastBubble, setLastBubble] = createSignal<HTMLDivElement>()
onMount(() => {
if (props.streamingMessageId) return
if (props.messages.length === 0) {
props.onAllBubblesDisplayed()
}
props.onScrollToBottom(
inputRef?.offsetTop ? inputRef?.offsetTop - 50 : undefined
)
props.onScrollToBottom(inputRef, 50)
})
const displayNextMessage = async (bubbleOffsetTop?: number) => {
const displayNextMessage = async (bubbleRef?: HTMLDivElement) => {
if (
(props.settings.typingEmulation?.delayBetweenBubbles ??
defaultSettings.typingEmulation.delayBetweenBubbles) > 0 &&
@ -66,9 +64,9 @@ export const ChatChunk = (props: Props) => {
? displayedMessageIndex()
: displayedMessageIndex() + 1
)
props.onScrollToBottom(bubbleOffsetTop)
props.onScrollToBottom(bubbleRef)
if (displayedMessageIndex() === props.messages.length) {
setLastBubbleOffsetTop(bubbleOffsetTop)
setLastBubble(bubbleRef)
props.onAllBubblesDisplayed()
}
}
@ -143,7 +141,7 @@ export const ChatChunk = (props: Props) => {
defaultSettings.general.isInputPrefillEnabled
}
hasError={props.hasError}
onTransitionEnd={() => props.onScrollToBottom(lastBubbleOffsetTop())}
onTransitionEnd={() => props.onScrollToBottom(lastBubble())}
onSubmit={props.onSubmit}
onSkip={props.onSkip}
/>

View File

@ -224,14 +224,27 @@ export const ConversationContainer = (props: Props) => {
])
}
const autoScrollToBottom = (offsetTop?: number) => {
const chunks = chatChunks()
const lastChunkWasStreaming =
chunks.length >= 2 && chunks[chunks.length - 2].streamingMessageId
if (lastChunkWasStreaming) return
setTimeout(() => {
chatContainer?.scrollTo(0, offsetTop ?? chatContainer.scrollHeight)
}, 50)
const autoScrollToBottom = (lastElement?: HTMLDivElement, offset = 0) => {
if (!chatContainer) return
if (!lastElement) {
setTimeout(() => {
chatContainer?.scrollTo(0, chatContainer.scrollHeight)
}, 50)
return
}
const lastElementRect = lastElement.getBoundingClientRect()
const containerRect = chatContainer.getBoundingClientRect()
const lastElementTopRelative =
lastElementRect.top - containerRect.top + chatContainer.scrollTop
const isLastElementInVisibleArea =
lastElementTopRelative < chatContainer.scrollTop + containerRect.height &&
lastElementTopRelative + lastElementRect.height > chatContainer.scrollTop
if (isLastElementInVisibleArea)
setTimeout(() => {
chatContainer?.scrollTo(0, lastElement.offsetTop - offset)
}, 50)
}
const handleAllBubblesDisplayed = async () => {

View File

@ -21,7 +21,7 @@ type Props = {
message: ChatMessage
typingEmulation: Settings['typingEmulation']
isTypingSkipped: boolean
onTransitionEnd?: (offsetTop?: number) => void
onTransitionEnd?: (ref?: HTMLDivElement) => void
onCompleted: (reply?: string) => void
}

View File

@ -7,7 +7,7 @@ import clsx from 'clsx'
type Props = {
content: AudioBubbleBlock['content']
onTransitionEnd?: (offsetTop?: number) => void
onTransitionEnd?: (ref?: HTMLDivElement) => void
}
const showAnimationDuration = 400
@ -28,10 +28,7 @@ export const AudioBubble = (props: Props) => {
if (isPlayed) return
isPlayed = true
setIsTyping(false)
setTimeout(
() => props.onTransitionEnd?.(ref?.offsetTop),
showAnimationDuration
)
setTimeout(() => props.onTransitionEnd?.(ref), showAnimationDuration)
}, typingDuration)
})

View File

@ -8,7 +8,7 @@ import { botContainerHeight } from '@/utils/botContainerHeightSignal'
type Props = {
content: CustomEmbedBubbleProps['content']
onTransitionEnd?: (offsetTop?: number) => void
onTransitionEnd?: (ref?: HTMLDivElement) => void
onCompleted: (reply?: string) => void
}
@ -43,10 +43,7 @@ export const CustomEmbedBubble = (props: Props) => {
typingTimeout = setTimeout(() => {
setIsTyping(false)
setTimeout(
() => props.onTransitionEnd?.(ref?.offsetTop),
showAnimationDuration
)
setTimeout(() => props.onTransitionEnd?.(ref), showAnimationDuration)
}, 2000)
})

View File

@ -7,7 +7,7 @@ import { defaultEmbedBubbleContent } from '@typebot.io/schemas/features/blocks/b
type Props = {
content: EmbedBubbleBlock['content']
onTransitionEnd?: (offsetTop?: number) => void
onTransitionEnd?: (ref?: HTMLDivElement) => void
}
let typingTimeout: NodeJS.Timeout
@ -24,7 +24,7 @@ export const EmbedBubble = (props: Props) => {
typingTimeout = setTimeout(() => {
setIsTyping(false)
setTimeout(() => {
props.onTransitionEnd?.(ref?.offsetTop)
props.onTransitionEnd?.(ref)
}, showAnimationDuration)
}, 2000)
})

View File

@ -7,7 +7,7 @@ import { defaultImageBubbleContent } from '@typebot.io/schemas/features/blocks/b
type Props = {
content: ImageBubbleBlock['content']
onTransitionEnd?: (offsetTop?: number) => void
onTransitionEnd?: (ref?: HTMLDivElement) => void
}
export const showAnimationDuration = 400
@ -27,7 +27,7 @@ export const ImageBubble = (props: Props) => {
if (!isTyping()) return
setIsTyping(false)
setTimeout(() => {
props.onTransitionEnd?.(ref?.offsetTop)
props.onTransitionEnd?.(ref)
}, showAnimationDuration)
}

View File

@ -11,7 +11,7 @@ type Props = {
content: TextBubbleBlock['content']
typingEmulation: Settings['typingEmulation']
isTypingSkipped: boolean
onTransitionEnd?: (offsetTop?: number) => void
onTransitionEnd?: (ref?: HTMLDivElement) => void
}
export const showAnimationDuration = 400
@ -28,7 +28,7 @@ export const TextBubble = (props: Props) => {
if (!isTyping()) return
setIsTyping(false)
setTimeout(() => {
props.onTransitionEnd?.(ref?.offsetTop)
props.onTransitionEnd?.(ref)
}, showAnimationDuration)
}

View File

@ -15,7 +15,7 @@ import {
type Props = {
content: VideoBubbleBlock['content']
onTransitionEnd?: (offsetTop?: number) => void
onTransitionEnd?: (ref?: HTMLDivElement) => void
}
export const showAnimationDuration = 400
@ -39,7 +39,7 @@ export const VideoBubble = (props: Props) => {
if (!isTyping()) return
setIsTyping(false)
setTimeout(() => {
props.onTransitionEnd?.(ref?.offsetTop)
props.onTransitionEnd?.(ref)
}, showAnimationDuration)
}, typingDuration)
})

View File

@ -525,7 +525,6 @@ const setInputs = (
: '0'
)
console.log(hexToRgb(inputs?.border?.color ?? '').join(', '))
documentStyle.setProperty(
cssVariableNames.chat.inputs.borderColor,
hexToRgb(inputs?.border?.color ?? '').join(', ')

View File

@ -1,6 +1,6 @@
{
"name": "@typebot.io/nextjs",
"version": "0.2.75",
"version": "0.2.76",
"description": "Convenient library to display typebots on your Next.js website",
"main": "dist/index.js",
"types": "dist/index.d.ts",

View File

@ -1,6 +1,6 @@
{
"name": "@typebot.io/react",
"version": "0.2.75",
"version": "0.2.76",
"description": "Convenient library to display typebots on your React app",
"main": "dist/index.js",
"types": "dist/index.d.ts",