Repository: react-materialize/react-materialize
Pull request #727 (2018-12-16 15:34:55)

3.0.0 textarea
Implement textarea


Patch:
diff --git a/src/Select.js b/src/Select.js
deleted file mode 100644
index c5c0122..0000000
--- a/src/Select.js
+++ /dev/null
@@ -1,196 +0,0 @@
-import React, { Component } from 'react';
-import PropTypes from 'prop-types';
-import cx from 'classnames';
-import idgen from './idgen';
-import constants from './constants';
-
-class Select extends Component {
-  constructor(props) {
-    super(props);
-
-    this.id = props.id || idgen();
-    this.handleChange = this.handleChange.bind(this);
-    this.state = { value: props.value };
-  }
-
-  handleChange(event) {
-    const { onChange } = this.props;
-    let value = event.target.value;
-
-    if (this.props.multiple) {
-      value = this.instance.getSelectedValues();
-    }
-
-    if (onChange) onChange(event);
-
-    this.setState({ value });
-  }
-
-  componentDidMount() {
-    if (typeof M !== 'undefined') {
-      this.instance = M.FormSelect.init(this._selectRef);
-    }
-  }
-
-  render() {
-    const {
-      s,
-      m,
-      l,
-      disabled,
-      noLayout,
-      browserDefault,
-      icon,
-      label,
-      selectClassName,
-      success,
-      error,
-      validate,
-      children,
-      multiple,
-      value,
-      type
-    } = this.props;
-
-    const sizes = { s, m, l };
-
-    let responsiveClasses;
-    if (!noLayout) {
-      responsiveClasses = { col: true };
-      constants.SIZES.forEach(size => {
-        responsiveClasses[size + sizes[size]] = sizes[size];
-      });
-    }
-
-    const wrapperClasses = cx('input-field', responsiveClasses);
-
-    const selectProps = {
-      type: 'select',
-      id: this.id,
-      value: this.state.value,
-      disabled,
-      multiple
-    };
-
-    const renderLabel = () =>
-      label && (
-        <label
-          data-success={success}
-          data-error={error}
-          htmlFor={selectProps.id}
-        >
-          {label}
-        </label>
-      );
-
-    const renderIcon = () =>
-      icon && <i className="material-icons prefix">{icon}</i>;
-
-    const renderOption = child =>
-      React.cloneElement(child, { key: child.props.value });
-
-    const renderOptions = () => React.Children.map(children, renderOption);
-
-    return (
-      <div className={wrapperClasses}>
-        {renderIcon()}
-        <select
-          value={this.state.value}
-          ref={el => {
-            this._selectRef = el;
-          }}
-          onChange={this.handleChange}
-          className={cx(
-            {
-              validate,
-              multiple,
-              ['browser-default']: browserDefault
-            },
-            selectClassName
-          )}
-          {...selectProps}
-        >
-          {renderOptions()}
-        </select>
-        {renderLabel()}
-      </div>
-    );
-  }
-}
-
-Select.propTypes = {
-  /*
-   * Use browser default styles
-   */
-  browserDefault: PropTypes.bool,
-  /*
-   * Strip away all layout classes such as col and sX
-   */
-  noLayout: PropTypes.bool,
-  /*
-   * Responsive size for Small
-   */
-  s: PropTypes.number,
-  /*
-   * Responsive size for Medium
-   */
-  m: PropTypes.number,
-  /*
-   * Responsive size for Large
-   */
-  l: PropTypes.number,
-  /*
-   * disabled input
-   */
-  disabled: PropTypes.bool,
-  /*
-   * override id
-   * @default idgen()
-   */
-  id: PropTypes.string,
-  /*
-   * prefix icon
-   */
-  icon: PropTypes.string,
-  /*
-   * label text
-   */
-  label: PropTypes.string,
-  /*
-   * Input initial value
-   */
-  value: PropTypes.string,
-  /*
-   * Add validate class to input
-   */
-  validate: PropTypes.bool,
-  /*
-   * Custom success message
-   */
-  success: PropTypes.string,
-  /*
-   * Custom error message
-   */
-  error: PropTypes.string,
-  /*
-   * Additional classes for input
-   */
-  selectClassName: PropTypes.string,
-  /*
-   * override type="text"
-   */
-  type: PropTypes.string,
-  /*
-   * onChange callback
-   */
-  onChange: PropTypes.func,
-  /*
-   * Render a multiple dropdown,
-   * use instance.getSelectedValues()
-   * to get array of selected values
-   */
-  multiple: PropTypes.bool,
-  children: PropTypes.any
-};
-
-export default Select;
diff --git a/src/TextInput.js b/src/TextInput.js
index 8c64784..c92e46a 100644
--- a/src/TextInput.js
+++ b/src/TextInput.js
@@ -7,16 +7,20 @@ import constants from './constants';
 class TextInput extends Component {
   constructor(props) {
     super(props);
+    this.state = {
+      value: ''
+    };
 
     this.id = props.id || idgen();
+    this.handleChange = this.handleChange.bind(this);
+  }
 
-    if (props.password) {
-      this.id = 'password';
-    }
+  handleChange(event) {
+    const { target: { value } } = event;
+    const { onChange } = this.props;
+    if (onChange) onChange(event);
 
-    if (props.email) {
-      this.id = 'email';
-    }
+    this.setState({ value });
   }
 
   componentDidUpdate(prevProps) {
@@ -41,8 +45,6 @@ class TextInput extends Component {
       inputClassName,
       success,
       error,
-      password,
-      email,
       validate,
       value,
       type
@@ -60,29 +62,17 @@ class TextInput extends Component {
 
     const wrapperClasses = cx('input-field', responsiveClasses);
 
-    let computedType;
-    if (type) {
-      computedType = type;
-    } else if (password) {
-      computedType = 'password';
-    } else if (email) {
-      computedType = 'email';
-    } else {
-      computedType = 'text';
-    }
-
     const inputProps = {
       placeholder,
-      type: computedType,
+      type: type || 'text',
       id: this.id,
-      defaultValue: value,
+      value: value || this.state.value,
       disabled
     };
 
     const renderLabel = () =>
       label && (
         <label
-          className={cx({ active: value || placeholder })}
           data-success={success}
           data-error={error}
           htmlFor={inputProps.id}
@@ -91,15 +81,6 @@ class TextInput extends Component {
         </label>
       );
 
-    const renderHelper = () =>
-      [error || success] && (
-        <span
-          className="helper-text"
-          data-error={error}
-          data-success={success}
-        />
-      );
-
     const renderIcon = () =>
       icon && <i className="material-icons prefix">{icon}</i>;
 
@@ -115,7 +96,6 @@ class TextInput extends Component {
           {...inputProps}
         />
         {renderLabel()}
-        {renderHelper()}
       </div>
     );
   }
@@ -190,15 +170,7 @@ TextInput.propTypes = {
   /*
    * onChange callback
    */
-  onChange: PropTypes.func,
-  /*
-   * password type
-   */
-  password: PropTypes.bool,
-  /*
-   * email type
-   */
-  email: PropTypes.bool
+  onChange: PropTypes.func
 };
 
 export default TextInput;
diff --git a/src/Textarea.js b/src/Textarea.js
new file mode 100644
index 0000000..2cf7cc0
--- /dev/null
+++ b/src/Textarea.js
@@ -0,0 +1,71 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import idgen from './idgen';
+import Icon from './Icon';
+import TextInput from './TextInput';
+
+class Textarea extends Component {
+  componentDidUpdate() {
+    M.textareaAutoResize(this._textarea);
+  }
+
+  renderIcon(icon) {
+    return <Icon className="material-icons prefix">{icon}</Icon>;
+  }
+
+  render() {
+    const { iconClassName, icon, label, id, onChange, ...other } = this.props;
+
+    const computedId = id || idgen();
+
+    return (
+      <div className="input-field">
+        {icon && this.renderIcon(icon, iconClassName)}
+        <textarea
+          ref={input => {
+            this._textarea = input;
+          }}
+          onChange={onChange}
+          id={computedId}
+          className="materialize-textarea"
+          {...other}
+        />
+        <label htmlFor={computedId}>{label}</label>
+      </div>
+    );
+  }
+}
+
+Textarea.propTypes = {
+  /*
+   * render icon next to input
+   */
+  icon: PropTypes.string,
+  /*
+   * icon classname
+   */
+  iconClassName: PropTypes.string,
+  /*
+   * textarea label
+   */
+  label: PropTypes.string,
+  /*
+   * onChange callback
+   */
+  onChange: PropTypes.func,
+  /*
+   * override id
+   * @default idgen()
+   */
+  id: PropTypes.string,
+  /*
+   * disabled input
+   */
+  disabled: PropTypes.bool,
+  /*
+  * predefined value
+  */
+  value: PropTypes.string
+};
+
+export default Textarea;
diff --git a/src/index.js b/src/index.js
index 721685e..9c4c9f0 100644
--- a/src/index.js
+++ b/src/index.js
@@ -33,13 +33,13 @@ import SearchForm from './SearchForm';
 import Section from './Section';
 import SideNav from './SideNav';
 import SideNavItem from './SideNavItem';
-import Select from './Select';
 import Slide from './Slide';
 import Slider from './Slider';
 import Tab from './Tab';
 import Table from './Table';
 import Tabs from './Tabs';
 import Tag from './Tag';
+import Textarea from './Textarea';
 import TextInput from './TextInput';
 import Toast from './Toast';
 
@@ -79,13 +79,13 @@ export {
   Section,
   SideNav,
   SideNavItem,
-  Select,
   Slide,
   Slider,
   Tab,
   Table,
   Tabs,
   Tag,
+  Textarea,
   TextInput,
   Toast
 };
diff --git a/stories/Checkbox.stories.js b/stories/Checkbox.stories.js
index 5b489bd..f162bb7 100644
--- a/stories/Checkbox.stories.js
+++ b/stories/Checkbox.stories.js
@@ -2,7 +2,7 @@ import React from 'react';
 import { storiesOf } from '@storybook/react';
 import Checkbox from '../src/Checkbox';
 
-const stories = storiesOf('components/Checkbox', module);
+const stories = storiesOf('javascript/Checkbox', module);
 
 stories.addParameters({
   info: {
diff --git a/stories/Select.stories.js b/stories/Select.stories.js
deleted file mode 100644
index 79eddec..0000000
--- a/stories/Select.stories.js
+++ /dev/null
@@ -1,53 +0,0 @@
-import React from 'react';
-import { storiesOf } from '@storybook/react';
-import Select from '../src/Select';
-
-const stories = storiesOf('components/Select', module);
-
-stories.addParameters({
-  info: {
-    text: `Select allows user input through specified options.`
-  }
-});
-
-stories.add('Default', () => (
-  <Select>
-    <option value="" disabled selected>Choose your option</option>
-    <option value="1">Option 1</option>
-    <option value="2">Option 2</option>
-    <option value="3">Option 3</option>
-  </Select>
-));
-
-stories.add('Multiple', () => (
-  <Select multiple>
-    <option value="" disabled selected>Choose your option</option>
-    <option value="1">Option 1</option>
-    <option value="2">Option 2</option>
-    <option value="3">Option 3</option>
-  </Select>
-));
-
-stories.add('Label', () => (
-  <Select label="Choose your option">
-    <option value="1">Option 1</option>
-    <option value="2" selected>Option 2</option>
-    <option value="3">Option 3</option>
-  </Select>
-));
-
-stories.add('Disabled', () => (
-  <Select disabled>
-    <option value="1">Option 1</option>
-    <option value="2" selected>Option 2</option>
-    <option value="3">Option 3</option>
-  </Select>
-));
-
-stories.add('Browser default', () => (
-  <Select browserDefault>
-    <option value="1">Option 1</option>
-    <option value="2" selected>Option 2</option>
-    <option value="3">Option 3</option>
-  </Select>
-));
diff --git a/stories/TextInput.stories.js b/stories/TextInput.stories.js
deleted file mode 100644
index d48974c..0000000
--- a/stories/TextInput.stories.js
+++ /dev/null
@@ -1,52 +0,0 @@
-import React from 'react';
-import { storiesOf } from '@storybook/react';
-import TextInput from '../src/TextInput';
-
-const stories = storiesOf('components/TextInput', module);
-
-stories.addParameters({
-  info: {
-    text: `Text fields allow user input. The border should light
-     up simply and clearly indicating which field the user is 
-     currently editing.
-     The validate prop leverages HTML5 validation and will add
-      a valid and invalid class accordingly. If you don't want 
-      the Green and Red validation states, just remove the validate prop.`
-  }
-});
-
-stories.add('Default', () => (
-  <TextInput />
-));
-
-stories.add('Label', () => (
-  <TextInput label="First Name" />
-));
-
-stories.add('Placeholder', () => (
-  <TextInput placeholder="First Name" />
-));
-
-stories.add('Predefined value', () => (
-  <TextInput value="John" />
-));
-
-stories.add('Disabled', () => (
-  <TextInput disabled value="John" />
-));
-
-stories.add('Password', () => (
-  <TextInput password label="Password" />
-));
-
-stories.add('Email validate', () => (
-  <TextInput email validate label="Email" />
-));
-
-stories.add('with custom error/success', () => (
-  <TextInput email validate label="Email" error="Wrong Email sir" success="Great" />
-));
-
-stories.add('with Icon', () => (
-  <TextInput icon="email" label="Email" />
-));
diff --git a/stories/Textarea.stories.js b/stories/Textarea.stories.js
new file mode 100644
index 0000000..2880274
--- /dev/null
+++ b/stories/Textarea.stories.js
@@ -0,0 +1,24 @@
+import React from 'react';
+import { storiesOf } from '@storybook/react';
+import Textarea from '../src/Textarea';
+
+const stories = storiesOf('components/Textarea', module);
+
+stories.addParameters({
+  info: {
+    text: `Textareas allow larger expandable user input. 
+    The border should light up simply and clearly 
+    indicating which field the user is currently editing.
+    Textareas will auto resize to the text inside.`
+  }
+});
+
+stories.add('Textarea', () => <Textarea />);
+
+stories.add('Disabled', () => <Textarea value="I was here" disabled />);
+
+stories.add('Placeholder', () => <Textarea placeholder="I was here" />);
+
+stories.add('Label', () => <Textarea label="Write something here..." />);
+
+stories.add('Icon', () => <Textarea icon="mode_edit" />);


Test patch:
diff --git a/test/Select.spec.js b/test/Select.spec.js
deleted file mode 100644
index a169c26..0000000
--- a/test/Select.spec.js
+++ /dev/null
@@ -1,98 +0,0 @@
-import React from 'react';
-import { shallow, mount } from 'enzyme';
-import Select from '../src/Select';
-import mocker from './helper/new-mocker';
-
-describe('<Select />', () => {
-  let wrapper;
-
-  test('handles layout classes', () => {
-    wrapper = shallow(<Select s={4} m={6} />);
-
-    expect(wrapper.hasClass('input-field')).toBeTruthy();
-    expect(wrapper.hasClass('col')).toBeTruthy();
-    expect(wrapper.hasClass('s4')).toBeTruthy();
-    expect(wrapper.hasClass('m6')).toBeTruthy();
-  });
-
-  test('handles removing layout classes', () => {
-    wrapper = shallow(<Select s={4} m={6} noLayout />);
-
-    expect(wrapper.hasClass('input-field')).toBeTruthy();
-    expect(wrapper.hasClass('col')).toBeFalsy();
-    expect(wrapper.hasClass('s4')).toBeFalsy();
-    expect(wrapper.hasClass('m6')).toBeFalsy();
-  });
-
-  test('renders browserDefault', () => {
-    wrapper = shallow(<Select browserDefault />);
-    expect(wrapper.find('select').hasClass('browser-default')).toBeTruthy();
-  });
-
-  test('renders label', () => {
-    wrapper = shallow(<Select label="Name" />);
-    expect(wrapper.find('label').text()).toEqual('Name');
-  });
-
-  test('handles validate', () => {
-    wrapper = shallow(<Select validate />);
-    expect(wrapper.find('select.validate')).toHaveLength(1);
-  });
-
-  test('handles selectClassName', () => {
-    wrapper = shallow(<Select validate selectClassName={'hello red'} />);
-    expect(wrapper.find('select.hello.red.validate')).toHaveLength(1);
-  });
-
-  test('calls onChange if provided', () => {
-    wrapper = shallow(<Select />);
-    wrapper.setState({ value: 'hello' });
-    expect(wrapper.find('select').prop('value')).toEqual('hello');
-  });
-
-  test('with icon', () => {
-    wrapper = shallow(<Select icon="cloud" />);
-    expect(wrapper.find('i.material-icons.prefix')).toHaveLength(1);
-  });
-
-  test('renders options', () => {
-    wrapper = shallow(
-      <Select>
-        <option value={1}>1</option>
-        <option value={2}>2</option>
-      </Select>
-    );
-
-    expect(wrapper.find('option')).toHaveLength(2);
-  });
-
-  test('renders multiple', () => {
-    wrapper = shallow(<Select multiple />);
-    expect(wrapper.find('select.multiple')).toHaveLength(1);
-  });
-
-  describe('initialises', () => {
-    const selectInitMock = jest.fn();
-    const selectInstanceDestroyMock = jest.fn();
-    const selectMock = {
-      init: (el, options) => {
-        selectInitMock(options);
-        return {
-          destroy: selectInstanceDestroyMock
-        };
-      }
-    };
-    const restore = mocker('FormSelect', selectMock);
-
-    beforeEach(() => {
-      selectInitMock.mockClear();
-      selectInstanceDestroyMock.mockClear();
-    });
-
-    test('calls FormSelect', () => {
-      mount(<Select />);
-
-      expect(selectInitMock).toHaveBeenCalledTimes(1);
-    });
-  });
-});
diff --git a/test/TextInput.spec.js b/test/TextInput.spec.js
index 8d4eea9..4a082d8 100644
--- a/test/TextInput.spec.js
+++ b/test/TextInput.spec.js
@@ -44,10 +44,16 @@ describe('<TextInput />', () => {
   });
 
   test('handles other types', () => {
-    wrapper = shallow(<TextInput password />);
+    wrapper = shallow(<TextInput type="password" />);
     expect(wrapper.find('input').prop('type')).toEqual('password');
   });
 
+  test('calls onChange if provided', () => {
+    wrapper = shallow(<TextInput />);
+    wrapper.setState({ value: 'hello' });
+    expect(wrapper.find('input').prop('value')).toEqual('hello');
+  });
+
   test('with icon', () => {
     wrapper = shallow(<TextInput icon="cloud" />);
     expect(wrapper.find('i.material-icons.prefix')).toHaveLength(1);
diff --git a/test/Textarea.spec.js b/test/Textarea.spec.js
new file mode 100644
index 0000000..a3785eb
--- /dev/null
+++ b/test/Textarea.spec.js
@@ -0,0 +1,32 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import Textarea from '../src/Textarea';
+
+describe('<Textarea />', () => {
+  let wrapper;
+
+  test('Textarea', () => {
+    wrapper = shallow(<Textarea />);
+    expect(wrapper).toMatchSnapshot();
+  });
+
+  test('Disabled', () => {
+    wrapper = shallow(<Textarea value="I was here" disabled />);
+    expect(wrapper).toMatchSnapshot();
+  });
+
+  test('Placeholder', () => {
+    wrapper = shallow(<Textarea placeholder="I was here" />);
+    expect(wrapper).toMatchSnapshot();
+  });
+
+  test('Label', () => {
+    wrapper = shallow(<Textarea label="Write something here..." />);
+    expect(wrapper).toMatchSnapshot();
+  });
+
+  test('Icon', () => {
+    wrapper = shallow(<Textarea icon="mode_edit" />);
+    expect(wrapper).toMatchSnapshot();
+  });
+});
diff --git a/test/__snapshots__/Textarea.spec.js.snap b/test/__snapshots__/Textarea.spec.js.snap
new file mode 100644
index 0000000..fff9088
--- /dev/null
+++ b/test/__snapshots__/Textarea.spec.js.snap
@@ -0,0 +1,81 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`<Textarea /> Disabled 1`] = `
+<div
+  className="input-field"
+>
+  <textarea
+    className="materialize-textarea"
+    disabled={true}
+    id="mockId"
+    value="I was here"
+  />
+  <label
+    htmlFor="mockId"
+  />
+</div>
+`;
+
+exports[`<Textarea /> Icon 1`] = `
+<div
+  className="input-field"
+>
+  <Icon
+    className="material-icons prefix"
+  >
+    mode_edit
+  </Icon>
+  <textarea
+    className="materialize-textarea"
+    id="mockId"
+  />
+  <label
+    htmlFor="mockId"
+  />
+</div>
+`;
+
+exports[`<Textarea /> Label 1`] = `
+<div
+  className="input-field"
+>
+  <textarea
+    className="materialize-textarea"
+    id="mockId"
+  />
+  <label
+    htmlFor="mockId"
+  >
+    Write something here...
+  </label>
+</div>
+`;
+
+exports[`<Textarea /> Placeholder 1`] = `
+<div
+  className="input-field"
+>
+  <textarea
+    className="materialize-textarea"
+    id="mockId"
+    placeholder="I was here"
+  />
+  <label
+    htmlFor="mockId"
+  />
+</div>
+`;
+
+exports[`<Textarea /> Textarea 1`] = `
+<div
+  className="input-field"
+>
+  <textarea
+    className="materialize-textarea"
+    id="mockId"
+  />
+  <label
+    htmlFor="mockId"
+  />
+</div>
+`;