diff --git a/fixtures/dom/.gitignore b/fixtures/dom/.gitignore new file mode 100644 index 00000000000..bcc245d36a2 --- /dev/null +++ b/fixtures/dom/.gitignore @@ -0,0 +1,17 @@ +# See http://help.github.com/ignore-files/ for more about ignoring files. + +# dependencies +node_modules + +# testing +coverage + +# production +build +public/react.development.js +public/react-dom.development.js + +# misc +.DS_Store +.env +npm-debug.log diff --git a/fixtures/dom/README.md b/fixtures/dom/README.md new file mode 100644 index 00000000000..744ac92c88f --- /dev/null +++ b/fixtures/dom/README.md @@ -0,0 +1,17 @@ +# DOM Fixtures + +A set of DOM test cases for quickly identifying browser issues. + +## Setup + +To reference a local build of React, first run `npm run build` at the root +of the React project. Then: + +``` +cd fixtures/dom +npm install +npm start +``` + +The `start` command runs a script that copies over the local build of react into +the public directory. diff --git a/fixtures/dom/package.json b/fixtures/dom/package.json new file mode 100644 index 00000000000..d27936a4da2 --- /dev/null +++ b/fixtures/dom/package.json @@ -0,0 +1,23 @@ +{ + "name": "react-fixtures", + "version": "0.1.0", + "private": true, + "devDependencies": { + "react-scripts": "0.9.5" + }, + "dependencies": { + "classnames": "^2.2.5", + "query-string": "^4.2.3", + "prop-types": "^15.5.6", + "react": "^15.4.1", + "react-dom": "^15.4.1", + "semver": "^5.3.0" + }, + "scripts": { + "start": "react-scripts start", + "prestart": "cp ../../build/dist/{react,react-dom}.development.js public/", + "build": "react-scripts build && cp build/index.html build/200.html", + "test": "react-scripts test --env=jsdom", + "eject": "react-scripts eject" + } +} diff --git a/fixtures/dom/public/favicon.ico b/fixtures/dom/public/favicon.ico new file mode 100644 index 00000000000..5c125de5d89 Binary files /dev/null and b/fixtures/dom/public/favicon.ico differ diff --git a/fixtures/dom/public/index.html b/fixtures/dom/public/index.html new file mode 100644 index 00000000000..ed1922517a1 --- /dev/null +++ b/fixtures/dom/public/index.html @@ -0,0 +1,33 @@ + + + + + + + + React App + + + + +
+ + + diff --git a/fixtures/dom/public/react-loader.js b/fixtures/dom/public/react-loader.js new file mode 100644 index 00000000000..af8fe8874e7 --- /dev/null +++ b/fixtures/dom/public/react-loader.js @@ -0,0 +1,43 @@ +/** + * Take a version from the window query string and load a specific + * version of React. + * + * @example + * http://localhost:3000?version=15.4.1 + * (Loads React 15.4.1) + */ + +var REACT_PATH = 'react.development.js'; +var DOM_PATH = 'react-dom.development.js'; + +function parseQuery(qstr) { + var query = {}; + + var a = qstr.substr(1).split('&'); + + for (var i = 0; i < a.length; i++) { + var b = a[i].split('='); + + query[decodeURIComponent(b[0])] = decodeURIComponent(b[1] || ''); + } + + return query; +} + +var query = parseQuery(window.location.search); +var version = query.version || 'local'; + +if (version !== 'local') { + REACT_PATH = 'https://unpkg.com/react@' + version + '/dist/react.min.js'; + DOM_PATH = 'https://unpkg.com/react-dom@' + version + '/dist/react-dom.min.js'; +} + +document.write(''); + +// Versions earlier than 14 do not use ReactDOM +if (version === 'local' || parseFloat(version, 10) > 0.13) { + document.write(''); +} else { + // Aliasing React to ReactDOM for compatibility. + document.write(''); +} diff --git a/fixtures/dom/src/components/App.js b/fixtures/dom/src/components/App.js new file mode 100644 index 00000000000..a22b1d918c4 --- /dev/null +++ b/fixtures/dom/src/components/App.js @@ -0,0 +1,18 @@ +const React = window.React; +import Header from './Header'; +import Fixtures from './fixtures'; + +import '../style.css'; + +function App () { + return ( +
+
+
+ +
+
+ ); +} + +export default App; diff --git a/fixtures/dom/src/components/Fixture.js b/fixtures/dom/src/components/Fixture.js new file mode 100644 index 00000000000..6074fa7d44c --- /dev/null +++ b/fixtures/dom/src/components/Fixture.js @@ -0,0 +1,22 @@ +const PropTypes = window.PropTypes; +const React = window.React; + +const propTypes = { + children: PropTypes.node.isRequired, +}; + +class Fixture extends React.Component { + render() { + const { children } = this.props; + + return ( +
+ {children} +
+ ); + } +} + +Fixture.propTypes = propTypes; + +export default Fixture diff --git a/fixtures/dom/src/components/FixtureSet.js b/fixtures/dom/src/components/FixtureSet.js new file mode 100644 index 00000000000..ff98b2884c1 --- /dev/null +++ b/fixtures/dom/src/components/FixtureSet.js @@ -0,0 +1,29 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +const propTypes = { + title: PropTypes.node.isRequired, + description: PropTypes.node.isRequired, +}; + +class FixtureSet extends React.Component { + + render() { + const { title, description, children } = this.props; + + return ( +
+

{title}

+ {description && ( +

{description}

+ )} + + {children} +
+ ); + } +} + +FixtureSet.propTypes = propTypes; + +export default FixtureSet diff --git a/fixtures/dom/src/components/Header.js b/fixtures/dom/src/components/Header.js new file mode 100644 index 00000000000..6d1b32599fa --- /dev/null +++ b/fixtures/dom/src/components/Header.js @@ -0,0 +1,73 @@ +import { parse, stringify } from 'query-string'; +import getVersionTags from '../tags'; +const React = window.React; + +class Header extends React.Component { + constructor(props, context) { + super(props, context); + const query = parse(window.location.search); + const version = query.version || 'local'; + const versions = [version]; + this.state = { version, versions }; + } + componentWillMount() { + getVersionTags() + .then(tags => { + let versions = tags.map(tag => tag.name.slice(1)); + versions = [`local`, ...versions]; + this.setState({ versions }); + }) + } + handleVersionChange(event) { + const query = parse(window.location.search); + query.version = event.target.value; + if (query.version === 'local') { + delete query.version; + } + window.location.search = stringify(query); + } + handleFixtureChange(event) { + window.location.pathname = event.target.value; + } + render() { + return ( +
+
+ + + React Sandbox (v{React.version}) + + +
+ + +
+
+
+ ); + } +} + +export default Header; diff --git a/fixtures/dom/src/components/TestCase.js b/fixtures/dom/src/components/TestCase.js new file mode 100644 index 00000000000..053eac94678 --- /dev/null +++ b/fixtures/dom/src/components/TestCase.js @@ -0,0 +1,146 @@ +import cn from 'classnames'; +import semver from 'semver'; +import React from 'react'; +import PropTypes from 'prop-types'; +import { parse } from 'query-string'; +import { semverString } from './propTypes'; + +const propTypes = { + children: PropTypes.node.isRequired, + title: PropTypes.node.isRequired, + resolvedIn: semverString, + resolvedBy: PropTypes.string +}; + +class TestCase extends React.Component { + constructor(props, context) { + super(props, context); + + this.state = { + complete: false, + }; + } + + handleChange = (e) => { + this.setState({ + complete: e.target.checked + }) + }; + + render() { + const { + title, + description, + resolvedIn, + resolvedBy, + affectedBrowsers, + children, + } = this.props; + + let { complete } = this.state; + + const { version } = parse(window.location.search); + const isTestRelevant = ( + !version || + !resolvedIn || + semver.gte(version, resolvedIn) + ); + + complete = !isTestRelevant || complete; + + return ( +
+

+ +

+ +
+ {resolvedIn && ( +
First supported in:
)} + {resolvedIn && ( +
+ + {resolvedIn} + +
+ )} + + {resolvedBy && ( +
Fixed by:
)} + {resolvedBy && ( +
+ + {resolvedBy} + +
+ )} + + {affectedBrowsers && +
Affected browsers:
} + {affectedBrowsers && +
{affectedBrowsers}
+ } +
+ +

+ {description} +

+ +
+ {!isTestRelevant &&( +

+ Note: This test case was fixed in a later version of React. + This test is not expected to pass for the selected version, and that's ok! +

+ )} + + {children} +
+
+ ); + } +} + +TestCase.propTypes = propTypes; + +TestCase.Steps = class extends React.Component { + render() { + const { children } = this.props; + return ( +
+

Steps to reproduce:

+
    + {children} +
+
+ ) + } +} + +TestCase.ExpectedResult = class extends React.Component { + render() { + const { children } = this.props + return ( +
+

Expected Result:

+

+ {children} +

+
+ ) + } +} +export default TestCase diff --git a/fixtures/dom/src/components/fixtures/buttons/index.js b/fixtures/dom/src/components/fixtures/buttons/index.js new file mode 100644 index 00000000000..e7182280d63 --- /dev/null +++ b/fixtures/dom/src/components/fixtures/buttons/index.js @@ -0,0 +1,41 @@ +const React = window.React; + +import FixtureSet from '../../FixtureSet'; +import TestCase from '../../TestCase'; + +function onButtonClick() { + window.alert(`This shouldn't have happened!`); +} + +export default class ButtonTestCases extends React.Component { + render() { + return ( + + + +
  • Click on the disabled button
  • +
    + + Nothing should happen + + +
    + + +
  • Click on the disabled button, which contains a span
  • +
    + + Nothing should happen + + +
    +
    + ); + } +} diff --git a/fixtures/dom/src/components/fixtures/index.js b/fixtures/dom/src/components/fixtures/index.js new file mode 100644 index 00000000000..27be0fe24f2 --- /dev/null +++ b/fixtures/dom/src/components/fixtures/index.js @@ -0,0 +1,38 @@ +const React = window.React; +import RangeInputFixtures from './range-inputs'; +import TextInputFixtures from './text-inputs'; +import SelectFixtures from './selects'; +import TextAreaFixtures from './textareas'; +import InputChangeEvents from './input-change-events'; +import NumberInputFixtures from './number-inputs'; +import PasswordInputFixtures from './password-inputs'; +import ButtonFixtures from './buttons'; + +/** + * A simple routing component that renders the appropriate + * fixture based on the location pathname. + */ +function FixturesPage() { + switch (window.location.pathname) { + case '/text-inputs': + return ; + case '/range-inputs': + return ; + case '/selects': + return ; + case '/textareas': + return ; + case '/input-change-events': + return ; + case '/number-inputs': + return ; + case '/password-inputs': + return ; + case '/buttons': + return + default: + return

    Please select a test fixture.

    ; + } +} + +module.exports = FixturesPage; diff --git a/fixtures/dom/src/components/fixtures/input-change-events/InputPlaceholderFixture.js b/fixtures/dom/src/components/fixtures/input-change-events/InputPlaceholderFixture.js new file mode 100644 index 00000000000..35419b896a7 --- /dev/null +++ b/fixtures/dom/src/components/fixtures/input-change-events/InputPlaceholderFixture.js @@ -0,0 +1,61 @@ +import React from 'react'; + +import Fixture from '../../Fixture'; + + + +class InputPlaceholderFixture extends React.Component { + constructor(props, context) { + super(props, context); + + this.state = { + placeholder: 'A placeholder', + changeCount: 0, + }; + } + + handleChange = () => { + this.setState(({ changeCount }) => { + return { + changeCount: changeCount + 1 + } + }) + } + handleGeneratePlaceholder = () => { + this.setState({ + placeholder: `A placeholder: ${Math.random() * 100}` + }) + } + + handleReset = () => { + this.setState({ + changeCount: 0, + }) + } + + render() { + const { placeholder, changeCount } = this.state; + const color = changeCount === 0 ? 'green' : 'red'; + + return ( + + + {' '} + + +

    + onChange{' calls: '}{changeCount} +

    + +
    + ) + } +} + +export default InputPlaceholderFixture; diff --git a/fixtures/dom/src/components/fixtures/input-change-events/RadioClickFixture.js b/fixtures/dom/src/components/fixtures/input-change-events/RadioClickFixture.js new file mode 100644 index 00000000000..fb44166b34b --- /dev/null +++ b/fixtures/dom/src/components/fixtures/input-change-events/RadioClickFixture.js @@ -0,0 +1,52 @@ +import React from 'react'; + +import Fixture from '../../Fixture'; + +class RadioClickFixture extends React.Component { + constructor(props, context) { + super(props, context); + + this.state = { + changeCount: 0, + }; + } + + handleChange = () => { + this.setState(({ changeCount }) => { + return { + changeCount: changeCount + 1 + } + }) + } + + handleReset = () => { + this.setState({ + changeCount: 0, + }) + } + + render() { + const { changeCount } = this.state; + const color = changeCount === 0 ? 'green' : 'red'; + + return ( + + + {' '} +

    + onChange{' calls: '}{changeCount} +

    + +
    + ) + } +} + +export default RadioClickFixture; diff --git a/fixtures/dom/src/components/fixtures/input-change-events/RangeKeyboardFixture.js b/fixtures/dom/src/components/fixtures/input-change-events/RangeKeyboardFixture.js new file mode 100644 index 00000000000..12ccd1f43ac --- /dev/null +++ b/fixtures/dom/src/components/fixtures/input-change-events/RangeKeyboardFixture.js @@ -0,0 +1,75 @@ +import React from 'react'; + +import Fixture from '../../Fixture'; + + +class RangeKeyboardFixture extends React.Component { + constructor(props, context) { + super(props, context); + + this.state = { + keydownCount: 0, + changeCount: 0, + }; + } + + componentDidMount() { + this.input.addEventListener('keydown', this.handleKeydown, false) + } + + componentWillUnmount() { + this.input.removeEventListener('keydown', this.handleKeydown, false) + } + + handleChange = () => { + this.setState(({ changeCount }) => { + return { + changeCount: changeCount + 1 + } + }) + } + + handleKeydown = (e) => { + // only interesting in arrow key events + if (![37, 38, 39, 40].includes(e.keyCode)) + return; + + this.setState(({ keydownCount }) => { + return { + keydownCount: keydownCount + 1 + } + }) + } + + handleReset = () => { + this.setState({ + keydownCount: 0, + changeCount: 0, + }) + } + + render() { + const { keydownCount, changeCount } = this.state; + const color = keydownCount === changeCount ? 'green' : 'red'; + + return ( + + this.input = r} + onChange={this.handleChange} + /> + {' '} + +

    + onKeyDown{' calls: '}{keydownCount} + {' vs '} + onChange{' calls: '}{changeCount} +

    + +
    + ) + } +} + +export default RangeKeyboardFixture; diff --git a/fixtures/dom/src/components/fixtures/input-change-events/index.js b/fixtures/dom/src/components/fixtures/input-change-events/index.js new file mode 100644 index 00000000000..703585ed437 --- /dev/null +++ b/fixtures/dom/src/components/fixtures/input-change-events/index.js @@ -0,0 +1,81 @@ +import React from 'react'; + +import FixtureSet from '../../FixtureSet'; +import TestCase from '../../TestCase'; +import RangeKeyboardFixture from './RangeKeyboardFixture'; +import RadioClickFixture from './RadioClickFixture'; +import InputPlaceholderFixture from './InputPlaceholderFixture'; + +class InputChangeEvents extends React.Component { + render() { + return ( + + + +
  • Focus range input
  • +
  • change value via the keyboard arrow keys
  • +
    + + + The onKeyDown call count should be equal to + the onChange call count. + + + +
    + + + +
  • Click on the Radio input (or label text)
  • +
    + + + The onChange call count should remain at 0 + + + +
    + + + +
  • Click on the Text input
  • +
  • Click on the "Change placeholder" button
  • +
    + + + The onChange call count should remain at 0 + + + +
    +
    + ); + } +} + + +export default InputChangeEvents diff --git a/fixtures/dom/src/components/fixtures/number-inputs/NumberTestCase.js b/fixtures/dom/src/components/fixtures/number-inputs/NumberTestCase.js new file mode 100644 index 00000000000..1a3025c4932 --- /dev/null +++ b/fixtures/dom/src/components/fixtures/number-inputs/NumberTestCase.js @@ -0,0 +1,35 @@ +const React = window.React; + +import Fixture from '../../Fixture'; + +class NumberTestCase extends React.Component { + state = { value: '' }; + onChange = (event) => { + const parsed = parseFloat(event.target.value, 10) + const value = isNaN(parsed) ? '' : parsed + + this.setState({ value }) + } + render() { + return ( + +
    {this.props.children}
    + +
    +
    + Controlled + + Value: {JSON.stringify(this.state.value)} +
    + +
    + Uncontrolled + +
    +
    +
    + ); + } +} + +export default NumberTestCase; diff --git a/fixtures/dom/src/components/fixtures/number-inputs/index.js b/fixtures/dom/src/components/fixtures/number-inputs/index.js new file mode 100644 index 00000000000..7900bd85943 --- /dev/null +++ b/fixtures/dom/src/components/fixtures/number-inputs/index.js @@ -0,0 +1,165 @@ +const React = window.React; + +import FixtureSet from '../../FixtureSet'; +import TestCase from '../../TestCase'; +import NumberTestCase from './NumberTestCase'; + +function NumberInputs() { + return ( + + + +
  • Type "3.1"
  • +
  • Press backspace, eliminating the "1"
  • +
    + + + The field should read "3.", preserving the decimal place + + + + +

    + Notes: Chrome and Safari clear trailing + decimals on blur. React makes this concession so that the + value attribute remains in sync with the value property. +

    +
    + + + +
  • Type "0.01"
  • +
    + + + The field should read "0.01" + + + +
    + + + +
  • Type "2e"
  • +
  • Type 4, to read "2e4"
  • +
    + + + The field should read "2e4". The parsed value should read "20000" + + + +
    + + + +
  • Type "3.14"
  • +
  • Press "e", so that the input reads "3.14e"
  • +
    + + + The field should read "3.14e", the parsed value should be empty + + + +
    + + + +
  • Type "3.14"
  • +
  • Move the text cursor to after the decimal place
  • +
  • Press "e" twice, so that the value reads "3.ee14"
  • +
    + + + The field should read "3.ee14" + + + +
    + + + +
  • Type "3.0"
  • +
    + + + The field should read "3.0" + + + +
    + + + +
  • Type "300"
  • +
  • Move the cursor to after the "3"
  • +
  • Type "."
  • +
    + + + The field should read "3.00", not "3" + + +
    + + + +
  • Type "3"
  • +
  • Select the entire value"
  • +
  • Type '-' to replace '3' with '-'
  • +
    + + + The field should read "-", not be blank. + + +
    + + + +
  • Type "-"
  • +
  • Type '3'
  • +
    + + + The field should read "-3". + + +
    +
    + ); +} + +export default NumberInputs; diff --git a/fixtures/dom/src/components/fixtures/password-inputs/PasswordTestCase.js b/fixtures/dom/src/components/fixtures/password-inputs/PasswordTestCase.js new file mode 100644 index 00000000000..085ff760b64 --- /dev/null +++ b/fixtures/dom/src/components/fixtures/password-inputs/PasswordTestCase.js @@ -0,0 +1,32 @@ +const React = window.React; + +import Fixture from '../../Fixture'; + +class PasswordTestCase extends React.Component { + state = { value: '' }; + onChange = (event) => { + this.setState({ value: event.target.value }) + } + render() { + return ( + +
    {this.props.children}
    + +
    +
    + Controlled + + Value: {JSON.stringify(this.state.value)} +
    + +
    + Uncontrolled + +
    +
    +
    + ); + } +} + +export default PasswordTestCase; diff --git a/fixtures/dom/src/components/fixtures/password-inputs/index.js b/fixtures/dom/src/components/fixtures/password-inputs/index.js new file mode 100644 index 00000000000..bec39919781 --- /dev/null +++ b/fixtures/dom/src/components/fixtures/password-inputs/index.js @@ -0,0 +1,31 @@ +const React = window.React; + +import FixtureSet from '../../FixtureSet'; +import TestCase from '../../TestCase'; +import PasswordTestCase from './PasswordTestCase' + +function NumberInputs() { + return ( + + + +
  • Type any string (not an actual password
  • +
    + + + The field should include the "unmasking password" icon. + + + +
    +
    + ); +} + +export default NumberInputs; diff --git a/fixtures/dom/src/components/fixtures/range-inputs/index.js b/fixtures/dom/src/components/fixtures/range-inputs/index.js new file mode 100644 index 00000000000..bea61f453e1 --- /dev/null +++ b/fixtures/dom/src/components/fixtures/range-inputs/index.js @@ -0,0 +1,26 @@ +const React = window.React; + +class RangeInputs extends React.Component { + state = { value: 0.5 }; + onChange = (event) => { + this.setState({ value: event.target.value }); + } + render() { + return ( +
    +
    + Controlled + + Value: {this.state.value} +
    + +
    + Uncontrolled + +
    +
    + ); + } +} + +export default RangeInputs; diff --git a/fixtures/dom/src/components/fixtures/selects/index.js b/fixtures/dom/src/components/fixtures/selects/index.js new file mode 100644 index 00000000000..a520e40c3b9 --- /dev/null +++ b/fixtures/dom/src/components/fixtures/selects/index.js @@ -0,0 +1,35 @@ +const React = window.React; + +class SelectFixture extends React.Component { + state = { value: '' }; + onChange = (event) => { + this.setState({ value: event.target.value }); + } + render() { + return ( +
    +
    + Controlled + + Value: {this.state.value} +
    +
    + Uncontrolled + +
    +
    + ); + } +} + +export default SelectFixture; diff --git a/fixtures/dom/src/components/fixtures/text-inputs/InputTestCase.js b/fixtures/dom/src/components/fixtures/text-inputs/InputTestCase.js new file mode 100644 index 00000000000..44e5f0eed27 --- /dev/null +++ b/fixtures/dom/src/components/fixtures/text-inputs/InputTestCase.js @@ -0,0 +1,62 @@ +const React = window.React; + +import Fixture from '../../Fixture'; + +class InputTestCase extends React.Component { + static defaultProps = { + type: 'text', + defaultValue: '', + parseAs: 'text' + } + + constructor () { + super(...arguments); + + this.state = { + value: this.props.defaultValue + }; + } + + onChange = (event) => { + const raw = event.target.value; + + switch (this.props.type) { + case 'number': + const parsed = parseFloat(event.target.value, 10); + + this.setState({ value: isNaN(parsed) ? '' : parsed }); + + break; + default: + this.setState({ value: raw }); + } + } + + render() { + const { children, type, defaultValue } = this.props; + const { value } = this.state; + + return ( + +
    {children}
    + +
    +
    + Controlled {type} + +

    + Value: {JSON.stringify(this.state.value)} +

    +
    + +
    + Uncontrolled {type} + +
    +
    +
    + ); + } +} + +export default InputTestCase; diff --git a/fixtures/dom/src/components/fixtures/text-inputs/README.md b/fixtures/dom/src/components/fixtures/text-inputs/README.md new file mode 100644 index 00000000000..110a2fd1114 --- /dev/null +++ b/fixtures/dom/src/components/fixtures/text-inputs/README.md @@ -0,0 +1,65 @@ +# Text Inputs + +There are a couple of important concepts to be aware of when working on text +inputs in React. + +## `defaultValue` vs `value` + +An input's value is drawn from two properties: `defaultValue` and `value`. + +The `defaultValue` property directly maps to the value _attribute_, for example: + +```javascript +var input = document.createElement('input') +input.defaultValue = 'hello' + +console.log(input.getAttribute('value')) // => "hello" +``` + +The `value` property manipulates the _working value_ for the input. This property +changes whenever the user interacts with an input, or it is modified with JavaScript: + +```javascript +var input = document.createElement('input') +input.defaultValue = 'hello' +input.value = 'goodbye' + +console.log(input.getAttribute('value')) // => "hello" +console.log(input.value) // => "goodbye" +``` + +## value detachment + +Until `value` is manipulated by a user or JavaScript, manipulating `defaultValue` +will also modify the `value` property: + +```javascript +var input = document.createElement('input') +// This will turn into 3 +input.defaultValue = 3 +// This will turn into 5 +input.defaultValue = 5 +// This will turn into 7 +input.value = 7 +// This will do nothing +input.defaultValue = 1 +``` + +**React detaches all inputs**. This prevents `value` from accidentally updating if +`defaultValue` changes. + +## Form reset events + +React does not support `form.reset()` for controlled inputs. This is a feature, +not a bug. `form.reset()` works by reverting an input's `value` _property_ to +that of the current `defaultValue`. Since React assigns the value `attribute` +every time a controlled input's value changes, controlled inputs will never +"revert" back to their original display value. + +## Number inputs + +Chrome (55) and Safari (10) attempt to "correct" the value of number inputs any +time the value property or attribute are changed. This leads to some edge documented +here: + +https://github.com/facebook/react/pull/7359#issuecomment-256499693 diff --git a/fixtures/dom/src/components/fixtures/text-inputs/index.js b/fixtures/dom/src/components/fixtures/text-inputs/index.js new file mode 100644 index 00000000000..6d6ee9a687d --- /dev/null +++ b/fixtures/dom/src/components/fixtures/text-inputs/index.js @@ -0,0 +1,94 @@ +const React = window.React; + +import Fixture from '../../Fixture'; +import FixtureSet from '../../FixtureSet'; +import TestCase from '../../TestCase'; +import InputTestCase from './InputTestCase'; + +class TextInputFixtures extends React.Component { + render() { + return ( + + + +
  • Move the cursor to after "2" in the text field
  • +
  • Type ".2" into the text input
  • +
    + + + The text field's value should not update. + + + +
    +
    + Value as number + {}} /> +
    + +
    + Value as string + {}} /> +
    +
    +
    + +

    + This issue was first introduced when we added extra logic + to number inputs to prevent unexpected behavior in Chrome + and Safari (see the number input test case). +

    +
    + + + +
  • Type "user@example.com"
  • +
  • Select "@"
  • +
  • Type ".", to replace "@" with a period
  • +
    + + + The text field's cursor should not jump to the end. + + + +
    + + + +
  • Type "http://www.example.com"
  • +
  • Select "www."
  • +
  • Press backspace/delete
  • +
    + + + The text field's cursor should not jump to the end. + + + +
    + + + + + + + + + + + + + + + + +
    + ); + } +} + +module.exports = TextInputFixtures; diff --git a/fixtures/dom/src/components/fixtures/textareas/index.js b/fixtures/dom/src/components/fixtures/textareas/index.js new file mode 100644 index 00000000000..e2508f32b52 --- /dev/null +++ b/fixtures/dom/src/components/fixtures/textareas/index.js @@ -0,0 +1,34 @@ +const React = window.React; + +class TextAreaFixtures extends React.Component { + state = { value: '' }; + onChange = (event) => { + this.setState({ value: event.target.value }); + } + render() { + return ( +
    +
    +
    + Controlled +