From b69ba4ca282f5308c600222768e205c224b23ea4 Mon Sep 17 00:00:00 2001
From: Paul <paulmakles@gmail.com>
Date: Tue, 6 Jul 2021 11:04:51 +0100
Subject: [PATCH] Fix: Textarea AutoSize did not resize correctly with long
 lines.

---
 .../common/messaging/MessageBase.tsx          |  2 +-
 .../common/messaging/MessageBox.tsx           |  8 +-
 src/components/ui/ComboBox.tsx                |  2 +-
 src/components/ui/TextArea.tsx                | 12 +--
 src/lib/TextAreaAutoSize.tsx                  | 88 +++++++++++++------
 .../channels/messaging/MessageEditor.tsx      |  4 +-
 src/styles/_variables.scss                    |  7 ++
 7 files changed, 83 insertions(+), 40 deletions(-)

diff --git a/src/components/common/messaging/MessageBase.tsx b/src/components/common/messaging/MessageBase.tsx
index afddb0d..1ed75ff 100644
--- a/src/components/common/messaging/MessageBase.tsx
+++ b/src/components/common/messaging/MessageBase.tsx
@@ -151,7 +151,7 @@ export const MessageContent = styled.div`
     flex-grow: 1;
     display: flex;
     // overflow: hidden;
-    font-size: 0.875rem;
+    font-size: var(--text-size);
     flex-direction: column;
     justify-content: center;
 `;
diff --git a/src/components/common/messaging/MessageBox.tsx b/src/components/common/messaging/MessageBox.tsx
index 340d073..1243cbb 100644
--- a/src/components/common/messaging/MessageBox.tsx
+++ b/src/components/common/messaging/MessageBox.tsx
@@ -65,7 +65,7 @@ const Base = styled.div`
     background: var(--message-box);
 
     textarea {
-        font-size: 0.875rem;
+        font-size: var(--text-size);
         background: transparent;
     }
 `;
@@ -75,7 +75,7 @@ const Blocked = styled.div`
     align-items: center;
     padding: 14px 0;
     user-select: none;
-    font-size: 0.875rem;
+    font-size: var(--text-size);
     color: var(--tertiary-foreground);
 
     svg {
@@ -423,10 +423,10 @@ function MessageBox({ channel, draft }: Props) {
                     autoFocus
                     hideBorder
                     maxRows={20}
-                    padding={12}
                     id="message"
-                    value={draft ?? ""}
                     onKeyUp={onKeyUp}
+                    value={draft ?? ""}
+                    padding="var(--message-box-padding)"
                     onKeyDown={(e) => {
                         if (onKeyDown(e)) return;
 
diff --git a/src/components/ui/ComboBox.tsx b/src/components/ui/ComboBox.tsx
index 5d5c796..5b794b4 100644
--- a/src/components/ui/ComboBox.tsx
+++ b/src/components/ui/ComboBox.tsx
@@ -6,7 +6,7 @@ export default styled.select`
     font-family: inherit;
     color: var(--secondary-foreground);
     background: var(--secondary-background);
-    font-size: 0.875rem;
+    font-size: var(--text-size);
     border: none;
     outline: 2px solid transparent;
     transition: outline-color 0.2s ease-in-out;
diff --git a/src/components/ui/TextArea.tsx b/src/components/ui/TextArea.tsx
index ab087d5..1a2016b 100644
--- a/src/components/ui/TextArea.tsx
+++ b/src/components/ui/TextArea.tsx
@@ -2,8 +2,8 @@ import styled, { css } from "styled-components";
 
 export interface TextAreaProps {
     code?: boolean;
-    padding?: number;
-    lineHeight?: number;
+    padding?: string;
+    lineHeight?: string;
     hideBorder?: boolean;
 }
 
@@ -17,8 +17,8 @@ export default styled.textarea<TextAreaProps>`
     display: block;
     color: var(--foreground);
     background: var(--secondary-background);
-    padding: ${(props) => props.padding ?? DEFAULT_TEXT_AREA_PADDING}px;
-    line-height: ${(props) => props.lineHeight ?? DEFAULT_LINE_HEIGHT}px;
+    padding: ${(props) => (props.padding) ?? 'var(--textarea-padding)'};
+    line-height: ${(props) => (props.lineHeight) ?? 'var(--textarea-line-height)'};
 
     ${(props) =>
         props.hideBorder &&
@@ -31,7 +31,7 @@ export default styled.textarea<TextAreaProps>`
         css`
             border-radius: 4px;
             transition: border-color 0.2s ease-in-out;
-            border: ${TEXT_AREA_BORDER_WIDTH}px solid transparent;
+            border: var(--input-border-width) solid transparent;
         `}
 
     &:focus {
@@ -40,7 +40,7 @@ export default styled.textarea<TextAreaProps>`
         ${(props) =>
             !props.hideBorder &&
             css`
-                border: ${TEXT_AREA_BORDER_WIDTH}px solid var(--accent);
+                border: var(--input-border-width) solid var(--accent);
             `}
     }
 
diff --git a/src/lib/TextAreaAutoSize.tsx b/src/lib/TextAreaAutoSize.tsx
index b067b95..5660ded 100644
--- a/src/lib/TextAreaAutoSize.tsx
+++ b/src/lib/TextAreaAutoSize.tsx
@@ -1,4 +1,6 @@
-import { useEffect, useRef } from "preact/hooks";
+import styled from "styled-components";
+
+import { useEffect, useLayoutEffect, useRef } from "preact/hooks";
 
 import TextArea, {
     DEFAULT_LINE_HEIGHT,
@@ -12,7 +14,7 @@ import { isTouchscreenDevice } from "./isTouchscreenDevice";
 
 type TextAreaAutoSizeProps = Omit<
     JSX.HTMLAttributes<HTMLTextAreaElement>,
-    "style" | "value"
+    "style" | "value" | "onChange"
 > &
     TextAreaProps & {
         forceFocus?: boolean;
@@ -22,8 +24,37 @@ type TextAreaAutoSizeProps = Omit<
         value: 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) {
     const {
         autoFocus,
@@ -39,19 +70,13 @@ export default function TextAreaAutoSize(props: TextAreaAutoSizeProps) {
         onChange,
         ...textAreaProps
     } = 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 ghost = useRef<HTMLDivElement>();
+
+    useLayoutEffect(() => {
+        ref.current.style.height = ghost.current.clientHeight + 'px';
+    }, [ghost, props.value]);
 
     useEffect(() => {
         if (isTouchscreenDevice) return;
@@ -101,18 +126,29 @@ export default function TextAreaAutoSize(props: TextAreaAutoSizeProps) {
     }, [ref]);
 
     return (
-        <TextArea
-            ref={ref}
-            value={value}
-            padding={padding}
-            style={{ height }}
-            hideBorder={hideBorder}
-            lineHeight={lineHeight}
-
-            onChange={ev => {
-                onChange && onChange(ev);
-            }}
-            {...textAreaProps}
-        />
+        <Container>
+            <TextArea
+                ref={ref}
+                value={value}
+                padding={padding}
+                style={{ height: minHeight }}
+                hideBorder={hideBorder}
+                lineHeight={lineHeight}
+                onChange={(ev) => {
+                    onChange && onChange(ev);
+                }}
+                {...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>
     );
 }
diff --git a/src/pages/channels/messaging/MessageEditor.tsx b/src/pages/channels/messaging/MessageEditor.tsx
index 3aa1a43..ba93b91 100644
--- a/src/pages/channels/messaging/MessageEditor.tsx
+++ b/src/pages/channels/messaging/MessageEditor.tsx
@@ -23,9 +23,9 @@ const EditorBase = styled.div`
     textarea {
         resize: none;
         padding: 12px;
-        font-size: 0.875rem;
         border-radius: 3px;
         white-space: pre-wrap;
+        font-size: var(--text-size);
         background: var(--secondary-header);
     }
 
@@ -101,9 +101,9 @@ export default function MessageEditor({ message, finish }: Props) {
             <TextAreaAutoSize
                 forceFocus
                 maxRows={3}
-                padding={12}
                 value={content}
                 maxLength={2000}
+                padding="var(--message-box-padding)"
                 onChange={(ev) => {
                     onChange(ev);
                     setContent(ev.currentTarget.value);
diff --git a/src/styles/_variables.scss b/src/styles/_variables.scss
index ec7151d..21a9b50 100644
--- a/src/styles/_variables.scss
+++ b/src/styles/_variables.scss
@@ -1,9 +1,16 @@
 :root {
     --ligatures: none;
+    --text-size: 14px;
     --font: "Open Sans";
     --app-height: 100vh;
     --codeblock-font: "Fira Code";
     --sidebar-active: var(--secondary-background);
+    
+    --input-border-width: 2px;
+
+    --textarea-padding: 16px;
+    --textarea-line-height: 20px;
+    --message-box-padding: 12px;
 
     --bottom-navigation-height: 50px;
 }
-- 
GitLab