storytellurx-v1/app/hooks/use-field-sizing.ts
Elias Rhouzlane 91b337752a feat: enhance global background image and scrolling functionality
- Added a new filter to the GlobalBackgroundImageDisplay component for improved visual effects.
- Modified the PositionalScroller to update positions based on clicked elements, enhancing user interaction.
- Updated ScrollProgressTracker to allow scrolling effects to be applied to the body.
- Removed the deprecated use-field-sizing utility and replaced it with a more robust implementation.
- Improved persona styles for better layout and responsiveness in chat interfaces.
- Integrated a new ScrollArea component for better scroll handling in UI.
- Updated index and layout pages to reflect new components and improved structure.
- Added new dependencies for Radix UI components to enhance UI capabilities.
2025-06-30 19:42:05 +02:00

90 lines
3.3 KiB
TypeScript

// useFieldSizing.ts
import React, { useCallback, useLayoutEffect, useRef } from "react";
/**
* A type alias for the DOM elements that this hook can be applied to.
*/
type ResizableField = HTMLInputElement | HTMLTextAreaElement;
/**
* The return type of the useFieldSizing hook.
*/
interface UseFieldSizingReturn<T extends ResizableField> {
/**
* A React ref object to be attached to the target input or textarea element.
* If you passed an external ref to the hook, this will be the same ref.
*/
ref: React.RefObject<T | null>;
/**
* A function to manually trigger the resize logic. This is essential for
* non-controlled components where you might change the element's value
* programmatically (e.g., `ref.current.value = '...'`).
*/
triggerManuallyHandleResize: () => void;
}
/**
* A React hook that dynamically adjusts the height of an <input> or <textarea>
* to perfectly fit its content, mimicking the behavior of the CSS property
* `field-sizing: content`.
*
* It works seamlessly with both controlled and uncontrolled components.
*
* @param externalRef - An optional external ref to be used instead of the internal one.
* @returns An object containing the ref to attach to the DOM element and a function to manually trigger resizing.
*/
export function useFieldSizing<T extends ResizableField>(
externalRef?: React.RefObject<T>,
): UseFieldSizingReturn<T> {
const internalRef = useRef<T | null>(null);
const ref = externalRef || internalRef;
/**
* The core resizing logic.
* It resets the element's height to 'auto' to allow the browser to calculate
* the natural scrollHeight based on the content, then sets the height to that value.
* This is the standard and most reliable way to auto-size a textarea.
*/
const handleResize = useCallback(() => {
const element = ref.current;
if (element) {
// Temporarily reset height to let the browser calculate the new scrollHeight
element.style.height = "auto";
element.style.overflow = "hidden";
// Set the height to the calculated scrollHeight.
// This includes padding but not borders or margins, which is perfect.
element.style.height = !element.value.trim()
? "auto"
: `${Math.max(0, element.scrollHeight)}px`;
}
}, [ref]);
// We use useLayoutEffect to run the logic synchronously after DOM mutations
// but before the browser has painted. This prevents any flickering.
useLayoutEffect(() => {
const element = ref.current;
if (!element) {
return;
}
// The 'input' event is crucial for catching user-typed text, pastes, etc.
element.addEventListener("input", handleResize);
// Window resizing can cause text to reflow, requiring a height adjustment.
window.addEventListener("resize", handleResize);
// Perform an initial resize to set the correct height on mount,
// which is important for components with an initial `value` or `defaultValue`.
handleResize();
// Cleanup function to remove event listeners when the component unmounts.
return () => {
element.removeEventListener("input", handleResize);
window.removeEventListener("resize", handleResize);
};
}, [ref, handleResize]); // Rerun effect if the ref or callback changes
return {
ref,
triggerManuallyHandleResize: handleResize,
};
}