-
-
Notifications
You must be signed in to change notification settings - Fork 45
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add
TextareaAutosize
component and multiline
support for `I…
…nputBase`
- Loading branch information
Showing
6 changed files
with
267 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
--- | ||
"@suid/material": minor | ||
"@suid/base": minor | ||
--- | ||
|
||
Add `TextareaAutosize` component and `multiline` support for `InputBase` |
215 changes: 215 additions & 0 deletions
215
packages/base/src/TextareaAutosize/TextareaAutosize.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,215 @@ | ||
import { TextareaAutosizeTypeMap } from "."; | ||
import createComponentFactory from "./../createComponentFactory"; | ||
import createEffectWithCleaning from "@suid/system/createEffectWithCleaning"; | ||
import createRef from "@suid/system/createRef"; | ||
import { debounce, unstable_ownerWindow as ownerWindow } from "@suid/utils"; | ||
import { | ||
createSignal, | ||
createEffect, | ||
on, | ||
splitProps, | ||
mergeProps, | ||
JSX, | ||
} from "solid-js"; | ||
|
||
const $ = createComponentFactory<TextareaAutosizeTypeMap>()({ | ||
name: "MuiTextareaAutosize", | ||
selfPropNames: ["ref", "maxRows", "minRows"], | ||
}); | ||
|
||
function getStyleValue( | ||
computedStyle: CSSStyleDeclaration, | ||
property: keyof CSSStyleDeclaration | ||
) { | ||
return parseInt(computedStyle[property] as string, 10) || 0; | ||
} | ||
|
||
const styles = { | ||
shadow: { | ||
// 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)", | ||
} as JSX.CSSProperties, | ||
}; | ||
|
||
const TextareaAutosize = $.defineComponent(function (props) { | ||
const ref = createRef<HTMLTextAreaElement>(props); | ||
|
||
const [, other] = splitProps(props, ["maxRows", "minRows", "style", "value"]); | ||
|
||
const baseProps = mergeProps({ minRows: 1 }, props); | ||
|
||
let shadowRef!: HTMLTextAreaElement; | ||
const renders = { current: 0 }; | ||
const [state, setState] = createSignal<{ | ||
overflow?: boolean; | ||
outerHeightStyle?: number; | ||
}>({}); | ||
|
||
const syncHeight = () => { | ||
const input = ref.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; | ||
} | ||
|
||
shadowRef.style.width = computedStyle.width; | ||
shadowRef.value = input.value || props.placeholder || "x"; | ||
|
||
if (shadowRef.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. | ||
shadowRef.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 = shadowRef.scrollHeight; | ||
|
||
// Measure height of a textarea with a single row | ||
shadowRef.value = "x"; | ||
const singleRowHeight = shadowRef.scrollHeight; | ||
|
||
// The height of the outer content | ||
let outerHeight = innerHeight; | ||
|
||
if (baseProps.minRows) { | ||
outerHeight = Math.max( | ||
Number(baseProps.minRows) * singleRowHeight, | ||
outerHeight | ||
); | ||
} | ||
if (props.maxRows) { | ||
outerHeight = Math.min( | ||
Number(props.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, | ||
}; | ||
} | ||
|
||
if (process.env.NODE_ENV !== "production") { | ||
if (renders.current === 20) { | ||
console.error( | ||
[ | ||
"MUI: Too many re-renders. The layout is unstable.", | ||
"TextareaAutosize limits the number of renders to prevent an infinite loop.", | ||
].join("\n") | ||
); | ||
} | ||
} | ||
|
||
return prevState; | ||
}); | ||
}; | ||
|
||
createEffectWithCleaning(() => { | ||
syncHeight(); | ||
const handleResize = debounce(() => { | ||
renders.current = 0; | ||
syncHeight(); | ||
}); | ||
const containerWindow = ownerWindow(ref.current); | ||
containerWindow.addEventListener("resize", handleResize); | ||
let resizeObserver: ResizeObserver | undefined; | ||
|
||
if (typeof ResizeObserver !== "undefined") { | ||
resizeObserver = new ResizeObserver(handleResize); | ||
resizeObserver.observe(ref.current); | ||
} | ||
return () => { | ||
handleResize.clear(); | ||
containerWindow.removeEventListener("resize", handleResize); | ||
if (resizeObserver) { | ||
resizeObserver.disconnect(); | ||
} | ||
}; | ||
}); | ||
|
||
createEffect( | ||
on( | ||
() => [props.value], | ||
() => { | ||
renders.current = 0; | ||
syncHeight(); | ||
} | ||
) | ||
); | ||
|
||
const style1 = mergeProps( | ||
{ | ||
get height() { | ||
return `${state().outerHeightStyle}px`; | ||
}, | ||
get overflow() { | ||
return state().overflow ? "hidden" : null; | ||
}, | ||
}, | ||
() => props.style | ||
) as JSX.CSSProperties; | ||
|
||
const style2 = mergeProps(styles.shadow, () => props.style, { | ||
padding: 0, | ||
}) as JSX.CSSProperties; | ||
|
||
return ( | ||
<> | ||
<textarea | ||
ref={ref} | ||
// Apply the rows prop to get a "correct" first SSR paint | ||
rows={baseProps.minRows} | ||
style={style1} | ||
{...other} | ||
/> | ||
<textarea | ||
aria-hidden | ||
class={props.class} | ||
readOnly | ||
ref={shadowRef} | ||
tabIndex={-1} | ||
style={style2} | ||
/> | ||
</> | ||
); | ||
}, false); | ||
|
||
export default TextareaAutosize; |
34 changes: 34 additions & 0 deletions
34
packages/base/src/TextareaAutosize/TextareaAutosizeProps.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
import * as ST from "@suid/types"; | ||
|
||
export type TextareaAutosizeTypeMap< | ||
P = {}, | ||
D extends ST.ElementType = "div" | ||
> = { | ||
name: "MuiTextareaAutosize"; | ||
defaultPropNames: "minRows"; | ||
selfProps: { | ||
ref?: ST.Ref<HTMLTextAreaElement>; | ||
|
||
/** | ||
* Maximum number of rows to display. | ||
*/ | ||
maxRows?: string | number; | ||
|
||
/** | ||
* Minimum number of rows to display. | ||
* @default 1 | ||
*/ | ||
minRows?: string | number; | ||
}; | ||
props: P & | ||
TextareaAutosizeTypeMap["selfProps"] & | ||
Omit<ST.PropsOf<"textarea">, "children" | "rows">; | ||
defaultComponent: D; | ||
}; | ||
|
||
export type TextareaAutosizeProps< | ||
D extends ST.ElementType = TextareaAutosizeTypeMap["defaultComponent"], | ||
P = {} | ||
> = ST.OverrideProps<TextareaAutosizeTypeMap<P, D>, D>; | ||
|
||
export default TextareaAutosizeProps; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
export { default } from "./TextareaAutosize"; | ||
export * from "./TextareaAutosize"; | ||
|
||
export * from "./TextareaAutosizeProps"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters