react-native-dropdown-picker icon indicating copy to clipboard operation
react-native-dropdown-picker copied to clipboard

Best way to document a stable multi-dropdown nested view overlap / zindex solution?

Open mikehardy opened this issue 2 years ago • 24 comments

Hi there :wave:

This library is great, but it suffers from zIndexing / overlap issues that are basically out of it's control. There are issues related to it many times in the repo, I imagine it's a real bummer to deal with because it's just the way react-native lays out view elements.

But I think there's a solution that is really more about documentation than a technical fix.

Background, confirmed via testing on react-native 0.64.2 on real iOS device, iOS simulator and Android emuator

  • on react-native (and in the web with standard CSS) - zIndex only applies to things in the same "stacking context"
  • on react-native each view gets it's own stacking context! So if you nest elements (like dropdowns from this library) in their own View elements for styling, the zIndex/zIndexInverse props here don't matter, they're in separate stacking contexts. (if the dropdowns are siblings - not in separate views - it works, which confuses people)
  • on react-native android there is a "collapsable" View property that is true by default and it means that views that are only used for layout but don't draw anything are pruned from the native tree, so even more confusing, your zIndex/zIndexInverse props work on Android even if dropdowns are nested in separate views! What a nightmare to explain.

So what to do?

The solution I just tested, and it seems to work on both platforms is this:

  • if you are not nesting the dropdowns in separate views, keep on using zIndex/zIndexInverse - it works when the dropdowns are layout siblings because they share a stacking context on both platforms
  • if you are nesting the dropdowns in separate views, ~~stop using zIndex/zIndexInverse (it won't work on iOS and only works because of the "collapsable" memory optimization on Android) and instead set a zIndex prop on the parent Views dynamically, based on open state of the dropdown. If it's open, set zIndex to 1, if it is not open, set it to 0. Works?~~ [edit: see below comment - on iOS zIndex/zIndexInverse does no harm, it is required and works on Android, and parent-view zIndex is required on iOS but breaks Android, so it requires a platform-specific include of parent-view zIndex]

Here's an App.js that should show it working even with some dropdowns going up and some going down.


function getItemsArray() {
  return  [{label: 'Apple', value: 'apple'},
  {label: 'Banana', value: 'banana'},
  {label: 'Cranberry', value: 'cranberr'},
  {label: 'Durian', value: 'durian'},
  {label: 'Eggplant', value: 'eggplant'},
]
}

const App = () => {
  const [open, setOpen] = useState(false);
  const [value, setValue] = useState(null);
  const [items, setItems] = useState(getItemsArray());
  const [open2, setOpen2] = useState(false);
  const [value2, setValue2] = useState(null);
  const [items2, setItems2] = useState(getItemsArray());
  const [open3, setOpen3] = useState(false);
  const [value3, setValue3] = useState(null);
  const [items3, setItems3] = useState(getItemsArray());
  const [open4, setOpen4] = useState(false);
  const [value4, setValue4] = useState(null);
  const [items4, setItems4] = useState(getItemsArray());
  const [open5, setOpen5] = useState(false);
  const [value5, setValue5] = useState(null);
  const [items5, setItems5] = useState(getItemsArray());
  const [open6, setOpen6] = useState(false);
  const [value6, setValue6] = useState(null);
  const [items6, setItems6] = useState(getItemsArray());
  const [open7, setOpen7] = useState(false);
  const [value7, setValue7] = useState(null);
  const [items7, setItems7] = useState(getItemsArray());
  const [open8, setOpen8] = useState(false);
  const [value8, setValue8] = useState(null);
  const [items8, setItems8] = useState(getItemsArray());
  const [open9, setOpen9] = useState(false);
  const [value9, setValue9] = useState(null);
  const [items9, setItems9] = useState(getItemsArray());
  BootSplash.hide();

  return (
    <SafeAreaView style={{flex: 1}}>
      <ScrollView style={{flex: 1, flexGrow: 1}}
        contentContainerStyle={{flexGrow: 1}}>
        <Text style={{flex: 1, color: 'black'}}>Hi</Text>
        <View style={{zIndex: open ? 1: 0 }}>
        <DropDownPicker
          listMode="SCROLLVIEW"
          open={open}
          value={value}
          items={items}
          setOpen={setOpen}
          setValue={setValue}
          setItems={setItems}
        /></View><View style={{zIndex: open2 ? 1: 0 }}>
        <DropDownPicker
          listMode="SCROLLVIEW"
          open={open2}
          value={value2}
          items={items2}
          setOpen={setOpen2}
          setValue={setValue2}
          setItems={setItems2}
          /></View><View style={{zIndex: open3 ? 1: 0 }}>
          <DropDownPicker
          listMode="SCROLLVIEW"
          open={open3}
          value={value3}
          items={items3}
          setOpen={setOpen3}
          setValue={setValue3}
          setItems={setItems3}
        /></View><View style={{zIndex: open4 ? 1: 0 }}>
        <DropDownPicker
          listMode="SCROLLVIEW"
          open={open4}
          value={value4}
          items={items4}
          setOpen={setOpen4}
          setValue={setValue4}
          setItems={setItems4}
        /></View><View style={{zIndex: open5 ? 1: 0 }}>
        <DropDownPicker
          listMode="SCROLLVIEW"
          open={open5}
          value={value5}
          items={items5}
          setOpen={setOpen5}
          setValue={setValue5}
          setItems={setItems5}
          /></View><View style={{zIndex: open6 ? 1: 0 }}>
          <DropDownPicker
          listMode="SCROLLVIEW"
          open={open6}
          value={value6}
          items={items6}
          setOpen={setOpen6}
          setValue={setValue6}
          setItems={setItems6}
          /></View><View style={{zIndex: open7 ? 1: 0 }}>
          <DropDownPicker
          listMode="SCROLLVIEW"
          open={open7}
          value={value7}
          items={items7}
          setOpen={setOpen7}
          setValue={setValue7}
          setItems={setItems7}
          /></View><View style={{zIndex: open8 ? 1: 0 }}>
          <DropDownPicker
          listMode="SCROLLVIEW"
          open={open8}
          value={value8}
          items={items8}
          setOpen={setOpen8}
          setValue={setValue8}
          setItems={setItems8}
          /></View><View style={{zIndex: open9 ? 1: 0 }}>
          <DropDownPicker
          listMode="SCROLLVIEW"
          open={open9}
          value={value9}
          items={items9}
          setOpen={setOpen9}
          setValue={setValue9}
          setItems={setItems9}
          /></View>
          </ScrollView>
    </SafeAreaView>
  );
};

I think the best way to show this would be an entire page explaining it, with examples of the dead-end solutions (maybe?) plus this one to demonstrate why things work depending on the situation. It may involve a change to the "multiple dropdown" page or replace it?

I'd be happy to try a more formal writeup but first I thought I'd just propose it as a thought to see what you think.

Thanks!

mikehardy avatar Jul 14 '21 02:07 mikehardy

I'm nearly complete with the screen that prompted this deep-dive, and I noticed that even the above solution is not quite enough to be cross-platform. It certainly fixes the common issue on iOS caused by the dropdown in non-sibling views having unexpected overlap behavior.

However! If you apply the parent-container zIndex, then Android dropdowns are no longer touchable (as noted in other issues). But Android will continue to work correctly with the Dropdown zIndex/zIndexInverse props so long as the parent view container has no zIndex property at all.

So the complete, works on both platform solutions actually has zIndex/zIndexInverse as documented in the multiple dropdown guide here, but also has a parent View zIndex prop included dynamically, like this chunk of layout:

        <View
          style={[{
            flexDirection: 'row',
            justifyContent: 'space-evenly',}, 
            (Platform.OS === 'ios' ? {zIndex: areaOpen ? 1 : 0} : {})]
          }>
          <View style={{justifyContent: 'center', alignContent: 'flex-end'}}>
            <FontAwesome
              name="puzzle-piece"
              size={30}
              color={styles.blue.color}
            />
          </View>
          <View style={styles.filterTextViewStyle}>
            <Text style={{textAlign: 'left'}}>Área laboral:</Text>
          </View>
          <View style={{justifyContent: 'center'}}>
            <DropDownPicker
              open={areaOpen}
              value={areaValue}
              items={areaItems}
              setOpen={setAreaOpen}
              setValue={setAreaValue}
              setItems={setAreaItems}
              listMode="SCROLLVIEW"
              style={styles.dropDownStyle}
              zIndexInverse={7000}
              zIndex={1000}
              // containerStyle={{borderColor: 'green', borderWidth: 1}}
              dropDownContainerStyle={{width: 200, backgroundColor: 'white'}}
              listItemContainerStyle={{
                width: 200,
                // borderColor: 'red',
                // borderWidth: 1,
              }}
            />
          </View>
        </View>

mikehardy avatar Jul 14 '21 03:07 mikehardy

This has been user tested on real devices android + ios, plus android emulator / iOS simulator on current stable versions of everything and looks quite nice (nested View styling...) plus works everywhere FYI.

Please note that I am not handling the "multiple dropdowns open at once" issue in my code above, which is an error. But this is focused on the zIndex / overlap issue

mikehardy avatar Jul 19 '21 16:07 mikehardy

No idea who is reading any of this but this library is solid. It deserves more community helping. So, here's the close function for multi-dropdowns, just call this as the onOpen prop for the dropdowns, as in: onOpen{closeAllDropdowns())

Note that these are all my open dropdown state / state-toggle names. Yours will differ, but the pattern is super simple. Tested --> working, onOpen is apparently called before setXXXOpen() so there is not a race condition in my testing.

  const closeAllOpen = () => {
    fechaOpen && setFechaOpen(false);
    provinciaOpen && setProvinciaOpen(false);
    nivelOpen && setNivelOpen(false);
    discapacidadOpen && setDiscapacidadOpen(false);
    salarioOpen && setSalarioOpen(false);
    tipoOpen && setTipoOpen(false);
    areaOpen && setAreaOpen(false);
    return true;
  };

mikehardy avatar Jul 21 '21 04:07 mikehardy

Hey @mikehardy

Thanks for your tips and deep-dive, They will help people deal with the common but complex issues. Unfortunately I'm too busy these days, I have to work on other projects.

I'll be happy if you accept my invitation to join the react-native-dropdown-picker and react-native-dropdown-picker-website repos as a collaborator.

hossein-zare avatar Jul 21 '21 09:07 hossein-zare

Hey @hossein-zare sure, this is integrated in an app I use now and I can help keep it running, As mentioned I think it's the best dropdown package for react-native right now (nicely done!) so it really does deserve to keep moving along. I did not see any invitations though, so if you have already sent them please re-send and I'll hit the correct github buttons :-).

mikehardy avatar Jul 21 '21 21:07 mikehardy

@mikehardy done! :)

For your information: We need to merge the dev-5.x branch (PRs) into the 5.x branch before publishing the package. You can also message me on Twitter for more information, I started following you just now.

hossein-zare avatar Jul 22 '21 09:07 hossein-zare

Okay - for anyone else following along - I'm a collaborator on this repo (and the docs repo) now, and I usually rotate through the list of repositories I work on one a time, doing bursts of work each time I pass through, then letting them sit. After documenting the solution above here :point_up: I consider it already sort of "available for use / help" for anyone, so it's not super-urgent to make it in the official docs and it took some time to work out, so I'll let it sit, but I should be back through here in a little while with a real PR with it as an example

If anyone else wants to take the above and clean it up before I get to it, that would be great though of course :-)

mikehardy avatar Jul 22 '21 15:07 mikehardy

When the Picker is wrapped in Animated component, it seems it ignores zIndexes on android :(

TomasSestak avatar Aug 09 '21 12:08 TomasSestak

@TomasSestak interesting! One of the things I found while developing the solution above was that "layout only" Views were collapsed as they went from JS to Native components on Android as an optimization. That's the underlying reason why the zIndex is conditional on platform in my solution above.

zIndex will only be applied in react-native (or CSS on the web!) in peer elements in the same View container. There is no zIndex relationship between elements that have different View parents.

That is a fundamental understanding you must have to make zIndex tricks work.

But - on react-native android, layout-only Views are collapsed (and thus do not exist) at the native level so there is a little bit of wiggle room / difficult to understand but maybe useful behavior on android to set zIndexes that do actually relate to each other even if they are in different react view hierarchies.

Given that as a basic understanding you might try re-organizing your views, or doing it in a more iOS-way (as above) if you need to wrap your dropdowns in other components.

mikehardy avatar Aug 09 '21 15:08 mikehardy

Hey, just replying to this with something that might be useful for android users using FlatList. from https://github.com/facebook/react-native/issues/18616 (for zIndex/overlapping) Note that on Android you should use elevation in place of zIndex for ordering. In my layout of multiple drop pickers I have a state variable holding a UID/Key for which dropdown is open, and on each close/open call just call setState for each.

Pseudo code:

import { Platform } from "react-native";
import DropDownPicker from "react-native-dropdown-picker";

class Example extends Component {
  state = {
    activeDropdown: "",
    fields: [
      { key: "1", label: "test", options: ["A", "B", "C"] },
      { key: "2", label: "other field", options: ["D", "E", "F"] },
    ],
  };

  render() {
    return (
      <>
        <FlatList
          data={this.state.fields}
          extraData={this.state}
          CellRendererComponent={({ children, index, style, ...props }) => {
            const cellStyle = [
              style,
              Platform.OS === "android"
                ? { elevation: this.state.fields.length - index }
                : { zIndex: this.state.fields.length - index },
            ];

            return (
              <View style={cellStyle} index={index} {...props}>
                {children}
              </View>
            );
          }}
          renderItem={(item, index) => {
            return (
              <View
                style={
                  this.state.activeDropdown === item.key
                    ? Platform.OS === "android"
                      ? { elevation: 10 }
                      : { zIndex: 10 }
                    : {}
                }
              >
                <DropDownPicker
                  data={item.options}
                  open={this.state.activeDropdown === item.key}
                  onOpen={() => {
                    this.setState({ activeDropdown: item.key });
                  }}
                  onClose={() => {
                    this.setState({ activeDropdown: "" });
                  }}
                  handleChange={(value) => {
                    if (this.state.activeDropdown === item.key) {
                      // handle change here
                    }
                  }}
                  placeholder={item.label}
                />
              </View>
            );
          }}
        />
      </>
    );
  }
}

I can always make a expo snack if requested.

tek256 avatar Aug 13 '21 05:08 tek256

Oh that's a really nice piece of information - I honestly was not even aware of elevation existing

I have the ability to merge PRs and such now to really elaborate on this case (stated generally: one or more pickers being occluded by other elements when opened) and since it is by far the most common difficulty in using the library I would love to have a concrete example like a snack if you could generate one :pray:

mikehardy avatar Aug 13 '21 14:08 mikehardy

@hossein-zare @mikehardy Here is my code. Can you tell me where the problem is? I have tried everything but its not working. I'm using expo on android for this project. Snack Link: https://snack.expo.dev/@mali_ai/react-native-dropdown-picker Edited: Also I came to notice that the second dropdown is giving different behaviors on snack and VS code. When I run the app from VS code both dropdowns are not working correctly and the options are not clickable but when I run the app from snack, the second dropdown is working fine.

h-muhammad-ali avatar Dec 24 '21 17:12 h-muhammad-ali

@mali-ai Create a new issue. We don't discuss personal issues in this thread.

hossein-zare avatar Dec 31 '21 10:12 hossein-zare

To make it work, I should increase the parent height please help me

amirbhz86 avatar Jan 02 '22 13:01 amirbhz86

adding zIndex and zIndexInverse property to DropDownPicker doesn't help me in android I can select just in parent container not any more

amirbhz86 avatar Jan 02 '22 13:01 amirbhz86

@mikehardy's solution didn't work for me on android. When I tried to select something, it instead opened the picker underneath the one that was open.

I figured out that you can dynamically expand the padding and then set a negative margin of the parent view that contains the dropdown, and that solves the issue.

import React, {useState} from 'react';
import {View, SafeAreaView, ScrollView, Text} from 'react-native';
import DropDownPicker from 'react-native-dropdown-picker';
import {RFValue} from 'react-native-responsive-fontsize'; // Can scale numbers according to the display size

function getItemsArray() {
  return [
    {label: 'Apple', value: 'apple'},
    {label: 'Banana', value: 'banana'},
    {label: 'Cranberry', value: 'cranberr'},
    {label: 'Durian', value: 'durian'},
    {label: 'Eggplant', value: 'eggplant'},
  ];
}

export default function App() {
  const [open, setOpen] = useState(false);
  const [value, setValue] = useState(null);
  const [items, setItems] = useState(getItemsArray());
  const [open2, setOpen2] = useState(false);
  const [value2, setValue2] = useState(null);
  const [items2, setItems2] = useState(getItemsArray());
  const [open3, setOpen3] = useState(false);
  const [value3, setValue3] = useState(null);
  const [items3, setItems3] = useState(getItemsArray());

  const listItemHeight = RFValue(50, 747); // My phone is 747 UI pixels high
  const numberOfItemsToDisplay = items.length > 5 ? 5 : items.length;

  return (
    <>
      <View
        style={{
          zIndex: open ? 1 : 0,
          paddingBottom: open ? listItemHeight * numberOfItemsToDisplay : 0,
          marginBottom: open ? -listItemHeight * numberOfItemsToDisplay : 0,
          backgroundColor: 'red',
        }}>
        <DropDownPicker
          style={{height: RFValue(60, 747)}}
          dropDownContainerStyle={{
            maxHeight: listItemHeight * numberOfItemsToDisplay,
          }}
          listItemContainerStyle={{
            height: listItemHeight,
            borderBottomWidth: 1,
            borderBottomColor: '#DDD',
          }}
          listMode="SCROLLVIEW"
          open={open}
          value={value}
          items={items}
          setOpen={setOpen}
          setValue={setValue}
          setItems={setItems}
        />
      </View>
      <View
        style={{
          zIndex: open2 ? 1 : 0,
          paddingBottom: open2 ? listItemHeight * numberOfItemsToDisplay : 0,
          marginBottom: open2 ? -listItemHeight * numberOfItemsToDisplay : 0,
        }}>
        <DropDownPicker
          style={{height: RFValue(60, 747)}}
          dropDownContainerStyle={{
            height: listItemHeight * numberOfItemsToDisplay,
          }}
          listItemContainerStyle={{
            height: listItemHeight,
            borderBottomWidth: 1,
            borderBottomColor: '#DDD',
          }}
          listMode="SCROLLVIEW"
          open={open2}
          value={value2}
          items={items2}
          setOpen={setOpen2}
          setValue={setValue2}
          setItems={setItems2}
        />
      </View>
      <View
        style={{
          zIndex: open3 ? 1 : 0,
          paddingBottom: open3 ? listItemHeight * numberOfItemsToDisplay : 0,
          marginBottom: open3 ? -listItemHeight * numberOfItemsToDisplay : 0,
        }}>
        <DropDownPicker
          style={{height: RFValue(60, 747)}}
          dropDownContainerStyle={{
            height: listItemHeight * numberOfItemsToDisplay,
          }}
          listItemContainerStyle={{
            height: listItemHeight,
            borderBottomWidth: 1,
            borderBottomColor: '#DDD',
          }}
          listMode="SCROLLVIEW"
          open={open3}
          value={value3}
          items={items3}
          setOpen={setOpen3}
          setValue={setValue3}
          setItems={setItems3}
        />
      </View>
    </>
  );

nbarshain avatar Apr 12 '22 21:04 nbarshain

Hi! To fix it this is my approach:

Screenshot 2022-06-11 at 21 13 19

You could even calculate the desired heigh programatically, in my case I was not doing a production project so I will left the value hardcoded.

Kind regards, mugiwara

mugiwarafx avatar Jun 11 '22 19:06 mugiwarafx

I have a dynamic form with multiple consecutive form element views, many of which contain dropdowns. I have it working like a charm in iOS by managing the dropdown open/closed states centrally and setting parent style zIndex values according to which dropdown is open. But I cannot seem to make the dropdown items clickable in Android when they hang outside of the dropdown's parent view, no matter what combination of incantations I try. Oddly I have found that, at least cosmetically, I need the zIndex even for Android. I'm not sure if it's because I'm using an old version of React Native (0.63.4) or perhaps related to the Android version I'm running in the emulator (API 30). For now my workaround is to use listMode MODAL on Android, but it would be really nice to get this figured out.

I tried various combinations of zIndex, elevation, and the zIndex/zIndexInverse props to no avail. (To be honest, I don't even know what the zIndexInverse is supposed to mean, nor how those props are actually meant to work. The documentation is sparse, and I haven't dug into the library's code.) I am using the "against the rules" backgroundColor for the parent elements, which is important to our UI styling, but even removing that did not seem to change anything about my tests. (Based on what was said above in this thread, it seems perhaps that the backgroundColor simply affects the collapsibility of the parent view.)

This is perhaps not even the right thread to share this in, as it is not specifically related to overlapping views. I cannot click anywhere in the dropdown where it overflows its parent view, regardless of whether anything else is overlapping it. But this does seem to be the most thorough discussion of these issues, so here I am.

Android without parent style zIndex set: image

Android with parent style zIndex set: image

lafiosca avatar Oct 17 '22 03:10 lafiosca

The zIndex and zIndexInverse is - if I remember correctly - used for when the dropdown is hanging down or when it goes up if I recall correctly? I strongly recommend going spelunking in the code to check things, it's not too bad to read through.

Other than that I have to apologize: it appears you've gone past where I was with regard to both problems and attempts so I do not have specific guidance, other than to double and triple check "The Rules" and make sure you are not violating them: https://hossein-zare.github.io/react-native-dropdown-picker-website/docs/rules

mikehardy avatar Oct 17 '22 03:10 mikehardy

solution so far i had is setting

useEffect(() => { setzIndex(1000 * count_of_dropdowns - index_of_dropdown); //set zIndex of specific dropdown setZIndexInverse(1000 * index_of_dropdown + 1); //set ZIndexInverse of specific dropdown }, []);

gauravstanza avatar Oct 30 '22 08:10 gauravstanza

Always works for me

const MyDropdownInputContainer = ({
  open,
  children,
}: ViewProps & { open?: boolean }) => (
  <View style={{ ...(open && { zIndex: 1 }) }}>{children}</View>
);
<MyDropdownInputContainer isOpen={someDropdownOpen}>
  <DropDownPicker
    ...
    open={someDropdownOpen}
    setOpen={setSomeDropdownOpen}
    zIndex={someDropdownOpen ? 1 : 0}
    zIndexInverse={someDropdownOpen ? 1 : 0}
  />
</MyDropdownInputContainer>

~~Note: found that only some Android devices work with elevation, most others need zIndex~~ scratch that, after further testing on 6 Android devices and about as many simulators, elevation does not seem to be required with zIndex, and furthermore it adds unwanted styling.

frozencap avatar Feb 09 '23 20:02 frozencap

If there is a solution we can agree upon I think this deserves to be promoted to the docs as this is inevitably going to keep on coming up in the issues

frozencap avatar Feb 09 '23 21:02 frozencap

Always works for me

const MyDropdownInputContainer = ({
  open,
  children,
}: ViewProps & { open?: boolean }) => (
  <View style={{ ...(open && { zIndex: 1 }) }}>{children}</View>
);
<MyDropdownInputContainer isOpen={someDropdownOpen}>
  <DropDownPicker
    ...
    open={someDropdownOpen}
    setOpen={setSomeDropdownOpen}
    zIndex={someDropdownOpen ? 1 : 0}
    zIndexInverse={someDropdownOpen ? 1 : 0}
  />
</MyDropdownInputContainer>

~Note: found that only some Android devices work with elevation, most others need zIndex~ scratch that, after further testing on 6 Android devices and about as many simulators, elevation does not seem to be required with zIndex, and furthermore it adds unwanted styling.

Thanks, this is the only way I could get it to work!

TreeOfLearning avatar Apr 21 '23 09:04 TreeOfLearning

In my case, what worked was just adding a dynamic zIndex to the CellRendererComponent by using the index value from the props. Rest at other places, I didn't have to wrap the DropDownPicker component in a View, and neither did I have to use a zIndex prop for the DropDownPicker component. I tried all the issue threads in this repo that contained the zIndex or Flatlist keyword. In my case, I am using SectionList with the following prop, that's all, no other change was needed for me.

CellRendererComponent={({children, index, style, ...props}) => {
  return (
    // static value didn't work, somehow using the dynamic index makes it work
    <View style={[style, {zIndex: -1 * index}]} index={index} {...props}>
      {children}
    </View>
  );
}}

Update: Also, I just have single dropdown, so not sure in case of multiple dropdowns what other settings would go in place.

meherhowji avatar Sep 13 '23 11:09 meherhowji