mirror of
https://github.com/mattermost/mattermost.git
synced 2025-02-25 18:55:24 -06:00
MM-47075 : Migrate "components/suggestion/suggestion_list.jsx" and tests to Typescript (#23837)
This commit is contained in:
parent
49dddaa0f0
commit
f3b1f33dff
@ -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;
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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) {
|
||||
|
@ -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', () => {
|
||||
|
@ -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;
|
@ -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()}
|
@ -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]}
|
@ -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';
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user