Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[TS migration] Migrate 'OptionsList' component to TypeScript #33871

Merged
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,89 +1,72 @@
import PropTypes from 'prop-types';
import {isEmpty, isEqual} from 'lodash';
import type {ForwardedRef} from 'react';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we able to use some native JS methods to replace lodash?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I replaced isEmpty from lodash with isEmptyString from StringUtils.

However, I didn't find any replacement for the isEqual in our codebase, and isEqual from lodash is already used in a few typescript files.

import React, {forwardRef, memo, useEffect, useRef} from 'react';
import type {SectionListData, SectionListRenderItem} from 'react-native';
import {View} from 'react-native';
import _ from 'underscore';
import OptionRow from '@components/OptionRow';
import OptionsListSkeletonView from '@components/OptionsListSkeletonView';
import SectionList from '@components/SectionList';
import Text from '@components/Text';
import usePrevious from '@hooks/usePrevious';
import useThemeStyles from '@hooks/useThemeStyles';
import type {OptionData} from '@libs/ReportUtils';
import variables from '@styles/variables';
import CONST from '@src/CONST';
import {defaultProps as optionsListDefaultProps, propTypes as optionsListPropTypes} from './optionsListPropTypes';
import type {BaseOptionListProps, OptionsList, Section} from './types';

const propTypes = {
/** Determines whether the keyboard gets dismissed in response to a drag */
keyboardDismissMode: PropTypes.string,

/** Called when the user begins to drag the scroll view. Only used for the native component */
onScrollBeginDrag: PropTypes.func,

/** Callback executed on scroll. Only used for web/desktop component */
onScroll: PropTypes.func,

/** List styles for SectionList */
listStyles: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]),

...optionsListPropTypes,
};

const defaultProps = {
keyboardDismissMode: 'none',
onScrollBeginDrag: () => {},
onScroll: () => {},
listStyles: [],
...optionsListDefaultProps,
};

function BaseOptionsList({
keyboardDismissMode,
onScrollBeginDrag,
onScroll,
listStyles,
focusedIndex,
selectedOptions,
headerMessage,
isLoading,
sections,
onLayout,
hideSectionHeaders,
shouldHaveOptionSeparator,
showTitleTooltip,
optionHoveredStyle,
contentContainerStyles,
sectionHeaderStyle,
showScrollIndicator,
listContainerStyles: listContainerStylesProp,
shouldDisableRowInnerPadding,
shouldPreventDefaultFocusOnSelectRow,
disableFocusOptions,
canSelectMultipleOptions,
shouldShowMultipleOptionSelectorAsButton,
multipleOptionSelectorButtonText,
onAddToSelection,
highlightSelectedOptions,
onSelectRow,
boldStyle,
isDisabled,
innerRef,
isRowMultilineSupported,
isLoadingNewOptions,
nestedScrollEnabled,
bounces,
renderFooterContent,
}) {
function BaseOptionsList(
{
keyboardDismissMode = 'none',
onScrollBeginDrag = () => {},
onScroll = () => {},
listStyles = [],
focusedIndex = 0,
selectedOptions = [],
headerMessage = '',
isLoading = false,
sections = [],
onLayout,
hideSectionHeaders = false,
shouldHaveOptionSeparator = false,
showTitleTooltip = false,
optionHoveredStyle = undefined,
contentContainerStyles = [],
kosmydel marked this conversation as resolved.
Show resolved Hide resolved
sectionHeaderStyle = undefined,
kosmydel marked this conversation as resolved.
Show resolved Hide resolved
showScrollIndicator = false,
listContainerStyles: listContainerStylesProp,
shouldDisableRowInnerPadding = false,
shouldPreventDefaultFocusOnSelectRow = false,
disableFocusOptions = false,
canSelectMultipleOptions = false,
shouldShowMultipleOptionSelectorAsButton,
multipleOptionSelectorButtonText,
onAddToSelection,
highlightSelectedOptions = false,
onSelectRow,
boldStyle = false,
isDisabled = false,
isRowMultilineSupported = false,
isLoadingNewOptions = false,
nestedScrollEnabled = true,
bounces = true,
renderFooterContent,
}: BaseOptionListProps,
ref: ForwardedRef<OptionsList>,
) {
const styles = useThemeStyles();
const flattenedData = useRef();
const previousSections = usePrevious(sections);
const flattenedData = useRef<
Array<{
length: number;
offset: number;
}>
>([]);
const previousSections = usePrevious<Array<SectionListData<OptionData, Section>>>(sections);
const didLayout = useRef(false);
kosmydel marked this conversation as resolved.
Show resolved Hide resolved

const listContainerStyles = listContainerStylesProp || [styles.flex1];
const listContainerStyles = listContainerStylesProp ?? [styles.flex1];

/**
* This helper function is used to memoize the computation needed for getItemLayout. It is run whenever section data changes.
*
* @returns {Array<Object>}
*/
const buildFlatSectionArray = () => {
let offset = 0;
Expand All @@ -92,8 +75,7 @@ function BaseOptionsList({
const flatArray = [{length: 0, offset}];

// Build the flat array
for (let sectionIndex = 0; sectionIndex < sections.length; sectionIndex++) {
const section = sections[sectionIndex];
for (const section of sections) {
// Add the section header
const sectionHeaderHeight = section.title && !hideSectionHeaders ? variables.optionsListSectionHeaderHeight : 0;
flatArray.push({length: sectionHeaderHeight, offset});
Expand All @@ -119,7 +101,7 @@ function BaseOptionsList({
};

useEffect(() => {
if (_.isEqual(sections, previousSections)) {
if (isEqual(sections, previousSections)) {
return;
}
flattenedData.current = buildFlatSectionArray();
Expand All @@ -138,8 +120,8 @@ function BaseOptionsList({
* This function is used to compute the layout of any given item in our list.
* We need to implement it so that we can programmatically scroll to items outside the virtual render window of the SectionList.
*
* @param {Array} data - This is the same as the data we pass into the component
* @param {Number} flatDataArrayIndex - This index is provided by React Native, and refers to a flat array with data from all the sections. This flat array has some quirks:
* @param data - This is the same as the data we pass into the component
* @param flatDataArrayIndex - This index is provided by React Native, and refers to a flat array with data from all the sections. This flat array has some quirks:
*
* 1. It ALWAYS includes a list header and a list footer, even if we don't provide/render those.
* 2. Each section includes a header, even if we don't provide/render one.
Expand All @@ -148,14 +130,15 @@ function BaseOptionsList({
*
* [{header}, {sectionHeader}, {item}, {item}, {sectionHeader}, {item}, {item}, {footer}]
*
* @returns {Object}
* @returns
*/
const getItemLayout = (data, flatDataArrayIndex) => {
if (!_.has(flattenedData.current, flatDataArrayIndex)) {
// eslint-disable-next-line @typescript-eslint/naming-convention
const getItemLayout = (_data: Array<SectionListData<OptionData, Section>> | null, flatDataArrayIndex: number) => {
if (!flattenedData.current[flatDataArrayIndex]) {
flattenedData.current = buildFlatSectionArray();
}

const targetItem = flattenedData.current[flatDataArrayIndex];

return {
length: targetItem.length,
offset: targetItem.offset,
Expand All @@ -165,10 +148,8 @@ function BaseOptionsList({

/**
* Returns the key used by the list
* @param {Object} option
* @return {String}
*/
const extractKey = (option) => option.keyForList;
const extractKey = (option: OptionData) => option.keyForList ?? '';

/**
* Function which renders a row in the list
Expand All @@ -180,9 +161,10 @@ function BaseOptionsList({
*
* @return {Component}
*/
const renderItem = ({item, index, section}) => {
const isItemDisabled = isDisabled || section.isDisabled || !!item.isDisabled;
const isSelected = _.some(selectedOptions, (option) => {

const renderItem: SectionListRenderItem<OptionData, Section> = ({item, index, section}) => {
const isItemDisabled = isDisabled || !!section.isDisabled || !!item.isDisabled;
const isSelected = selectedOptions?.some((option) => {
if (option.accountID && option.accountID === item.accountID) {
return true;
}
Expand All @@ -191,7 +173,7 @@ function BaseOptionsList({
return true;
}

if (_.isEmpty(option.name)) {
if (isEmpty(option.name)) {
return false;
}

Expand All @@ -200,7 +182,7 @@ function BaseOptionsList({

return (
<OptionRow
keyForList={item.keyForList}
keyForList={item.keyForList ?? ''}
option={item}
showTitleTooltip={showTitleTooltip}
hoverStyle={optionHoveredStyle}
Expand All @@ -224,15 +206,8 @@ function BaseOptionsList({

/**
* Function which renders a section header component
*
* @param {Object} params
* @param {Object} params.section
* @param {String} params.section.title
* @param {Boolean} params.section.shouldShow
*
* @return {Component}
*/
const renderSectionHeader = ({section: {title, shouldShow}}) => {
const renderSectionHeader = ({section: {title, shouldShow}}: {section: SectionListData<OptionData, Section>}) => {
if (!title && shouldShow && !hideSectionHeaders && sectionHeaderStyle) {
return <View style={sectionHeaderStyle} />;
}
Expand Down Expand Up @@ -265,8 +240,8 @@ function BaseOptionsList({
<Text style={[styles.textLabel, styles.colorMuted]}>{headerMessage}</Text>
</View>
) : null}
<SectionList
ref={innerRef}
<SectionList<OptionData, Section>
ref={ref}
style={listStyles}
indicatorStyle="white"
keyboardShouldPersistTaps="always"
Expand Down Expand Up @@ -299,23 +274,15 @@ function BaseOptionsList({
);
}

BaseOptionsList.propTypes = propTypes;
BaseOptionsList.defaultProps = defaultProps;
BaseOptionsList.displayName = 'BaseOptionsList';

// using memo to avoid unnecessary rerenders when parents component rerenders (thus causing this component to rerender because shallow comparison is used for some props).
export default memo(
forwardRef((props, ref) => (
<BaseOptionsList
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
innerRef={ref}
/>
)),
forwardRef(BaseOptionsList),
(prevProps, nextProps) =>
nextProps.focusedIndex === prevProps.focusedIndex &&
nextProps.selectedOptions.length === prevProps.selectedOptions.length &&
nextProps?.selectedOptions?.length === prevProps?.selectedOptions?.length &&
nextProps.headerMessage === prevProps.headerMessage &&
nextProps.isLoading === prevProps.isLoading &&
_.isEqual(nextProps.sections, prevProps.sections),
isEqual(nextProps.sections, prevProps.sections),
);
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import React, {forwardRef} from 'react';
import type {ForwardedRef} from 'react';
import {Keyboard} from 'react-native';
import BaseOptionsList from './BaseOptionsList';
import {defaultProps, propTypes} from './optionsListPropTypes';
import type {OptionsListProps, OptionsList as OptionsListType} from './types';

const OptionsList = forwardRef((props, ref) => (
const OptionsList = forwardRef((props: OptionsListProps, ref: ForwardedRef<OptionsListType>) => (
kosmydel marked this conversation as resolved.
Show resolved Hide resolved
<BaseOptionsList
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
Expand All @@ -12,8 +13,4 @@ const OptionsList = forwardRef((props, ref) => (
/>
));

kosmydel marked this conversation as resolved.
Show resolved Hide resolved
OptionsList.propTypes = propTypes;
OptionsList.defaultProps = defaultProps;
OptionsList.displayName = 'OptionsList';

export default OptionsList;
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import React, {forwardRef, useCallback, useEffect, useRef} from 'react';
import type {ForwardedRef} from 'react';
import {Keyboard} from 'react-native';
import _ from 'underscore';
import withWindowDimensions from '@components/withWindowDimensions';
import * as DeviceCapabilities from '@libs/DeviceCapabilities';
import BaseOptionsList from './BaseOptionsList';
import {defaultProps, propTypes} from './optionsListPropTypes';
import type {OptionsListProps, OptionsList as OptionsListType} from './types';

function OptionsList(props) {
function OptionsList(props: OptionsListProps, ref: ForwardedRef<OptionsListType>) {
const isScreenTouched = useRef(false);

useEffect(() => {
Expand Down Expand Up @@ -43,25 +42,13 @@ function OptionsList(props) {
return (
<BaseOptionsList
// eslint-disable-next-line react/jsx-props-no-spreading
{..._.omit(props, 'forwardedRef')}
ref={props.forwardedRef}
{...props}
ref={ref}
onScroll={onScroll}
/>
);
}

OptionsList.displayName = 'OptionsList';
OptionsList.propTypes = propTypes;
OptionsList.defaultProps = defaultProps;

const OptionsListWithRef = forwardRef((props, ref) => (
<OptionsList
forwardedRef={ref}
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
/>
));

OptionsListWithRef.displayName = 'OptionsListWithRef';

export default withWindowDimensions(OptionsListWithRef);
export default forwardRef(OptionsList);
Loading
Loading