summaryrefslogtreecommitdiff
path: root/packages/taler-wallet-webextension/src/mui/input/InputBase.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'packages/taler-wallet-webextension/src/mui/input/InputBase.tsx')
-rw-r--r--packages/taler-wallet-webextension/src/mui/input/InputBase.tsx562
1 files changed, 562 insertions, 0 deletions
diff --git a/packages/taler-wallet-webextension/src/mui/input/InputBase.tsx b/packages/taler-wallet-webextension/src/mui/input/InputBase.tsx
new file mode 100644
index 000000000..d811a3dbb
--- /dev/null
+++ b/packages/taler-wallet-webextension/src/mui/input/InputBase.tsx
@@ -0,0 +1,562 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+import { css } from "@linaria/core";
+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.js";
+import { FormControlContext, useFormControl } from "./FormControl.js";
+
+const rootStyle = css`
+ color: ${theme.palette.text.primary};
+ line-height: 1.4375em;
+ box-sizing: border-box;
+ position: relative;
+ cursor: text;
+ display: inline-flex;
+ align-items: center;
+`;
+const rootDisabledStyle = css`
+ color: ${theme.palette.text.disabled};
+ cursor: default;
+`;
+const rootMultilineStyle = css`
+ padding: 4px 0 5px;
+`;
+const fullWidthStyle = css`
+ width: "100%";
+`;
+
+export function InputBaseRoot({
+ class: _class,
+ disabled,
+ error,
+ multiline,
+ focused,
+ fullWidth,
+ startAdornment,
+ endAdornment,
+ children,
+}: any): VNode {
+ const fcs = useFormControl({});
+ return (
+ <div
+ data-disabled={!disabled ? undefined : true}
+ data-focused={!focused ? undefined : true}
+ data-multiline={multiline}
+ data-hasStart={!!startAdornment}
+ data-hasEnd={!!endAdornment}
+ data-error={!error ? undefined : true}
+ class={[
+ _class,
+ rootStyle,
+ theme.typography.body1,
+ disabled && rootDisabledStyle,
+ multiline && rootMultilineStyle,
+ fullWidth && fullWidthStyle,
+ ].join(" ")}
+ style={{
+ "--color-main": theme.palette[fcs.color].main,
+ }}
+ >
+ {children}
+ </div>
+ );
+}
+
+const componentStyle = css`
+ font: inherit;
+ letter-spacing: inherit;
+ color: currentColor;
+ border: 0px;
+ box-sizing: content-box;
+ background: none;
+ height: 1.4375em;
+ margin: 0px;
+ -webkit-tap-highlight-color: transparent;
+ display: block;
+ min-width: 0px;
+ width: 100%;
+ animation-name: "auto-fill-cancel";
+ animation-duration: 10ms;
+
+ @keyframes auto-fill {
+ from {
+ display: block;
+ }
+ }
+ @keyframes auto-fill-cancel {
+ from {
+ display: block;
+ }
+ }
+ &::placeholder {
+ color: "currentColor";
+ opacity: ${theme.palette.mode === "light" ? 0.42 : 0.5};
+ transition: ${theme.transitions.create("opacity", {
+ duration: theme.transitions.duration.shorter,
+ })};
+ }
+ &:not(focus)::placeholder {
+ opacity: 0;
+ }
+ &:focus::placeholder {
+ opacity: ${theme.palette.mode === "light" ? 0.42 : 0.5};
+ }
+ &:focus {
+ outline: 0;
+ }
+ &:invalid {
+ box-shadow: none;
+ }
+ &::-webkit-search-decoration {
+ -webkit-appearance: none;
+ }
+ &:-webkit-autofill {
+ animation-duration: 5000s;
+ animation-name: auto-fill;
+ }
+ textarea {
+ height: "auto";
+ resize: "none";
+ padding: 0px;
+ padding-top: 0px;
+ }
+`;
+const componentDisabledStyle = css`
+ opacity: 1;
+ --webkit-text-fill-color: ${theme.palette.text.disabled};
+`;
+const componentSmallStyle = css`
+ padding-top: 1px;
+`;
+const componentMultilineStyle = css`
+ height: auto;
+ resize: none;
+ padding: 0px;
+ padding-top: 0px;
+`;
+const searchStyle = css`
+ -moz-appearance: textfield;
+ -webkit-appearance: textfield;
+`;
+
+export function InputBaseComponent({
+ disabled,
+ size,
+ multiline,
+ type,
+ class: _class,
+ startAdornment,
+ endAdornment,
+ ...props
+}: any): VNode {
+ return (
+ <Fragment>
+ {startAdornment}
+ <input
+ disabled={disabled}
+ type={type}
+ class={[
+ componentStyle,
+ _class,
+ disabled && componentDisabledStyle,
+ size === "small" && componentSmallStyle,
+ // multiline && componentMultilineStyle,
+ type === "search" && searchStyle,
+ ].join(" ")}
+ {...props}
+ />
+ {endAdornment}
+ </Fragment>
+ );
+}
+
+export function InputBase({
+ Root = InputBaseRoot,
+ Input,
+ onChange,
+ onInput,
+ name,
+ placeholder,
+ readOnly,
+ onKeyUp,
+ onKeyDown,
+ rows,
+ type = "text",
+ value,
+ maxRows,
+ minRows,
+ onClick,
+ ...props
+}: any): VNode {
+ const fcs = useFormControl(props);
+ // const [focused, setFocused] = useState(false);
+ useLayoutEffect(() => {
+ if (value && value !== "") {
+ fcs.onFilled();
+ } else {
+ fcs.onEmpty();
+ }
+ }, [value, fcs]);
+
+ const handleFocus = (event: JSX.TargetedFocusEvent<EventTarget>): void => {
+ // Fix a bug with IE11 where the focus/blur events are triggered
+ // while the component is disabled.
+ if (fcs.disabled) {
+ event.stopPropagation();
+ return;
+ }
+
+ // if (onFocus) {
+ // onFocus(event);
+ // }
+ // if (inputPropsProp.onFocus) {
+ // inputPropsProp.onFocus(event);
+ // }
+
+ fcs.onFocus();
+ };
+
+ const handleBlur = (): void => {
+ // if (onBlur) {
+ // onBlur(event);
+ // }
+ // if (inputPropsProp.onBlur) {
+ // inputPropsProp.onBlur(event);
+ // }
+
+ fcs.onBlur();
+ };
+
+ const handleChange = (
+ event: JSX.TargetedEvent<HTMLElement & { value?: string }>,
+ ): void => {
+ // if (inputPropsProp.onChange) {
+ // inputPropsProp.onChange(event, ...args);
+ // }
+
+ // Perform in the willUpdate
+ if (onChange) {
+ onChange(event.currentTarget.value);
+ }
+ };
+
+ const handleInput = (
+ event: JSX.TargetedEvent<HTMLElement & { value?: string }>,
+ ): void => {
+ // if (inputPropsProp.onChange) {
+ // inputPropsProp.onChange(event, ...args);
+ // }
+
+ // Perform in the willUpdate
+ if (onInput) {
+ event.currentTarget.value = onInput(event.currentTarget.value);
+ }
+ };
+
+ const handleClick = (
+ event: JSX.TargetedMouseEvent<HTMLElement & { value?: string }>,
+ ): void => {
+ // if (inputRef.current && event.currentTarget === event.target) {
+ // inputRef.current.focus();
+ // }
+
+ if (onClick) {
+ onClick(event.currentTarget.value);
+ }
+ };
+
+ const rowsProps = {
+ minRows: rows ? rows : minRows,
+ maxRows: rows ? rows : maxRows,
+ };
+ if (props.multiline) {
+ Input = TextareaAutoSize;
+ }
+
+ return (
+ <Root {...fcs} onClick={handleClick}>
+ <FormControlContext.Provider value={null}>
+ <Input
+ aria-invalid={fcs.error ? true : undefined}
+ // aria-describedby={}
+ disabled={fcs.disabled ? true : undefined}
+ name={name}
+ placeholder={!placeholder ? undefined : placeholder}
+ readOnly={readOnly}
+ required={fcs.required}
+ rows={rows}
+ value={value}
+ onKeyDown={onKeyDown}
+ onKeyUp={onKeyUp}
+ type={type}
+ onInput={handleInput}
+ 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: any[]): void {
+ const later = () => {
+ func.apply({}, args);
+ };
+ clearTimeout(timeout);
+ timeout = setTimeout(later, wait);
+ }
+
+ debounced.clear = () => {
+ clearTimeout(timeout);
+ };
+
+ return debounced;
+}
+
+export function TextareaAutoSize({
+ // disabled,
+ // size,
+ onChange,
+ onInput,
+ 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;
+ }
+
+ 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");
+
+ // 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;
+
+ 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: any): void => {
+ renders.current = 0;
+
+ if (!isControlled) {
+ syncHeight();
+ }
+
+ if (onChange) {
+ onChange(event.target.value);
+ }
+ };
+ const handleInput = (event: any): void => {
+ renders.current = 0;
+
+ if (!isControlled) {
+ syncHeight();
+ }
+
+ if (onInput) {
+ event.currentTarget.value = onInput(event.currentTarget.value);
+ }
+ };
+
+ return (
+ <Fragment>
+ <textarea
+ class={[
+ componentStyle,
+ componentMultilineStyle,
+ _class,
+ disabled && componentDisabledStyle,
+ // size === "small" && componentSmallStyle,
+ multiline && componentMultilineStyle,
+ type === "search" && searchStyle,
+ ].join(" ")}
+ value={value}
+ onChange={handleChange}
+ onInput={handleInput}
+ 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>
+ );
+}