diff --git a/webapp/channels/src/components/admin_console/user_autocomplete_setting/user_autocomplete_setting.tsx b/webapp/channels/src/components/admin_console/user_autocomplete_setting/user_autocomplete_setting.tsx index b7d781bfa3..9ce123e69a 100644 --- a/webapp/channels/src/components/admin_console/user_autocomplete_setting/user_autocomplete_setting.tsx +++ b/webapp/channels/src/components/admin_console/user_autocomplete_setting/user_autocomplete_setting.tsx @@ -9,7 +9,7 @@ import {UserAutocomplete} from '@mattermost/types/autocomplete'; import GenericUserProvider from 'components/suggestion/generic_user_provider'; import Setting from 'components/admin_console/setting'; import SuggestionBox from 'components/suggestion/suggestion_box'; -import SuggestionList from 'components/suggestion/suggestion_list.jsx'; +import SuggestionList from 'components/suggestion/suggestion_list'; export type Props = { id: string; diff --git a/webapp/channels/src/components/quick_switch_modal/quick_switch_modal.tsx b/webapp/channels/src/components/quick_switch_modal/quick_switch_modal.tsx index 250b334bad..b6deaf349b 100644 --- a/webapp/channels/src/components/quick_switch_modal/quick_switch_modal.tsx +++ b/webapp/channels/src/components/quick_switch_modal/quick_switch_modal.tsx @@ -18,7 +18,7 @@ import * as UserAgent from 'utils/user_agent'; import FormattedMarkdownMessage from 'components/formatted_markdown_message'; import SuggestionBox from 'components/suggestion/suggestion_box'; import SuggestionBoxComponent from 'components/suggestion/suggestion_box/suggestion_box'; -import SuggestionList from 'components/suggestion/suggestion_list.jsx'; +import SuggestionList from 'components/suggestion/suggestion_list'; import SwitchChannelProvider from 'components/suggestion/switch_channel_provider'; import NoResultsIndicator from 'components/no_results_indicator/no_results_indicator'; diff --git a/webapp/channels/src/components/suggestion/modal_suggestion_list.tsx b/webapp/channels/src/components/suggestion/modal_suggestion_list.tsx index cc57c0b8b8..3cfc517a5b 100644 --- a/webapp/channels/src/components/suggestion/modal_suggestion_list.tsx +++ b/webapp/channels/src/components/suggestion/modal_suggestion_list.tsx @@ -3,14 +3,14 @@ import React from 'react'; -import SuggestionList from 'components/suggestion/suggestion_list.jsx'; +import SuggestionList from 'components/suggestion/suggestion_list'; import {getClosestParent} from 'utils/utils'; // eslint-disable-next-line @typescript-eslint/no-empty-interface interface SuggestionItem {} type SuggestionListProps = { - ariaLiveRef?: React.Ref; + ariaLiveRef?: React.RefObject; renderDividers?: string[]; renderNoResults?: boolean; preventClose?: () => void; diff --git a/webapp/channels/src/components/suggestion/search_suggestion_list.tsx b/webapp/channels/src/components/suggestion/search_suggestion_list.tsx index c9668f466f..ef18bba7b7 100644 --- a/webapp/channels/src/components/suggestion/search_suggestion_list.tsx +++ b/webapp/channels/src/components/suggestion/search_suggestion_list.tsx @@ -18,12 +18,13 @@ interface Item extends UserProfile { } interface Props { - ariaLiveRef?: React.Ref; + ariaLiveRef?: React.RefObject; + inputRef?: React.RefObject; open: boolean; position?: 'top' | 'bottom'; renderDividers?: string[]; renderNoResults?: boolean; - onCompleteWord: (term: string, matchedPretext: string, e?: React.MouseEvent) => boolean; + onCompleteWord: (term: string, matchedPretext: string, e?: React.KeyboardEventHandler) => boolean; preventClose?: () => void; onItemHover: (term: string) => void; pretext: string; @@ -53,7 +54,6 @@ export default class SearchSuggestionList extends SuggestionList { constructor(props: Props) { super(props); - this.itemRefs = new Map(); this.popoverRef = React.createRef(); this.itemsContainerRef = React.createRef(); this.suggestionReadOut = React.createRef(); @@ -84,7 +84,7 @@ export default class SearchSuggestionList extends SuggestionList { } getContent = () => { - return this.itemsContainerRef.current?.parentNode; + return this.itemsContainerRef?.current?.parentNode as HTMLDivElement | null; }; renderChannelDivider(type: string) { diff --git a/webapp/channels/src/components/suggestion/suggestion_box/suggestion_box.test.jsx b/webapp/channels/src/components/suggestion/suggestion_box/suggestion_box.test.jsx index 9e6d07ae7e..b87ddf30be 100644 --- a/webapp/channels/src/components/suggestion/suggestion_box/suggestion_box.test.jsx +++ b/webapp/channels/src/components/suggestion/suggestion_box/suggestion_box.test.jsx @@ -7,7 +7,7 @@ import {shallow, mount} from 'enzyme'; import CommandProvider from 'components/suggestion/command_provider/command_provider'; import AtMentionProvider from 'components/suggestion/at_mention_provider/at_mention_provider.jsx'; import SuggestionBox from 'components/suggestion/suggestion_box/suggestion_box'; -import SuggestionList from 'components/suggestion/suggestion_list.jsx'; +import SuggestionList from 'components/suggestion/suggestion_list'; import * as Utils from 'utils/utils'; jest.mock('mattermost-redux/client', () => { diff --git a/webapp/channels/src/components/suggestion/suggestion_list.d.ts b/webapp/channels/src/components/suggestion/suggestion_list.d.ts deleted file mode 100644 index b2536df358..0000000000 --- a/webapp/channels/src/components/suggestion/suggestion_list.d.ts +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import React from 'react'; - -// Since SuggestionLists contain items of different types without any common properties, I don't know of any good way -// to define a shared type for them. Confirming that a SuggestionItem matches what its component expects will be left -// up to the component. -// eslint-disable-next-line @typescript-eslint/no-empty-interface -interface SuggestionItem {} - -interface Props { - ariaLiveRef?: React.Ref; - open: boolean; - position?: 'top' | 'bottom'; - renderDividers?: string[]; - renderNoResults?: boolean; - onCompleteWord: (term: string, matchedPretext, e?: MouseEvent) => boolean; - preventClose?: () => void; - onItemHover: (term: string) => void; - pretext: string; - cleared: boolean; - matchedPretext: string[]; - items: any[]; - terms: string[]; - selection: string; - components: Array>>; - wrapperHeight?: number; - - // suggestionBoxAlgn is an optional object that can be passed to align the SuggestionList with the keyboard caret - // as the user is typing. - suggestionBoxAlgn?: { - lineHeight: number; - pixelsToMoveX: number; - pixelsToMoveY: number; - }; -} - -declare module 'components/suggestion/suggestion_list' { - declare class SuggestionList extends React.PureComponent { - currentLabel: string; - announceLabel: () => void; - itemRefs: Map; - currentItem: Item; - } -} - -export default SuggestionList; diff --git a/webapp/channels/src/components/suggestion/suggestion_list.test.jsx b/webapp/channels/src/components/suggestion/suggestion_list.test.tsx similarity index 94% rename from webapp/channels/src/components/suggestion/suggestion_list.test.jsx rename to webapp/channels/src/components/suggestion/suggestion_list.test.tsx index 3b4adf7ab8..792fe5b9a8 100644 --- a/webapp/channels/src/components/suggestion/suggestion_list.test.jsx +++ b/webapp/channels/src/components/suggestion/suggestion_list.test.tsx @@ -4,7 +4,7 @@ import React from 'react'; import {shallow} from 'enzyme'; -import SuggestionList from 'components/suggestion/suggestion_list.jsx'; +import SuggestionList from 'components/suggestion/suggestion_list'; describe('components/SuggestionList', () => { const baseProps = { @@ -21,7 +21,7 @@ describe('components/SuggestionList', () => { }; test('should not throw error when currentLabel is null and label is generated', () => { - const wrapper = shallow( + const wrapper = shallow( ; + inputRef?: React.RefObject; + open: boolean; + position?: 'top' | 'bottom'; + renderDividers?: string[]; + renderNoResults?: boolean; + onCompleteWord: (term: string, matchedPretext: string, e?: React.KeyboardEventHandler) => boolean; + preventClose?: () => void; + onItemHover: (term: string) => void; + pretext: string; + cleared: boolean; + matchedPretext: string[]; + items: any[]; + terms: string[]; + selection: string; + components: Array>; + wrapperHeight?: number; -export default class SuggestionList extends React.PureComponent { - static propTypes = { - ariaLiveRef: PropTypes.object, - inputRef: PropTypes.object, - open: PropTypes.bool.isRequired, - position: PropTypes.oneOf(['top', 'bottom']), - renderDividers: PropTypes.arrayOf(PropTypes.string), - renderNoResults: PropTypes.bool, - onCompleteWord: PropTypes.func.isRequired, - preventClose: PropTypes.func, - onItemHover: PropTypes.func.isRequired, - pretext: PropTypes.string.isRequired, - cleared: PropTypes.bool.isRequired, - matchedPretext: PropTypes.array.isRequired, - items: PropTypes.array.isRequired, - terms: PropTypes.array.isRequired, - selection: PropTypes.string.isRequired, - components: PropTypes.array.isRequired, - suggestionBoxAlgn: PropTypes.object, + // suggestionBoxAlgn is an optional object that can be passed to align the SuggestionList with the keyboard caret + // as the user is typing. + suggestionBoxAlgn?: { + lineHeight?: number; + pixelsToMoveX?: number; + pixelsToMoveY?: number; }; +} +export default class SuggestionList extends React.PureComponent { static defaultProps = { renderDividers: [], renderNoResults: false, }; + contentRef: React.RefObject; + wrapperRef: React.RefObject; + itemRefs: Map; + currentLabel: string | null; + currentItem: any; + maxHeight: number; - constructor(props) { + constructor(props: Props) { super(props); this.contentRef = React.createRef(); this.wrapperRef = React.createRef(); this.itemRefs = new Map(); - this.suggestionReadOut = React.createRef(); this.currentLabel = ''; this.currentItem = {}; + this.maxHeight = 0; } componentDidMount() { this.updateMaxHeight(); } - componentDidUpdate(prevProps) { + componentDidUpdate(prevProps: Props) { if (this.props.selection !== prevProps.selection && this.props.selection) { this.scrollToItem(this.props.selection); } @@ -79,7 +90,7 @@ export default class SuggestionList extends React.PureComponent { return; } - const inputHeight = this.props.inputRef.current.clientHeight ?? 0; + const inputHeight = (this.props.inputRef as React.RefObject).current?.clientHeight ?? 0; this.maxHeight = Math.min( window.innerHeight - (inputHeight + Constants.POST_MODAL_PADDING), @@ -87,25 +98,25 @@ export default class SuggestionList extends React.PureComponent { ); if (this.contentRef.current) { - this.contentRef.current.style['max-height'] = this.maxHeight; + this.contentRef.current.style.maxHeight = `${this.maxHeight}px`; } }; announceLabel() { - const suggestionReadOut = this.props.ariaLiveRef.current; + const suggestionReadOut = this.props.ariaLiveRef?.current; if (suggestionReadOut) { - suggestionReadOut.innerHTML = this.currentLabel; + suggestionReadOut.innerHTML = this.currentLabel as string; } } removeLabel() { - const suggestionReadOut = this.props.ariaLiveRef.current; + const suggestionReadOut = this.props.ariaLiveRef?.current; if (suggestionReadOut) { suggestionReadOut.innerHTML = ''; } } - generateLabel(item) { + generateLabel(item: any) { if (item.username) { this.currentLabel = item.username; if ((item.first_name || item.last_name) && item.nickname) { @@ -131,7 +142,7 @@ export default class SuggestionList extends React.PureComponent { return this.contentRef.current; }; - scrollToItem = (term) => { + scrollToItem = (term: string) => { const content = this.getContent(); if (!content) { return; @@ -150,10 +161,9 @@ export default class SuggestionList extends React.PureComponent { return; } - const itemTop = item.offsetTop - this.getComputedCssProperty(item, 'marginTop'); + const itemTop = (item as HTMLElement).offsetTop - this.getComputedCssProperty(item, 'marginTop'); const itemBottomMargin = this.getComputedCssProperty(item, 'marginBottom') + this.getComputedCssProperty(item, 'paddingBottom'); - const itemBottom = item.offsetTop + this.getComputedCssProperty(item, 'height') + itemBottomMargin; - + const itemBottom = (item as HTMLElement).offsetTop + this.getComputedCssProperty(item, 'height') + itemBottomMargin; if (itemTop - contentTopPadding < contentTop) { // the item is off the top of the visible space content.scrollTop = itemTop - contentTopPadding; @@ -164,8 +174,8 @@ export default class SuggestionList extends React.PureComponent { } }; - getComputedCssProperty(element, property) { - return parseInt(getComputedStyle(element)[property], 10); + getComputedCssProperty(element: Element | Text, property: string) { + return parseInt(getComputedStyle(element as HTMLElement).getPropertyValue(property) || '0', 10); } getTransform() { @@ -176,20 +186,25 @@ export default class SuggestionList extends React.PureComponent { const {lineHeight, pixelsToMoveX} = this.props.suggestionBoxAlgn; let pixelsToMoveY = this.props.suggestionBoxAlgn.pixelsToMoveY; - if (this.props.position === 'bottom') { + if (this.props.position === 'bottom' && pixelsToMoveY) { // Add the line height and 4 extra px so it looks less tight - pixelsToMoveY += this.props.suggestionBoxAlgn.lineHeight + 4; + pixelsToMoveY += (lineHeight || 0) + 4; } // If the suggestion box was invoked from the first line in the post box, stick to the top of the post box - pixelsToMoveY = pixelsToMoveY > lineHeight ? pixelsToMoveY : 0; + // if the lineHeight is smalller or undefined, then pixelsToMoveY should be 0 + if (lineHeight && pixelsToMoveY) { + pixelsToMoveY = pixelsToMoveY > lineHeight ? pixelsToMoveY : 0; + } else { + pixelsToMoveY = 0; + } return { transform: `translate(${pixelsToMoveX}px, ${pixelsToMoveY}px)`, }; } - renderDivider(type) { + renderDivider(type: string) { const id = type ? 'suggestion.' + type : 'suggestion.default'; return (
this.itemRefs.set(term, ref)} + ref={(ref: any) => this.itemRefs.set(term, ref)} item={this.props.items[i]} term={term} matchedPretext={this.props.matchedPretext[i]} diff --git a/webapp/channels/src/components/textbox/textbox.tsx b/webapp/channels/src/components/textbox/textbox.tsx index ade4638d8c..641c60b656 100644 --- a/webapp/channels/src/components/textbox/textbox.tsx +++ b/webapp/channels/src/components/textbox/textbox.tsx @@ -19,7 +19,7 @@ import CommandProvider from 'components/suggestion/command_provider/command_prov import EmoticonProvider from 'components/suggestion/emoticon_provider'; import SuggestionBox from 'components/suggestion/suggestion_box'; import SuggestionBoxComponent from 'components/suggestion/suggestion_box/suggestion_box'; -import SuggestionList from 'components/suggestion/suggestion_list.jsx'; +import SuggestionList from 'components/suggestion/suggestion_list'; import * as Utils from 'utils/utils';