Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[TextareaAutosize] Convert code to TypeScript #35862

Merged
merged 20 commits into from
Feb 16, 2023
Merged
2 changes: 1 addition & 1 deletion docs/pages/base/api/textarea-autosize.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "<ul><li><a href=\"/base/react-textarea-autosize/\">Textarea Autosize</a></li>\n<li><a href=\"/material-ui/react-textarea-autosize/\">Textarea Autosize</a></li></ul>",
"cssComponent": false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@ import {
strictModeDoubleLoggingSupressed,
} from 'test/utils';
import TextareaAutosize from '@mui/base/TextareaAutosize';
import { KebabKeys } from '@mui/types';

describe('<TextareaAutosize />', () => {
const { clock, render } = createRenderer();
const mount = createMount;
const mount = createMount();

describeConformanceUnstyled(<TextareaAutosize />, () => ({
render,
Expand All @@ -28,21 +29,28 @@ describe('<TextareaAutosize />', () => {
'ownerStatePropagation',
'propsSpread',
'refForwarding',
'rootClass',
Copy link
Contributor Author

@sai6855 sai6855 Jan 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'rootClass' is not present in fullSuite, hence removed to make typescript happy.

const fullSuite = {
componentProp: testComponentProp,
slotsProp: testSlotsProp,
slotPropsProp: testSlotPropsProp,
slotPropsCallbacks: testSlotPropsCallbacks,
mergeClassName: testClassName,
propsSpread: testPropForwarding,
reactTestRenderer: testReactTestRenderer,
refForwarding: describeRef,
ownerStatePropagation: testOwnerStatePropagation,
};

'slotsProp',
],
}));

describe('layout', () => {
const getComputedStyleStub = {};
const getComputedStyleStub = new Map<Element, Partial<KebabKeys<CSSStyleDeclaration>>>();
function setLayout(
input,
shadow,
{ getComputedStyle, scrollHeight, lineHeight: lineHeightArg },
input: HTMLTextAreaElement,
shadow: Element,
{
getComputedStyle,
scrollHeight,
lineHeight: lineHeightArg,
}: {
getComputedStyle: Partial<KebabKeys<CSSStyleDeclaration>>;
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(() => {
Expand All @@ -57,7 +65,9 @@ describe('<TextareaAutosize />', () => {
this.skip();
}

stub(window, 'getComputedStyle').value((node) => getComputedStyleStub[node] || {});
stub(window, 'getComputedStyle').value(
(node: Element) => getComputedStyleStub.get(node) || {},
);
});

after(() => {
Expand All @@ -69,10 +79,11 @@ describe('<TextareaAutosize />', () => {

it('should handle the resize event', () => {
const { container } = render(<TextareaAutosize />);
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<HTMLTextAreaElement>('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: {
Expand All @@ -93,8 +104,8 @@ describe('<TextareaAutosize />', () => {
it('should update when uncontrolled', () => {
const handleChange = spy();
const { container } = render(<TextareaAutosize onChange={handleChange} />);
const input = container.querySelector('textarea[aria-hidden=null]');
const shadow = container.querySelector('textarea[aria-hidden=true]');
const input = container.querySelector<HTMLTextAreaElement>('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, {
Expand All @@ -107,7 +118,8 @@ describe('<TextareaAutosize />', () => {
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);
Expand All @@ -116,8 +128,8 @@ describe('<TextareaAutosize />', () => {
it('should take the border into account with border-box', () => {
const border = 5;
const { container, forceUpdate } = render(<TextareaAutosize />);
const input = container.querySelector('textarea[aria-hidden=null]');
const shadow = container.querySelector('textarea[aria-hidden=true]');
const input = container.querySelector<HTMLTextAreaElement>('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, {
Expand All @@ -136,8 +148,8 @@ describe('<TextareaAutosize />', () => {
it('should take the padding into account with content-box', () => {
const padding = 5;
const { container, forceUpdate } = render(<TextareaAutosize />);
const input = container.querySelector('textarea[aria-hidden=null]');
const shadow = container.querySelector('textarea[aria-hidden=true]');
const input = container.querySelector<HTMLTextAreaElement>('textarea[aria-hidden=null]')!;
const shadow = container.querySelector('textarea[aria-hidden=true]')!;
setLayout(input, shadow, {
getComputedStyle: {
'box-sizing': 'border-box',
Expand All @@ -155,8 +167,8 @@ describe('<TextareaAutosize />', () => {
const minRows = 3;
const lineHeight = 15;
const { container, forceUpdate } = render(<TextareaAutosize minRows={minRows} />);
const input = container.querySelector('textarea[aria-hidden=null]');
const shadow = container.querySelector('textarea[aria-hidden=true]');
const input = container.querySelector<HTMLTextAreaElement>('textarea[aria-hidden=null]')!;
const shadow = container.querySelector('textarea[aria-hidden=true]')!;
setLayout(input, shadow, {
getComputedStyle: {
'box-sizing': 'content-box',
Expand All @@ -173,8 +185,8 @@ describe('<TextareaAutosize />', () => {
const maxRows = 3;
const lineHeight = 15;
const { container, forceUpdate } = render(<TextareaAutosize maxRows={maxRows} />);
const input = container.querySelector('textarea[aria-hidden=null]');
const shadow = container.querySelector('textarea[aria-hidden=true]');
const input = container.querySelector<HTMLTextAreaElement>('textarea[aria-hidden=null]')!;
const shadow = container.querySelector('textarea[aria-hidden=true]')!;
setLayout(input, shadow, {
getComputedStyle: {
'box-sizing': 'content-box',
Expand All @@ -191,8 +203,8 @@ describe('<TextareaAutosize />', () => {
const maxRows = 3;
const lineHeight = 15;
const { container, forceUpdate } = render(<TextareaAutosize maxRows={maxRows} />);
const input = container.querySelector('textarea[aria-hidden=null]');
const shadow = container.querySelector('textarea[aria-hidden=true]');
const input = container.querySelector<HTMLTextAreaElement>('textarea[aria-hidden=null]')!;
const shadow = container.querySelector('textarea[aria-hidden=true]')!;
setLayout(input, shadow, {
getComputedStyle: {
'box-sizing': 'border-box',
Expand Down Expand Up @@ -228,8 +240,8 @@ describe('<TextareaAutosize />', () => {
it('should update its height when the "maxRows" prop changes', () => {
const lineHeight = 15;
const { container, forceUpdate, setProps } = render(<TextareaAutosize maxRows={3} />);
const input = container.querySelector('textarea[aria-hidden=null]');
const shadow = container.querySelector('textarea[aria-hidden=true]');
const input = container.querySelector<HTMLTextAreaElement>('textarea[aria-hidden=null]')!;
const shadow = container.querySelector('textarea[aria-hidden=true]')!;
setLayout(input, shadow, {
getComputedStyle: {
'box-sizing': 'content-box',
Expand All @@ -248,8 +260,8 @@ describe('<TextareaAutosize />', () => {
it('should not sync height if container width is 0px', () => {
const lineHeight = 15;
const { container, forceUpdate } = render(<TextareaAutosize />);
const input = container.querySelector('textarea[aria-hidden=null]');
const shadow = container.querySelector('textarea[aria-hidden=true]');
const input = container.querySelector<HTMLTextAreaElement>('textarea[aria-hidden=null]')!;
const shadow = container.querySelector('textarea[aria-hidden=true]')!;

setLayout(input, shadow, {
getComputedStyle: {
Expand Down Expand Up @@ -280,8 +292,8 @@ describe('<TextareaAutosize />', () => {
describe('warnings', () => {
it('warns if layout is unstable but not crash', () => {
const { container, forceUpdate } = render(<TextareaAutosize maxRows={3} />);
const input = container.querySelector('textarea[aria-hidden=null]');
const shadow = container.querySelector('textarea[aria-hidden=true]');
const input = container.querySelector<HTMLTextAreaElement>('textarea[aria-hidden=null]')!;
const shadow = container.querySelector('textarea[aria-hidden=true]')!;
let index = 0;
setLayout(input, shadow, {
getComputedStyle: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,29 @@ import {
unstable_useEnhancedEffect as useEnhancedEffect,
unstable_ownerWindow as ownerWindow,
} from '@mui/utils';
import { KebabKeys } from '@mui/types';
import { TextareaAutosizeProps } from './TextareaAutosize.types';

function getStyleValue(computedStyle, property) {
return parseInt(computedStyle[property], 10) || 0;
type State =
| {
outerHeightStyle?: undefined;
overflow?: undefined;
}
| {
outerHeightStyle: number;
overflow: boolean | undefined;
};

function getStyleValue(
computedStyle: KebabKeys<CSSStyleDeclaration>,
property: keyof KebabKeys<CSSStyleDeclaration>,
) {
return parseInt(`${computedStyle[property]}`, 10) || 0;
}

const styles = {
const styles: {
shadow: React.CSSProperties;
} = {
shadow: {
// Visibility needed to hide the extra text area on iPads
visibility: 'hidden',
Expand All @@ -28,31 +45,53 @@ const styles = {
},
};

function isEmpty(obj) {
function isEmpty(obj: State) {
return obj === undefined || obj === null || Object.keys(obj).length === 0;
}

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<Element>,
) {
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<HTMLInputElement>(null);
const handleRef = useForkRef(ref, inputRef);
const shadowRef = React.useRef(null);
const shadowRef = React.useRef<HTMLTextAreaElement>(null);
const renders = React.useRef(0);
const [state, setState] = React.useState({});
const [state, setState] = React.useState<State>({});

const getUpdatedState = React.useCallback(() => {
const input = inputRef.current;
if (!input) {
return {};
}
oliviertassinari marked this conversation as resolved.
Show resolved Hide resolved
const containerWindow = ownerWindow(input);
const computedStyle = containerWindow.getComputedStyle(input);
const computedStyle = containerWindow.getComputedStyle(
input,
) as unknown as KebabKeys<CSSStyleDeclaration>;

// If input's width is shrunk and it's not visible, don't sync height.
if (computedStyle.width === '0px') {
return {};
}

const inputShallow = shadowRef.current;
if (!inputShallow) {
return {};
}
oliviertassinari marked this conversation as resolved.
Show resolved Hide resolved
inputShallow.style.width = computedStyle.width;
inputShallow.value = input.value || props.placeholder || 'x';
if (inputShallow.value.slice(-1) === '\n') {
Expand Down Expand Up @@ -94,8 +133,8 @@ const TextareaAutosize = React.forwardRef(function TextareaAutosize(props, ref)
return { outerHeightStyle, overflow };
}, [maxRows, minRows, props.placeholder]);

const updateState = (prevState, newState) => {
const { outerHeightStyle, overflow } = newState;
const updateState = (prevState: State, newState: State) => {
const { outerHeightStyle = 0, overflow } = newState;
oliviertassinari marked this conversation as resolved.
Show resolved Hide resolved
// Need a large enough difference to update the height.
// This prevents infinite rendering loop.
if (
Expand Down Expand Up @@ -164,18 +203,23 @@ const TextareaAutosize = React.forwardRef(function TextareaAutosize(props, ref)
syncHeightWithFlushSycn();
}
});
const containerWindow = ownerWindow(inputRef.current);
containerWindow.addEventListener('resize', handleResize);
let resizeObserver;

if (typeof ResizeObserver !== 'undefined') {
resizeObserver = new ResizeObserver(handleResize);
resizeObserver.observe(inputRef.current);
let resizeObserver: ResizeObserver;
let containerWindow: Window;
if (inputRef.current) {
containerWindow = ownerWindow(inputRef.current);
containerWindow.addEventListener('resize', handleResize);

if (typeof ResizeObserver !== 'undefined') {
resizeObserver = new ResizeObserver(handleResize);
resizeObserver.observe(inputRef.current);
}
}

return () => {
handleResize.clear();
containerWindow.removeEventListener('resize', handleResize);
if (containerWindow) {
Copy link
Member

@oliviertassinari oliviertassinari Feb 10, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would be in favor of removing all the defensive checks. e.g. this one and if (inputRef.current) {. As far as I know, these are guaranteed to be defined even if TypeScript doesn't know it. We would ship less code.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 We haven't seen any bug reports related to not having these checks in the JS version, so it's safe to skip them here as well.

@sai6855 would you mind correcting this? The rest of the PR looks fine to me.

Copy link
Contributor Author

@sai6855 sai6855 Feb 15, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done, removed defensive checks , commit: cb83064 . cc @michaldudak

containerWindow.removeEventListener('resize', handleResize);
}
if (resizeObserver) {
resizeObserver.disconnect();
}
Expand All @@ -190,7 +234,7 @@ const TextareaAutosize = React.forwardRef(function TextareaAutosize(props, ref)
renders.current = 0;
}, [value]);

const handleChange = (event) => {
const handleChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
renders.current = 0;

if (!isControlled) {
Expand All @@ -209,12 +253,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={Number(minRows)}
oliviertassinari marked this conversation as resolved.
Show resolved Hide resolved
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}
Expand All @@ -238,7 +282,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
Expand Down Expand Up @@ -273,6 +317,6 @@ TextareaAutosize.propTypes /* remove-proptypes */ = {
PropTypes.number,
PropTypes.string,
]),
};
} as any;

export default TextareaAutosize;
Original file line number Diff line number Diff line change
Expand Up @@ -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;
2 changes: 0 additions & 2 deletions packages/mui-base/src/TextareaAutosize/index.d.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { default } from './TextareaAutosize';
export * from './TextareaAutosize.types';
Loading