Fix composer scrollbar resize loop (#1050)
This commit is contained in:
@@ -98,13 +98,19 @@ export function useComposerHeightMirror({
|
|||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
measure();
|
measure();
|
||||||
}, [value, measure]);
|
}, [maxHeight, minHeight, value, measure]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const source = textareaRef.current;
|
const source = textareaRef.current;
|
||||||
if (!source || !(source instanceof HTMLElement)) return;
|
if (!source || !(source instanceof HTMLElement)) return;
|
||||||
if (typeof ResizeObserver === "undefined") return;
|
if (typeof ResizeObserver === "undefined") return;
|
||||||
const observer = new ResizeObserver(() => measure());
|
let previousWidth = source.clientWidth;
|
||||||
|
const observer = new ResizeObserver(() => {
|
||||||
|
const nextWidth = source.clientWidth;
|
||||||
|
if (Math.abs(nextWidth - previousWidth) < 1) return;
|
||||||
|
previousWidth = nextWidth;
|
||||||
|
measure();
|
||||||
|
});
|
||||||
observer.observe(source);
|
observer.observe(source);
|
||||||
return () => observer.disconnect();
|
return () => observer.disconnect();
|
||||||
}, [textareaRef, measure]);
|
}, [textareaRef, measure]);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
|
useWindowDimensions,
|
||||||
NativeSyntheticEvent,
|
NativeSyntheticEvent,
|
||||||
TextInputContentSizeChangeEventData,
|
TextInputContentSizeChangeEventData,
|
||||||
TextInputKeyPressEventData,
|
TextInputKeyPressEventData,
|
||||||
@@ -144,7 +145,8 @@ export interface MessageInputRef {
|
|||||||
|
|
||||||
const MIN_INPUT_HEIGHT_MOBILE = 30;
|
const MIN_INPUT_HEIGHT_MOBILE = 30;
|
||||||
const MIN_INPUT_HEIGHT_DESKTOP = 46;
|
const MIN_INPUT_HEIGHT_DESKTOP = 46;
|
||||||
const MAX_INPUT_HEIGHT = 160;
|
const DEFAULT_MAX_INPUT_HEIGHT = 160;
|
||||||
|
const MAX_INPUT_VIEWPORT_RATIO = 0.5;
|
||||||
const MIN_INPUT_HEIGHT = isWeb ? MIN_INPUT_HEIGHT_DESKTOP : MIN_INPUT_HEIGHT_MOBILE;
|
const MIN_INPUT_HEIGHT = isWeb ? MIN_INPUT_HEIGHT_DESKTOP : MIN_INPUT_HEIGHT_MOBILE;
|
||||||
|
|
||||||
type WebTextInputKeyPressEvent = NativeSyntheticEvent<
|
type WebTextInputKeyPressEvent = NativeSyntheticEvent<
|
||||||
@@ -983,17 +985,22 @@ function computeIsDictationStartEnabled(
|
|||||||
return (isReadyForDictation ?? isConnected) && !disabled;
|
return (isReadyForDictation ?? isConnected) && !disabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
function computeTextInputHeightStyle(inputHeight: number) {
|
function resolveMaxInputHeight(windowHeight: number): number {
|
||||||
|
if (!Number.isFinite(windowHeight) || windowHeight <= 0) return DEFAULT_MAX_INPUT_HEIGHT;
|
||||||
|
return Math.max(DEFAULT_MAX_INPUT_HEIGHT, Math.floor(windowHeight * MAX_INPUT_VIEWPORT_RATIO));
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeTextInputHeightStyle(inputHeight: number, maxInputHeight: number) {
|
||||||
if (isWeb) {
|
if (isWeb) {
|
||||||
return {
|
return {
|
||||||
height: inputHeight,
|
height: inputHeight,
|
||||||
minHeight: MIN_INPUT_HEIGHT,
|
minHeight: MIN_INPUT_HEIGHT,
|
||||||
maxHeight: MAX_INPUT_HEIGHT,
|
maxHeight: maxInputHeight,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
minHeight: MIN_INPUT_HEIGHT,
|
minHeight: MIN_INPUT_HEIGHT,
|
||||||
maxHeight: MAX_INPUT_HEIGHT,
|
maxHeight: maxInputHeight,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1191,6 +1198,8 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
|
|||||||
attachmentSlot,
|
attachmentSlot,
|
||||||
} = resolveMessageInputProps(props);
|
} = resolveMessageInputProps(props);
|
||||||
const isCompact = useIsCompactFormFactor();
|
const isCompact = useIsCompactFormFactor();
|
||||||
|
const { height: windowHeight } = useWindowDimensions();
|
||||||
|
const maxInputHeight = resolveMaxInputHeight(windowHeight);
|
||||||
const buttonIconSize = isWeb ? ICON_SIZE.md : ICON_SIZE.lg;
|
const buttonIconSize = isWeb ? ICON_SIZE.md : ICON_SIZE.lg;
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const voice = useVoiceOptional();
|
const voice = useVoiceOptional();
|
||||||
@@ -1522,7 +1531,7 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
|
|||||||
}, [getWebTextArea]);
|
}, [getWebTextArea]);
|
||||||
|
|
||||||
const inputScrollbar = useWebElementScrollbar(webTextareaRef, {
|
const inputScrollbar = useWebElementScrollbar(webTextareaRef, {
|
||||||
enabled: isWeb && inputHeight >= MAX_INPUT_HEIGHT,
|
enabled: isWeb,
|
||||||
});
|
});
|
||||||
|
|
||||||
usePasteImagesEffect({
|
usePasteImagesEffect({
|
||||||
@@ -1536,20 +1545,24 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
|
|||||||
|
|
||||||
const setBoundedInputHeight = useCallback(
|
const setBoundedInputHeight = useCallback(
|
||||||
(nextHeight: number) => {
|
(nextHeight: number) => {
|
||||||
const bounded = Math.max(MIN_INPUT_HEIGHT, Math.min(MAX_INPUT_HEIGHT, nextHeight));
|
const bounded = Math.max(MIN_INPUT_HEIGHT, Math.min(maxInputHeight, nextHeight));
|
||||||
if (Math.abs(inputHeightRef.current - bounded) < 1) return;
|
if (Math.abs(inputHeightRef.current - bounded) < 1) return;
|
||||||
inputHeightRef.current = bounded;
|
inputHeightRef.current = bounded;
|
||||||
setInputHeight(bounded);
|
setInputHeight(bounded);
|
||||||
onHeightChange?.(bounded);
|
onHeightChange?.(bounded);
|
||||||
},
|
},
|
||||||
[onHeightChange],
|
[maxInputHeight, onHeightChange],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setBoundedInputHeight(inputHeightRef.current);
|
||||||
|
}, [setBoundedInputHeight]);
|
||||||
|
|
||||||
useComposerHeightMirror({
|
useComposerHeightMirror({
|
||||||
value,
|
value,
|
||||||
textareaRef: webTextareaRef,
|
textareaRef: webTextareaRef,
|
||||||
minHeight: MIN_INPUT_HEIGHT,
|
minHeight: MIN_INPUT_HEIGHT,
|
||||||
maxHeight: MAX_INPUT_HEIGHT,
|
maxHeight: maxInputHeight,
|
||||||
onHeight: setBoundedInputHeight,
|
onHeight: setBoundedInputHeight,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1674,8 +1687,8 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
|
|||||||
[inputWrapperStyle, inputAnimatedStyle],
|
[inputWrapperStyle, inputAnimatedStyle],
|
||||||
);
|
);
|
||||||
const textInputStyle = useMemo(
|
const textInputStyle = useMemo(
|
||||||
() => [styles.textInput, computeTextInputHeightStyle(inputHeight)],
|
() => [styles.textInput, computeTextInputHeightStyle(inputHeight, maxInputHeight)],
|
||||||
[inputHeight],
|
[inputHeight, maxInputHeight],
|
||||||
);
|
);
|
||||||
const sendButtonCombinedStyle = useMemo(
|
const sendButtonCombinedStyle = useMemo(
|
||||||
() => [styles.sendButton, isSendButtonDisabled && styles.buttonDisabled],
|
() => [styles.sendButton, isSendButtonDisabled && styles.buttonDisabled],
|
||||||
@@ -1727,7 +1740,7 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
|
|||||||
onBlur={handleInputBlur}
|
onBlur={handleInputBlur}
|
||||||
style={textInputStyle}
|
style={textInputStyle}
|
||||||
multiline
|
multiline
|
||||||
scrollEnabled={isWeb ? inputHeight >= MAX_INPUT_HEIGHT : true}
|
scrollEnabled={isWeb ? inputHeight >= maxInputHeight : true}
|
||||||
onContentSizeChange={handleContentSizeChange}
|
onContentSizeChange={handleContentSizeChange}
|
||||||
editable={!isDictating && !isRealtimeVoiceForCurrentAgent && !disabled}
|
editable={!isDictating && !isRealtimeVoiceForCurrentAgent && !disabled}
|
||||||
onKeyPress={shouldHandleWebKeyPress ? handleDesktopKeyPress : undefined}
|
onKeyPress={shouldHandleWebKeyPress ? handleDesktopKeyPress : undefined}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useEffect, useState, type ReactNode, type RefObject } from "react";
|
import { useCallback, useLayoutEffect, useState, type ReactNode, type RefObject } from "react";
|
||||||
import {
|
import {
|
||||||
type FlatList,
|
type FlatList,
|
||||||
type LayoutChangeEvent,
|
type LayoutChangeEvent,
|
||||||
@@ -21,8 +21,25 @@ function ensureHideScrollbarStyle(): void {
|
|||||||
if (document.getElementById(HIDE_SCROLLBAR_STYLE_ID)) return;
|
if (document.getElementById(HIDE_SCROLLBAR_STYLE_ID)) return;
|
||||||
const style = document.createElement("style");
|
const style = document.createElement("style");
|
||||||
style.id = HIDE_SCROLLBAR_STYLE_ID;
|
style.id = HIDE_SCROLLBAR_STYLE_ID;
|
||||||
style.textContent =
|
style.textContent = `
|
||||||
"[data-hide-scrollbar]::-webkit-scrollbar { display: none; width: 0; height: 0; }";
|
[data-hide-scrollbar] {
|
||||||
|
scrollbar-width: none;
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
scrollbar-gutter: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-hide-scrollbar]::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-hide-scrollbar]::-webkit-scrollbar-button {
|
||||||
|
display: none;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
`;
|
||||||
document.head.appendChild(style);
|
document.head.appendChild(style);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,18 +71,25 @@ export function useWebElementScrollbar(
|
|||||||
contentSize: 0,
|
contentSize: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (!enabled) return;
|
if (!enabled) return;
|
||||||
const element = elementRef.current;
|
const element = elementRef.current;
|
||||||
if (!element) return;
|
if (!element) return;
|
||||||
|
|
||||||
|
type ScrollbarStyle = CSSStyleDeclaration & {
|
||||||
|
scrollbarWidth: string;
|
||||||
|
msOverflowStyle: string;
|
||||||
|
scrollbarGutter: string;
|
||||||
|
};
|
||||||
|
const style = element.style as ScrollbarStyle;
|
||||||
|
const previousScrollbarWidth = style.scrollbarWidth;
|
||||||
|
const previousMsOverflowStyle = style.msOverflowStyle;
|
||||||
|
const previousScrollbarGutter = style.scrollbarGutter;
|
||||||
|
|
||||||
element.setAttribute("data-hide-scrollbar", "");
|
element.setAttribute("data-hide-scrollbar", "");
|
||||||
(
|
style.scrollbarWidth = "none";
|
||||||
element.style as CSSStyleDeclaration & { scrollbarWidth: string; msOverflowStyle: string }
|
style.msOverflowStyle = "none";
|
||||||
).scrollbarWidth = "none";
|
style.scrollbarGutter = "auto";
|
||||||
(
|
|
||||||
element.style as CSSStyleDeclaration & { scrollbarWidth: string; msOverflowStyle: string }
|
|
||||||
).msOverflowStyle = "none";
|
|
||||||
ensureHideScrollbarStyle();
|
ensureHideScrollbarStyle();
|
||||||
|
|
||||||
function update() {
|
function update() {
|
||||||
@@ -94,12 +118,9 @@ export function useWebElementScrollbar(
|
|||||||
element.removeEventListener("scroll", update);
|
element.removeEventListener("scroll", update);
|
||||||
resizeObserver.disconnect();
|
resizeObserver.disconnect();
|
||||||
element.removeAttribute("data-hide-scrollbar");
|
element.removeAttribute("data-hide-scrollbar");
|
||||||
(
|
style.scrollbarWidth = previousScrollbarWidth;
|
||||||
element.style as CSSStyleDeclaration & { scrollbarWidth: string; msOverflowStyle: string }
|
style.msOverflowStyle = previousMsOverflowStyle;
|
||||||
).scrollbarWidth = "";
|
style.scrollbarGutter = previousScrollbarGutter;
|
||||||
(
|
|
||||||
element.style as CSSStyleDeclaration & { scrollbarWidth: string; msOverflowStyle: string }
|
|
||||||
).msOverflowStyle = "";
|
|
||||||
};
|
};
|
||||||
}, [contentRef, elementRef, enabled]);
|
}, [contentRef, elementRef, enabled]);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user