[fix] ref.focus() should focus any element type

Ensure that programmatic focus can be moved to any element. Each
instance of a primitive component type (e.g., `View`, `Text`, etc.)
includes a `focus` method. However, on the web only certain elements can
receive programmatic focus by default: those that can also receive
keyboard focus, e.g., `a`, `button`, `input`, etc. All other element
types must set `tabIndex="-1"` in order to be programmatically focusable
without also being focusable via keyboard or mouse.

Fix #1099
This commit is contained in:
Nicolas Gallagher
2018-11-10 12:03:44 -08:00
parent 91c9457392
commit b6be677db9
2 changed files with 41 additions and 4 deletions
@@ -2,8 +2,8 @@
import UIManager from '..'; import UIManager from '..';
const createStyledNode = (style = {}) => { const createStyledNode = (name = 'div', style = {}) => {
const root = document.createElement('div'); const root = document.createElement(name);
Object.keys(style).forEach(prop => { Object.keys(style).forEach(prop => {
root.style[prop] = style[prop]; root.style[prop] = style[prop];
}); });
@@ -18,6 +18,29 @@ const componentStub = {
}; };
describe('apis/UIManager', () => { describe('apis/UIManager', () => {
describe('focus', () => {
test('sets tabIndex="-1" on elements not programmatically focusable by default', () => {
const node = createStyledNode();
UIManager.focus(node);
expect(node.getAttribute('tabIndex')).toEqual('-1');
});
test('doesn\'t set tabIndex="-1" on elements with an existing tabIndex', () => {
const node = createStyledNode();
node.tabIndex = 0;
UIManager.focus(node);
expect(node.getAttribute('tabIndex')).toEqual('0');
});
test('doesn\'t set tabIndex="-1" on elements focusable by default', () => {
['a', 'input', 'select', 'textarea'].forEach(name => {
const node = createStyledNode(name);
UIManager.focus(node);
expect(node.getAttribute('tabIndex')).toBeNull();
});
});
});
describe('updateView', () => { describe('updateView', () => {
test('supports className alias for class', () => { test('supports className alias for class', () => {
const node = createStyledNode(); const node = createStyledNode();
@@ -27,7 +50,7 @@ describe('apis/UIManager', () => {
}); });
test('adds correct DOM styles to existing style', () => { test('adds correct DOM styles to existing style', () => {
const node = createStyledNode({ color: 'red' }); const node = createStyledNode('div', { color: 'red' });
const props = { style: { marginTop: 0, marginBottom: 0, opacity: 0 } }; const props = { style: { marginTop: 0, marginBottom: 0, opacity: 0 } };
UIManager.updateView(node, props, componentStub); UIManager.updateView(node, props, componentStub);
expect(node.getAttribute('style')).toEqual( expect(node.getAttribute('style')).toEqual(
@@ -36,7 +59,7 @@ describe('apis/UIManager', () => {
}); });
test('replaces input and textarea text', () => { test('replaces input and textarea text', () => {
const node = createStyledNode(); const node = createStyledNode('textarea');
node.value = 'initial'; node.value = 'initial';
const textProp = { text: 'expected-text' }; const textProp = { text: 'expected-text' };
const valueProp = { value: 'expected-value' }; const valueProp = { value: 'expected-value' };
@@ -37,6 +37,13 @@ const measureLayout = (node, relativeToNativeNode, callback) => {
} }
}; };
const focusableElements = {
A: true,
INPUT: true,
SELECT: true,
TEXTAREA: true
};
const UIManager = { const UIManager = {
blur(node) { blur(node) {
try { try {
@@ -46,6 +53,13 @@ const UIManager = {
focus(node) { focus(node) {
try { try {
const name = node.nodeName;
// A tabIndex of -1 allows element to be programmatically focused but
// prevents keyboard focus, so we don't want to set the value on elements
// that support keyboard focus by default.
if (node.getAttribute('tabIndex') == null && focusableElements[name] == null) {
node.setAttribute('tabIndex', '-1');
}
node.focus(); node.focus();
} catch (err) {} } catch (err) {}
}, },