SearchInput.tsx 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  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 { observer } from 'mobx-react'
  16. import styled, { css } from 'styled-components'
  17. import autobind from 'autobind-decorator'
  18. import SearchButton from '@src/components/ui/SearchButton'
  19. import TextInput from '@src/components/ui/TextInput'
  20. import StatusIcon from '@src/components/ui/StatusComponents/StatusIcon'
  21. import { ThemeProps } from '@src/components/Theme'
  22. const Input = styled(TextInput)<any>`
  23. padding-left: 32px;
  24. ${props => (props.loading || (props.showClose && props.value) ? 'padding-right: 32px;' : '')}
  25. width: 50px;
  26. opacity: 0;
  27. transition: all ${ThemeProps.animations.swift};
  28. `
  29. const InputAnimation = (props: any) => css`
  30. ${Input} {
  31. width: ${props.width};
  32. opacity: 1;
  33. }
  34. `
  35. const Wrapper = styled.div<any>`
  36. position: relative;
  37. width: ${props => (props.open ? props.width : '50px')};
  38. ${props => (props.open ? InputAnimation(props) : '')}
  39. `
  40. const SearchButtonStyled = styled(SearchButton)<any>`
  41. position: absolute;
  42. top: 8px;
  43. left: 8px;
  44. `
  45. const StatusIconStyled = styled(StatusIcon)`
  46. position: absolute;
  47. right: 8px;
  48. top: 8px;
  49. `
  50. type Props = {
  51. onChange?: (value: string) => void,
  52. onCloseClick?: () => void,
  53. alwaysOpen?: boolean,
  54. loading?: boolean,
  55. focusOnMount?: boolean,
  56. disablePrimary?: boolean,
  57. useFilterIcon?: boolean,
  58. placeholder?: string,
  59. width?: string,
  60. value?: string,
  61. className?: string,
  62. }
  63. type State = {
  64. open: boolean,
  65. hover?: boolean,
  66. focus: boolean,
  67. }
  68. @observer
  69. class SearchInput extends React.Component<Props, State> {
  70. static defaultProps = {
  71. placeholder: 'Search',
  72. width: `${ThemeProps.inputSizes.regular.width}px`,
  73. value: '',
  74. }
  75. state: State = {
  76. open: false,
  77. focus: false,
  78. }
  79. input: HTMLElement | null | undefined
  80. itemMouseDown: boolean | undefined
  81. componentDidMount() {
  82. window.addEventListener('mousedown', this.handlePageClick, false)
  83. if (this.props.focusOnMount && this.input) this.input.focus()
  84. }
  85. componentWillUnmount() {
  86. window.removeEventListener('mousedown', this.handlePageClick, false)
  87. }
  88. @autobind
  89. handlePageClick() {
  90. if (!this.itemMouseDown) {
  91. this.setState({ open: false })
  92. }
  93. }
  94. handleSearchButtonClick() {
  95. if (this.input) this.input.focus()
  96. this.setState(prevState => ({ open: !prevState.open }))
  97. }
  98. handleMouseEnter() {
  99. this.setState({ hover: true })
  100. }
  101. handleMouseLeave() {
  102. this.setState({ hover: false })
  103. }
  104. handleFocus() {
  105. this.setState({ focus: true })
  106. }
  107. handleBlur() {
  108. this.setState({ focus: false })
  109. }
  110. render() {
  111. return (
  112. <Wrapper
  113. open={this.state.open || this.props.alwaysOpen || this.props.value !== ''}
  114. onMouseDown={() => { this.itemMouseDown = true }}
  115. onMouseUp={() => { this.itemMouseDown = false }}
  116. onMouseEnter={() => { this.handleMouseEnter() }}
  117. onMouseLeave={() => { this.handleMouseLeave() }}
  118. width={this.props.width}
  119. className={this.props.className}
  120. >
  121. <Input
  122. _ref={(input: HTMLElement | null | undefined) => { this.input = input }}
  123. placeholder={this.props.placeholder}
  124. onChange={(e: { target: { value: string } }) => {
  125. if (this.props.onChange) this.props.onChange(e.target.value)
  126. }}
  127. onFocus={() => { this.handleFocus() }}
  128. onBlur={() => { this.handleBlur() }}
  129. loading={this.props.loading}
  130. value={this.props.value}
  131. disablePrimary={this.props.disablePrimary}
  132. showClose={
  133. !this.props.loading
  134. && (this.state.open || this.props.alwaysOpen || this.props.value !== '')
  135. }
  136. onCloseClick={() => { if (this.props.onCloseClick) this.props.onCloseClick() }}
  137. />
  138. <SearchButtonStyled
  139. primary={
  140. this.state.open
  141. || (this.props.alwaysOpen && (this.state.hover || this.state.focus))
  142. || (this.props.value !== '' && (this.state.hover || this.state.focus))
  143. }
  144. onClick={() => { this.handleSearchButtonClick() }}
  145. useFilterIcon={this.props.useFilterIcon}
  146. />
  147. {this.props.loading ? <StatusIconStyled status="RUNNING" /> : null}
  148. </Wrapper>
  149. )
  150. }
  151. }
  152. export default SearchInput