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

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

Open
mikehardy opened this issue Jul 14, 2021 · 24 comments
Assignees
Labels
documentation Improvements or additions to documentation

Comments

@mikehardy
Copy link
Collaborator

mikehardy commented Jul 14, 2021

Hi there 👋

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
Copy link
Collaborator Author

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
Copy link
Collaborator Author

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
Copy link
Collaborator Author

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;
  };

@hossein-zare

This comment has been minimized.

@mikehardy

This comment has been minimized.

@hossein-zare

This comment has been minimized.

@mikehardy mikehardy added the documentation Improvements or additions to documentation label Jul 22, 2021
@mikehardy mikehardy self-assigned this Jul 22, 2021
@mikehardy
Copy link
Collaborator Author

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 ☝️ 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 :-)

@TomasSestak
Copy link

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

@mikehardy
Copy link
Collaborator Author

@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.

@tek256
Copy link
Contributor

tek256 commented Aug 13, 2021

Hey, just replying to this with something that might be useful for android users using FlatList.
from facebook/react-native#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.

@mikehardy
Copy link
Collaborator Author

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 🙏

@h-muhammad-ali
Copy link

h-muhammad-ali commented Dec 24, 2021

@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.

@hossein-zare
Copy link
Owner

hossein-zare commented Dec 31, 2021

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

@amirbhz86
Copy link

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

@amirbhz86
Copy link

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

@nbarshain
Copy link

nbarshain commented Apr 12, 2022

@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>
    </>
  );

@mugiwarafx
Copy link

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

@lafiosca
Copy link

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

@mikehardy
Copy link
Collaborator Author

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

@gauravstanza
Copy link

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 }, []);

@ice-cap0
Copy link

ice-cap0 commented Feb 9, 2023

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.

@ice-cap0
Copy link

ice-cap0 commented Feb 9, 2023

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

@TreeOfLearning
Copy link

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!

@meherhowji
Copy link

meherhowji commented Sep 13, 2023

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.

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

No branches or pull requests