JedWatson / react-select

The Select Component for React.js
https://react-select.com/
MIT License
27.62k 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?

geon commented 8 years ago

Really important feature.

There are plenty of situations where you you don't want the drop-down to cause overflow. Actually, it should be the default behavior, since that's how the native select box work.

oluckyman commented 8 years ago

one more use case when dropdown should flow above all elements and should not be restricted by container’s size

ss 2016-03-10 at 4 13 28 pm
khmelevskii commented 8 years ago

+1

juan0087 commented 8 years ago

@oluckyman Having the same issue here! If I have other elements below the Select, I have no issues since there is plenty of space and I also reduce the height of the dropdown, but when the select is closer to the bottom of the page it pushed the main container.

Were you able to find a solution for this?

dropdown position

maxmatthews commented 8 years ago

Has anyone had any luck with this? I can't use a react-select in a modal without it causing scrolling issues. If I try and hijack .Select-menu-outer and set it to position: fixed I get weird scrolling issues. Someone suggested using react-tether, but it doesn't look like that's an easy implementation.

juan0087 commented 8 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!

maxmatthews commented 8 years ago

@juan0087 WOW! Thanks for that detailed response. Will give it a shot right now, and I'm sure it will help others in the future.

juan0087 commented 8 years ago

It's a pleasure! It's not 100% the correct way to do it but it does the job.

maxmatthews commented 8 years ago

@juan0087 Using your solution I still have a problem with the select menu getting cropped by the bounding modal box/div. Any suggestions? screen shot 2016-05-23 at 11 20 34 am

screen shot 2016-05-23 at 11 03 24 am

maxmatthews commented 8 years ago

To further complicate things, if you scroll in Chrome it uncrops the menu and shows the whole thing. Could that possible be related to this comment in menu.scss?

    // Unfortunately, having both border-radius and allows scrolling using overflow defined on the same
    // element forces the browser to repaint on scroll.  However, if these definitions are split into an
    // outer and an inner element, the browser is able to optimize the scrolling behavior and does not
    // have to repaint on scroll.
maxmatthews commented 8 years ago

I just ended up setting my modal to overflow: auto and had to make no other changes. So my problem is resolved, but in case anyone else stumbles upon this, check out the react-modal closed issue above.

benkeen commented 8 years ago

+1

M3lkior commented 8 years ago

Same Issue here.

image

oluckyman commented 8 years ago

I ended up using dropdownComponent prop with my own wrapper component around the menu component. That wrapper is using 'portal' technic to render menu into body node. Also I’ve used tether lib to position menu. Pretty happy with this solution. Bonus: now the menu drops up if there is no place below the input.

M3lkior commented 8 years ago

Can you provide the snippet @oluckyman ?

oluckyman commented 8 years ago

My case is pretty complex. So here are only important parts. In my Dropdown component render I do:

      <Select
        dropdownComponent={ DropdownMenu }

And here are parts of DropdownMenu component:

  componentDidMount() {
    /*
     * DropdownMenu is called from dropdown component. In order to get the
     * dropdown component dom node we have to render menu first.
     * Now in `componentDidMount` when it was renderd we can access parentNode and
     * inherit some CSS styles from it and rerender menu again with proper styling.
     * So this is why we call `setState` here and cause second render
     */
    const dropdownFieldNode = ReactDOM.findDOMNode(this).parentNode;

and in render I return:

    return (
      <TetherComponent target={ dropdownFieldNode } options={ options }>
        { this.props.children }
      </TetherComponent>
    );

And here is TetherComponent as is (just a wrapper around tether lib):

import React from 'react';
import ReactDOM from 'react-dom';
import Tether from 'tether';

/**
 * This component renders `children` in a tetherContainer node and
 * positions the children near the `target` node using rules from `options`
 */
const TetherComponent = React.createClass({
  propTypes: {
    children: React.PropTypes.node,
    target: React.PropTypes.object,
    options: React.PropTypes.object,
  },

  componentWillMount() {
    // init tether container
    this.tetherContainer = document.getElementById('tetherContainer');
    if (!this.tetherContainer) {
      this.tetherContainer = document.createElement('div');
      this.tetherContainer.id = 'tetherContainer';
      document.body.appendChild(this.tetherContainer);
    }
  },

  componentDidMount() {
    this.update();
  },

  componentDidUpdate() {
    this.update();
  },

  componentWillUnmount() {
    this.destroy();
  },

  update() {
    if (!this.props.target) return;

    this.element = ReactDOM.render(this.props.children, this.tetherContainer);

    if (!this.tether) {
      this.tether = new Tether({
        ...this.props.options,
        element: this.element,
        target: this.props.target,
      });
    }
    this.tether.position();
  },

  destroy() {
    ReactDOM.unmountComponentAtNode(this.tetherContainer);
    this.tether.destroy();
  },

  render() {
    return <div />;
  }
});

export default TetherComponent;
iyn commented 8 years ago

Would be good to find easier solution to this problem, anybody has ideas where to look?

mehrdad-shokri commented 8 years ago

@bvaughn can help on this one? thanks

jrmyio commented 8 years ago

Maybe it's possible to overwrite react-select 's render method and wrap the components in "react-tether" 's TetherComponent? Currently experimenting with this but I am not sure if this is the right way to go.

"react-tether" requires you two have two children next to eachother where the first child is the target (in react-select case that would be the Input/Value components) and the second child has to be the dropdown (renderOuter).

Has anyone else tried this approach?

eng1neer commented 8 years ago

Thanks @oluckyman for the great solution. I've modified it a bit to work with react-select:

TetheredSelect component that overrides Select's menu rendering:

import React from 'react';
import Select from 'react-select';
import ReactDOM from 'react-dom';
import TetherComponent from './TetherComponent';

export default class TetheredSelect extends Select {
    constructor(props) {
        super(props);

        this.renderOuter = this._renderOuter;
    }

    componentDidMount() {
        super.componentDidMount.call(this);

        this.dropdownFieldNode = ReactDOM.findDOMNode(this);
    }

    _renderOuter() {
        const menu = super.renderOuter.apply(this, arguments);

        const options = {
            attachment: 'top left',
            targetAttachment: 'bottom left',
            constraints: [
                {
                    to: 'window',
                    attachment: 'together',
                }
            ]
        };

        return (
            <TetherComponent
                target={this.dropdownFieldNode}
                options={options}
                matchWidth
            >
                {/* Apply position:static to our menu so that it's parent will get the correct dimensions and we can tether the parent */}
                {React.cloneElement(menu, {style: {position: 'static'}})}
            </TetherComponent>
        )
    }
}

(Btw, does anybody know, why renderOuter can't be directly overridden here?)

TetherComponent:

import React, {Component} from 'react';
import ReactDOM from 'react-dom';
import Tether from 'tether';

class TetheredChildrenComponent extends Component {
    render() {
        return this.props.children;
    }

    componentDidMount() {
        this.props.position();
    }

    componentDidUpdate() {
        this.props.position();
    }
}

export default class TetherComponent extends Component {
    componentDidMount() {
        this.tetherContainer = document.createElement('div');
        document.body.appendChild(this.tetherContainer);

        this.renderTetheredContent();
    }

    componentDidUpdate() {
        this.renderTetheredContent();
    }

    componentWillUnmount() {
        this.destroyTetheredContent();
    }

    renderTetheredContent() {
        ReactDOM.render(
            <TetheredChildrenComponent
                target={this.props.target}
                position={this.position}
            >
                {this.props.children}
            </TetheredChildrenComponent>,
            this.tetherContainer
        );
    }

    position = () => {
        if (!this.tether) {
            this.tether = new Tether({
                ...this.props.options,
                element: this.tetherContainer,
                target: this.props.target,
            });
        }

        if (this.props.matchWidth) {
            this.tetherContainer.style.width = `${this.props.target.clientWidth}px`;
        }

        this.tether.position();
    };

    destroyTetheredContent() {
        ReactDOM.unmountComponentAtNode(this.tetherContainer);

        this.tether.destroy();

        document.body.removeChild(this.tetherContainer);
    }

    render() {
        return null;
    }
}
stinoga commented 8 years ago

@eng1neer Awesome! Any way to get that working with Select.AsyncCreatable? It doesn't have a componentDidMount function to call via super.

eng1neer commented 8 years ago

@stinoga

Select.AsyncCreatable's source is pretty much concise:

import React from 'react';
import Select from './Select';

const AsyncCreatable = React.createClass({
    displayName: 'AsyncCreatableSelect',

    render () {
        return (
            <Select.Async {...this.props}>
                {(asyncProps) => (
                    <Select.Creatable {...this.props}>
                        {(creatableProps) => (
                            <Select
                                {...asyncProps}
                                {...creatableProps}
                                onInputChange={(input) => {
                                    creatableProps.onInputChange(input);
                                    return asyncProps.onInputChange(input);
                                }}
                            />
                        )}
                    </Select.Creatable>
                )}
            </Select.Async>
        );
    }
});

module.exports = AsyncCreatable;

I would just replace the inner Select with the subclass

stinoga commented 8 years ago

@eng1neer You'd update the code directly in the npm module with the TetherComponent class? Seems a bit hacky.

eng1neer commented 8 years ago

@stinoga No, just copy the Select.AsyncCreatable to your project with replaced import

import Select from './PathToYourTetheredSelectFromTheAbove';

stinoga commented 8 years ago

@eng1neer Thanks! For anyone who may need it, here's my full code implementing Tether with Select.AsyncCreatable:

import React from 'react';
import ReactDOM from 'react-dom';
import Select from 'react-select';
import Tether from 'tether';

class TetheredChildrenComponent extends React.Component {
  render() {
    return this.props.children;
  }

  componentDidMount() {
    this.props.position();
  }

  componentDidUpdate() {
    this.props.position();
  }
}

class TetherComponent extends React.Component {
  constructor(props) {
    super(props);
    this.position = this.position.bind(this);
  }

  componentDidMount() {
    this.tetherContainer = document.createElement('div');
    document.body.appendChild(this.tetherContainer);

    this.renderTetheredContent();
  }

  componentDidUpdate() {
    this.renderTetheredContent();
  }

  componentWillUnmount() {
    this.destroyTetheredContent();
  }

  position() {
    if (!this.tether) {
      this.tether = new Tether({
        ...this.props.options,
        element: this.tetherContainer,
        target: this.props.target,
      });
    }

    if (this.props.matchWidth) {
      this.tetherContainer.style.width = `${this.props.target.clientWidth}px`;
    }

    this.tether.position();
  }

  renderTetheredContent() {
    ReactDOM.render(
      <TetheredChildrenComponent
        target={this.props.target}
        position={this.position}
      >
        {this.props.children}
      </TetheredChildrenComponent>,
      this.tetherContainer
    );
  }

  destroyTetheredContent() {
    ReactDOM.unmountComponentAtNode(this.tetherContainer);

    this.tether.destroy();

    document.body.removeChild(this.tetherContainer);
  }

  render() {
    return null;
  }
}

class TetheredSelectWrap extends Select {
  constructor(props) {
    super(props);

    this.renderOuter = this._renderOuter;
  }

  componentDidMount() {
    super.componentDidMount.call(this);

    this.dropdownFieldNode = ReactDOM.findDOMNode(this);
  }

  _renderOuter() {
    const menu = super.renderOuter.apply(this, arguments);

    // Don't return an updated menu render if we don't have one
    if (!menu) {
      return;
    }

    const options = {
      attachment: 'top left',
      targetAttachment: 'bottom left',
      constraints: [
        {
          to: 'window',
          attachment: 'together',
        }
      ]
    };

    return (
      <TetherComponent
        target={this.dropdownFieldNode}
        options={options}
        matchWidth
      >
        {/* Apply position:static to our menu so that it's parent will get the correct dimensions and we can tether the parent */}
        {React.cloneElement(menu, {style: {position: 'static'}})}
      </TetherComponent>
    );
  }
}

// Call the AsyncCreatable code from react-select with our extended tether class
class TetheredSelect extends React.Component {
  render () {
    return (
      <TetheredSelectWrap.Async {...this.props}>
        {(asyncProps) => (
          <TetheredSelectWrap.Creatable {...this.props}>
            {(creatableProps) => (
              <TetheredSelectWrap
                {...asyncProps}
                {...creatableProps}
                onInputChange={(input) => {
                  creatableProps.onInputChange(input);
                  return asyncProps.onInputChange(input);
                }}
              />
              )}
            </TetheredSelectWrap.Creatable>
            )}
          </TetheredSelectWrap.Async>
    );
  }
}

export default TetheredSelect;
stinoga commented 8 years ago

Actually, I was able to shorten this a good bit using react-tether and react-dimensions:

import React from 'react';
import Dimensions from 'react-dimensions';
import Select from 'react-select';
import TetherComponent from 'react-tether';

class TetheredSelectWrap extends Select {
  constructor(props) {
    super(props);

    this.renderOuter = this._renderOuter;
  }

  componentDidMount() {
    super.componentDidMount.call(this);
  }

  _renderOuter() {
    const {containerWidth} = this.props;
    const menu = super.renderOuter.apply(this, arguments);

    // Don't return an updated menu render if we don't have one
    if (!menu) {
      return;
    }

    return (
      <TetherComponent
        renderElementTo="body"
        ref="tethered-component"
        attachment="top left"
        targetAttachment="top left"
        constraints={[{
          to: 'window',
          attachment: 'together',
          pin: ['top']
        }]}
      >
        {/* Apply position:static to our menu so that it's parent will get the correct dimensions and we can tether the parent */}
        <div></div>
        {React.cloneElement(menu, {style: {position: 'static', width: containerWidth}})}
      </TetherComponent>
    );
  }
}

// Call the AsyncCreatable code from react-select with our extended tether class
class TetheredSelect extends React.Component {
  render () {
    return (
      <TetheredSelectWrap.Async {...this.props}>
        {(asyncProps) => (
          <TetheredSelectWrap.Creatable {...this.props}>
            {(creatableProps) => (
              <TetheredSelectWrap
                {...asyncProps}
                {...creatableProps}
                onInputChange={(input) => {
                  creatableProps.onInputChange(input);
                  return asyncProps.onInputChange(input);
                }}
              />
              )}
            </TetheredSelectWrap.Creatable>
            )}
          </TetheredSelectWrap.Async>
    );
  }
}

export default Dimensions()(TetheredSelect);
eng1neer commented 8 years ago

@stinoga That's cool! I didn't know that react-tether can replant elements to body

SeanRoberts commented 8 years ago

Is anyone currently working on adding this either as default behaviour or as an option? @stinoga's solution seems to work well.

burtyish commented 8 years ago

Thanks for everyone sharing here. Since I found this thread useful, I want to share my experience on using Dimensions here. Wrapping the entire Select with Dimensions, as in https://github.com/JedWatson/react-select/issues/810#issuecomment-250274937, results in the entire component being wrapped in a div styled by tether.js thus:

{
    width: 100%;
    height: 100%;
    padding: 0px;
    border: 0px;
}

I found this can create a problem, for instance if the Select has a float style.

I solved this by getting Dimensions to wrap just Select's outer element. Here's my tweak. Note: I solved it for regular, not CreatableSelect.

import React, { Component } from 'react';
import Select from 'react-select';
import TetherComponent from 'react-tether';
import Dimensions from 'react-dimensions';

class WrappedOuter extends Component {
    render() {
        const {
            outer,
            containerWidth
        } = this.props;

        return (
            <TetherComponent
                attachment="top left"
                targetAttachment="bottom left"
                constraints={[{
                    to: 'scrollParent',
                    attachment: 'together',
                }]}
                classes={{element: 'tethered-select-options'}}
            >
                {/* The first child is tether's target */}
                <div></div>
                {/* Apply position:static to our menu so that it's parent will get the correct dimensions and we can tether the parent */}
                {React.cloneElement(outer, {style: {position: 'static', minWidth: containerWidth}})}
            </TetherComponent>
        );
    }
}

WrappedOuter = Dimensions()(WrappedOuter); // <---- Here's where the outer element is wrapped with Dimensions

export default class TetheredSelect extends Select {
    constructor(props) {
        super(props);

        this.renderOuter = this._renderOuter;
    }

    componentDidMount() {
        super.componentDidMount.call(this);
    }

    _renderOuter() {
        const outer = super.renderOuter.apply(this, arguments);

        // Don't return an updated menu render if we don't have one
        if (!outer) {
            return null;
        }

        return <WrappedOuter outer={outer}/>;
    }
}
jrmyio commented 8 years ago

Is it just me or is stinoga's example breaking touch support?

russpowers commented 7 years ago

In case anyone needs it, here's a hacked together TypeScript version based on the post by @burtyish. I think I fixed the mobile issues, see the _handleTouchOutside method, it needs to check that the touch event was inside the tethered item (which is not inside the react-select wrapper). It does this by overriding the react-select handleTouchOutside method. I also switched to react-measure for measurements, since I use that elsewhere in my project.

import * as React from 'react';
import * as Select from 'react-select';
var TetherComponent = require('react-tether');
import * as Measure from "react-measure";

function WrappedOuter(outer: JSX.Element, dimensions: Measure.Dimensions){
    console.log(dimensions)
    return (
        <TetherComponent
            attachment="top left"
            targetAttachment="bottom left"
            classes={{element: 'tethered-select-options'}}
        >
            {/* The first child is tether's target */}
            <div></div>
            {/* Apply position:static to our menu so that it's parent will get the correct dimensions and we can tether the parent */}
            {React.cloneElement(outer, {style: {position: 'static', minWidth: dimensions.width}})}
        </TetherComponent>
    );
}

class MeasuredOuter extends React.Component<{ outer: JSX.Element }, {}> {
    render() {
        return (
            <Measure>
                { (dimensions: Measure.Dimensions) => WrappedOuter(this.props.outer, dimensions) }
            </Measure>
        );
    }
}

export class TetheredSelect extends Select {
    constructor(props: any) {
        super(props);
        this.mySuper = Object.getPrototypeOf(Object.getPrototypeOf(this)); // Is there a better way to get an untyped super in TypeScript? 
        this.renderOuter = this._renderOuter;
        this.handleTouchOutside = this._handleTouchOutside;
    }

    componentDidMount() {
        this.mySuper.componentDidMount.call(this);
    }

    _handleTouchOutside(event: any) {
        // The original react-select code is modified to also check if the touch came from inside the tethered container
        if (this.wrapper && !this.wrapper.contains(event.target) && !this.measuredOuter.contains(event.target)) {
            (this as any).closeMenu();
        }
    }

    _renderOuter() {
        const outer = this.mySuper.renderOuter.apply(this, arguments);

        // Don't return an updated menu render if we don't have one
        if (!outer) {
            return null;
        }

        return <MeasuredOuter ref={x => this.measuredOuter = x } outer={outer} />;
    }

    handleTouchOutside: any;
    measuredOuter: any;
    renderOuter: any;
    mySuper: any;
    wrapper: any;
}
pgoldweic commented 7 years ago

Could somebody explain (to a newbie) how to use the TetheredSelect component created above? I've tried using it in my code in place of react-select's Select, but I haven't been successful (instead of a dropdown display, I get a '0' value instead :-(). Thanks!

Update: I've actually been able to use the solution as suggested by @eng1neer, but not the later one.

SeanRoberts commented 7 years ago

Here's an ES6 implementation of @russpowers' TypeScript component if anyone wants it:

import React, { Component } from 'react';
import Select from 'react-select';
import TetherComponent from 'react-tether';
import dimensions from 'react-dimensions';

class WrappedOuter extends Component {
  render() {
    const {
      outer,
      containerWidth
    } = this.props;

    return (
      <TetherComponent
        attachment="top left"
        targetAttachment="bottom left"
        constraints={[{
          to: 'scrollParent',
          attachment: 'together'
        }]}
        classes={{ element: 'tethered-select-options' }}
      >
        {/* The first child is tether's target */}
        <div />
        {/*
          Apply position:static to our menu so that it's parent
          will get the correct dimensions and we can tether the parent
        */}
        {React.cloneElement(outer, {
          style: { position: 'static', minWidth: containerWidth }
        })}
      </TetherComponent>
    );
  }
}

const DimensionsWrappedOuter = dimensions()(WrappedOuter);

export default class TetheredSelect extends Select {
  constructor(props) {
    super(props);

    this.renderOuter = this._renderOuter;
    this.handleTouchOutside = this._handleTouchOutside;
  }

  componentDidMount() {
    super.componentDidMount.call(this);
  }

  _handleTouchOutside(event) {
    // The original react-select code is modified to also check if the
    // touch came from inside the tethered container
    const isNotInWrapper = this.wrapper &&
      !this.wrapper.contains(event.target);
    const isNotInOuter = this.measuredOuter &&
      !this.measuredOuter.contains(event.target);

    if (isNotInWrapper && isNotInOuter) {
      this.closeMenu();
    }
  }

  _renderOuter() {
    const outer = super.renderOuter.apply(this, arguments);

    // Don't return an updated menu render if we don't have one
    if (!outer) {
      return null;
    }

    return (
      <DimensionsWrappedOuter
        ref={(x) => (this.measuredOuter = x)}
        outer={outer} />
    );
  }
}

And here's the same handleClickOutside function applied to @stinoga's component for Async/Createable

import React from 'react';
import Dimensions from 'react-dimensions';
import Select from 'react-select';
import TetherComponent from 'react-tether';

class TetheredSelectWrap extends Select {
  constructor(props) {
    super(props);

    this.renderOuter = this._renderOuter;
    this.handleTouchOutside = this._handleTouchOutside;
  }

  _handleTouchOutside(event) {
    // The original react-select code is modified to also check if the
    // touch came from inside the tethered container
    if (this.wrapper && !this.wrapper.contains(event.target)) {
      this.closeMenu();
    }
  }

  componentDidMount() {
    super.componentDidMount.call(this);
  }

  _renderOuter() {
    const { containerWidth } = this.props;
    const menu = super.renderOuter.apply(this, arguments);

    // Don't return an updated menu render if we don't have one
    if (!menu) {
      return <noscript />;
    }

    return (
      <TetherComponent
        renderElementTo="body"
        ref="tethered-component"
        attachment="top left"
        targetAttachment="top left"
        constraints={[{
          to: 'window',
          attachment: 'together',
          pin: ['top']
        }]}
      >
        {/* Apply position:static to our menu so that it's parent will get the correct dimensions and we can tether the parent */}
        <div></div>
        {React.cloneElement(menu, {style: {position: 'static', width: containerWidth}})}
      </TetherComponent>
    );
  }
}

// Call the AsyncCreatable code from react-select with our extended tether class
class TetheredSelect extends React.Component {
  render() {
    return (
      <TetheredSelectWrap.Async {...this.props}>
        {(asyncProps) => (
          <TetheredSelectWrap.Creatable {...this.props}>
            {(creatableProps) => (
              <TetheredSelectWrap
                {...asyncProps}
                {...creatableProps}
                onInputChange={(input) => {
                  creatableProps.onInputChange(input);
                  return asyncProps.onInputChange(input);
                }}
              />
            )}
          </TetheredSelectWrap.Creatable>
        )}
      </TetheredSelectWrap.Async>
    );
  }
}

export default Dimensions()(TetheredSelect);

These both seem to work on desktop and mobile for me.

ozziexsh commented 7 years ago

I ran into the same problem as @burtyish. (before actually reading his comment) I found a fix that works for the regular react-select (haven't tested creatable/async) that doesn't use react-dimensions

import React from 'react';
import Select from 'react-select';
import TetherComponent from 'react-tether';

/** from https://github.com/JedWatson/react-select/issues/810#issuecomment-250274937 **/
export default class TetheredSelectWrap extends Select {

    constructor(props) {
        super(props);

        this.renderOuter = this._renderOuter;
    }

    _renderOuter() {
        const menu = super.renderOuter.apply(this, arguments);

        // Don't return an updated menu render if we don't have one
        if (!menu) {
            return;
        }

        /** this.wrapper comes from the ref of the main Select component (super.render()) **/
        const selectWidth = this.wrapper ? this.wrapper.offsetWidth : null;

        return (
            <TetherComponent
                renderElementTo="body"
                ref="tethered-component"
                attachment="top left"
                targetAttachment="top left"
                constraints={[{
                    to: 'window',
                    attachment: 'together',
                    pin: ['top']
                }]}
            >
                {/* Apply position:static to our menu so that it's parent will get the correct dimensions and we can tether the parent */}
                <div></div>
                {React.cloneElement(menu, {style: {position: 'static', width: selectWidth}})}
            </TetherComponent>
        );
    }

}

Now your component doesn't need to be the full width, and has one less dependency.

negrin commented 7 years ago

a noob question.... how can i load a wrap component to the drop down? for example "nehero" "TetheredSelectWrap" component i saw that oluckyman used "dropdownComponent={ DropdownMenu }" but i don't see any support of it in "Select.js" only "optionComponent" i'm using version: 1.0.0

cherihung commented 7 years ago

@nehero's solution worked well for me. But I based the width calculation off this.control instead of this.wrapper. That gives a more accurate width for visually aligning the dropdown widths and the select.

tutok commented 7 years ago

After applying @nehero's solution I am lost ipad support. I can not select any option even in chrome device emulator. I have tried react-fastclick but it does not help. It looks like menu rendered inside TetherComponent stoped reacting on touch events?

I am using: "react-select": "^1.0.0-rc.3"

cherihung commented 7 years ago

@tutok i've not dealt with the touch support issue in my application using @nehero's solution. but this is probably the way? see. the _handleTouchOutside solution from @russpowers's comment https://github.com/JedWatson/react-select/issues/810#issuecomment-263863746

matthewhartman commented 7 years ago

@oluckyman I solved your particular issue by adding the following CSS rule for .Select-menu-outer

.Select-menu-outer { position: relative; }

This will make the dropdown selection part of the layout instead of floating above it and you will be able to scroll the container to see the rest of the drop down results. (trying to spin up a codepen example to show you as I encountered this same problem on a project I was working on)

oluckyman commented 7 years ago

thanks for your efforts @matthewhartman. I do not use react-select anymore but go ahead and share your example with community 👍

SeanRoberts commented 7 years ago

@oluckyman What did you switch to?

oluckyman commented 7 years ago

@SeanRoberts to Ant Design UI Library. It has Select component as well (https://ant.design/components/select/). Under the hood, they use rc-* components, so probably for Select it’s https://github.com/react-component/select

kamagatos commented 6 years ago

If you are using react 16+ and you're not willing to add new dependencies to your project (react-tether and react-dimensions), you could use react portals to achieve the same behavior.

import React from 'react'
import ReactDOM from 'react-dom'
import ReactSelect from 'react-select'

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(
            <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: dimensions.top + dimensions.height,
                    left: 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>,
            document.body
        )
    }
}
fluke commented 6 years ago

@kamagatos I think it'd be great if you could make a pull request to integrate React Portals into this project. It'll be very useful.

guilleCM commented 6 years ago

I found a css solution without using Portals. In your Select wrapper component capture and handle the onOpen function of the Select component to repositionate the container with fixed position based on the Select-control div. Probably not the best solution, but it's simple and it works. Hope it helps


export class MySelectWrapper extends React.Component {
  //constructor, state, and wathever methods you need
 onOpen() {
    let inputWrapper = $('.Select-control').get(0).getBoundingClientRect();
    $('.Select-menu-outer').css({
      'top': inputWrapper.top+inputWrapper.height+'px',
      'left': inputWrapper.left+'px',
      'width': inputWrapper.width+'px',
    })
}
render() {
     return(
       <Select
           onOpen={this.onOpen}
           menuContainerStyle={{'position':'fixed', 'zIndex': '1500'}}
           //...rest of properties
        />
     )
}```
Enigma007x commented 6 years ago

@kamagatos Thank you for this! Made my life so much easier.

One note, I was using this in a page that is scrollable and I had to make the following addition when setting the top so that it worked when scrolling:

top: dimensions.top + dimensions.height + window.pageYOffset

I agree that it would be great if this were somehow baked into react-select. Thanks again!

spaja commented 6 years ago

@kamagatos using Portals is a way to go! The only problem with your example is that when the page is scrolled down, the menu will have the wrong top position: top: dimensions.top + dimensions.height, in your example. You should also add a vertical scroll value like this top: dimensions.top + dimensions.height + window.scrollY so when your page is scrolled down before opening select - the outer menu will appear as it should - below your field. If your page could have a horizontal scroll, just think about that also.

spaja commented 6 years ago

There is still one problem with the @kamagatos Portal solution: when you open your menu on a page with a lot of content(with browser scroll bar) and then we scroll with our browser, the menu stays open and floating. Anyone managed to close the react-select menu on browser scrolling?

MitchellONeill commented 6 years ago

@guilleCM Not sure if this has changed since you posted, but onOpen fires before the Select-menu-outer element has been created, so you cannot apply the css to it.

guilleCM commented 6 years ago

@MitchellONeill for me is working, and when I debug on the web browser, I can select the $('.Select-menu-outer') element and set the new css rules (previously, in the render method, I set position: fixed in the style inline declaration with react). Sorry about my english 1a69634c6aa3b9894ac91e23a7ac5b7b 1