JedWatson / react-select

The Select Component for React.js
https://react-select.com/
MIT License
27.63k stars 4.13k forks source link

positioning the component in containers that have `overflow: scroll;` #810

Closed ksmth closed 4 years ago

ksmth commented 8 years ago

I recently ran into this issue when using react-select components in a modal. For mobile the contents of the modal are scrollable. In this particular case, the Select was at the bottom of the content. Activating the Select made the container overflow and scrollable.

Maybe the container notion of http://react-bootstrap.github.io/react-overlays/examples/ might make sense? Alternatively, maybe make use of https://github.com/souporserious/react-tether?

RayMcCl commented 6 years ago

@spaja I managed to resolve this using a similar approach found within the Select2 jQuery package by creating an overlay which prevents scrolling on subcomponents and adjusting the height of the select dropdown relative to the window scroll. See below for reference:

Select Component

import React, {Fragment} from 'react';
import ReactSelect from 'react-select';
import 'react-select/dist/react-select.css';

export default class Select extends ReactSelect {

    renderOuter(options, valueArray, focusedOption) {
        const dimensions = this.wrapper ? this.wrapper.getBoundingClientRect() : null;
        const menu = super.renderMenu(options, valueArray, focusedOption)

        if (!menu || !dimensions) return null;

        const maxHeight = document.body.offsetHeight - (dimensions.top + dimensions.height)
        return ReactDOM.createPortal(
            <Fragment>
                <div className="Select-mask"></div>
                <div
                    ref={ref => { this.menuContainer = ref }}
                    className="Select-menu-outer"
                    onClick={(e) => { e.stopPropagation() }}
                    style={{
                        ...this.props.menuContainerStyle,
                        zIndex: 9999,
                        position: 'absolute',
                        width: dimensions.width,
                        top: window.scrollY + dimensions.top + dimensions.height,
                        left: window.scrollX + dimensions.left,
                        maxHeight: Math.min(maxHeight, 200),
                        overflow: 'hidden'
                    }}
                >

                    <div
                        ref={ref => { this.menu = ref }}
                        role="listbox"
                        tabIndex={-1}
                        className="Select-menu"
                        id={`${this._instancePrefix}-list`}
                        style={{
                            ...this.props.menuStyle,
                            maxHeight: Math.min(maxHeight, 200)
                        }}
                        onScroll={this.handleMenuScroll}
                        onMouseDown={this.handleMouseDownOnMenu}
                    >
                        {menu}
                    </div>
                </div>
            </Fragment>,
            document.body
        )
    }
}

Select CSS

.Select-mask {
    position: fixed;
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;
}
novikov-alexander-zz commented 6 years ago

For v2 solution check #2439

nynka commented 6 years ago

Thanks @kamagatos and @Enigma007x , your suggestions using Portals helped alot! I'm using react-select with dx-react-grid.

guilleCM commented 6 years ago

Thanks @nehero your solution saved me a lot of work! 👍 I want to marry you hahaha

opami commented 6 years ago

@kamagatos with the React Portal solution do you have working touch events in your environment?

I've tried your solution and the one from @nehero but with both solutions I seem to be losing the touch event support and cannot get the selects to work on iPad or Chrome devtools device testing options.

Anyone faced similar issues and if so, have you found any ways to work around those?

opami commented 6 years ago

Answering my own questions. It seems that in the Select component the following function will close the menu every time on touch devices:

    handleTouchOutside (event) {
        // handle touch outside on ios to dismiss menu
        if (this.wrapper && !this.wrapper.contains(event.target)) {
            this.closeMenu();
        }
    }

My guess is that because with both the react-tether and the ReactDOM.createPortal solutions the actual menu element is rendered directly to the body element in the dom, the this.wrapper.contains(event.target) call always return false.

To fix this I did override the handleTouchOutside function to also look whether the touch is inside the menu and menuContainer elements. Here is the updated version

    handleTouchOutside (event) {
        // handle touch outside on ios to dismiss menu
                if (this.wrapper && !this.wrapper.contains(event.target) &&
                    this.menuContainer && !this.menuContainer.contains(event.target) &&
                    this.menu && !this.menu.contains(event.target)) {

                    this.closeMenu();
                }
    }

With my quick testing this seemed to work. However I don't know if this has any unwanted side effects

Pyroboomka commented 6 years ago

@opami Don't know if it's connected or not, but since my old yarn.lock died, after reinstalling my fork i could not even select elements from menu with a mouse, only with a keyboard. Your solution seems to work, so thank you very much, you saved me :)

kkseven07 commented 6 years ago

Does not work for me, added menuPortalTarget={document.body} to Select component, but the problem still did not disappear. Outer container has overflow:auto, because its a modal which should have scroll if it has height more than some px.

crohn commented 5 years ago

To me this issue gets worse when using Async (even not in mobile), I think because the height of the menu changes after options get loaded.

Is there a way to trigger positioning computation manually? As far as I understand, MenuPlacer passes a ref callback to Menu in which getPlacement magic happens, updating MenuPlacer state. So I guess the answer is no.

Do you think exposing an API to trigger positioning computation could be useful in future releases? Something like the focus or blur you already expose.

oskarkook commented 5 years ago

menuPortalTarget is decent, but in a fixed scrollable modal, in some cases it does not work well with scrolling (i.e. if you have another fixed element below the scrollable area). You can use closeMenuOnScroll, but it is quite visibly not closing the menu quickly enough.

For now, I have manually overriden react-select's menu placing logic by manually calculating menuPlacement prop value and passing it to Select. The problem is that Select contains some state inside of it, so you have to fully rerender Select when menu placement changes (you do this by passing a new key prop).

As mentioned by @crohn, I think it should be considered to allow explicitly overriding the menu placement logic.

andreifg1 commented 4 years ago

Did you try using menuPosition="fixed" like that: <ReactSelect {...props} menuPosition="fixed" /> ?

vitorbertolucci commented 4 years ago

@andreifg1 menuPosition="fixed" did the trick on v3 👍

binarytrance commented 4 years ago

@maxmatthews you can try the following, it worked for me.

1 - Comment out the rule /*top:100px*/ from the original css class .Select-menu-outer 2 - Add the following custom css to your stylesheet:

.menu-outer-top .Select-menu-outer {
    bottom: 35px!important;
    border-bottom-right-radius: 0px!important;
    border-bottom-left-radius: 0px!important;
    border-top-right-radius: 4px!important;
    border-top-left-radius: 4px!important;
}

3 - Add the class .menu-outer-top to the Select to manually change to position of the dropdown (See screenshot below)

                <Select
                  name="form-field-Country"
                  value="one"
                  className="menu-outer-top"
                  placeholder=" Please choose a country"
                  value = {Country.value}
                  clearable = {false}
                  options={this.props.countries}
                  onChange={this.SelecCountryChange}
                  optionComponent={CountryOption}

                  />

Example: 683jq

Final results: image

Hope it helps!

for me this answer worked. but the dropdown comonent was not taking the class specified i.e .Select-menu-outer maybe because I am using styled components. It did have <some_gibberish_code>-menu as it's class. This is the selector I used .menu-outer-top[class*='menu'] {// add css here }

bladey commented 4 years ago

Hi all,

In an effort to sustain the react-select project going forward, we're closing issues that appear to have been resolved via community comments or issues that are now redundant based on their age.

However, if you feel this issue is still relevant and you'd like us to review it - please leave a comment and we'll do our best to get back to you, and we'll re-open it if necessary.