Skip to content

Country selection - select by keyboard #2540 #309

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jan 24, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
199 changes: 166 additions & 33 deletions components/Dropdown/Dropdown.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,50 +4,183 @@ import React, { PropTypes } from 'react'
import classNames from 'classnames'
import enhanceDropdown from './enhanceDropdown'

function Dropdown(props) {
const { className, pointerShadow, noPointer, pointerLeft, isOpen, handleClick, theme, noAutoclose } = props
const ddClasses = classNames('dropdown-wrap', {
[`${className}`] : true,
[`${ theme }`] : true
})
const ndClasses = classNames('Dropdown', {
'pointer-shadow' : pointerShadow,
'pointer-hide' : noPointer,
'pointer-left' : pointerLeft,
'no-autoclose' : noAutoclose,
hide : !isOpen
})

return (
<div className={ddClasses} onClick={noAutoclose ? () => { } : handleClick}>
{
props.children.map((child, index) => {
if (child.props.className.indexOf('dropdown-menu-header') > -1)
return noAutoclose ? React.cloneElement(child, {
onClick: handleClick,
key: child.props.key || index
}) : child
})
}

<div className = {ndClasses}>
class Dropdown extends React.Component {
constructor(props) {
super(props)
}

render() {
const props = this.props
const { children, className, pointerShadow, noPointer, pointerLeft, isOpen, handleClick, theme, noAutoclose, handleKeyboardNavigation } = props
const ddClasses = classNames('dropdown-wrap', {
[`${className}`] : true,
[`${ theme }`] : true
})
const ndClasses = classNames('Dropdown', {
'pointer-shadow' : pointerShadow,
'pointer-hide' : noPointer,
'pointer-left' : pointerLeft,
'no-autoclose' : noAutoclose,
hide : !isOpen
})

let childSelectionIndex = -1
const focusOnNextChild = () => {
const listChild = this.listRef.getElementsByTagName('li')
if (listChild.length === 0) {
return
}
childSelectionIndex += 1
if (childSelectionIndex >= listChild.length) {
childSelectionIndex -= 1
} else {
listChild[childSelectionIndex].focus()
}
}
const focusOnPreviousChild = () => {
const listChild = this.listRef.getElementsByTagName('li')
if (listChild.length === 0) {
return
}
childSelectionIndex -= 1
if (childSelectionIndex < 0) {
childSelectionIndex = 0
} else {
listChild[childSelectionIndex].focus()
}
}
let searchKey = ''
let timer
const focusOnCharacter = (value) => {
searchKey += value
if (timer) {
clearTimeout(timer)
}
timer = setTimeout(() => { searchKey = '' }, 500)
const listChild = this.listRef.getElementsByTagName('li')
if (listChild.length === 0) {
return
}
const length = listChild.length
for (let i = 0; i < length; i++) {
let textContent = listChild[i].textContent
if (textContent && textContent.length > 0) {
textContent = textContent.toLowerCase()
const search = searchKey.toLowerCase()
if (textContent.startsWith(search)) {
childSelectionIndex = i
listChild[i].focus()
return true
}
}
}
return false
}
const onFocus = () => {
this.containerRef.classList.add('focused')
}
const onBlur = () => {
this.containerRef.classList.remove('focused')
}
const onKeydown = (e) => {
if (!handleKeyboardNavigation) {
return
}
const keyCode = e.keyCode
if (keyCode === 32 || keyCode === 38 || keyCode === 40) { // space or Up/Down
// open dropdown menu
if (!noAutoclose && !isOpen) {
e.preventDefault()
handleClick(event)
} else {
if (keyCode === 40) {
focusOnNextChild()
} else if (keyCode === 38) {
focusOnPreviousChild()
}
e.preventDefault()
}
} else if (isOpen) {
const value = String.fromCharCode(e.keyCode)
if (focusOnCharacter(value)) {
e.preventDefault()
}
}
}
const onChildKeydown = (e) => {
if (!handleKeyboardNavigation) {
return
}
const keyCode = e.keyCode
if (keyCode === 38 || keyCode === 40 || keyCode === 13) { // Up/Down or enter
if (keyCode === 40) {
focusOnNextChild()
} else if (keyCode === 38) {
focusOnPreviousChild()
} else if (keyCode === 13) { // enter
const listChild = this.listRef.getElementsByTagName('li')
if (listChild.length === 0) {
return
}
listChild[childSelectionIndex].click()
this.handleKeyboardRef.focus()
}
e.preventDefault()
} else {
const value = String.fromCharCode(e.keyCode)
if (focusOnCharacter(value)) {
e.preventDefault()
}
}
}

const setListRef = (c) => this.listRef = c
const setContainerRef = (c) => this.containerRef = c
const setHandleKeyboardRef = (c) => this.handleKeyboardRef = c

const childrenWithProps = React.Children.map(children, child =>
React.cloneElement(child, {onKeyDown: onChildKeydown})
)
return (
<div ref={setContainerRef} className={ddClasses} onClick={noAutoclose ? () => { } : handleClick}>
{handleKeyboardNavigation && (<a ref={setHandleKeyboardRef} tabIndex="0" onFocus={onFocus} onBlur={onBlur} onKeyDown={onKeydown} className="handle-keyboard" href="javascript:;"></a>)}
{
props.children.map((child) => {
if (child.props.className.indexOf('dropdown-menu-list') > -1)
return child
childrenWithProps.map((child, index) => {
if (child.props.className.indexOf('dropdown-menu-header') > -1)
return noAutoclose ? React.cloneElement(child, {
onClick: handleClick,
key: child.props.key || index
}) : child
})
}
<div ref={setListRef} className = {ndClasses}>
{
childrenWithProps.map((child) => {
if (child.props.className.indexOf('dropdown-menu-list') > -1)
return child
})
}
</div>
</div>
</div>
)
)

}
}

Dropdown.propTypes = {
children: PropTypes.array.isRequired,
/*
If true, prevents dropdown closing when clicked inside dropdown
*/
noAutoclose: PropTypes.bool
noAutoclose: PropTypes.bool,
/*
If true, prevents handle keyboard event
*/
handleKeyboardNavigation: PropTypes.bool
}

Dropdown.defaultProps = {
handleKeyboardNavigation: false
}

export default enhanceDropdown(Dropdown)
23 changes: 23 additions & 0 deletions components/Dropdown/Dropdown.scss
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,10 @@
@include ellipsis;
}

li:focus,
li:hover {
background-color: $tc-gray-neutral-dark;
outline: none;
}
}
}
Expand All @@ -75,6 +77,25 @@
border-bottom: 2px solid $tc-gray-20;
border-right: 2px solid $tc-gray-20;
}

.dropdown-wrap {
&.focused {
box-shadow: 0 0 2px 0 rgba(6, 129, 255, 0.7);
border: 1px solid $tc-dark-blue-100!important;
}
.handle-keyboard {
position: absolute;
width: 100%;
max-height: 40px;
top: 0;
left: 0;
height: 100%;

&:focus {
outline: none;
}
}
}

.Dropdown.hide {
display: none;
Expand Down Expand Up @@ -155,8 +176,10 @@
padding: 0 20px;
@include ellipsis;
}
li:focus,
li:hover {
background-color: $tc-gray-neutral-dark;
outline: none;
}
}
}
Expand Down
12 changes: 6 additions & 6 deletions components/Dropdown/DropdownExamples.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const DropdownExamples = {
<ul className="dropdown-menu-list">
{
items.map((link, i) => {
return <li key={i}><a href="javascript:;">{link}</a></li>
return <li tabIndex="-1" key={i}><a href="javascript:;">{link}</a></li>
})
}
</ul>
Expand All @@ -32,7 +32,7 @@ const DropdownExamples = {
<ul className="dropdown-menu-list">
{
items.map((link, i) => {
return <li key={i}><a href="javascript:;">{link}</a></li>
return <li tabIndex="-1" key={i}><a href="javascript:;">{link}</a></li>
})
}
</ul>
Expand All @@ -45,7 +45,7 @@ const DropdownExamples = {
<ul className="dropdown-menu-list">
{
items.map((link, i) => {
return <li key={i}><a href="javascript:;">{link}</a></li>
return <li tabIndex="-1" key={i}><a href="javascript:;">{link}</a></li>
})
}
</ul>
Expand All @@ -58,7 +58,7 @@ const DropdownExamples = {
<ul className="dropdown-menu-list">
{
items.map((link, i) => {
return <li key={i}><a href="javascript:;">{link}</a></li>
return <li tabIndex="-1" key={i}><a href="javascript:;">{link}</a></li>
})
}
</ul>
Expand All @@ -71,7 +71,7 @@ const DropdownExamples = {
<ul className="dropdown-menu-list">
{
items.map((link, i) => {
return <li key={i}><a href="javascript:;">{link}</a></li>
return <li tabIndex="-1" key={i}><a href="javascript:;">{link}</a></li>
})
}
</ul>
Expand All @@ -84,7 +84,7 @@ const DropdownExamples = {
<ul className="dropdown-menu-list">
{
items.map((link, i) => {
return <li key={i}><a href="javascript:;">{link}</a></li>
return <li tabIndex="-1" key={i}><a href="javascript:;">{link}</a></li>
})
}
</ul>
Expand Down
4 changes: 2 additions & 2 deletions components/Formsy/PhoneInput.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,14 +98,14 @@ class PhoneInput extends Component {
min={minValue}
max={maxValue}
/>
<Dropdown pointerShadow>
<Dropdown handleKeyboardNavigation pointerShadow>
<div className="dropdown-menu-header flex center middle">{this.state.currentCountry ? this.state.currentCountry.alpha3 : ''}
<IconDown width={20} height={12} fill="#fff" wrapperClass="arrow" /></div>
<ul className="dropdown-menu-list">
{
this.props.listCountry.map((country, i) => {
/* eslint-disable react/jsx-no-bind */
return <li className={(this.state.currentCountry.code === country.code) ? 'selected' : ''} onClick={() => this.choseCountry(country)} key={i}><a href="javascript:;">{country.name}</a></li>
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>
})
}
</ul>
Expand Down
2 changes: 1 addition & 1 deletion components/Formsy/PhoneInput.scss
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@
}

.Dropdown {
width: 200px;
width: auto;
margin-left: -150px;
margin-top: 30px;
color: black;
Expand Down