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 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;

View File

@ -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';

View File

@ -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<HTMLDivElement>;
ariaLiveRef?: React.RefObject<HTMLDivElement>;
renderDividers?: string[];
renderNoResults?: boolean;
preventClose?: () => void;

View File

@ -18,12 +18,13 @@ interface Item extends UserProfile {
}
interface Props {
ariaLiveRef?: React.Ref<HTMLDivElement>;
ariaLiveRef?: React.RefObject<HTMLDivElement>;
inputRef?: React.RefObject<HTMLInputElement>;
open: boolean;
position?: 'top' | 'bottom';
renderDividers?: string[];
renderNoResults?: boolean;
onCompleteWord: (term: string, matchedPretext: string, e?: React.MouseEvent<HTMLDivElement>) => boolean;
onCompleteWord: (term: string, matchedPretext: string, e?: React.KeyboardEventHandler<HTMLDivElement>) => 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) {

View File

@ -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', () => {

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 {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<SuggestionList>(
<SuggestionList
{...baseProps}
ariaLiveRef={React.createRef()}

View File

@ -1,7 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import PropTypes from 'prop-types';
import React from 'react';
import ReactDOM from 'react-dom';
import {FormattedMessage} from 'react-intl';
@ -13,50 +12,62 @@ import {isEmptyObject} from 'utils/utils';
import FormattedMarkdownMessage from 'components/formatted_markdown_message';
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 {
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<Props> {
static defaultProps = {
renderDividers: [],
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);
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<HTMLInputElement>).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 (
<div
@ -246,7 +261,7 @@ export default class SuggestionList extends React.PureComponent {
// ReactComponent names need to be upper case when used in JSX
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));
prevItemType = item.type;
}
@ -263,7 +278,7 @@ export default class SuggestionList extends React.PureComponent {
items.push(
<Component
key={term}
ref={(ref) => 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]}

View File

@ -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';