Skip to content

Instantly share code, notes, and snippets.

@souporserious
Created September 19, 2017 16:48
Show Gist options
  • Save souporserious/8ab46fd0b68127002ee2c2c859758ad4 to your computer and use it in GitHub Desktop.
Save souporserious/8ab46fd0b68127002ee2c2c859758ad4 to your computer and use it in GitHub Desktop.
Loops through children to provide selected states
function firstDefined(...args) {
return args.filter(a => typeof a !== 'undefined')[0]
}
function shallowEqual(a, b) {
return JSON.stringify(a) === JSON.stringify(b)
}
class Item extends PureComponent {
static displayName = 'ItemList.Item'
render() {
const {
children,
clearHighlightedItem,
deselect,
highlight,
index,
isHighlighted,
isSelected,
multiple,
select,
value,
...restProps
} = this.props
if (typeof children === 'function') {
return children({
clearHighlightedItem,
deselect,
highlight,
index,
isHighlighted,
isSelected,
multiple,
select,
itemProps: restProps,
})
}
return (
<Menu.Item
{...restProps}
onClick={multiple ? (isSelected ? deselect : select) : select}
onMouseOver={highlight}
onMouseOut={clearHighlightedItem}
backgroundColor={isHighlighted && 'grey-2'}
>
{isSelected && <Icon name="checkmark" />}
{children}
</Menu.Item>
)
}
}
let itemListId = 0
class ItemGroup extends Component {
static displayName = 'ItemGroup'
static defaultProps = {
channel: 'item-list',
component: Menu,
defaultHighlightedIndex: null,
defaultValue: [],
multiple: false,
value: null,
onChange: () => null,
}
state = {
value: this.props.value || this.props.defaultValue,
highlightedIndex: this.props.defaultHighlightedIndex,
}
id = `${this.props.channel}-${itemListId++}`
lastSelectedValue = null
componentWillMount() {
const { defaultHighlightedIndex, defaultValue } = this.props
if (defaultValue !== undefined && defaultHighlightedIndex === null) {
this.highlightItem(this.getItemIndex(defaultValue))
}
}
componentWillReceiveProps(nextProps) {
if (!shallowEqual(this.props.value, nextProps.value)) {
this.setState({ value: nextProps.value })
}
}
isUncontrolled() {
return this.props.value === null
}
isItemHighlighted = index => {
return this.state.highlightedIndex === index
}
isItemSelected = value => {
return Array.isArray(this.state.value)
? this.state.value.some(selectedValue =>
shallowEqual(selectedValue, value)
)
: shallowEqual(this.state.value, value)
}
getItemIndex = value => {
let itemIndex = -1
this.getItems().some((item, index) => {
if (shallowEqual(item.props.value, value)) {
itemIndex = index
return true
}
})
return itemIndex
}
getSelectedItemIndex = value => {
const values = Array.isArray(this.state.value)
? this.state.value
: [this.state.value]
let itemIndex = -1
values.some((_value, index) => {
if (shallowEqual(_value, value)) {
itemIndex = index
return true
}
})
return itemIndex
}
getChildren(children = this.props.children) {
return typeof children === 'function'
? children(this.getControllerStateAndMethods()).props.children
: children
}
getItems = (children = this.getChildren()) => {
return Children.toArray(children).reduce((items, child) => {
if (child === undefined || child === null) {
return items
}
if (child.type && child.type.displayName.indexOf('Group') > -1) {
return [
...items,
...this.getItems(this.getChildren(child.props.children)),
]
} else if (child.type && child.type.displayName.indexOf('Item') > -1) {
return [...items, child]
} else {
return items
}
}, [])
}
highlightItem = index => {
this.setState({ highlightedIndex: index })
}
moveHighlightedItem = (amount = 0, contain = true) => {
const items = this.getItems()
let nextIndex = null
if (this.state.highlightedIndex === null) {
nextIndex = amount >= 0 ? amount - 1 : items.length + amount
} else {
items.forEach((item, index) => {
if (this.state.highlightedIndex === index) {
nextIndex = index + amount
}
})
}
if (nextIndex >= items.length) {
nextIndex = contain ? 0 : null
} else if (nextIndex < 0) {
nextIndex = contain ? items.length - 1 : null
}
this.setState({ highlightedIndex: nextIndex })
}
clearHighlightedItem = () => {
this.setState({ highlightedIndex: null })
}
selectHighlightedItem = (toggle = this.props.multiple) => {
const items = this.getItems()
const item = items[this.state.highlightedIndex]
if (item) {
const value = firstDefined(item.props.value, this.state.highlightedIndex)
if (toggle && this.isItemSelected(value)) {
this.deselectItem(value)
} else {
this.selectItem(value)
}
}
}
selectItem = itemValue => {
let value = itemValue
if (this.props.multiple) {
value = Array.isArray(this.state.value) ? [...this.state.value] : []
const position = this.getSelectedItemIndex(itemValue)
if (position > -1) {
value.splice(position, 1)
}
value.push(itemValue)
}
if (this.isUncontrolled()) {
this.setState({ value })
}
this.lastSelectedValue = itemValue
this.props.onChange({ value, selected: true })
}
deselectItem = itemValue => {
let value = ''
if (this.props.multiple) {
value = Array.isArray(this.state.value) ? [...this.state.value] : []
const itemIndex = this.getSelectedItemIndex(itemValue)
if (itemIndex > -1) {
value.splice(itemIndex, 1)
}
}
if (this.isUncontrolled()) {
this.setState({ value })
}
if (this.lastSelectedValue === itemValue) {
this.lastSelectedValue = null
}
this.props.onChange({ value, selected: false })
}
moveSelectedItem = (amount = 0, deselectLastSelectedItem = true) => {
const items = this.getItems()
let nextIndex = -1
let nextItem = null
if (this.lastSelectedValue === null) {
nextIndex = amount >= 0 ? amount - 1 : items.length + amount
nextItem = items[nextIndex]
} else {
items.some((item, index) => {
if (this.lastSelectedValue === firstDefined(item.props.value, index)) {
nextIndex = index + amount
nextItem = items[nextIndex]
return true
}
})
}
if (this.lastSelectedValue && deselectLastSelectedItem) {
this.deselectItem(this.lastSelectedValue)
}
if (nextItem) {
this.selectItem(firstDefined(nextItem.props.value, nextIndex))
}
}
deselectAllItems = () => {
const value = this.props.multiple ? [] : ''
if (this.isUncontrolled()) {
this.setState({ value })
}
this.props.onChange({
value,
selected: false,
})
}
selectAllItems = () => {
const items = this.getItems()
const values = items.map((item, index) =>
firstDefined(item.props.value, index)
)
const value = this.props.multiple ? values : values[0]
if (this.isUncontrolled()) {
this.setState({ value })
}
this.props.onChange({
value,
selected: true,
})
}
handleItemSelect = value => () => {
this.selectItem(value)
}
handleItemDeselect = value => () => {
this.deselectItem(value)
}
handleItemHighlight = index => () => {
this.highlightItem(index)
}
handleItemGroupChange = child => e => {
if (this.isUncontrolled()) {
this.setState(({ value: previousValue }) => {
const value = Array.isArray(previousValue)
? [...previousValue]
: [previousValue]
if (Array.isArray(e.value)) {
e.value.forEach(v => {
const position = value.indexOf(v)
if (position > -1) {
value.splice(position, 1)
}
value.push(v)
})
} else {
const position = value.indexOf(e.value)
if (position > -1) {
value.splice(position, 1)
}
value.push(e.value)
}
const changeEvent = {
value,
selected: true,
}
this.props.onChange(changeEvent)
child.props.onChange(changeEvent)
return { value }
})
}
}
getControllerStateAndMethods = () => {
return this.state
}
render() {
const {
channel,
children,
component,
defaultHighlightedIndex,
defaultValue,
multiple,
value,
...restProps
} = this.props
let itemIndex = 0
const cloneItems = children => {
const items = []
Children.toArray(children).forEach(child => {
let newChild = child
if (child === undefined || child === null) {
return
}
if (child.type) {
if (child.type && child.type.displayName.indexOf('Group') > -1) {
newChild = cloneElement(child, {
multiple,
value: this.state.value,
onChange: this.handleItemGroupChange(child),
children: cloneItems(child.props.children, itemIndex),
})
} else if (
child.type.displayName.indexOf('Item') > -1 &&
child.props.index === undefined
) {
const index = itemIndex++
const value = firstDefined(child.props.value, index)
newChild = cloneElement(child, {
index,
multiple,
id: `${this.id}-item-${index}`,
clearHighlightedItem: this.clearHighlightedItem,
deselect: this.handleItemDeselect(value),
highlight: this.handleItemHighlight(index),
isHighlighted: this.isItemHighlighted(index),
isSelected: this.isItemSelected(value),
select: this.handleItemSelect(value),
})
}
}
if (newChild !== undefined && newChild !== null) {
items.push(newChild)
}
})
return items
}
if (typeof children === 'function') {
const wrapper = children(this.getControllerStateAndMethods())
return cloneElement(
wrapper,
restProps,
cloneItems(wrapper.props.children)
)
} else {
return createElement(component, restProps, cloneItems(children))
}
}
}
@souporserious
Copy link
Author

souporserious commented Sep 19, 2017

usage:

class MySelect extends Component {
  static defaultProps = {
    emptyValue: 'Select an option',
    getLabel: v => v,
    renderValue: v => v,
  }

  handleInputKeyDown = e => {
    if (e.key === 'ArrowUp') {
      e.preventDefault()
      this.itemList.moveHighlightedItem(-1)
    }

    if (e.key === 'ArrowDown') {
      e.preventDefault()
      this.itemList.moveHighlightedItem(1)
    }

    if (e.key === 'Enter') {
      e.preventDefault()
      this.itemList.selectHighlightedItem()
    }

    if (e.key === ' ') {
      e.preventDefault()
      this.itemList.selectHighlightedItem()
    }
  }

  render() {
    const {
      defaultValue,
      emptyValue,
      getLabel,
      multiple,
      options,
      renderValue,
      onChange,
      value,
    } = this.props
    return (
      <ItemGroup
        ref={c => (this.itemList = c)}
        defaultValue={defaultValue}
        value={value}
        multiple={multiple}
        onChange={onChange}
      >
        {controller => {
          const selectedValue = multiple
            ? controller.value.map(v => renderValue(v))
            : renderValue(controller.value)
          return (
            <Box padding={1}>
              <Input.Faux
                display="flex"
                tabIndex="0"
                cursor="pointer"
                userSelect="none"
                onKeyDown={this.handleInputKeyDown}
              >
                {selectedValue.length > 0 ? selectedValue : emptyValue}
              </Input.Faux>
              {options.map((option, index) => (
                <Item
                  key={index}
                  value={option}
                  multiple={multiple}
                  children={getLabel(option)}
                />
              ))}
            </Box>
          )
        }}
      </ItemGroup>
    )
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment