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

Merge pull request #483 from smiclea/numerical-stepper

Add support for integer fields with no min. / max.
Nashwan Azhari 6 лет назад
Родитель
Сommit
65e0441fbb

+ 203 - 0
src/components/atoms/Stepper/Stepper.jsx

@@ -0,0 +1,203 @@
+/* eslint-disable jsx-a11y/mouse-events-have-key-events */
+// @flow
+
+import * as React from 'react'
+import styled, { css } from 'styled-components'
+
+import StyleProps from '../../styleUtils/StyleProps'
+import Palette from '../../styleUtils/Palette'
+
+import upSvg from './images/up.svg'
+import downSvg from './images/down.svg'
+
+const Wrapper = styled.div`
+  position: relative;
+`
+const getInputWidth = (props: any) => {
+  if (props.width) {
+    return props.width
+  }
+
+  if (props.large) {
+    return `${StyleProps.inputSizes.large.width}px`
+  }
+
+  return `${StyleProps.inputSizes.regular.width}px`
+}
+const borderColor = (props: any, defaultColor = Palette.grayscale[3]) => props.highlight ? Palette.alert : defaultColor
+const Input = styled.input`
+  width: ${props => getInputWidth(props)};
+  height: ${(props: any) => props.height || `${StyleProps.inputSizes.regular.height}px`};
+  line-height: ${(props: any) => props.lineHeight || 'normal'};
+  border-radius: ${StyleProps.borderRadius};
+  background-color: #FFF;
+  border: ${(props: any) => props.embedded ? '0' : `1px solid ${borderColor(props)}`};
+  border-top-left-radius: ${(props: any) => props.embedded ? '0' : StyleProps.borderRadius};
+  border-top-right-radius: ${StyleProps.borderRadius};
+  border-bottom-left-radius: ${(props: any) => props.embedded ? '0' : StyleProps.borderRadius};
+  border-bottom-right-radius: ${StyleProps.borderRadius};
+  color: ${Palette.black};
+  padding: 0 8px 0 ${(props: any) => props.embedded ? '0' : '16px'};
+  font-size: inherit;
+  transition: all ${StyleProps.animations.swift};
+  box-sizing: border-box;
+  &:hover {
+    border-color: ${(props: any) => borderColor(props, props.disablePrimary ? undefined : Palette.primaryLight)};
+  }
+  &:focus {
+    border-color: ${(props: any) => borderColor(props, props.disablePrimary ? undefined : Palette.primaryLight)};
+    outline: none;
+  }
+  &:disabled {
+    color: ${Palette.grayscale[3]};
+    border-color: ${Palette.grayscale[0]};
+    background-color: ${Palette.grayscale[0]};
+  }
+  &::placeholder {
+    color: ${Palette.grayscale[3]};
+  }
+`
+const StepsButtons = styled.div`
+  display: flex;
+  flex-direction: column;
+  position: absolute;
+  right: ${(p: any) => p.embedded ? '0' : '1px'};
+  top: ${(p: any) => p.embedded ? '0' : '1px'};
+`
+const StepButton = css`
+  background: #F1ECEF;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+`
+const StepButtonUp = styled.div`
+  width: 17px;
+  height: 15px;
+  border-bottom: 1px solid ${Palette.secondaryLight};
+  border-top-right-radius: 4px;
+  ${StepButton}
+`
+const StepButtonDown = styled.div`
+  width: 17px;
+  height: 14px;
+  border-bottom-right-radius: 4px;
+  ${StepButton}
+`
+const StepImage = styled.div`
+  width: 8px;
+  height: 4px;
+  background: url('${(props: any) => props.up ? upSvg : downSvg}') center no-repeat;
+  transform: scale(1);
+  animation: ${StyleProps.animations.swift};
+`
+
+type State = {
+  inputValue: ?string
+}
+
+type Props = {
+  _ref?: (ref: HTMLElement) => void,
+  disabled?: ?boolean,
+  highlight?: boolean,
+  large?: boolean,
+  onChange?: (value: ?number) => void,
+  type?: string,
+  value?: number,
+  embedded?: boolean,
+  height?: string,
+  width?: string,
+  name?: string,
+  minimum?: ?number,
+  maximum?: ?number,
+}
+
+class Stepper extends React.Component<Props, State> {
+  state = {
+    inputValue: null,
+  }
+
+  commitChange(inputValue: string) {
+    let { onChange, minimum, maximum } = this.props
+    if (!onChange) {
+      return
+    }
+    if (inputValue === '') {
+      onChange(null)
+    } else {
+      let value = Number(inputValue.replace(/\D/g, '')) || 0
+      if (minimum != null && value < minimum) {
+        value = minimum
+      }
+      if (maximum != null && value > maximum) {
+        value = maximum
+      }
+      onChange(value)
+    }
+  }
+
+  handleInputChange(inputValue: string) {
+    if (inputValue.indexOf('Not S') > -1) {
+      inputValue = ''
+    }
+
+    this.setState({ inputValue })
+  }
+
+  handleInputBlur() {
+    this.commitChange(this.state.inputValue || '')
+    this.setState({ inputValue: null })
+  }
+
+  render() {
+    const { _ref, value, type } = this.props
+    let downImageRef: ?HTMLElement
+    let upImageRef: ?HTMLElement
+    let scale = (imageRef: ?HTMLElement, direction: 'up' | 'down') => {
+      if (!imageRef) {
+        return
+      }
+      imageRef.style.transform = `scale(${direction === 'down' ? '0.8' : '1'})`
+    }
+
+    return (
+      <Wrapper>
+        <Input
+          {...this.props}
+          type={type || 'text'}
+          innerRef={ref => {
+            if (_ref && ref) { _ref(ref) }
+          }}
+          value={this.state.inputValue !== null ? this.state.inputValue : (value == null ? 'Not Set' : value)}
+          onChange={e => { this.handleInputChange(e.target.value) }}
+          onBlur={() => { this.handleInputBlur() }}
+        />
+        <StepsButtons embedded={this.props.embedded}>
+          <StepButtonUp
+            onClick={() => { this.commitChange(`${(value || 0) + 1}`) }}
+            onMouseDown={() => { scale(upImageRef, 'down') }}
+            onMouseUp={() => { scale(upImageRef, 'up') }}
+            onMouseLeave={() => { scale(upImageRef, 'up') }}
+          >
+            <StepImage
+              up
+              innerRef={ref => { upImageRef = ref }}
+            />
+          </StepButtonUp>
+          <StepButtonDown
+            onClick={() => { this.commitChange(`${value && value > 0 ? value - 1 : 0}`) }}
+            onMouseDown={() => { scale(downImageRef, 'down') }}
+            onMouseUp={() => { scale(downImageRef, 'up') }}
+            onMouseLeave={() => { scale(downImageRef, 'up') }}
+          >
+            <StepImage
+              innerRef={ref => { downImageRef = ref }}
+              down
+            />
+          </StepButtonDown>
+        </StepsButtons>
+      </Wrapper>
+    )
+  }
+}
+
+export default Stepper

+ 13 - 0
src/components/atoms/Stepper/images/down.svg

@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="8px" height="4px" viewBox="0 0 8 4" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <!-- Generator: Sketch 55.1 (78136) - https://sketchapp.com -->
+    <title>Page 1</title>
+    <desc>Created with Sketch.</desc>
+    <g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="Forms/Input/Stepper" transform="translate(-215.000000, -39.000000)" fill="#777A8B">
+            <g id="Group" transform="translate(211.000000, 16.000000)">
+                <polygon id="Page-1" points="4 23 8 27 12 23"></polygon>
+            </g>
+        </g>
+    </g>
+</svg>

+ 13 - 0
src/components/atoms/Stepper/images/up.svg

@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="8px" height="4px" viewBox="0 0 8 4" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <!-- Generator: Sketch 55.1 (78136) - https://sketchapp.com -->
+    <title>Page 1</title>
+    <desc>Created with Sketch.</desc>
+    <g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="Forms/Input/Stepper" transform="translate(-215.000000, -22.000000)" fill="#777A8B">
+            <g id="Group" transform="translate(211.000000, 16.000000)">
+                <polygon id="Page-1" transform="translate(8.000000, 8.000000) rotate(-180.000000) translate(-8.000000, -8.000000) " points="4 6 8 10 12 6"></polygon>
+            </g>
+        </g>
+    </g>
+</svg>

+ 6 - 0
src/components/atoms/Stepper/package.json

@@ -0,0 +1,6 @@
+{
+  "name": "Stepper",
+  "version": "0.0.0",
+  "private": true,
+  "main": "./Stepper.jsx"
+}

+ 31 - 41
src/components/molecules/FieldInput/FieldInput.jsx

@@ -27,6 +27,7 @@ import DropdownInput from '../DropdownInput/DropdownInput'
 import TextArea from '../../atoms/TextArea/TextArea'
 import TextArea from '../../atoms/TextArea/TextArea'
 import PropertiesTable from '../PropertiesTable/PropertiesTable'
 import PropertiesTable from '../PropertiesTable/PropertiesTable'
 import AutocompleteDropdown from '../../molecules/AutocompleteDropdown'
 import AutocompleteDropdown from '../../molecules/AutocompleteDropdown'
+import Stepper from '../../atoms/Stepper'
 
 
 import type { Field } from '../../../types/Field'
 import type { Field } from '../../../types/Field'
 
 
@@ -135,20 +136,40 @@ class FieldInput extends React.Component<Props> {
   }
   }
 
 
   renderIntInput() {
   renderIntInput() {
+    if (this.props.minimum && this.props.maximum && this.props.maximum - this.props.minimum <= 10) {
+      let items = []
+
+      for (let i = this.props.minimum; i <= this.props.maximum; i += 1) {
+        items.push({
+          label: i.toString(),
+          value: i,
+        })
+      }
+      return (
+        <Dropdown
+          width={this.props.width}
+          selectedItem={this.props.value}
+          items={items}
+          onChange={item => { if (this.props.onChange) this.props.onChange(item.value) }}
+          disabled={this.props.disabled}
+          disabledLoading={this.props.disabledLoading}
+          highlight={this.props.highlight}
+          required={this.props.layout === 'page' ? false : this.props.required}
+        />
+      )
+    }
+
     return (
     return (
-      <TextInput
-        highlight={this.props.highlight}
-        width={this.props.width}
+      <Stepper
+        highlight={Boolean(this.props.highlight)}
+        width={this.props.width ? `${this.props.width}px` : undefined}
         value={this.props.value}
         value={this.props.value}
-        onChange={e => {
-          let value = Number(e.target.value.replace(/\D/g, '')) || ''
-          if (this.props.onChange) {
-            this.props.onChange(value)
-          }
-        }}
-        placeholder={LabelDictionary.get(this.props.name)}
         disabled={this.props.disabled}
         disabled={this.props.disabled}
         disabledLoading={this.props.disabledLoading}
         disabledLoading={this.props.disabledLoading}
+        required={this.props.layout === 'page' ? false : this.props.required}
+        onChange={value => { if (this.props.onChange) this.props.onChange(value) }}
+        minimum={this.props.minimum}
+        maximum={this.props.maximum}
       />
       />
     )
     )
   }
   }
@@ -264,34 +285,6 @@ class FieldInput extends React.Component<Props> {
     )
     )
   }
   }
 
 
-  renderIntDropdown() {
-    if (!this.props.minimum || !this.props.maximum) {
-      return null
-    }
-
-    let items = []
-
-    for (let i = this.props.minimum; i <= this.props.maximum; i += 1) {
-      items.push({
-        label: i.toString(),
-        value: i,
-      })
-    }
-
-    return (
-      <Dropdown
-        width={this.props.width}
-        selectedItem={this.props.value}
-        items={items}
-        onChange={item => { if (this.props.onChange) this.props.onChange(item.value) }}
-        disabled={this.props.disabled}
-        disabledLoading={this.props.disabledLoading}
-        highlight={this.props.highlight}
-        required={this.props.layout === 'page' ? false : this.props.required}
-      />
-    )
-  }
-
   renderRadioInput() {
   renderRadioInput() {
     return (
     return (
       <RadioInput
       <RadioInput
@@ -348,9 +341,6 @@ class FieldInput extends React.Component<Props> {
         }
         }
         return this.renderTextInput()
         return this.renderTextInput()
       case 'integer':
       case 'integer':
-        if (this.props.minimum || this.props.maximum) {
-          return this.renderIntDropdown()
-        }
         return this.renderIntInput()
         return this.renderIntInput()
       case 'radio':
       case 'radio':
         return this.renderRadioInput()
         return this.renderRadioInput()

+ 21 - 1
src/components/organisms/WizardOptions/story.jsx

@@ -14,9 +14,28 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 
 import React from 'react'
 import React from 'react'
 import { storiesOf } from '@storybook/react'
 import { storiesOf } from '@storybook/react'
+
+import configLoader from '../../../utils/Config'
+
 import WizardOptions from '.'
 import WizardOptions from '.'
 
 
 let fields = [
 let fields = [
+  {
+    name: 'integer with small min max',
+    type: 'integer',
+    minimum: 10,
+    maximum: 20,
+  },
+  {
+    name: 'integer with min',
+    type: 'integer',
+    minimum: 10,
+  },
+  {
+    name: 'integer with max',
+    type: 'integer',
+    maximum: 20,
+  },
   {
   {
     name: 'list_all_destination_networks',
     name: 'list_all_destination_networks',
     type: 'boolean',
     type: 'boolean',
@@ -57,6 +76,7 @@ let fields = [
     type: 'strict-boolean',
     type: 'strict-boolean',
   },
   },
 ]
 ]
+configLoader.config = { passwordFields: [] }
 
 
 class Wrapper extends React.Component {
 class Wrapper extends React.Component {
   constructor() {
   constructor() {
@@ -75,7 +95,7 @@ class Wrapper extends React.Component {
 
 
   render() {
   render() {
     return (
     return (
-      <div style={{ width: '800px', display: 'flex', justifyContent: 'center' }}>
+      <div style={{ width: '1000px', display: 'flex', justifyContent: 'center' }}>
         <WizardOptions
         <WizardOptions
           {...this.props}
           {...this.props}
           data={this.state.data}
           data={this.state.data}