Skip to content

Commit

Permalink
Merge pull request Expensify#33871 from software-mansion-labs/@kosmyd…
Browse files Browse the repository at this point in the history
…el/ts/OptionsList
  • Loading branch information
youssef-lr authored Jan 22, 2024
2 parents ec91f2c + 5bb60b8 commit 333f758
Show file tree
Hide file tree
Showing 10 changed files with 266 additions and 312 deletions.
Original file line number Diff line number Diff line change
@@ -1,89 +1,73 @@
import PropTypes from 'prop-types';
import isEqual from 'lodash/isEqual';
import type {ForwardedRef} from 'react';
import React, {forwardRef, memo, useEffect, useRef} from 'react';
import type {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 StringUtils from '@libs/StringUtils';
import variables from '@styles/variables';
import CONST from '@src/CONST';
import {defaultProps as optionsListDefaultProps, propTypes as optionsListPropTypes} from './optionsListPropTypes';
import type {BaseOptionListProps, OptionsList, OptionsListData, 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,
contentContainerStyles,
sectionHeaderStyle,
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<OptionsListData[]>(sections);
const didLayout = useRef(false);

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 +76,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 +102,7 @@ function BaseOptionsList({
};

useEffect(() => {
if (_.isEqual(sections, previousSections)) {
if (isEqual(sections, previousSections)) {
return;
}
flattenedData.current = buildFlatSectionArray();
Expand All @@ -138,23 +121,21 @@ 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.
*
* For example, given a list with two sections, two items in each section, no header, no footer, and no section headers, the flat array might look something like this:
*
* [{header}, {sectionHeader}, {item}, {item}, {sectionHeader}, {item}, {item}, {footer}]
*
* @returns {Object}
*/
const getItemLayout = (data, flatDataArrayIndex) => {
if (!_.has(flattenedData.current, flatDataArrayIndex)) {
// eslint-disable-next-line @typescript-eslint/naming-convention
const getItemLayout = (_data: OptionsListData[] | null, flatDataArrayIndex: number) => {
if (!flattenedData.current[flatDataArrayIndex]) {
flattenedData.current = buildFlatSectionArray();
}

const targetItem = flattenedData.current[flatDataArrayIndex];
return {
length: targetItem.length,
Expand All @@ -165,10 +146,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 +159,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 +171,7 @@ function BaseOptionsList({
return true;
}

if (_.isEmpty(option.name)) {
if (!option.name || StringUtils.isEmptyString(option.name)) {
return false;
}

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

return (
<OptionRow
keyForList={item.keyForList}
keyForList={item.keyForList ?? ''}
option={item}
showTitleTooltip={showTitleTooltip}
hoverStyle={optionHoveredStyle}
Expand All @@ -224,15 +204,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: OptionsListData}) => {
if (!title && shouldShow && !hideSectionHeaders && sectionHeaderStyle) {
return <View style={sectionHeaderStyle} />;
}
Expand Down Expand Up @@ -265,8 +238,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 +272,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),
);
19 changes: 0 additions & 19 deletions src/components/OptionsList/index.native.js

This file was deleted.

20 changes: 20 additions & 0 deletions src/components/OptionsList/index.native.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import React, {forwardRef} from 'react';
import type {ForwardedRef} from 'react';
import {Keyboard} from 'react-native';
import BaseOptionsList from './BaseOptionsList';
import type {OptionsListProps, OptionsList as OptionsListType} from './types';

function OptionsList(props: OptionsListProps, ref: ForwardedRef<OptionsListType>) {
return (
<BaseOptionsList
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
ref={ref}
onScrollBeginDrag={() => Keyboard.dismiss()}
/>
);
}

OptionsList.displayName = 'OptionsList';

export default forwardRef(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

0 comments on commit 333f758

Please sign in to comment.