From 636dfe82a2b2ff6f11ef14cd63b79c85bd2f5224 Mon Sep 17 00:00:00 2001 From: Flarnie Marchan Date: Wed, 3 May 2017 12:59:25 -0700 Subject: [PATCH] Add 'dom' fixture to 15.6-dev branch **what is the change?:** This seems useful, even for this short-lived branch, and some commits we cherry-picked were making updates to it. So we just pulled the fixture in from master. **why make this change?:** To bring things into a bit more consistency with master. **test plan:** Manually tested the fixture - things seem to work. **issue:** None - but related to cherry-picking https://github.com/facebook/react/pull/9584 --- fixtures/dom/.gitignore | 17 + fixtures/dom/README.md | 17 + fixtures/dom/package.json | 23 + fixtures/dom/public/favicon.ico | Bin 0 -> 24838 bytes fixtures/dom/public/index.html | 33 + fixtures/dom/public/react-loader.js | 43 + fixtures/dom/src/components/App.js | 18 + fixtures/dom/src/components/Fixture.js | 22 + fixtures/dom/src/components/FixtureSet.js | 29 + fixtures/dom/src/components/Header.js | 73 + fixtures/dom/src/components/TestCase.js | 146 + .../src/components/fixtures/buttons/index.js | 41 + fixtures/dom/src/components/fixtures/index.js | 38 + .../InputPlaceholderFixture.js | 61 + .../input-change-events/RadioClickFixture.js | 52 + .../RangeKeyboardFixture.js | 75 + .../fixtures/input-change-events/index.js | 81 + .../fixtures/number-inputs/NumberTestCase.js | 35 + .../fixtures/number-inputs/index.js | 165 + .../password-inputs/PasswordTestCase.js | 32 + .../fixtures/password-inputs/index.js | 31 + .../components/fixtures/range-inputs/index.js | 26 + .../src/components/fixtures/selects/index.js | 35 + .../fixtures/text-inputs/InputTestCase.js | 62 + .../components/fixtures/text-inputs/README.md | 65 + .../components/fixtures/text-inputs/index.js | 94 + .../components/fixtures/textareas/index.js | 34 + fixtures/dom/src/components/propTypes.js | 15 + fixtures/dom/src/index.js | 8 + fixtures/dom/src/style.css | 216 + fixtures/dom/src/tags.js | 70 + fixtures/dom/yarn.lock | 5490 +++++++++++++++++ 32 files changed, 7147 insertions(+) create mode 100644 fixtures/dom/.gitignore create mode 100644 fixtures/dom/README.md create mode 100644 fixtures/dom/package.json create mode 100644 fixtures/dom/public/favicon.ico create mode 100644 fixtures/dom/public/index.html create mode 100644 fixtures/dom/public/react-loader.js create mode 100644 fixtures/dom/src/components/App.js create mode 100644 fixtures/dom/src/components/Fixture.js create mode 100644 fixtures/dom/src/components/FixtureSet.js create mode 100644 fixtures/dom/src/components/Header.js create mode 100644 fixtures/dom/src/components/TestCase.js create mode 100644 fixtures/dom/src/components/fixtures/buttons/index.js create mode 100644 fixtures/dom/src/components/fixtures/index.js create mode 100644 fixtures/dom/src/components/fixtures/input-change-events/InputPlaceholderFixture.js create mode 100644 fixtures/dom/src/components/fixtures/input-change-events/RadioClickFixture.js create mode 100644 fixtures/dom/src/components/fixtures/input-change-events/RangeKeyboardFixture.js create mode 100644 fixtures/dom/src/components/fixtures/input-change-events/index.js create mode 100644 fixtures/dom/src/components/fixtures/number-inputs/NumberTestCase.js create mode 100644 fixtures/dom/src/components/fixtures/number-inputs/index.js create mode 100644 fixtures/dom/src/components/fixtures/password-inputs/PasswordTestCase.js create mode 100644 fixtures/dom/src/components/fixtures/password-inputs/index.js create mode 100644 fixtures/dom/src/components/fixtures/range-inputs/index.js create mode 100644 fixtures/dom/src/components/fixtures/selects/index.js create mode 100644 fixtures/dom/src/components/fixtures/text-inputs/InputTestCase.js create mode 100644 fixtures/dom/src/components/fixtures/text-inputs/README.md create mode 100644 fixtures/dom/src/components/fixtures/text-inputs/index.js create mode 100644 fixtures/dom/src/components/fixtures/textareas/index.js create mode 100644 fixtures/dom/src/components/propTypes.js create mode 100644 fixtures/dom/src/index.js create mode 100644 fixtures/dom/src/style.css create mode 100644 fixtures/dom/src/tags.js create mode 100644 fixtures/dom/yarn.lock diff --git a/fixtures/dom/.gitignore b/fixtures/dom/.gitignore new file mode 100644 index 0000000000000..bcc245d36a251 --- /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 0000000000000..744ac92c88fa8 --- /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 0000000000000..d27936a4da275 --- /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 0000000000000000000000000000000000000000..5c125de5d897c1ff5692a656485b3216123dcd89 GIT binary patch literal 24838 zcmeI4X^>UL6@VY56)S&I{`6Nu0RscWCdj@GJHx(%?6_-;yKy1n;EEf9f}pr1CW5HA zYt$%U#C=}?jWH&%G@BaHBxsWAoUb3}&6%Ei@4Ii_JRa1`RQ23*yU)_wJ$?H0>6gj0 z${d_I^w5kvTW3xYEc?FvyP3>p$!py@`@T`|dVepIsjbbvR}af%KKy7YuQ%SDC^zmNWPYR^7avI5P-@dKev}UZ^aDAOyci9Nn zwR4qEz~tSvrp|#ACvWzo9`3B;`}^{t18dxaH;?xT7#hmJiKAaI;|O=$yxzXNOHGw~ z^!5pE^SW`av%t_$22LFPsM^l%=PSp!3r`>9w%s+^ZQYnnTQ*Ggd9-1~kj_o$YdW@b ztCkJ(ZGYjusqV5L4{^)R9Gt@gzU1t|?xhE&c^q(|(R#oa*}Sj5c({A$mhrB8*Y@tc zr)K#C{KOp-eHl35ZWJ1&zkmI>9DL%!KJE@_!=W?aH;i?ZDb0O1HPFy6 zcV0Kf)eZ0BHmz9vowF7EA{z*aue9M)iJP&Zd)qYlfJ-c^sS1qY^?>s)!!Ta@x zr@Lz|80r)7<{QVk9Z$}5SDaVtz*Rc?oH5~Wcjoc^eA&EdJ^h@aZ-BvL{K2s_7Cvfr zFL&(R?D&(9OxsS%z_BzI9^Ai^AOF$PUpGk~oO(=OpMc3@Zh&KH1a9>G%%0rC)t@oQ z4d~M`hX+g^Wf8P>A&&qjq|tZe*44Laq7qVPK#QIc)s*Qj34P`NL`Q{xBI`SnR!RC? zlGdTvC%oVZ@0BgcH>}qc!uzul@{i@sH}L0|=eZBJ9qF!HHaw?`s0(_DJj(v`(memI z6jH}=BfGlSlRV4)ouv#h*65yRR>G zo;I#~BVK&l&{+H=_~Nq$d%bFLh7GE5pS&>Fr{RMe>)MM19~z6F1oQo_y>vtlpEZF# zIc82TpMc3z9;{Q)=zG5B#4+96yHCvYy8p4;C%6x`%y$2HccC9|#vGVD)**C0xX|R| z%h)}ze!Tnrvvb@RZ!GX@2lMEq`=`08b`9$%FnN@*zJLo2wD5?MbE&LN)Z>Kty*;m= zt{Cn0>Q3nk)`bR^{dVf!3ECg6Yz4YcskI>$XH*L8E)MsudhnkP0B>+M(XEcErHUBKi~ z1`fEP&WPhp{@Ew?cPlR(ma9iw8NbJWHqp=btCtM*FnP*@ZwwlJ&-Y|LEjgvJzUtPc zz5CrWNBRV8d0-bpWAl<=zM1PU8lJseDxBK^QuuCj2fg{&2#*IG5ezf1B(o%lU+OZx7So4D?yi2*h zFBkr5pG3AJs83uy!~C3mQZLp~ss7-N9oAY>t)!eC#s)CrPukK!(!G*)H?v(~JCoj# zfvgTxMV{4?zL1neQ;ITVBAdFDf`1yG$o{g7^1sR_n{RZ7tnXio?tM%240}(z9xFY0 zlz{^-G*RET;-`7`>e0b{{`!2kM)t7Si9ZqD$~wh*hyGC>z~qs@0T&u*;h}hiKGEga zHkJ;%7aNc^o_0(>Z{Gp069H;TwPTUnvvX0SJ+kGGZ0lFBWocl>kaa)AoiMta+x_-J-?#KHFnJ*! zwD1V?)4s#|?O)DlMBhVv4IgZs?d>b<6%xK3<{o91H?-%8?PK!_fm#3d>{{gQ z?*8`b{G6?bZKdO{_9IVlz{R$PcGjeL|3*|@upby()_Lf^eQ&XQe)CjsbJ3Uolrgt< zweld3GH|fZpn(=1@PencO_a_)v6tU?WV-w8wfXLbOGae0{<*C?Ead$6v+> z|EQKThJTmwXK!c6AOD+FgtDv7i<48{-OPce!KDVkzR+XKOcREPha(;$}iUb!*)f-Fb}Y4@r9z-_{OIg z`xn^T#ZtEPv_T$M*Sr+=Z{q#~8$|7Y{0!*2u${D*Jj%dfOrS~FzpH*_|55J!7kl4w z?LT!7T(!3!632pmZh?dh`n-z$_ts42pn6;c`}hx;TSYd0idsqal5&0uGV=UM{c9xQ z1KK6&TS+a^H|6B_hPo1W3 zh+Dun!`UkP%H3}*@IE18q{7&MH2f3?T6o}Jf+xI@fh=SyUOArw`*w1_-PUlHZTHc@ z--yqIxPtI}IjPRzLIZ8cPv4P=>?A&=E~~0)>&J#V;TwAR*6}`01iu~U$@prtzW6YS ze}E>gUX+0YuF}B+Uhw2x7a7Q+oOzMNFHTNN<)40Rzg#`pABKF18@l}5A>RL`?Ri;Z zC8ExD$)im1@R{N7(wIog8$Yn(6%q$yd9(zKe};OnH%;mWBs7)>ls~T3Wi6!Xqw6+dpJLVS1P| z9qV%io-nE*rYcPxiS31>U_>mbPTXxkC*!?*zefr#2vF|qr8{|4|u^7-pD|f z&OPc->UKu)=iHgIpysp;Lsbyj}GJWoBkufOA={CRTUjr%af zc5pUH9{pg?M5%+)oN`q9yBbBt@+3xHV)qGm8b)Cp-w7~CwEhtBUk0rbjrqM zTb|tQ3-5-pw^cul`T+X&s?O;?V(FD!(Q9Qg@(LTCNz{0-vBM^SX5lti3|GpxFn4;Ax6pGc~t)R!Bo${lYH(* z!F&5X*?S&}YoDCyzwv1H+XI(+rL`;RN9}iLxlfr-r&vGG8OQa@=>+a)+Ij)sd_{wu z1Am(+3-RFr4&N8N6+hqo19S#;SA1-hG>07p3}&*j4CR+rqdV)^6n; z_vFr!(a%-=#=kb{pYmNL@6|DWkw~%E2V2jYl*e1}c{e$fib?(O+hs}eoBLRo&9(;J}YV}0Mi;LZAe{U$(s= zT<-IaV$Z+q-P!~3{HxN>Kbw30jXzM&I(S<6Ksx^}HvU2Vntb!etSsm0>)j}Me^+L5{2yz--)?W`Q?az z!WLG4UNP}+#C+NKH+ZG-Q=E>IPp%LuKLx$$8NAOGr(#~P>!EA zDYlpXDR=xM?Xv5(-qp74Cw3LzBeASHSBY`OezkbOyjP!G%WSymju_C$VBl--z + + + + + + + React App + + + + +
+ + + diff --git a/fixtures/dom/public/react-loader.js b/fixtures/dom/public/react-loader.js new file mode 100644 index 0000000000000..af8fe8874e7b3 --- /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 0000000000000..a22b1d918c4f6 --- /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 0000000000000..6074fa7d44c4e --- /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 0000000000000..ff98b2884c1bb --- /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 0000000000000..6d1b32599fa8a --- /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 0000000000000..053eac9467884 --- /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 0000000000000..e7182280d637b --- /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 0000000000000..27be0fe24f284 --- /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 0000000000000..35419b896a71a --- /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 0000000000000..fb44166b34b0b --- /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 0000000000000..12ccd1f43ac47 --- /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 0000000000000..703585ed43795 --- /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 0000000000000..1a3025c49323b --- /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 0000000000000..7900bd8594345 --- /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 0000000000000..085ff760b646b --- /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 0000000000000..bec39919781bf --- /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 0000000000000..bea61f453e15b --- /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 0000000000000..a520e40c3b92f --- /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 0000000000000..44e5f0eed27a7 --- /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 0000000000000..110a2fd1114e2 --- /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 0000000000000..6d6ee9a687db6 --- /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 0000000000000..e2508f32b52c5 --- /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 +