diff --git a/src/components/views/auth/CountryDropdown.js b/src/components/views/auth/CountryDropdown.js index d8aa88c798..567bcf59ef 100644 --- a/src/components/views/auth/CountryDropdown.js +++ b/src/components/views/auth/CountryDropdown.js @@ -21,6 +21,7 @@ import sdk from '../../../index'; import { COUNTRIES } from '../../../phonenumber'; import SdkConfig from "../../../SdkConfig"; +import { _t } from "../../../languageHandler"; const COUNTRIES_BY_ISO2 = {}; for (const c of COUNTRIES) { @@ -130,10 +131,17 @@ export default class CountryDropdown extends React.Component { // values between mounting and the initial value propgating const value = this.props.value || this.state.defaultCountry.iso2; - return { options } ; diff --git a/src/components/views/elements/Dropdown.js b/src/components/views/elements/Dropdown.js index 4c5e14b3ba..67a8663b3e 100644 --- a/src/components/views/elements/Dropdown.js +++ b/src/components/views/elements/Dropdown.js @@ -1,6 +1,7 @@ /* Copyright 2017 Vector Creations Ltd Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> +Copyright 2019 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,11 +16,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; +import React, {createRef} from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; import AccessibleButton from './AccessibleButton'; import { _t } from '../../../languageHandler'; +import {Key} from "../../../Keyboard"; class MenuOption extends React.Component { constructor(props) { @@ -48,9 +50,14 @@ class MenuOption extends React.Component { mx_Dropdown_option_highlight: this.props.highlighted, }); - return
{ this.props.children }
; @@ -66,6 +73,7 @@ MenuOption.propTypes = { dropdownKey: PropTypes.string, onClick: PropTypes.func.isRequired, onMouseEnter: PropTypes.func.isRequired, + inputRef: PropTypes.any, }; /* @@ -111,6 +119,7 @@ export default class Dropdown extends React.Component { } componentWillMount() { + this._button = createRef(); // Listen for all clicks on the document so we can close the // menu when the user clicks somewhere else document.addEventListener('click', this._onDocumentClick, false); @@ -169,40 +178,47 @@ export default class Dropdown extends React.Component { } } - _onMenuOptionClick(dropdownKey) { + _close() { this.setState({ expanded: false, }); + // their focus was on the input, its getting unmounted, move it to the button + if (this._button.current) { + this._button.current.focus(); + } + } + + _onMenuOptionClick(dropdownKey) { + this._close(); this.props.onOptionChange(dropdownKey); } _onInputKeyPress(e) { - // This needs to be on the keypress event because otherwise - // it can't cancel the form submission - if (e.key == 'Enter') { - this.setState({ - expanded: false, - }); - this.props.onOptionChange(this.state.highlightedOption); + // This needs to be on the keypress event because otherwise it can't cancel the form submission + if (e.key === Key.ENTER) { e.preventDefault(); } } _onInputKeyUp(e) { - // These keys don't generate keypress events and so needs to - // be on keyup - if (e.key == 'Escape') { - this.setState({ - expanded: false, - }); - } else if (e.key == 'ArrowDown') { - this.setState({ - highlightedOption: this._nextOption(this.state.highlightedOption), - }); - } else if (e.key == 'ArrowUp') { - this.setState({ - highlightedOption: this._prevOption(this.state.highlightedOption), - }); + // These keys don't generate keypress events and so needs to be on keyup + switch (e.key) { + case Key.ENTER: + this.props.onOptionChange(this.state.highlightedOption); + // fallthrough + case Key.ESCAPE: + this._close(); + break; + case Key.ARROW_DOWN: + this.setState({ + highlightedOption: this._nextOption(this.state.highlightedOption), + }); + break; + case Key.ARROW_UP: + this.setState({ + highlightedOption: this._prevOption(this.state.highlightedOption), + }); + break; } } @@ -250,20 +266,34 @@ export default class Dropdown extends React.Component { return keys[(index - 1) % keys.length]; } + _scrollIntoView(node) { + if (node) { + node.scrollIntoView({ + block: "nearest", + behavior: "auto", + }); + } + } + _getMenuOptions() { const options = React.Children.map(this.props.children, (child) => { + const highlighted = this.state.highlightedOption === child.key; return ( - { child } ); }); if (options.length === 0) { - return [
+ return [
{ _t("No results") }
]; } @@ -279,23 +309,36 @@ export default class Dropdown extends React.Component { let menu; if (this.state.expanded) { if (this.props.searchEnabled) { - currentValue = ; + currentValue = ( + + ); } - menu =
- { this._getMenuOptions() } -
; + menu = ( +
+ { this._getMenuOptions() } +
+ ); } if (!currentValue) { const selectedChild = this.props.getShortOption ? this.props.getShortOption(this.props.value) : this.childrenByKey[this.props.value]; - currentValue =
+ currentValue =
{ selectedChild }
; } @@ -311,9 +354,18 @@ export default class Dropdown extends React.Component { // Note the menu sits inside the AccessibleButton div so it's anchored // to the input, but overflows below it. The root contains both. return
- + { currentValue } - + { menu }
; @@ -321,6 +373,7 @@ export default class Dropdown extends React.Component { } Dropdown.propTypes = { + id: PropTypes.string.isRequired, // The width that the dropdown should be. If specified, // the dropped-down part of the menu will be set to this // width. @@ -340,4 +393,6 @@ Dropdown.propTypes = { value: PropTypes.string, // negative for consistency with HTML disabled: PropTypes.bool, + // ARIA label + label: PropTypes.string.isRequired, }; diff --git a/src/components/views/elements/LanguageDropdown.js b/src/components/views/elements/LanguageDropdown.js index 451c97d958..ebe26cfad8 100644 --- a/src/components/views/elements/LanguageDropdown.js +++ b/src/components/views/elements/LanguageDropdown.js @@ -21,6 +21,7 @@ import PropTypes from 'prop-types'; import sdk from '../../../index'; import * as languageHandler from '../../../languageHandler'; import SettingsStore from "../../../settings/SettingsStore"; +import { _t } from "../../../languageHandler"; function languageMatchesSearchQuery(query, language) { if (language.label.toUpperCase().indexOf(query.toUpperCase()) == 0) return true; @@ -105,9 +106,14 @@ export default class LanguageDropdown extends React.Component { value = this.props.value || language; } - return { options } ; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 0c855aed64..b9894152e1 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1268,6 +1268,7 @@ "Rotate Right": "Rotate Right", "Rotate clockwise": "Rotate clockwise", "Download this file": "Download this file", + "Language Dropdown": "Language Dropdown", "Manage Integrations": "Manage Integrations", "%(nameList)s %(transitionList)s": "%(nameList)s %(transitionList)s", "%(severalUsers)sjoined %(count)s times|other": "%(severalUsers)sjoined %(count)s times", @@ -1633,6 +1634,7 @@ "User Status": "User Status", "powered by Matrix": "powered by Matrix", "This homeserver would like to make sure you are not a robot.": "This homeserver would like to make sure you are not a robot.", + "Country Dropdown": "Country Dropdown", "Custom Server Options": "Custom Server Options", "You can use the custom server options to sign into other Matrix servers by specifying a different homeserver URL. This allows you to use this app with an existing Matrix account on a different homeserver.": "You can use the custom server options to sign into other Matrix servers by specifying a different homeserver URL. This allows you to use this app with an existing Matrix account on a different homeserver.", "To continue, please enter your password.": "To continue, please enter your password.",