Fix: Textarea AutoSize did not resize correctly with long lines.

This commit is contained in:
Paul 2021-07-06 11:04:51 +01:00
parent 56b509a16a
commit b69ba4ca28
7 changed files with 83 additions and 40 deletions

View file

@ -151,7 +151,7 @@ export const MessageContent = styled.div`
flex-grow: 1; flex-grow: 1;
display: flex; display: flex;
// overflow: hidden; // overflow: hidden;
font-size: 0.875rem; font-size: var(--text-size);
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
`; `;

View file

@ -65,7 +65,7 @@ const Base = styled.div`
background: var(--message-box); background: var(--message-box);
textarea { textarea {
font-size: 0.875rem; font-size: var(--text-size);
background: transparent; background: transparent;
} }
`; `;
@ -75,7 +75,7 @@ const Blocked = styled.div`
align-items: center; align-items: center;
padding: 14px 0; padding: 14px 0;
user-select: none; user-select: none;
font-size: 0.875rem; font-size: var(--text-size);
color: var(--tertiary-foreground); color: var(--tertiary-foreground);
svg { svg {
@ -423,10 +423,10 @@ function MessageBox({ channel, draft }: Props) {
autoFocus autoFocus
hideBorder hideBorder
maxRows={20} maxRows={20}
padding={12}
id="message" id="message"
value={draft ?? ""}
onKeyUp={onKeyUp} onKeyUp={onKeyUp}
value={draft ?? ""}
padding="var(--message-box-padding)"
onKeyDown={(e) => { onKeyDown={(e) => {
if (onKeyDown(e)) return; if (onKeyDown(e)) return;

View file

@ -6,7 +6,7 @@ export default styled.select`
font-family: inherit; font-family: inherit;
color: var(--secondary-foreground); color: var(--secondary-foreground);
background: var(--secondary-background); background: var(--secondary-background);
font-size: 0.875rem; font-size: var(--text-size);
border: none; border: none;
outline: 2px solid transparent; outline: 2px solid transparent;
transition: outline-color 0.2s ease-in-out; transition: outline-color 0.2s ease-in-out;

View file

@ -2,8 +2,8 @@ import styled, { css } from "styled-components";
export interface TextAreaProps { export interface TextAreaProps {
code?: boolean; code?: boolean;
padding?: number; padding?: string;
lineHeight?: number; lineHeight?: string;
hideBorder?: boolean; hideBorder?: boolean;
} }
@ -17,8 +17,8 @@ export default styled.textarea<TextAreaProps>`
display: block; display: block;
color: var(--foreground); color: var(--foreground);
background: var(--secondary-background); background: var(--secondary-background);
padding: ${(props) => props.padding ?? DEFAULT_TEXT_AREA_PADDING}px; padding: ${(props) => (props.padding) ?? 'var(--textarea-padding)'};
line-height: ${(props) => props.lineHeight ?? DEFAULT_LINE_HEIGHT}px; line-height: ${(props) => (props.lineHeight) ?? 'var(--textarea-line-height)'};
${(props) => ${(props) =>
props.hideBorder && props.hideBorder &&
@ -31,7 +31,7 @@ export default styled.textarea<TextAreaProps>`
css` css`
border-radius: 4px; border-radius: 4px;
transition: border-color 0.2s ease-in-out; transition: border-color 0.2s ease-in-out;
border: ${TEXT_AREA_BORDER_WIDTH}px solid transparent; border: var(--input-border-width) solid transparent;
`} `}
&:focus { &:focus {
@ -40,7 +40,7 @@ export default styled.textarea<TextAreaProps>`
${(props) => ${(props) =>
!props.hideBorder && !props.hideBorder &&
css` css`
border: ${TEXT_AREA_BORDER_WIDTH}px solid var(--accent); border: var(--input-border-width) solid var(--accent);
`} `}
} }

View file

@ -1,4 +1,6 @@
import { useEffect, useRef } from "preact/hooks"; import styled from "styled-components";
import { useEffect, useLayoutEffect, useRef } from "preact/hooks";
import TextArea, { import TextArea, {
DEFAULT_LINE_HEIGHT, DEFAULT_LINE_HEIGHT,
@ -12,7 +14,7 @@ import { isTouchscreenDevice } from "./isTouchscreenDevice";
type TextAreaAutoSizeProps = Omit< type TextAreaAutoSizeProps = Omit<
JSX.HTMLAttributes<HTMLTextAreaElement>, JSX.HTMLAttributes<HTMLTextAreaElement>,
"style" | "value" "style" | "value" | "onChange"
> & > &
TextAreaProps & { TextAreaProps & {
forceFocus?: boolean; forceFocus?: boolean;
@ -22,8 +24,37 @@ type TextAreaAutoSizeProps = Omit<
value: string; value: string;
id?: string; id?: string;
onChange?: (ev: JSX.TargetedEvent<HTMLTextAreaElement, Event>) => void;
}; };
const Container = styled.div`
flex-grow: 1;
display: flex;
flex-direction: column;
`;
const Ghost = styled.div<{ lineHeight: string, maxRows: number }>`
flex: 0;
width: 100%;
overflow: hidden;
visibility: hidden;
position: relative;
> div {
width: 100%;
white-space: pre-wrap;
word-break: break-all;
top: 0;
position: absolute;
font-size: var(--text-size);
line-height: ${(props) => props.lineHeight};
max-height: calc(calc( ${(props) => props.lineHeight} * ${ (props) => props.maxRows } ));
}
`;
export default function TextAreaAutoSize(props: TextAreaAutoSizeProps) { export default function TextAreaAutoSize(props: TextAreaAutoSizeProps) {
const { const {
autoFocus, autoFocus,
@ -39,19 +70,13 @@ export default function TextAreaAutoSize(props: TextAreaAutoSizeProps) {
onChange, onChange,
...textAreaProps ...textAreaProps
} = props; } = props;
const line = lineHeight ?? DEFAULT_LINE_HEIGHT;
const heightPadding =
((padding ?? DEFAULT_TEXT_AREA_PADDING) +
(hideBorder ? 0 : TEXT_AREA_BORDER_WIDTH)) *
2;
const height = Math.max(
Math.min(value.split("\n").length, maxRows ?? Infinity) * line +
heightPadding,
minHeight ?? 0,
);
const ref = useRef<HTMLTextAreaElement>(); const ref = useRef<HTMLTextAreaElement>();
const ghost = useRef<HTMLDivElement>();
useLayoutEffect(() => {
ref.current.style.height = ghost.current.clientHeight + 'px';
}, [ghost, props.value]);
useEffect(() => { useEffect(() => {
if (isTouchscreenDevice) return; if (isTouchscreenDevice) return;
@ -101,18 +126,29 @@ export default function TextAreaAutoSize(props: TextAreaAutoSizeProps) {
}, [ref]); }, [ref]);
return ( return (
<Container>
<TextArea <TextArea
ref={ref} ref={ref}
value={value} value={value}
padding={padding} padding={padding}
style={{ height }} style={{ height: minHeight }}
hideBorder={hideBorder} hideBorder={hideBorder}
lineHeight={lineHeight} lineHeight={lineHeight}
onChange={(ev) => {
onChange={ev => {
onChange && onChange(ev); onChange && onChange(ev);
}} }}
{...textAreaProps} {...textAreaProps}
/> />
<Ghost lineHeight={lineHeight ?? 'var(--textarea-line-height)'} maxRows={maxRows ?? 5}>
<div ref={ghost} style={{ padding }}>
{props.value
? props.value
.split("\n")
.map((x) => `\u200e${x}`)
.join("\n")
: undefined ?? "\n"}
</div>
</Ghost>
</Container>
); );
} }

View file

@ -23,9 +23,9 @@ const EditorBase = styled.div`
textarea { textarea {
resize: none; resize: none;
padding: 12px; padding: 12px;
font-size: 0.875rem;
border-radius: 3px; border-radius: 3px;
white-space: pre-wrap; white-space: pre-wrap;
font-size: var(--text-size);
background: var(--secondary-header); background: var(--secondary-header);
} }
@ -101,9 +101,9 @@ export default function MessageEditor({ message, finish }: Props) {
<TextAreaAutoSize <TextAreaAutoSize
forceFocus forceFocus
maxRows={3} maxRows={3}
padding={12}
value={content} value={content}
maxLength={2000} maxLength={2000}
padding="var(--message-box-padding)"
onChange={(ev) => { onChange={(ev) => {
onChange(ev); onChange(ev);
setContent(ev.currentTarget.value); setContent(ev.currentTarget.value);

View file

@ -1,9 +1,16 @@
:root { :root {
--ligatures: none; --ligatures: none;
--text-size: 14px;
--font: "Open Sans"; --font: "Open Sans";
--app-height: 100vh; --app-height: 100vh;
--codeblock-font: "Fira Code"; --codeblock-font: "Fira Code";
--sidebar-active: var(--secondary-background); --sidebar-active: var(--secondary-background);
--input-border-width: 2px;
--textarea-padding: 16px;
--textarea-line-height: 20px;
--message-box-padding: 12px;
--bottom-navigation-height: 50px; --bottom-navigation-height: 50px;
} }