Skip to content

Commit 76a63d4

Browse files
authored
feat: allow configurable menu focus behavior (#286)
1 parent 30745a6 commit 76a63d4

File tree

6 files changed

+824
-475
lines changed

6 files changed

+824
-475
lines changed

package.json

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -66,49 +66,49 @@
6666
},
6767
"devDependencies": {
6868
"@4c/rollout": "^1.2.0",
69-
"@babel/cli": "^7.2.0",
69+
"@babel/cli": "^7.2.3",
7070
"@babel/core": "^7.2.2",
71-
"@babel/plugin-proposal-class-properties": "^7.2.1",
72-
"@babel/polyfill": "^7.0.0",
73-
"@babel/preset-env": "^7.2.0",
71+
"@babel/plugin-proposal-class-properties": "^7.3.0",
72+
"@babel/polyfill": "^7.2.5",
73+
"@babel/preset-env": "^7.3.1",
7474
"@babel/preset-react": "^7.0.0",
7575
"babel-eslint": "^10.0.1",
76-
"babel-plugin-add-module-exports": "^0.2.1",
76+
"babel-plugin-add-module-exports": "^1.0.0",
7777
"babel-plugin-istanbul": "^5.1.0",
7878
"chai": "^4.2.0",
79-
"codecov": "^3.1.0",
79+
"codecov": "^3.2.0",
8080
"enzyme": "^3.8.0",
81-
"enzyme-adapter-react-16": "^1.7.1",
82-
"eslint": "^5.10.0",
83-
"eslint-config-prettier": "^3.3.0",
84-
"eslint-plugin-mocha": "^5.2.0",
85-
"eslint-plugin-react": "^7.11.1",
86-
"gh-pages": "^1.2.0",
87-
"husky": "^0.14.3",
81+
"enzyme-adapter-react-16": "^1.9.1",
82+
"eslint": "^5.13.0",
83+
"eslint-config-prettier": "^4.0.0",
84+
"eslint-plugin-mocha": "^5.3.0",
85+
"eslint-plugin-react": "^7.12.4",
86+
"gh-pages": "^2.0.1",
87+
"husky": "^1.3.1",
8888
"jquery": "^3.3.1",
89-
"karma": "^3.1.4",
89+
"karma": "^4.0.0",
9090
"karma-chrome-launcher": "^2.2.0",
9191
"karma-coverage": "^1.1.2",
9292
"karma-mocha": "^1.3.0",
9393
"karma-mocha-reporter": "^2.2.5",
9494
"karma-sinon-chai": "^2.0.2",
9595
"karma-sourcemap-loader": "^0.3.7",
9696
"karma-webpack": "4.0.0-rc.1",
97-
"lint-staged": "^7.2.2",
97+
"lint-staged": "^8.1.4",
9898
"lodash": "^4.17.11",
9999
"mocha": "^5.2.0",
100-
"prettier": "^1.15.3",
101-
"react": "^16.6.3",
100+
"prettier": "^1.16.4",
101+
"react": "^16.8.2",
102102
"react-bootstrap": "^0.32.4",
103-
"react-dom": "^16.6.3",
103+
"react-dom": "^16.8.2",
104104
"react-live": "^1.12.0",
105-
"react-transition-group": "^2.5.1",
106-
"rimraf": "^2.6.2",
105+
"react-transition-group": "^2.5.3",
106+
"rimraf": "^2.6.3",
107107
"simulant": "^0.2.2",
108108
"sinon": "^6.2.0",
109109
"sinon-chai": "^3.3.0",
110-
"webpack": "^4.28.0",
110+
"webpack": "^4.29.3",
111111
"webpack-atoms": "^7.0.1",
112-
"webpack-cli": "^3.1.2"
112+
"webpack-cli": "^3.2.3"
113113
}
114114
}

src/Dropdown.js

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,16 @@ const propTypes = {
2929
*/
3030
drop: PropTypes.oneOf(['up', 'left', 'right', 'down']),
3131

32+
/**
33+
* Controls the focus behavior for when the Dropdown is opened. Set to
34+
* `true` to always focus the first menu item, `keyboard` to focus only when
35+
* navigating via the keyboard, or `false` to disable completely
36+
*
37+
* The Default behavior is `false` **unless** the Menu has a `role="menu"`
38+
* where it will default to `keyboard` to match the recommended [ARIA Authoring practices](https://www.w3.org/TR/wai-aria-practices-1.1/#menubutton).
39+
*/
40+
focusFirstItemOnShow: PropTypes.oneOf([false, true, 'keyboard']),
41+
3242
/**
3343
* A css slector string that will return __focusable__ menu items.
3444
* Selectors should be relative to the menu component:
@@ -125,6 +135,8 @@ class Dropdown extends React.Component {
125135
if (show && !prevOpen) {
126136
this.maybeFocusFirst();
127137
}
138+
this._lastSourceEvent = null;
139+
128140
if (!show && prevOpen) {
129141
// if focus hasn't already moved from the menu let's return it
130142
// to the toggle
@@ -159,7 +171,18 @@ class Dropdown extends React.Component {
159171
}
160172

161173
maybeFocusFirst() {
162-
if (!this.hasMenuRole()) return;
174+
const type = this._lastSourceEvent;
175+
let { focusFirstItemOnShow } = this.props;
176+
if (focusFirstItemOnShow == null) {
177+
focusFirstItemOnShow = this.hasMenuRole() ? 'keyboard' : false;
178+
}
179+
180+
if (
181+
focusFirstItemOnShow === false ||
182+
(focusFirstItemOnShow === 'keyboard' && !/^key.+$/.test(type))
183+
) {
184+
return;
185+
}
163186

164187
const { itemSelector } = this.props;
165188
let first = qsa(this.menu, itemSelector)[0];
@@ -172,16 +195,19 @@ class Dropdown extends React.Component {
172195

173196
handleKeyDown = event => {
174197
const { key, target } = event;
175-
const isInput = /input|textarea/i.test(target.tagName);
198+
176199
// Second only to https://github.com/twbs/bootstrap/blob/8cfbf6933b8a0146ac3fbc369f19e520bd1ebdac/js/src/dropdown.js#L400
177200
// in inscrutability
201+
const isInput = /input|textarea/i.test(target.tagName);
178202
if (
179203
isInput &&
180204
(key === ' ' || (key !== 'Escape' && this.menu.contains(target)))
181205
) {
182206
return;
183207
}
184208

209+
this._lastSourceEvent = event.type;
210+
185211
switch (key) {
186212
case 'ArrowUp': {
187213
let next = this.getNextFocusedChild(target, -1);
@@ -209,6 +235,7 @@ class Dropdown extends React.Component {
209235

210236
toggleOpen(event) {
211237
let show = !this.props.show;
238+
212239
this.props.onToggle(show, event);
213240
}
214241

src/Overlay.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import PropTypes from 'prop-types';
22
import elementType from 'prop-types-extra/lib/elementType';
3+
import componentOrElement from 'prop-types-extra/lib/componentOrElement';
34
import React from 'react';
45
import ReactDOM from 'react-dom';
56

@@ -168,6 +169,12 @@ Overlay.propTypes = {
168169
/** Specify where the overlay element is positioned in relation to the target element */
169170
placement: PropTypes.oneOf(placements),
170171

172+
/**
173+
* A Node, Component instance, or function that returns either. The `container` will have the Portal children
174+
* appended to it.
175+
*/
176+
container: PropTypes.oneOfType([componentOrElement, PropTypes.func]),
177+
171178
/**
172179
* Enables the Popper.js `flip` modifier, allowing the Overlay to
173180
* automatically adjust it's placement in case of overlap with the viewport or toggle.
@@ -211,7 +218,7 @@ Overlay.propTypes = {
211218
* Specify event for toggling overlay
212219
*/
213220
rootCloseEvent: RootCloseWrapper.propTypes.event,
214-
221+
215222
/**
216223
* Specify disabled for disable RootCloseWrapper
217224
*/
@@ -271,6 +278,7 @@ Overlay.propTypes = {
271278

272279
export default forwardRef(
273280
(props, ref) => (
281+
// eslint-disable-next-line react/prop-types
274282
<WaitForContainer container={props.container}>
275283
{container => <Overlay {...props} ref={ref} container={container} />}
276284
</WaitForContainer>

0 commit comments

Comments
 (0)