Skip to content

Commit

Permalink
[Input][base] Pass the rows prop to the underlying textarea (mui#33873)
Browse files Browse the repository at this point in the history
  • Loading branch information
michaldudak authored and Daniel Rabe committed Nov 29, 2022
1 parent 7577ca6 commit 9659a39
Show file tree
Hide file tree
Showing 3 changed files with 152 additions and 92 deletions.
32 changes: 32 additions & 0 deletions packages/mui-base/src/InputUnstyled/InputUnstyled.test.tsx
Expand Up @@ -30,4 +30,36 @@ describe('<InputUnstyled />', () => {

expect(screen.getByRole('textbox')).to.have.tagName('textarea');
});

describe('prop: multiline', () => {
it('should pass the rows prop to the underlying textarea when multiline=true', () => {
const { getByRole } = render(<InputUnstyled multiline rows={5} />);
expect(getByRole('textbox')).to.have.attribute('rows', '5');
});

it('should not pass the minRows or maxRows prop to the underlying textarea slot when default host component is used', () => {
const { getByRole } = render(<InputUnstyled multiline minRows={5} maxRows={10} />);
expect(getByRole('textbox')).not.to.have.attribute('minRows');
expect(getByRole('textbox')).not.to.have.attribute('maxRows');
});

it('should pass the minRows or maxRows prop to the underlying textarea slot if a custom component is used', () => {
const CustomTextarea = React.forwardRef(
({ minRows, maxRows, ownerState, ...other }: any, ref) => {
expect(minRows).to.equal(5);
expect(maxRows).to.equal(10);
return <textarea {...other} ref={ref} />;
},
);

render(
<InputUnstyled
multiline
minRows={5}
maxRows={10}
components={{ Textarea: CustomTextarea }}
/>,
);
});
});
});
40 changes: 19 additions & 21 deletions packages/mui-base/src/InputUnstyled/InputUnstyled.tsx
Expand Up @@ -42,8 +42,6 @@ const InputUnstyled = React.forwardRef(function InputUnstyled(
endAdornment,
error,
id,
maxRows,
minRows,
multiline = false,
name,
onClick,
Expand All @@ -55,10 +53,12 @@ const InputUnstyled = React.forwardRef(function InputUnstyled(
placeholder,
readOnly,
required,
rows,
type = 'text',
startAdornment,
value,
type: typeProp,
rows,
minRows,
maxRows,
...other
} = props;

Expand All @@ -81,6 +81,8 @@ const InputUnstyled = React.forwardRef(function InputUnstyled(
value,
});

const type = !multiline ? typeProp ?? 'text' : undefined;

const ownerState: InputUnstyledOwnerState = {
...props,
disabled: disabledState,
Expand Down Expand Up @@ -134,38 +136,34 @@ const InputUnstyled = React.forwardRef(function InputUnstyled(
className: [classes.root, rootStateClasses, className],
});

let Input = components.Input ?? 'input';
let inputProps: WithOptionalOwnerState<InputUnstyledInputSlotProps> = useSlotProps({
const Input = multiline ? components.Textarea ?? 'textarea' : components.Input ?? 'input';
const inputProps: WithOptionalOwnerState<InputUnstyledInputSlotProps> = useSlotProps({
elementType: Input,
getSlotProps: (otherHandlers: EventHandlers) =>
getInputProps({ ...otherHandlers, ...propsToForward }),
externalSlotProps: componentsProps.input,
additionalProps: {
rows: multiline ? rows : undefined,
...(multiline &&
!isHostComponent(Input) && {
minRows: rows || minRows,
maxRows: rows || maxRows,
}),
},
ownerState,
className: [classes.input, inputStateClasses],
});

if (multiline) {
const hasHostTextarea = isHostComponent(components.Textarea ?? 'textarea');

const { ownerState: ownerStateInputProps, ...inputPropsWithoutOwnerState } = inputProps;

if (rows) {
if (process.env.NODE_ENV !== 'production') {
if (process.env.NODE_ENV !== 'production') {
if (multiline) {
if (rows) {
if (minRows || maxRows) {
console.warn(
'MUI: You can not use the `minRows` or `maxRows` props when the input `rows` prop is set.',
);
}
}
}

inputProps = {
...(!hasHostTextarea && { minRows: rows || minRows, maxRows: rows || maxRows }),
...(hasHostTextarea ? inputPropsWithoutOwnerState : inputProps),
type: undefined,
};

Input = components.Textarea ?? 'textarea';
}

return (
Expand Down
172 changes: 101 additions & 71 deletions packages/mui-base/src/InputUnstyled/InputUnstyled.types.ts
Expand Up @@ -6,105 +6,134 @@ import { SlotComponentProps } from '../utils';

export interface InputUnstyledComponentsPropsOverrides {}

export interface InputUnstyledOwnProps extends UseInputParameters {
'aria-describedby'?: string;
'aria-label'?: string;
'aria-labelledby'?: string;
export interface SingleLineInputUnstyledProps {
/**
* This prop helps users to fill forms faster, especially on mobile devices.
* The name can be confusing, as it's more like an autofill.
* You can learn more about it [following the specification](https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofill).
* Maximum number of rows to display when multiline option is set to true.
*/
autoComplete?: string;
maxRows?: undefined;
/**
* If `true`, the `input` element is focused during the first mount.
* Minimum number of rows to display when multiline option is set to true.
*/
autoFocus?: boolean;
minRows?: undefined;
/**
* Class name applied to the root element.
* If `true`, a `textarea` element is rendered.
* @default false
*/
className?: string;
multiline?: false;
/**
* The components used for each slot inside the InputBase.
* Either a string to use a HTML element or a component.
* @default {}
* Number of rows to display when multiline option is set to true.
*/
components?: {
Root?: React.ElementType;
Input?: React.ElementType;
Textarea?: React.ElementType;
};
rows?: undefined;
/**
* The props used for each slot inside the Input.
* @default {}
* Type of the `input` element. It should be [a valid HTML5 input type](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#Form_%3Cinput%3E_types).
* @default 'text'
*/
componentsProps?: {
root?: SlotComponentProps<
'div',
InputUnstyledComponentsPropsOverrides,
InputUnstyledOwnerState
>;
input?: SlotComponentProps<
'input',
InputUnstyledComponentsPropsOverrides,
InputUnstyledOwnerState
>;
};
type?: React.HTMLInputTypeAttribute;
}

export interface MultiLineInputUnstyledProps {
/**
* Trailing adornment for this input.
* Maximum number of rows to display when multiline option is set to true.
*/
endAdornment?: React.ReactNode;
maxRows?: number;
/**
* The id of the `input` element.
* Minimum number of rows to display when multiline option is set to true.
*/
id?: string;
minRows?: number;
/**
* If `true`, a `textarea` element is rendered.
* @default false
*/
multiline?: boolean;
/**
* Name attribute of the `input` element.
*/
name?: string;
onKeyDown?: React.KeyboardEventHandler<HTMLInputElement>;
onKeyUp?: React.KeyboardEventHandler<HTMLInputElement>;
/**
* The short hint displayed in the `input` before the user enters a value.
*/
placeholder?: string;
/**
* It prevents the user from changing the value of the field
* (not from interacting with the field).
*/
readOnly?: boolean;
multiline: true;
/**
* Number of rows to display when multiline option is set to true.
*/
rows?: number;
/**
* Leading adornment for this input.
*/
startAdornment?: React.ReactNode;
/**
* Minimum number of rows to display when multiline option is set to true.
*/
minRows?: number;
/**
* Maximum number of rows to display when multiline option is set to true.
*/
maxRows?: number;
/**
* Type of the `input` element. It should be [a valid HTML5 input type](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#Form_%3Cinput%3E_types).
* @default 'text'
*/
type?: React.HTMLInputTypeAttribute;
/**
* The value of the `input` element, required for a controlled component.
*/
value?: unknown;
type?: undefined;
}

export type InputUnstyledOwnProps = (SingleLineInputUnstyledProps | MultiLineInputUnstyledProps) &
UseInputParameters & {
'aria-describedby'?: string;
'aria-label'?: string;
'aria-labelledby'?: string;
/**
* This prop helps users to fill forms faster, especially on mobile devices.
* The name can be confusing, as it's more like an autofill.
* You can learn more about it [following the specification](https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofill).
*/
autoComplete?: string;
/**
* If `true`, the `input` element is focused during the first mount.
*/
autoFocus?: boolean;
/**
* Class name applied to the root element.
*/
className?: string;
/**
* The components used for each slot inside the InputBase.
* Either a string to use a HTML element or a component.
* @default {}
*/
components?: {
Root?: React.ElementType;
Input?: React.ElementType;
Textarea?: React.ElementType;
};
/**
* The props used for each slot inside the Input.
* @default {}
*/
componentsProps?: {
root?: SlotComponentProps<
'div',
InputUnstyledComponentsPropsOverrides,
InputUnstyledOwnerState
>;
input?: SlotComponentProps<
'input',
InputUnstyledComponentsPropsOverrides,
InputUnstyledOwnerState
>;
};
/**
* Trailing adornment for this input.
*/
endAdornment?: React.ReactNode;
/**
* The id of the `input` element.
*/
id?: string;
/**
* Name attribute of the `input` element.
*/
name?: string;
onKeyDown?: React.KeyboardEventHandler<HTMLInputElement>;
onKeyUp?: React.KeyboardEventHandler<HTMLInputElement>;
/**
* The short hint displayed in the `input` before the user enters a value.
*/
placeholder?: string;
/**
* It prevents the user from changing the value of the field
* (not from interacting with the field).
*/
readOnly?: boolean;
/**
* Leading adornment for this input.
*/
startAdornment?: React.ReactNode;
/**
* The value of the `input` element, required for a controlled component.
*/
value?: unknown;
};

export interface InputUnstyledTypeMap<P = {}, D extends React.ElementType = 'div'> {
props: P & InputUnstyledOwnProps;
defaultComponent: D;
Expand All @@ -121,6 +150,7 @@ export type InputUnstyledOwnerState = Simplify<
Omit<InputUnstyledProps, 'component' | 'components' | 'componentsProps'> & {
formControlContext: FormControlUnstyledState | undefined;
focused: boolean;
type: React.InputHTMLAttributes<HTMLInputElement>['type'] | undefined;
}
>;

Expand Down

0 comments on commit 9659a39

Please sign in to comment.