software-mansion / react-native-gesture-handler

Declarative API exposing platform native touch and gesture system to React Native.
https://docs.swmansion.com/react-native-gesture-handler/
MIT License
5.85k stars 954 forks source link

Closing a Swipeable element when another is opened #764

Closed darrylyoung closed 3 years ago

darrylyoung commented 4 years ago

I'm currently using Swipeable to create a list of items, each of which the user can swipe to delete. It's working fine but if I swipe a row and open one and then swipe another, the previously-opened row is still swiped open but I'd like it to be closed.

I've searched for solutions and made a little progress myself using refs but I've not been able to get it working perfectly. I've also come across quite a few people asking for the same thing and it seems like pretty standard behaviour. It'd be great if you could add some documentation with examples covering this.

Thank you.

Rubi91 commented 4 years ago

any update? 😭

anniewey commented 4 years ago

Need help on this too! I'm working on closing Swipeable by index instead of last swiped item.

darrylyoung commented 4 years ago

Need help on this too! I'm working on closing Swipeable by index instead of last swiped item.

Yeah, I played around with a solution like that but nothing felt that good so, unfortunately, I removed the whole swiping gesture from my app.

anniewey commented 4 years ago

I tried using ref as well and it seems to be working fine except that the close animation is delayed a little.

shhhiiiii commented 4 years ago

Hi, can someone help me. T tried the example project but when i tried to close the swipeable view but its not working. Am I missing or doingsomething.. Any help is really appreciated. Thank you so much.

this is the sample project link : https://github.com/kmagiera/react-native-gesture-handler/blob/master/Example/swipeable/AppleStyleSwipeableRow.js

darrylyoung commented 4 years ago

Any chance we can get an update on this? It seems like a few people are wondering the same thing. Thanks!

MateusGabi commented 4 years ago

I would like to introduce this function i my apps

AmanSharma2609 commented 4 years ago

I tried using ref as well and it seems to be working fine except that the close animation is delayed a little.

can you show us your workaround ?

anniewey commented 4 years ago

I tried using ref as well and it seems to be working fine except that the close animation is delayed a little.

can you show us your workaround ?

@AmanSharma2609 The idea is using ref and close(). Hope it helps

let row: Array<any> = [];
let prevOpenedRow;

renderItem ({ item, index }) {
  return (
    <Swipeable
    ref={ref => row[index] = ref}
    friction={2}
    leftThreshold={80}
    rightThreshold={40}
    renderRightActions={renderRightActions}
    containerStyle={style.swipeRowStyle}
        onSwipeableOpen={closeRow(index)}
    ...
    >
    ...
    </Swipeable>);
}

closeRow(index) {
    if (prevOpenedRow && prevOpenedRow !== row[index]) {
        prevOpenedRow.close();
    }
    prevOpenedRow = row[index];
}
GusNando commented 4 years ago

i use array of ref to fixed this issue, i think this issue cause by single ref, when we iterate over array the ref became the last ref of item in list so i did give all the list of item a ref based on index or item.id and called the ref based on index or id

first

constructor(props) {
    super(props);
    this.state = {
    };
    this.refsArray = []; // add this
  }

then in swipeable collect the ref by passing index or your custom id

<Swipeable
        ref={ref => {
          this.refsArray[index] = ref; //or this.refsArray[item.id] 
        }} 
        friction={2}
        leftThreshold={30}
        rightThreshold={40}
        renderRightActions={(progress) => this.renderRightActions(progress, item)}
      >

then to close one item do this in your method this.refsArray[index].close(); // or this.refsArray[item.id].close(); if you are using custom id

AmanSharma2609 commented 4 years ago

I tried using ref as well and it seems to be working fine except that the close animation is delayed a little.

can you show us your workaround ?

@AmanSharma2609 The idea is using ref and close(). Hope it helps

let row: Array<any> = [];
let prevOpenedRow;

renderItem ({ item, index }) {
  return (
    <Swipeable
  ref={ref => row[index] = ref}
  friction={2}
  leftThreshold={80}
  rightThreshold={40}
  renderRightActions={renderRightActions}
  containerStyle={style.swipeRowStyle}
        onSwipeableOpen={closeRow(index)}
  ...
    >
    ...
    </Swipeable>);
}

closeRow(index) {
    if (prevOpenedRow && prevOpenedRow !== row[index]) {
      prevOpenedRow.close();
    }
    prevOpenedRow = row[index];
}

@anniewey It worked thank you!!

pmarcosfelipe commented 4 years ago

I tried using ref as well and it seems to be working fine except that the close animation is delayed a little.

This solution works for me. I'm using react-native-gesture-handler v1.4.1 and we have the method onSwipeableWillOpen(). Try to use this instead onSwipeableOpen().

hoanganhnh2009 commented 4 years ago
List.js
let swipedCardRef = null;
  const onOpen = ref => {
    if (swipedCardRef) swipedCardRef.current.close();
    swipedCardRef = ref;
  };
  const onClose = ref => {
    if (ref == swipedCardRef) {
      swipedCardRef = null;
    }
  };
  const renderItem = ({ item, index }) => {
    return (
      <Item
        item={item}
        index={item}
        removeItem={removeItem(item)}
        onOpen={onOpen}
        onClose={onClose}
      />
    );
  };
<Flatlist data={[...]} renderItem={renderItem} ... />

Item.js
const onSwipeOpen = () => {
    if (!isOpened) {
      isOpened = true;
      onOpen(rowRef);
    }
  };
  const onSwipeClose = () => {
    if (isOpened) {
      isOpened = false;
      onClose(rowRef);
    }
  };
  return (
    <Swipeable
      ref={rowRef}
      renderLeftActions={renderLeftActions}
      onSwipeableOpen={onSwipeOpen}
      onSwipeableClose={onSwipeClose}
    >
      <TouchableOpacity
        onPressIn={onSwipeOpen}
      >
        ...
      </TouchableOpacity>
    </Swipeable>
  );

It can help you!

calendee commented 3 years ago

I wrapped my list in a SwipeProvider. It uses Context to maintain the state of which item is open. When an item opens, it updates the context with an id. All the other swipe-ables use a hook to see if the currently open id matches their id. If it doesn't, they close themselves.

Lsleiman commented 3 years ago

@calendee i’m trying to do the same thing. What prop do you use to force close the non matching rows?

calendee commented 3 years ago

@Lsleiman Sorry for the slow response.

Here's a sample: https://gist.github.com/calendee/ba37861b237b57ee49b7949766c9a0da

Lsleiman commented 3 years ago

This seems to be working for me. Sharing just in case

 let rowRefs = new Map();

  const renderItem = ({item}) => (
  <Swipeable
    key={item.key}
    ref={ref => {
      if (ref && !rowRefs.get(item.key)) {
        rowRefs.set(item.key, ref);
      }
    }}
    onSwipeableWillOpen={()=>{
        [...rowRefs.entries()].forEach(([key, ref]) => {
          if (key !== item.key && ref) ref.close();
        });
     }}
     >
   </Swipeable>
  );
ShravanMeena commented 3 years ago

rowRef

I'm getting rowRef is undefined

Lsleiman commented 3 years ago

@ShravanMeena I used rowRefs in my example

barunprasad commented 3 years ago

@Lsleiman Sorry for the slow response.

Here's a sample: https://gist.github.com/calendee/ba37861b237b57ee49b7949766c9a0da

Thank you @calendee . Your solution worked for me. I had three tabs and also had to close all swipeables when switching tabs. Using react-navigation as the navigation solution. Here is a gist in case someone needs it https://gist.github.com/barunprasad/a738d944fa9abf4e6993f719b13827ad

jayzyaj commented 3 years ago

@Lsleiman this solves my issue! Thanks alot

theristes commented 3 years ago

Here what I did:


const EMPTY_KEY = ''; 

const ComponentList = (props) => {

const row: Array<any> = [];
const [key, setKey] = React.useState<string | any>(EMPTY_KEY);

const handleWillOpen = (index : any) => () => (key !== EMPTY_KEY) && (key !== index) && row[key].close();

const handleOpen = (index : any) => () => setKey(index);

return <Swipeable 
              ref={ref => row[index] = ref}
              {....renders}
              onSwipeableRightWillOpen={handleWillOpen(index)}
              onSwipeableLeftWillOpen={handleWillOpen(index)}
              onSwipeableOpen={handleOpen(index)}>
              <View
                {....components}
            </View> 
          </Swipeable>

not using useCallback, or creating context whatever, just save the key, and make sure that before open some different key close the prior one.

developerdanx1 commented 3 years ago

Here what I did:


const EMPTY_KEY = ''; 

const row: Array<any> = [];
const [key, setKey] = React.useState<string | any>(EMPTY_KEY);

const handleWillOpen = (index : any) => () => (key !== EMPTY_KEY) && (key !== index) && row[key].close();

const handleOpen = (index : any) => () => setKey(index);

return <Swipeable 
              ref={ref => row[index] = ref}
              {....renders}
              onSwipeableRightWillOpen={handleWillOpen(index)}
              onSwipeableLeftWillOpen={handleWillOpen(index)}
              onSwipeableOpen={handleOpen(index)}>
              <View
                {....components}
            </View> 
          </Swipeable>

not using useCallback, or creating context whatever, just save the key, and make sure that before open some different key close the prior one.

Hi @theristes, I've tried your code but it seems not to work on my end. I've created a gist, if you are willing to take a quick look. Thanks :) https://gist.github.com/DeveloperDanX/9aac8af280f01b84fbb35bbd57717441

jerus1403 commented 3 years ago

@anniewey I used your solution and it worked but Idk if this happened to you but when I connect the app to my device and I got this log from xCode.

Screen Shot 2021-04-14 at 4 52 10 PM

Can anyone confirm this is also happening to you or it's just me? I logged (prevOpenedRow) in the console from the closeRow function and this is what I found in the xCode console.

arnaudbzn commented 3 years ago

@calendee Thank you for your solution! Because we are using a FlatList with +100 swipeable items we had to use use-context-selector to only re-render the opened item and item to close. The naming and Context state is sligthy different from your Gist but to give you an idea of the useContextSelector usage:

export function useSwipeableList(id: string) {
  const isOpenedItem = useContextSelector(
    SwipeableListContext,
    (state) => state?.openedItemId === id
  );

  const setOpenedItemId = useContextSelector(
    SwipeableListContext,
    (state) => state?.setOpenedItemId
  );

  return { isOpenedItem, setOpenedItemId };
 }
jenschr commented 2 years ago

It took me a long time to find a complete solution, but this reply on StackOverflow by user nodir.dev has a minimal and complete example like this:

const YourComponent = () => {

    const swipeRef = React.useRef()

    const closeSwipable = () => {
        swipeRef?.current?.close()
    }

    return (
        <Swipeable ref={swipeRef}>
        </Swipeable>
    )
}

The trick is to get the reference and React.useRef() solves that (not intuitive if you never used it). With this, I could easily add the close-method exactly where I wanted in my flow.

sammiepls commented 2 years ago

The way I handled it was to create a a prevRef outside of the item list, and to create a swipeRef for each row. Then onSwipeableOpen, I close the prevRef row, and then set prevRef to the current swipeRef. This way I don't need to keep an array of refs.

const prevRef = React.useRef<Swipeable>(null)
...
<FlatList 
  renderItem={<Item ref={ref} .../>
/>
const Item = React.forwardRef<Swipeable, ItemProps>(props, prevRef) => {

const swipeRef = React.useRef<Swipeable>(null)

return (
    <Swipeable 
       ref={swipeRef} 
       onSwipeableOpen={() => {
          if(prevRef 
            && typeof prevRef !==  "function" 
            && prevRef.current !== null) {
                if (prevRef.current !== swipeRef.current) {
                    prevRef.current.close();
                  }
                  prevRef.current = swipeRef.current;
                   }
     }}
   />
)
}
hilelt commented 2 years ago

This seems to be working for me. Sharing just in case

 let rowRefs = new Map();

  const renderItem = ({item}) => (
  <Swipeable
    key={item.key}
    ref={ref => {
      if (ref && !rowRefs.get(item.key)) {
        rowRefs.set(item.key, ref);
      }
    }}
    onSwipeableWillOpen={()=>{
        [...rowRefs.entries()].forEach(([key, ref]) => {
          if (key !== item.key && ref) ref.close();
        });
     }}
     >
   </Swipeable>
  );

This is simple and works perfectly! in case anyone comes across this, if you are using child components to render your row items set your 'rowRefs = new Map();' on the parent and pass it down to the child.

matheusmirandaferreira commented 2 years ago

@Lsleiman Desculpe pela resposta lenta.

Aqui está um exemplo: https://gist.github.com/calendee/ba37861b237b57ee49b7949766c9a0da

Thanks, i searched for how to implement the onpenLeft and right method everywhere

monkeedev commented 2 years ago

This solution is the same as @sammiepls proposed, but with some additionals.

My <Swipeable> component works with onSwipeableOpen and onSwipeableWillOpen. It looks like component closes faster:

/* List.tsx */

  const ref = useRef<Swipeable>(null);

  const renderItem = (itemData: {item: ITodo}) => (
    <ListItem ref={ref} {...itemData.item} />
  );

  <FlatList
    data={data}
    renderItem={item => renderItem(item)}
    ...
  />
/* ListItem.tsx */

export const ListItem = React.forwardRef((props, previousRef) => {
  const swipeRef = useRef<Swipeable>(null);

  const handleSwipeableWillOpen = () => {
    if (previousRef && previousRef.current !== null) {
      if (previousRef.current !== swipeRef.current) {
        previousRef.current?.close();
      }
    }
  };

  const handleSwipeableOpen = () => {
    previousRef.current = swipeRef.current;
  };

  return (
    <Swipeable
      ref={swipeRef}
      onSwipeableOpen={handleSwipeableOpen}
      onSwipeableWillOpen={handleSwipeableWillOpen}
      ...
    >
      <View style={[styles.row, theme.row]}>
         {/* something */}
      </View>
    </Swipeable>
  );
}
Vahabov commented 2 years ago

Who wrote this on the hook?

jerus1403 commented 2 years ago

This is how it looks on a functional component with hooks

let swipeRow = [], prevOpenRow;
const deleteItemHandler = async (type, id) => {
        await dispatch(deleteItem(type, id));
    };

    const closeRow = useCallback((id) => {
        if (prevOpenRow && prevOpenRow !== swipeRow[id]) {
            prevOpenRow.close();
        }
        prevOpenRow = swipeRow[id];
    }, [swipeRow]);

const renderRightAction = (
        iconName,
        text,
        color,
        x,
        progress,
        editItemParams
    ) => {
        const { item } = editItemParams;
        const { id, type } = item;
        const trans = progress.interpolate({
            inputRange: [0, 1],
            outputRange: [x, 0],
            extrapolate: 'clamp'
        });

        const navigateToEditScreen = (params) => {
            closeRow(id);
            navigation.navigate(EditScreen, params)
        }
        const onPressHandler = () => {
            if (text === trashBtn) {
                deleteItemHandler(type, id)
            } else {
                navigateToEditScreen(editItemParams)
            }
        }
        return (
            <Animated.View style={{ flex: 1, transform: [{ translateX: trans }] }}>
                <TouchableOpacity
                    style={[styles.swipeBtn, { backgroundColor: color }]}
                    onPress={onPressHandler}
                >
                    <Icon name={iconName} color={appColors.white} size={25} />
                    <Text style={styles.swipeBtnTextStyle}>{text}</Text>
                </TouchableOpacity>
            </Animated.View>
        );
    };

const renderRightActions = (
        progress,
        _dragAnimatedValue,
        editItemParams
    ) => {
        return (
            <View
                style={{
                    width: 192,
                    flexDirection: I18nManager.isRTL ? 'row-reverse' : 'row',
                }}>
                {renderRightAction(editIcon, editBtn, appColors.lightGray, 190, progress, editItemParams)}
                {renderRightAction(trashIcon, trashBtn, appColors.deleteRed, 95, progress, editItemParams)}
            </View>
        )
    };

const renderItem = (index, item, type) => {
        const { id, amount, tag, timestamp } = item;
        const editItemParams = {
            item: {
                type,
                id,
                tag,
                amount,
                timestamp
            }
        }
        return (
            <Swipeable
                key={id}
                ref={ref => swipeRow[id] = ref}
                renderRightActions={(progress, _dragAnimatedValue) => renderRightActions(progress, _dragAnimatedValue, editItemParams)}
                onSwipeableWillOpen={() => closeRow(id)}
                overshootRight={false}
                rightThreshold={10}
                friction={1}
            >
                <Pressable
                    style={[itemContainerStyle, styles.itemContainer]}
                    onPress={() => closeRow(id)}
                >
                    <Item
                        type={type}
                        tag={tag}
                        amount={amount}
                        timestamp={timestamp}
                    />
                </Pressable >
            </Swipeable>
        )
    };
SMEETT commented 2 years ago

I tried using ref as well and it seems to be working fine except that the close animation is delayed a little.

can you show us your workaround ?

@AmanSharma2609 The idea is using ref and close(). Hope it helps

let row: Array<any> = [];
let prevOpenedRow;

renderItem ({ item, index }) {
  return (
    <Swipeable
  ref={ref => row[index] = ref}
  friction={2}
  leftThreshold={80}
  rightThreshold={40}
  renderRightActions={renderRightActions}
  containerStyle={style.swipeRowStyle}
        onSwipeableOpen={closeRow(index)}
  ...
    >
    ...
    </Swipeable>);
}

closeRow(index) {
    if (prevOpenedRow && prevOpenedRow !== row[index]) {
      prevOpenedRow.close();
    }
    prevOpenedRow = row[index];
}

This works fine, the only thing I changed to get rid of the delay was to use "onBegan" instead of "onSwipeableOpen"

WenLonG12345 commented 2 years ago

Can anyone help me out? I was trying to do the same in FlatList but unfortunately it is not working. Here is my approach.

    const swipeRow = [];
    let prevOpenRow;

    const closeRow = useCallback((id) => {
        if (prevOpenRow && prevOpenRow !== swipeRow[id]) {
            prevOpenRow.close();
        }

        prevOpenRow = swipeRow[id];
    }, [swipeRow]);

 return (
        <Swipeable
            ref={ref => swipeRow[index] = ref}
            friction={2}
            enableTrackpadTwoFingerGesture
            rightThreshold={40}
            renderRightActions={(progress, dragX) => (
                <RightActions progress={progress} dragX={dragX} onPress={() => { }} />
            )}
            onSwipeableOpen={() => closeRow(index) }
            style={{ flex: 1 }}
        >
  {..view}
     </Swipeable>
}

Anything I miss out or done wrongly?

Pha696362 commented 2 years ago

state = { swipeRow=[]; }

render (item,index){

return ( <Swipeable ref={ref => this.state.swipeRow[index] = ref} renderRightActions={() => this.leftSwipe(item)}> ) }

onClose() { this.state.swipe.map((reference) => { if (reference) { reference.close(); this.setState({isConfirmModalVisible: false}); } }); }

franck-nadeau commented 1 year ago

I needed to do this, and none of the above solutions felt right. So I pushed this PR which once merged in will allow us to do the following:

  1. Close any previously opened Swipeable views when one is opened

    const openedRow = useRef();
    function renderItem({item: payable, index, separators}) {
    return (
      <Swipeable
        onSwipeableWillOpen={() => openedRow.current?.close()} // Close a previously opened row
        onSwipeableOpen={(_, swipeable) => (openedRow.current = swipeable)} // Keep a link to the currenlty
  2. Close a Swipeable view when it is being deleted.

        renderRightActions={(_, __, swipeable) => {
          return (
             <TouchableOpacity
                onPress={() => {
                  swipeable.close(); // Close the Swipeable when the user presses the button
theristes commented 1 year ago

you have so save the key, otherwise you will not be able to recognise the Swipeable

franck-nadeau commented 1 year ago

you have so save the key, otherwise you will not be able to recognise the Swipeable

Not sure what you mean by that, but the code does work.

For # 1, I am keeping a reference to the last opened Swipeable object (which is the same that the other codes samples are doing in their array).

For # 2, the swipeable object ref is kept in the context.

So I don't follow why you are asking for the keys to be kept. If you see a flaw with this, please do point it out on my PR.

mirsahib commented 1 year ago

Don't know why this is so complicated here what i do it self closing and you don't have to wait for another row to get close

let row: Array<any> = [];
const closeRow = (index: number) => {
        setTimeout(() => {
            console.log('close row', index)
            if(row[index]){
                row[index].close();
            }
        },2000)
    };

 <Swipeable renderRightActions={(progress, dragX) => rightActions(progress, dragX, props.onDelete)}
                onSwipeableOpen={() => closeRow(props.index)}
                ref={ref => (row[props.index] = ref)}
            >
                <View style={{
                    flexDirection: 'row',
                    backgroundColor: 'blue',
                    marginVertical: '2%',
                    padding: 10,
                    justifyContent: 'center'
                }}>
                    <Text>{props.item.title}</Text>
                </View>
   </Swipeable>
mMarcos208 commented 1 year ago

It's work for me.

I created an array for refs.

const swipeableRef = useRef<Swipeable[]>([])

I pushed each ref for my array, for example:

<Swipeable
      ref={(ref) => {
           if (ref) {
             swipeableRef.current.push(ref)
     }

To closed the specific line, I'm using.

swipeableRef.current?.[index].close()

greenuns commented 1 year ago

When a swipeable row is opened and I navigate to a different stack (using tab navigation for example) and then come back to the page with the swipeable row the row is still opened. Is there a way to "reset" the swipeables? So that whenever I return to the page with the swipeable rows all rows are always closed?

I know this isn't exactly the topic but it is kind of related. Hope this question doesn't violate any rules.

mksmanish commented 1 year ago

I tried using ref as well and it seems to be working fine except that the close animation is delayed a little.

can you show us your workaround ?

@AmanSharma2609 The idea is using ref and close(). Hope it helps

let row: Array<any> = [];
let prevOpenedRow;

renderItem ({ item, index }) {
  return (
    <Swipeable
  ref={ref => row[index] = ref}
  friction={2}
  leftThreshold={80}
  rightThreshold={40}
  renderRightActions={renderRightActions}
  containerStyle={style.swipeRowStyle}
        onSwipeableOpen={closeRow(index)}
  ...
    >
    ...
    </Swipeable>);
}

closeRow(index) {
    if (prevOpenedRow && prevOpenedRow !== row[index]) {
      prevOpenedRow.close();
    }
    prevOpenedRow = row[index];
}

How you are using the prevOpenedRow and what is its intail value,