2

Blueprint's Select component is exactly what I need for my current React project, with one exception: I need to add some elements to its popover and don't see any way to do it.

Specifically, I'd like to add a title (e.g. an H2 element) above the filter input, and a bar of buttons (e.g., some Button components in a DIV) below the list. Select seems highly configurable but I see no way to add elements inside the popover...what am I missing?

Dave Feldman
  • 104
  • 1
  • 9
  • 28

2 Answers2

2

If you want to extend the select's Menu and add custom elements to it, then you have to provide the itemListRenderer prop.

Here's what the docs says:

By default, Select renders the displayed items in a Menu. This behavior can be overridden by providing the itemListRenderer prop, giving you full control over the layout of the items. For example, you can group items under a common heading, or render large data sets using react-virtualized.

itemListRenderer example:

If provided, the itemListRenderer prop will be called to render the contents of the dropdown menu. It has access to the items, the current query, and a renderItem callback for rendering a single item. A ref handler (itemsParentRef) is given as well; it should be attached to the parent element of the rendered menu items so that the currently selected item can be scrolled into view automatically.

Therefore in the Menu component's body you can place your custom headings and buttons:

import { ItemListRenderer } from "@blueprintjs/select";

const renderMenu: ItemListRenderer<Film> = ({ items, itemsParentRef, query, renderItem }) => {
    const renderedItems = items.map(renderItem).filter(item => item != null);
    return (
        <Menu ulRef={itemsParentRef}>
            <h2>Your heading can be styled here</h2>
            <MenuItem
                disabled={true}
                text={`Found ${renderedItems.length} items matching "${query}"`}
            />
            {renderedItems}
            <div>
              <button>Button name</button>
            </div>
        </Menu>
    );
};

<FilmSelect
    itemListRenderer={renderMenu}
    itemPredicate={filterFilm}
    itemRenderer={renderFilm}
    items={...}
    onItemSelect={...}
/>
Jordan Enev
  • 16,904
  • 3
  • 42
  • 67
  • Thanks for the suggestion. Unfortunately, `itemListRenderer` only controls the area below the search/filter input, and I want to put a header at the top of the popover _above_ the input. Ideally there's be a `popoverRenderer` or a `filterInputRenderer` or something, but I don't see anything like that. – Dave Feldman Sep 04 '18 at 15:51
  • 1
    You're welcome! Looking into the [Select source code](https://github.com/palantir/blueprint/blob/27e9aff0ef4aaf437e1dee34e14bdc12f70c6422/packages/select/src/components/select/select.tsx#L147), there's no official API way to render something before the filterable input. It's a workaround to pass `filterable=false`, that will hide the filterable input. Therefore you can render your header component at the top of the menu and later implement and render a custom filtration below the header. [This is already mentioned in the docs](https://blueprintjs.com/docs/#select/select-component.querying). – Jordan Enev Sep 05 '18 at 09:24
  • Despite it's a custom filtration, it will reuse most of the Blueprint Select API. Please check [Querying](https://blueprintjs.com/docs/#select/select-component.querying) and [Controlled usage](https://blueprintjs.com/docs/#select/select-component.controlled-usage) documentation sections. – Jordan Enev Sep 05 '18 at 09:27
  • Thanks! Your suggestions and a bit of experimentation ultimately yielded a workable answer (posted below.) – Dave Feldman Sep 05 '18 at 13:53
  • Glad I helped and supported you :) – Jordan Enev Sep 05 '18 at 13:55
1

Jordan's suggestions above, plus a little experimentation, ultimately yielded a workable answer:

  1. Set filterable to false to hide the built-in filter input.
  2. Use itemListRenderer to render not only the dropdown items, but also an InputGroup to serve as a replacement filter.
  3. Use InputGroup's inputRef prop to capture a ref to the underlying HTML input. Use that to focus the input when it appears, via the onOpening property of Select's popoverProps prop.

Here's a simple component implementing the above:

// Extends Blueprint's Select component with header and footer props that
// can be any arbitrary elements or components
class ExtendedSelect extends Component {
  constructor(props) {
    super(props);
    this.inputRef = null;
    this.state = {query: ""};
  }

  handleInputChanged = event => {
    this.setState({query: event.target.value});
  }

  receiveInputRef = (ref) => {
    this.inputRef = ref;
  }

  handlePopoverOpening = () => {
    if (this.inputRef) {
      this.inputRef.focus();
    }
  }

  listRenderer = ({filteredItems, renderItem}) => {
    // Apply the supplied item renderer to the filtered list of items
    const renderedItems = filteredItems.map(renderItem);

    return (
      <div>
        {this.props.header}
        <InputGroup inputRef={this.receiveInputRef} value={this.state.query} onChange={this.handleInputChanged} leftIcon="search" />
        <Menu>
          {renderedItems}
        </Menu>
        {this.props.footer}
      </div>
    );
  }

  render() {
    return (
        <Select
          items={this.props.items}
          filterable={false}
          query={this.state.query}
          itemListRenderer={this.listRenderer}
          itemPredicate={this.props.itemPredicate}
          itemRenderer={this.props.itemRenderer}
          popoverProps={{onOpening:this.handlePopoverOpening}}
          onItemSelect={this.props.onItemSelect}
          >

          {this.props.children}
        </Select>
    );
  }
}

(Note that I'm only passing some of Select's props into the custom component—I suspect I'd know a way to pass them all were I a more experienced React developer.)

This works surprisingly well—e.g., other than a little work to focus the input when it appears, all of Select's other built-in behavior works as expected, like keyboard navigation of the menu while the input is focused.

Dave Feldman
  • 104
  • 1
  • 9
  • 28
  • 1
    Great! That's exactly [what I suggested](https://stackoverflow.com/questions/52130239/add-elements-to-blueprint-select-component-popover#comment91313174_52161931). Also if you want to pass all the `this.props` at once, you can do it using the spread operator: ` – Jordan Enev Sep 05 '18 at 14:15