├── .babelrc ├── .eslintrc.js ├── .github └── ISSUE_TEMPLATE │ └── custom.md ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── config └── babel-preset.js ├── demo ├── demo.html ├── demo.js ├── github.png ├── index.css └── index.js ├── jest.config.js ├── package.json ├── prettier.config.js ├── src ├── TimeInput.js └── lib │ ├── caret.js │ ├── get-base.js │ ├── get-group-id.js │ ├── get-groups.js │ ├── is-twelve-hour-time.js │ ├── replace-char-at.js │ ├── stringify.js │ ├── time-string-adder.js │ ├── toggle-24-hour.js │ ├── validate.js │ └── zero-pad.js ├── test ├── arrow-keys.test.js ├── change.test.js ├── classnames.test.js ├── delete.test.js ├── get-base.test.js ├── get-group-id.test.js ├── get-groups.test.js ├── is-twelve-hour-time.test.js ├── lib │ └── renderTimeInput.js ├── replace-char-at.test.js ├── stringify.test.js ├── tabbing.test.js ├── time-string-adder.test.js ├── toggle-24-hour.test.js └── zero-pad.test.js ├── webpack.commonjs.config.js ├── webpack.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets":[ 3 | "./config/babel-preset.js", 4 | "stage-2", 5 | "react" 6 | ], 7 | "env": { 8 | "test": { 9 | "presets": [["es2015"], "react"] 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es6: true 5 | }, 6 | extends: ['eslint:recommended', 'plugin:react/recommended'], 7 | parserOptions: { 8 | ecmaFeatures: { 9 | experimentalObjectRestSpread: true, 10 | jsx: true 11 | }, 12 | sourceType: 'module' 13 | }, 14 | plugins: ['react'], 15 | rules: { 16 | indent: ['error', 2], 17 | 'linebreak-style': ['error', 'unix'], 18 | quotes: ['error', 'single'], 19 | semi: ['error', 'always'] 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/custom.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Custom issue template 3 | about: Describe this issue template's purpose here. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | milestone: 1 8 | 9 | --- 10 | 11 | 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | docs 4 | .DS_Store 5 | dist 6 | es 7 | yarn-error.log 8 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | docs/ 2 | demo/ 3 | test/ 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8" 4 | 5 | before_script: 6 | - npm install -g codecov 7 | 8 | after_success: 'npm run coverage-ci' 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Radu Mardale 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-keyboard-time-input 2 | 3 | [![Build Status](https://travis-ci.org/radumardale/react-keyboard-time-input.svg?branch=master)](https://travis-ci.org/radumardale/react-keyboard-time-input) 4 | [![codecov](https://codecov.io/gh/radumardale/react-keyboard-time-input/branch/master/graph/badge.svg)](https://codecov.io/gh/radumardale/react-keyboard-time-input) 5 | 6 | [![PeerDependencies](https://img.shields.io/david/peer/radumardale/react-keyboard-time-input.svg)](https://david-dm.org/radumardale/react-keyboard-time-input#info=peerDependencies&view=list) 7 | [![Dependencies](https://img.shields.io/david/radumardale/react-keyboard-time-input.svg)](https://david-dm.org/radumardale/react-keyboard-time-input) 8 | [![DevDependencies](https://img.shields.io/david/dev/radumardale/react-keyboard-time-input.svg)](https://david-dm.org/radumardale/react-keyboard-time-input#info=devDependencies&view=list) 9 | 10 | Forked from [alanclarke/time-input](https://github.com/alanclarke/time-input). 11 | 12 | A keyboard friendly react component for capturing time. 13 | 14 | [DEMO HERE](https://radumardale.github.io/react-keyboard-time-input/) 15 | 16 | ## Features 17 | - supports `react 16` 18 | - supports es6 and commonjs modules 19 | - small UI surface area (just a form input) 20 | - keyboard friendly (can type times, use up and down keys to go forwards and backwards in time, can tab between time groups) 21 | - simple api (infers most options from value, e.g. 24hr time or 12hr, whether to display seconds and milliseconds) 22 | - easy going UX: ignores invalid input and simply skips over separator if omitted 23 | - no dependencies 24 | - 95% test coverage 25 | 26 | ## Installation 27 | ``` 28 | yarn add react-keyboard-time-input 29 | ``` 30 | or 31 | ``` 32 | npm install react-keyboard-time-input 33 | ``` 34 | 35 | ## Usage 36 | ```js 37 | import ReactDom from 'ReactDom'; 38 | import TimeInput from 'react-keyboard-time-input'; 39 | 40 | function render (value) { 41 | ReactDom.render(( 42 | 43 | ), document.body) 44 | } 45 | 46 | render() 47 | ``` 48 | 49 | ## Valid formats 50 | ```js 51 | /* 52 | * '12:00' 53 | * '12:00 AM' 54 | * '12:00:00' 55 | * '12:00:00:000 AM' 56 | */ 57 | ``` 58 | ## Run tests 59 | ``` 60 | npm test 61 | ``` 62 | -------------------------------------------------------------------------------- /config/babel-preset.js: -------------------------------------------------------------------------------- 1 | const env = require('babel-preset-env').buildPreset; 2 | module.exports = { 3 | presets: [ 4 | [ 5 | 'es2015', 6 | { 7 | loose: true, 8 | modules: process.env.BABEL_ENV === 'commonjs' ? 'commonjs' : false 9 | } 10 | ] 11 | ] 12 | }; 13 | -------------------------------------------------------------------------------- /demo/demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Demo react-keyboard-time-input 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /demo/demo.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import TimeInput from '../dist/TimeInput'; 4 | import gitLogo from './github.png'; 5 | 6 | export default class DemoTimeInput extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | this.state = { 10 | val1: '11:30:00:000 PM', 11 | val2: '11:30:00:000', 12 | val3: '11:30 PM', 13 | val4: '11:30' 14 | }; 15 | } 16 | 17 | onInputChange(val) { 18 | return time => { 19 | this.setState({ 20 | [val]: time 21 | }); 22 | }; 23 | } 24 | 25 | render() { 26 | const keys = Object.keys(this.state); 27 | 28 | return ( 29 |
30 |
31 |
32 |

react-keyboard-time-input demo

33 | 36 | 37 | 38 |
39 |
40 | 41 |
42 | {keys.map(valName => ( 43 | 49 | ))} 50 |
51 |
52 | ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /demo/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/radumardale/react-keyboard-time-input/bc98738a90601f54fbcc25773b6eb9af1d713f65/demo/github.png -------------------------------------------------------------------------------- /demo/index.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Source+Sans+Pro'); 2 | 3 | html, body { 4 | font-family: 'Source Sans Pro', sans-serif; 5 | background: white; 6 | height: 100%; 7 | padding: 0; 8 | margin: 0; 9 | position: relative; 10 | color: #263238; 11 | } 12 | input { 13 | font-family: 'Source Sans Pro', sans-serif; 14 | } 15 | .TimeInput { 16 | display: flex; 17 | align-items: center; 18 | justify-content: center; 19 | margin-bottom: 40px; 20 | } 21 | .TimeInput-input { 22 | color: #455A64; 23 | padding: 6px 8px; 24 | border-radius: 2px; 25 | border: 1px solid #CFD8DC; 26 | font-size: 1.2em; 27 | } 28 | .demo-header { 29 | display: flex; 30 | background: #CFD8DC; 31 | padding: 12px; 32 | align-items:center; 33 | border-bottom: 1px solid #90A4AE; 34 | } 35 | .middle-container { 36 | margin-top: 24px; 37 | display: flex; 38 | flex-direction: column; 39 | max-width: 720px; 40 | margin-left: auto; 41 | margin-right: auto; 42 | } 43 | .demo-header__middle{ 44 | align-items:center; 45 | max-width: 720px; 46 | display: flex; 47 | flex-direction: row; 48 | flex:1 0 auto; 49 | margin-left: auto; 50 | margin-right: auto; 51 | } 52 | .demo-header__middle h4 { 53 | color: #37474F; 54 | flex: 1 0 auto; 55 | margin: 0; 56 | } 57 | a.github-link { 58 | display: flex; 59 | align-items: center; 60 | } 61 | 62 | a.github-link img { 63 | opacity: 0.6; 64 | height: 1em; 65 | } 66 | a.github-link:hover img { 67 | opacity: 1; 68 | } -------------------------------------------------------------------------------- /demo/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDom from 'react-dom'; 3 | 4 | import DemoTimeInput from './demo.js'; 5 | import './index.css'; 6 | 7 | ReactDom.render(, document.getElementById('main')); 8 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | collectCoverage: false, 3 | collectCoverageFrom: ['src/**/*.{js,jsx}'], 4 | testMatch: ['/test/*.test.js'], 5 | bail: true, 6 | verbose: false 7 | }; 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-keyboard-time-input", 3 | "version": "2.1.1", 4 | "description": "A keyboard friendly react component for capturing time", 5 | "module": "./es/TimeInput", 6 | "jsnext:main": "./es/TimeInput", 7 | "main": "./dist/TimeInput", 8 | "scripts": { 9 | "predeploy-docs": "./node_modules/.bin/webpack --config webpack.config.js", 10 | "deploy-docs": "gh-pages -d docs", 11 | "eslint": "eslint --config ./.eslintrc.js ./src --ext .js,.jsx", 12 | "pretest": "yarn run eslint", 13 | "test": "jest --coverage", 14 | "coverage-ci": "codecov", 15 | "start": "webpack-dev-server --config webpack.config.js", 16 | "compile:common-one-file": "BABEL_ENV=commonjs NODE_ENV=production ./node_modules/.bin/webpack --config webpack.commonjs.config.js", 17 | "compile:es": "NODE_ENV=production babel src/ --out-dir ./es", 18 | "compile": "BABEL_ENV=commonjs NODE_ENV=production babel src/ --out-dir ./dist", 19 | "preprepublish": "yarn run test", 20 | "prepublish": "yarn run compile && yarn run compile:es" 21 | }, 22 | "author": "Alan Clarke (alz.so)", 23 | "contributors": [ 24 | "Matt Livingston ", 25 | "Radu Mardale " 26 | ], 27 | "license": "MIT", 28 | "dependencies": {}, 29 | "devDependencies": { 30 | "babel-cli": "^6.10.1", 31 | "babel-core": "^6.26.0", 32 | "babel-jest": "^22.0.6", 33 | "babel-loader": "7.1.2", 34 | "babel-plugin-transform-rename-import": "^2.1.1", 35 | "babel-preset-env": "^1.6.1", 36 | "babel-preset-es2015": "^6.24.1", 37 | "babel-preset-react": "^6.24.1", 38 | "babel-preset-stage-2": "^6.24.1", 39 | "babel-register": "^6.5.2", 40 | "create-react-class": "^15.5.3", 41 | "eslint": "^4.15.0", 42 | "eslint-plugin-react": "^7.5.1", 43 | "file-loader": "^1.1.6", 44 | "gh-pages": "^1.1.0", 45 | "html-webpack-plugin": "^2.30.1", 46 | "jest": "^22.0.6", 47 | "prettier": "^1.10.2", 48 | "prop-types": "^15.5.10", 49 | "raw-loader": "^0.5.1", 50 | "react": "^16.2.0", 51 | "react-addons-test-utils": "^15.6.2", 52 | "react-dom": "^16.2.0", 53 | "react-test-utils": "0.0.1", 54 | "regenerator-runtime": "^0.11.1", 55 | "source-map-loader": "^0.2.3", 56 | "standard": "^10.0.3", 57 | "style-loader": "^0.19.1", 58 | "webpack": "^3.10.0", 59 | "webpack-dev-server": "^2.10.1", 60 | "webpack-hot-middleware": "^2.7.1" 61 | }, 62 | "peerDependencies": { 63 | "react": "^0.14.0 || ^15.0.0 || ^16.0.0" 64 | }, 65 | "directories": { 66 | "test": "test" 67 | }, 68 | "standard": { 69 | "ignore": [ 70 | "/dist/*" 71 | ] 72 | }, 73 | "keywords": [ 74 | "simple", 75 | "field", 76 | "form", 77 | "input", 78 | "picker", 79 | "keyboard time", 80 | "simple time", 81 | "time", 82 | "time input", 83 | "timepicker", 84 | "keyboard timepicker" 85 | ], 86 | "repository": { 87 | "type": "git", 88 | "url": "git+https://github.com/radumardale/react-keyboard-time-input.git" 89 | }, 90 | "bugs": { 91 | "url": "https://github.com/radumardale/react-keyboard-time-input/issues" 92 | }, 93 | "homepage": "https://github.com/radumardale/react-keyboard-time-input#readme" 94 | } 95 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | // .prettierrc.js 2 | module.exports = { 3 | singleQuote: true, 4 | parser: 'flow', 5 | trailingComma: 'none', 6 | jsxBracketSameLine: true 7 | }; 8 | -------------------------------------------------------------------------------- /src/TimeInput.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import CreateReactClass from 'create-react-class'; 3 | import PropTypes from 'prop-types'; 4 | 5 | import isTwelveHourTime from './lib/is-twelve-hour-time'; 6 | import replaceCharAt from './lib/replace-char-at'; 7 | import getGroupId from './lib/get-group-id'; 8 | import getGroups from './lib/get-groups'; 9 | import adder from './lib/time-string-adder'; 10 | import caret from './lib/caret'; 11 | import validate from './lib/validate'; 12 | 13 | const SILHOUETTE = '00:00:00:000 AM'; 14 | 15 | // isSeparator :: Char -> Bool 16 | const isSeparator = char => /[:\s]/.test(char); 17 | 18 | var TimeInput = CreateReactClass({ 19 | getInitialState() { 20 | return {}; 21 | }, 22 | getDefaultProps() { 23 | return { 24 | value: '12:00 AM' 25 | }; 26 | }, 27 | propTypes: { 28 | className: PropTypes.string, 29 | value: PropTypes.string, 30 | onChange: PropTypes.func 31 | }, 32 | render() { 33 | var className = 'TimeInput'; 34 | if (this.props.className) { 35 | className += ' ' + this.props.className; 36 | } 37 | return ( 38 |
39 | { 42 | this.input = input; 43 | }} 44 | type="text" 45 | value={this.format(this.props.value)} 46 | onChange={this.handleChange} 47 | onBlur={this.handleBlur} 48 | onKeyDown={this.handleKeyDown} 49 | /> 50 |
51 | ); 52 | }, 53 | format(val) { 54 | if (isTwelveHourTime(val)) val = val.replace(/^00/, '12'); 55 | return val.toUpperCase(); 56 | }, 57 | componentDidMount() { 58 | this.mounted = true; 59 | }, 60 | componentWillUnmount() { 61 | this.mounted = false; 62 | }, 63 | componentDidUpdate() { 64 | var index = this.state.caretIndex; 65 | if (index || index === 0) caret.set(this.input, index); 66 | }, 67 | handleBlur() { 68 | if (this.mounted) this.setState({ caretIndex: null }); 69 | }, 70 | handleEscape() { 71 | if (this.mounted) this.input.blur(); 72 | }, 73 | handleTab(event) { 74 | var start = caret.start(this.input); 75 | var value = this.props.value; 76 | var groups = getGroups(value); 77 | var groupId = getGroupId(start); 78 | if (event.shiftKey) { 79 | if (!groupId) return; 80 | groupId--; 81 | } else { 82 | if (groupId >= groups.length - 1) return; 83 | groupId++; 84 | } 85 | event.preventDefault(); 86 | var index = groupId * 3; 87 | if (this.props.value.charAt(index) === ' ') index++; 88 | if (this.mounted) this.setState({ caretIndex: index }); 89 | }, 90 | handleArrows(event) { 91 | event.preventDefault(); 92 | var start = caret.start(this.input); 93 | var value = this.props.value; 94 | var amount = event.which === 38 ? 1 : -1; 95 | if (event.shiftKey) { 96 | amount *= 2; 97 | if (event.metaKey) amount *= 2; 98 | } 99 | value = adder(value, getGroupId(start), amount); 100 | this.onChange(value, start); 101 | }, 102 | silhouette() { 103 | return this.props.value.replace(/\d/g, (val, i) => SILHOUETTE.charAt(i)); 104 | }, 105 | handleBackspace(event) { 106 | event.preventDefault(); 107 | var start = caret.start(this.input); 108 | var value = this.props.value; 109 | var end = caret.end(this.input); 110 | if (!start && !end) return; 111 | var diff = end - start; 112 | var silhouette = this.silhouette(); 113 | if (!diff) { 114 | if (value[start - 1] === ':') start--; 115 | value = replaceCharAt(value, start - 1, silhouette.charAt(start - 1)); 116 | start--; 117 | } else { 118 | while (diff--) { 119 | if (value[end - 1] !== ':') { 120 | value = replaceCharAt(value, end - 1, silhouette.charAt(end - 1)); 121 | } 122 | end--; 123 | } 124 | } 125 | if (value.charAt(start - 1) === ':') start--; 126 | this.onChange(value, start); 127 | }, 128 | handleForwardSpace(event) { 129 | event.preventDefault(); 130 | var start = caret.start(this.input); 131 | var value = this.props.value; 132 | var end = caret.end(this.input); 133 | if ((start === end) === value.length - 1) return; 134 | var diff = end - start; 135 | var silhouette = this.silhouette(); 136 | if (!diff) { 137 | if (value[start] === ':') start++; 138 | value = replaceCharAt(value, start, silhouette.charAt(start)); 139 | start++; 140 | } else { 141 | while (diff--) { 142 | if (value[end - 1] !== ':') { 143 | value = replaceCharAt(value, start, silhouette.charAt(start)); 144 | } 145 | start++; 146 | } 147 | } 148 | if (value.charAt(start) === ':') start++; 149 | this.onChange(value, start); 150 | }, 151 | handleKeyDown(event) { 152 | switch (event.which) { 153 | case 9: // Tab 154 | return this.handleTab(event); 155 | case 8: // Backspace 156 | return this.handleBackspace(event); 157 | case 46: // Forward 158 | return this.handleForwardSpace(event); 159 | case 27: // Esc 160 | return this.handleEscape(event); 161 | case 38: // Left 162 | case 40: // Right 163 | return this.handleArrows(event); 164 | default: 165 | break; 166 | } 167 | }, 168 | handleChange(event) { 169 | let value = this.props.value; 170 | let newValue = this.input.value; 171 | let diff = newValue.length - value.length; 172 | let end = caret.start(this.input); 173 | let insertion; 174 | let start = end - Math.abs(diff); 175 | event.preventDefault(); 176 | if (diff > 0) { 177 | insertion = newValue.slice(end - diff, end); 178 | while (diff--) { 179 | let oldChar = value.charAt(start); 180 | let newChar = insertion.charAt(0); 181 | if (isSeparator(oldChar)) { 182 | if (isSeparator(newChar)) { 183 | insertion = insertion.slice(1); 184 | start++; 185 | } else { 186 | start++; 187 | diff++; 188 | end++; 189 | } 190 | } else { 191 | value = replaceCharAt(value, start, newChar); 192 | insertion = insertion.slice(1); 193 | start++; 194 | } 195 | } 196 | newValue = value; 197 | } else { 198 | if (newValue.charAt(start) === ':') start++; 199 | // apply default to selection 200 | let result = value; 201 | for (var i = start; i < end; i++) { 202 | result = replaceCharAt(result, i, newValue.charAt(i)); 203 | } 204 | newValue = result; 205 | } 206 | if (validate(newValue)) { 207 | if (newValue.charAt(end) === ':') end++; 208 | this.onChange(newValue, end); 209 | } else { 210 | var caretIndex = this.props.value.length - (newValue.length - end); 211 | if (this.mounted) this.setState({ caretIndex: caretIndex }); 212 | } 213 | }, 214 | onChange(str, caretIndex) { 215 | if (this.props.onChange) this.props.onChange(this.format(str)); 216 | if (this.mounted && typeof caretIndex === 'number') 217 | this.setState({ caretIndex: caretIndex }); 218 | } 219 | }); 220 | 221 | export default TimeInput; 222 | -------------------------------------------------------------------------------- /src/lib/caret.js: -------------------------------------------------------------------------------- 1 | export default { 2 | start: el => el.selectionStart, 3 | end: el => el.selectionEnd, 4 | set: (el, start, end) => { 5 | el.setSelectionRange(start, end || start); 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /src/lib/get-base.js: -------------------------------------------------------------------------------- 1 | // getBase 2 | export default (groupId, twelveHourTime) => { 3 | if (!groupId) return twelveHourTime ? 12 : 24; 4 | if (groupId < 3) return 60; 5 | return 1000; 6 | }; 7 | -------------------------------------------------------------------------------- /src/lib/get-group-id.js: -------------------------------------------------------------------------------- 1 | export default index => { 2 | if (index < 3) return 0; 3 | if (index < 6) return 1; 4 | if (index < 9) return 2; 5 | if (index < 13) return 3; 6 | return 4; 7 | }; 8 | -------------------------------------------------------------------------------- /src/lib/get-groups.js: -------------------------------------------------------------------------------- 1 | export default str => str.split(/[:\s+]/); 2 | -------------------------------------------------------------------------------- /src/lib/is-twelve-hour-time.js: -------------------------------------------------------------------------------- 1 | // isTwelveHourTime 2 | export default groups => /[a-z]/i.test(groups[groups.length - 1]); 3 | -------------------------------------------------------------------------------- /src/lib/replace-char-at.js: -------------------------------------------------------------------------------- 1 | // replaceCharAt 2 | export default (str, index, replacement) => { 3 | str = str.split(''); 4 | str[index] = replacement; 5 | return str.join(''); 6 | }; 7 | -------------------------------------------------------------------------------- /src/lib/stringify.js: -------------------------------------------------------------------------------- 1 | import isTwelveHourTime from './is-twelve-hour-time'; 2 | // stringify 3 | export default groups => { 4 | if (isTwelveHourTime(groups)) 5 | return groups.slice(0, -1).join(':') + ' ' + groups[groups.length - 1]; 6 | return groups.join(':'); 7 | }; 8 | -------------------------------------------------------------------------------- /src/lib/time-string-adder.js: -------------------------------------------------------------------------------- 1 | import zeroPad from './zero-pad'; 2 | import getGroups from './get-groups'; 3 | import getBase from './get-base'; 4 | import stringify from './stringify'; 5 | import toggle24Hr from './toggle-24-hour'; 6 | import isTwelveHourTime from './is-twelve-hour-time'; 7 | 8 | const add = (groups, groupId, amount, twelveHourTime) => { 9 | var base = getBase(groupId, twelveHourTime); 10 | if (!groupId && groups[groupId] === '12' && twelveHourTime) 11 | groups[groupId] = '00'; 12 | var val = Number(groups[groupId]) + amount; 13 | groups = replace(groups, groupId, (val + base) % base); 14 | if (groupId && val >= base) 15 | return add(groups, groupId - 1, 1, twelveHourTime); 16 | if (groupId && val < 0) return add(groups, groupId - 1, -1, twelveHourTime); 17 | if (!groupId && twelveHourTime) { 18 | if (val >= base || val < 0) toggle24Hr(groups); 19 | if (groups[0] === '00') groups[0] = '12'; 20 | } 21 | return groups; 22 | }; 23 | 24 | const replace = (groups, groupId, amount) => { 25 | var digits = groups[groupId].length; 26 | groups[groupId] = zeroPad(String(amount), digits); 27 | return groups; 28 | }; 29 | 30 | // export default function adder(str, groupId, amount) { 31 | export default (str, groupId, amount) => { 32 | var groups = getGroups(str); 33 | var twelveHourTime = isTwelveHourTime(groups); 34 | if (twelveHourTime && groupId === groups.length - 1) 35 | return stringify(toggle24Hr(groups)); 36 | return stringify(add(groups, groupId, amount, twelveHourTime)); 37 | }; 38 | -------------------------------------------------------------------------------- /src/lib/toggle-24-hour.js: -------------------------------------------------------------------------------- 1 | // toggle24Hr 2 | export default groups => { 3 | var m = groups[groups.length - 1].toUpperCase(); 4 | groups[groups.length - 1] = m === 'AM' ? 'PM' : 'AM'; 5 | return groups; 6 | }; 7 | -------------------------------------------------------------------------------- /src/lib/validate.js: -------------------------------------------------------------------------------- 1 | // validate 2 | export default val => 3 | /^[0-2][0-9]:[0-5][0-9](:[0-5][0-9](:[0-9][0-9][0-9])?)?(\s+[ap]m)?$/i.test( 4 | val 5 | ); 6 | -------------------------------------------------------------------------------- /src/lib/zero-pad.js: -------------------------------------------------------------------------------- 1 | // zeroPad 2 | export default (val, digits) => { 3 | while (val.length < digits) val = '0' + val; 4 | return val; 5 | }; 6 | -------------------------------------------------------------------------------- /test/arrow-keys.test.js: -------------------------------------------------------------------------------- 1 | /* global describe it */ 2 | import ReactTestUtils from 'react-dom/test-utils'; 3 | import caret from '../src/lib/caret'; 4 | import render from './lib/renderTimeInput'; 5 | 6 | describe('up', function() { 7 | var timeInput; 8 | it('should increment group caret is in and preserve the caret position', function() { 9 | [0, 1, 2].forEach(n => { 10 | timeInput = arrow('00:00:00:000', n, true); 11 | expect(timeInput.input.value).toEqual('01:00:00:000'); 12 | expect(caret.start(timeInput.input)).toEqual(n); 13 | }); 14 | [3, 4, 5].forEach(n => { 15 | timeInput = arrow('00:15:00:000', n, true); 16 | expect(timeInput.input.value).toEqual('00:16:00:000'); 17 | expect(caret.start(timeInput.input)).toEqual(n); 18 | }); 19 | [6, 7, 8].forEach(n => { 20 | timeInput = arrow('00:00:50:000', n, true); 21 | expect(timeInput.input.value).toEqual('00:00:51:000'); 22 | expect(caret.start(timeInput.input)).toEqual(n); 23 | }); 24 | [9, 10, 11].forEach(n => { 25 | timeInput = arrow('00:00:00:100', n, true); 26 | expect(timeInput.input.value).toEqual('00:00:00:101'); 27 | expect(caret.start(timeInput.input)).toEqual(n); 28 | }); 29 | }); 30 | it('should go back to zero when incrementing max value', function() { 31 | timeInput = arrow('23:00:00:000', 1, true); 32 | expect(timeInput.input.value).toEqual('00:00:00:000'); 33 | expect(caret.start(timeInput.input)).toEqual(1); 34 | }); 35 | it('should increment previous group when max value exceeded', function() { 36 | timeInput = arrow('00:59:00:000', 3, true); 37 | expect(timeInput.input.value).toEqual('01:00:00:000'); 38 | timeInput = arrow('00:00:59:000', 6, true); 39 | expect(timeInput.input.value).toEqual('00:01:00:000'); 40 | timeInput = arrow('00:00:00:999', 9, true); 41 | expect(timeInput.input.value).toEqual('00:00:01:000'); 42 | }); 43 | it('should increment 2x more when shift key is pressed', function() { 44 | expect(arrow('00:00:00:000', 0, true, true).input.value).toEqual( 45 | '02:00:00:000' 46 | ); 47 | expect(arrow('00:15:00:000', 3, true, true).input.value).toEqual( 48 | '00:17:00:000' 49 | ); 50 | expect(arrow('00:00:40:000', 6, true, true).input.value).toEqual( 51 | '00:00:42:000' 52 | ); 53 | expect(arrow('00:00:00:100', 9, true, true).input.value).toEqual( 54 | '00:00:00:102' 55 | ); 56 | }); 57 | it('should increment 4x more when shift key is pressed', function() { 58 | expect(arrow('00:00:00:100', 9, true, true, true).input.value).toEqual( 59 | '00:00:00:104' 60 | ); 61 | }); 62 | it('should toggle AM/PM when carat is over AM/PM', function() { 63 | expect(arrow('01:00 AM', 6, true).input.value).toEqual('01:00 PM'); 64 | expect(arrow('01:00:00:000 PM', 13, true).input.value).toEqual( 65 | '01:00:00:000 AM' 66 | ); 67 | }); 68 | }); 69 | 70 | describe('down', function() { 71 | var timeInput; 72 | it('should decriment group caret is in and preserve the caret position', function() { 73 | [0, 1, 2].forEach(n => { 74 | timeInput = arrow('01:00:00:000', n); 75 | expect(timeInput.input.value).toEqual('00:00:00:000'); 76 | expect(caret.start(timeInput.input)).toEqual(n); 77 | }); 78 | [3, 4, 5].forEach(n => { 79 | timeInput = arrow('00:15:00:000', n); 80 | expect(timeInput.input.value).toEqual('00:14:00:000'); 81 | expect(caret.start(timeInput.input)).toEqual(n); 82 | }); 83 | [6, 7, 8].forEach(n => { 84 | timeInput = arrow('00:00:50:000', n); 85 | expect(timeInput.input.value).toEqual('00:00:49:000'); 86 | expect(caret.start(timeInput.input)).toEqual(n); 87 | }); 88 | [9, 10, 11].forEach(n => { 89 | timeInput = arrow('00:00:00:100', n); 90 | expect(timeInput.input.value).toEqual('00:00:00:099'); 91 | expect(caret.start(timeInput.input)).toEqual(n); 92 | }); 93 | }); 94 | it('should go back when decrimenting max value', function() { 95 | timeInput = arrow('00:00:00:000', 1); 96 | expect(timeInput.input.value).toEqual('23:00:00:000'); 97 | expect(caret.start(timeInput.input)).toEqual(1); 98 | }); 99 | it('should increment previous group when max value exceeded', function() { 100 | timeInput = arrow('01:00:00:000', 3); 101 | expect(timeInput.input.value).toEqual('00:59:00:000'); 102 | timeInput = arrow('00:01:00:000', 6); 103 | expect(timeInput.input.value).toEqual('00:00:59:000'); 104 | timeInput = arrow('00:00:01:000', 9); 105 | expect(timeInput.input.value).toEqual('00:00:00:999'); 106 | }); 107 | it('should decrement 2x more when shift key is pressed', function() { 108 | expect(arrow('02:00:00:000', 0, false, true).input.value).toEqual( 109 | '00:00:00:000' 110 | ); 111 | expect(arrow('00:17:00:000', 3, false, true).input.value).toEqual( 112 | '00:15:00:000' 113 | ); 114 | expect(arrow('00:00:42:000', 6, false, true).input.value).toEqual( 115 | '00:00:40:000' 116 | ); 117 | expect(arrow('00:00:00:102', 9, false, true).input.value).toEqual( 118 | '00:00:00:100' 119 | ); 120 | }); 121 | it('should decrement 4x more when shift and meta keys are pressed', function() { 122 | expect(arrow('00:00:00:104', 9, false, true, true).input.value).toEqual( 123 | '00:00:00:100' 124 | ); 125 | }); 126 | it('should toggle AM/PM when carat is over AM/PM', function() { 127 | expect(arrow('01:00 AM', 6, false).input.value).toEqual('01:00 PM'); 128 | expect(arrow('01:00:00:000 PM', 13, false).input.value).toEqual( 129 | '01:00:00:000 AM' 130 | ); 131 | }); 132 | }); 133 | 134 | function arrow(value, caretIndex, up, shiftKey, metaKey) { 135 | document.body.innerHTML = '
'; 136 | var timeInput = render(value); 137 | caret.set(timeInput.input, caretIndex); 138 | ReactTestUtils.Simulate.keyDown(timeInput.input, { 139 | keyCode: up ? 38 : 40, 140 | which: up ? 38 : 40, 141 | shiftKey: shiftKey, 142 | metaKey: metaKey 143 | }); 144 | return timeInput; 145 | } 146 | -------------------------------------------------------------------------------- /test/change.test.js: -------------------------------------------------------------------------------- 1 | /* global describe it */ 2 | import ReactTestUtils from 'react-dom/test-utils'; 3 | import caret from '../src/lib/caret'; 4 | import render from './lib/renderTimeInput'; 5 | 6 | describe('change', function() { 7 | var timeInput; 8 | 9 | it('should replace char to right of caret with typed character and move caret right', function() { 10 | timeInput = typeSomething('00:00', '100:00', 1); 11 | expect(timeInput.input.value).toEqual('10:00'); 12 | expect(caret.start(timeInput.input)).toEqual(1); 13 | }); 14 | 15 | it('should skip carat over preceeding ":"', function() { 16 | timeInput = typeSomething('00:00', '010:00', 2); 17 | expect(timeInput.input.value).toEqual('01:00'); 18 | expect(caret.start(timeInput.input)).toEqual(3); 19 | }); 20 | 21 | it('should skip over a subsequent ":"', function() { 22 | timeInput = typeSomething('00:00', '001:00', 3); 23 | expect(timeInput.input.value).toEqual('00:10'); 24 | expect(caret.start(timeInput.input)).toEqual(4); 25 | }); 26 | 27 | it('should not be affeced by a preceeding ":"', function() { 28 | timeInput = typeSomething('00:00', '00:100', 4); 29 | expect(timeInput.input.value).toEqual('00:10'); 30 | expect(caret.start(timeInput.input)).toEqual(4); 31 | }); 32 | 33 | it('should convert 00 to 12 if in 12hr mode', function() { 34 | timeInput = typeSomething('00:00 PM', '00:100 PM', 4); 35 | expect(timeInput.input.value).toEqual('12:10 PM'); 36 | expect(caret.start(timeInput.input)).toEqual(4); 37 | }); 38 | 39 | it('should allow user to change AM/PM', function() { 40 | timeInput = typeSomething('00:00 PM', '12:00 APM', 7); 41 | expect(timeInput.input.value).toEqual('12:00 AM'); 42 | expect(caret.start(timeInput.input)).toEqual(7); 43 | }); 44 | 45 | it('should not error if onchange prop is not passed in', function() { 46 | timeInput = typeSomething('00:00', '00:00', 1, true); 47 | expect(timeInput.input.value).toEqual('00:00'); 48 | expect(caret.start(timeInput.input)).toEqual(1); 49 | }); 50 | 51 | it('should accept input when entire text is selected', function() { 52 | timeInput = typeSomething('00:00', '11', 2); 53 | expect(timeInput.input.value).toEqual('11:00'); 54 | expect(caret.start(timeInput.input)).toEqual(3); 55 | }); 56 | 57 | it('should accept input when entire groups are selected', function() { 58 | timeInput = typeSomething('00:00', '1:00', 1); 59 | expect(timeInput.input.value).toEqual('10:00'); 60 | expect(caret.start(timeInput.input)).toEqual(1); 61 | timeInput = typeSomething('00:00', '00:1', 4); 62 | expect(timeInput.input.value).toEqual('00:10'); 63 | expect(caret.start(timeInput.input)).toEqual(4); 64 | timeInput = typeSomething('00:00:00:000', '00:00:00:1', 10); 65 | expect(timeInput.input.value).toEqual('00:00:00:100'); 66 | expect(caret.start(timeInput.input)).toEqual(10); 67 | }); 68 | 69 | it.skip('should accept input when entire groups are pasted over', function() { 70 | timeInput = typeSomething('00:00:00:000', '00:00:00:11', 10); 71 | expect(timeInput.input.value).toEqual('00:00:00:110'); 72 | expect(caret.start(timeInput.input)).toEqual(10); 73 | timeInput = typeSomething('00:00:00:000', '11:000', 2); 74 | expect(timeInput.input.value).toEqual('11:00:00:000'); 75 | expect(caret.start(timeInput.input)).toEqual(2); 76 | timeInput = typeSomething('00:00:00:000', '00:11', 2); 77 | expect(timeInput.input.value).toEqual('00:11:00:000'); 78 | expect(caret.start(timeInput.input)).toEqual(2); 79 | }); 80 | 81 | it('should not change if value is invalid', function() { 82 | timeInput = typeSomething('00:00', 'foobar', 0, true); 83 | expect(timeInput.input.value).toEqual('00:00'); 84 | expect(caret.start(timeInput.input)).toEqual(0); 85 | }); 86 | }); 87 | 88 | function typeSomething(oldValue, newValue, caretIndex, omitOnChange) { 89 | document.body.innerHTML = '
'; 90 | var timeInput = render(oldValue, null, omitOnChange); 91 | timeInput.input.value = newValue; 92 | caret.set(timeInput.input, caretIndex); 93 | ReactTestUtils.Simulate.change(timeInput.input); 94 | return timeInput; 95 | } 96 | -------------------------------------------------------------------------------- /test/classnames.test.js: -------------------------------------------------------------------------------- 1 | /* global describe it */ 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | import TimeInput from '../src/TimeInput'; 5 | 6 | describe('classnames', function() { 7 | it('should render any provided classnames additionally', function() { 8 | document.body.innerHTML = '
'; 9 | ReactDOM.render( 10 | , 11 | document.body.firstElementChild 12 | ); 13 | var el = document.body.getElementsByTagName('input')[0]; 14 | expect(el.parentElement.className).toEqual('TimeInput extra-1 extra-2'); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /test/delete.test.js: -------------------------------------------------------------------------------- 1 | /* global describe it */ 2 | import ReactTestUtils from 'react-dom/test-utils'; 3 | import caret from '../src/lib/caret'; 4 | import render from './lib/renderTimeInput'; 5 | 6 | describe('backspace', function() { 7 | var timeInput; 8 | 9 | it('should replace char to left of caret with defaultValue and move caret left', function() { 10 | timeInput = deleteSomething('11:11:11:111', false, 1); 11 | expect(timeInput.input.value).toEqual('01:11:11:111'); 12 | expect(caret.start(timeInput.input)).toEqual(0); 13 | }); 14 | 15 | it('should ignore ":"s to the left of caret', function() { 16 | timeInput = deleteSomething('11:11:11:111', false, 3); 17 | expect(timeInput.input.value).toEqual('10:11:11:111'); 18 | expect(caret.start(timeInput.input)).toEqual(1); 19 | }); 20 | 21 | describe('multiple', function() { 22 | it('should replace all selected multiple chars', function() { 23 | timeInput = deleteSomething('11:11:11:111', false, 4, 10); 24 | expect(timeInput.input.value).toEqual('11:10:00:011'); 25 | expect(caret.start(timeInput.input)).toEqual(4); 26 | }); 27 | }); 28 | }); 29 | 30 | describe('forward delete', function() { 31 | var timeInput; 32 | 33 | it('should replace char to right of caret with defaultValue and move caret right', function() { 34 | timeInput = deleteSomething('11:11:11:111', true, 0); 35 | expect(timeInput.input.value).toEqual('01:11:11:111'); 36 | expect(caret.start(timeInput.input)).toEqual(1); 37 | }); 38 | 39 | it('should ignore ":"s to the right of caret', function() { 40 | timeInput = deleteSomething('11:11:11:111', true, 2); 41 | expect(timeInput.input.value).toEqual('11:01:11:111'); 42 | expect(caret.start(timeInput.input)).toEqual(4); 43 | }); 44 | 45 | describe('multiple', function() { 46 | it('should replace all selected multiple chars', function() { 47 | timeInput = deleteSomething('11:11:11:111', true, 4, 10); 48 | expect(timeInput.input.value).toEqual('11:10:00:011'); 49 | expect(caret.start(timeInput.input)).toEqual(10); 50 | }); 51 | }); 52 | }); 53 | 54 | function deleteSomething(newValue, forward, start, end) { 55 | document.body.innerHTML = '
'; 56 | var timeInput = render('11:11:11:111'); 57 | timeInput.input.value = newValue; 58 | caret.set(timeInput.input, start, end); 59 | ReactTestUtils.Simulate.keyDown(timeInput.input, { 60 | keyCode: forward ? 46 : 8, 61 | which: forward ? 46 : 8 62 | }); 63 | return timeInput; 64 | } 65 | -------------------------------------------------------------------------------- /test/get-base.test.js: -------------------------------------------------------------------------------- 1 | /* global describe it */ 2 | import getBase from '../src/lib/get-base'; 3 | 4 | describe('getBase', function() { 5 | it('should return the correct base for group 0', function() { 6 | expect(getBase(0, true)).toEqual(12); 7 | expect(getBase(0, false)).toEqual(24); 8 | }); 9 | it('should return the correct base for all other groups', function() { 10 | [1, 2].forEach(checkBase); 11 | function checkBase(groupId) { 12 | expect(getBase(groupId)).toEqual(60); 13 | } 14 | expect(getBase(3)).toEqual(1000); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /test/get-group-id.test.js: -------------------------------------------------------------------------------- 1 | /* global describe it */ 2 | import getGroupId from '../src/lib/get-group-id'; 3 | 4 | describe('getGroupId', function() { 5 | it('should return the correct group index', function() { 6 | [[0, 1, 2], [3, 4, 5], [6, 7, 8], [9, 10, 11, 12], [14]].forEach(check); 7 | 8 | function check(indexes, groupId) { 9 | indexes.forEach(checkIndex); 10 | function checkIndex(index) { 11 | expect(getGroupId(index)).toEqual(groupId); 12 | } 13 | } 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /test/get-groups.test.js: -------------------------------------------------------------------------------- 1 | /* global describe it */ 2 | import getGroups from '../src/lib/get-groups'; 3 | 4 | describe('getGroups', function() { 5 | it('should correctly split 12 hour time strings', function() { 6 | expect(getGroups('01:02 PM')).toEqual(['01', '02', 'PM']); 7 | expect(getGroups('01:02 AM')).toEqual(['01', '02', 'AM']); 8 | expect(getGroups('01:02:03:004 PM')).toEqual([ 9 | '01', 10 | '02', 11 | '03', 12 | '004', 13 | 'PM' 14 | ]); 15 | expect(getGroups('01:02:03:004 AM')).toEqual([ 16 | '01', 17 | '02', 18 | '03', 19 | '004', 20 | 'AM' 21 | ]); 22 | }); 23 | it('should correctly split 24 hour time strings', function() { 24 | expect(getGroups('01:02')).toEqual(['01', '02']); 25 | expect(getGroups('01:02:03:004')).toEqual(['01', '02', '03', '004']); 26 | expect(getGroups('01:02:03:004')).toEqual(['01', '02', '03', '004']); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /test/is-twelve-hour-time.test.js: -------------------------------------------------------------------------------- 1 | /* global describe it */ 2 | import isTwelveHourTime from '../src/lib/is-twelve-hour-time'; 3 | 4 | describe('is12HourTime', function() { 5 | it('should return true for 12 hour time strings', function() { 6 | expect( 7 | ['00:00 PM', '00:00 AM', '00:00:00:000 PM', '00:00:00:000 AM'] 8 | .map(isTwelveHourTime) 9 | .reduce(alsoTrue, true) 10 | ).toEqual(true); 11 | }); 12 | it('should return false for 24 hour time strings', function() { 13 | expect( 14 | ['00:00', '00:00', '00:00:00:000', '00:00:00:000'] 15 | .map(isTwelveHourTime) 16 | .reduce(alsoFalse, true) 17 | ).toEqual(true); 18 | }); 19 | }); 20 | 21 | function alsoTrue(memo, val) { 22 | return memo && val; 23 | } 24 | 25 | function alsoFalse(memo, val) { 26 | return memo && !val; 27 | } 28 | -------------------------------------------------------------------------------- /test/lib/renderTimeInput.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import TimeInput from '../../src/TimeInput'; 4 | 5 | export default (value, el, omitOnChange) => { 6 | var timeInput = render(value); 7 | timeInput.input.focus(); 8 | return timeInput; 9 | function render(value) { 10 | return ReactDOM.render( 11 | , 16 | el || document.body.firstElementChild 17 | ); 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /test/replace-char-at.test.js: -------------------------------------------------------------------------------- 1 | /* global describe it */ 2 | import replaceCharAt from '../src/lib/replace-char-at'; 3 | 4 | describe('replaceCharAt', function() { 5 | it('should remove the char at the specified index', function() { 6 | expect(replaceCharAt('abcd', 0, 1)).toEqual('1bcd'); 7 | expect(replaceCharAt('abcd', 1, 1)).toEqual('a1cd'); 8 | expect(replaceCharAt('abcd', 2, 1)).toEqual('ab1d'); 9 | expect(replaceCharAt('abcd', 3, 1)).toEqual('abc1'); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /test/stringify.test.js: -------------------------------------------------------------------------------- 1 | /* global describe it */ 2 | import getGroups from '../src/lib/get-groups'; 3 | import stringify from '../src/lib/stringify'; 4 | 5 | describe('stringify', function() { 6 | it('should correctly stringify 12 hour time strings', function() { 7 | ['00:00 PM', '00:00 AM', '00:00:00:000 PM', '00:00:00:000 AM'].forEach( 8 | check 9 | ); 10 | 11 | function check(str) { 12 | expect(stringify(getGroups(str))).toEqual(str); 13 | } 14 | }); 15 | it('should correctly stringify 24 hour time strings', function() { 16 | ['00:00', '00:00', '00:00:00:000', '00:00:00:000'].forEach(check); 17 | 18 | function check(str) { 19 | expect(stringify(getGroups(str))).toEqual(str); 20 | } 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /test/tabbing.test.js: -------------------------------------------------------------------------------- 1 | /* global describe it */ 2 | import ReactTestUtils from 'react-dom/test-utils'; 3 | import caret from '../src/lib/caret'; 4 | import render from './lib/renderTimeInput'; 5 | 6 | describe('tabbing', function() { 7 | var timeInput; 8 | 9 | it('should move carat to the next input group', function() { 10 | timeInput = tab(0); 11 | expect(caret.start(timeInput.input)).toEqual(3); 12 | timeInput = tab(3); 13 | expect(caret.start(timeInput.input)).toEqual(6); 14 | timeInput = tab(6); 15 | expect(caret.start(timeInput.input)).toEqual(9); 16 | timeInput = tab(10); 17 | expect(caret.start(timeInput.input)).toEqual(13); 18 | }); 19 | 20 | it('should do nothing if already in last group', function() { 21 | timeInput = tab(13); 22 | expect(caret.start(timeInput.input)).toEqual(13); 23 | }); 24 | 25 | it('should forget the carat index on blur', function() { 26 | timeInput = tab(13); 27 | ReactTestUtils.Simulate.blur(timeInput.input); 28 | expect(timeInput.state.caretIndex).toEqual(null); 29 | }); 30 | 31 | it('should blur on escape', function() { 32 | timeInput = tab(0); 33 | ReactTestUtils.Simulate.keyDown(timeInput.input, { 34 | keyCode: 27, 35 | which: 27 36 | }); 37 | expect(timeInput.state.caretIndex).toEqual(null); 38 | }); 39 | 40 | describe('with shift', function() { 41 | it('should move carat to the previous input group', function() { 42 | timeInput = tab(3, true); 43 | expect(caret.start(timeInput.input)).toEqual(0); 44 | timeInput = tab(6, true); 45 | expect(caret.start(timeInput.input)).toEqual(3); 46 | timeInput = tab(13, true); 47 | expect(caret.start(timeInput.input)).toEqual(9); 48 | }); 49 | 50 | it('should do nothing if already in first group', function() { 51 | timeInput = tab(0, true); 52 | expect(caret.start(timeInput.input)).toEqual(0); 53 | }); 54 | }); 55 | }); 56 | 57 | function tab(caretIndex, shift) { 58 | document.body.innerHTML = '
'; 59 | var timeInput = render('11:11:11:111 AM'); 60 | caret.set(timeInput.input, caretIndex); 61 | ReactTestUtils.Simulate.keyDown(timeInput.input, { 62 | keyCode: 9, 63 | which: 9, 64 | shiftKey: shift 65 | }); 66 | return timeInput; 67 | } 68 | -------------------------------------------------------------------------------- /test/time-string-adder.test.js: -------------------------------------------------------------------------------- 1 | /* global describe it */ 2 | import adder from '../src/lib/time-string-adder'; 3 | 4 | describe('adder', function() { 5 | it('should add correctly to each group', function() { 6 | expect(adder('00:00', 0, 1)).toEqual('01:00'); 7 | expect(adder('00:00', 1, 2)).toEqual('00:02'); 8 | expect(adder('00:00:00', 2, 3)).toEqual('00:00:03'); 9 | expect(adder('00:00:00:000', 3, 4)).toEqual('00:00:00:004'); 10 | }); 11 | it('should subtract correctly from each group', function() { 12 | expect(adder('01:00', 0, -1)).toEqual('00:00'); 13 | expect(adder('00:02', 1, -2)).toEqual('00:00'); 14 | expect(adder('00:00:03', 2, -3)).toEqual('00:00:00'); 15 | expect(adder('00:00:00:004', 3, -4)).toEqual('00:00:00:000'); 16 | }); 17 | it('should overflow to preceeding group', function() { 18 | expect(adder('00:59', 1, 1)).toEqual('01:00'); 19 | expect(adder('00:00:59', 2, 1)).toEqual('00:01:00'); 20 | expect(adder('00:00:00:999', 3, 1)).toEqual('00:00:01:000'); 21 | expect(adder('12:00', 0, 1)).toEqual('13:00'); 22 | expect(adder('12:00 AM', 0, 1)).toEqual('01:00 AM'); 23 | }); 24 | it('should underflow to preceeding group', function() { 25 | expect(adder('01:00', 1, -1)).toEqual('00:59'); 26 | expect(adder('00:01:00', 2, -1)).toEqual('00:00:59'); 27 | expect(adder('00:00:01:00', 3, -1)).toEqual('00:00:00:999'); 28 | }); 29 | it('should toggle AM/PM when overflowing group zero', function() { 30 | expect(adder('11:59 PM', 1, 1)).toEqual('12:00 AM'); 31 | expect(adder('12:00 AM', 1, -1)).toEqual('11:59 PM'); 32 | expect(adder('11:59 AM', 1, 1)).toEqual('12:00 PM'); 33 | expect(adder('12:00 PM', 1, -1)).toEqual('11:59 AM'); 34 | }); 35 | it('should not toggle AM/PM when not overflowing group zero', function() { 36 | expect(adder('10:59 PM', 1, 1)).toEqual('11:00 PM'); 37 | expect(adder('11:00 PM', 1, -1)).toEqual('10:59 PM'); 38 | expect(adder('10:59 AM', 1, 1)).toEqual('11:00 AM'); 39 | expect(adder('11:00 AM', 1, -1)).toEqual('10:59 AM'); 40 | }); 41 | it('should toggle 24hr/12hr', function() { 42 | expect(adder('00:00 PM', 2)).toEqual('00:00 AM'); 43 | expect(adder('00:00:00 PM', 3)).toEqual('00:00:00 AM'); 44 | expect(adder('00:00:00:000 PM', 4)).toEqual('00:00:00:000 AM'); 45 | expect(adder('00:00 AM', 2)).toEqual('00:00 PM'); 46 | expect(adder('00:00:00 AM', 3)).toEqual('00:00:00 PM'); 47 | expect(adder('00:00:00:000 AM', 4)).toEqual('00:00:00:000 PM'); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /test/toggle-24-hour.test.js: -------------------------------------------------------------------------------- 1 | /* global describe it */ 2 | import toggle24Hr from '../src/lib/toggle-24-hour'; 3 | 4 | describe('toggle24Hr', function() { 5 | it('should correctly stringify 12 hour time strings', function() { 6 | expect(toggle24Hr(['01', '02', 'AM'])).toEqual(['01', '02', 'PM']); 7 | expect(toggle24Hr(['01', '02', 'PM'])).toEqual(['01', '02', 'AM']); 8 | expect(toggle24Hr(['01', '02', '03', '004', 'AM'])).toEqual([ 9 | '01', 10 | '02', 11 | '03', 12 | '004', 13 | 'PM' 14 | ]); 15 | expect(toggle24Hr(['01', '02', '03', '004', 'PM'])).toEqual([ 16 | '01', 17 | '02', 18 | '03', 19 | '004', 20 | 'AM' 21 | ]); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /test/zero-pad.test.js: -------------------------------------------------------------------------------- 1 | /* global describe it */ 2 | import pad from '../src/lib/zero-pad'; 3 | 4 | describe('zeroPad', function() { 5 | it('should zero pad a number, given n digits', function() { 6 | expect(pad('1', 2)).toEqual('01'); 7 | expect(pad('1', 3)).toEqual('001'); 8 | expect(pad('01', 4)).toEqual('0001'); 9 | expect(pad('001', 5)).toEqual('00001'); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /webpack.commonjs.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | module.exports = { 5 | devtool: 'source-map', 6 | entry: './src/TimeInput.js', 7 | output: { 8 | path: path.join(__dirname, 'dist'), 9 | filename: 'commonjs.js', 10 | library: 'TimeInput', 11 | libraryTarget: 'commonjs' 12 | }, 13 | plugins: [ 14 | new webpack.DefinePlugin({ 15 | 'process.env.NODE_ENV': JSON.stringify('production') 16 | }), 17 | new webpack.NoEmitOnErrorsPlugin() 18 | ], 19 | module: { 20 | loaders: [ 21 | { 22 | test: /\.js$/, 23 | loader: 'babel-loader', 24 | include: __dirname, 25 | exclude: /node_modules/ 26 | } 27 | ] 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | // var HtmlWebpackPlugin = require('html-webpack-plugin'); 5 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 6 | const HtmlWebpackPluginConfig = new HtmlWebpackPlugin({ 7 | template: './demo/demo.html', 8 | filename: 'index.html', 9 | inject: 'body' 10 | }); 11 | 12 | module.exports = { 13 | devtool: 'source-map', 14 | entry: './demo/index.js', 15 | output: { 16 | path: path.join(__dirname, 'docs'), 17 | filename: 'bundle.js' 18 | }, 19 | plugins: [ 20 | HtmlWebpackPluginConfig, 21 | new webpack.HotModuleReplacementPlugin(), 22 | new webpack.NoEmitOnErrorsPlugin() 23 | ], 24 | module: { 25 | loaders: [ 26 | { 27 | test: /\.js$/, 28 | loader: 'babel-loader', 29 | include: __dirname, 30 | exclude: /node_modules/ 31 | }, 32 | { 33 | test: /\.css?$/, 34 | loaders: ['style-loader', 'raw-loader'], 35 | include: __dirname, 36 | exclude: /node_modules/ 37 | }, 38 | { 39 | test: /\.(png|jpg|gif)$/, 40 | use: [ 41 | { 42 | loader: 'file-loader', 43 | options: {} 44 | } 45 | ] 46 | } 47 | ] 48 | } 49 | }; 50 | --------------------------------------------------------------------------------