diff --git a/components/Dropdown/Dropdown.jsx b/components/Dropdown/Dropdown.jsx index 46ccefeab..381dedfe3 100644 --- a/components/Dropdown/Dropdown.jsx +++ b/components/Dropdown/Dropdown.jsx @@ -4,42 +4,167 @@ 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 ( -
{ } : 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 - }) - } - -
+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 ( +
{ } : handleClick}> + {handleKeyboardNavigation && ()} { - 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 }) } +
+ { + childrenWithProps.map((child) => { + if (child.props.className.indexOf('dropdown-menu-list') > -1) + return child + }) + } +
-
- ) + ) + + } } Dropdown.propTypes = { @@ -47,7 +172,15 @@ Dropdown.propTypes = { /* 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) diff --git a/components/Dropdown/Dropdown.scss b/components/Dropdown/Dropdown.scss index 07cd37aff..86a5bc9c1 100644 --- a/components/Dropdown/Dropdown.scss +++ b/components/Dropdown/Dropdown.scss @@ -56,8 +56,10 @@ @include ellipsis; } + li:focus, li:hover { background-color: $tc-gray-neutral-dark; + outline: none; } } } @@ -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; @@ -155,8 +176,10 @@ padding: 0 20px; @include ellipsis; } + li:focus, li:hover { background-color: $tc-gray-neutral-dark; + outline: none; } } } diff --git a/components/Dropdown/DropdownExamples.jsx b/components/Dropdown/DropdownExamples.jsx index e6324037f..33af951d6 100644 --- a/components/Dropdown/DropdownExamples.jsx +++ b/components/Dropdown/DropdownExamples.jsx @@ -19,7 +19,7 @@ const DropdownExamples = { @@ -32,7 +32,7 @@ const DropdownExamples = { @@ -45,7 +45,7 @@ const DropdownExamples = { @@ -58,7 +58,7 @@ const DropdownExamples = { @@ -71,7 +71,7 @@ const DropdownExamples = { @@ -84,7 +84,7 @@ const DropdownExamples = { diff --git a/components/Formsy/PhoneInput.jsx b/components/Formsy/PhoneInput.jsx index 93a8da336..ce167f70b 100644 --- a/components/Formsy/PhoneInput.jsx +++ b/components/Formsy/PhoneInput.jsx @@ -98,14 +98,14 @@ class PhoneInput extends Component { min={minValue} max={maxValue} /> - +
{this.state.currentCountry ? this.state.currentCountry.alpha3 : ''}
    { this.props.listCountry.map((country, i) => { /* eslint-disable react/jsx-no-bind */ - return
  • this.choseCountry(country)} key={i}>{country.name}
  • + return
  • this.choseCountry(country)} key={i}>{country.name}
  • }) }
diff --git a/components/Formsy/PhoneInput.scss b/components/Formsy/PhoneInput.scss index d3d0b24ff..5cba08053 100644 --- a/components/Formsy/PhoneInput.scss +++ b/components/Formsy/PhoneInput.scss @@ -69,7 +69,7 @@ } .Dropdown { - width: 200px; + width: auto; margin-left: -150px; margin-top: 30px; color: black;