segmentio / evergreen

🌲 Evergreen React UI Framework by Segment
https://evergreen.segment.com
MIT License
12.39k stars 832 forks source link

SelectMenu outline #60

Closed jeroenransijn closed 6 years ago

jeroenransijn commented 7 years ago

A DropdownMenu component that can be triggered by buttons and select like interfaces.

Remaining questions

Design Example

dropdown-menu-example

Current implementation

I have already build a working implementation of this thing, but there is still some API tweaks I would like to make — and some implementation details to consider.

Take a look at the current code and implementation ```jsx import React, { PureComponent } from 'react' import PropTypes from 'prop-types' import fuzzaldrin from 'fuzzaldrin-plus' import { Pane } from 'evergreen-layers' import VirtualList from 'react-tiny-virtual-list' import { TableRow, TextTableCell, SearchTableHeaderCell } from 'evergreen-table' const fuzzyFilter = (items, input) => fuzzaldrin.filter(items, input) const CheckIcon = ({ fill = 'currentColor', ...props }) => ( ) CheckIcon.propTypes = { fill: PropTypes.string, } const itemRenderer = ({ key, name, onSelect, isSelected, style, height }) => ( {name} ) itemRenderer.propTypes = { key: PropTypes.oneOf([PropTypes.string, PropTypes.number]), name: PropTypes.string, style: PropTypes.any, height: PropTypes.number, onSelect: PropTypes.func, isSelected: PropTypes.bool, } const ItemShapePropType = PropTypes.shape({ name: PropTypes.string, value: PropTypes.oneOf([PropTypes.string, PropTypes.number]), }) export default class SelectableList extends PureComponent { static propTypes = { items: PropTypes.arrayOf(ItemShapePropType), close: PropTypes.func, selected: PropTypes.arrayOf(ItemShapePropType), onSelect: PropTypes.func, itemSize: PropTypes.number, listHeight: PropTypes.number, renderItem: PropTypes.func, placeholder: PropTypes.string, itemsFilter: PropTypes.func, defaultSearchValue: PropTypes.string, } static defaultProps = { items: [], // Including border bottom // For some reason passing height to TableRow doesn't work itemSize: 33, onSelect: () => {}, selected: [], listHeight: 208, renderItem: itemRenderer, itemsFilter: fuzzyFilter, placeholder: 'Filter...', defaultSearchValue: '', } constructor(props, context) { super(props, context) this.state = { searchValue: props.defaultSearchValue, selected: props.selected, } } componentDidMount() { window.setTimeout(() => { this.searchRef.querySelector('input').focus() }, 1) } componentWillReceiveProps(nextProps) { if (nextProps.selected !== this.state.selected) { this.setState({ selected: nextProps.selected, }) } } isSelected = item => { const { selected } = this.state return !!selected.find(selectedItem => selectedItem.value === item.value) } search = items => { const { itemsFilter } = this.props const { searchValue } = this.state return searchValue.trim() === '' ? items : itemsFilter(items.map(item => item.name), searchValue).map(name => items.find(n => n.name === name), ) } handleChange = searchValue => { this.setState({ searchValue, }) } handleSelect = item => { this.props.onSelect(item) } render() { const { items: originalItems, close, onSelect, itemSize, selected, listHeight, renderItem, itemsFilter, placeholder, defaultSearchValue, ...props } = this.props const items = this.search(originalItems) return ( (this.searchRef = ref)} borderRight={null} height={32} /> { const item = items[index] return renderItem({ key: item.value, name: item.name, style, height: itemSize, onSelect: () => this.handleSelect(item), isSelected: this.isSelected(item), }) }} /> ) } } ```

Usage example

<MenuPopover
  title={title}
  items={items}
  onSelect={this.handleSelect}
  selected={this.state.selected}
>
  <StatelessInlineMenuButton {...statelessProps}>
    {name}
  </StatelessInlineMenuButton>
</MenuPopover>

Items API

In some cases you want the label to be different in the list as in the button. For example if you are dealing with =, you want = (equals) in the menu list. I am suggesting the following API, which is different from what's in the current implementation:

const items = [
  {
    label: '=',
    labelInList: '= (equals)', // optional
    value: 'equals'
  },
  {
    label: '!=',
    labelInList: '!= (not equals)', // optional
    value: 'not_equals'
  }
]
ItemShapePropType ```jsx const ItemShapePropType = PropTypes.shape({ label: PropTypes.string, labelInList: PropTypes.string, value: PropTypes.oneOf([PropTypes.string, PropTypes.number]), }) ```

Naming considerations

Some of the options I could think about for naming this thing are:

Option 1 — Menu

Option 2 — OptionList

Option 3 — SelectableList

Option 4 — SelectMenu

I am not sure about naming yet, the term Menu might be better used in a more traditional dropdown menu like this:

screen shot 2017-11-30 at 1 00 16 pm

How is this different from Autocomplete and Combobox?

Combobox is a little bulky in some cases, it does add more affordability, so i think there will still be use cases for it. We'll want to make the look and feel consistent of the actual list of both this component and the Autocomplete.

Rowno commented 6 years ago

I vote for SelectMenu. Also for using Downshift for the accessibility.

Also what's the difference between evergreen-select-menu and evergreen-select-menu-popover?

jeroenransijn commented 6 years ago

@Rowno one would be inside of a Popover, so you can simply wrap it around a Button. It's for convenience. Otherwise engineers will need to do some plumbing every time they want to use this, instead of simply passing in items and a onSelect. What do you think?

Rowno commented 6 years ago

Would you ever use evergreen-select-menu though?

jeroenransijn commented 6 years ago

@Rowno probably not, it's probably better to export multiple components from the same package instead:

import SelectMenu, { SelectMenuStandalone } from 'evergreen-select-menu'