aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSebastian <sebasjm@gmail.com>2022-04-07 10:11:47 -0300
committerSebastian <sebasjm@gmail.com>2022-04-07 10:12:05 -0300
commitfae6c420a35524d1f0d3959716c00cfc947fc0d6 (patch)
treefd17c9ecb47e222ab02f6b443b6ab44e126f5a10
parentf300850b19f8b4084bfac88a4cfd06e43bc75a8e (diff)
multiline for input
-rw-r--r--packages/taler-wallet-webextension/src/mui/TextField.stories.tsx64
-rw-r--r--packages/taler-wallet-webextension/src/mui/TextField.tsx4
-rw-r--r--packages/taler-wallet-webextension/src/mui/input/InputBase.tsx262
3 files changed, 300 insertions, 30 deletions
diff --git a/packages/taler-wallet-webextension/src/mui/TextField.stories.tsx b/packages/taler-wallet-webextension/src/mui/TextField.stories.tsx
index e46b7f46f..d0ee3b2f6 100644
--- a/packages/taler-wallet-webextension/src/mui/TextField.stories.tsx
+++ b/packages/taler-wallet-webextension/src/mui/TextField.stories.tsx
@@ -84,25 +84,45 @@ export const Standard = (): VNode => BasicExample("standard");
export const Filled = (): VNode => BasicExample("filled");
export const Outlined = (): VNode => BasicExample("outlined");
-export const Color = (): VNode => (
- <Container>
- <TextField
- variant="standard"
- label="Outlined secondary"
- color="secondary"
- focused
- />
- <TextField
- label="Filled success"
- variant="standard"
- color="success"
- focused
- />
- <TextField
- label="Standard warning"
- variant="standard"
- color="warning"
- focused
- />
- </Container>
-);
+export const Color = (): VNode => {
+ const [value, onChange] = useState("");
+ return (
+ <Container>
+ <TextField
+ variant="standard"
+ label="Outlined secondary"
+ color="secondary"
+ />
+ <TextField label="Filled success" variant="standard" color="success" />
+ <TextField label="Standard warning" variant="standard" color="warning" />
+ </Container>
+ );
+};
+
+export const Multiline = (): VNode => {
+ const [value, onChange] = useState("");
+ return (
+ <Container>
+ {/* <TextField
+ {...{ value, onChange }}
+ label="Multiline"
+ variant="standard"
+ multiline
+ /> */}
+ <TextField
+ {...{ value, onChange }}
+ label="Max row 4"
+ variant="standard"
+ multiline
+ maxRows={4}
+ />
+ {/* <TextField
+ {...{ value, onChange }}
+ label="Row 10"
+ variant="standard"
+ multiline
+ rows={10}
+ /> */}
+ </Container>
+ );
+};
diff --git a/packages/taler-wallet-webextension/src/mui/TextField.tsx b/packages/taler-wallet-webextension/src/mui/TextField.tsx
index 1987e9f6b..e1b9b3e5f 100644
--- a/packages/taler-wallet-webextension/src/mui/TextField.tsx
+++ b/packages/taler-wallet-webextension/src/mui/TextField.tsx
@@ -28,7 +28,9 @@ export interface Props {
onChange?: (s: string) => void;
placeholder?: string;
required?: boolean;
- focused?: boolean;
+
+ //FIXME: change to "grabFocus"
+ // focused?: boolean;
rows?: number;
select?: boolean;
type?: string;
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}
/>
</FormControlContext.Provider>
</Root>
);
}
+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<HTMLTextAreaElement>(null);
+ // const handleRef = useForkRef(ref, inputRef);
+ const shadowRef = useRef<HTMLTextAreaElement>(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 <input onClick={(e) => 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 (
+ <Fragment>
+ <textarea
+ class={[
+ componentStyle,
+ componentMultilineStyle,
+ // _class,
+ disabled && componentDisabledStyle,
+ // size === "small" && componentSmallStyle,
+ multiline && componentMultilineStyle,
+ type === "search" && searchStyle,
+ ].join(" ")}
+ value={value}
+ onChange={handleChange}
+ ref={inputRef}
+ // Apply the rows prop to get a "correct" first SSR paint
+ rows={minRows}
+ style={{
+ height: state.outerHeightStyle,
+ // Need a large enough difference to allow scrolling.
+ // This prevents infinite rendering loop.
+ overflow: state.overflow ? "hidden" : null,
+ ...style,
+ }}
+ // {...props}
+ />
+
+ <textarea
+ aria-hidden
+ class={[
+ componentStyle,
+ componentMultilineStyle,
+ shadowStyle,
+ type === "search" && searchStyle,
+ ].join(" ")}
+ readOnly
+ ref={shadowRef}
+ tabIndex={-1}
+ />
+ </Fragment>
+ );
}