-
-
Notifications
You must be signed in to change notification settings - Fork 32.8k
[Textarea] Refactor the implementation #15331
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
Merged
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
5bb2a81
[Textarea] Refactor the implementation
oliviertassinari 0449482
reduce bundle size
oliviertassinari 7a949e0
Revert "reduce bundle size"
oliviertassinari ec7ddb1
Sebastian review
oliviertassinari 415ec90
one textarea, smaller
oliviertassinari File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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 hidden or 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 |
|---|---|---|
| @@ -1,246 +1,156 @@ | ||
| import React from 'react'; | ||
| import PropTypes from 'prop-types'; | ||
| import clsx from 'clsx'; | ||
| import { useForkRef } from '../utils/reactHelpers'; | ||
| import debounce from 'debounce'; // < 1kb payload overhead when lodash/debounce is > 3kb. | ||
| import EventListener from 'react-event-listener'; | ||
| import withStyles from '../styles/withStyles'; | ||
| import { setRef } from '../utils/reactHelpers'; | ||
|
|
||
| const ROWS_HEIGHT = 19; | ||
|
|
||
| export const styles = { | ||
| /* Styles applied to the root element. */ | ||
| root: { | ||
| position: 'relative', // because the shadow has position: 'absolute', | ||
| width: '100%', | ||
| }, | ||
| textarea: { | ||
| width: '100%', | ||
| height: '100%', | ||
| resize: 'none', | ||
| font: 'inherit', | ||
| padding: 0, | ||
| cursor: 'inherit', | ||
| boxSizing: 'border-box', | ||
| lineHeight: 'inherit', | ||
| border: 'none', | ||
| outline: 'none', | ||
| background: 'transparent', | ||
| }, | ||
| shadow: { | ||
| // Overflow also needed to here to remove the extra row | ||
| // added to textareas in Firefox. | ||
| overflow: 'hidden', | ||
| // Visibility needed to hide the extra text area on iPads | ||
| visibility: 'hidden', | ||
| position: 'absolute', | ||
| height: 'auto', | ||
| whiteSpace: 'pre-wrap', | ||
| }, | ||
| }; | ||
|
|
||
| function getStyleValue(computedStyle, property) { | ||
| return parseInt(computedStyle[property], 10) || 0; | ||
| } | ||
|
|
||
| const useEnhancedEffect = typeof window !== 'undefined' ? React.useLayoutEffect : React.useEffect; | ||
|
|
||
| /** | ||
| * @ignore - internal component. | ||
| * | ||
| * To make public in v4+. | ||
| */ | ||
| class Textarea extends React.Component { | ||
| constructor(props) { | ||
| super(); | ||
| this.isControlled = props.value != null; | ||
| // <Input> expects the components it renders to respond to 'value' | ||
| // so that it can check whether they are filled. | ||
| this.value = props.value || props.defaultValue || ''; | ||
| this.state = { | ||
| height: Number(props.rows) * ROWS_HEIGHT, | ||
| }; | ||
| const Textarea = React.forwardRef(function Textarea(props, ref) { | ||
| const { onChange, rowsMax, rowsMin, style, value, ...other } = props; | ||
|
|
||
| if (typeof window !== 'undefined') { | ||
| this.handleResize = debounce(() => { | ||
| this.syncHeightWithShadow(); | ||
| }, 166); // Corresponds to 10 frames at 60 Hz. | ||
| } | ||
| } | ||
| const { current: isControlled } = React.useRef(value != null); | ||
| const inputRef = React.useRef(); | ||
| const [state, setState] = React.useState({}); | ||
| const handleRef = useForkRef(ref, inputRef); | ||
|
|
||
| componentDidMount() { | ||
| this.syncHeightWithShadow(); | ||
| } | ||
| const syncHeight = React.useCallback(() => { | ||
| const input = inputRef.current; | ||
| const savedValue = input.value; | ||
| const savedHeight = input.style.height; | ||
| const savedOverflow = input.style.overflow; | ||
|
|
||
| componentDidUpdate() { | ||
| this.syncHeightWithShadow(); | ||
| } | ||
| input.style.overflow = 'hidden'; | ||
| input.style.height = '0'; | ||
|
|
||
| componentWillUnmount() { | ||
| this.handleResize.clear(); | ||
| } | ||
| // The height of the inner content | ||
| input.value = savedValue || props.placeholder || 'x'; | ||
| const innerHeight = input.scrollHeight; | ||
|
|
||
| handleRefInput = ref => { | ||
| this.inputRef = ref; | ||
| const computedStyle = window.getComputedStyle(input); | ||
| const boxSizing = computedStyle['box-sizing']; | ||
|
|
||
| setRef(this.props.textareaRef, ref); | ||
| }; | ||
| // Measure height of a textarea with a single row | ||
| input.value = 'x'; | ||
| const singleRowHeight = input.scrollHeight; | ||
|
|
||
| handleRefSinglelineShadow = ref => { | ||
| this.singlelineShadowRef = ref; | ||
| }; | ||
|
|
||
| handleRefShadow = ref => { | ||
| this.shadowRef = ref; | ||
| }; | ||
| // The height of the outer content | ||
| let outerHeight = innerHeight; | ||
|
|
||
| handleChange = event => { | ||
| this.value = event.target.value; | ||
|
|
||
| if (!this.isControlled) { | ||
| // The component is not controlled, we need to update the shallow value. | ||
| this.shadowRef.value = this.value; | ||
| this.syncHeightWithShadow(); | ||
| if (rowsMin != null) { | ||
| outerHeight = Math.max(Number(rowsMin) * singleRowHeight, outerHeight); | ||
| } | ||
|
|
||
| if (this.props.onChange) { | ||
| this.props.onChange(event); | ||
| if (rowsMax != null) { | ||
| outerHeight = Math.min(Number(rowsMax) * singleRowHeight, outerHeight); | ||
| } | ||
| }; | ||
|
|
||
| syncHeightWithShadow() { | ||
| const props = this.props; | ||
|
|
||
| // Guarding for **broken** shallow rendering method that call componentDidMount | ||
| // but doesn't handle refs correctly. | ||
| // To remove once the shallow rendering has been fixed. | ||
| if (!this.shadowRef) { | ||
| return; | ||
| outerHeight = Math.max(outerHeight, singleRowHeight); | ||
|
|
||
| if (boxSizing === 'content-box') { | ||
| outerHeight -= | ||
| getStyleValue(computedStyle, 'padding-bottom') + | ||
| getStyleValue(computedStyle, 'padding-top'); | ||
| } else if (boxSizing === 'border-box') { | ||
| outerHeight += | ||
| getStyleValue(computedStyle, 'border-bottom-width') + | ||
| getStyleValue(computedStyle, 'border-top-width'); | ||
| } | ||
|
|
||
| if (this.isControlled) { | ||
| // The component is controlled, we need to update the shallow value. | ||
| this.shadowRef.value = props.value == null ? '' : String(props.value); | ||
| } | ||
|
|
||
| let lineHeight = this.singlelineShadowRef.scrollHeight; | ||
| // The Textarea might not be visible (p.ex: display: none). | ||
| // In this case, the layout values read from the DOM will be 0. | ||
| lineHeight = lineHeight === 0 ? ROWS_HEIGHT : lineHeight; | ||
| input.style.overflow = savedOverflow; | ||
| input.style.height = savedHeight; | ||
| input.value = savedValue; | ||
|
|
||
| setState(prevState => { | ||
| // Need a large enough different to update the height. | ||
| // This prevents infinite rendering loop. | ||
| if (innerHeight > 0 && Math.abs((prevState.innerHeight || 0) - innerHeight) > 1) { | ||
| return { | ||
| innerHeight, | ||
| outerHeight, | ||
| }; | ||
| } | ||
|
|
||
| return prevState; | ||
| }); | ||
| }, [setState, rowsMin, rowsMax, props.placeholder]); | ||
|
|
||
| React.useEffect(() => { | ||
| const handleResize = debounce(() => { | ||
| syncHeight(); | ||
| }, 166); // Corresponds to 10 frames at 60 Hz. | ||
|
|
||
| window.addEventListener('resize', handleResize); | ||
| return () => { | ||
| handleResize.clear(); | ||
| window.removeEventListener('resize', handleResize); | ||
| }; | ||
| }, [syncHeight]); | ||
|
|
||
| let newHeight = this.shadowRef.scrollHeight; | ||
| useEnhancedEffect(() => { | ||
| syncHeight(); | ||
| }); | ||
|
|
||
| // Guarding for jsdom, where scrollHeight isn't present. | ||
| // See https://github.com/tmpvar/jsdom/issues/1013 | ||
| if (newHeight === undefined) { | ||
| return; | ||
| const handleChange = event => { | ||
| if (!isControlled) { | ||
| syncHeight(); | ||
| } | ||
|
|
||
| if (Number(props.rowsMax) >= Number(props.rows)) { | ||
| newHeight = Math.min(Number(props.rowsMax) * lineHeight, newHeight); | ||
| if (onChange) { | ||
| onChange(event); | ||
| } | ||
| }; | ||
|
|
||
| newHeight = Math.max(newHeight, lineHeight); | ||
|
|
||
| // Need a large enough different to update the height. | ||
| // This prevents infinite rendering loop. | ||
| if (Math.abs(this.state.height - newHeight) > 1) { | ||
| this.setState({ | ||
| height: newHeight, | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| render() { | ||
| const { | ||
| classes, | ||
| className, | ||
| defaultValue, | ||
| onChange, | ||
| rows, | ||
| rowsMax, | ||
| style, | ||
| textareaRef, | ||
| value, | ||
| ...other | ||
| } = this.props; | ||
|
|
||
| return ( | ||
| <div className={classes.root}> | ||
| <EventListener target="window" onResize={this.handleResize} /> | ||
| <textarea | ||
| aria-hidden="true" | ||
| className={clsx(classes.textarea, classes.shadow)} | ||
| readOnly | ||
| ref={this.handleRefSinglelineShadow} | ||
| rows="1" | ||
| tabIndex={-1} | ||
| value="" | ||
| /> | ||
| <textarea | ||
| aria-hidden="true" | ||
| className={clsx(classes.textarea, classes.shadow)} | ||
| defaultValue={defaultValue} | ||
| readOnly | ||
| ref={this.handleRefShadow} | ||
| rows={rows} | ||
| tabIndex={-1} | ||
| value={value} | ||
| /> | ||
| <textarea | ||
| rows={rows} | ||
| className={clsx(classes.textarea, className)} | ||
| defaultValue={defaultValue} | ||
| value={value} | ||
| onChange={this.handleChange} | ||
| ref={this.handleRefInput} | ||
| style={{ height: this.state.height, ...style }} | ||
| {...other} | ||
| /> | ||
| </div> | ||
| ); | ||
| } | ||
| } | ||
| return ( | ||
| <textarea | ||
| value={value} | ||
| onChange={handleChange} | ||
| ref={handleRef} | ||
| style={{ | ||
| height: state.outerHeight, | ||
| overflow: state.outerHeight === state.innerHeight ? 'hidden' : null, | ||
| ...style, | ||
| }} | ||
| {...other} | ||
| /> | ||
| ); | ||
| }); | ||
|
|
||
| Textarea.propTypes = { | ||
| /** | ||
| * Override or extend the styles applied to the component. | ||
| * See [CSS API](#css) below for more details. | ||
| */ | ||
| classes: PropTypes.object.isRequired, | ||
| /** | ||
| * @ignore | ||
| */ | ||
| className: PropTypes.string, | ||
| /** | ||
| * @ignore | ||
| */ | ||
| defaultValue: PropTypes.any, | ||
| /** | ||
| * @ignore | ||
| */ | ||
| disabled: PropTypes.bool, | ||
| /** | ||
| * @ignore | ||
| */ | ||
| onChange: PropTypes.func, | ||
| /** | ||
| * Number of rows to display when multiline option is set to true. | ||
| * @ignore | ||
| */ | ||
| rows: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), | ||
| placeholder: PropTypes.string, | ||
| /** | ||
| * Maximum number of rows to display when multiline option is set to true. | ||
| */ | ||
| rowsMax: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), | ||
| /** | ||
| * @ignore | ||
| * Minimum number of rows to display when multiline option is set to true. | ||
| */ | ||
| style: PropTypes.object, | ||
| rowsMin: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), | ||
| /** | ||
| * @deprecated | ||
| * Use that property to pass a ref callback to the native textarea element. | ||
| * @ignore | ||
| */ | ||
| textareaRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), | ||
| style: PropTypes.object, | ||
| /** | ||
| * @ignore | ||
| */ | ||
| value: PropTypes.any, | ||
| }; | ||
|
|
||
| Textarea.defaultProps = { | ||
| rows: 1, | ||
| }; | ||
|
|
||
| export default withStyles(styles, { name: 'MuiPrivateTextarea' })(Textarea); | ||
| export default Textarea; | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.