Skip to content

Commit 3ede60d

Browse files
authored
Merge pull request #309 from suppermancool/issue-2540
Country selection - select by keyboard #2540
2 parents 59cb1e7 + e93e64b commit 3ede60d

File tree

5 files changed

+198
-42
lines changed

5 files changed

+198
-42
lines changed

components/Dropdown/Dropdown.jsx

Lines changed: 166 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -4,50 +4,183 @@ import React, { PropTypes } from 'react'
44
import classNames from 'classnames'
55
import enhanceDropdown from './enhanceDropdown'
66

7-
function Dropdown(props) {
8-
const { className, pointerShadow, noPointer, pointerLeft, isOpen, handleClick, theme, noAutoclose } = props
9-
const ddClasses = classNames('dropdown-wrap', {
10-
[`${className}`] : true,
11-
[`${ theme }`] : true
12-
})
13-
const ndClasses = classNames('Dropdown', {
14-
'pointer-shadow' : pointerShadow,
15-
'pointer-hide' : noPointer,
16-
'pointer-left' : pointerLeft,
17-
'no-autoclose' : noAutoclose,
18-
hide : !isOpen
19-
})
20-
21-
return (
22-
<div className={ddClasses} onClick={noAutoclose ? () => { } : handleClick}>
23-
{
24-
props.children.map((child, index) => {
25-
if (child.props.className.indexOf('dropdown-menu-header') > -1)
26-
return noAutoclose ? React.cloneElement(child, {
27-
onClick: handleClick,
28-
key: child.props.key || index
29-
}) : child
30-
})
31-
}
32-
33-
<div className = {ndClasses}>
7+
class Dropdown extends React.Component {
8+
constructor(props) {
9+
super(props)
10+
}
11+
12+
render() {
13+
const props = this.props
14+
const { children, className, pointerShadow, noPointer, pointerLeft, isOpen, handleClick, theme, noAutoclose, handleKeyboardNavigation } = props
15+
const ddClasses = classNames('dropdown-wrap', {
16+
[`${className}`] : true,
17+
[`${ theme }`] : true
18+
})
19+
const ndClasses = classNames('Dropdown', {
20+
'pointer-shadow' : pointerShadow,
21+
'pointer-hide' : noPointer,
22+
'pointer-left' : pointerLeft,
23+
'no-autoclose' : noAutoclose,
24+
hide : !isOpen
25+
})
26+
27+
let childSelectionIndex = -1
28+
const focusOnNextChild = () => {
29+
const listChild = this.listRef.getElementsByTagName('li')
30+
if (listChild.length === 0) {
31+
return
32+
}
33+
childSelectionIndex += 1
34+
if (childSelectionIndex >= listChild.length) {
35+
childSelectionIndex -= 1
36+
} else {
37+
listChild[childSelectionIndex].focus()
38+
}
39+
}
40+
const focusOnPreviousChild = () => {
41+
const listChild = this.listRef.getElementsByTagName('li')
42+
if (listChild.length === 0) {
43+
return
44+
}
45+
childSelectionIndex -= 1
46+
if (childSelectionIndex < 0) {
47+
childSelectionIndex = 0
48+
} else {
49+
listChild[childSelectionIndex].focus()
50+
}
51+
}
52+
let searchKey = ''
53+
let timer
54+
const focusOnCharacter = (value) => {
55+
searchKey += value
56+
if (timer) {
57+
clearTimeout(timer)
58+
}
59+
timer = setTimeout(() => { searchKey = '' }, 500)
60+
const listChild = this.listRef.getElementsByTagName('li')
61+
if (listChild.length === 0) {
62+
return
63+
}
64+
const length = listChild.length
65+
for (let i = 0; i < length; i++) {
66+
let textContent = listChild[i].textContent
67+
if (textContent && textContent.length > 0) {
68+
textContent = textContent.toLowerCase()
69+
const search = searchKey.toLowerCase()
70+
if (textContent.startsWith(search)) {
71+
childSelectionIndex = i
72+
listChild[i].focus()
73+
return true
74+
}
75+
}
76+
}
77+
return false
78+
}
79+
const onFocus = () => {
80+
this.containerRef.classList.add('focused')
81+
}
82+
const onBlur = () => {
83+
this.containerRef.classList.remove('focused')
84+
}
85+
const onKeydown = (e) => {
86+
if (!handleKeyboardNavigation) {
87+
return
88+
}
89+
const keyCode = e.keyCode
90+
if (keyCode === 32 || keyCode === 38 || keyCode === 40) { // space or Up/Down
91+
// open dropdown menu
92+
if (!noAutoclose && !isOpen) {
93+
e.preventDefault()
94+
handleClick(event)
95+
} else {
96+
if (keyCode === 40) {
97+
focusOnNextChild()
98+
} else if (keyCode === 38) {
99+
focusOnPreviousChild()
100+
}
101+
e.preventDefault()
102+
}
103+
} else if (isOpen) {
104+
const value = String.fromCharCode(e.keyCode)
105+
if (focusOnCharacter(value)) {
106+
e.preventDefault()
107+
}
108+
}
109+
}
110+
const onChildKeydown = (e) => {
111+
if (!handleKeyboardNavigation) {
112+
return
113+
}
114+
const keyCode = e.keyCode
115+
if (keyCode === 38 || keyCode === 40 || keyCode === 13) { // Up/Down or enter
116+
if (keyCode === 40) {
117+
focusOnNextChild()
118+
} else if (keyCode === 38) {
119+
focusOnPreviousChild()
120+
} else if (keyCode === 13) { // enter
121+
const listChild = this.listRef.getElementsByTagName('li')
122+
if (listChild.length === 0) {
123+
return
124+
}
125+
listChild[childSelectionIndex].click()
126+
this.handleKeyboardRef.focus()
127+
}
128+
e.preventDefault()
129+
} else {
130+
const value = String.fromCharCode(e.keyCode)
131+
if (focusOnCharacter(value)) {
132+
e.preventDefault()
133+
}
134+
}
135+
}
136+
137+
const setListRef = (c) => this.listRef = c
138+
const setContainerRef = (c) => this.containerRef = c
139+
const setHandleKeyboardRef = (c) => this.handleKeyboardRef = c
140+
141+
const childrenWithProps = React.Children.map(children, child =>
142+
React.cloneElement(child, {onKeyDown: onChildKeydown})
143+
)
144+
return (
145+
<div ref={setContainerRef} className={ddClasses} onClick={noAutoclose ? () => { } : handleClick}>
146+
{handleKeyboardNavigation && (<a ref={setHandleKeyboardRef} tabIndex="0" onFocus={onFocus} onBlur={onBlur} onKeyDown={onKeydown} className="handle-keyboard" href="javascript:;"></a>)}
34147
{
35-
props.children.map((child) => {
36-
if (child.props.className.indexOf('dropdown-menu-list') > -1)
37-
return child
148+
childrenWithProps.map((child, index) => {
149+
if (child.props.className.indexOf('dropdown-menu-header') > -1)
150+
return noAutoclose ? React.cloneElement(child, {
151+
onClick: handleClick,
152+
key: child.props.key || index
153+
}) : child
38154
})
39155
}
156+
<div ref={setListRef} className = {ndClasses}>
157+
{
158+
childrenWithProps.map((child) => {
159+
if (child.props.className.indexOf('dropdown-menu-list') > -1)
160+
return child
161+
})
162+
}
163+
</div>
40164
</div>
41-
</div>
42-
)
165+
)
166+
167+
}
43168
}
44169

45170
Dropdown.propTypes = {
46171
children: PropTypes.array.isRequired,
47172
/*
48173
If true, prevents dropdown closing when clicked inside dropdown
49174
*/
50-
noAutoclose: PropTypes.bool
175+
noAutoclose: PropTypes.bool,
176+
/*
177+
If true, prevents handle keyboard event
178+
*/
179+
handleKeyboardNavigation: PropTypes.bool
180+
}
181+
182+
Dropdown.defaultProps = {
183+
handleKeyboardNavigation: false
51184
}
52185

53186
export default enhanceDropdown(Dropdown)

components/Dropdown/Dropdown.scss

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,10 @@
5656
@include ellipsis;
5757
}
5858

59+
li:focus,
5960
li:hover {
6061
background-color: $tc-gray-neutral-dark;
62+
outline: none;
6163
}
6264
}
6365
}
@@ -75,6 +77,25 @@
7577
border-bottom: 2px solid $tc-gray-20;
7678
border-right: 2px solid $tc-gray-20;
7779
}
80+
81+
.dropdown-wrap {
82+
&.focused {
83+
box-shadow: 0 0 2px 0 rgba(6, 129, 255, 0.7);
84+
border: 1px solid $tc-dark-blue-100!important;
85+
}
86+
.handle-keyboard {
87+
position: absolute;
88+
width: 100%;
89+
max-height: 40px;
90+
top: 0;
91+
left: 0;
92+
height: 100%;
93+
94+
&:focus {
95+
outline: none;
96+
}
97+
}
98+
}
7899

79100
.Dropdown.hide {
80101
display: none;
@@ -155,8 +176,10 @@
155176
padding: 0 20px;
156177
@include ellipsis;
157178
}
179+
li:focus,
158180
li:hover {
159181
background-color: $tc-gray-neutral-dark;
182+
outline: none;
160183
}
161184
}
162185
}

components/Dropdown/DropdownExamples.jsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ const DropdownExamples = {
1919
<ul className="dropdown-menu-list">
2020
{
2121
items.map((link, i) => {
22-
return <li key={i}><a href="javascript:;">{link}</a></li>
22+
return <li tabIndex="-1" key={i}><a href="javascript:;">{link}</a></li>
2323
})
2424
}
2525
</ul>
@@ -32,7 +32,7 @@ const DropdownExamples = {
3232
<ul className="dropdown-menu-list">
3333
{
3434
items.map((link, i) => {
35-
return <li key={i}><a href="javascript:;">{link}</a></li>
35+
return <li tabIndex="-1" key={i}><a href="javascript:;">{link}</a></li>
3636
})
3737
}
3838
</ul>
@@ -45,7 +45,7 @@ const DropdownExamples = {
4545
<ul className="dropdown-menu-list">
4646
{
4747
items.map((link, i) => {
48-
return <li key={i}><a href="javascript:;">{link}</a></li>
48+
return <li tabIndex="-1" key={i}><a href="javascript:;">{link}</a></li>
4949
})
5050
}
5151
</ul>
@@ -58,7 +58,7 @@ const DropdownExamples = {
5858
<ul className="dropdown-menu-list">
5959
{
6060
items.map((link, i) => {
61-
return <li key={i}><a href="javascript:;">{link}</a></li>
61+
return <li tabIndex="-1" key={i}><a href="javascript:;">{link}</a></li>
6262
})
6363
}
6464
</ul>
@@ -71,7 +71,7 @@ const DropdownExamples = {
7171
<ul className="dropdown-menu-list">
7272
{
7373
items.map((link, i) => {
74-
return <li key={i}><a href="javascript:;">{link}</a></li>
74+
return <li tabIndex="-1" key={i}><a href="javascript:;">{link}</a></li>
7575
})
7676
}
7777
</ul>
@@ -84,7 +84,7 @@ const DropdownExamples = {
8484
<ul className="dropdown-menu-list">
8585
{
8686
items.map((link, i) => {
87-
return <li key={i}><a href="javascript:;">{link}</a></li>
87+
return <li tabIndex="-1" key={i}><a href="javascript:;">{link}</a></li>
8888
})
8989
}
9090
</ul>

components/Formsy/PhoneInput.jsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,14 +98,14 @@ class PhoneInput extends Component {
9898
min={minValue}
9999
max={maxValue}
100100
/>
101-
<Dropdown pointerShadow>
101+
<Dropdown handleKeyboardNavigation pointerShadow>
102102
<div className="dropdown-menu-header flex center middle">{this.state.currentCountry ? this.state.currentCountry.alpha3 : ''}
103103
<IconDown width={20} height={12} fill="#fff" wrapperClass="arrow" /></div>
104104
<ul className="dropdown-menu-list">
105105
{
106106
this.props.listCountry.map((country, i) => {
107107
/* eslint-disable react/jsx-no-bind */
108-
return <li className={(this.state.currentCountry.code === country.code) ? 'selected' : ''} onClick={() => this.choseCountry(country)} key={i}><a href="javascript:;">{country.name}</a></li>
108+
return <li tabIndex="-1" className={(this.state.currentCountry.code === country.code) ? 'selected' : ''} onClick={() => this.choseCountry(country)} key={i}><a href="javascript:;">{country.name}</a></li>
109109
})
110110
}
111111
</ul>

components/Formsy/PhoneInput.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@
6969
}
7070

7171
.Dropdown {
72-
width: 200px;
72+
width: auto;
7373
margin-left: -150px;
7474
margin-top: 30px;
7575
color: black;

0 commit comments

Comments
 (0)