Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Touchable presses not firing in absolute positioned container with zIndex (for a dropdown)

I have been trying to tweak the styles and elements render order so that Android and iOS behave the same. Did a lot of research and the current answers don't seem to solve the issue.

  • Product: I am writing a cross platform dropdown component
  • Issue/Behavior: On iOS the presses fire correctly, however on android they fall off.
  • Example: Here is a codepen showing what it should do Please fork it if you want to make changes on the codepen

The issue is this exact code works on iOS but not on an Android device.

class Dropdown extends React.Component {
  constructor() {
    super()
    this.state = {
      open: false,
      selected: undefined,
    }
  }

  handleSelectPress = () => {
    this.setState({open: !this.state.open});
  }
  handleOptionPress = (item) => {
    this.setState({selected: item, open: !this.state.open});
    this.props.onSelectOption(item);
  }
  render () {
    const { selected = {}, open } = this.state
    const { placeholder = '', options = [] } = this.props

    return (
      <Select
        onPress={this.handleSelectPress}
        value={selected.name || placeholder}
        open={open}
        options={options}
        onOptionPress={this.handleOptionPress} />
    )
  }
}

const shadowStyles = {
  ios: {
    shadowOpacity: 0.3,
    shadowRadius: 3,
    shadowOffset: {
        height: 1,
        width: 1,
    },
  },
  android: {
    elevation: 5,
  },
}

class Select extends React.PureComponent {
  render () {
    const { value, open, options, onOptionPress } = this.props
    const shadowStyle = shadowStyles[Platform.OS]
    return (
      <View style={[shadowStyle, styles.wrap]}>
        <TouchableWithoutFeedback onPress={this.props.onPress}>
          <View style={styles.selectContainer}>
            <Text>{value}</Text>
          </View>
        </TouchableWithoutFeedback>
        {open && <View style={styles.menu}>
          { options.map( (item, idx) => <Option value={item} key={idx} onPress={onOptionPress} />)}
        </View>}
      </View>

    )
  }
}

class Option extends React.PureComponent {
  handlePress = () => {
    this.props.onPress(this.props.value)
  }
  render () {
    const { value } = this.props
    return (
      <TouchableOpacity onPress={this.handlePress}>
        <View style={styles.optionContainer}>
          <Text>{value.name}</Text>
        </View>
      </TouchableOpacity>
    )
  }
}

class App extends React.Component {
  render() {
    return (
      <View style={styles.root}>
        <Dropdown
          placeholder="--- SELECT ---"
          options={
            [
              { name: "Press me!", id: "1" },
              { name: "No Me!", id: "2" },
              { name: "Cmon guys, here!", id: "3" },
            ]
          }
          onSelectOption={ (item) => console.log(`item pressed! ${item}`)}
        />
      </View>
    )
  }
}

const styles = StyleSheet.create({
    root: {
      width: 360,
      height: 640,
      backgroundColor: '#292c2e'
    },
    wrap: {
      position: 'relative',
      zIndex: 10,
      backgroundColor: 'rgb(73,75,77)'
    },
    selectContainer: {
      padding: 16,
    },
    menu: {
      backgroundColor: 'rgb(73,75,77)',
      position: 'absolute',
      top: '100%',
      left: 0,
      right: 0,
      maxHeight: 300
    },
    optionContainer: {
      padding: 16
    }

  });

React Native version - 0.59.3

Few things to note:

  • I'm not looking for a package that already does this.
  • I'm using zIndex so that the menu element is rendered on top of other elements (rendered after the dropdown component).
  • I tried using TouchableHighlight, TouchableOpacity, TouchableWithoutFeedback and for android only TouchableNativeFeedback
  • I tried changing the "menu" element between a View and a ScrollView (initially was scroll)
  • I played around with elevation and z-index on the option / touchables
  • The touch on the Select that toggles the open flag works properly on both Android and iOS so I'm pretty sure its narrowed down to an absolute positioned parent.
  • I'm planning on adding animations, polishing the styles... etc (acutally using styled-components in the project). But first need it to function :D
like image 552
John Ruddell Avatar asked Apr 08 '19 18:04

John Ruddell


2 Answers

You have to set position for Select instead of Option

here is a snack: https://snack.expo.io/HkDHHo6YV

change wrap and menu style with given below:

wrap: {
  position: 'absolute',
  zIndex: 10,
  width: "100%",
  backgroundColor: 'rgb(73,75,77)',
},
menu: {
  backgroundColor: 'rgb(73,75,77)',
  maxHeight: 300,
},

Edit, Working solution

The missing piece was to account for the space under the dropdown since it was completely absolutely positioned. I had to change the Select component to render a "padding" element to account for the space.

class Select extends React.PureComponent {
  render() {
    const { value, open, options, onOptionPress } = this.props;
    const shadowStyle = shadowStyles[Platform.OS];
    return (
      <>
        <View style={{height: 60}}></View>
        <View style={[shadowStyle, styles.wrap]}>
          <TouchableWithoutFeedback onPress={this.props.onPress}>
            <View style={styles.selectContainer}>
              <Text>{value}</Text>
            </View>
          </TouchableWithoutFeedback>
          {open && (
            <View style={styles.menu}>
              {options.map((item, idx) => (
                <Option value={item} key={idx} onPress={onOptionPress} />
              ))}
            </View>
          )}
        </View>
      </>
    );
  }
}

Then adjusted the height of the styles.selectContainer to be the same height 60

selectContainer: {
  padding: 16,
  height: 60
},

Full working example

import * as React from 'react';
import {
  Text,
  Button,
  View,
  StyleSheet,
  Platform,
  TouchableWithoutFeedback,
  TouchableOpacity,
} from 'react-native';

class Dropdown extends React.Component {
  constructor() {
    super();
    this.state = {
      open: false,
      selected: undefined,
    };
  }

  handleSelectPress = () => {
    this.setState({ open: !this.state.open });
  };
  handleOptionPress = item => {
    this.setState({ selected: item, open: !this.state.open });
    this.props.onSelectOption(item);
  };
  render() {
    const { selected = {}, open } = this.state;
    const { placeholder = '', options = [] } = this.props;

    return (
      <Select
        onPress={this.handleSelectPress}
        value={selected.name || placeholder}
        open={open}
        options={options}
        onOptionPress={this.handleOptionPress}
      />
    );
  }
}

const shadowStyles = {
  ios: {
    shadowOpacity: 0.3,
    shadowRadius: 3,
    shadowOffset: {
      height: 1,
      width: 1,
    },
  },
  android: {
    elevation: 5,
  },
};

class Select extends React.PureComponent {
  render() {
    const { value, open, options, onOptionPress } = this.props;
    const shadowStyle = shadowStyles[Platform.OS];
    return (
      <>
        <View style={{height: 60}}></View>
        <View style={[shadowStyle, styles.wrap]}>
          <TouchableWithoutFeedback onPress={this.props.onPress}>
            <View style={styles.selectContainer}>
              <Text>{value}</Text>
            </View>
          </TouchableWithoutFeedback>
          {open && (
            <View style={styles.menu}>
              {options.map((item, idx) => (
                <Option value={item} key={idx} onPress={onOptionPress} />
              ))}
            </View>
          )}
        </View>
      </>
    );
  }
}

class Option extends React.PureComponent {
  handlePress = () => {
    this.props.onPress(this.props.value);
  };
  render() {
    const { value } = this.props;
    return (
      <TouchableOpacity onPress={this.handlePress}>
        <View style={styles.optionContainer}>
          <Text>{value.name}</Text>
        </View>
      </TouchableOpacity>
    );
  }
}

export default class App extends React.Component {
  render() {
    return (
      <View style={styles.root}>
        <Dropdown
          placeholder="--- SELECT ---"
          options={[
            { name: 'Press me!', id: '1' },
            { name: 'No Me!', id: '2' },
            { name: 'Cmon guys, here!', id: '3' },
          ]}
          onSelectOption={item => console.log(`item pressed! ${item}`)}
        />
        <Text>hellof</Text>
        <View style={{backgroundColor: "red", height: 250}} />
      </View>
    );
  }
}

const styles = StyleSheet.create({
  root: {
    width: 360,
    height: 640,
    backgroundColor: '#292c2e',
    marginTop: 100
  },
  wrap: {
    position: 'absolute',
    zIndex: 10,
    width: "100%",
    backgroundColor: 'rgb(73,75,77)',
  },
  selectContainer: {
    padding: 16,
    height: 60
  },
  menu: {
    backgroundColor: 'rgb(73,75,77)',
    maxHeight: 300,
  },
  optionContainer: {
    padding: 16,
    height: 60
  },
});
like image 145
Shashin Bhayani Avatar answered Oct 21 '22 17:10

Shashin Bhayani


There is a problem with your styles in styles.menu not with zIndex.

You have given position:'absolute' and top:'100%'.

This means your child view is positioned as absolute to the parent view and your child view will start right after your parent view is completing because of top:"100%".

Conclusion: your options are rendering out of the parent. yes, they are visible because there are no other components below and more of that zIndex is higher. and you can not touch anything that is not withing the range of parent.

Solution:

See working snack here

in styles.menu, change these,

  menu: {
    backgroundColor: 'rgb(73,75,77)',
    zIndex: 1005,
    top: 0,
    left: 0,
    right: 0,
    maxHeight: 300,
  },

and you can remove zIndex: 1000 and 1005 also :)

like image 27
Jaydeep Galani Avatar answered Oct 21 '22 15:10

Jaydeep Galani