From fae6c420a35524d1f0d3959716c00cfc947fc0d6 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Thu, 7 Apr 2022 10:11:47 -0300 Subject: multiline for input --- .../src/mui/input/InputBase.tsx | 262 ++++++++++++++++++++- 1 file changed, 255 insertions(+), 7 deletions(-) (limited to 'packages/taler-wallet-webextension/src/mui/input') diff --git a/packages/taler-wallet-webextension/src/mui/input/InputBase.tsx b/packages/taler-wallet-webextension/src/mui/input/InputBase.tsx index 5284cdad5..8992aa690 100644 --- a/packages/taler-wallet-webextension/src/mui/input/InputBase.tsx +++ b/packages/taler-wallet-webextension/src/mui/input/InputBase.tsx @@ -1,6 +1,12 @@ import { css } from "@linaria/core"; -import { h, JSX, VNode } from "preact"; -import { useLayoutEffect } from "preact/hooks"; +import { Fragment, h, JSX, VNode } from "preact"; +import { + useCallback, + useEffect, + useLayoutEffect, + useRef, + useState, +} from "preact/hooks"; // eslint-disable-next-line import/extensions import { theme } from "../style"; import { FormControlContext, useFormControl } from "./FormControl.js"; @@ -13,6 +19,10 @@ const rootStyle = css` cursor: text; display: inline-flex; align-items: center; + + [data-multiline] { + padding: 4px 0 5px; + } `; const rootDisabledStyle = css` color: ${theme.palette.text.disabled}; @@ -103,6 +113,12 @@ const componentStyle = css` animation-duration: 5000s; animation-name: auto-fill; } + textarea { + height: "auto"; + resize: "none"; + padding: 0px; + padding-top: 0px; + } `; const componentDisabledStyle = css` opacity: 1; @@ -139,7 +155,7 @@ export function InputBaseComponent({ _class, disabled && componentDisabledStyle, size === "small" && componentSmallStyle, - multiline && componentMultilineStyle, + // multiline && componentMultilineStyle, type === "search" && searchStyle, ].join(" ")} {...props} @@ -159,6 +175,8 @@ export function InputBase({ rows, type = "text", value, + maxRows, + minRows, onClick, ...props }: any): VNode { @@ -226,8 +244,12 @@ export function InputBase({ } }; - if (!Input) { - Input = props.multiline ? TextareaAutoSize : InputBaseComponent; + const rowsProps = { + minRows: rows ? rows : minRows, + maxRows: rows ? rows : maxRows, + }; + if (props.multiline) { + Input = TextareaAutoSize; } return ( @@ -249,12 +271,238 @@ export function InputBase({ onChange={handleChange} onBlur={handleBlur} onFocus={handleFocus} + {...rowsProps} + {...props} /> ); } +const shadowStyle = css` + visibility: hidden; + position: absolute; + overflow: hidden; + height: 0px; + top: 0px; + left: 0px; + transform: translateZ(0); +`; + +function ownerDocument(node: Node | null | undefined): Document { + return (node && node.ownerDocument) || document; +} +function ownerWindow(node: Node | null | undefined): Window { + const doc = ownerDocument(node); + return doc.defaultView || window; +} +function getStyleValue( + computedStyle: CSSStyleDeclaration, + property: any, +): number { + return parseInt(computedStyle[property], 10) || 0; +} + +function debounce(func: any, wait = 166): any { + let timeout: any; + function debounced(...args) { + const later = () => { + func.apply(this, args); + }; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + } + + debounced.clear = () => { + clearTimeout(timeout); + }; + + return debounced; +} + +export function TextareaAutoSize({ + // disabled, + // size, + onChange, + value, + multiline, + focused, + disabled, + error, + minRows = 1, + maxRows, + style, + type, + class: _class, + ...props +}: any): VNode { + // const { onChange, maxRows, minRows = 1, style, value, ...other } = props; + + const { current: isControlled } = useRef(value != null); + const inputRef = useRef(null); + // const handleRef = useForkRef(ref, inputRef); + const shadowRef = useRef(null); + const renders = useRef(0); + const [state, setState] = useState<{ outerHeightStyle: any; overflow: any }>({ + outerHeightStyle: undefined, + overflow: undefined, + }); + + const syncHeight = useCallback(() => { + const input = inputRef.current; + const inputShallow = shadowRef.current; + if (!input || !inputShallow) return; + const containerWindow = ownerWindow(input); + const computedStyle = containerWindow.getComputedStyle(input); + + // If input's width is shrunk and it's not visible, don't sync height. + if (computedStyle.width === "0px") { + return; + } -export function TextareaAutoSize(): VNode { - return null} />; + inputShallow.style.width = computedStyle.width; + inputShallow.value = input.value || props.placeholder || "x"; + if (inputShallow.value.slice(-1) === "\n") { + // Certain fonts which overflow the line height will cause the textarea + // to report a different scrollHeight depending on whether the last line + // is empty. Make it non-empty to avoid this issue. + inputShallow.value += " "; + } + + const boxSizing: string = computedStyle["box-sizing" as any]; + const padding = + getStyleValue(computedStyle, "padding-bottom") + + getStyleValue(computedStyle, "padding-top"); + const border = + getStyleValue(computedStyle, "border-bottom-width") + + getStyleValue(computedStyle, "border-top-width"); + + // console.log(boxSizing, padding, border); + // The height of the inner content + const innerHeight = inputShallow.scrollHeight; + + // Measure height of a textarea with a single row + inputShallow.value = "x"; + const singleRowHeight = inputShallow.scrollHeight; + + // The height of the outer content + let outerHeight = innerHeight; + + if (minRows) { + outerHeight = Math.max(Number(minRows) * singleRowHeight, outerHeight); + } + if (maxRows) { + outerHeight = Math.min(Number(maxRows) * singleRowHeight, outerHeight); + } + outerHeight = Math.max(outerHeight, singleRowHeight); + + // Take the box sizing into account for applying this value as a style. + const outerHeightStyle = + outerHeight + (boxSizing === "border-box" ? padding + border : 0); + const overflow = Math.abs(outerHeight - innerHeight) <= 1; + + console.log("height", outerHeight, minRows, maxRows); + setState((prevState) => { + // Need a large enough difference to update the height. + // This prevents infinite rendering loop. + if ( + renders.current < 20 && + ((outerHeightStyle > 0 && + Math.abs((prevState.outerHeightStyle || 0) - outerHeightStyle) > 1) || + prevState.overflow !== overflow) + ) { + renders.current += 1; + return { + overflow, + outerHeightStyle, + }; + } + + return prevState; + }); + }, [maxRows, minRows, props.placeholder]); + + useLayoutEffect(() => { + const handleResize = debounce(() => { + renders.current = 0; + syncHeight(); + }); + const containerWindow = ownerWindow(inputRef.current); + containerWindow.addEventListener("resize", handleResize); + let resizeObserver: any; + + if (typeof ResizeObserver !== "undefined") { + resizeObserver = new ResizeObserver(handleResize); + resizeObserver.observe(inputRef.current); + } + + return () => { + handleResize.clear(); + containerWindow.removeEventListener("resize", handleResize); + if (resizeObserver) { + resizeObserver.disconnect(); + } + }; + }, [syncHeight]); + + useLayoutEffect(() => { + syncHeight(); + }); + + useLayoutEffect(() => { + renders.current = 0; + }, [value]); + + const handleChange = (event) => { + renders.current = 0; + + if (!isControlled) { + syncHeight(); + } + + if (onChange) { + onChange(event); + } + }; + + return ( + +