From cb88093ec0f7c9c3b4d0b4cd0a28fded6b405f17 Mon Sep 17 00:00:00 2001 From: Jacob Devera Date: Wed, 13 Nov 2019 16:13:40 -0800 Subject: [PATCH 1/8] fix: add focus manager, focus reference element on close --- package-lock.json | 5 ++ package.json | 3 +- src/Popover/Popover.js | 30 +++++++++- src/utils/_Popper.js | 2 +- src/utils/focusManager.js | 121 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 157 insertions(+), 4 deletions(-) create mode 100644 src/utils/focusManager.js diff --git a/package-lock.json b/package-lock.json index 9766c0a54..d47c6ec40 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20846,6 +20846,11 @@ "has-symbols": "^1.0.0" } }, + "tabbable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-4.0.0.tgz", + "integrity": "sha512-H1XoH1URcBOa/rZZWxLxHCtOdVUEev+9vo5YdYhC9tCY4wnybX+VQrCYuy9ubkg69fCBxCONJOSLGfw0DWMffQ==" + }, "table": { "version": "5.4.6", "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", diff --git a/package.json b/package.json index 6e0370cbc..d99dcffe7 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,8 @@ "react-focus-lock": "^2.1.1", "react-overlays": "^1.1.2", "react-popper": "^1.3.3", - "shortid": "^2.2.14" + "shortid": "^2.2.14", + "tabbable": "^4.0.0" }, "devDependencies": { "@babel/cli": "^7.1.5", diff --git a/src/Popover/Popover.js b/src/Popover/Popover.js index 8c550e3f8..1190ddb11 100644 --- a/src/Popover/Popover.js +++ b/src/Popover/Popover.js @@ -1,5 +1,7 @@ import chain from 'chain-function'; import classnames from 'classnames'; +import { findDOMNode } from 'react-dom'; +import FocusManager from '../utils/focusManager'; import Popper from '../utils/_Popper'; import { POPPER_PLACEMENTS } from '../utils/constants'; import PropTypes from 'prop-types'; @@ -23,16 +25,29 @@ class Popover extends Component { } }; + handleFocusManager = () => { + if (this.state.isExpanded && this.popover) { + this.focusManager = new FocusManager(this.popover); + } + } + handleOutsideClick = () => { if (this.state.isExpanded) { this.setState({ isExpanded: false }); + if (this.focusManager) { + this.focusManager.remove(); + } + + if (this.controlRef) { + this.controlRef.focus(); + } } }; render() { - const { + let { disableEdgeDetection, disableStyles, onClickOutside, @@ -52,8 +67,19 @@ class Popover extends Component { onClickFunctions = chain(this.triggerBody, control.props.onClick); } + popperProps = { + innerRef: (c) => { + this.popover = findDOMNode(c); + this.handleFocusManager(); + }, + ...popperProps + }; + const referenceComponent = React.cloneElement(control, { - onClick: onClickFunctions + onClick: onClickFunctions, + ref: (c) => { + this.controlRef = findDOMNode(c); + } }); const popoverClasses = classnames('fd-popover', className); diff --git a/src/utils/_Popper.js b/src/utils/_Popper.js index 8cd49d2e8..37ed28161 100644 --- a/src/utils/_Popper.js +++ b/src/utils/_Popper.js @@ -88,6 +88,7 @@ class Popper extends React.Component { let popper = ( {({ ref, style, placement, outOfBoundaries, arrowProps }) => { @@ -97,7 +98,6 @@ class Popper extends React.Component { return (
{ + if (!document.body.contains(this.container)) { + this.remove(); + } + } + + findParentTabbableElement = (target) => { + let index = this.tabbableNodes.indexOf(target); + + if (index >= 0) { + return index; + } else { + if (target.parentNode) { + return this.findParentTabbableElement(target.parentNode); + } else { + return -1; + } + } + } + + focusHander = () => { + this.checkTrapNode(); + }; + + handleTab = (e) => { + this.update(); + let currentIndex = this.findParentTabbableElement(e.target), + lastNode = this.tabbableNodes[this.tabbableNodes.length - 1], + firstNode = this.tabbableNodes[0]; + + // loop focus to last node from first for shift + tab + // regular tabbing allowed to pass through browser UI and forced + // to first node with ensureFocusTrapped() + if (e.shiftKey) { + if (this.tabbableNodes[currentIndex] === firstNode) { + this.tryFocus(lastNode); + } else { + this.tryFocus(this.tabbableNodes[currentIndex - 1]); + } + e.preventDefault(); + } + } + + injectTrapElement = () => { + let trapElement = document.createElement('span'); + trapElement.tabIndex = '-1'; + trapElement.className = 'cnqr-trap-element'; + trapElement.style.outline = 'none'; + + this.container.insertBefore(trapElement, this.container.childNodes[0]); + + return trapElement; + } + + keyHandler = (e) => { + this.checkTrapNode(); + + if (e.keyCode === keycode.codes.tab) { + this.handleTab(e); + } + }; + + ensureFocusTrapped = (e) => { + // re-entering page from browser UI + if (e.target === window && this.container && !this.container.contains(document.activeElement)) { + this.tryFocus(this.tabbableNodes[0]); + } + } + + remove = () => { + this.removeTrapListeners(); + + if (this.trapElement && this.container.contains(this.trapElement)) { + this.container.removeChild(this.trapElement); + } + } + + removeTrapListeners = () => { + window.removeEventListener('focus', this.ensureFocusTrapped, true); + document.removeEventListener('focus', this.focusHander, true); + document.removeEventListener('keydown', this.keyHandler, true); + } + + setupTrapListeners = () => { + window.addEventListener('focus', this.ensureFocusTrapped, true); + document.addEventListener('focus', this.focusHander, true); + document.addEventListener('keydown', this.keyHandler, true); + } + + tryFocus = (node) => { + if (node) { + let posX = window.pageXOffset, posY = window.pageYOffset; + node.focus(); + this.focusElm = node; + window.scrollTo(posX, posY); + } + } + + update = () => { + this.tabbableNodes = tabbable(this.container); + } +} From 72229302a1ddbce737cbca5eadb95bc68ddf07cb Mon Sep 17 00:00:00 2001 From: Jacob Devera Date: Thu, 14 Nov 2019 13:07:49 -0800 Subject: [PATCH 2/8] fix: adjust focus trapping --- src/utils/focusManager.js | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/src/utils/focusManager.js b/src/utils/focusManager.js index 6255d574d..cb6dbf1aa 100644 --- a/src/utils/focusManager.js +++ b/src/utils/focusManager.js @@ -6,7 +6,6 @@ export default class FocusManager { this.container = trapNode; this.tabbableNodes = tabbable(this.container); this.enableTrapNode = enableTrapNode; - this.focusElm; if (this.enableTrapNode) { this.setupTrapListeners(); @@ -43,25 +42,28 @@ export default class FocusManager { handleTab = (e) => { this.update(); - let currentIndex = this.findParentTabbableElement(e.target), - lastNode = this.tabbableNodes[this.tabbableNodes.length - 1], - firstNode = this.tabbableNodes[0]; + const currentIndex = this.findParentTabbableElement(e.target); + const lastNode = this.tabbableNodes[this.tabbableNodes.length - 1]; + const firstNode = this.tabbableNodes[0]; - // loop focus to last node from first for shift + tab - // regular tabbing allowed to pass through browser UI and forced - // to first node with ensureFocusTrapped() if (e.shiftKey) { if (this.tabbableNodes[currentIndex] === firstNode) { this.tryFocus(lastNode); } else { this.tryFocus(this.tabbableNodes[currentIndex - 1]); } - e.preventDefault(); + } else { + if (this.tabbableNodes[currentIndex] === lastNode) { + this.tryFocus(firstNode); + } else { + this.tryFocus(this.tabbableNodes[currentIndex + 1]); + } } + e.preventDefault(); } injectTrapElement = () => { - let trapElement = document.createElement('span'); + const trapElement = document.createElement('span'); trapElement.tabIndex = '-1'; trapElement.className = 'cnqr-trap-element'; trapElement.style.outline = 'none'; @@ -79,13 +81,6 @@ export default class FocusManager { } }; - ensureFocusTrapped = (e) => { - // re-entering page from browser UI - if (e.target === window && this.container && !this.container.contains(document.activeElement)) { - this.tryFocus(this.tabbableNodes[0]); - } - } - remove = () => { this.removeTrapListeners(); @@ -110,7 +105,6 @@ export default class FocusManager { if (node) { let posX = window.pageXOffset, posY = window.pageYOffset; node.focus(); - this.focusElm = node; window.scrollTo(posX, posY); } } From a05259091fcf014464070bf9ae75187a3553b9ef Mon Sep 17 00:00:00 2001 From: Jacob Devera Date: Thu, 14 Nov 2019 13:27:42 -0800 Subject: [PATCH 3/8] fix: simplify logic --- src/Popover/Popover.js | 3 -- src/utils/focusManager.js | 66 +++++++-------------------------------- 2 files changed, 12 insertions(+), 57 deletions(-) diff --git a/src/Popover/Popover.js b/src/Popover/Popover.js index 1190ddb11..babe89daf 100644 --- a/src/Popover/Popover.js +++ b/src/Popover/Popover.js @@ -36,9 +36,6 @@ class Popover extends Component { this.setState({ isExpanded: false }); - if (this.focusManager) { - this.focusManager.remove(); - } if (this.controlRef) { this.controlRef.focus(); diff --git a/src/utils/focusManager.js b/src/utils/focusManager.js index cb6dbf1aa..c680a125e 100644 --- a/src/utils/focusManager.js +++ b/src/utils/focusManager.js @@ -2,23 +2,18 @@ import keycode from 'keycode'; import tabbable from 'tabbable'; export default class FocusManager { - constructor(trapNode, enableTrapNode = true) { + constructor(trapNode) { this.container = trapNode; this.tabbableNodes = tabbable(this.container); - this.enableTrapNode = enableTrapNode; - if (this.enableTrapNode) { - this.setupTrapListeners(); - this.trapElement = this.injectTrapElement(); - this.tryFocus(this.trapElement); - } else { - this.tryFocus(this.tabbableNodes[0]); - } + document.addEventListener('keydown', this.keyHandler, true); + + this.tryFocus(this.tabbableNodes[0]); } - checkTrapNode = () => { + checkTrapNodeExists = () => { if (!document.body.contains(this.container)) { - this.remove(); + document.removeEventListener('keydown', this.keyHandler, true); } } @@ -36,12 +31,10 @@ export default class FocusManager { } } - focusHander = () => { - this.checkTrapNode(); - }; - handleTab = (e) => { - this.update(); + e.preventDefault(); + + this.tabbableNodes = tabbable(this.container); const currentIndex = this.findParentTabbableElement(e.target); const lastNode = this.tabbableNodes[this.tabbableNodes.length - 1]; const firstNode = this.tabbableNodes[0]; @@ -59,57 +52,22 @@ export default class FocusManager { this.tryFocus(this.tabbableNodes[currentIndex + 1]); } } - e.preventDefault(); - } - - injectTrapElement = () => { - const trapElement = document.createElement('span'); - trapElement.tabIndex = '-1'; - trapElement.className = 'cnqr-trap-element'; - trapElement.style.outline = 'none'; - - this.container.insertBefore(trapElement, this.container.childNodes[0]); - - return trapElement; } keyHandler = (e) => { - this.checkTrapNode(); + this.checkTrapNodeExists(); if (e.keyCode === keycode.codes.tab) { this.handleTab(e); } }; - remove = () => { - this.removeTrapListeners(); - - if (this.trapElement && this.container.contains(this.trapElement)) { - this.container.removeChild(this.trapElement); - } - } - - removeTrapListeners = () => { - window.removeEventListener('focus', this.ensureFocusTrapped, true); - document.removeEventListener('focus', this.focusHander, true); - document.removeEventListener('keydown', this.keyHandler, true); - } - - setupTrapListeners = () => { - window.addEventListener('focus', this.ensureFocusTrapped, true); - document.addEventListener('focus', this.focusHander, true); - document.addEventListener('keydown', this.keyHandler, true); - } - tryFocus = (node) => { if (node) { - let posX = window.pageXOffset, posY = window.pageYOffset; + const posX = window.pageXOffset; + const posY = window.pageYOffset; node.focus(); window.scrollTo(posX, posY); } } - - update = () => { - this.tabbableNodes = tabbable(this.container); - } } From 278c1ffb4e822a72309e25e56859050e248fb92d Mon Sep 17 00:00:00 2001 From: Jacob Devera Date: Fri, 15 Nov 2019 09:24:11 -0800 Subject: [PATCH 4/8] fix: unit tests --- src/Popover/Popover.js | 2 +- src/utils/{ => focusManager}/focusManager.js | 0 src/utils/focusManager/focusManager.test.js | 148 +++++++++++++++++++ 3 files changed, 149 insertions(+), 1 deletion(-) rename src/utils/{ => focusManager}/focusManager.js (100%) create mode 100644 src/utils/focusManager/focusManager.test.js diff --git a/src/Popover/Popover.js b/src/Popover/Popover.js index babe89daf..a66a4d173 100644 --- a/src/Popover/Popover.js +++ b/src/Popover/Popover.js @@ -1,7 +1,7 @@ import chain from 'chain-function'; import classnames from 'classnames'; import { findDOMNode } from 'react-dom'; -import FocusManager from '../utils/focusManager'; +import FocusManager from '../utils/focusManager/focusManager'; import Popper from '../utils/_Popper'; import { POPPER_PLACEMENTS } from '../utils/constants'; import PropTypes from 'prop-types'; diff --git a/src/utils/focusManager.js b/src/utils/focusManager/focusManager.js similarity index 100% rename from src/utils/focusManager.js rename to src/utils/focusManager/focusManager.js diff --git a/src/utils/focusManager/focusManager.test.js b/src/utils/focusManager/focusManager.test.js new file mode 100644 index 000000000..be56d6a83 --- /dev/null +++ b/src/utils/focusManager/focusManager.test.js @@ -0,0 +1,148 @@ +import FocusManager from './focusManager'; + +let ce = global.document.createElement; + +// limitation with jest and 'offsetParent': https://github.com/jsdom/jsdom/issues/1261#issuecomment-362928131 +Object.defineProperty(HTMLElement.prototype, 'offsetParent', { + get() { + return this.parentNode; + } +}); + +describe('focus manager', () => { + beforeAll(() => { + global.document.createElement = function() { + let [type] = arguments; + let element = ce.apply(this, arguments); + if (type === 'a') { + element.setAttribute('tabIndex', 0); + } + return element; + }; + }); + + afterAll(() => { + global.document.createElement = ce; + }); + + const createNode = () => { + let div = document.createElement('div'); + let a1 = document.createElement('a'); + let a2 = document.createElement('a'); + let a3 = document.createElement('a'); + let a4 = document.createElement('a'); + let span = document.createElement('span'); + + div.id = 'mainDiv'; + + div.appendChild(a1); + div.appendChild(a2); + div.appendChild(span); + div.appendChild(a3); + + span.appendChild(a4); + return div; + }; + + describe('Default Behavior', () => { + it('should setup', () => { + const manager = new FocusManager(createNode()); + + expect(manager).toBeTruthy(); + }); + + it('should have tabbable elements', () => { + const manager = new FocusManager(createNode()); + + expect(manager.tabbableNodes.length).toEqual(4); + }); + + it('should focus on nodes via tryFocus', () => { + const node = createNode(); + const manager = new FocusManager(node); + const link = node.querySelector('a'); + + manager.tryFocus(node); + + expect(document.activeElement).toEqual(link); + }); + }); + + describe('Event Behavior', () => { + it('should handle tabbing back 1 element', () => { + const node = createNode(); + const manager = new FocusManager(node); + + const anchors = node.querySelectorAll('a'); + const a1 = anchors[0]; + const a2 = anchors[1]; + + const event = { + preventDefault: () => {}, + shiftKey: true, + target: a2 + }; + + manager.handleTab(event); + + expect(document.activeElement).toEqual(a1); + }); + + it('should handle tabbing forward 1 element', () => { + const node = createNode(); + const manager = new FocusManager(node); + + const anchors = node.querySelectorAll('a'); + const a2 = anchors[1]; + const a3 = anchors[2]; + + const event = { + preventDefault: () => {}, + shiftKey: false, + target: a2 + }; + + manager.handleTab(event); + + expect(document.activeElement).toEqual(a3); + }); + + it('should redirect to the last element when on first element', () => { + const node = createNode(); + const manager = new FocusManager(node); + + const anchors = node.querySelectorAll('a'); + const a1 = anchors[0]; + const a4 = anchors[3]; + + const event = { + preventDefault: () => {}, + shiftKey: true, + target: a1 + }; + + manager.handleTab(event); + + expect(document.activeElement).toEqual(a4); + }); + + it('should redirect to the first element when on last element', () => { + const node = createNode(); + const manager = new FocusManager(node); + + const anchors = node.querySelectorAll('a'); + const a1 = anchors[0]; + const a4 = anchors[3]; + + const event = { + preventDefault: () => {}, + shiftKey: false, + target: a4 + }; + + manager.handleTab(event); + + expect(document.activeElement).toEqual(a1); + }); + }); +}); From 9977dc4c7036a494d5b150f1679b1d2a3965fc57 Mon Sep 17 00:00:00 2001 From: Jacob Devera Date: Fri, 15 Nov 2019 09:59:20 -0800 Subject: [PATCH 5/8] fix: remove unnecessary handling --- src/utils/focusManager/focusManager.js | 10 +--------- src/utils/focusManager/focusManager.test.js | 6 ------ 2 files changed, 1 insertion(+), 15 deletions(-) diff --git a/src/utils/focusManager/focusManager.js b/src/utils/focusManager/focusManager.js index c680a125e..062b7a614 100644 --- a/src/utils/focusManager/focusManager.js +++ b/src/utils/focusManager/focusManager.js @@ -6,17 +6,11 @@ export default class FocusManager { this.container = trapNode; this.tabbableNodes = tabbable(this.container); - document.addEventListener('keydown', this.keyHandler, true); + this.container.addEventListener('keydown', this.keyHandler, true); this.tryFocus(this.tabbableNodes[0]); } - checkTrapNodeExists = () => { - if (!document.body.contains(this.container)) { - document.removeEventListener('keydown', this.keyHandler, true); - } - } - findParentTabbableElement = (target) => { let index = this.tabbableNodes.indexOf(target); @@ -55,8 +49,6 @@ export default class FocusManager { } keyHandler = (e) => { - this.checkTrapNodeExists(); - if (e.keyCode === keycode.codes.tab) { this.handleTab(e); } diff --git a/src/utils/focusManager/focusManager.test.js b/src/utils/focusManager/focusManager.test.js index be56d6a83..4a88f6ef6 100644 --- a/src/utils/focusManager/focusManager.test.js +++ b/src/utils/focusManager/focusManager.test.js @@ -45,12 +45,6 @@ describe('focus manager', () => { }; describe('Default Behavior', () => { - it('should setup', () => { - const manager = new FocusManager(createNode()); - - expect(manager).toBeTruthy(); - }); - it('should have tabbable elements', () => { const manager = new FocusManager(createNode()); From 210b5b813abbdd6d4c2dedb2316b18c4a67635ea Mon Sep 17 00:00:00 2001 From: Jacob Devera Date: Fri, 15 Nov 2019 10:15:55 -0800 Subject: [PATCH 6/8] chore: nits --- src/utils/focusManager/focusManager.js | 3 +-- src/utils/focusManager/focusManager.test.js | 18 +++++++++--------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/utils/focusManager/focusManager.js b/src/utils/focusManager/focusManager.js index 062b7a614..29f1e02f9 100644 --- a/src/utils/focusManager/focusManager.js +++ b/src/utils/focusManager/focusManager.js @@ -5,14 +5,13 @@ export default class FocusManager { constructor(trapNode) { this.container = trapNode; this.tabbableNodes = tabbable(this.container); - this.container.addEventListener('keydown', this.keyHandler, true); this.tryFocus(this.tabbableNodes[0]); } findParentTabbableElement = (target) => { - let index = this.tabbableNodes.indexOf(target); + const index = this.tabbableNodes.indexOf(target); if (index >= 0) { return index; diff --git a/src/utils/focusManager/focusManager.test.js b/src/utils/focusManager/focusManager.test.js index 4a88f6ef6..df49ce21a 100644 --- a/src/utils/focusManager/focusManager.test.js +++ b/src/utils/focusManager/focusManager.test.js @@ -1,6 +1,6 @@ import FocusManager from './focusManager'; -let ce = global.document.createElement; +const ce = global.document.createElement; // limitation with jest and 'offsetParent': https://github.com/jsdom/jsdom/issues/1261#issuecomment-362928131 Object.defineProperty(HTMLElement.prototype, 'offsetParent', { @@ -12,8 +12,8 @@ Object.defineProperty(HTMLElement.prototype, 'offsetParent', { describe('focus manager', () => { beforeAll(() => { global.document.createElement = function() { - let [type] = arguments; - let element = ce.apply(this, arguments); + const [type] = arguments; + const element = ce.apply(this, arguments); if (type === 'a') { element.setAttribute('tabIndex', 0); } @@ -26,12 +26,12 @@ describe('focus manager', () => { }); const createNode = () => { - let div = document.createElement('div'); - let a1 = document.createElement('a'); - let a2 = document.createElement('a'); - let a3 = document.createElement('a'); - let a4 = document.createElement('a'); - let span = document.createElement('span'); + const div = document.createElement('div'); + const a1 = document.createElement('a'); + const a2 = document.createElement('a'); + const a3 = document.createElement('a'); + const a4 = document.createElement('a'); + const span = document.createElement('span'); div.id = 'mainDiv'; From 7a918236ab9f6f9fba706327efb5533ca46f422c Mon Sep 17 00:00:00 2001 From: Jacob Devera Date: Mon, 18 Nov 2019 11:57:28 -0800 Subject: [PATCH 7/8] fix: separate innerRef from popper props --- src/Popover/Popover.js | 10 ++++------ src/utils/_Popper.js | 5 ++++- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/Popover/Popover.js b/src/Popover/Popover.js index a66a4d173..6083130df 100644 --- a/src/Popover/Popover.js +++ b/src/Popover/Popover.js @@ -64,12 +64,9 @@ class Popover extends Component { onClickFunctions = chain(this.triggerBody, control.props.onClick); } - popperProps = { - innerRef: (c) => { - this.popover = findDOMNode(c); - this.handleFocusManager(); - }, - ...popperProps + const innerRef = (c) => { + this.popover = findDOMNode(c); + this.handleFocusManager(); }; const referenceComponent = React.cloneElement(control, { @@ -86,6 +83,7 @@ class Popover extends Component { {({ ref, style, placement, outOfBoundaries, arrowProps }) => { @@ -98,6 +99,7 @@ class Popper extends React.Component { return (
Date: Wed, 20 Nov 2019 13:35:24 -0800 Subject: [PATCH 8/8] fix: arrow key navigation for certain components --- src/ComboboxInput/ComboboxInput.js | 3 +- src/Dropdown/Dropdown.Component.js | 21 ++++--- src/Dropdown/__stories__/Dropdown.stories.js | 9 ++- src/Popover/Popover.js | 14 ++++- src/Popover/__stories__/Popover.stories.js | 9 ++- src/utils/focusManager/focusManager.js | 65 ++++++++++++-------- 6 files changed, 79 insertions(+), 42 deletions(-) diff --git a/src/ComboboxInput/ComboboxInput.js b/src/ComboboxInput/ComboboxInput.js index 1150719a5..60638e23f 100644 --- a/src/ComboboxInput/ComboboxInput.js +++ b/src/ComboboxInput/ComboboxInput.js @@ -38,7 +38,8 @@ const ComboboxInput = React.forwardRef(({ placeholder, menu, compact, className,
} disableStyles={disableStyles} - noArrow /> + noArrow + useArrowKeyNavigation />
); }); diff --git a/src/Dropdown/Dropdown.Component.js b/src/Dropdown/Dropdown.Component.js index 9e36e15ca..7458c017d 100644 --- a/src/Dropdown/Dropdown.Component.js +++ b/src/Dropdown/Dropdown.Component.js @@ -28,7 +28,8 @@ export const DropdownComponent = () => { } control={} id='jhqD0555' - noArrow /> + noArrow + useArrowKeyNavigation /> @@ -49,7 +50,8 @@ export const DropdownComponent = () => { } id='jhqD0556' - noArrow /> + noArrow + useArrowKeyNavigation /> @@ -75,7 +77,8 @@ export const DropdownComponent = () => { } id='jhqD0557' - noArrow /> + noArrow + useArrowKeyNavigation /> @@ -97,7 +100,8 @@ export const DropdownComponent = () => { } id='jhqD0558' - noArrow /> + noArrow + useArrowKeyNavigation /> @@ -122,7 +126,8 @@ export const DropdownComponent = () => { } id='jhqD0559' - noArrow /> + noArrow + useArrowKeyNavigation /> @@ -143,7 +148,8 @@ export const DropdownComponent = () => { } id='jhqD0560' - noArrow /> + noArrow + useArrowKeyNavigation /> @@ -170,7 +176,8 @@ export const DropdownComponent = () => { } disabled id='jhqD0561' - noArrow /> + noArrow + useArrowKeyNavigation /> diff --git a/src/Dropdown/__stories__/Dropdown.stories.js b/src/Dropdown/__stories__/Dropdown.stories.js index eda091941..52ffa4aa5 100644 --- a/src/Dropdown/__stories__/Dropdown.stories.js +++ b/src/Dropdown/__stories__/Dropdown.stories.js @@ -25,7 +25,8 @@ storiesOf('Components|Dropdown', module) } control={} id='jhqD0555' - noArrow /> + noArrow + useArrowKeyNavigation /> )) .add('disable styles', () => ( @@ -45,7 +46,8 @@ storiesOf('Components|Dropdown', module) control={} disableStyles id='jhqD0555' - noArrow /> + noArrow + useArrowKeyNavigation /> )) .add('custom styles', () => ( @@ -65,6 +67,7 @@ storiesOf('Components|Dropdown', module) control={} disableStyles id='jhqD0555' - noArrow /> + noArrow + useArrowKeyNavigation /> )); diff --git a/src/Popover/Popover.js b/src/Popover/Popover.js index 6083130df..5e0c09ca0 100644 --- a/src/Popover/Popover.js +++ b/src/Popover/Popover.js @@ -5,6 +5,7 @@ import FocusManager from '../utils/focusManager/focusManager'; import Popper from '../utils/_Popper'; import { POPPER_PLACEMENTS } from '../utils/constants'; import PropTypes from 'prop-types'; +import tabbable from 'tabbable'; import withStyles from '../utils/WithStyles/WithStyles'; import React, { Component } from 'react'; @@ -27,7 +28,7 @@ class Popover extends Component { handleFocusManager = () => { if (this.state.isExpanded && this.popover) { - this.focusManager = new FocusManager(this.popover); + this.focusManager = new FocusManager(this.popover, this.props.useArrowKeyNavigation); } } @@ -38,13 +39,18 @@ class Popover extends Component { }); if (this.controlRef) { - this.controlRef.focus(); + if (tabbable.isTabbable(this.controlRef)) { + this.controlRef.focus(); + } else { + const firstTabbableNode = tabbable(this.controlRef)[0]; + firstTabbableNode && firstTabbableNode.focus(); + } } } }; render() { - let { + const { disableEdgeDetection, disableStyles, onClickOutside, @@ -56,6 +62,7 @@ class Popover extends Component { className, placement, popperProps, + useArrowKeyNavigation, ...rest } = this.props; @@ -113,6 +120,7 @@ Popover.propTypes = { noArrow: PropTypes.bool, placement: PropTypes.oneOf(POPPER_PLACEMENTS), popperProps: PropTypes.object, + useArrowKeyNavigation: PropTypes.bool, onClickOutside: PropTypes.func, onEscapeKey: PropTypes.func }; diff --git a/src/Popover/__stories__/Popover.stories.js b/src/Popover/__stories__/Popover.stories.js index 54a6c7c8c..1d3d9b3b0 100644 --- a/src/Popover/__stories__/Popover.stories.js +++ b/src/Popover/__stories__/Popover.stories.js @@ -23,7 +23,8 @@ storiesOf('Components|Popover', module) .add('Default', () => ( } /> + control={