Skip to content

Commit b565f49

Browse files
acustigaearon
authored andcommitted
Minimally support iframes (nested browsing contexts) in selection event handling (#12037)
* Prefer node’s window and document over globals * Support active elements in nested browsing contexts * Avoid invoking defaultView getter unnecessarily * Prefer node’s window and document over globals * Support active elements in nested browsing contexts * Avoid invoking defaultView getter unnecessarily * Implement selection event fixtures * Prefer node’s window and document over globals * Avoid invoking defaultView getter unnecessarily * Fix react-scripts to work with alphas after 16.0.0 The current logic just checks if the version is an alpha with a major version of 16 to account for weirdness with the 16 RC releases, but now we have alphas for newer minor releases that don't have weirdness * Run prettier on new selection events fixtures * Add fixture for onSelect in iframes, remove DraftJS fixture The DraftJs fixture wasn't really working in all supported browsers anyways, so just drop it and try to cover our bases without using it directly * Purge remnants of draft.js from fixtures * Use prop-types import instead of window global * Make fixtures’ Iframe component Firefox-compatible * Fix switch case for SelectionEventsFixture * Remove draft.js / immutable.js dependencies * Cache owner doc as var to avoid reading it twice * Add documentation for getActiveElementDeep to explain try/catch Add documentation for getActiveElementDeep to explain try/catch * Ensure getActiveElement always returns DOM element * Tighten up isNode and isTextNode * Remove ie8 compatibility * Specify cross-origin example in getActiveElementDeep * Revert back to returning null if document is not defined
1 parent 1609cf3 commit b565f49

File tree

13 files changed

+253
-41
lines changed

13 files changed

+253
-41
lines changed

fixtures/dom/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
"query-string": "^4.2.3",
1313
"react": "^15.4.1",
1414
"react-dom": "^15.4.1",
15-
"semver": "^5.3.0"
15+
"semver": "^5.5.0"
1616
},
1717
"scripts": {
1818
"start": "react-scripts start",

fixtures/dom/src/components/Fixture.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const PropTypes = window.PropTypes;
1+
import PropTypes from 'prop-types';
22
const React = window.React;
33

44
const propTypes = {

fixtures/dom/src/components/Header.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ class Header extends React.Component {
6666
<option value="/media-events">Media Events</option>
6767
<option value="/pointer-events">Pointer Events</option>
6868
<option value="/mouse-events">Mouse Events</option>
69+
<option value="/selection-events">Selection Events</option>
6970
</select>
7071
</label>
7172
<label htmlFor="react_version">

fixtures/dom/src/components/Iframe.js

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
const React = window.React;
2+
const ReactDOM = window.ReactDOM;
3+
4+
class IframePortal extends React.Component {
5+
iframeRef = null;
6+
7+
handleRef = ref => {
8+
if (ref !== this.iframeRef) {
9+
this.iframeRef = ref;
10+
if (ref) {
11+
if (ref.contentDocument && this.props.head) {
12+
ref.contentDocument.head.innerHTML = this.props.head;
13+
}
14+
// Re-render must take place in the next tick (Firefox)
15+
setTimeout(() => {
16+
this.forceUpdate();
17+
});
18+
}
19+
}
20+
};
21+
22+
render() {
23+
const ref = this.iframeRef;
24+
let portal = null;
25+
if (ref && ref.contentDocument) {
26+
portal = ReactDOM.createPortal(
27+
this.props.children,
28+
ref.contentDocument.body
29+
);
30+
}
31+
32+
return (
33+
<div>
34+
<iframe
35+
style={{border: 'none', height: this.props.height}}
36+
ref={this.handleRef}
37+
/>
38+
{portal}
39+
</div>
40+
);
41+
}
42+
}
43+
44+
class IframeSubtree extends React.Component {
45+
warned = false;
46+
render() {
47+
if (!this.warned) {
48+
console.error(
49+
`IFrame has not yet been implemented for React v${React.version}`
50+
);
51+
this.warned = true;
52+
}
53+
return <div>{this.props.children}</div>;
54+
}
55+
}
56+
57+
export default (ReactDOM.createPortal ? IframePortal : IframeSubtree);

fixtures/dom/src/components/fixtures/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import CustomElementFixtures from './custom-elements';
1313
import MediaEventsFixtures from './media-events';
1414
import PointerEventsFixtures from './pointer-events';
1515
import MouseEventsFixtures from './mouse-events';
16+
import SelectionEventsFixtures from './selection-events';
1617

1718
const React = window.React;
1819

@@ -52,6 +53,8 @@ function FixturesPage() {
5253
return <PointerEventsFixtures />;
5354
case '/mouse-events':
5455
return <MouseEventsFixtures />;
56+
case '/selection-events':
57+
return <SelectionEventsFixtures />;
5558
default:
5659
return <p>Please select a test fixture.</p>;
5760
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import TestCase from '../../TestCase';
2+
import Iframe from '../../Iframe';
3+
const React = window.React;
4+
5+
class OnSelectIframe extends React.Component {
6+
state = {count: 0, value: 'Select Me!'};
7+
8+
_onSelect = event => {
9+
this.setState(({count}) => ({count: count + 1}));
10+
};
11+
12+
_onChange = event => {
13+
this.setState({value: event.target.value});
14+
};
15+
16+
render() {
17+
const {count, value} = this.state;
18+
return (
19+
<Iframe height={60}>
20+
Selection Event Count: {count}
21+
<input
22+
type="text"
23+
onSelect={this._onSelect}
24+
value={value}
25+
onChange={this._onChange}
26+
/>
27+
</Iframe>
28+
);
29+
}
30+
}
31+
32+
export default class OnSelectEventTestCase extends React.Component {
33+
render() {
34+
return (
35+
<TestCase
36+
title="onSelect events within iframes"
37+
description="onSelect events should fire for elements rendered inside iframes">
38+
<TestCase.Steps>
39+
<li>Highlight some of the text in the input below</li>
40+
<li>Move the cursor around using the arrow keys</li>
41+
</TestCase.Steps>
42+
<TestCase.ExpectedResult>
43+
The displayed count should increase as you highlight or move the
44+
cursor
45+
</TestCase.ExpectedResult>
46+
<OnSelectIframe />
47+
</TestCase>
48+
);
49+
}
50+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import TestCase from '../../TestCase';
2+
import Iframe from '../../Iframe';
3+
const React = window.React;
4+
5+
export default class ReorderedInputsTestCase extends React.Component {
6+
state = {count: 0};
7+
8+
componentDidMount() {
9+
this.interval = setInterval(() => {
10+
this.setState({count: this.state.count + 1});
11+
}, 2000);
12+
}
13+
14+
componentWillUnmount() {
15+
clearInterval(this.interval);
16+
}
17+
18+
renderInputs() {
19+
const inputs = [
20+
<input key={1} defaultValue="Foo" />,
21+
<input key={2} defaultValue="Bar" />,
22+
];
23+
if (this.state.count % 2 === 0) {
24+
inputs.reverse();
25+
}
26+
return inputs;
27+
}
28+
29+
render() {
30+
return (
31+
<TestCase title="Reordered input elements in iframes" description="">
32+
<TestCase.Steps>
33+
<li>The two inputs below swap positions every two seconds</li>
34+
<li>Select the text in either of them</li>
35+
<li>Wait for the swap to occur</li>
36+
</TestCase.Steps>
37+
<TestCase.ExpectedResult>
38+
The selection you made should be maintained
39+
</TestCase.ExpectedResult>
40+
<Iframe height={50}>{this.renderInputs()}</Iframe>
41+
</TestCase>
42+
);
43+
}
44+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import FixtureSet from '../../FixtureSet';
2+
import ReorderedInputsTestCase from './ReorderedInputsTestCase';
3+
import OnSelectEventTestCase from './OnSelectEventTestCase';
4+
const React = window.React;
5+
6+
export default function SelectionEvents() {
7+
return (
8+
<FixtureSet
9+
title="Selection Restoration"
10+
description="
11+
When React commits changes it may perform operations which cause existing
12+
selection state to be lost. This is manually managed by reading the
13+
selection state before commits and then restoring it afterwards.
14+
">
15+
<ReorderedInputsTestCase />
16+
<OnSelectEventTestCase />
17+
</FixtureSet>
18+
);
19+
}

fixtures/dom/src/react-loader.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import semver from 'semver';
2+
13
/**
24
* Take a version from the window query string and load a specific
35
* version of React.
@@ -42,9 +44,11 @@ export default function loadReact() {
4244
let version = query.version || 'local';
4345

4446
if (version !== 'local') {
47+
const {major, minor, prerelease} = semver(version);
48+
const [preReleaseStage, preReleaseVersion] = prerelease;
4549
// The file structure was updated in 16. This wasn't the case for alphas.
4650
// Load the old module location for anything less than 16 RC
47-
if (parseInt(version, 10) >= 16 && version.indexOf('alpha') < 0) {
51+
if (major >= 16 && !(minor === 0 && preReleaseStage === 'alpha')) {
4852
REACT_PATH =
4953
'https://unpkg.com/react@' + version + '/umd/react.development.js';
5054
DOM_PATH =

fixtures/dom/yarn.lock

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2153,6 +2153,14 @@ [email protected]:
21532153
version "4.0.0"
21542154
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-4.0.0.tgz#864ef1379aced55ce6f95debecdce179f7a0cd1d"
21552155

2156+
draft-js@^0.10.5:
2157+
version "0.10.5"
2158+
resolved "https://registry.yarnpkg.com/draft-js/-/draft-js-0.10.5.tgz#bfa9beb018fe0533dbb08d6675c371a6b08fa742"
2159+
dependencies:
2160+
fbjs "^0.8.15"
2161+
immutable "~3.7.4"
2162+
object-assign "^4.1.0"
2163+
21562164
duplexer2@^0.1.4:
21572165
version "0.1.4"
21582166
resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1"
@@ -2672,7 +2680,7 @@ fbjs@^0.8.1, fbjs@^0.8.4:
26722680
setimmediate "^1.0.5"
26732681
ua-parser-js "^0.7.9"
26742682

2675-
fbjs@^0.8.16:
2683+
fbjs@^0.8.15, fbjs@^0.8.16:
26762684
version "0.8.16"
26772685
resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.16.tgz#5e67432f550dc41b572bf55847b8aca64e5337db"
26782686
dependencies:
@@ -3302,6 +3310,10 @@ ignore@^3.3.3:
33023310
version "3.3.3"
33033311
resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.3.tgz#432352e57accd87ab3110e82d3fea0e47812156d"
33043312

3313+
immutable@~3.7.4:
3314+
version "3.7.6"
3315+
resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.7.6.tgz#13b4d3cb12befa15482a26fe1b2ebae640071e4b"
3316+
33053317
imurmurhash@^0.1.4:
33063318
version "0.1.4"
33073319
resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
@@ -5878,6 +5890,10 @@ semver@^5.0.3:
58785890
version "5.4.1"
58795891
resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e"
58805892

5893+
semver@^5.5.0:
5894+
version "5.5.0"
5895+
resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab"
5896+
58815897
58825898
version "0.14.1"
58835899
resolved "https://registry.yarnpkg.com/send/-/send-0.14.1.tgz#a954984325392f51532a7760760e459598c89f7a"

0 commit comments

Comments
 (0)