JedWatson/react-select

How do I make the dropdown open at the top instead of bottom?

Closed this issue ยท 24 comments

How do I make the dropdown open at the top instead of bottom?

I too have this issue, where I have a dropdown near the bottom of the page and the dropdown menu breaks things when open because there is no room at the bottom. Being able to specify the direction that the dropdown opens would be a big plus.

Is there an easy way to do this?

vjpr commented

+1

medv commented

Add this to your css somewhere:

.Select-menu-outer{top: auto; bottom: 100%}

Of course then you would have to check how far you are scrolled and decide whether to open above/below. This would make a great addition, maybe your scrollable container's node passed as a prop to react-select for it to measure its dropdown height, the container's scrollTop vs the select. Not sure what the best way to get this done in react yet!

wanted to share my approach to this, in case it helps anyone:

import { findDOMNode } from 'react-dom';
import Select from 'react-select';

const MAX_MENU_HEIGHT = 200;
const AVG_OPTION_HEIGHT = 36;

class MySelect extends Component {
  constructor(props) {
    super(props);

    this.state = {
      dropUp: false,
    };

    this.determineDropUp = this.determineDropUp.bind(this);
  }

  componentDidMount() {
    this.determineDropUp(this.props);
    window.addEventListener('resize', this.determineDropUp);
    window.addEventListener('scroll', this.determineDropUp);
  }

  componentWillReceiveProps(nextProps) {
    this.determineDropUp(nextProps);
  }

  componentWillUnmount() {
    window.removeEventListener('resize', this.determineDropUp);
    window.removeEventListener('scroll', this.determineDropUp);
  }

  determineDropUp(props = {}) {
    const options = props.options || this.props.options || [];
    const node = findDOMNode(this.selectInst);

    if (!node) return;

    const windowHeight = window.innerHeight;
    const menuHeight = Math.min(MAX_MENU_HEIGHT, (options.length * AVG_OPTION_HEIGHT));
    const instOffsetWithMenu = node.getBoundingClientRect().bottom + menuHeight;

    this.setState({
      dropUp: instOffsetWithMenu >= windowHeight,
    });
  }

  render() {
    const className = this.state.dropUp ? 'drop-up' : '';
    
    return (
      <Select {...this.props} className={className} ref={inst => (this.selectInst = inst)} />
    );
  }
}
.drop-up .Select-menu-outer {
  top: auto;
  bottom: 100%;
}

the calculations may require some tweaking based on your page layout. it unfortunately doesn't work for async options since we don't have access to the loaded options via props. I also debounce the determineDropUp method to 100ms to avoid any thrashing from the resize and scroll listeners.

+1

medv commented

@vitosamson looks good, though I would base my solution on measurement so that your css can be king in terms of determining how big something is and what are its limits (regarding your pixel constants at the top of the file).

@medv I don't think that's possible, since the menu would have to already be open in order to measure its height and the height of the options.

medv commented

@vinayakpatil that's generally when you render a proxy somewhere off-screen or below layers with the same style directive and gather data from it. For each differently-styled react-select component that would subscribe to this thinking, you would spawn a dummy proxy menu (located in the same dom path available with the same selector you target your real component with so all css applies) that has a single item and take readings from that, then your measurements are correct.

It introduces a little performance loss due to this process (keeping in mind window-resize etc), but it solves a lot of front-end questions for those of us that can use css well and depend on it doing its job. Specific media-query directives defined in your css will save you a lot of time and issues - I have not used react-select in a project that didn't have the same component drawn in at least 2-3 ways depending on matching media query. Not a pre-set "breakpoint", since screen sizes are varied these days. It's better to use something like jeet.gs or lost-grid to write specific rules for your components from mobile-first up to wherever they feel like they don't belong anymore, at which point you bump them up to the next presentation rules.

Thanks @vitosamson! Here's the CSS I used in conjunction with what he already used to achieve a bit more visual consistency, i.e. margin fix for positioning, corner radii, remove box-shadow. Hope it helps.

.drop-up .Select-menu-outer {
	top: auto;
	bottom: 100%;
	margin-top: 0px;
	margin-bottom: -1px;
	border-bottom-right-radius: 0px;
	border-bottom-left-radius: 0px;
	border-top-left-radius: 4px;
	border-top-right-radius: 4px;
	box-shadow: none;
}

.drop-up.is-open > .Select-control {
	border-bottom-right-radius: 4px;
	border-bottom-left-radius: 4px;
	border-top-left-radius: 0px;
	border-top-right-radius: 0px;
}
dmr commented

In buildo/react-components#800 they integrated an option for this behavior

menuPosition="top"

--> How do you feel about this option for react-select?

+1

+1 hope this issue can be close tomorrow.

added to Select :

menuContainerStyle={{top: 'auto', bottom: '100%'}}

adccb commented

bump!

hey @JedWatson, i'd love to see some kind of response to this. this should be done by the library, not left to individual apps to hack together a solution. thanks!

@JedWatson Many libraries use https://popper.js.org/ for this desired intelligent popover positioning

add to Select :

menuPlacement = "top"

add to Select :

menuPlacement = "top"

@cathylollipop it work!

Use menuPlacement. Docs for the props API. Thanks @cathylollipop

Add this props to your component menuPlacement="auto" .

menuPlacement="auto" doesn't work for me, menu is still showing up below the component, even when it renders outside of the screen.

const [isOpenAbove, setIsOpenAbove] = useState<boolean | null>(null)

  const refSelectField = useRef<HTMLDivElement | null>(null)
  const refSelectDropdownField = useRef<HTMLDivElement | null>(null)

  useLayoutEffect(() => {
    function positionDropdown() {
      if (refSelectField.current) {
        const selectRect = refSelectField.current.getBoundingClientRect()
        const spaceBelow = window.innerHeight - selectRect.bottom
        if (refSelectDropdownField.current) {
          if (spaceBelow < refSelectDropdownField.current.offsetHeight) {
            setIsOpenAbove(true)
          } else {
            setIsOpenAbove(false)
          }
        }
      }
    }
    window.addEventListener('resize', positionDropdown)
    positionDropdown()

    return () => {
      window.removeEventListener('resize', positionDropdown)
    }
  }, [onFocus])
<div
          className={clsx(
            'absolute left-0',
            isOpenAbove
              ? 'bottom-auto top-0 -translate-y-full'
              : ' bottom-0 translate-y-full'
          )}
          ref={refSelectDropdownField}>{...custom dropdown}</div>

// Function to focus the select element
const openSelect = () => {
if (selectRef.current) {
selectRef.current.showPicker();
}
};

Open Select Option 1 Option 2 Option 3