Просмотр исходного кода

Add a close button to search inputs

Sergiu Miclea 8 лет назад
Родитель
Сommit
c562225656

+ 14 - 0
src/components/atoms/TextInput/images/close.svg

@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <!-- Generator: Sketch 48.2 (47327) - http://www.bohemiancoding.com/sketch -->
+    <title>Icon/Close</title>
+    <desc>Created with Sketch.</desc>
+    <defs></defs>
+    <g id="AM-Long-List-Filtered-Edit" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" transform="translate(-537.000000, -562.000000)">
+        <g id="Icon/Close" transform="translate(537.000000, 562.000000)">
+            <rect id="Rectangle-1" fill="#A4AAB5" fill-rule="evenodd" x="0" y="0" width="16" height="16" rx="4"></rect>
+            <path d="M12,4 L4,12" id="Line" stroke="#FFFFFF" stroke-width="1.5" stroke-linecap="round"></path>
+            <path d="M12,12 L4,4" id="Line-Copy" stroke="#FFFFFF" stroke-width="1.5" stroke-linecap="round"></path>
+        </g>
+    </g>
+</svg>

+ 36 - 5
src/components/atoms/TextInput/index.jsx

@@ -20,6 +20,7 @@ import Palette from '../../styleUtils/Palette'
 import StyleProps from '../../styleUtils/StyleProps'
 
 import starImage from './images/star.svg'
+import closeImage from './images/close.svg'
 
 const Wrapper = styled.div`
   position: relative;
@@ -35,23 +36,24 @@ const getInputWidth = props => {
 
   return `${StyleProps.inputSizes.regular.width}px`
 }
+const borderColor = (props, defaultColor = Palette.grayscale[3]) => props.highlight ? Palette.alert : defaultColor
 const Input = styled.input`
   width: ${props => getInputWidth(props)};
   height: ${StyleProps.inputSizes.regular.height}px;
   line-height: ${StyleProps.inputSizes.regular.height}px;
   border-radius: ${StyleProps.borderRadius};
   background-color: #FFF;
-  border: 1px solid ${props => props.highlight ? Palette.alert : Palette.grayscale[3]};
+  border: 1px solid ${props => borderColor(props)};
   color: ${Palette.black};
   padding: 0 ${props => props.customRequired ? '29px' : '8px'} 0 16px;
   font-size: inherit;
   transition: all ${StyleProps.animations.swift};
   box-sizing: border-box;
   &:hover {
-    border-color: ${props => props.highlight ? Palette.alert : Palette.primary};
+    border-color: ${props => borderColor(props, props.disablePrimary ? null : Palette.primary)};
   }
   &:focus {
-    border-color: ${props => props.highlight ? Palette.alert : Palette.primary};
+    border-color: ${props => borderColor(props, props.disablePrimary ? null : Palette.primary)};
     outline: none;
   }
   &:disabled {
@@ -72,6 +74,16 @@ const Required = styled.div`
   height: 8px;
   background: url('${starImage}') center no-repeat;
 `
+const Close = styled.div`
+  display: ${props => props.show ? 'block' : 'none'};
+  width: 16px;
+  height: 16px;
+  background: url('${closeImage}') center no-repeat;
+  position: absolute;
+  top: 8px;
+  right: 8px;
+  cursor: pointer;
+`
 
 type Props = {
   _ref?: (ref: HTMLElement) => void,
@@ -83,13 +95,32 @@ type Props = {
   placeholder?: string,
   type?: string,
   value?: string,
+  showClose?: boolean,
+  onCloseClick?: () => void,
 }
 const TextInput = (props: Props) => {
-  const { _ref, required } = props
+  const { _ref, required, value, onChange, showClose, onCloseClick } = props
+  let input
   return (
     <Wrapper>
-      <Input innerRef={_ref} type="text" customRequired={required} {...props} />
+      <Input
+        innerRef={ref => { input = ref; if (_ref) _ref(ref) }}
+        type="text"
+        customRequired={required}
+        value={value}
+        onChange={onChange}
+        {...props}
+      />
       <Required show={required} />
+      <Close
+        show={showClose && value}
+        onClick={() => {
+          input.focus()
+          // $FlowIgnore
+          if (onChange) onChange({ target: { value: '' } })
+          if (onCloseClick) onCloseClick()
+        }}
+      />
     </Wrapper>
   )
 }

+ 16 - 0
src/components/atoms/TextInput/story.jsx

@@ -20,6 +20,19 @@ import TextInput from '.'
 const Wrapper = styled.div`
   display: inline-block;
 `
+class StatefulInput extends React.Component {
+  constructor() {
+    super()
+
+    this.state = {
+      value: '',
+    }
+  }
+
+  render() {
+    return <TextInput {...this.props} value={this.state.value} onChange={e => { this.setState({ value: e.target.value }) }} />
+  }
+}
 
 storiesOf('TextInput', module)
   .add('default', () => (
@@ -34,3 +47,6 @@ storiesOf('TextInput', module)
   .add('large', () => (
     <Wrapper><TextInput large /></Wrapper>
   ))
+  .add('with close', () => (
+    <Wrapper><StatefulInput showClose /></Wrapper>
+  ))

+ 2 - 1
src/components/molecules/MainListFilter/index.jsx

@@ -78,6 +78,7 @@ type Props = {
   onFilterItemClick: (item: DictItem) => void,
   onReloadButtonClick: () => void,
   onSearchChange: (value: string) => void,
+  searchValue: string,
   onSelectAllChange: (checked: boolean) => void,
   onActionChange: (action: string) => void,
   actions: DictItem[],
@@ -134,7 +135,7 @@ class MainListFilter extends React.Component<Props> {
           />
           {this.renderFilterGroup()}
           <ReloadButton style={{ marginRight: '16px' }} onClick={this.props.onReloadButtonClick} />
-          <SearchInput onChange={this.props.onSearchChange} />
+          <SearchInput onChange={this.props.onSearchChange} value={this.props.searchValue} />
         </Main>
         {this.renderSelectionInfo()}
       </Wrapper>

+ 36 - 21
src/components/molecules/SearchInput/index.jsx

@@ -24,49 +24,56 @@ import StatusIcon from '../../atoms/StatusIcon'
 import StyleProps from '../../styleUtils/StyleProps'
 
 const Input = styled(TextInput) `
-  position: absolute;
-  top: -8px;
-  left: -8px;
   padding-left: 32px;
-  ${props => props.loading ? 'padding-right: 32px;' : ''}
+  ${props => props.loading || (props.showClose && props.value) ? 'padding-right: 32px;' : ''}
   width: 50px;
   opacity: 0;
   transition: all ${StyleProps.animations.swift};
 `
-const InputAnimation = css`
+const InputAnimation = props => css`
   ${Input} {
-    width: ${StyleProps.inputSizes.regular.width}px;
+    width: ${props.width};
     opacity: 1;
   }
 `
 const Wrapper = styled.div`
   position: relative;
-  ${props => props.open ? InputAnimation : ''}
+  width: ${props => props.open ? props.width : '50px'};
+  ${props => props.open ? InputAnimation(props) : ''}
 `
 const SearchButtonStyled = styled(SearchButton)`
-  position: relative;
+  position: absolute;
+  top: 8px;
+  left: 8px;
 `
 const StatusIconStyled = styled(StatusIcon)`
   position: absolute;
-  left: 144px;
-  top: 0;
+  right: 8px;
+  top: 8px;
 `
 
 type Props = {
-  onChange: (value: string) => void,
+  onChange?: (value: string) => void,
+  onCloseClick?: () => void,
   alwaysOpen?: boolean,
   loading?: boolean,
-  placeholder: string,
+  focusOnMount?: boolean,
+  disablePrimary?: boolean,
+  useFilterIcon?: boolean,
+  placeholder?: string,
+  width?: string,
+  value?: string,
+  className?: string,
 }
 type State = {
   open: boolean,
   hover?: boolean,
   focus?: boolean,
-  value: string,
 }
 class SearchInput extends React.Component<Props, State> {
-  static defaultProps = {
+  static defaultProps: $Shape<Props> = {
     placeholder: 'Search',
+    width: `${StyleProps.inputSizes.regular.width}px`,
   }
 
   input: HTMLElement
@@ -86,6 +93,8 @@ class SearchInput extends React.Component<Props, State> {
 
   componentDidMount() {
     window.addEventListener('mousedown', this.handlePageClick, false)
+
+    this.props.focusOnMount && this.input.focus()
   }
 
   componentWillUnmount() {
@@ -122,31 +131,37 @@ class SearchInput extends React.Component<Props, State> {
   render() {
     return (
       <Wrapper
-        open={this.state.open || this.props.alwaysOpen || this.state.value !== ''}
+        open={this.state.open || this.props.alwaysOpen || this.props.value !== ''}
         onMouseDown={() => { this.itemMouseDown = true }}
         onMouseUp={() => { this.itemMouseDown = false }}
         onMouseEnter={() => { this.handleMouseEnter() }}
         onMouseLeave={() => { this.handleMouseLeave() }}
+        width={this.props.width}
+        className={this.props.className}
       >
         <Input
           _ref={input => { this.input = input }}
           placeholder={this.props.placeholder}
-          onChange={e => {
-            this.setState({ value: e.target.value })
-            this.props.onChange(e.target.value)
-          }}
-          value={this.state.value}
+          onChange={e => { if (this.props.onChange) this.props.onChange(e.target.value) }}
           onFocus={() => { this.handleFocus() }}
           onBlur={() => { this.handleBlur() }}
           loading={this.props.loading}
+          value={this.props.value}
+          disablePrimary={this.props.disablePrimary}
+          showClose={
+            !this.props.loading &&
+            (this.state.open || this.props.alwaysOpen || this.props.value !== '')
+          }
+          onCloseClick={() => { if (this.props.onCloseClick) this.props.onCloseClick() }}
         />
         <SearchButtonStyled
           primary={
             this.state.open ||
             (this.props.alwaysOpen && (this.state.hover || this.state.focus)) ||
-            (this.state.value !== '' && (this.state.hover || this.state.focus))
+            (this.props.value !== '' && (this.state.hover || this.state.focus))
           }
           onClick={() => { this.handleSearchButtonClick() }}
+          useFilterIcon={this.props.useFilterIcon}
         />
         {this.props.loading ? <StatusIconStyled status="RUNNING" /> : null}
       </Wrapper>

+ 18 - 1
src/components/molecules/SearchInput/story.jsx

@@ -16,7 +16,24 @@ import React from 'react'
 import { storiesOf } from '@storybook/react'
 import SearchInput from '.'
 
+class Wrapper extends React.Component {
+  constructor() {
+    super()
+
+    this.state = {
+      value: '',
+    }
+  }
+
+  render() {
+    return <SearchInput {...this.props} value={this.state.value} onChange={value => { this.setState({ value }) }} />
+  }
+}
+
 storiesOf('SearchInput', module)
   .add('default', () => (
-    <SearchInput />
+    <Wrapper />
+  ))
+  .add('always open', () => (
+    <Wrapper alwaysOpen />
   ))

+ 1 - 0
src/components/organisms/FilterList/index.jsx

@@ -146,6 +146,7 @@ class FilterList extends React.Component<Props, State> {
           selectedValue={this.state.filterStatus}
           onReloadButtonClick={this.props.onReloadButtonClick}
           onSearchChange={text => { this.handleSearchChange(text) }}
+          searchValue={this.state.filterText}
           onSelectAllChange={selected => { this.handleSelectAllChange(selected) }}
           selectAllSelected={this.state.selectAllSelected}
           selectionInfo={{

+ 3 - 1
src/components/organisms/WizardInstances/index.jsx

@@ -210,9 +210,10 @@ class WizardInstances extends React.Component<Props, State> {
   }
 
   handleSeachInputChange(searchText: string) {
+    this.setState({ searchText })
+
     clearTimeout(this.timeout)
     this.timeout = setTimeout(() => {
-      this.setState({ searchText })
       this.props.onSearchInputChange(searchText)
     }, 500)
   }
@@ -317,6 +318,7 @@ class WizardInstances extends React.Component<Props, State> {
         <SearchInput
           alwaysOpen
           onChange={searchText => { this.handleSeachInputChange(searchText) }}
+          value={this.state.searchText}
           loading={this.props.searching}
           placeholder="Search VMs"
         />