Dropdown.jsx 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  1. /*
  2. Copyright (C) 2017 Cloudbase Solutions SRL
  3. This program is free software: you can redistribute it and/or modify
  4. it under the terms of the GNU Affero General Public License as
  5. published by the Free Software Foundation, either version 3 of the
  6. License, or (at your option) any later version.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. GNU Affero General Public License for more details.
  11. You should have received a copy of the GNU Affero General Public License
  12. along with this program. If not, see <http://www.gnu.org/licenses/>.
  13. */
  14. import React from 'react'
  15. import PropTypes from 'prop-types'
  16. import styled from 'styled-components'
  17. import ReactDOM from 'react-dom'
  18. import offset from 'document-offset'
  19. import { DropdownButton } from 'components'
  20. import Palette from '../../styleUtils/Palette'
  21. import StyleProps from '../../styleUtils/StyleProps'
  22. const Wrapper = styled.div`
  23. position: relative;
  24. `
  25. const getWidth = props => {
  26. if (props.large) {
  27. return StyleProps.inputSizes.large.width - 2
  28. }
  29. if (props.width) {
  30. return props.width - 2
  31. }
  32. return StyleProps.inputSizes.regular.width - 2
  33. }
  34. const List = styled.div`
  35. position: absolute;
  36. background: white;
  37. cursor: pointer;
  38. width: ${props => getWidth(props)}px;
  39. border: 1px solid ${Palette.grayscale[3]};
  40. border-radius: ${StyleProps.borderRadius};
  41. z-index: 1000;
  42. `
  43. const ListItems = styled.div`
  44. max-height: 400px;
  45. overflow: auto;
  46. `
  47. const Tip = styled.div`
  48. position: absolute;
  49. width: 10px;
  50. height: 10px;
  51. background: ${props => props.primary ? Palette.primary : 'white'};
  52. border-top: 1px solid ${Palette.grayscale[3]};
  53. border-left: 1px solid ${Palette.grayscale[3]};
  54. border-bottom: 1px solid ${props => props.primary ? Palette.primary : 'white'};
  55. border-right: 1px solid ${props => props.primary ? Palette.primary : 'white'};
  56. transform: rotate(45deg);
  57. right: 8px;
  58. top: -6px;
  59. z-index: 11;
  60. transition: all ${StyleProps.animations.swift};
  61. `
  62. const ListItem = styled.div`
  63. position: relative;
  64. color: ${Palette.grayscale[4]};
  65. padding: 8px 16px;
  66. transition: all ${StyleProps.animations.swift};
  67. ${props => props.selected ? `font-weight: ${StyleProps.fontWeights.medium};` : ''}
  68. &:first-child {
  69. border-top-left-radius: ${StyleProps.borderRadius};
  70. border-top-right-radius: ${StyleProps.borderRadius};
  71. }
  72. &:last-child {
  73. border-bottom-left-radius: ${StyleProps.borderRadius};
  74. border-bottom-right-radius: ${StyleProps.borderRadius};
  75. }
  76. &:hover {
  77. background: ${Palette.primary};
  78. color: white;
  79. }
  80. `
  81. class Dropdown extends React.Component {
  82. static propTypes = {
  83. selectedItem: PropTypes.any,
  84. items: PropTypes.array,
  85. labelField: PropTypes.string,
  86. className: PropTypes.string,
  87. onChange: PropTypes.func,
  88. noItemsMessage: PropTypes.string,
  89. noSelectionMessage: PropTypes.string,
  90. disabled: PropTypes.bool,
  91. width: PropTypes.number,
  92. }
  93. static defaultProps = {
  94. noSelectionMessage: 'Select an item',
  95. }
  96. constructor() {
  97. super()
  98. this.state = {
  99. showDropdownList: false,
  100. }
  101. this.handlePageClick = this.handlePageClick.bind(this)
  102. }
  103. componentDidMount() {
  104. window.addEventListener('mousedown', this.handlePageClick, false)
  105. }
  106. componentDidUpdate() {
  107. this.updateListPosition()
  108. }
  109. componentWillUnmount() {
  110. window.removeEventListener('mousedown', this.handlePageClick, false)
  111. }
  112. getLabel(item) {
  113. let labelField = this.props.labelField || 'label'
  114. if (item === null || item === undefined) {
  115. return this.props.noSelectionMessage
  116. }
  117. return (item[labelField] !== null && item[labelField] !== undefined && item[labelField].toString()) || item.toString()
  118. }
  119. updateListPosition() {
  120. if (!this.state.showDropdownList || !this.listRef || !this.buttonRef) {
  121. return
  122. }
  123. let buttonHeight = this.buttonRef.offsetHeight
  124. let tipHeight = 8
  125. let buttonOffset = offset(this.buttonRef)
  126. let listTop = buttonOffset.top + buttonHeight + tipHeight
  127. let listHeight = this.listRef.offsetHeight
  128. if (listTop + listHeight > window.innerHeight) {
  129. listTop = window.innerHeight - listHeight - 10
  130. this.tipRef.style.display = 'none'
  131. } else {
  132. this.tipRef.style.display = 'block'
  133. }
  134. this.listRef.style.top = `${listTop}px`
  135. this.listRef.style.left = `${buttonOffset.left}px`
  136. }
  137. handlePageClick() {
  138. if (!this.itemMouseDown) {
  139. this.setState({ showDropdownList: false })
  140. }
  141. }
  142. handleButtonClick() {
  143. if (this.props.disabled) {
  144. return
  145. }
  146. this.setState({ showDropdownList: !this.state.showDropdownList })
  147. }
  148. handleItemClick(item) {
  149. this.setState({ showDropdownList: false, firstItemHover: false })
  150. if (this.props.onChange) {
  151. this.props.onChange(item)
  152. }
  153. }
  154. handleItemMouseEnter(index) {
  155. if (index === 0) {
  156. this.setState({ firstItemHover: true })
  157. }
  158. }
  159. handleItemMouseLeave(index) {
  160. if (index === 0) {
  161. this.setState({ firstItemHover: false })
  162. }
  163. }
  164. renderList() {
  165. if (!this.props.items || this.props.items.length === 0 || !this.state.showDropdownList) {
  166. return null
  167. }
  168. let selectedLabel = this.getLabel(this.props.selectedItem)
  169. let list = ReactDOM.createPortal((
  170. <List {...this.props} innerRef={ref => { this.listRef = ref }}>
  171. <Tip innerRef={ref => { this.tipRef = ref }} primary={this.state.firstItemHover} />
  172. <ListItems>
  173. {this.props.items.map((item, i) => {
  174. let label = this.getLabel(item)
  175. let listItem = (
  176. <ListItem
  177. key={label}
  178. onMouseDown={() => { this.itemMouseDown = true }}
  179. onMouseUp={() => { this.itemMouseDown = false }}
  180. onMouseEnter={() => { this.handleItemMouseEnter(i) }}
  181. onMouseLeave={() => { this.handleItemMouseLeave(i) }}
  182. onClick={() => { this.handleItemClick(item) }}
  183. selected={label === selectedLabel}
  184. >{label}
  185. </ListItem>
  186. )
  187. return listItem
  188. })}
  189. </ListItems>
  190. </List>
  191. ), document.body)
  192. return list
  193. }
  194. render() {
  195. let buttonValue = () => {
  196. if (this.props.items && this.props.items.length) {
  197. return this.getLabel(this.props.selectedItem)
  198. }
  199. return this.props.noItemsMessage || ''
  200. }
  201. return (
  202. <Wrapper
  203. className={this.props.className}
  204. onMouseDown={() => { this.itemMouseDown = true }}
  205. onMouseUp={() => { this.itemMouseDown = false }}
  206. >
  207. <DropdownButton
  208. {...this.props}
  209. innerRef={ref => { this.buttonRef = ref }}
  210. onMouseDown={() => { this.itemMouseDown = true }}
  211. onMouseUp={() => { this.itemMouseDown = false }}
  212. value={buttonValue()}
  213. onClick={() => this.handleButtonClick()}
  214. />
  215. {this.renderList()}
  216. </Wrapper>
  217. )
  218. }
  219. }
  220. export default Dropdown