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

Undraggable items when list is scrolled to the end #67

Open
JB712 opened this issue Jan 2, 2025 · 3 comments
Open

Undraggable items when list is scrolled to the end #67

JB712 opened this issue Jan 2, 2025 · 3 comments

Comments

@JB712
Copy link

JB712 commented Jan 2, 2025

I'm facing a very specific issue on iOS simulator and real devices.

When the DragList is longer than the screen size and is scrolled down to the end, trying to reorder with a long press always fail as the moment the item is detecting long press onPressOut is automatically fired so the item is directly unselected (see video attached).

Draglist is included in a simple View to add a button to its page (button enables user to hide items on the list). The whole page is included in a SafeAreaProvider (from react-native-safe-area-context), a PersistGate (from redux-persist/lib/integration/react) and a Provider (from react-redux).

Simulator.Screen.Recording.-.iPhone.12.mini.-.2025-01-02.at.10.52.07.mp4
System:
  OS: macOS 15.2
  CPU: (8) arm64 Apple M2
  Memory: 119.86 MB / 8.00 GB
  Shell:
    version: "5.9"
    path: /bin/zsh
Binaries:
  Node:
    version: 18.19.0
    path: /opt/homebrew/opt/node@18/bin/node
  Yarn: Not Found
  npm:
    version: 10.2.3
    path: /opt/homebrew/opt/node@18/bin/npm
  Watchman:
    version: 2024.01.22.00
    path: /opt/homebrew/bin/watchman
Managers:
  CocoaPods:
    version: 1.15.2
    path: /opt/homebrew/opt/ruby/bin/pod
SDKs:
  iOS SDK:
    Platforms:
      - DriverKit 24.0
      - iOS 18.0
      - macOS 15.0
      - tvOS 18.0
      - visionOS 2.0
      - watchOS 11.0
  Android SDK: Not Found
IDEs:
  Android Studio: Not Found
  Xcode:
    version: 16.0/16A242d
    path: /usr/bin/xcodebuild
Languages:
  Java: Not Found
  Ruby:
    version: 3.3.0
    path: /opt/homebrew/opt/ruby/bin/ruby
npmPackages:
  "@react-native-community/cli": Not Found
  react:
    installed: 18.2.0
    wanted: 18.2.0
  react-native:
    installed: 0.74.6
    wanted: 0.74.6
  react-native-macos: Not Found
npmGlobalPackages:
  "*react-native*": Not Found
Android:
  hermesEnabled: true
  newArchEnabled: false
iOS:
  hermesEnabled: true
  newArchEnabled: false
@JB712
Copy link
Author

JB712 commented Jan 2, 2025

Here is the page code :

import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {Animated, TouchableOpacity, View} from 'react-native';
import {useDispatch, useSelector} from 'react-redux';
import {Text} from 'comps';
import {_s, colors} from 'styles';
import {useColumnNumbers} from 'utils';
import {testIDs} from 'utils/testIDs';
import {selectSortedUserStats, selectUserStatsVisibleList, userStatsActions} from 'reduxL/userStats/userStatsSlice';
import DragList from 'react-native-draglist';
import {Icon} from '@rneui/base';

const fadingDuration = 400;

export const MyStatsPage = ({componentId}) => {
  const dispatch = useDispatch();

  const [organiseMode, setOrganiseMode] = useState(false);
  const [displayHidden, setDisplayHidden] = useState(false);
  const [animCurVal, setAnimCurVal] = useState(0);

  const numColumns = useColumnNumbers(350);

  const visibleList = useSelector(selectUserStatsVisibleList);
  const listLoaded = useSelector(selectSortedUserStats);

  const isVisible = useCallback((name) => visibleList.includes(name), [visibleList]);
  const listDisplayed = useMemo(() => listLoaded.filter((item) => isVisible(item.name)), [isVisible, listLoaded]);

  const animHeight = useRef(new Animated.Value(0)).current;

  useEffect(() => {
    const heightListener = animHeight.addListener(({value}) => {
      //console.log('listening value ::: ', value);
      setAnimCurVal(value);
    });

    return () => animHeight.removeListener(heightListener);
  });

  const expandHidden = Animated.timing(animHeight, {
    toValue: 1,
    duration: fadingDuration * (1 - animCurVal),
    useNativeDriver: false,
  });
  const foldHidden = Animated.timing(animHeight, {
    toValue: 0,
    duration: fadingDuration * animCurVal,
    useNativeDriver: false,
  });

  function switchOrganise() {
    console.log('switchOrganise tap');
    //setDisplayHidden((prevHidden) => !prevHidden);
    setOrganiseMode((opened) => {
      if (opened) {
        console.log('start folding items');
        expandHidden.stop();
        foldHidden.start(() => {
          console.log('folded items');
          setDisplayHidden((prevHidden) => false);
        });
      } else {
        console.log('start display items');
        foldHidden.stop(); //WARNING: foldHidden.stop() executes start() callback (so setDisplayHidden need to be called after)
        setDisplayHidden((prevMode) => true);
        expandHidden.start(() => console.log('displayed items'));
      }
      return !opened;
    });
  }

  function switchItemVisibility(name) {
    if (organiseMode) {
      dispatch(userStatsActions.switchVisibility({name, value: !isVisible(name)}));
    }
  }

  function onReordered(fromLocalIndex, toLocalIndex) {
    let fromIndex = fromLocalIndex;
    let toIndex = toLocalIndex;

    if (!displayHidden) {
      //si la liste n'est pas complètement affichée, faire la correspondance avec la liste complete (via name)
      const fromName = listDisplayed[fromLocalIndex].name;
      const toName = listDisplayed[toLocalIndex].name;
      fromIndex = listLoaded.findIndex((element) => element.name === fromName);
      toIndex = listLoaded.findIndex((element) => element.name === toName);
    }
    dispatch(userStatsActions.reorderOne({fromIndex, toIndex}));
  }

  console.log('item loaded', [listDisplayed, listLoaded, visibleList, numColumns, animCurVal]);

  return (
    <View style={[_s.flex, _s.mainBG, {paddingTop: 5}]}>
      <Icon
        disabled={listLoaded.length === 0}
        type="font-awesome-5"
        name={organiseMode ? 'eye-slash' : 'eye'}
        color={colors.darkBlue}
        solid
        reverse
        onPress={switchOrganise}
        containerStyle={{position: 'absolute', zIndex: 10, bottom: 18, right: 18}}
      />
      <DragList
        initialNumToRender={6}
        windowSize={5}
        data={displayHidden ? listLoaded : listDisplayed}
        numColumns={numColumns}
        key={numColumns}
        onReordered={onReordered}
        renderItem={({item, onDragStart, onDragEnd, isActive}) => (
          <RenderItem
            item={item}
            onDragStart={onDragStart}
            onDragEnd={onDragEnd}
            isActive={isActive}
            organiseMode={organiseMode}
            isVisible={isVisible}
            switchItemVisibility={switchItemVisibility}
            numColumns={numColumns}
            animHeight={animHeight}
          />
        )}
        keyExtractor={(item) => item.name}
        testID={testIDs.userStats.itemList}
      />
    </View>
  );
};

const AnimatedTouchable = Animated.createAnimatedComponent(TouchableOpacity);

const RenderItem = ({
  item,
  onDragStart,
  onDragEnd,
  isActive,
  organiseMode,
  isVisible,
  switchItemVisibility,
  numColumns,
  animHeight,
}) => {
  console.log('isVisible & numCol', [isVisible, numColumns]);

  return (
    <AnimatedTouchable
      style={[
        _s.cardStyle,
        {flexDirection: 'row', flex: 1, marginHorizontal: 10, marginVertical: 5, padding: 0},
        {backgroundColor: isVisible(item.name) ? 'white' : 'lightgrey'},
        isActive && {borderWidth: 2, borderColor: 'grey', marginHorizontal: 8, marginVertical: 3},
        //{height: 65},
        !isVisible(item.name) &&
          (numColumns > 1
            ? {transform: [{scaleY: animHeight}]}
            : {
                height: animHeight.interpolate({inputRange: [0, 1], outputRange: [0, 65]}),
                marginVertical: animHeight.interpolate({inputRange: [0, 1], outputRange: [0, 5]}),
                overflow: 'hidden',
                transform: [{scaleY: animHeight}], //necessary because on slow shrink out, content is weirdly smashed
              }),
        //{transform: [{scaleY: animHeight}]},
      ]} //TODO: faire un style générique
      onPress={() => switchItemVisibility(item.name)}
      onLongPress={onDragStart}
      onPressOut={onDragEnd}
      activeOpacity={0.8}
      testID={testIDs.rdv.itemG}
    >
      <Icon type="font-awesome-5" name={item.iconName} color={item.color} solid reverse />
      <View style={[{flex: 1, flexDirection: 'column', justifyContent: 'space-evenly'}]}>
        <Text>{item.title}</Text>
        <Text style={[{fontSize: 20, fontWeight: 'bold'}]}>{item.value}</Text>
      </View>
    </AnimatedTouchable>
  );
};

@JB712
Copy link
Author

JB712 commented Jan 3, 2025

From my understanding, it is onDragEnd() that is fired before panGrantedRef.current is set true (before onPanResponderGrant()) but I don't know what is causing this onDragEnd().

(In fact onDragEnd() is usually called at the beginning of the movement of the item but after onPanResponderGrant() so reset() is not called. Here onDragEnd() is called at the moment the item become active so no movement has been done yet)

Update: if I use a onPressIn() and I click while moving my mouse, the onMoveShouldSetPanResponderCapture() is fired (and so onPanResponderGrant()) before onDragEnd(). I still don't understand why onDragEnd() is called while nothing is happening but static pressure on the screen ...

@fivecar
Copy link
Owner

fivecar commented Jan 15, 2025

@JB712 : thanks for taking the time to report this. Interestingly, I don't get this problem (i.e. in production code that uses this library, I can scroll to the end of a multipage list and press-and-hold an item to move it around, no problem). I wonder if there's a way we can simplify the repro in your case in order to identify what's different between your code and mine.

Here's how my code handles it:

    <TouchableOpacity
      style={containerStyle}
      delayLongPress={LONG_PRESS_MS}
      onPress={onItemPress}
      onLongPress={onDragStart}
      onPressOut={onDragEnd}
      {...rest}
    >
    ....
  </TouchableOpacity>

It's also interesting that you only get this if the list is scrolled to the end.

One more thought: when you say onDragEnd() is called while nothing is happening, can I confirm that what you mean is your AnimatedTouchable calls onPressOut? From the code you pasted, I think that's what you mean. If so, this library doesn't control when AnimatedTouchable decides when onPressOut is called...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants