diff --git a/docs/pages/base/api/textarea-autosize.json b/docs/pages/base/api/textarea-autosize.json index f775a92e61519e..afcb9836ac3a17 100644 --- a/docs/pages/base/api/textarea-autosize.json +++ b/docs/pages/base/api/textarea-autosize.json @@ -10,7 +10,7 @@ "styles": { "classes": [], "globalClasses": {}, "name": null }, "spread": false, "forwardsRefTo": "HTMLTextAreaElement", - "filename": "/packages/mui-base/src/TextareaAutosize/TextareaAutosize.js", + "filename": "/packages/mui-base/src/TextareaAutosize/TextareaAutosize.tsx", "inheritance": null, "demos": "", "cssComponent": false diff --git a/packages/mui-base/src/TextareaAutosize/TextareaAutosize.test.js b/packages/mui-base/src/TextareaAutosize/TextareaAutosize.test.tsx similarity index 77% rename from packages/mui-base/src/TextareaAutosize/TextareaAutosize.test.js rename to packages/mui-base/src/TextareaAutosize/TextareaAutosize.test.tsx index cd0756671df658..53715300970af5 100644 --- a/packages/mui-base/src/TextareaAutosize/TextareaAutosize.test.js +++ b/packages/mui-base/src/TextareaAutosize/TextareaAutosize.test.tsx @@ -13,7 +13,7 @@ import TextareaAutosize from '@mui/base/TextareaAutosize'; describe('', () => { const { clock, render } = createRenderer(); - const mount = createMount; + const mount = createMount(); describeConformanceUnstyled(, () => ({ render, @@ -28,21 +28,28 @@ describe('', () => { 'ownerStatePropagation', 'propsSpread', 'refForwarding', - 'rootClass', 'slotsProp', ], })); describe('layout', () => { - const getComputedStyleStub = {}; + const getComputedStyleStub = new Map>(); function setLayout( - input, - shadow, - { getComputedStyle, scrollHeight, lineHeight: lineHeightArg }, + input: HTMLTextAreaElement, + shadow: Element, + { + getComputedStyle, + scrollHeight, + lineHeight: lineHeightArg, + }: { + getComputedStyle: Partial; + scrollHeight?: number; + lineHeight?: number | (() => number); + }, ) { const lineHeight = typeof lineHeightArg === 'function' ? lineHeightArg : () => lineHeightArg; - getComputedStyleStub[input] = getComputedStyle; + getComputedStyleStub.set(input, getComputedStyle); let index = 0; stub(shadow, 'scrollHeight').get(() => { @@ -57,7 +64,9 @@ describe('', () => { this.skip(); } - stub(window, 'getComputedStyle').value((node) => getComputedStyleStub[node] || {}); + stub(window, 'getComputedStyle').value( + (node: Element) => getComputedStyleStub.get(node) || {}, + ); }); after(() => { @@ -69,14 +78,15 @@ describe('', () => { it('should handle the resize event', () => { const { container } = render(); - const input = container.querySelector('textarea[aria-hidden=null]'); - const shadow = container.querySelector('textarea[aria-hidden=true]'); - expect(input.style).to.have.property('height', ''); - expect(input.style).to.have.property('overflow', ''); + const input = container.querySelector('textarea[aria-hidden=null]')!; + const shadow = container.querySelector('textarea[aria-hidden=true]')!; + + expect(input.style).to.have.property('height', '0px'); + expect(input.style).to.have.property('overflow', 'hidden'); setLayout(input, shadow, { getComputedStyle: { - 'box-sizing': 'content-box', + boxSizing: 'content-box', }, scrollHeight: 30, lineHeight: 15, @@ -93,13 +103,13 @@ describe('', () => { it('should update when uncontrolled', () => { const handleChange = spy(); const { container } = render(); - const input = container.querySelector('textarea[aria-hidden=null]'); - const shadow = container.querySelector('textarea[aria-hidden=true]'); + const input = container.querySelector('textarea[aria-hidden=null]')!; + const shadow = container.querySelector('textarea[aria-hidden=true]')!; expect(input.style).to.have.property('height', '0px'); expect(input.style).to.have.property('overflow', 'hidden'); setLayout(input, shadow, { getComputedStyle: { - 'box-sizing': 'content-box', + boxSizing: 'content-box', }, scrollHeight: 30, lineHeight: 15, @@ -107,7 +117,8 @@ describe('', () => { act(() => { input.focus(); }); - fireEvent.change(document.activeElement, { target: { value: 'a' } }); + const activeElement = document.activeElement!; + fireEvent.change(activeElement, { target: { value: 'a' } }); expect(input.style).to.have.property('height', '30px'); expect(input.style).to.have.property('overflow', 'hidden'); expect(handleChange.callCount).to.equal(1); @@ -116,14 +127,14 @@ describe('', () => { it('should take the border into account with border-box', () => { const border = 5; const { container, forceUpdate } = render(); - const input = container.querySelector('textarea[aria-hidden=null]'); - const shadow = container.querySelector('textarea[aria-hidden=true]'); + const input = container.querySelector('textarea[aria-hidden=null]')!; + const shadow = container.querySelector('textarea[aria-hidden=true]')!; expect(input.style).to.have.property('height', '0px'); expect(input.style).to.have.property('overflow', 'hidden'); setLayout(input, shadow, { getComputedStyle: { - 'box-sizing': 'border-box', - 'border-bottom-width': `${border}px`, + boxSizing: 'border-box', + borderBottomWidth: `${border}px`, }, scrollHeight: 30, lineHeight: 15, @@ -136,12 +147,12 @@ describe('', () => { it('should take the padding into account with content-box', () => { const padding = 5; const { container, forceUpdate } = render(); - const input = container.querySelector('textarea[aria-hidden=null]'); - const shadow = container.querySelector('textarea[aria-hidden=true]'); + const input = container.querySelector('textarea[aria-hidden=null]')!; + const shadow = container.querySelector('textarea[aria-hidden=true]')!; setLayout(input, shadow, { getComputedStyle: { - 'box-sizing': 'border-box', - 'padding-top': `${padding}px`, + boxSizing: 'border-box', + paddingTop: `${padding}px`, }, scrollHeight: 30, lineHeight: 15, @@ -155,11 +166,11 @@ describe('', () => { const minRows = 3; const lineHeight = 15; const { container, forceUpdate } = render(); - const input = container.querySelector('textarea[aria-hidden=null]'); - const shadow = container.querySelector('textarea[aria-hidden=true]'); + const input = container.querySelector('textarea[aria-hidden=null]')!; + const shadow = container.querySelector('textarea[aria-hidden=true]')!; setLayout(input, shadow, { getComputedStyle: { - 'box-sizing': 'content-box', + boxSizing: 'content-box', }, scrollHeight: 30, lineHeight, @@ -173,11 +184,11 @@ describe('', () => { const maxRows = 3; const lineHeight = 15; const { container, forceUpdate } = render(); - const input = container.querySelector('textarea[aria-hidden=null]'); - const shadow = container.querySelector('textarea[aria-hidden=true]'); + const input = container.querySelector('textarea[aria-hidden=null]')!; + const shadow = container.querySelector('textarea[aria-hidden=true]')!; setLayout(input, shadow, { getComputedStyle: { - 'box-sizing': 'content-box', + boxSizing: 'content-box', }, scrollHeight: 100, lineHeight, @@ -191,11 +202,11 @@ describe('', () => { const maxRows = 3; const lineHeight = 15; const { container, forceUpdate } = render(); - const input = container.querySelector('textarea[aria-hidden=null]'); - const shadow = container.querySelector('textarea[aria-hidden=true]'); + const input = container.querySelector('textarea[aria-hidden=null]')!; + const shadow = container.querySelector('textarea[aria-hidden=true]')!; setLayout(input, shadow, { getComputedStyle: { - 'box-sizing': 'border-box', + boxSizing: 'border-box', }, scrollHeight: lineHeight * 2, lineHeight, @@ -205,7 +216,7 @@ describe('', () => { expect(input.style).to.have.property('overflow', 'hidden'); setLayout(input, shadow, { getComputedStyle: { - 'box-sizing': 'border-box', + boxSizing: 'border-box', }, scrollHeight: lineHeight * 3, lineHeight, @@ -215,7 +226,7 @@ describe('', () => { expect(input.style).to.have.property('overflow', 'hidden'); setLayout(input, shadow, { getComputedStyle: { - 'box-sizing': 'border-box', + boxSizing: 'border-box', }, scrollHeight: lineHeight * 4, lineHeight, @@ -228,11 +239,11 @@ describe('', () => { it('should update its height when the "maxRows" prop changes', () => { const lineHeight = 15; const { container, forceUpdate, setProps } = render(); - const input = container.querySelector('textarea[aria-hidden=null]'); - const shadow = container.querySelector('textarea[aria-hidden=true]'); + const input = container.querySelector('textarea[aria-hidden=null]')!; + const shadow = container.querySelector('textarea[aria-hidden=true]')!; setLayout(input, shadow, { getComputedStyle: { - 'box-sizing': 'content-box', + boxSizing: 'content-box', }, scrollHeight: 100, lineHeight, @@ -248,12 +259,12 @@ describe('', () => { it('should not sync height if container width is 0px', () => { const lineHeight = 15; const { container, forceUpdate } = render(); - const input = container.querySelector('textarea[aria-hidden=null]'); - const shadow = container.querySelector('textarea[aria-hidden=true]'); + const input = container.querySelector('textarea[aria-hidden=null]')!; + const shadow = container.querySelector('textarea[aria-hidden=true]')!; setLayout(input, shadow, { getComputedStyle: { - 'box-sizing': 'content-box', + boxSizing: 'content-box', }, scrollHeight: lineHeight * 2, lineHeight, @@ -265,7 +276,7 @@ describe('', () => { setLayout(input, shadow, { getComputedStyle: { - 'box-sizing': 'content-box', + boxSizing: 'content-box', width: '0px', }, scrollHeight: lineHeight * 3, @@ -280,12 +291,12 @@ describe('', () => { describe('warnings', () => { it('warns if layout is unstable but not crash', () => { const { container, forceUpdate } = render(); - const input = container.querySelector('textarea[aria-hidden=null]'); - const shadow = container.querySelector('textarea[aria-hidden=true]'); + const input = container.querySelector('textarea[aria-hidden=null]')!; + const shadow = container.querySelector('textarea[aria-hidden=true]')!; let index = 0; setLayout(input, shadow, { getComputedStyle: { - 'box-sizing': 'content-box', + boxSizing: 'content-box', }, scrollHeight: 100, lineHeight: () => { diff --git a/packages/mui-base/src/TextareaAutosize/TextareaAutosize.js b/packages/mui-base/src/TextareaAutosize/TextareaAutosize.tsx similarity index 79% rename from packages/mui-base/src/TextareaAutosize/TextareaAutosize.js rename to packages/mui-base/src/TextareaAutosize/TextareaAutosize.tsx index e47acd62bcf0d5..7a1f0f63b3a07b 100644 --- a/packages/mui-base/src/TextareaAutosize/TextareaAutosize.js +++ b/packages/mui-base/src/TextareaAutosize/TextareaAutosize.tsx @@ -7,12 +7,20 @@ import { unstable_useEnhancedEffect as useEnhancedEffect, unstable_ownerWindow as ownerWindow, } from '@mui/utils'; +import { TextareaAutosizeProps } from './TextareaAutosize.types'; -function getStyleValue(computedStyle, property) { - return parseInt(computedStyle[property], 10) || 0; +type State = { + outerHeightStyle: number; + overflow?: boolean | undefined; +}; + +function getStyleValue(value: string) { + return parseInt(value, 10) || 0; } -const styles = { +const styles: { + shadow: React.CSSProperties; +} = { shadow: { // Visibility needed to hide the extra text area on iPads visibility: 'hidden', @@ -28,31 +36,56 @@ const styles = { }, }; -function isEmpty(obj) { - return obj === undefined || obj === null || Object.keys(obj).length === 0; +function isEmpty(obj: State) { + return ( + obj === undefined || + obj === null || + Object.keys(obj).length === 0 || + (obj.outerHeightStyle === 0 && !obj.overflow) + ); } -const TextareaAutosize = React.forwardRef(function TextareaAutosize(props, ref) { +/** + * + * Demos: + * + * - [Textarea Autosize](https://mui.com/base/react-textarea-autosize/) + * - [Textarea Autosize](https://mui.com/material-ui/react-textarea-autosize/) + * + * API: + * + * - [TextareaAutosize API](https://mui.com/base/api/textarea-autosize/) + */ +const TextareaAutosize = React.forwardRef(function TextareaAutosize( + props: TextareaAutosizeProps, + ref: React.ForwardedRef, +) { const { onChange, maxRows, minRows = 1, style, value, ...other } = props; const { current: isControlled } = React.useRef(value != null); - const inputRef = React.useRef(null); + const inputRef = React.useRef(null); const handleRef = useForkRef(ref, inputRef); - const shadowRef = React.useRef(null); + const shadowRef = React.useRef(null); const renders = React.useRef(0); - const [state, setState] = React.useState({}); + const [state, setState] = React.useState({ + outerHeightStyle: 0, + }); const getUpdatedState = React.useCallback(() => { - const input = inputRef.current; + 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 {}; + return { + outerHeightStyle: 0, + }; } - const inputShallow = shadowRef.current; + const inputShallow = shadowRef.current!; + inputShallow.style.width = computedStyle.width; inputShallow.value = input.value || props.placeholder || 'x'; if (inputShallow.value.slice(-1) === '\n') { @@ -62,12 +95,11 @@ const TextareaAutosize = React.forwardRef(function TextareaAutosize(props, ref) inputShallow.value += ' '; } - const boxSizing = computedStyle['box-sizing']; + const boxSizing = computedStyle.boxSizing; const padding = - getStyleValue(computedStyle, 'padding-bottom') + getStyleValue(computedStyle, 'padding-top'); + getStyleValue(computedStyle.paddingBottom) + getStyleValue(computedStyle.paddingTop); const border = - getStyleValue(computedStyle, 'border-bottom-width') + - getStyleValue(computedStyle, 'border-top-width'); + getStyleValue(computedStyle.borderBottomWidth) + getStyleValue(computedStyle.borderTopWidth); // The height of the inner content const innerHeight = inputShallow.scrollHeight; @@ -94,7 +126,7 @@ const TextareaAutosize = React.forwardRef(function TextareaAutosize(props, ref) return { outerHeightStyle, overflow }; }, [maxRows, minRows, props.placeholder]); - const updateState = (prevState, newState) => { + const updateState = (prevState: State, newState: State) => { const { outerHeightStyle, overflow } = newState; // Need a large enough difference to update the height. // This prevents infinite rendering loop. @@ -164,13 +196,16 @@ const TextareaAutosize = React.forwardRef(function TextareaAutosize(props, ref) syncHeightWithFlushSycn(); } }); - const containerWindow = ownerWindow(inputRef.current); + let resizeObserver: ResizeObserver; + + const input = inputRef.current!; + const containerWindow = ownerWindow(input); + containerWindow.addEventListener('resize', handleResize); - let resizeObserver; if (typeof ResizeObserver !== 'undefined') { resizeObserver = new ResizeObserver(handleResize); - resizeObserver.observe(inputRef.current); + resizeObserver.observe(input); } return () => { @@ -190,7 +225,7 @@ const TextareaAutosize = React.forwardRef(function TextareaAutosize(props, ref) renders.current = 0; }, [value]); - const handleChange = (event) => { + const handleChange = (event: React.ChangeEvent) => { renders.current = 0; if (!isControlled) { @@ -209,12 +244,12 @@ const TextareaAutosize = React.forwardRef(function TextareaAutosize(props, ref) onChange={handleChange} ref={handleRef} // Apply the rows prop to get a "correct" first SSR paint - rows={minRows} + rows={minRows as number} style={{ height: state.outerHeightStyle, // Need a large enough difference to allow scrolling. // This prevents infinite rendering loop. - overflow: state.overflow ? 'hidden' : null, + overflow: state.overflow ? 'hidden' : undefined, ...style, }} {...other} @@ -238,7 +273,7 @@ const TextareaAutosize = React.forwardRef(function TextareaAutosize(props, ref) TextareaAutosize.propTypes /* remove-proptypes */ = { // ----------------------------- Warning -------------------------------- // | These PropTypes are generated from the TypeScript type definitions | - // | To update them edit the d.ts file and run "yarn proptypes" | + // | To update them edit TypeScript types and run "yarn proptypes" | // ---------------------------------------------------------------------- /** * @ignore @@ -273,6 +308,6 @@ TextareaAutosize.propTypes /* remove-proptypes */ = { PropTypes.number, PropTypes.string, ]), -}; +} as any; export default TextareaAutosize; diff --git a/packages/mui-base/src/TextareaAutosize/TextareaAutosize.d.ts b/packages/mui-base/src/TextareaAutosize/TextareaAutosize.types.ts similarity index 52% rename from packages/mui-base/src/TextareaAutosize/TextareaAutosize.d.ts rename to packages/mui-base/src/TextareaAutosize/TextareaAutosize.types.ts index 70f2f048af38a2..db83a932802cb3 100644 --- a/packages/mui-base/src/TextareaAutosize/TextareaAutosize.d.ts +++ b/packages/mui-base/src/TextareaAutosize/TextareaAutosize.types.ts @@ -13,16 +13,3 @@ export interface TextareaAutosizeProps */ minRows?: string | number; } - -/** - * - * Demos: - * - * - [Textarea Autosize](https://mui.com/base/react-textarea-autosize/) - * - [Textarea Autosize](https://mui.com/material-ui/react-textarea-autosize/) - * - * API: - * - * - [TextareaAutosize API](https://mui.com/base/api/textarea-autosize/) - */ -export default function TextareaAutosize(props: TextareaAutosizeProps): JSX.Element; diff --git a/packages/mui-base/src/TextareaAutosize/index.d.ts b/packages/mui-base/src/TextareaAutosize/index.d.ts deleted file mode 100644 index 8da10b5232403f..00000000000000 --- a/packages/mui-base/src/TextareaAutosize/index.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from './TextareaAutosize'; -export * from './TextareaAutosize'; diff --git a/packages/mui-base/src/TextareaAutosize/index.js b/packages/mui-base/src/TextareaAutosize/index.ts similarity index 52% rename from packages/mui-base/src/TextareaAutosize/index.js rename to packages/mui-base/src/TextareaAutosize/index.ts index 05c119fc457ddb..4561e403520716 100644 --- a/packages/mui-base/src/TextareaAutosize/index.js +++ b/packages/mui-base/src/TextareaAutosize/index.ts @@ -1 +1,2 @@ export { default } from './TextareaAutosize'; +export * from './TextareaAutosize.types';