MM-47075 : Migrate "components/suggestion/suggestion_list.jsx" and tests to Typescript (#23837)

This commit is contained in:
ridker 2023-07-19 15:48:55 -03:00 committed by GitHub
parent 49dddaa0f0
commit f3b1f33dff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 69 additions and 102 deletions

View File

@ -9,7 +9,7 @@ import {UserAutocomplete} from '@mattermost/types/autocomplete';
import GenericUserProvider from 'components/suggestion/generic_user_provider'; import GenericUserProvider from 'components/suggestion/generic_user_provider';
import Setting from 'components/admin_console/setting'; import Setting from 'components/admin_console/setting';
import SuggestionBox from 'components/suggestion/suggestion_box'; 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 = { export type Props = {
id: string; id: string;

View File

@ -18,7 +18,7 @@ import * as UserAgent from 'utils/user_agent';
import FormattedMarkdownMessage from 'components/formatted_markdown_message'; import FormattedMarkdownMessage from 'components/formatted_markdown_message';
import SuggestionBox from 'components/suggestion/suggestion_box'; import SuggestionBox from 'components/suggestion/suggestion_box';
import SuggestionBoxComponent from 'components/suggestion/suggestion_box/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 SwitchChannelProvider from 'components/suggestion/switch_channel_provider';
import NoResultsIndicator from 'components/no_results_indicator/no_results_indicator'; import NoResultsIndicator from 'components/no_results_indicator/no_results_indicator';

View File

@ -3,14 +3,14 @@
import React from 'react'; import React from 'react';
import SuggestionList from 'components/suggestion/suggestion_list.jsx'; import SuggestionList from 'components/suggestion/suggestion_list';
import {getClosestParent} from 'utils/utils'; import {getClosestParent} from 'utils/utils';
// eslint-disable-next-line @typescript-eslint/no-empty-interface // eslint-disable-next-line @typescript-eslint/no-empty-interface
interface SuggestionItem {} interface SuggestionItem {}
type SuggestionListProps = { type SuggestionListProps = {
ariaLiveRef?: React.Ref<HTMLDivElement>; ariaLiveRef?: React.RefObject<HTMLDivElement>;
renderDividers?: string[]; renderDividers?: string[];
renderNoResults?: boolean; renderNoResults?: boolean;
preventClose?: () => void; preventClose?: () => void;

View File

@ -18,12 +18,13 @@ interface Item extends UserProfile {
} }
interface Props { interface Props {
ariaLiveRef?: React.Ref<HTMLDivElement>; ariaLiveRef?: React.RefObject<HTMLDivElement>;
inputRef?: React.RefObject<HTMLInputElement>;
open: boolean; open: boolean;
position?: 'top' | 'bottom'; position?: 'top' | 'bottom';
renderDividers?: string[]; renderDividers?: string[];
renderNoResults?: boolean; renderNoResults?: boolean;
onCompleteWord: (term: string, matchedPretext: string, e?: React.MouseEvent<HTMLDivElement>) => boolean; onCompleteWord: (term: string, matchedPretext: string, e?: React.KeyboardEventHandler<HTMLDivElement>) => boolean;
preventClose?: () => void; preventClose?: () => void;
onItemHover: (term: string) => void; onItemHover: (term: string) => void;
pretext: string; pretext: string;
@ -53,7 +54,6 @@ export default class SearchSuggestionList extends SuggestionList {
constructor(props: Props) { constructor(props: Props) {
super(props); super(props);
this.itemRefs = new Map();
this.popoverRef = React.createRef(); this.popoverRef = React.createRef();
this.itemsContainerRef = React.createRef(); this.itemsContainerRef = React.createRef();
this.suggestionReadOut = React.createRef(); this.suggestionReadOut = React.createRef();
@ -84,7 +84,7 @@ export default class SearchSuggestionList extends SuggestionList {
} }
getContent = () => { getContent = () => {
return this.itemsContainerRef.current?.parentNode; return this.itemsContainerRef?.current?.parentNode as HTMLDivElement | null;
}; };
renderChannelDivider(type: string) { renderChannelDivider(type: string) {

View File

@ -7,7 +7,7 @@ import {shallow, mount} from 'enzyme';
import CommandProvider from 'components/suggestion/command_provider/command_provider'; import CommandProvider from 'components/suggestion/command_provider/command_provider';
import AtMentionProvider from 'components/suggestion/at_mention_provider/at_mention_provider.jsx'; import AtMentionProvider from 'components/suggestion/at_mention_provider/at_mention_provider.jsx';
import SuggestionBox from 'components/suggestion/suggestion_box/suggestion_box'; 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'; import * as Utils from 'utils/utils';
jest.mock('mattermost-redux/client', () => { jest.mock('mattermost-redux/client', () => {

View File

@ -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<HTMLDivElement>;
open: boolean;
position?: 'top' | 'bottom';
renderDividers?: string[];
renderNoResults?: boolean;
onCompleteWord: (term: string, matchedPretext, e?: MouseEvent<HTMLDivElement>) => boolean;
preventClose?: () => void;
onItemHover: (term: string) => void;
pretext: string;
cleared: boolean;
matchedPretext: string[];
items: any[];
terms: string[];
selection: string;
components: Array<React.FunctionComponent<SuggestionProps<unknown>>>;
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<Props> {
currentLabel: string;
announceLabel: () => void;
itemRefs: Map<any, any>;
currentItem: Item;
}
}
export default SuggestionList;

View File

@ -4,7 +4,7 @@
import React from 'react'; import React from 'react';
import {shallow} from 'enzyme'; import {shallow} from 'enzyme';
import SuggestionList from 'components/suggestion/suggestion_list.jsx'; import SuggestionList from 'components/suggestion/suggestion_list';
describe('components/SuggestionList', () => { describe('components/SuggestionList', () => {
const baseProps = { const baseProps = {
@ -21,7 +21,7 @@ describe('components/SuggestionList', () => {
}; };
test('should not throw error when currentLabel is null and label is generated', () => { test('should not throw error when currentLabel is null and label is generated', () => {
const wrapper = shallow( const wrapper = shallow<SuggestionList>(
<SuggestionList <SuggestionList
{...baseProps} {...baseProps}
ariaLiveRef={React.createRef()} ariaLiveRef={React.createRef()}

View File

@ -1,7 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import {FormattedMessage} from 'react-intl'; import {FormattedMessage} from 'react-intl';
@ -13,50 +12,62 @@ import {isEmptyObject} from 'utils/utils';
import FormattedMarkdownMessage from 'components/formatted_markdown_message'; import FormattedMarkdownMessage from 'components/formatted_markdown_message';
import LoadingSpinner from 'components/widgets/loading/loading_spinner'; import LoadingSpinner from 'components/widgets/loading/loading_spinner';
// When this file is migrated to TypeScript, type definitions for its props already exist in ./suggestion_list.d.ts. interface Props {
ariaLiveRef?: React.RefObject<HTMLDivElement>;
inputRef?: React.RefObject<HTMLDivElement>;
open: boolean;
position?: 'top' | 'bottom';
renderDividers?: string[];
renderNoResults?: boolean;
onCompleteWord: (term: string, matchedPretext: string, e?: React.KeyboardEventHandler<HTMLDivElement>) => boolean;
preventClose?: () => void;
onItemHover: (term: string) => void;
pretext: string;
cleared: boolean;
matchedPretext: string[];
items: any[];
terms: string[];
selection: string;
components: Array<React.FunctionComponent<any>>;
wrapperHeight?: number;
export default class SuggestionList extends React.PureComponent { // suggestionBoxAlgn is an optional object that can be passed to align the SuggestionList with the keyboard caret
static propTypes = { // as the user is typing.
ariaLiveRef: PropTypes.object, suggestionBoxAlgn?: {
inputRef: PropTypes.object, lineHeight?: number;
open: PropTypes.bool.isRequired, pixelsToMoveX?: number;
position: PropTypes.oneOf(['top', 'bottom']), pixelsToMoveY?: number;
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,
}; };
}
export default class SuggestionList extends React.PureComponent<Props> {
static defaultProps = { static defaultProps = {
renderDividers: [], renderDividers: [],
renderNoResults: false, renderNoResults: false,
}; };
contentRef: React.RefObject<HTMLDivElement>;
wrapperRef: React.RefObject<HTMLDivElement>;
itemRefs: Map<string, any>;
currentLabel: string | null;
currentItem: any;
maxHeight: number;
constructor(props) { constructor(props: Props) {
super(props); super(props);
this.contentRef = React.createRef(); this.contentRef = React.createRef();
this.wrapperRef = React.createRef(); this.wrapperRef = React.createRef();
this.itemRefs = new Map(); this.itemRefs = new Map();
this.suggestionReadOut = React.createRef();
this.currentLabel = ''; this.currentLabel = '';
this.currentItem = {}; this.currentItem = {};
this.maxHeight = 0;
} }
componentDidMount() { componentDidMount() {
this.updateMaxHeight(); this.updateMaxHeight();
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps: Props) {
if (this.props.selection !== prevProps.selection && this.props.selection) { if (this.props.selection !== prevProps.selection && this.props.selection) {
this.scrollToItem(this.props.selection); this.scrollToItem(this.props.selection);
} }
@ -79,7 +90,7 @@ export default class SuggestionList extends React.PureComponent {
return; return;
} }
const inputHeight = this.props.inputRef.current.clientHeight ?? 0; const inputHeight = (this.props.inputRef as React.RefObject<HTMLInputElement>).current?.clientHeight ?? 0;
this.maxHeight = Math.min( this.maxHeight = Math.min(
window.innerHeight - (inputHeight + Constants.POST_MODAL_PADDING), window.innerHeight - (inputHeight + Constants.POST_MODAL_PADDING),
@ -87,25 +98,25 @@ export default class SuggestionList extends React.PureComponent {
); );
if (this.contentRef.current) { if (this.contentRef.current) {
this.contentRef.current.style['max-height'] = this.maxHeight; this.contentRef.current.style.maxHeight = `${this.maxHeight}px`;
} }
}; };
announceLabel() { announceLabel() {
const suggestionReadOut = this.props.ariaLiveRef.current; const suggestionReadOut = this.props.ariaLiveRef?.current;
if (suggestionReadOut) { if (suggestionReadOut) {
suggestionReadOut.innerHTML = this.currentLabel; suggestionReadOut.innerHTML = this.currentLabel as string;
} }
} }
removeLabel() { removeLabel() {
const suggestionReadOut = this.props.ariaLiveRef.current; const suggestionReadOut = this.props.ariaLiveRef?.current;
if (suggestionReadOut) { if (suggestionReadOut) {
suggestionReadOut.innerHTML = ''; suggestionReadOut.innerHTML = '';
} }
} }
generateLabel(item) { generateLabel(item: any) {
if (item.username) { if (item.username) {
this.currentLabel = item.username; this.currentLabel = item.username;
if ((item.first_name || item.last_name) && item.nickname) { if ((item.first_name || item.last_name) && item.nickname) {
@ -131,7 +142,7 @@ export default class SuggestionList extends React.PureComponent {
return this.contentRef.current; return this.contentRef.current;
}; };
scrollToItem = (term) => { scrollToItem = (term: string) => {
const content = this.getContent(); const content = this.getContent();
if (!content) { if (!content) {
return; return;
@ -150,10 +161,9 @@ export default class SuggestionList extends React.PureComponent {
return; 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 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) { if (itemTop - contentTopPadding < contentTop) {
// the item is off the top of the visible space // the item is off the top of the visible space
content.scrollTop = itemTop - contentTopPadding; content.scrollTop = itemTop - contentTopPadding;
@ -164,8 +174,8 @@ export default class SuggestionList extends React.PureComponent {
} }
}; };
getComputedCssProperty(element, property) { getComputedCssProperty(element: Element | Text, property: string) {
return parseInt(getComputedStyle(element)[property], 10); return parseInt(getComputedStyle(element as HTMLElement).getPropertyValue(property) || '0', 10);
} }
getTransform() { getTransform() {
@ -176,20 +186,25 @@ export default class SuggestionList extends React.PureComponent {
const {lineHeight, pixelsToMoveX} = this.props.suggestionBoxAlgn; const {lineHeight, pixelsToMoveX} = this.props.suggestionBoxAlgn;
let pixelsToMoveY = this.props.suggestionBoxAlgn.pixelsToMoveY; 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 // 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 // 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 { return {
transform: `translate(${pixelsToMoveX}px, ${pixelsToMoveY}px)`, transform: `translate(${pixelsToMoveX}px, ${pixelsToMoveY}px)`,
}; };
} }
renderDivider(type) { renderDivider(type: string) {
const id = type ? 'suggestion.' + type : 'suggestion.default'; const id = type ? 'suggestion.' + type : 'suggestion.default';
return ( return (
<div <div
@ -246,7 +261,7 @@ export default class SuggestionList extends React.PureComponent {
// ReactComponent names need to be upper case when used in JSX // ReactComponent names need to be upper case when used in JSX
const Component = this.props.components[i]; const Component = this.props.components[i];
if ((renderDividers.includes('all') || renderDividers.includes(item.type)) && prevItemType !== item.type) { if ((renderDividers?.includes('all') || renderDividers?.includes(item.type)) && prevItemType !== item.type) {
items.push(this.renderDivider(item.type)); items.push(this.renderDivider(item.type));
prevItemType = item.type; prevItemType = item.type;
} }
@ -263,7 +278,7 @@ export default class SuggestionList extends React.PureComponent {
items.push( items.push(
<Component <Component
key={term} key={term}
ref={(ref) => this.itemRefs.set(term, ref)} ref={(ref: any) => this.itemRefs.set(term, ref)}
item={this.props.items[i]} item={this.props.items[i]}
term={term} term={term}
matchedPretext={this.props.matchedPretext[i]} matchedPretext={this.props.matchedPretext[i]}

View File

@ -19,7 +19,7 @@ import CommandProvider from 'components/suggestion/command_provider/command_prov
import EmoticonProvider from 'components/suggestion/emoticon_provider'; import EmoticonProvider from 'components/suggestion/emoticon_provider';
import SuggestionBox from 'components/suggestion/suggestion_box'; import SuggestionBox from 'components/suggestion/suggestion_box';
import SuggestionBoxComponent from 'components/suggestion/suggestion_box/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'; import * as Utils from 'utils/utils';