Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
09890aa
Fix - Two dates appear highlighted at the same time
dmkt9 Oct 31, 2025
5006837
Merge branch 'main' into fix/73489
dmkt9 Nov 5, 2025
e314bf8
Fix - Two dates appear highlighted at the same time
dmkt9 Nov 5, 2025
fc5a58e
Fix - Two dates appear highlighted at the same time
dmkt9 Nov 5, 2025
09806e8
Fix - Two dates appear highlighted at the same time
dmkt9 Nov 5, 2025
ef88fb3
Fix - Two dates appear highlighted at the same time
dmkt9 Nov 5, 2025
76b631a
fix lint
dmkt9 Nov 5, 2025
0f0edb5
fix typecheck
dmkt9 Nov 5, 2025
b7e932b
Merge branch 'main' into fix/73489
dmkt9 Nov 6, 2025
f3c1882
Merge branch 'main' into fix/73489
dmkt9 Nov 8, 2025
bc3b846
Fix - Two dates appear highlighted at the same time
dmkt9 Nov 8, 2025
dfc57af
Fix - Two dates appear highlighted at the same time
dmkt9 Nov 8, 2025
195bc3f
Fix - Two dates appear highlighted at the same time
dmkt9 Nov 8, 2025
236a55a
Merge branch 'main' into fix/73489
dmkt9 Nov 13, 2025
53bb4b6
Fix - Two dates highlighted when scrolling date list with keyboard ar…
dmkt9 Nov 13, 2025
9fa5544
fix test
dmkt9 Nov 17, 2025
6883488
Merge branch 'main' into fix/73489
dmkt9 Nov 17, 2025
f50a560
fix lint
dmkt9 Nov 17, 2025
9acd689
Revert "fix lint"
dmkt9 Nov 18, 2025
7c1a983
fix lint
dmkt9 Nov 18, 2025
2bdb551
Merge branch 'main' into fix/73489
dmkt9 Nov 20, 2025
7e0980a
Fix - Two dates highlighted when scrolling date list with keyboard ar…
dmkt9 Nov 20, 2025
6abe191
Fix - Two dates highlighted when scrolling date list with keyboard ar…
dmkt9 Nov 20, 2025
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
80 changes: 54 additions & 26 deletions src/components/SelectionList/BaseSelectionList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ function BaseSelectionList<TItem extends ListItem>({
shouldPreventDefaultFocusOnSelectRow = false,
shouldShowTextInput = !!textInputOptions?.label,
shouldHighlightSelectedItem = true,
shouldDisableHoverStyle = false,
setShouldDisableHoverStyle = () => {},
}: SelectionListProps<TItem>) {
const styles = useThemeStyles();
const isFocused = useIsFocused();
Expand Down Expand Up @@ -152,7 +154,11 @@ function BaseSelectionList<TItem extends ListItem>({

const debouncedScrollToIndex = useDebounce(scrollToIndex, CONST.TIMING.LIST_SCROLLING_DEBOUNCE_TIME, {leading: true, trailing: true});

const [focusedIndex, setFocusedIndex] = useArrowKeyFocusManager({
const onArrowUpDownCallback = useCallback(() => {
setShouldDisableHoverStyle(true);
}, [setShouldDisableHoverStyle]);

const [focusedIndex, setFocusedIndex, currentHoverIndexRef] = useArrowKeyFocusManager({
initialFocusedIndex,
maxIndex: data.length - 1,
disabledIndexes: dataDetails.disabledArrowKeyIndexes,
Expand All @@ -166,6 +172,7 @@ function BaseSelectionList<TItem extends ListItem>({
},
...(!hasKeyBeenPressed.current && {setHasKeyBeenPressed}),
isFocused,
onArrowUpDownCallback,
});

const selectRow = useCallback(
Expand Down Expand Up @@ -287,37 +294,58 @@ function BaseSelectionList<TItem extends ListItem>({
);
};

const setCurrentHoverIndex = useCallback(
(hoverIndex: number | null) => {
if (shouldDisableHoverStyle) {
return;
}
currentHoverIndexRef.current = hoverIndex;
},
[currentHoverIndexRef, shouldDisableHoverStyle],
);

const renderItem: ListRenderItem<TItem> = ({item, index}: ListRenderItemInfo<TItem>) => {
const isItemDisabled = isDisabled || item.isDisabled;
const selected = isItemSelected(item);
const isItemFocused = (!isDisabled || selected) && focusedIndex === index;

return (
<ListItemRenderer
ListItem={ListItem}
selectRow={selectRow}
keyForList={item.keyForList}
showTooltip={shouldShowTooltips}
item={item}
setFocusedIndex={setFocusedIndex}
index={index}
normalizedIndex={index}
isFocused={isItemFocused}
isDisabled={isItemDisabled}
canSelectMultiple={canSelectMultiple}
shouldSingleExecuteRowSelect={shouldSingleExecuteRowSelect}
shouldPreventDefaultFocusOnSelectRow={shouldPreventDefaultFocusOnSelectRow}
rightHandSideComponent={rightHandSideComponent}
isMultilineSupported={isRowMultilineSupported}
isAlternateTextMultilineSupported={(alternateNumberOfSupportedLines ?? 0) > 1}
alternateTextNumberOfLines={alternateNumberOfSupportedLines}
shouldIgnoreFocus={shouldIgnoreFocus}
wrapperStyle={style?.listItemWrapperStyle}
titleStyles={style?.listItemTitleStyles}
singleExecution={singleExecution}
shouldHighlightSelectedItem={shouldHighlightSelectedItem}
shouldSyncFocus={!isTextInputFocusedRef.current && hasKeyBeenPressed.current}
/>
<View
onMouseMove={() => setCurrentHoverIndex(index)}
onMouseEnter={() => setCurrentHoverIndex(index)}
onMouseLeave={(e) => {
e.stopPropagation();
setCurrentHoverIndex(null);
}}
>
<ListItemRenderer
ListItem={ListItem}
selectRow={selectRow}
keyForList={item.keyForList}
showTooltip={shouldShowTooltips}
item={item}
setFocusedIndex={setFocusedIndex}
index={index}
normalizedIndex={index}
isFocused={isItemFocused}
isDisabled={isItemDisabled}
canSelectMultiple={canSelectMultiple}
shouldSingleExecuteRowSelect={shouldSingleExecuteRowSelect}
shouldPreventDefaultFocusOnSelectRow={shouldPreventDefaultFocusOnSelectRow}
rightHandSideComponent={rightHandSideComponent}
isMultilineSupported={isRowMultilineSupported}
isAlternateTextMultilineSupported={(alternateNumberOfSupportedLines ?? 0) > 1}
alternateTextNumberOfLines={alternateNumberOfSupportedLines}
shouldIgnoreFocus={shouldIgnoreFocus}
wrapperStyle={style?.listItemWrapperStyle}
titleStyles={style?.listItemTitleStyles}
singleExecution={singleExecution}
shouldHighlightSelectedItem={shouldHighlightSelectedItem}
shouldSyncFocus={!isTextInputFocusedRef.current && hasKeyBeenPressed.current}
shouldDisableHoverStyle={shouldDisableHoverStyle}
shouldStopMouseLeavePropagation={false}
/>
</View>
);
};

Expand Down
9 changes: 7 additions & 2 deletions src/components/SelectionList/ListItem/BaseListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, {useRef} from 'react';
import {View} from 'react-native';
import {getButtonRole} from '@components/Button/utils';
import Icon from '@components/Icon';
// eslint-disable-next-line no-restricted-imports
import * as Expensicons from '@components/Icon/Expensicons';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
import PressableWithFeedback from '@components/Pressable/PressableWithFeedback';
Expand Down Expand Up @@ -42,6 +43,8 @@ function BaseListItem<TItem extends ListItem>({
testID,
shouldUseDefaultRightHandSideCheckmark = true,
shouldHighlightSelectedItem = true,
shouldDisableHoverStyle,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Checklist from #79395:
We need to pass shouldDisableHoverStyle down to UserListItem as well. Without passing it, we see two highlighted states when using keyboard arrow navigation.

shouldStopMouseLeavePropagation = true,
}: BaseListItemProps<TItem>) {
const theme = useTheme();
const styles = useThemeStyles();
Expand All @@ -55,7 +58,9 @@ function BaseListItem<TItem extends ListItem>({
useSyncFocus(pressableRef, !!isFocused, shouldSyncFocus);
const handleMouseLeave = (e: React.MouseEvent<Element, MouseEvent>) => {
bind.onMouseLeave();
e.stopPropagation();
if (shouldStopMouseLeavePropagation) {
e.stopPropagation();
}
setMouseUp();
};

Expand Down Expand Up @@ -103,7 +108,7 @@ function BaseListItem<TItem extends ListItem>({
isNested
hoverDimmingValue={1}
pressDimmingValue={item.isInteractive === false ? 1 : variables.pressDimValue}
hoverStyle={[!item.isDisabled && item.isInteractive !== false && styles.hoveredComponentBG, hoverStyle]}
hoverStyle={!shouldDisableHoverStyle ? [!item.isDisabled && item.isInteractive !== false && styles.hoveredComponentBG, hoverStyle] : undefined}
dataSet={{[CONST.SELECTION_SCRAPER_HIDDEN_ELEMENT]: true, [CONST.INNER_BOX_SHADOW_ELEMENT]: shouldShowBlueBorderOnFocus}}
onMouseDown={(e) => e.preventDefault()}
id={keyForList ?? ''}
Expand Down
4 changes: 4 additions & 0 deletions src/components/SelectionList/ListItem/ListItemRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ function ListItemRenderer<TItem extends ListItem>({
titleContainerStyles,
shouldUseDefaultRightHandSideCheckmark,
shouldHighlightSelectedItem,
shouldDisableHoverStyle,
shouldStopMouseLeavePropagation,
}: ListItemRendererProps<TItem>) {
const handleOnCheckboxPress = () => {
if (isTransactionGroupListItemType(item)) {
Expand Down Expand Up @@ -98,6 +100,8 @@ function ListItemRenderer<TItem extends ListItem>({
titleContainerStyles={titleContainerStyles}
shouldUseDefaultRightHandSideCheckmark={shouldUseDefaultRightHandSideCheckmark}
shouldHighlightSelectedItem={shouldHighlightSelectedItem}
shouldDisableHoverStyle={shouldDisableHoverStyle}
shouldStopMouseLeavePropagation={shouldStopMouseLeavePropagation}
/>
{item.footerContent && item.footerContent}
</>
Expand Down
4 changes: 4 additions & 0 deletions src/components/SelectionList/ListItem/RadioListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ function RadioListItem<TItem extends ListItem>({
wrapperStyle,
titleStyles,
shouldHighlightSelectedItem = true,
shouldDisableHoverStyle,
shouldStopMouseLeavePropagation,
}: RadioListItemProps<TItem>) {
const styles = useThemeStyles();
const fullTitle = isMultilineSupported ? item.text?.trimStart() : item.text;
Expand All @@ -47,6 +49,8 @@ function RadioListItem<TItem extends ListItem>({
shouldSyncFocus={shouldSyncFocus}
pendingAction={item.pendingAction}
shouldHighlightSelectedItem={shouldHighlightSelectedItem}
shouldDisableHoverStyle={shouldDisableHoverStyle}
shouldStopMouseLeavePropagation={shouldStopMouseLeavePropagation}
>
<>
{!!item.leftElement && item.leftElement}
Expand Down
12 changes: 12 additions & 0 deletions src/components/SelectionList/ListItem/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,12 @@ type ListItemProps<TItem extends ListItem> = CommonListItemProps<TItem> & {

/** Whether to highlight the selected item */
shouldHighlightSelectedItem?: boolean;

/** Whether to disable the hover style of the item */
shouldDisableHoverStyle?: boolean;

/** Whether to call stopPropagation on the mouseleave event in BaseListItem */
shouldStopMouseLeavePropagation?: boolean;
};

type ValidListItem =
Expand Down Expand Up @@ -268,6 +274,12 @@ type BaseListItemProps<TItem extends ListItem> = CommonListItemProps<TItem> & {
shouldShowRightCaret?: boolean;
/** Whether to highlight the selected item */
shouldHighlightSelectedItem?: boolean;

/** Whether to disable the hover style of the item */
shouldDisableHoverStyle?: boolean;

/** Whether to call stopPropagation on the mouseleave event in BaseListItem */
shouldStopMouseLeavePropagation?: boolean;
};
type RadioListItemProps<TItem extends ListItem> = ListItemProps<TItem>;

Expand Down
32 changes: 32 additions & 0 deletions src/components/SelectionList/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type {SelectionListProps} from './types';
function SelectionList<TItem extends ListItem>({ref, ...props}: SelectionListProps<TItem>) {
const [isScreenTouched, setIsScreenTouched] = useState(false);
const [shouldDebounceScrolling, setShouldDebounceScrolling] = useState(false);
const [shouldDisableHoverStyle, setShouldDisableHoverStyle] = useState(false);

const touchStart = () => setIsScreenTouched(true);
const touchEnd = () => setIsScreenTouched(false);
Expand Down Expand Up @@ -52,6 +53,35 @@ function SelectionList<TItem extends ListItem>({ref, ...props}: SelectionListPro
};
}, []);

useEffect(() => {
if (canUseTouchScreen()) {
return;
}

let lastClientX = 0;
let lastClientY = 0;
const mouseMoveHandler = (event: MouseEvent) => {
// On Safari, scrolling can also trigger a mousemove event,
// so this comparison is needed to filter out cases where the mouse hasn't actually moved.
if (event.clientX === lastClientX && event.clientY === lastClientY) {
return;
}

lastClientX = event.clientX;
lastClientY = event.clientY;

setShouldDisableHoverStyle(false);
};
const wheelHandler = () => setShouldDisableHoverStyle(false);

document.addEventListener('mousemove', mouseMoveHandler, {passive: true});
document.addEventListener('wheel', wheelHandler, {passive: true});
return () => {
document.removeEventListener('mousemove', mouseMoveHandler);
document.removeEventListener('wheel', wheelHandler);
};
}, []);

return (
<BaseSelectionList
// eslint-disable-next-line react/jsx-props-no-spreading
Expand All @@ -61,6 +91,8 @@ function SelectionList<TItem extends ListItem>({ref, ...props}: SelectionListPro
// For example, a long press will trigger a focus event on mobile chrome.
shouldIgnoreFocus={isMobileChrome() && isScreenTouched}
shouldDebounceScrolling={shouldDebounceScrolling}
shouldDisableHoverStyle={shouldDisableHoverStyle}
setShouldDisableHoverStyle={setShouldDisableHoverStyle}
/>
);
}
Expand Down
4 changes: 4 additions & 0 deletions src/components/SelectionList/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,10 @@ type SelectionListProps<TItem extends ListItem> = {

/** Whether to highlight the selected item */
shouldHighlightSelectedItem?: boolean;

/** Whether hover style should be disabled */
shouldDisableHoverStyle?: boolean;
setShouldDisableHoverStyle?: React.Dispatch<React.SetStateAction<boolean>>;
};

type TextInputOptions = {
Expand Down
9 changes: 7 additions & 2 deletions src/components/SelectionListWithSections/BaseListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, {useRef} from 'react';
import {View} from 'react-native';
import {getButtonRole} from '@components/Button/utils';
import Icon from '@components/Icon';
// eslint-disable-next-line no-restricted-imports
import * as Expensicons from '@components/Icon/Expensicons';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
import PressableWithFeedback from '@components/Pressable/PressableWithFeedback';
Expand Down Expand Up @@ -44,6 +45,8 @@ function BaseListItem<TItem extends ListItem>({
forwardedFSClass,
shouldShowRightCaret = false,
shouldHighlightSelectedItem = true,
shouldDisableHoverStyle,
shouldStopMouseLeavePropagation = true,
}: BaseListItemProps<TItem>) {
const theme = useTheme();
const styles = useThemeStyles();
Expand All @@ -57,7 +60,9 @@ function BaseListItem<TItem extends ListItem>({
useSyncFocus(pressableRef, !!isFocused, shouldSyncFocus);
const handleMouseLeave = (e: React.MouseEvent<Element, MouseEvent>) => {
bind.onMouseLeave();
e.stopPropagation();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Not sure, but I think removing this one might cause a regression.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I’ve added a prop passed down from BaseSelectionListWithSections. By default, we call stopPropagation inside BaseListItem, but for BaseSelectionListWithSections, the propagation is stopped at the View wrapper instead.

if (shouldStopMouseLeavePropagation) {
e.stopPropagation();
}
setMouseUp();
};

Expand Down Expand Up @@ -105,7 +110,7 @@ function BaseListItem<TItem extends ListItem>({
isNested
hoverDimmingValue={1}
pressDimmingValue={item.isInteractive === false ? 1 : variables.pressDimValue}
hoverStyle={[!item.isDisabled && item.isInteractive !== false && styles.hoveredComponentBG, hoverStyle]}
hoverStyle={!shouldDisableHoverStyle ? [!item.isDisabled && item.isInteractive !== false && styles.hoveredComponentBG, hoverStyle] : undefined}
dataSet={{[CONST.SELECTION_SCRAPER_HIDDEN_ELEMENT]: true, [CONST.INNER_BOX_SHADOW_ELEMENT]: shouldShowBlueBorderOnFocus}}
onMouseDown={(e) => e.preventDefault()}
id={keyForList ?? ''}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ function BaseSelectionListItemRenderer<TItem extends ListItem>({
userBillingFundID,
shouldShowRightCaret,
shouldHighlightSelectedItem = true,
shouldDisableHoverStyle,
shouldStopMouseLeavePropagation,
}: BaseSelectionListItemRendererProps<TItem>) {
const handleOnCheckboxPress = () => {
if (isTransactionGroupListItemType(item)) {
Expand Down Expand Up @@ -118,6 +120,8 @@ function BaseSelectionListItemRenderer<TItem extends ListItem>({
shouldShowRightCaret={shouldShowRightCaret}
shouldHighlightSelectedItem={shouldHighlightSelectedItem}
sectionIndex={sectionIndex}
shouldDisableHoverStyle={shouldDisableHoverStyle}
shouldStopMouseLeavePropagation={shouldStopMouseLeavePropagation}
/>
{item.footerContent && item.footerContent}
</>
Expand Down
Loading
Loading