From 44bbf609732e4169e2c6e979f8d17d750c40ff38 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 6 Jul 2021 09:56:02 +0100 Subject: [PATCH] Convert Dropdown to Typescript --- .../elements/{Dropdown.js => Dropdown.tsx} | 239 ++++++++---------- 1 file changed, 108 insertions(+), 131 deletions(-) rename src/components/views/elements/{Dropdown.js => Dropdown.tsx} (68%) diff --git a/src/components/views/elements/Dropdown.js b/src/components/views/elements/Dropdown.tsx similarity index 68% rename from src/components/views/elements/Dropdown.js rename to src/components/views/elements/Dropdown.tsx index f95247e9ae..c2ff59a2d3 100644 --- a/src/components/views/elements/Dropdown.js +++ b/src/components/views/elements/Dropdown.tsx @@ -1,7 +1,6 @@ /* -Copyright 2017 Vector Creations Ltd Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2017 - 2021 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. @@ -16,34 +15,38 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { createRef } from 'react'; -import PropTypes from 'prop-types'; +import React, { ChangeEvent, createRef, CSSProperties, ReactElement, ReactNode, Ref } from 'react'; import classnames from 'classnames'; + import AccessibleButton from './AccessibleButton'; import { _t } from '../../../languageHandler'; import { Key } from "../../../Keyboard"; import { replaceableComponent } from "../../../utils/replaceableComponent"; -class MenuOption extends React.Component { - constructor(props) { - super(props); - this._onMouseEnter = this._onMouseEnter.bind(this); - this._onClick = this._onClick.bind(this); - } +interface IMenuOptionProps { + children: ReactElement; + highlighted?: boolean; + dropdownKey: string; + id?: string; + inputRef?: Ref; + onClick(dropdownKey: string): void; + onMouseEnter(dropdownKey: string): void; +} +class MenuOption extends React.Component { static defaultProps = { disabled: false, }; - _onMouseEnter() { + private onMouseEnter = () => { this.props.onMouseEnter(this.props.dropdownKey); - } + }; - _onClick(e) { + private onClick = (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); this.props.onClick(this.props.dropdownKey); - } + }; render() { const optClasses = classnames({ @@ -54,8 +57,8 @@ class MenuOption extends React.Component { return
{ + private readonly buttonRef = createRef(); + private dropdownRootElement: HTMLDivElement = null; + private ignoreEvent: MouseEvent = null; + private childrenByKey: Record = {}; + + constructor(props: IProps) { super(props); - this.dropdownRootElement = null; - this.ignoreEvent = null; + this.reindexChildren(this.props.children); - this._onInputClick = this._onInputClick.bind(this); - this._onRootClick = this._onRootClick.bind(this); - this._onDocumentClick = this._onDocumentClick.bind(this); - this._onMenuOptionClick = this._onMenuOptionClick.bind(this); - this._onInputChange = this._onInputChange.bind(this); - this._collectRoot = this._collectRoot.bind(this); - this._collectInputTextBox = this._collectInputTextBox.bind(this); - this._setHighlightedOption = this._setHighlightedOption.bind(this); - - this.inputTextBox = null; - - this._reindexChildren(this.props.children); - - const firstChild = React.Children.toArray(props.children)[0]; + const firstChild = React.Children.toArray(props.children)[0] as ReactElement; this.state = { // True if the menu is dropped-down expanded: false, // The key of the highlighted option // (the option that would become selected if you pressed enter) - highlightedOption: firstChild ? firstChild.key : null, + highlightedOption: firstChild ? firstChild.key as string : null, // the current search query searchQuery: '', }; - } - // TODO: [REACT-WARNING] Replace component with real class, use constructor for refs - UNSAFE_componentWillMount() { // eslint-disable-line camelcase - 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); + document.addEventListener('click', this.onDocumentClick, false); } componentWillUnmount() { - document.removeEventListener('click', this._onDocumentClick, false); + document.removeEventListener('click', this.onDocumentClick, false); } // TODO: [REACT-WARNING] Replace with appropriate lifecycle event @@ -135,21 +144,21 @@ export default class Dropdown extends React.Component { if (!nextProps.children || nextProps.children.length === 0) { return; } - this._reindexChildren(nextProps.children); + this.reindexChildren(nextProps.children); const firstChild = nextProps.children[0]; this.setState({ highlightedOption: firstChild ? firstChild.key : null, }); } - _reindexChildren(children) { + private reindexChildren(children: ReactElement[]): void { this.childrenByKey = {}; React.Children.forEach(children, (child) => { this.childrenByKey[child.key] = child; }); } - _onDocumentClick(ev) { + private onDocumentClick = (ev: MouseEvent) => { // Close the dropdown if the user clicks anywhere that isn't // within our root element if (ev !== this.ignoreEvent) { @@ -157,9 +166,9 @@ export default class Dropdown extends React.Component { expanded: false, }); } - } + }; - _onRootClick(ev) { + private onRootClick = (ev: MouseEvent) => { // This captures any clicks that happen within our elements, // such that we can then ignore them when they're seen by the // click listener on the document handler, ie. not close the @@ -167,9 +176,9 @@ export default class Dropdown extends React.Component { // NB. We can't just stopPropagation() because then the event // doesn't reach the React onClick(). this.ignoreEvent = ev; - } + }; - _onInputClick(ev) { + private onInputClick = (ev: React.MouseEvent) => { if (this.props.disabled) return; if (!this.state.expanded) { @@ -178,24 +187,24 @@ export default class Dropdown extends React.Component { }); ev.preventDefault(); } - } + }; - _close() { + private 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(); + if (this.buttonRef.current) { + this.buttonRef.current.focus(); } } - _onMenuOptionClick(dropdownKey) { - this._close(); + private onMenuOptionClick = (dropdownKey: string) => { + this.close(); this.props.onOptionChange(dropdownKey); - } + }; - _onInputKeyDown = (e) => { + private onInputKeyDown = (e: React.KeyboardEvent) => { let handled = true; // These keys don't generate keypress events and so needs to be on keyup @@ -204,16 +213,16 @@ export default class Dropdown extends React.Component { this.props.onOptionChange(this.state.highlightedOption); // fallthrough case Key.ESCAPE: - this._close(); + this.close(); break; case Key.ARROW_DOWN: this.setState({ - highlightedOption: this._nextOption(this.state.highlightedOption), + highlightedOption: this.nextOption(this.state.highlightedOption), }); break; case Key.ARROW_UP: this.setState({ - highlightedOption: this._prevOption(this.state.highlightedOption), + highlightedOption: this.prevOption(this.state.highlightedOption), }); break; default: @@ -224,53 +233,46 @@ export default class Dropdown extends React.Component { e.preventDefault(); e.stopPropagation(); } - } + }; - _onInputChange(e) { + private onInputChange = (e: ChangeEvent) => { this.setState({ - searchQuery: e.target.value, + searchQuery: e.currentTarget.value, }); if (this.props.onSearchChange) { - this.props.onSearchChange(e.target.value); + this.props.onSearchChange(e.currentTarget.value); } - } + }; - _collectRoot(e) { + private collectRoot = (e: HTMLDivElement) => { if (this.dropdownRootElement) { - this.dropdownRootElement.removeEventListener( - 'click', this._onRootClick, false, - ); + this.dropdownRootElement.removeEventListener('click', this.onRootClick, false); } if (e) { - e.addEventListener('click', this._onRootClick, false); + e.addEventListener('click', this.onRootClick, false); } this.dropdownRootElement = e; - } + }; - _collectInputTextBox(e) { - this.inputTextBox = e; - if (e) e.focus(); - } - - _setHighlightedOption(optionKey) { + private setHighlightedOption = (optionKey: string) => { this.setState({ highlightedOption: optionKey, }); - } + }; - _nextOption(optionKey) { + private nextOption(optionKey: string): string { const keys = Object.keys(this.childrenByKey); const index = keys.indexOf(optionKey); return keys[(index + 1) % keys.length]; } - _prevOption(optionKey) { + private prevOption(optionKey: string): string { const keys = Object.keys(this.childrenByKey); const index = keys.indexOf(optionKey); return keys[(index - 1) % keys.length]; } - _scrollIntoView(node) { + private scrollIntoView(node: Element) { if (node) { node.scrollIntoView({ block: "nearest", @@ -279,18 +281,18 @@ export default class Dropdown extends React.Component { } } - _getMenuOptions() { + private getMenuOptions() { const options = React.Children.map(this.props.children, (child) => { const highlighted = this.state.highlightedOption === child.key; return ( { child } @@ -307,7 +309,7 @@ export default class Dropdown extends React.Component { render() { let currentValue; - const menuStyle = {}; + const menuStyle: CSSProperties = {}; if (this.props.menuWidth) menuStyle.width = this.props.menuWidth; let menu; @@ -316,10 +318,10 @@ export default class Dropdown extends React.Component { currentValue = ( - { this._getMenuOptions() } + { this.getMenuOptions() }
); } @@ -356,14 +358,14 @@ 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
+ return
@@ -374,28 +376,3 @@ 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. - menuWidth: PropTypes.number, - // Called when the selected option changes - onOptionChange: PropTypes.func.isRequired, - // Called when the value of the search field changes - onSearchChange: PropTypes.func, - searchEnabled: PropTypes.bool, - // Function that, given the key of an option, returns - // a node representing that option to be displayed in the - // box itself as the currently-selected option (ie. as - // opposed to in the actual dropped-down part). If - // unspecified, the appropriate child element is used as - // in the dropped-down menu. - getShortOption: PropTypes.func, - value: PropTypes.string, - // negative for consistency with HTML - disabled: PropTypes.bool, - // ARIA label - label: PropTypes.string.isRequired, -};