fix(search): adapt input shape and button position to content line count (#17529)

This commit is contained in:
chanx
2026-07-29 17:15:19 +08:00
committed by GitHub
parent c3aa8053c0
commit f8e01d558e
3 changed files with 70 additions and 17 deletions

View File

@@ -1,26 +1,67 @@
import { useLayoutEffect, type RefObject } from 'react';
import { useLayoutEffect, useState, type RefObject } from 'react';
/**
* Auto-resize a textarea to fit its content, clamped to a maximum height.
*
* useLayoutEffect runs synchronously after DOM mutation but before the
* browser paints, so the height adjustment never produces a visible flicker.
*
* Measurement also re-runs when the element's content-box width changes
* (e.g. responsive layout or font-metric changes): soft-wrapping can add or
* remove lines without `value` changing, which would otherwise leave
* `isMultiLine` stale and misposition controls. A ResizeObserver watches
* width only; height-only changes (which we cause ourselves when setting
* `el.style.height`) are ignored to avoid feedback loops.
*
* Returns `isMultiLine` - true when the rendered content spans more than one
* line (including soft-wrapped lines), derived from scrollHeight vs. the
* computed single-line height.
*/
export function useAutoResizeTextarea(
ref: RefObject<HTMLTextAreaElement | null>,
value: string,
maxHeight = 160,
) {
const [isMultiLine, setIsMultiLine] = useState(false);
useLayoutEffect(() => {
const el = ref.current;
if (!el) return;
el.style.height = 'auto';
// scrollHeight excludes borders while height is border-box, so the
// content box ends up a couple of pixels short and overflow-y-auto
// would show a scrollbar even for a single line. Hide it until the
// content actually exceeds the max height.
const overflowing = el.scrollHeight > maxHeight;
el.style.overflowY = overflowing ? 'auto' : 'hidden';
el.style.height = `${Math.min(el.scrollHeight, maxHeight)}px`;
const measure = () => {
el.style.height = 'auto';
// scrollHeight excludes borders while height is border-box, so the
// content box ends up a couple of pixels short and overflow-y-auto
// would show a scrollbar even for a single line. Hide it until the
// content actually exceeds the max height.
const scrollHeight = el.scrollHeight;
const overflowing = scrollHeight > maxHeight;
el.style.overflowY = overflowing ? 'auto' : 'hidden';
el.style.height = `${Math.min(scrollHeight, maxHeight)}px`;
const style = getComputedStyle(el);
const lineHeight =
parseFloat(style.lineHeight) || parseFloat(style.fontSize) * 1.2;
const verticalPadding =
parseFloat(style.paddingTop) + parseFloat(style.paddingBottom);
setIsMultiLine(scrollHeight > verticalPadding + lineHeight + 1);
};
measure();
let lastWidth: number | undefined;
const ro = new ResizeObserver((entries) => {
for (const entry of entries) {
const width = entry.contentRect.width;
if (width === lastWidth) continue;
lastWidth = width;
measure();
}
});
ro.observe(el);
return () => ro.disconnect();
}, [ref, value, maxHeight]);
return isMultiLine;
}