From 078abf08d8f81ef7169140de2ac241fccfcd9c60 Mon Sep 17 00:00:00 2001 From: Stian Didriksen Date: Sun, 7 Aug 2016 14:32:32 +0200 Subject: [PATCH] React select fork WIP (#122) * update storybook * update readme and add logo * initial work on react-select fork * initial import of react-select code * fix tabs versus spaces madness * refactor in progress * refactoring fork * refactoring select * convert Select/Value to es6 class * convert Select/Option to es6 class * refactoring to uikit * refactoring creation of options * refactor styling * wip --- .eslintrc.yml | 30 +- .storybook/config.js | 1 + .storybook/static/logo.svg | 24 + .storybook/uikit.scss | 3 + .storybook/webpack.config.js | 9 +- README.md | 11 +- package.json | 7 +- src/Select/CreateOption.js | 21 + src/Select/Option.js | 100 ++++ src/Select/Value.js | 104 ++++ src/Select/index.js | 1075 ++++++++++++++++++++++++++++++++++ src/index.js | 1 + src/scss/Select.scss | 117 ++++ src/stories/Select.js | 64 ++ 14 files changed, 1536 insertions(+), 31 deletions(-) create mode 100644 .storybook/static/logo.svg create mode 100644 src/Select/CreateOption.js create mode 100644 src/Select/Option.js create mode 100644 src/Select/Value.js create mode 100644 src/Select/index.js create mode 100644 src/scss/Select.scss create mode 100644 src/stories/Select.js diff --git a/.eslintrc.yml b/.eslintrc.yml index 3d493005..9046cbde 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -4,23 +4,13 @@ rules: semi: - error - never - react/no-danger: - - error - react/no-comment-textnodes: - - error - react/no-direct-mutation-state: - - error - react/no-string-refs: - - error - react/jsx-handler-names: - - error - react/react-in-jsx-scope: - - off - react/jsx-uses-react: - - off - react/jsx-filename-extension: - - off - import/no-extraneous-dependencies: - - off - global-require: - - off + react/no-danger: error + react/no-comment-textnodes: error + react/no-direct-mutation-state: error + react/no-string-refs: error + react/jsx-handler-names: error + react/react-in-jsx-scope: off + react/jsx-uses-react: off + react/jsx-filename-extension: off + import/no-extraneous-dependencies: off + global-require: off diff --git a/.storybook/config.js b/.storybook/config.js index b3fad334..d5c0cb61 100644 --- a/.storybook/config.js +++ b/.storybook/config.js @@ -14,6 +14,7 @@ addDecorator(story => ( function loadStories() { require('../src/stories/button') require('../src/stories/dropdown') + require('../src/stories/Select') } configure(loadStories, module) diff --git a/.storybook/static/logo.svg b/.storybook/static/logo.svg new file mode 100644 index 00000000..70e3ff35 --- /dev/null +++ b/.storybook/static/logo.svg @@ -0,0 +1,24 @@ + + + + logo + Created with Sketch. + + + + + + + + + \ No newline at end of file diff --git a/.storybook/uikit.scss b/.storybook/uikit.scss index 777b9e5f..f216da45 100644 --- a/.storybook/uikit.scss +++ b/.storybook/uikit.scss @@ -3,4 +3,7 @@ $icon-font-path: "~uikit/dist/fonts"; @import "~uikit/dist/scss/uikit-mixins.scss"; @import "~uikit/dist/scss/uikit-variables.scss"; @import "~uikit/dist/scss/uikit.scss"; +@import "~uikit/dist/scss/components/autocomplete.scss"; @import "~uikit/dist/scss/components/form-advanced.scss"; + +@import "~uikit-react/scss/Select.scss"; diff --git a/.storybook/webpack.config.js b/.storybook/webpack.config.js index 555504bb..0631dfad 100644 --- a/.storybook/webpack.config.js +++ b/.storybook/webpack.config.js @@ -21,9 +21,12 @@ module.exports = { }, module: { loaders: [ - { test: /\.scss$/, loader: ExtractTextPlugin.extract( - 'style-loader', 'css!postcss!sass?sourceMap' - ) }, + { + test: /\.scss$/, + loader: ExtractTextPlugin.extract( + 'style-loader', 'css!postcss!sass?sourceMap' + ), + }, { test: /\.css$/, loaders: ['style', 'css'] }, { test: /\.svg$/, loader: 'url-loader?limit=10000&mimetype=image/svg+xml' }, { diff --git a/README.md b/README.md index 98e579b3..3e9be506 100644 --- a/README.md +++ b/README.md @@ -4,14 +4,15 @@ uikit-react [![Build Status](https://travis-ci.org/stipsan/uikit-react.svg)](https://travis-ci.org/stipsan/uikit-react) [![Coverage Status](https://coveralls.io/repos/github/stipsan/uikit-react/badge.svg)](https://coveralls.io/github/stipsan/uikit-react) - - -# Be warned, this is barely even alpha atm! + React UI component built on top of the UIkit CSS. -The docs folder shows how easy it is to setup a project that compiles the UIkit less and puts it all together using Browserify. +This library makes no assumptions how you provide the CSS, load it any way you like. +The minimum required css for components can function can be seen in the [SCSS file](.storybook/uikit.scss) we use to compile UIkit for our [Storybook](http://uikit-react.io). + +Check our [Storybook](http://uikit-react.io) to see docs and usage examples on each component in the library. -A changelog.md will be started up as soon as we reach 0.1.0. Versions in the 0.0.x range is not at all meant to be used in production. +We use GitHub Releases as our [Changelog](https://github.com/stipsan/uikit-react/releases). Inspired by * [material ui](http://material-ui.com/) diff --git a/package.json b/package.json index ff383404..cef3117c 100644 --- a/package.json +++ b/package.json @@ -46,8 +46,8 @@ }, "homepage": "https://github.com/stipsan/uikit-react", "devDependencies": { - "@kadira/react-storybook-addon-info": "3.1.1", - "@kadira/storybook": "2.0.0", + "@kadira/react-storybook-addon-info": "3.1.2", + "@kadira/storybook": "2.2.0", "@kadira/storybook-deployer": "1.0.0", "autoprefixer": "6.4.0", "babel-cli": "6.11.4", @@ -92,7 +92,8 @@ "url-loader": "0.5.7" }, "dependencies": { - "classnames": "2.2.5" + "classnames": "2.2.5", + "react-input-autosize": "1.1.0" }, "contributors": [ { diff --git a/src/Select/CreateOption.js b/src/Select/CreateOption.js new file mode 100644 index 00000000..9ec6bd44 --- /dev/null +++ b/src/Select/CreateOption.js @@ -0,0 +1,21 @@ +import classNames from 'classnames' +import { PropTypes } from 'react' + +const CreateOption = ({ isFocused, addLabelText, children }) => ( +
  • + + {addLabelText}  + + {children} + + +
  • +) + +CreateOption.propTypes = { + isFocused: PropTypes.bool.isRequired, + addLabelText: PropTypes.node.isRequired, + children: PropTypes.node.isRequired, +} + +export default CreateOption diff --git a/src/Select/Option.js b/src/Select/Option.js new file mode 100644 index 00000000..4c066cc6 --- /dev/null +++ b/src/Select/Option.js @@ -0,0 +1,100 @@ +import classNames from 'classnames' +import { Component, PropTypes } from 'react' + +export default class Option extends Component { + static propTypes = { + addLabelText: PropTypes.string, // text to display with value while creating + children: PropTypes.node, + className: PropTypes.string, // className (based on mouse position) + instancePrefix: PropTypes.string.isRequired, // unique prefix for the ids (used for aria) + isDisabled: PropTypes.bool, // the option is disabled + isFocused: PropTypes.bool, // the option is focused + isSelected: PropTypes.bool, // the option is selected + onFocus: PropTypes.func, // method to handle mouseEnter on option + onSelect: PropTypes.func, // method to handle click on option element + onUnfocus: PropTypes.func, // method to handle mouseLeave on option + option: PropTypes.object.isRequired, // object that is base for that option + optionIndex: PropTypes.number, // index of the option, used to generate + } + + onFocus = (event) => { + if (!this.props.isFocused) { + this.props.onFocus(this.props.option, event) + } + } + + handleBlockEvent = event => { + event.preventDefault() + event.stopPropagation() + } + + handleMouseDown = (event) => { + event.preventDefault() + event.stopPropagation() + this.props.onSelect(this.props.option, event) + } + + handleMouseEnter = (event) => { + this.onFocus(event) + } + + handleMouseMove = (event) => { + this.onFocus(event) + } + + handleTouchEnd= (event) => { + // Check if the view is being dragged, In this case + // we don't want to fire the click event (because the user only wants to scroll) + if (this.dragging) return + + this.handleMouseDown(event) + } + + handleTouchMove = () => { + // Set a flag that the view is being dragged + this.dragging = true + } + + handleTouchStart = () => { + // Set a flag that the view is not being dragged + this.dragging = false + } + + render() { + const { option, instancePrefix, optionIndex } = this.props + let className = classNames(this.props.className, option.className) + return option.disabled ? ( +
  • + + {this.props.children} + +
  • + ) : ( +
  • + + { + option.create ? + this.props.addLabelText.replace('{label}', option.label) : + this.props.children + } + +
  • + ) + } +} diff --git a/src/Select/Value.js b/src/Select/Value.js new file mode 100644 index 00000000..f3e39b16 --- /dev/null +++ b/src/Select/Value.js @@ -0,0 +1,104 @@ +import classNames from 'classnames' +import { Component, PropTypes } from 'react' + +export default class Value extends Component { + + static propTypes = { + children: PropTypes.node, + disabled: PropTypes.bool, // disabled prop passed to ReactSelect + id: PropTypes.string, // Unique id for the value - used for aria + onClick: PropTypes.func, // method to handle click on value label + onRemove: PropTypes.func, // method to handle removal of the value + value: PropTypes.object.isRequired, // the option object for this value + } + + handleRemove = event => { + event.preventDefault() + event.stopPropagation() + this.props.onRemove(this.props.value) + } + + handleMouseDown = event => { + if (event.type === 'mousedown' && event.button !== 0) { + return + } + if (this.props.onClick) { + event.stopPropagation() + this.props.onClick(this.props.value, event) + return + } + if (this.props.value.href) { + event.stopPropagation() + } + } + + handleTouchEndRemove = event => { + // Check if the view is being dragged, In this case + // we don't want to fire the click event (because the user only wants to scroll) + if (this.dragging) return + + // Fire the mouse events + this.onRemove(event) + } + + handleTouchMove = () => { + // Set a flag that the view is being dragged + this.dragging = true + } + + handleTouchStart = () => { + // Set a flag that the view is not being dragged + this.dragging = false + } + + renderRemoveIcon() { + if (this.props.disabled || !this.props.onRemove) { + return false + } + return ( + + ) + } + + renderLabel() { + let className = 'Select-value-label' + return this.props.onClick || this.props.value.href ? ( + + {this.props.children} + + ) : ( + + {this.props.children} + + ) + } + + render() { + return ( +
    + {this.renderRemoveIcon()} + {this.renderLabel()} +
    + ) + } + +} diff --git a/src/Select/index.js b/src/Select/index.js new file mode 100644 index 00000000..a0652c51 --- /dev/null +++ b/src/Select/index.js @@ -0,0 +1,1075 @@ +import classNames from 'classnames' +import Input from 'react-input-autosize' +import ReactDOM from 'react-dom' +import { Component, PropTypes } from 'react' + +import CreateOption from './CreateOption' +import Option from './Option' +import Value from './Value' + +function stringifyValue(value) { + if (typeof value === 'object') { + return JSON.stringify(value) + } + return value +} + +const stringOrNode = PropTypes.oneOfType([ + PropTypes.string, + PropTypes.node, +]) + +let instanceId = 1 + +export default class Select extends Component { + + static propTypes = { + addItemOnKeyCode: PropTypes.number, // The key code number that should trigger adding + addLabelText: PropTypes.string.isRequired, // placeholder displayed when you want to add a + allowCreate: PropTypes.bool, // whether to allow creation of new entries + 'aria-label': PropTypes.string, // Aria label (for assistive tech) + 'aria-labelledby': PropTypes.string, // HTML ID of an element that should be used as + autoBlur: PropTypes.bool, // automatically blur the component when an optio + autofocus: PropTypes.bool, // autofocus the component on mount + autosize: PropTypes.bool, // whether to enable autosizing or not + backspaceRemoves: PropTypes.bool, // whether backspace removes an item if + backspaceToRemoveMessage: PropTypes.string, // Message to use for screenreaders to press + className: PropTypes.string, // className for the outer element + clearAllText: stringOrNode, // title for the "clear" control when multi: true + clearValueText: stringOrNode, // title for the "clear" control + clearable: PropTypes.bool, // should it be possible to reset value + delimiter: PropTypes.string, // delimiter to use to join multiple values for + disabled: PropTypes.bool, // whether the Select is disabled or not + escapeClearsValue: PropTypes.bool, // whether escape clears the value when the menu is + filterOption: PropTypes.func, // method to filter a single option + filterOptions: PropTypes.any, // boolean to enable default filtering or function + ignoreAccents: PropTypes.bool, // whether to strip diacritics when filtering + ignoreCase: PropTypes.bool, // whether to perform case-insensitive filtering + inputProps: PropTypes.object, // custom attributes for the Input + inputRenderer: PropTypes.func, // returns a custom input component + isLoading: PropTypes.bool, // whether the Select is loading externally or + joinValues: PropTypes.bool, // joins multiple values into a single form + labelKey: PropTypes.string, // path of the label value in option objects + matchPos: PropTypes.string, // (any|start) match the start or entire + matchProp: PropTypes.string, // (any|label|value) which option property + menuBuffer: PropTypes.number, // optional buffer (in px) between the bottom + menuRenderer: PropTypes.func, // renders a custom menu with options + menuStyle: PropTypes.object, // optional style to apply to the menu + multi: PropTypes.bool, // multi-value input + name: PropTypes.string, // generates a hidden tag with this + newOptionCreator: PropTypes.func, // factory to create new options when allowCreate + noResultsText: stringOrNode, // placeholder displayed when there are no matching + onBlur: PropTypes.func, // onBlur handler: function (event) {} + onBlurResetsInput: PropTypes.bool, // whether input is cleared on blur + onChange: PropTypes.func.isRequired, // onChange handler: function (newValue) {} + onClose: PropTypes.func, // fires when the menu is closed + onFocus: PropTypes.func, // onFocus handler: function (event) {} + onInputChange: PropTypes.func, // onInputChange handler: function (inputValue) {} + onMenuScrollToBottom: PropTypes.func, // fires when the menu is scrolled to the bottom; + onOpen: PropTypes.func, // fires when the menu is opened + onValueClick: PropTypes.func, // onClick handler for value labels: function + openAfterFocus: PropTypes.bool, // boolean to enable opening dropdown when focused + openOnFocus: PropTypes.bool, // always open options menu on focus + optionClassName: PropTypes.string, // additional class(es) to apply to the