Dropdown.tsx 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699
  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 * as React from 'react'
  15. import { observer } from 'mobx-react'
  16. import styled, { css } from 'styled-components'
  17. import ReactDOM from 'react-dom'
  18. import autobind from 'autobind-decorator'
  19. import DropdownButton from '../../atoms/DropdownButton'
  20. import Palette from '../../styleUtils/Palette'
  21. import DomUtils from '../../../utils/DomUtils'
  22. import StyleProps from '../../styleUtils/StyleProps'
  23. import checkmarkImage from './images/checkmark'
  24. import tipImage from './images/tip'
  25. import requiredImage from './images/required.svg'
  26. const getWidth = (props: any) => {
  27. if (props.width) {
  28. return props.width - 2
  29. }
  30. return StyleProps.inputSizes.regular.width - 2
  31. }
  32. const Wrapper = styled.div<any>`
  33. position: relative;
  34. ${(props: any) => (props.embedded ? 'width: 100%;' : '')}
  35. &:focus {
  36. outline: none;
  37. }
  38. `
  39. const Required = styled.div<any>`
  40. position: absolute;
  41. width: 8px;
  42. height: 8px;
  43. right: ${(props: any) => props.right}px;
  44. top: 12px;
  45. background: url('${requiredImage}') center no-repeat;
  46. ${(props: any) => (props.disabledLoading ? StyleProps.animations.disabledLoading : '')}
  47. `
  48. const List = styled.div<any>`
  49. position: absolute;
  50. background: white;
  51. cursor: pointer;
  52. width: ${(props: any) => getWidth(props)}px;
  53. border: 1px solid ${Palette.grayscale[3]};
  54. border-radius: ${StyleProps.borderRadius};
  55. z-index: 1000;
  56. ${StyleProps.boxShadow}
  57. `
  58. const ListItems = styled.div<any>`
  59. max-height: 400px;
  60. overflow: auto;
  61. `
  62. export const Tip = styled.div<any>`
  63. position: absolute;
  64. width: 16px;
  65. height: 8px;
  66. right: 8px;
  67. top: -8px;
  68. z-index: 11;
  69. transition: all ${StyleProps.animations.swift};
  70. overflow: hidden;
  71. svg {
  72. #path {
  73. transition: all ${StyleProps.animations.swift};
  74. fill: ${props => (props.primary ? Palette.primary : 'white')};
  75. }
  76. }
  77. `
  78. const Checkmark = styled.div<any>`
  79. ${StyleProps.exactWidth('16px')}
  80. height: 16px;
  81. margin-right: 8px;
  82. margin-top: 1px;
  83. display: flex;
  84. justify-content: center;
  85. align-items: center;
  86. #symbol {
  87. transition: stroke ${StyleProps.animations.swift};
  88. stroke-dasharray: 12;
  89. stroke-dashoffset: ${(props: any) => (props.show ? 24 : 12)};
  90. animation-duration: 100ms;
  91. animation-timing-function: ease-in-out;
  92. animation-fill-mode: forwards;
  93. @keyframes dashOn {
  94. from { stroke-dashoffset: 12; }
  95. to { stroke-dashoffset: 24; }
  96. }
  97. @keyframes dashOff {
  98. from { stroke-dashoffset: 24; }
  99. to { stroke-dashoffset: 12; }
  100. }
  101. }
  102. `
  103. const getListItemColor = (props: any) => {
  104. if (props.disabled) {
  105. return Palette.grayscale[3]
  106. }
  107. if (props.multipleSelected) {
  108. return Palette.primary
  109. }
  110. if (props.selected) {
  111. return 'white'
  112. }
  113. if (props.dim) {
  114. return Palette.grayscale[3]
  115. }
  116. return Palette.grayscale[4]
  117. }
  118. const getListBackgroundColor = (props: any) => {
  119. if (props.arrowSelected) {
  120. return css`background: ${Palette.primary}44;`
  121. }
  122. if (props.selected) {
  123. return css`background: ${Palette.primary};`
  124. }
  125. return ''
  126. }
  127. const ListItem = styled.div<any>`
  128. position: relative;
  129. display: flex;
  130. color: ${(props: any) => getListItemColor(props)};
  131. ${(props: any) => getListBackgroundColor(props)}
  132. ${(props: any) => (props.selected ? css`font-weight: ${StyleProps.fontWeights.medium};` : '')}
  133. padding: 8px 16px;
  134. transition: all ${StyleProps.animations.swift};
  135. padding-left: ${(props: any) => props.paddingLeft}px;
  136. word-break: break-word;
  137. ${props => (props.disabled ? css`cursor: default;` : '')}
  138. &:first-child {
  139. border-top-left-radius: ${StyleProps.borderRadius};
  140. border-top-right-radius: ${StyleProps.borderRadius};
  141. }
  142. &:last-child {
  143. border-bottom-left-radius: ${StyleProps.borderRadius};
  144. border-bottom-right-radius: ${StyleProps.borderRadius};
  145. }
  146. &:hover {
  147. background: ${Palette.primary};
  148. color: white;
  149. ${Checkmark} #symbol {
  150. stroke: white;
  151. }
  152. }
  153. `
  154. const SubtitleLabel = styled.div`
  155. display: flex;
  156. font-size: 11px;
  157. `
  158. const DuplicatedLabel = styled.div`
  159. display: flex;
  160. font-size: 11px;
  161. span {
  162. text-overflow: ellipsis;
  163. white-space: nowrap;
  164. overflow: hidden;
  165. }
  166. `
  167. const Separator = styled.div<any>`
  168. width: calc(100% - 32px);
  169. height: 1px;
  170. margin: 8px 16px;
  171. background: ${Palette.grayscale[3]};
  172. `
  173. const Labels = styled.div<any>`
  174. word-break: break-word;
  175. max-width: 100%;
  176. `
  177. export const updateTipStyle = (
  178. listItemsRef: HTMLElement,
  179. tipRef: HTMLElement,
  180. firstItemRef: HTMLElement,
  181. ) => {
  182. const usableFirstItemRef = firstItemRef
  183. if (listItemsRef && tipRef && usableFirstItemRef) {
  184. const svgPath: SVGPathElement | null = tipRef.querySelector('#path')
  185. if (svgPath) {
  186. if (listItemsRef.clientHeight < listItemsRef.scrollHeight) {
  187. svgPath.style.fill = 'white'
  188. usableFirstItemRef.style.borderTopRightRadius = '0'
  189. } else {
  190. svgPath.style.fill = ''
  191. usableFirstItemRef.style.borderTopRightRadius = ''
  192. }
  193. }
  194. }
  195. }
  196. export const scrollItemIntoView = (
  197. listRef: HTMLElement,
  198. listItemsRef: HTMLElement,
  199. itemIndex: number,
  200. ) => {
  201. const usableListItemsRef = listItemsRef
  202. if (!listRef || !usableListItemsRef) {
  203. return
  204. }
  205. if (itemIndex === -1 || !listItemsRef.children[itemIndex]) {
  206. return
  207. }
  208. const child = usableListItemsRef.children[itemIndex] as HTMLElement | null
  209. const parentNode = child && (child.parentNode as HTMLElement | null)
  210. if (!parentNode || !child) {
  211. return
  212. }
  213. parentNode.scrollTop = child.offsetTop - parentNode.offsetTop - 32
  214. }
  215. export const handleKeyNavigation = (options: {
  216. submitKeys: string[],
  217. keyboardEvent: React.KeyboardEvent<HTMLInputElement | HTMLDivElement>,
  218. arrowSelection: number | null,
  219. items: any[],
  220. selectedItem: any,
  221. onSubmit: (item: any) => void,
  222. onGetValue: (item: any) => any,
  223. onSelection: (arrowSelection: number) => void,
  224. }) => {
  225. const {
  226. submitKeys, keyboardEvent, arrowSelection,
  227. items, onSubmit, onGetValue, selectedItem, onSelection,
  228. } = options
  229. if (submitKeys.find(k => k === keyboardEvent.key)) {
  230. keyboardEvent.preventDefault()
  231. if (arrowSelection == null) {
  232. return
  233. }
  234. window.handlingEnterKey = true // Needed for KeyboardManager conflict resolution
  235. const arrowSelectedItem = items[arrowSelection]
  236. if (arrowSelectedItem) {
  237. onSubmit(arrowSelectedItem)
  238. }
  239. setTimeout(() => { window.handlingEnterKey = false }, 100)
  240. return
  241. }
  242. if (keyboardEvent.key !== 'ArrowUp' && keyboardEvent.key !== 'ArrowDown') {
  243. return
  244. }
  245. keyboardEvent.preventDefault()
  246. const itemIndex = items.findIndex(i => onGetValue(i) === onGetValue(selectedItem))
  247. const currentIndex = arrowSelection == null ? itemIndex : arrowSelection
  248. const maxIndex = items.length - 1
  249. if (keyboardEvent.key === 'ArrowUp') {
  250. onSelection(currentIndex === 0 ? maxIndex : currentIndex - 1)
  251. }
  252. if (keyboardEvent.key === 'ArrowDown') {
  253. onSelection(currentIndex === maxIndex ? 0 : currentIndex + 1)
  254. }
  255. }
  256. type Props = {
  257. selectedItem?: any,
  258. items: any[],
  259. labelField?: string,
  260. valueField?: string,
  261. className?: string,
  262. onChange?: (item: any) => void,
  263. noItemsMessage?: string,
  264. noSelectionMessage?: string,
  265. disabled?: boolean,
  266. disabledLoading?: boolean,
  267. width?: number,
  268. 'data-test-id'?: string,
  269. embedded?: boolean,
  270. dimFirstItem?: boolean,
  271. multipleSelection?: boolean,
  272. selectedItems?: any[] | null,
  273. highlight?: boolean,
  274. required?: boolean,
  275. centered?: boolean,
  276. useBold?: boolean,
  277. primary?: boolean
  278. }
  279. type State = {
  280. showDropdownList: boolean,
  281. firstItemHover: boolean,
  282. arrowSelection: number | null,
  283. }
  284. @observer
  285. class Dropdown extends React.Component<Props, State> {
  286. static defaultProps = {
  287. noSelectionMessage: 'Select an item',
  288. }
  289. state: State = {
  290. showDropdownList: false,
  291. firstItemHover: false,
  292. arrowSelection: null,
  293. }
  294. buttonRef: HTMLElement | null | undefined
  295. listRef: HTMLElement | null | undefined
  296. listItemsRef: HTMLElement | null | undefined
  297. firstItemRef: HTMLElement | null | undefined
  298. tipRef: HTMLElement | null | undefined
  299. scrollableParent: HTMLElement | null | undefined
  300. wrapperRef: HTMLElement | null | undefined
  301. buttonRect: ClientRect | null | undefined
  302. itemMouseDown: boolean | undefined
  303. justFocused: boolean | undefined
  304. ignoreFocusHandler: boolean | undefined
  305. checkmarkRefs: { [ref: string]: HTMLElement } = {}
  306. componentDidMount() {
  307. if (this.buttonRef) {
  308. this.scrollableParent = DomUtils.getScrollableParent(this.buttonRef)
  309. this.scrollableParent.addEventListener('scroll', this.handleScroll)
  310. window.addEventListener('resize', this.handleScroll)
  311. this.buttonRect = this.buttonRef.getBoundingClientRect()
  312. }
  313. }
  314. UNSAFE_componentWillReceiveProps(newProps: Props) {
  315. if (!this.props.multipleSelection) {
  316. return
  317. }
  318. // Clear checkmark if items are removed in newProps
  319. const newSelectedItems = newProps.selectedItems || []
  320. const oldSelectedItems = this.props.selectedItems || []
  321. const hash = (item: any) => `${this.getLabel(item)}-${this.getValue(item) || ''}`
  322. const needsCheckmarkClear = oldSelectedItems.filter(
  323. oldItem => !newSelectedItems.find(newItem => hash(oldItem) === hash(newItem)),
  324. )
  325. needsCheckmarkClear.forEach(clearItem => {
  326. this.toggleCheckmarkAnimation(clearItem, this.checkmarkRefs[hash(clearItem)], true)
  327. })
  328. }
  329. UNSAFE_componentWillUpdate() {
  330. if (this.buttonRef) this.buttonRect = this.buttonRef.getBoundingClientRect()
  331. }
  332. componentDidUpdate() {
  333. this.updateListPosition()
  334. }
  335. componentWillUnmount() {
  336. window.removeEventListener('resize', this.handleScroll, false)
  337. if (this.scrollableParent) {
  338. this.scrollableParent.removeEventListener('scroll', this.handleScroll, false)
  339. }
  340. }
  341. getLabel(item: any) {
  342. const labelField = this.props.labelField || 'label'
  343. if (item == null) {
  344. return this.props.noSelectionMessage
  345. }
  346. if (item[labelField] != null) {
  347. return item[labelField].toString()
  348. }
  349. if (item.value != null) {
  350. return item.value.toString()
  351. }
  352. return item.toString()
  353. }
  354. getValue(item: any) {
  355. const valueField = this.props.valueField || 'value'
  356. if (item == null) {
  357. return null
  358. }
  359. return (item[valueField] != null && item[valueField].toString()) || this.getLabel(item)
  360. }
  361. @autobind
  362. handleScroll() {
  363. if (this.buttonRef) {
  364. if (DomUtils.isElementInViewport(this.buttonRef, this.scrollableParent)) {
  365. this.buttonRect = this.buttonRef.getBoundingClientRect()
  366. this.updateListPosition()
  367. } else if (this.state.showDropdownList) {
  368. this.setState({ showDropdownList: false })
  369. }
  370. }
  371. }
  372. toggleDropdownList(show: boolean = true) {
  373. if (this.props.disabled && show) {
  374. return
  375. }
  376. this.setState({ showDropdownList: show }, () => {
  377. this.scrollIntoView()
  378. })
  379. }
  380. handleFocus() {
  381. if (this.ignoreFocusHandler || this.props.disabled || this.props.disabledLoading) {
  382. return
  383. }
  384. this.justFocused = true
  385. this.toggleDropdownList(true)
  386. setTimeout(() => { this.justFocused = false }, 1000)
  387. }
  388. handleBlur() {
  389. if (!this.itemMouseDown) {
  390. this.setState({ showDropdownList: false })
  391. }
  392. }
  393. handleKeyPress(e: React.KeyboardEvent<HTMLDivElement>) {
  394. if (!this.state.showDropdownList) {
  395. return
  396. }
  397. handleKeyNavigation({
  398. submitKeys: ['Enter', ' '],
  399. keyboardEvent: e,
  400. arrowSelection: this.state.arrowSelection,
  401. items: this.props.items,
  402. selectedItem: this.props.selectedItem,
  403. onSubmit: item => { this.handleItemClick(item) },
  404. onGetValue: item => this.getValue(item),
  405. onSelection: arrowSelection => {
  406. this.setState({ arrowSelection }, () => {
  407. this.scrollIntoView(arrowSelection)
  408. })
  409. },
  410. })
  411. }
  412. handleButtonClick() {
  413. if (this.justFocused) {
  414. return
  415. }
  416. this.toggleDropdownList(!this.state.showDropdownList)
  417. }
  418. handleItemClick(item: any) {
  419. const resetFocus = () => {
  420. if (!this.wrapperRef) {
  421. return
  422. }
  423. this.ignoreFocusHandler = true
  424. this.wrapperRef.focus()
  425. setTimeout(() => { this.ignoreFocusHandler = false }, 100)
  426. }
  427. if (item.disabled) {
  428. resetFocus()
  429. return
  430. }
  431. if (!this.props.multipleSelection) {
  432. this.setState({ showDropdownList: false, firstItemHover: false }, () => {
  433. resetFocus()
  434. })
  435. } else {
  436. const selected = Boolean(
  437. this.props.selectedItems
  438. && this.props.selectedItems.find(i => this.getValue(i) === this.getValue(item)),
  439. )
  440. this.toggleCheckmarkAnimation(
  441. item,
  442. this.checkmarkRefs[`${this.getLabel(item)}-${this.getValue(item) || ''}`],
  443. selected,
  444. )
  445. resetFocus()
  446. }
  447. if (this.props.onChange) {
  448. this.props.onChange(item)
  449. }
  450. }
  451. handleItemMouseEnter(index: number) {
  452. if (index === 0) {
  453. this.setState({ firstItemHover: true })
  454. }
  455. }
  456. handleItemMouseLeave(index: number) {
  457. if (index === 0) {
  458. this.setState({ firstItemHover: false })
  459. }
  460. }
  461. toggleCheckmarkAnimation(item: any, checkmarkRef: HTMLElement, selected: boolean) {
  462. if (!item || !checkmarkRef) {
  463. return
  464. }
  465. const symbol = checkmarkRef.querySelector('#symbol') as HTMLElement
  466. if (symbol) {
  467. symbol.style.animationName = selected ? 'dashOff' : 'dashOn'
  468. }
  469. }
  470. updateListPosition() {
  471. if (!this.state.showDropdownList || !this.listRef || !this.buttonRef || !this.firstItemRef
  472. || !document.body || !this.buttonRect || !this.tipRef || !this.listItemsRef) {
  473. return
  474. }
  475. const buttonHeight = this.buttonRef.offsetHeight
  476. const tipHeight = 8
  477. let listTop = this.buttonRect.top + buttonHeight + tipHeight
  478. const listHeight = this.listRef.offsetHeight
  479. if (listTop + listHeight > window.innerHeight) {
  480. listTop = window.innerHeight - listHeight - 16
  481. this.tipRef.style.display = 'none'
  482. } else {
  483. this.tipRef.style.display = 'block'
  484. }
  485. // If a modal is opened, body scroll is removed and body top is set to replicate scroll position
  486. let scrollOffset = 0
  487. if (parseInt(document.body.style.top, 10) < 0) {
  488. scrollOffset = -parseInt(document.body && document.body.style.top, 10)
  489. }
  490. const widthDiff = this.listRef.offsetWidth - this.buttonRef.offsetWidth
  491. this.listRef.style.top = `${listTop + (window.pageYOffset || scrollOffset)}px`
  492. this.listRef.style.left = `${(this.buttonRect.left + window.pageXOffset) - widthDiff}px`
  493. updateTipStyle(this.listItemsRef, this.tipRef, this.firstItemRef)
  494. }
  495. scrollIntoView(itemIndex?: number) {
  496. const selectedItemIndex = this.props.items
  497. .findIndex(i => this.getValue(i) === this.getValue(this.props.selectedItem))
  498. const actualItemIndex = itemIndex != null ? itemIndex : selectedItemIndex
  499. if (this.listRef && this.listItemsRef) {
  500. scrollItemIntoView(this.listRef, this.listItemsRef, actualItemIndex)
  501. }
  502. }
  503. renderList() {
  504. if (!this.props.items || this.props.items.length === 0 || !this.state.showDropdownList) {
  505. return null
  506. }
  507. const { body } = document
  508. const selectedValue = this.getValue(this.props.selectedItem)
  509. const duplicatedLabels: any[] = []
  510. this.props.items.forEach((item, i) => {
  511. const label = this.getLabel(item)
  512. for (let j = i + 1; j < this.props.items.length; j += 1) {
  513. if (label === this.getLabel(this.props.items[j])
  514. && !duplicatedLabels.find(item2 => this.getLabel(item2) === label)) {
  515. duplicatedLabels.push(label)
  516. }
  517. }
  518. })
  519. const firstItemValue = this.props.items.length > 0 ? this.getValue(this.props.items[0]) : null
  520. const isFirstItemSelected = selectedValue === firstItemValue
  521. const list = ReactDOM.createPortal((
  522. <List
  523. // eslint-disable-next-line react/jsx-props-no-spreading
  524. {...this.props}
  525. ref={(ref: HTMLElement | null | undefined) => { this.listRef = ref }}
  526. >
  527. <Tip
  528. ref={(ref: HTMLElement | null | undefined) => { this.tipRef = ref }}
  529. primary={this.state.firstItemHover || isFirstItemSelected}
  530. dangerouslySetInnerHTML={{ __html: tipImage }}
  531. />
  532. <ListItems ref={(ref: HTMLElement | null | undefined) => { this.listItemsRef = ref }}>
  533. {this.props.items.map((item, i) => {
  534. if (item.separator === true) {
  535. // eslint-disable-next-line react/no-array-index-key
  536. return <Separator key={`sep-${i}`} />
  537. }
  538. const label = this.getLabel(item)
  539. const value = this.getValue(item)
  540. const duplicatedLabel = duplicatedLabels.find(l => l === label)
  541. const multipleSelected = this.props.selectedItems && this.props.selectedItems
  542. .find(j => this.getValue(j) === value)
  543. const listItem = (
  544. <ListItem
  545. data-test-id="dropdownListItem"
  546. ref={(ref: HTMLElement | null | undefined) => {
  547. if (i === 0) { this.firstItemRef = ref }
  548. }}
  549. key={value}
  550. onMouseDown={() => { this.itemMouseDown = true }}
  551. onMouseUp={() => { this.itemMouseDown = false }}
  552. onMouseEnter={() => { this.handleItemMouseEnter(i) }}
  553. onMouseLeave={() => { this.handleItemMouseLeave(i) }}
  554. onClick={() => { this.handleItemClick(item) }}
  555. selected={!this.props.multipleSelection && value === selectedValue}
  556. multipleSelected={this.props.multipleSelection && multipleSelected}
  557. dim={this.props.dimFirstItem && i === 0}
  558. paddingLeft={this.props.multipleSelection ? 8 : 16}
  559. arrowSelected={i === this.state.arrowSelection}
  560. disabled={item.disabled}
  561. >
  562. {this.props.multipleSelection ? (
  563. <Checkmark
  564. ref={(ref: HTMLElement) => { this.checkmarkRefs[`${label}-${value || ''}`] = ref }}
  565. dangerouslySetInnerHTML={{ __html: checkmarkImage }}
  566. show={multipleSelected}
  567. />
  568. ) : null}
  569. <Labels>
  570. {label === '' ? '\u00A0' : label}
  571. {item.subtitleLabel ? (
  572. <SubtitleLabel>{item.subtitleLabel}</SubtitleLabel>
  573. ) : null}
  574. {duplicatedLabel ? <DuplicatedLabel>(<span>{value || ''}</span>)</DuplicatedLabel> : ''}
  575. </Labels>
  576. </ListItem>
  577. )
  578. return listItem
  579. })}
  580. </ListItems>
  581. </List>
  582. ), body)
  583. return list
  584. }
  585. render() {
  586. const buttonValue = () => {
  587. if (this.props.items && this.props.items.length) {
  588. if (this.props.multipleSelection && this.props.selectedItems
  589. && this.props.selectedItems.length > 0) {
  590. return this.props.selectedItems.map(i => this.getLabel(this.props.items.find(item => this.getValue(item) === this.getValue(i)))).join(', ')
  591. }
  592. return this.getLabel(this.props.selectedItem)
  593. }
  594. return this.props.noItemsMessage || ''
  595. }
  596. return (
  597. <Wrapper
  598. className={this.props.className}
  599. data-test-id={this.props['data-test-id'] || 'dropdown'}
  600. embedded={this.props.embedded}
  601. tabIndex={0}
  602. ref={(ref: HTMLElement | null | undefined) => { this.wrapperRef = ref }}
  603. onFocus={() => { this.handleFocus() }}
  604. onBlur={() => { this.handleBlur() }}
  605. onKeyDown={(e: React.KeyboardEvent<HTMLDivElement>) => { this.handleKeyPress(e) }}
  606. >
  607. <DropdownButton
  608. // eslint-disable-next-line react/jsx-props-no-spreading
  609. {...this.props}
  610. data-test-id="dropdown-dropdownButton"
  611. customRef={ref => { this.buttonRef = ref }}
  612. value={buttonValue()}
  613. onClick={() => { this.handleButtonClick() }}
  614. outline={this.state.showDropdownList}
  615. />
  616. {this.props.required ? (
  617. <Required
  618. disabledLoading={this.props.disabledLoading}
  619. right={this.props.embedded ? -24 : -16}
  620. />
  621. ) : null}
  622. {this.renderList()}
  623. </Wrapper>
  624. )
  625. }
  626. }
  627. export default Dropdown