From 4ce110aa72aa81b03d9f667284483468df48ed3e Mon Sep 17 00:00:00 2001 From: suppermancool Date: Tue, 22 Jan 2019 11:59:25 +0700 Subject: [PATCH 1/5] Country selection - select by keyboard #2540 Country selection - select by keyboard #2540 --- components/Dropdown/Dropdown.jsx | 179 ++++++++++++++++++----- components/Dropdown/Dropdown.scss | 15 ++ components/Dropdown/DropdownExamples.jsx | 12 +- components/Formsy/PhoneInput.jsx | 2 +- 4 files changed, 168 insertions(+), 40 deletions(-) diff --git a/components/Dropdown/Dropdown.jsx b/components/Dropdown/Dropdown.jsx index 46ccefeab..5141eda2b 100644 --- a/components/Dropdown/Dropdown.jsx +++ b/components/Dropdown/Dropdown.jsx @@ -4,42 +4,147 @@ 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 = 0 + } + listChild[childSelectionIndex].focus() + } + const focusOnPreviousChild = () => { + const listChild = this.listRef.getElementsByTagName('li') + if (listChild.length === 0) { + return + } + childSelectionIndex -= 1 + if (childSelectionIndex < 0) { + childSelectionIndex = listChild.length - 1 + } + listChild[childSelectionIndex].focus() + } + const focusOnCharacter = (value) => { + const listChild = this.listRef.getElementsByTagName('li') + if (listChild.length === 0) { + return + } + const length = listChild.length + for (let i = 0; i < length; i++) { + const textContent = listChild[i].textContent + if (textContent && textContent.length > 0) { + const firstChar = textContent.charAt(0) + if (firstChar === value || firstChar === value.toLowerCase()) { + listChild[i].focus() + return true + } + } + } + return false + } + 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) { + const listChild = this.listRef.getElementsByTagName('li') + if (listChild.length === 0) { + return + } + listChild[childSelectionIndex].click() + } + e.preventDefault() + } else { + const value = String.fromCharCode(e.keyCode) + if (focusOnCharacter(value)) { + e.preventDefault() + } + } + } + + const setListRef = (c) => this.listRef = 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 +152,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..3f887c7f6 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,17 @@ border-bottom: 2px solid $tc-gray-20; border-right: 2px solid $tc-gray-20; } + + .dropdown-wrap { + .handle-keyboard { + position: absolute; + width: 100%; + max-height: 40px; + top: 0; + left: 0; + height: 100%; + } + } .Dropdown.hide { display: none; @@ -155,8 +168,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..9ab1e4bb3 100644 --- a/components/Formsy/PhoneInput.jsx +++ b/components/Formsy/PhoneInput.jsx @@ -98,7 +98,7 @@ class PhoneInput extends Component { min={minValue} max={maxValue} /> - +
{this.state.currentCountry ? this.state.currentCountry.alpha3 : ''}
    From 010550bd4f4836ccf3a4fcbe2d2f315a901e538d Mon Sep 17 00:00:00 2001 From: suppermancool Date: Tue, 22 Jan 2019 12:28:16 +0700 Subject: [PATCH 2/5] Country selection dropdown should not have horizontal scroll Country selection dropdown should not have horizontal scroll --- components/Dropdown/Dropdown.jsx | 2 +- components/Dropdown/Dropdown.scss | 6 ++++++ components/Formsy/PhoneInput.jsx | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/components/Dropdown/Dropdown.jsx b/components/Dropdown/Dropdown.jsx index 5141eda2b..c3df0dad3 100644 --- a/components/Dropdown/Dropdown.jsx +++ b/components/Dropdown/Dropdown.jsx @@ -123,7 +123,7 @@ class Dropdown extends React.Component { ) return (
    { } : handleClick}> - {handleKeyboardNavigation && ()} + {handleKeyboardNavigation && ()} { childrenWithProps.map((child, index) => { if (child.props.className.indexOf('dropdown-menu-header') > -1) diff --git a/components/Dropdown/Dropdown.scss b/components/Dropdown/Dropdown.scss index 3f887c7f6..4bb0e1a0a 100644 --- a/components/Dropdown/Dropdown.scss +++ b/components/Dropdown/Dropdown.scss @@ -37,6 +37,8 @@ font-size: 12px; display: block; line-height: 26px; + overflow: hidden; + white-space: pre-line; } } } @@ -86,6 +88,10 @@ top: 0; left: 0; height: 100%; + + &:focus { + outline: 5px auto -webkit-focus-ring-color; + } } } diff --git a/components/Formsy/PhoneInput.jsx b/components/Formsy/PhoneInput.jsx index 9ab1e4bb3..ce167f70b 100644 --- a/components/Formsy/PhoneInput.jsx +++ b/components/Formsy/PhoneInput.jsx @@ -105,7 +105,7 @@ class PhoneInput extends Component { { 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}
  • }) }
From 2ea1c06bbe666e6c04d0f408ad5f2e939b250e4e Mon Sep 17 00:00:00 2001 From: suppermancool Date: Thu, 24 Jan 2019 13:48:34 +0700 Subject: [PATCH 3/5] https://github.com/appirio-tech/connect-app/pull/2759#pullrequestreview-195864593 https://github.com/appirio-tech/connect-app/pull/2759#pullrequestreview-195864593 --- components/Dropdown/Dropdown.jsx | 29 +++++++++++++++++++++++------ components/Dropdown/Dropdown.scss | 8 +++++--- components/Formsy/PhoneInput.scss | 2 +- 3 files changed, 29 insertions(+), 10 deletions(-) diff --git a/components/Dropdown/Dropdown.jsx b/components/Dropdown/Dropdown.jsx index c3df0dad3..922cb4a24 100644 --- a/components/Dropdown/Dropdown.jsx +++ b/components/Dropdown/Dropdown.jsx @@ -47,17 +47,25 @@ class Dropdown extends React.Component { } 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++) { - const textContent = listChild[i].textContent + let textContent = listChild[i].textContent if (textContent && textContent.length > 0) { - const firstChar = textContent.charAt(0) - if (firstChar === value || firstChar === value.toLowerCase()) { + textContent = textContent.toLowerCase() + const search = searchKey.toLowerCase() + if (textContent.startsWith(search)) { listChild[i].focus() return true } @@ -65,6 +73,12 @@ class Dropdown extends React.Component { } return false } + const onFocus = () => { + this.containerRef.classList.add('focused') + } + const onBlur = () => { + this.containerRef.classList.remove('focused') + } const onKeydown = (e) => { if (!handleKeyboardNavigation) { return @@ -100,12 +114,13 @@ class Dropdown extends React.Component { focusOnNextChild() } else if (keyCode === 38) { focusOnPreviousChild() - } else if (keyCode === 13) { + } 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 { @@ -117,13 +132,15 @@ class Dropdown extends React.Component { } 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 && ()} +
{ } : handleClick}> + {handleKeyboardNavigation && ()} { childrenWithProps.map((child, index) => { if (child.props.className.indexOf('dropdown-menu-header') > -1) diff --git a/components/Dropdown/Dropdown.scss b/components/Dropdown/Dropdown.scss index 4bb0e1a0a..86a5bc9c1 100644 --- a/components/Dropdown/Dropdown.scss +++ b/components/Dropdown/Dropdown.scss @@ -37,8 +37,6 @@ font-size: 12px; display: block; line-height: 26px; - overflow: hidden; - white-space: pre-line; } } } @@ -81,6 +79,10 @@ } .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%; @@ -90,7 +92,7 @@ height: 100%; &:focus { - outline: 5px auto -webkit-focus-ring-color; + outline: none; } } } 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; From 19805e65a780a96819f895bebec2f3a86e4df4ab Mon Sep 17 00:00:00 2001 From: suppermancool Date: Thu, 24 Jan 2019 14:34:41 +0700 Subject: [PATCH 4/5] fix fail select when search by key #2759 fix fail select when search by key #2759 --- components/Dropdown/Dropdown.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/components/Dropdown/Dropdown.jsx b/components/Dropdown/Dropdown.jsx index 922cb4a24..d22fd0222 100644 --- a/components/Dropdown/Dropdown.jsx +++ b/components/Dropdown/Dropdown.jsx @@ -66,6 +66,7 @@ class Dropdown extends React.Component { textContent = textContent.toLowerCase() const search = searchKey.toLowerCase() if (textContent.startsWith(search)) { + childSelectionIndex = i listChild[i].focus() return true } From e93e64b2651a5ee1fe9d453d19a5e0c63ce9b07e Mon Sep 17 00:00:00 2001 From: suppermancool Date: Thu, 24 Jan 2019 15:10:28 +0700 Subject: [PATCH 5/5] fix key up/down bound issue fix key up/down bound issue --- components/Dropdown/Dropdown.jsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/components/Dropdown/Dropdown.jsx b/components/Dropdown/Dropdown.jsx index d22fd0222..381dedfe3 100644 --- a/components/Dropdown/Dropdown.jsx +++ b/components/Dropdown/Dropdown.jsx @@ -31,10 +31,11 @@ class Dropdown extends React.Component { return } childSelectionIndex += 1 - if (childSelectionIndex > listChild.length) { - childSelectionIndex = 0 + if (childSelectionIndex >= listChild.length) { + childSelectionIndex -= 1 + } else { + listChild[childSelectionIndex].focus() } - listChild[childSelectionIndex].focus() } const focusOnPreviousChild = () => { const listChild = this.listRef.getElementsByTagName('li') @@ -43,9 +44,10 @@ class Dropdown extends React.Component { } childSelectionIndex -= 1 if (childSelectionIndex < 0) { - childSelectionIndex = listChild.length - 1 + childSelectionIndex = 0 + } else { + listChild[childSelectionIndex].focus() } - listChild[childSelectionIndex].focus() } let searchKey = '' let timer