diff --git a/packages/react-native-web/src/exports/UIManager/__tests__/index-test.js b/packages/react-native-web/src/exports/UIManager/__tests__/index-test.js index 80bb0c29..f1caf8e9 100644 --- a/packages/react-native-web/src/exports/UIManager/__tests__/index-test.js +++ b/packages/react-native-web/src/exports/UIManager/__tests__/index-test.js @@ -2,8 +2,8 @@ import UIManager from '..'; -const createStyledNode = (style = {}) => { - const root = document.createElement('div'); +const createStyledNode = (name = 'div', style = {}) => { + const root = document.createElement(name); Object.keys(style).forEach(prop => { root.style[prop] = style[prop]; }); @@ -18,6 +18,29 @@ const componentStub = { }; 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', () => { test('supports className alias for class', () => { const node = createStyledNode(); @@ -27,7 +50,7 @@ describe('apis/UIManager', () => { }); 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 } }; UIManager.updateView(node, props, componentStub); expect(node.getAttribute('style')).toEqual( @@ -36,7 +59,7 @@ describe('apis/UIManager', () => { }); test('replaces input and textarea text', () => { - const node = createStyledNode(); + const node = createStyledNode('textarea'); node.value = 'initial'; const textProp = { text: 'expected-text' }; const valueProp = { value: 'expected-value' }; diff --git a/packages/react-native-web/src/exports/UIManager/index.js b/packages/react-native-web/src/exports/UIManager/index.js index 41a63cc6..20b1fca5 100644 --- a/packages/react-native-web/src/exports/UIManager/index.js +++ b/packages/react-native-web/src/exports/UIManager/index.js @@ -37,6 +37,13 @@ const measureLayout = (node, relativeToNativeNode, callback) => { } }; +const focusableElements = { + A: true, + INPUT: true, + SELECT: true, + TEXTAREA: true +}; + const UIManager = { blur(node) { try { @@ -46,6 +53,13 @@ const UIManager = { focus(node) { 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(); } catch (err) {} },