Skip to content
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

Support iframes (nested browsing contexts) in selection event handling (stale version) #7936

Closed
wants to merge 8 commits into from
227 changes: 227 additions & 0 deletions src/renderers/dom/client/__tests__/ReactInputSelection-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
/**
* Copyright 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @emails react-core
*/

'use strict';

describe('ReactInputSelection', () => {
var React;
var ReactDOM;
var ReactTestUtils;
var ReactInputSelection;
var textValue = 'the text contents';
var createAndMountElement = (type, props, children) => {
var element = React.createElement(type, props, children);
var instance = ReactTestUtils.renderIntoDocument(element);
return ReactDOM.findDOMNode(instance);
};
var makeGetSelection = (win = window) => () => ({
anchorNode: win.document.activeElement,
focusNode: win.document.activeElement,
anchorOffset: win.document.activeElement && win.document.activeElement.selectionStart,
focusOffset: win.document.activeElement && win.document.activeElement.selectionEnd,
});

beforeEach(() => {
jest.resetModuleRegistry();

React = require('React');
ReactDOM = require('ReactDOM');
ReactTestUtils = require('ReactTestUtils');
ReactInputSelection = require('ReactInputSelection');
});

describe('hasSelectionCapabilities', () => {
it('returns true for textareas', () => {
var textarea = document.createElement('textarea');
expect(ReactInputSelection.hasSelectionCapabilities(textarea)).toBe(true);
});

it('returns true for text inputs', () => {
var inputText = document.createElement('input');
var inputReadOnly = document.createElement('input');
inputReadOnly.readOnly = 'true';
var inputNumber = document.createElement('input');
inputNumber.type = 'number';
var inputEmail = document.createElement('input');
inputEmail.type = 'email';
var inputPassword = document.createElement('input');
inputPassword.type = 'password';
var inputHidden = document.createElement('input');
inputHidden.type = 'hidden';

expect(ReactInputSelection.hasSelectionCapabilities(inputText)).toBe(true);
expect(ReactInputSelection.hasSelectionCapabilities(inputReadOnly)).toBe(true);
expect(ReactInputSelection.hasSelectionCapabilities(inputNumber)).toBe(false);
expect(ReactInputSelection.hasSelectionCapabilities(inputEmail)).toBe(false);
expect(ReactInputSelection.hasSelectionCapabilities(inputPassword)).toBe(false);
expect(ReactInputSelection.hasSelectionCapabilities(inputHidden)).toBe(false);
});

it('returns true for contentEditable elements', () => {
var div = document.createElement('div');
div.contentEditable = 'true';
var body = document.createElement('body');
body.contentEditable = 'true';
var input = document.createElement('input');
input.contentEditable = 'true';
var select = document.createElement('select');
select.contentEditable = 'true';

expect(ReactInputSelection.hasSelectionCapabilities(div)).toBe(true);
expect(ReactInputSelection.hasSelectionCapabilities(body)).toBe(true);
expect(ReactInputSelection.hasSelectionCapabilities(input)).toBe(true);
expect(ReactInputSelection.hasSelectionCapabilities(select)).toBe(true);
});

it('returns false for any other type of HTMLElement', () => {
var select = document.createElement('select');
var iframe = document.createElement('iframe');

expect(ReactInputSelection.hasSelectionCapabilities(select)).toBe(false);
expect(ReactInputSelection.hasSelectionCapabilities(iframe)).toBe(false);
});
});

describe('getSelection', () => {
it('gets selection offsets from a textarea or input', () => {
var input = createAndMountElement('input', {defaultValue: textValue});
input.setSelectionRange(6, 11);
expect(ReactInputSelection.getSelection(input)).toEqual({start: 6, end: 11});

var textarea = createAndMountElement('textarea', {defaultValue: textValue});
textarea.setSelectionRange(6, 11);
expect(ReactInputSelection.getSelection(textarea)).toEqual({start: 6, end: 11});
});

it('gets selection offsets from a contentEditable element', () => {
var node = createAndMountElement('div', null, textValue);
node.selectionStart = 6;
node.selectionEnd = 11;
expect(ReactInputSelection.getSelection(node)).toEqual({start: 6, end: 11});
});

it('gets selection offsets as start: 0, end: 0 if no selection', () => {
var node = createAndMountElement('select');
expect(ReactInputSelection.getSelection(node)).toEqual({start: 0, end: 0});
});

it('gets selection on inputs in iframes', () => {
const iframe = document.createElement('iframe');
document.body.appendChild(iframe);
const input = document.createElement('input');
input.value = textValue;
iframe.contentDocument.body.appendChild(input);
input.select();
expect(input.selectionStart).toEqual(0);
expect(input.selectionEnd).toEqual(textValue.length);

document.body.removeChild(iframe);
});
});

describe('setSelection', () => {
it('sets selection offsets on textareas and inputs', () => {
var input = createAndMountElement('input', {defaultValue: textValue});
ReactInputSelection.setSelection(input, {start: 1, end: 10});
expect(input.selectionStart).toEqual(1);
expect(input.selectionEnd).toEqual(10);

var textarea = createAndMountElement('textarea', {defaultValue: textValue});
ReactInputSelection.setSelection(textarea, {start: 1, end: 10});
expect(textarea.selectionStart).toEqual(1);
expect(textarea.selectionEnd).toEqual(10);
});

it('sets selection on inputs in iframes', () => {
const iframe = document.createElement('iframe');
document.body.appendChild(iframe);
const input = document.createElement('input');
input.value = textValue;
iframe.contentDocument.body.appendChild(input);
ReactInputSelection.setSelection(input, {start: 1, end: 10});
expect(input.selectionStart).toEqual(1);
expect(input.selectionEnd).toEqual(10);

document.body.removeChild(iframe);
});
});

describe('getSelectionInformation/restoreSelection', () => {
it('gets and restores selection for inputs that get remounted', () => {
// Mock window getSelection if needed
var originalGetSelection = window.getSelection;
window.getSelection = window.getSelection || makeGetSelection(window);
var input = document.createElement('input');
input.value = textValue;
document.body.appendChild(input);
input.focus();
input.selectionStart = 1;
input.selectionEnd = 10;
var selectionInfo = ReactInputSelection.getSelectionInformation();
expect(selectionInfo.focusedElement).toBe(input);
expect(selectionInfo.activeElements[0].element).toBe(input);
expect(selectionInfo.activeElements[0].selectionRange).toEqual({start: 1, end: 10});
expect(document.activeElement).toBe(input);
input.setSelectionRange(0, 0);
document.body.removeChild(input);
expect(document.activeElement).not.toBe(input);
expect(input.selectionStart).not.toBe(1);
expect(input.selectionEnd).not.toBe(10);
document.body.appendChild(input);
ReactInputSelection.restoreSelection(selectionInfo);
expect(document.activeElement).toBe(input);
expect(input.selectionStart).toBe(1);
expect(input.selectionEnd).toBe(10);

document.body.removeChild(input);
window.getSelection = originalGetSelection;
});

it('gets and restores selection for inputs in an iframe that get remounted', () => {
var iframe = document.createElement('iframe');
document.body.appendChild(iframe);
var iframeDoc = iframe.contentDocument;
var iframeWin = iframeDoc.defaultView;
// Mock window and iframe getSelection if needed
var originalGetSelection = window.getSelection;
var originalIframeGetSelection = iframeWin.getSelection;
window.getSelection = window.getSelection || makeGetSelection(window);
iframeWin.getSelection = iframeWin.getSelection || makeGetSelection(iframeWin);

var input = document.createElement('input');
input.value = textValue;
iframeDoc.body.appendChild(input);
input.focus();
input.selectionStart = 1;
input.selectionEnd = 10;
var selectionInfo = ReactInputSelection.getSelectionInformation();
expect(selectionInfo.focusedElement).toBe(input);
expect(selectionInfo.activeElements[0].selectionRange).toEqual({start: 1, end: 10});
expect(document.activeElement).toBe(iframe);
expect(iframeDoc.activeElement).toBe(input);

input.setSelectionRange(0, 0);
iframeDoc.body.removeChild(input);
expect(iframeDoc.activeElement).not.toBe(input);
expect(input.selectionStart).not.toBe(1);
expect(input.selectionEnd).not.toBe(10);
iframeDoc.body.appendChild(input);
ReactInputSelection.restoreSelection(selectionInfo);
expect(iframeDoc.activeElement).toBe(input);
expect(input.selectionStart).toBe(1);
expect(input.selectionEnd).toBe(10);

document.body.removeChild(iframe);
window.getSelection = originalGetSelection;
iframeWin.getSelection = originalIframeGetSelection;
});
});
});
16 changes: 9 additions & 7 deletions src/renderers/dom/shared/ReactDOMSelection.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ function isCollapsed(anchorNode, anchorOffset, focusNode, focusOffset) {
* @return {object}
*/
function getIEOffsets(node) {
var selection = document.selection;
var selection = node.ownerDocument.selection;
var selectedRange = selection.createRange();
var selectedLength = selectedRange.text.length;

Expand All @@ -63,7 +63,8 @@ function getIEOffsets(node) {
* @return {?object}
*/
function getModernOffsets(node) {
var selection = window.getSelection && window.getSelection();
var win = node.ownerDocument.defaultView;
var selection = win.getSelection && win.getSelection();

if (!selection || selection.rangeCount === 0) {
return null;
Expand Down Expand Up @@ -119,7 +120,7 @@ function getModernOffsets(node) {
var end = start + rangeLength;

// Detect whether the selection is backward.
var detectionRange = document.createRange();
var detectionRange = node.ownerDocument.createRange();
detectionRange.setStart(anchorNode, anchorOffset);
detectionRange.setEnd(focusNode, focusOffset);
var isBackward = detectionRange.collapsed;
Expand All @@ -135,7 +136,7 @@ function getModernOffsets(node) {
* @param {object} offsets
*/
function setIEOffsets(node, offsets) {
var range = document.selection.createRange().duplicate();
var range = node.ownerDocument.selection.createRange().duplicate();
var start, end;

if (offsets.end === undefined) {
Expand Down Expand Up @@ -169,11 +170,12 @@ function setIEOffsets(node, offsets) {
* @param {object} offsets
*/
function setModernOffsets(node, offsets) {
if (!window.getSelection) {
var win = node.ownerDocument.defaultView;
if (!win.getSelection) {
return;
}

var selection = window.getSelection();
var selection = win.getSelection();
var length = node[getTextContentAccessor()].length;
var start = Math.min(offsets.start, length);
var end = offsets.end === undefined ?
Expand All @@ -191,7 +193,7 @@ function setModernOffsets(node, offsets) {
var endMarker = getNodeForCharacterOffset(node, end);

if (startMarker && endMarker) {
var range = document.createRange();
var range = node.ownerDocument.createRange();
range.setStart(startMarker.node, startMarker.offset);
selection.removeAllRanges();

Expand Down