UNPKG

@navikt/ds-react

Version:

React components from the Norwegian Labour and Welfare Administration.

183 lines 8.99 kB
var __rest = (this && this.__rest) || function (s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; }; /* https://github.com/mui/material-ui/blob/master/packages/mui-base/src/TextareaAutosize/TextareaAutosize.tsx */ import React, { forwardRef, useEffect, useRef, useState } from "react"; import ReactDOM from "react-dom"; import { useClientLayoutEffect } from "../util/hooks/index.js"; import debounce from "./debounce.js"; import { useMergeRefs } from "./hooks/useMergeRefs.js"; import { ownerWindow } from "./owner.js"; const checkState = (prevState, newState, renders) => { const { outerHeightStyle, overflow } = newState; // 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 newState; } if (process.env.NODE_ENV !== "production" && renders.current === 20) { console.error("Textarea: Too many re-renders. The layout is unstable. TextareaAutosize limits the number of renders to prevent an infinite loop."); } return prevState; }; function getStyleValue(value) { return parseInt(value, 10) || 0; } const TextareaAutosize = forwardRef((_a, ref) => { var _b, _c; var { className, onChange, maxRows, minRows = 1, autoScrollbar, style, value } = _a, other = __rest(_a, ["className", "onChange", "maxRows", "minRows", "autoScrollbar", "style", "value"]); const { current: isControlled } = useRef(value != null); const inputRef = useRef(null); const handleRef = useMergeRefs(inputRef, ref); const shadowRef = useRef(null); const renders = useRef(0); const [state, setState] = useState({ outerHeightStyle: 0 }); const getUpdatedState = React.useCallback(() => { const input = inputRef.current; 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 { outerHeightStyle: 0 }; } const inputShallow = shadowRef.current; inputShallow.style.width = computedStyle.width; inputShallow.value = input.value || other.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 = computedStyle.boxSizing; const padding = getStyleValue(computedStyle.paddingBottom) + getStyleValue(computedStyle.paddingTop); const border = getStyleValue(computedStyle.borderBottomWidth) + getStyleValue(computedStyle.borderTopWidth); // The height of the inner content const innerHeight = inputShallow.scrollHeight - padding; // Measure height of a textarea with a single row inputShallow.value = "x"; const singleRowHeight = inputShallow.scrollHeight - padding; // 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; return { outerHeightStyle, overflow }; }, [maxRows, minRows, other.placeholder]); const syncHeight = () => { const newState = getUpdatedState(); if (isEmpty(newState)) { return; } setState((prevState) => checkState(prevState, newState, renders)); }; useClientLayoutEffect(() => { const syncHeightWithFlushSync = () => { const newState = getUpdatedState(); if (isEmpty(newState)) { return; } // In React 18, state updates in a ResizeObserver's callback are happening after // the paint, this leads to an infinite rendering. // Using flushSync ensures that the state is updated before the next paint. // Related issue - https://github.com/facebook/react/issues/24331 ReactDOM.flushSync(() => { setState((prevState) => checkState(prevState, newState, renders)); }); }; const handleResize = debounce(() => { var _a, _b, _c; renders.current = 0; if (((_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.style.height) || ((_b = inputRef.current) === null || _b === void 0 ? void 0 : _b.style.width)) { // User has resized manually if (((_c = inputRef.current) === null || _c === void 0 ? void 0 : _c.style.overflow) === "hidden") { setState((oldState) => (Object.assign(Object.assign({}, oldState), { overflow: false }))); // The state update isn't important, we just need to trigger a rerender } return; } syncHeightWithFlushSync(); }, 166, true); const input = inputRef.current; const containerWindow = ownerWindow(input); containerWindow.addEventListener("resize", handleResize); let resizeObserver; if (typeof ResizeObserver !== "undefined") { resizeObserver = new ResizeObserver(handleResize); resizeObserver.observe(input); } return () => { handleResize.clear(); containerWindow.removeEventListener("resize", handleResize); if (resizeObserver) { resizeObserver.disconnect(); } }; }, [getUpdatedState]); useClientLayoutEffect(() => { syncHeight(); }); // biome-ignore lint/correctness/useExhaustiveDependencies: Since value is an external prop, we want to reset the renders on every time it changes, even when it is undefined or empty. useEffect(() => { renders.current = 0; }, [value]); const handleChange = (event) => { renders.current = 0; if (!isControlled) { syncHeight(); } if (onChange) { onChange(event); } }; const mainStyle = Object.assign({ "--__ac-textarea-height": state.outerHeightStyle + "px", "--__axc-textarea-height": state.outerHeightStyle + "px", // Need a large enough difference to allow scrolling. // This prevents infinite rendering loop. overflow: state.overflow && !autoScrollbar && !((_b = inputRef.current) === null || _b === void 0 ? void 0 : _b.style.height) && !((_c = inputRef.current) === null || _c === void 0 ? void 0 : _c.style.width) ? "hidden" : undefined }, style); return (React.createElement(React.Fragment, null, React.createElement("textarea", Object.assign({ value: value, onChange: handleChange, ref: handleRef, // Apply the rows prop to get a "correct" first SSR paint rows: minRows, style: mainStyle }, other, { className: className })), React.createElement("textarea", { "aria-hidden": true, className: className, readOnly: true, ref: shadowRef, tabIndex: -1, style: Object.assign({ // Visibility needed to hide the extra text area on iPads visibility: "hidden", // Remove from the content flow position: "absolute", // Ignore the scrollbar width overflow: "hidden", height: 0, top: 0, left: 0, // Create a new layer, increase the isolation of the computed values transform: "translateZ(0)" }, style) }))); }); function isEmpty(obj) { return (obj === undefined || obj === null || Object.keys(obj).length === 0 || (obj.outerHeightStyle === 0 && !obj.overflow)); } export default TextareaAutosize; //# sourceMappingURL=TextareaAutoSize.js.map