├── .circleci └── config.yml ├── .eslintrc.js ├── .fatherrc.js ├── .gitignore ├── .travis.yml ├── HISTORY.md ├── LICENSE.md ├── README.md ├── assets ├── index.less └── index │ ├── Header.less │ ├── Panel.less │ ├── Picker.less │ └── Select.less ├── examples ├── disabled.js ├── format.js ├── hidden.js ├── open.js ├── pick-time.js ├── step.js ├── twelve-hours.js └── value-and-defaultValue.js ├── index.d.ts ├── index.js ├── jest.config.js ├── now.json ├── package.json ├── src ├── Combobox.jsx ├── Header.jsx ├── Panel.jsx ├── Select.jsx ├── TimePicker.jsx ├── index.js └── placements.js └── tests ├── Header.spec.jsx ├── Select.spec.jsx ├── TimePicker.spec.jsx ├── __snapshots__ └── TimePicker.spec.jsx.snap ├── index.js └── util.js /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | lint: 4 | docker: 5 | - image: circleci/node:latest 6 | steps: 7 | - checkout 8 | - restore_cache: 9 | keys: 10 | - v1-dependencies-{{ checksum "package.json" }} 11 | - run: npm install 12 | - save_cache: 13 | paths: 14 | - node_modules 15 | key: v1-dependencies-{{ checksum "package.json" }} 16 | - run: npm run lint 17 | test: 18 | docker: 19 | - image: circleci/node:latest 20 | working_directory: ~/repo 21 | steps: 22 | - checkout 23 | - restore_cache: 24 | keys: 25 | - v1-dependencies-{{ checksum "package.json" }} 26 | - run: npm install 27 | - save_cache: 28 | paths: 29 | - node_modules 30 | key: v1-dependencies-{{ checksum "package.json" }} 31 | - run: npm test -- --coverage 32 | workflows: 33 | version: 2 34 | build_and_test: 35 | jobs: 36 | - lint 37 | - test 38 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports= require('@umijs/fabric/dist/eslint'); 2 | -------------------------------------------------------------------------------- /.fatherrc.js: -------------------------------------------------------------------------------- 1 | export default { 2 | cjs: 'babel', 3 | esm: { type: 'babel', importLibToEs: true }, 4 | preCommit: { 5 | eslint: true, 6 | prettier: true, 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | *.log 3 | .idea/ 4 | .ipr 5 | .iws 6 | *~ 7 | ~* 8 | *.diff 9 | *.patch 10 | *.bak 11 | .DS_Store 12 | Thumbs.db 13 | .project 14 | .*proj 15 | .svn/ 16 | *.swp 17 | *.swo 18 | *.pyc 19 | *.pyo 20 | .build 21 | node_modules 22 | .cache 23 | dist 24 | assets/**/*.css 25 | build 26 | lib 27 | coverage 28 | .vscode 29 | yarn.lock 30 | es/ 31 | package-lock.json 32 | src/*.map 33 | .prettierrc 34 | tslint.json 35 | tsconfig.test.json 36 | .prettierignore 37 | .storybook 38 | storybook/index.js 39 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - 10 5 | 6 | script: 7 | - | 8 | if [ "$TEST_TYPE" = coverage ]; then 9 | npm run coverage && \ 10 | bash <(curl -s https://codecov.io/bash) 11 | else 12 | npm run $TEST_TYPE 13 | fi 14 | env: 15 | matrix: 16 | - TEST_TYPE=lint 17 | - TEST_TYPE=test 18 | - TEST_TYPE=coverage 19 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | # History 2 | 3 | 3.7.2 / 2019-08-28 4 | --------------------------- 5 | 6 | - Fix React lifecycle wanring. #163 7 | 8 | 3.7.0 / 2019-06-13 9 | --------------------------- 10 | 11 | - Improve accessibility. #153 12 | 13 | 3.6.0 14 | --------------------------- 15 | 16 | - Refacotr dom structure. 17 | 18 | 3.5.0 / 2018-12-23 19 | --------------------------- 20 | 21 | - Add `popupStyle` 22 | - Add `onAmPmChange` 23 | - Add Typescript definition 24 | 25 | 3.4.0 / 2018-08-13 26 | --------------------------- 27 | 28 | - Add `inputIcon` 29 | 30 | 3.3.0 / 2018-02-08 31 | --------------------------- 32 | 33 | - Add `inputReadOnly` 34 | 35 | 3.2.0 / 2017-11-15 36 | --------------------------- 37 | 38 | - Add `blur()` and `autoFocus`. 39 | 40 | 41 | 3.1.0 / 2017-11-02 42 | --------------------------- 43 | 44 | - Upgrade to `rc-trigger@2.x`. 45 | 46 | 47 | 3.0.0 / 2017-10-22 48 | --------------------------- 49 | 50 | - Support React 16. 51 | - Add `onKeydown`. 52 | - Add `focusOnOpen`. 53 | - Add `hourStep` `minuteStep` `secondStep`. 54 | - Fix disabled style. 55 | - Use `this.xxx` to replace `this.refs.xxx`. 56 | 57 | 2.4.0 / 2017-05-02 58 | --------------------------- 59 | 60 | Add `popupClassName` prop. 61 | 62 | 2.3.0 / 2017-03-08 63 | --------------------------- 64 | 65 | Add `use12Hours` prop. 66 | 67 | 2.2.0 / 2016-11-11 68 | --------------------------- 69 | 70 | Add `showMinute` prop. 71 | 72 | 2.1.0 / 2016-10-25 73 | --------------------------- 74 | 75 | Add `addon` prop. 76 | 77 | 2.0.0 / 2016-08-04 78 | --------------------------- 79 | 80 | goodbye gregorian-calendar, hello moment 81 | 82 | 1.1.0 / 2016-01-14 83 | --------------------------- 84 | 85 | remove gregorianCalendarLocale prop, move to locale.calendar 86 | 87 | 1.0.0 / 2015-12-21 88 | ------------------------- 89 | 90 | release! 91 | 92 | 1.0.0-alpha9 / 2015-12-16 93 | ------------------ 94 | 95 | `fixed` update bugs when value empty. 96 | 97 | 1.0.0-alpha7 / 2015-12-12 98 | ------------------ 99 | 100 | `new` add options `disabledHours`, `disabledMinutes`, `disabledSeconds` and `hideDisabledOptions`. 101 | `remove` remove options `hourOptions`, `minuteOptions` and `secondOptions`. 102 | 103 | 1.0.0-alpha2 / 2015-12-03 104 | ------------------ 105 | 106 | `fixed` IE8 compatible. 107 | `new` add test users. 108 | 109 | 0.7.1 / 2015-11-20 110 | ------------------ 111 | 112 | `fixed` change value to null when clear input content to remove the react warning. 113 | 114 | 0.7.0 / 2015-11-20 115 | ------------------ 116 | 117 | `update` change the className of panel and its container. 118 | 119 | 0.5.6 / 2015-11-19 120 | ------------------ 121 | 122 | `fixed` use another method to change time and fix the bug about value.getTime(). 123 | 124 | 0.5.4 / 2015-11-19 125 | ------------------ 126 | 127 | `update` change value prop to defaultValue. 128 | 129 | 0.5.2 / 2015-11-19 130 | ------------------ 131 | 132 | `update` renew placements config. 133 | 134 | 0.5.1 / 2015-11-19 135 | ------------------ 136 | 137 | `update` change the className of select panel container. 138 | 139 | 0.5.0 / 2015-11-19 140 | ------------------ 141 | 142 | `update` clear input content and close select panel when click [x] on select panel. 143 | 144 | `new` can custom input className now. 145 | 146 | 147 | 0.4.0 / 2015-11-18 148 | ------------------ 149 | 150 | `update` clear input content when click [x] on select panel. 151 | 152 | 0.3.3 / 2015-11-17 153 | ------------------ 154 | 155 | `fixed` fix some bugs. 156 | 157 | 0.3.0 / 2015-11-17 158 | ------------------ 159 | 160 | `update` remove TimePanel and merge it to TimePicker. 161 | 162 | 0.2.0 / 2015-11-16 163 | ------------------ 164 | 165 | `update` rename the component, update document. 166 | 167 | 0.1.0 / 2015-11-12 168 | ------------------ 169 | 170 | `new` [#305](https://github.com/ant-design/ant-design/issues/305#issuecomment-147027817) release 0.1.0 ([@wuzhao](https://github.com/wuzhao)\) 171 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-present yiminghe 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TimePicker 2 | 3 | React Time Picker Control. 4 | 5 | [![NPM version][npm-image]][npm-url] 6 | [![build status][circleci-image]][circleci-url] 7 | [![Test coverage][coveralls-image]][coveralls-url] 8 | [![Dependencies](https://img.shields.io/david/react-component/time-picker.svg?style=flat-square)](https://david-dm.org/react-component/time-picker) 9 | [![DevDependencies](https://img.shields.io/david/dev/react-component/time-picker.svg?style=flat-square)](https://david-dm.org/react-component/time-picker?type=dev) 10 | [![npm download][download-image]][download-url] 11 | [![Storybook](https://gw.alipayobjects.com/mdn/ob_info/afts/img/A*CQXNTZfK1vwAAAAAAAAAAABjAQAAAQ/original)](https://github.com/react-component/time-picker) 12 | 13 | [Storybook]: https://github.com/storybooks/press/blob/master/badges/storybook.svg 14 | [npm-image]: http://img.shields.io/npm/v/rc-time-picker.svg?style=flat-square 15 | [npm-url]: http://npmjs.org/package/rc-time-picker 16 | [circleci-image]: https://img.shields.io/circleci/react-component/time-picker.svg?style=flat-square 17 | [circleci-url]: https://circleci.com/gh/react-component/time-picker 18 | [coveralls-image]: https://img.shields.io/coveralls/react-component/time-picker.svg?style=flat-square 19 | [coveralls-url]: https://coveralls.io/r/react-component/time-picker?branch=maste 20 | [node-image]: https://img.shields.io/badge/node.js-%3E=_0.10-green.svg?style=flat-square 21 | [node-url]: http://nodejs.org/download/ 22 | [download-image]: https://img.shields.io/npm/dm/rc-time-picker.svg?style=flat-square 23 | [download-url]: https://npmjs.org/package/rc-time-picker 24 | 25 | example 26 | -------- 27 | 28 | http://react-component.github.io/time-picker/ 29 | 30 | install 31 | ------- 32 | 33 | ``` 34 | npm install rc-time-picker 35 | ``` 36 | 37 | Usage 38 | ----- 39 | 40 | ``` 41 | import TimePicker from 'rc-time-picker'; 42 | import ReactDOM from 'react-dom'; 43 | import 'rc-time-picker/assets/index.css'; 44 | ReactDOM.render(, container); 45 | ``` 46 | 47 | API 48 | --- 49 | 50 | ### TimePicker 51 | 52 | | Name | Type | Default | Description | 53 | |-------------------------|-----------------------------------|---------|-------------| 54 | | prefixCls | String | 'rc-time-picker' | prefixCls of this component | 55 | | clearText | String | 'clear' | clear tooltip of icon | 56 | | disabled | Boolean | false | whether picker is disabled | 57 | | allowEmpty | Boolean | true | allow clearing text | 58 | | open | Boolean | false | current open state of picker. controlled prop | 59 | | defaultValue | moment | null | default initial value | 60 | | defaultOpenValue | moment | moment() | default open panel value, used to set utcOffset,locale if value/defaultValue absent | 61 | | value | moment | null | current value | 62 | | placeholder | String | '' | time input's placeholder | 63 | | className | String | '' | time picker className | 64 | | inputClassName | String | '' | time picker input element className | 65 | | id | String | '' | time picker id | 66 | | popupClassName | String | '' | time panel className | 67 | | popupStyle | object | {} | customize popup style 68 | | showHour | Boolean | true | whether show hour | | 69 | | showMinute | Boolean | true | whether show minute | 70 | | showSecond | Boolean | true | whether show second | 71 | | format | String | - | moment format | 72 | | disabledHours | Function | - | disabled hour options | 73 | | disabledMinutes | Function | - | disabled minute options | 74 | | disabledSeconds | Function | - | disabled second options | 75 | | use12Hours | Boolean | false | 12 hours display mode | 76 | | hideDisabledOptions | Boolean | false | whether hide disabled options | 77 | | onChange | Function | null | called when time-picker a different value | 78 | | onAmPmChange | Function | null | called when time-picker an am/pm value | 79 | | addon | Function | - | called from timepicker panel to render some addon to its bottom, like an OK button. Receives panel instance as parameter, to be able to close it like `panel.close()`.| 80 | | placement | String | bottomLeft | one of ['topLeft', 'topRight', 'bottomLeft', 'bottomRight'] | 81 | | transitionName | String | '' | | 82 | | name | String | - | sets the name of the generated input | 83 | | onOpen | Function({ open }) | | when TimePicker panel is opened | 84 | | onClose | Function({ open }) | | when TimePicker panel is closed | 85 | | hourStep | Number | 1 | interval between hours in picker | 86 | | minuteStep | Number | 1 | interval between minutes in picker | 87 | | secondStep | Number | 1 | interval between seconds in picker | 88 | | focusOnOpen | Boolean | false | automatically focus the input when the picker opens | 89 | | inputReadOnly | Boolean | false | set input to read only | 90 | | inputIcon | ReactNode | | specific the time-picker icon. | 91 | | clearIcon | ReactNode | | specific the clear icon. | 92 | 93 | ## Test Case 94 | 95 | ``` 96 | npm test 97 | ``` 98 | 99 | ## Coverage 100 | 101 | ``` 102 | npm run coverage 103 | ``` 104 | 105 | open coverage/ dir 106 | 107 | License 108 | ------- 109 | 110 | rc-time-picker is released under the MIT license. 111 | -------------------------------------------------------------------------------- /assets/index.less: -------------------------------------------------------------------------------- 1 | @prefixClass: rc-time-picker; 2 | 3 | .@{prefixClass} { 4 | display: inline-block; 5 | position: relative; 6 | box-sizing: border-box; 7 | * { 8 | box-sizing: border-box; 9 | } 10 | 11 | &-clear { 12 | position: absolute; 13 | right: 6px; 14 | cursor: pointer; 15 | overflow: hidden; 16 | width: 20px; 17 | height: 20px; 18 | text-align: center; 19 | line-height: 20px; 20 | top: 3px; 21 | margin: 0; 22 | 23 | &-icon:after { 24 | content: "x"; 25 | font-size: 12px; 26 | font-style: normal; 27 | color: #aaa; 28 | display: inline-block; 29 | line-height: 1; 30 | height: 20px; 31 | width: 20px; 32 | transition: color 0.3s ease; 33 | } 34 | 35 | &-icon:hover:after { 36 | color: #666; 37 | } 38 | } 39 | } 40 | 41 | @import "./index/Picker"; 42 | @import "./index/Panel"; 43 | @import "./index/Header"; 44 | @import "./index/Select"; 45 | -------------------------------------------------------------------------------- /assets/index/Header.less: -------------------------------------------------------------------------------- 1 | .@{prefixClass}-panel { 2 | &-input { 3 | margin: 0; 4 | padding: 0; 5 | width: 100%; 6 | cursor: auto; 7 | line-height: 1.5; 8 | outline: 0; 9 | border: 1px solid transparent; 10 | 11 | &-wrap { 12 | box-sizing: border-box; 13 | position: relative; 14 | padding: 6px; 15 | border-bottom: 1px solid #e9e9e9; 16 | } 17 | 18 | &-invalid { 19 | border-color: red; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /assets/index/Panel.less: -------------------------------------------------------------------------------- 1 | .@{prefixClass}-panel { 2 | z-index: 1070; 3 | width: 170px; 4 | position: absolute; 5 | box-sizing: border-box; 6 | 7 | * { 8 | box-sizing: border-box; 9 | } 10 | 11 | &-inner { 12 | display: inline-block; 13 | position: relative; 14 | outline: none; 15 | list-style: none; 16 | font-size: 12px; 17 | text-align: left; 18 | background-color: #fff; 19 | border-radius: 4px; 20 | box-shadow: 0 1px 5px #ccc; 21 | background-clip: padding-box; 22 | border: 1px solid #ccc; 23 | line-height: 1.5; 24 | } 25 | 26 | &-narrow { 27 | max-width: 113px; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /assets/index/Picker.less: -------------------------------------------------------------------------------- 1 | .@{prefixClass} { 2 | &-input { 3 | width: 100%; 4 | position: relative; 5 | display: inline-block; 6 | padding: 4px 7px; 7 | height: 28px; 8 | cursor: text; 9 | font-size: 12px; 10 | line-height: 1.5; 11 | color: #666; 12 | background-color: #fff; 13 | background-image: none; 14 | border: 1px solid #d9d9d9; 15 | border-radius: 4px; 16 | transition: border .2s cubic-bezier(0.645, 0.045, 0.355, 1), background .2s cubic-bezier(0.645, 0.045, 0.355, 1), box-shadow .2s cubic-bezier(0.645, 0.045, 0.355, 1); 17 | &[disabled] { 18 | color: #ccc; 19 | background: #f7f7f7; 20 | cursor: not-allowed; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /assets/index/Select.less: -------------------------------------------------------------------------------- 1 | .@{prefixClass}-panel-select { 2 | float: left; 3 | font-size: 12px; 4 | border: 1px solid #e9e9e9; 5 | border-width: 0 1px; 6 | margin-left: -1px; 7 | box-sizing: border-box; 8 | width: 56px; 9 | max-height: 144px; 10 | overflow-y: auto; 11 | position: relative; // Fix chrome weird render bug 12 | 13 | &-active { 14 | overflow-y: auto; 15 | } 16 | 17 | &:first-child { 18 | border-left: 0; 19 | margin-left: 0; 20 | } 21 | 22 | &:last-child { 23 | border-right: 0; 24 | } 25 | 26 | ul { 27 | list-style: none; 28 | box-sizing: border-box; 29 | margin: 0; 30 | padding: 0; 31 | width: 100%; 32 | } 33 | 34 | li { 35 | list-style: none; 36 | margin: 0; 37 | padding: 0 0 0 16px; 38 | width: 100%; 39 | height: 24px; 40 | line-height: 24px; 41 | text-align: left; 42 | cursor: pointer; 43 | user-select: none; 44 | 45 | &:hover { 46 | background: #edfaff; 47 | } 48 | } 49 | 50 | li&-option-selected { 51 | background: #f7f7f7; 52 | font-weight: bold; 53 | } 54 | 55 | li&-option-disabled { 56 | color: #ccc; 57 | &:hover { 58 | background: transparent; 59 | cursor: not-allowed; 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /examples/disabled.js: -------------------------------------------------------------------------------- 1 | /* eslint no-console:0 */ 2 | 3 | import '../assets/index.less'; 4 | import React from 'react'; 5 | import moment from 'moment'; 6 | import TimePicker from '..'; 7 | 8 | const showSecond = true; 9 | const str = showSecond ? 'HH:mm:ss' : 'HH:mm'; 10 | 11 | const now = moment() 12 | .hour(14) 13 | .minute(30); 14 | 15 | function generateOptions(length, excludedOptions) { 16 | const arr = []; 17 | for (let value = 0; value < length; value += 1) { 18 | if (excludedOptions.indexOf(value) < 0) { 19 | arr.push(value); 20 | } 21 | } 22 | return arr; 23 | } 24 | 25 | function onChange(value) { 26 | console.log(value && value.format(str)); 27 | } 28 | 29 | function disabledHours() { 30 | return [0, 1, 2, 3, 4, 5, 6, 7, 8, 22, 23]; 31 | } 32 | 33 | function disabledMinutes(h) { 34 | switch (h) { 35 | case 9: 36 | return generateOptions(60, [30]); 37 | case 21: 38 | return generateOptions(60, [0]); 39 | default: 40 | return generateOptions(60, [0, 30]); 41 | } 42 | } 43 | 44 | function disabledSeconds(h, m) { 45 | return [h + (m % 60)]; 46 | } 47 | 48 | const App = () => ( 49 | <> 50 |

Disabled picker

51 | 52 |

Disabled options

53 | 62 | 63 | ); 64 | 65 | export default App; 66 | -------------------------------------------------------------------------------- /examples/format.js: -------------------------------------------------------------------------------- 1 | import '../assets/index.less'; 2 | import React from 'react'; 3 | import moment from 'moment'; 4 | import TimePicker from '..'; 5 | 6 | const App = () => ( 7 | <> 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | 18 | export default App; 19 | -------------------------------------------------------------------------------- /examples/hidden.js: -------------------------------------------------------------------------------- 1 | /* eslint no-console:0 */ 2 | import '../assets/index.less'; 3 | import React from 'react'; 4 | import moment from 'moment'; 5 | import TimePicker from '..'; 6 | 7 | const showSecond = true; 8 | const str = showSecond ? 'HH:mm:ss' : 'HH:mm'; 9 | 10 | function onChange(value) { 11 | console.log(value && value.format(str)); 12 | } 13 | 14 | const App = () => ( 15 | [0, 1, 2, 3, 4, 5, 6, 7, 8, 22, 23]} 23 | disabledMinutes={() => [0, 2, 4, 6, 8]} 24 | hideDisabledOptions 25 | /> 26 | ); 27 | 28 | export default App; 29 | -------------------------------------------------------------------------------- /examples/open.js: -------------------------------------------------------------------------------- 1 | /* eslint no-console:0 */ 2 | import '../assets/index.less'; 3 | import React from 'react'; 4 | import moment from 'moment'; 5 | import TimePicker from '..'; 6 | 7 | const iconStyle = { 8 | position: 'absolute', 9 | width: '24px', 10 | right: 0, 11 | top: 0, 12 | bottom: 0, 13 | display: 'flex', 14 | alignItems: 'center', 15 | justifyContent: 'center', 16 | }; 17 | 18 | const starPath = 19 | 'M908.1 353.1l-253.9-36.9L540.7 86.1c-3' + 20 | '.1-6.3-8.2-11.4-14.5-14.5-15.8-7.8-35-1.3-42.9 14.5L3' + 21 | '69.8 316.2l-253.9 36.9c-7 1-13.4 4.3-18.3 9.3-12.3 12' + 22 | '.7-12.1 32.9 0.6 45.3l183.7 179.1-43.4 252.9c-1.2 6.9' + 23 | '-0.1 14.1 3.2 20.3 8.2 15.6 27.6 21.7 43.2 13.4L512 7' + 24 | '54l227.1 119.4c6.2 3.3 13.4 4.4 20.3 3.2 17.4-3 29.1-' + 25 | '19.5 26.1-36.9l-43.4-252.9 183.7-179.1c5-4.9 8.3-11.3' + 26 | ' 9.3-18.3 2.7-17.5-9.5-33.7-27-36.3zM664.8 561.6l36.1' + 27 | ' 210.3L512 672.7 323.1 772l36.1-210.3-152.8-149L417.6' + 28 | ' 382 512 190.7 606.4 382l211.2 30.7-152.8 148.9z'; 29 | 30 | const redoPath = 31 | 'M758.2 839.1C851.8 765.9 912 651.9 912' + 32 | ' 523.9 912 303 733.5 124.3 512.6 124 291.4 123.7 112 ' + 33 | '302.8 112 523.9c0 125.2 57.5 236.9 147.6 310.2 3.5 2.' + 34 | '8 8.6 2.2 11.4-1.3l39.4-50.5c2.7-3.4 2.1-8.3-1.2-11.1' + 35 | '-8.1-6.6-15.9-13.7-23.4-21.2-29.4-29.4-52.5-63.6-68.6' + 36 | '-101.7C200.4 609 192 567.1 192 523.9s8.4-85.1 25.1-12' + 37 | '4.5c16.1-38.1 39.2-72.3 68.6-101.7 29.4-29.4 63.6-52.' + 38 | '5 101.7-68.6C426.9 212.4 468.8 204 512 204s85.1 8.4 1' + 39 | '24.5 25.1c38.1 16.1 72.3 39.2 101.7 68.6 29.4 29.4 52' + 40 | '.5 63.6 68.6 101.7 16.7 39.4 25.1 81.3 25.1 124.5s-8.' + 41 | '4 85.1-25.1 124.5c-16.1 38.1-39.2 72.3-68.6 101.7-9.3' + 42 | ' 9.3-19.1 18-29.3 26L668.2 724c-4.1-5.3-12.5-3.5-14.1' + 43 | ' 3l-39.6 162.2c-1.2 5 2.6 9.9 7.7 9.9l167 0.8c6.7 0 1' + 44 | '0.5-7.7 6.3-12.9l-37.3-47.9z'; 45 | 46 | class App extends React.Component { 47 | state = { 48 | open: false, 49 | useIcon: false, 50 | }; 51 | 52 | getIcon = (path, style = {}) => ( 53 | 65 | 72 | 73 | 74 | 75 | ); 76 | 77 | setOpen = ({ open }) => { 78 | this.setState({ open }); 79 | }; 80 | 81 | toggleOpen = () => { 82 | const { open } = this.state; 83 | this.setState({ 84 | open: !open, 85 | }); 86 | }; 87 | 88 | toggleIcon = () => { 89 | const { useIcon } = this.state; 90 | this.setState({ 91 | useIcon: !useIcon, 92 | }); 93 | }; 94 | 95 | render() { 96 | const inputIcon = this.getIcon(starPath, iconStyle); 97 | const { useIcon, open } = this.state; 98 | const clearIcon = this.getIcon(redoPath, { ...iconStyle, right: 20 }); 99 | return ( 100 |
101 | 104 | 107 | 119 |
120 | ); 121 | } 122 | } 123 | 124 | export default App; 125 | -------------------------------------------------------------------------------- /examples/pick-time.js: -------------------------------------------------------------------------------- 1 | /* eslint no-console:0 */ 2 | import '../assets/index.less'; 3 | import React from 'react'; 4 | import moment from 'moment'; 5 | import TimePicker from '..'; 6 | 7 | const showSecond = true; 8 | const str = showSecond ? 'HH:mm:ss' : 'HH:mm'; 9 | 10 | function onChange(value) { 11 | console.log(value && value.format(str)); 12 | } 13 | 14 | const App = () => ( 15 | 22 | ); 23 | 24 | export default App; 25 | -------------------------------------------------------------------------------- /examples/step.js: -------------------------------------------------------------------------------- 1 | import '../assets/index.less'; 2 | import React from 'react'; 3 | import moment from 'moment'; 4 | import TimePicker from '..'; 5 | 6 | const App = () => ; 7 | 8 | export default App; 9 | -------------------------------------------------------------------------------- /examples/twelve-hours.js: -------------------------------------------------------------------------------- 1 | /* eslint no-console:0 */ 2 | import '../assets/index.less'; 3 | import React from 'react'; 4 | import moment from 'moment'; 5 | import TimePicker from '..'; 6 | 7 | const format = 'h:mm a'; 8 | 9 | const now = moment() 10 | .hour(0) 11 | .minute(0); 12 | 13 | function onChange(value) { 14 | console.log(value && value.format(format)); 15 | } 16 | 17 | const App = () => ( 18 | 27 | ); 28 | 29 | export default App; 30 | -------------------------------------------------------------------------------- /examples/value-and-defaultValue.js: -------------------------------------------------------------------------------- 1 | /* eslint no-console:0 */ 2 | import '../assets/index.less'; 3 | import React from 'react'; 4 | import moment from 'moment'; 5 | import TimePicker from '..'; 6 | 7 | class App extends React.Component { 8 | state = { 9 | value: moment(), 10 | }; 11 | 12 | handleValueChange = value => { 13 | console.log(value && value.format('HH:mm:ss')); 14 | this.setState({ value }); 15 | }; 16 | 17 | clear = () => { 18 | this.setState({ 19 | value: undefined, 20 | }); 21 | }; 22 | 23 | render() { 24 | const { value } = this.state; 25 | return ( 26 |
27 | 28 | 29 | 32 |
33 | ); 34 | } 35 | } 36 | 37 | export default App; 38 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | declare module "rc-time-picker" { 2 | import { Moment } from "moment"; 3 | import * as React from "react"; 4 | 5 | type TimePickerProps = { 6 | prefixCls?: string; 7 | clearText?: string; 8 | disabled?: boolean; 9 | allowEmpty?: boolean; 10 | open?: boolean; 11 | defaultValue?: Moment; 12 | defaultOpenValue?: Moment; 13 | value?: Moment; 14 | placeholder?: string; 15 | className?: string; 16 | inputClassName?: string; 17 | id?: string; 18 | popupClassName?: string; 19 | popupStyle?: any; 20 | showHour?: boolean; 21 | showMinute?: boolean; 22 | showSecond?: boolean; 23 | format?: string; 24 | disabledHours?: () => number[]; 25 | disabledMinutes?: (hour: number) => number[]; 26 | disabledSeconds?: (hour: number, minute: number) => number[]; 27 | use12Hours?: boolean; 28 | hideDisabledOptions?: boolean; 29 | onChange?: (newValue: Moment | null) => void; 30 | onAmPmChange?: (ampm: 'PM' | 'AM') => void; 31 | addon?: (instance: typeof Panel) => React.ReactNode; 32 | placement?: string; 33 | transitionName?: string; 34 | name?: string; 35 | autoComplete?: string; 36 | onFocus?: (event: React.FocusEvent) => void; 37 | onBlur?: (event: React.FocusEvent) => void; 38 | onKeyDown?: (event: React.KeyboardEvent) => void; 39 | autoFocus?: boolean; 40 | onOpen?: (newState: { open: true }) => void; 41 | onClose?: (newState: { open: false }) => void; 42 | hourStep?: number; 43 | minuteStep?: number; 44 | secondStep?: number; 45 | focusOnOpen?: boolean; 46 | inputReadOnly?: boolean; 47 | inputIcon?: React.ReactNode; 48 | clearIcon?: React.ReactNode; 49 | getPopupContainer?: React.ReactNode; 50 | }; 51 | export default class TimePicker extends React.Component { 52 | focus(): void; 53 | blur(): void; 54 | } 55 | class Panel extends React.Component { 56 | close(): void; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | export { default } from './src/'; 2 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | snapshotSerializers: [require.resolve('enzyme-to-json/serializer')], 3 | }; 4 | -------------------------------------------------------------------------------- /now.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "name": "rc-time-picker", 4 | "builds": [ 5 | { 6 | "src": "package.json", 7 | "use": "@now/static-build", 8 | "config": { "distDir": ".doc" } 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rc-time-picker", 3 | "version": "4.0.0-alpha.3", 4 | "description": "React TimePicker", 5 | "keywords": [ 6 | "react", 7 | "react-time-picker", 8 | "react-component", 9 | "timepicker", 10 | "time-picker", 11 | "ui component", 12 | "ui", 13 | "component" 14 | ], 15 | "files": [ 16 | "lib", 17 | "es", 18 | "assets/*.css", 19 | "assets/*.less", 20 | "index.d.ts" 21 | ], 22 | "main": "lib/index", 23 | "module": "es/index", 24 | "homepage": "http://github.com/react-component/time-picker", 25 | "author": "wuzhao.mail@gmail.com", 26 | "repository": { 27 | "type": "git", 28 | "url": "git@github.com:react-component/time-picker.git" 29 | }, 30 | "bugs": { 31 | "url": "http://github.com/react-component/time-picker/issues" 32 | }, 33 | "license": "MIT", 34 | "dependencies": { 35 | "classnames": "2.x", 36 | "moment": "2.x", 37 | "raf": "^3.4.1", 38 | "rc-trigger": "^4.0.0-alpha.8" 39 | }, 40 | "devDependencies": { 41 | "cross-env": "^7.0.0", 42 | "enzyme": "^3.8.0", 43 | "enzyme-to-json": "^3.4.0", 44 | "father": "^2.24.1", 45 | "np": "^6.0.0", 46 | "rc-util": "^5.1.0" 47 | }, 48 | "peerDependencies": { 49 | "react": "^16.0.0", 50 | "react-dom": "^16.0.0" 51 | }, 52 | "scripts": { 53 | "start": "cross-env NODE_ENV=development father doc dev --storybook", 54 | "build": "father doc build --storybook", 55 | "compile": "father build && lessc assets/index.less assets/index.css", 56 | "gh-pages": "npm run build && father doc deploy", 57 | "prepublishOnly": "npm run compile && np --yolo --no-publish", 58 | "lint": "eslint src/ examples/ --ext .tsx,.ts,.jsx,.js", 59 | "test": "father test", 60 | "coverage": "father test --coverage", 61 | "now-build": "npm run build" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Combobox.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Select from './Select'; 3 | 4 | const formatOption = (option, disabledOptions) => { 5 | let value = `${option}`; 6 | if (option < 10) { 7 | value = `0${option}`; 8 | } 9 | 10 | let disabled = false; 11 | if (disabledOptions && disabledOptions.indexOf(option) >= 0) { 12 | disabled = true; 13 | } 14 | 15 | return { 16 | value, 17 | disabled, 18 | }; 19 | }; 20 | 21 | class Combobox extends Component { 22 | onItemChange = (type, itemValue) => { 23 | const { 24 | onChange, 25 | defaultOpenValue, 26 | use12Hours, 27 | value: propValue, 28 | isAM, 29 | onAmPmChange, 30 | } = this.props; 31 | const value = (propValue || defaultOpenValue).clone(); 32 | 33 | if (type === 'hour') { 34 | if (use12Hours) { 35 | if (isAM) { 36 | value.hour(+itemValue % 12); 37 | } else { 38 | value.hour((+itemValue % 12) + 12); 39 | } 40 | } else { 41 | value.hour(+itemValue); 42 | } 43 | } else if (type === 'minute') { 44 | value.minute(+itemValue); 45 | } else if (type === 'ampm') { 46 | const ampm = itemValue.toUpperCase(); 47 | if (use12Hours) { 48 | if (ampm === 'PM' && value.hour() < 12) { 49 | value.hour((value.hour() % 12) + 12); 50 | } 51 | 52 | if (ampm === 'AM') { 53 | if (value.hour() >= 12) { 54 | value.hour(value.hour() - 12); 55 | } 56 | } 57 | } 58 | onAmPmChange(ampm); 59 | } else { 60 | value.second(+itemValue); 61 | } 62 | onChange(value); 63 | }; 64 | 65 | onEnterSelectPanel = range => { 66 | const { onCurrentSelectPanelChange } = this.props; 67 | onCurrentSelectPanelChange(range); 68 | }; 69 | 70 | getHourSelect(hour) { 71 | const { prefixCls, hourOptions, disabledHours, showHour, use12Hours, onEsc } = this.props; 72 | if (!showHour) { 73 | return null; 74 | } 75 | const disabledOptions = disabledHours(); 76 | let hourOptionsAdj; 77 | let hourAdj; 78 | if (use12Hours) { 79 | hourOptionsAdj = [12].concat(hourOptions.filter(h => h < 12 && h > 0)); 80 | hourAdj = hour % 12 || 12; 81 | } else { 82 | hourOptionsAdj = hourOptions; 83 | hourAdj = hour; 84 | } 85 | 86 | return ( 87 | formatOption(option, disabledOptions))} 119 | selectedIndex={minuteOptions.indexOf(minute)} 120 | type="minute" 121 | onSelect={this.onItemChange} 122 | onMouseEnter={() => this.onEnterSelectPanel('minute')} 123 | onEsc={onEsc} 124 | /> 125 | ); 126 | } 127 | 128 | getSecondSelect(second) { 129 | const { 130 | prefixCls, 131 | secondOptions, 132 | disabledSeconds, 133 | showSecond, 134 | defaultOpenValue, 135 | value: propValue, 136 | onEsc, 137 | } = this.props; 138 | if (!showSecond) { 139 | return null; 140 | } 141 | const value = propValue || defaultOpenValue; 142 | const disabledOptions = disabledSeconds(value.hour(), value.minute()); 143 | 144 | return ( 145 | this.onEnterSelectPanel('ampm')} 177 | onEsc={onEsc} 178 | /> 179 | ); 180 | } 181 | 182 | render() { 183 | const { prefixCls, defaultOpenValue, value: propValue } = this.props; 184 | const value = propValue || defaultOpenValue; 185 | return ( 186 |
187 | {this.getHourSelect(value.hour())} 188 | {this.getMinuteSelect(value.minute())} 189 | {this.getSecondSelect(value.second())} 190 | {this.getAMPMSelect(value.hour())} 191 |
192 | ); 193 | } 194 | } 195 | 196 | export default Combobox; 197 | -------------------------------------------------------------------------------- /src/Header.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import moment from 'moment'; 3 | import classNames from 'classnames'; 4 | 5 | class Header extends Component { 6 | static defaultProps = { 7 | inputReadOnly: false, 8 | }; 9 | 10 | constructor(props) { 11 | super(props); 12 | const { value, format } = props; 13 | this.state = { 14 | str: (value && value.format(format)) || '', 15 | invalid: false, 16 | }; 17 | } 18 | 19 | componentDidMount() { 20 | const { focusOnOpen } = this.props; 21 | if (focusOnOpen) { 22 | // requestAnimationFrame will cause jump on rc-trigger 3.x 23 | // https://github.com/ant-design/ant-design/pull/19698#issuecomment-552889571 24 | // use setTimeout can resolve it 25 | // 60ms is a magic timeout to avoid focusing before dropdown reposition correctly 26 | this.timeout = setTimeout(() => { 27 | this.refInput.focus(); 28 | this.refInput.select(); 29 | }, 60); 30 | } 31 | } 32 | 33 | componentDidUpdate(prevProps) { 34 | const { value, format } = this.props; 35 | if (value !== prevProps.value) { 36 | // eslint-disable-next-line react/no-did-update-set-state 37 | this.setState({ 38 | str: (value && value.format(format)) || '', 39 | invalid: false, 40 | }); 41 | } 42 | } 43 | 44 | componentWillUnmount() { 45 | if (this.timeout) { 46 | clearTimeout(this.timeout); 47 | } 48 | } 49 | 50 | onInputChange = event => { 51 | const str = event.target.value; 52 | this.setState({ 53 | str, 54 | }); 55 | const { 56 | format, 57 | hourOptions, 58 | minuteOptions, 59 | secondOptions, 60 | disabledHours, 61 | disabledMinutes, 62 | disabledSeconds, 63 | onChange, 64 | } = this.props; 65 | 66 | if (str) { 67 | const { value: originalValue } = this.props; 68 | const value = this.getProtoValue().clone(); 69 | const parsed = moment(str, format, true); 70 | if (!parsed.isValid()) { 71 | this.setState({ 72 | invalid: true, 73 | }); 74 | return; 75 | } 76 | value 77 | .hour(parsed.hour()) 78 | .minute(parsed.minute()) 79 | .second(parsed.second()); 80 | 81 | // if time value not allowed, response warning. 82 | if ( 83 | hourOptions.indexOf(value.hour()) < 0 || 84 | minuteOptions.indexOf(value.minute()) < 0 || 85 | secondOptions.indexOf(value.second()) < 0 86 | ) { 87 | this.setState({ 88 | invalid: true, 89 | }); 90 | return; 91 | } 92 | 93 | // if time value is disabled, response warning. 94 | const disabledHourOptions = disabledHours(); 95 | const disabledMinuteOptions = disabledMinutes(value.hour()); 96 | const disabledSecondOptions = disabledSeconds(value.hour(), value.minute()); 97 | if ( 98 | (disabledHourOptions && disabledHourOptions.indexOf(value.hour()) >= 0) || 99 | (disabledMinuteOptions && disabledMinuteOptions.indexOf(value.minute()) >= 0) || 100 | (disabledSecondOptions && disabledSecondOptions.indexOf(value.second()) >= 0) 101 | ) { 102 | this.setState({ 103 | invalid: true, 104 | }); 105 | return; 106 | } 107 | 108 | if (originalValue) { 109 | if ( 110 | originalValue.hour() !== value.hour() || 111 | originalValue.minute() !== value.minute() || 112 | originalValue.second() !== value.second() 113 | ) { 114 | // keep other fields for rc-calendar 115 | const changedValue = originalValue.clone(); 116 | changedValue.hour(value.hour()); 117 | changedValue.minute(value.minute()); 118 | changedValue.second(value.second()); 119 | onChange(changedValue); 120 | } 121 | } else if (originalValue !== value) { 122 | onChange(value); 123 | } 124 | } else { 125 | onChange(null); 126 | } 127 | 128 | this.setState({ 129 | invalid: false, 130 | }); 131 | }; 132 | 133 | onKeyDown = e => { 134 | const { onEsc, onKeyDown } = this.props; 135 | if (e.keyCode === 27) { 136 | onEsc(); 137 | } 138 | 139 | onKeyDown(e); 140 | }; 141 | 142 | getProtoValue() { 143 | const { value, defaultOpenValue } = this.props; 144 | return value || defaultOpenValue; 145 | } 146 | 147 | getInput() { 148 | const { prefixCls, placeholder, inputReadOnly } = this.props; 149 | const { invalid, str } = this.state; 150 | const invalidClass = invalid ? `${prefixCls}-input-invalid` : ''; 151 | return ( 152 | { 155 | this.refInput = ref; 156 | }} 157 | onKeyDown={this.onKeyDown} 158 | value={str} 159 | placeholder={placeholder} 160 | onChange={this.onInputChange} 161 | readOnly={!!inputReadOnly} 162 | /> 163 | ); 164 | } 165 | 166 | render() { 167 | const { prefixCls } = this.props; 168 | return
{this.getInput()}
; 169 | } 170 | } 171 | 172 | export default Header; 173 | -------------------------------------------------------------------------------- /src/Panel.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import moment from 'moment'; 3 | import classNames from 'classnames'; 4 | import Header from './Header'; 5 | import Combobox from './Combobox'; 6 | 7 | function noop() {} 8 | 9 | function generateOptions(length, disabledOptions, hideDisabledOptions, step = 1) { 10 | const arr = []; 11 | for (let value = 0; value < length; value += step) { 12 | if (!disabledOptions || disabledOptions.indexOf(value) < 0 || !hideDisabledOptions) { 13 | arr.push(value); 14 | } 15 | } 16 | return arr; 17 | } 18 | 19 | function toNearestValidTime(time, hourOptions, minuteOptions, secondOptions) { 20 | const hour = hourOptions 21 | .slice() 22 | .sort((a, b) => Math.abs(time.hour() - a) - Math.abs(time.hour() - b))[0]; 23 | const minute = minuteOptions 24 | .slice() 25 | .sort((a, b) => Math.abs(time.minute() - a) - Math.abs(time.minute() - b))[0]; 26 | const second = secondOptions 27 | .slice() 28 | .sort((a, b) => Math.abs(time.second() - a) - Math.abs(time.second() - b))[0]; 29 | return moment(`${hour}:${minute}:${second}`, 'HH:mm:ss'); 30 | } 31 | 32 | class Panel extends Component { 33 | static defaultProps = { 34 | prefixCls: 'rc-time-picker-panel', 35 | onChange: noop, 36 | disabledHours: noop, 37 | disabledMinutes: noop, 38 | disabledSeconds: noop, 39 | defaultOpenValue: moment(), 40 | use12Hours: false, 41 | addon: noop, 42 | onKeyDown: noop, 43 | onAmPmChange: noop, 44 | inputReadOnly: false, 45 | }; 46 | 47 | state = {}; 48 | 49 | static getDerivedStateFromProps(props, state) { 50 | if ('value' in props) { 51 | return { 52 | ...state, 53 | value: props.value, 54 | }; 55 | } 56 | return null; 57 | } 58 | 59 | onChange = newValue => { 60 | const { onChange } = this.props; 61 | this.setState({ value: newValue }); 62 | onChange(newValue); 63 | }; 64 | 65 | onAmPmChange = ampm => { 66 | const { onAmPmChange } = this.props; 67 | onAmPmChange(ampm); 68 | }; 69 | 70 | onCurrentSelectPanelChange = currentSelectPanel => { 71 | this.setState({ currentSelectPanel }); 72 | }; 73 | 74 | disabledHours = () => { 75 | const { use12Hours, disabledHours } = this.props; 76 | let disabledOptions = disabledHours(); 77 | if (use12Hours && Array.isArray(disabledOptions)) { 78 | if (this.isAM()) { 79 | disabledOptions = disabledOptions.filter(h => h < 12).map(h => (h === 0 ? 12 : h)); 80 | } else { 81 | disabledOptions = disabledOptions.map(h => (h === 12 ? 12 : h - 12)); 82 | } 83 | } 84 | return disabledOptions; 85 | }; 86 | 87 | // https://github.com/ant-design/ant-design/issues/5829 88 | close() { 89 | const { onEsc } = this.props; 90 | onEsc(); 91 | } 92 | 93 | isAM() { 94 | const { defaultOpenValue } = this.props; 95 | const { value } = this.state; 96 | const realValue = value || defaultOpenValue; 97 | return realValue.hour() >= 0 && realValue.hour() < 12; 98 | } 99 | 100 | render() { 101 | const { 102 | prefixCls, 103 | className, 104 | placeholder, 105 | disabledMinutes, 106 | disabledSeconds, 107 | hideDisabledOptions, 108 | showHour, 109 | showMinute, 110 | showSecond, 111 | format, 112 | defaultOpenValue, 113 | clearText, 114 | onEsc, 115 | addon, 116 | use12Hours, 117 | focusOnOpen, 118 | onKeyDown, 119 | hourStep, 120 | minuteStep, 121 | secondStep, 122 | inputReadOnly, 123 | clearIcon, 124 | } = this.props; 125 | const { value, currentSelectPanel } = this.state; 126 | const disabledHourOptions = this.disabledHours(); 127 | const disabledMinuteOptions = disabledMinutes(value ? value.hour() : null); 128 | const disabledSecondOptions = disabledSeconds( 129 | value ? value.hour() : null, 130 | value ? value.minute() : null, 131 | ); 132 | const hourOptions = generateOptions(24, disabledHourOptions, hideDisabledOptions, hourStep); 133 | const minuteOptions = generateOptions( 134 | 60, 135 | disabledMinuteOptions, 136 | hideDisabledOptions, 137 | minuteStep, 138 | ); 139 | const secondOptions = generateOptions( 140 | 60, 141 | disabledSecondOptions, 142 | hideDisabledOptions, 143 | secondStep, 144 | ); 145 | 146 | const validDefaultOpenValue = toNearestValidTime( 147 | defaultOpenValue, 148 | hourOptions, 149 | minuteOptions, 150 | secondOptions, 151 | ); 152 | 153 | return ( 154 |
155 |
176 | 197 | {addon(this)} 198 |
199 | ); 200 | } 201 | } 202 | 203 | export default Panel; 204 | -------------------------------------------------------------------------------- /src/Select.jsx: -------------------------------------------------------------------------------- 1 | /* eslint jsx-a11y/no-noninteractive-element-to-interactive-role: 0 */ 2 | import React, { Component } from 'react'; 3 | import classNames from 'classnames'; 4 | import raf from 'raf'; 5 | 6 | const scrollTo = (element, to, duration) => { 7 | // jump to target if duration zero 8 | if (duration <= 0) { 9 | raf(() => { 10 | // eslint-disable-next-line no-param-reassign 11 | element.scrollTop = to; 12 | }); 13 | return; 14 | } 15 | const difference = to - element.scrollTop; 16 | const perTick = (difference / duration) * 10; 17 | 18 | raf(() => { 19 | // eslint-disable-next-line no-param-reassign 20 | element.scrollTop += perTick; 21 | if (element.scrollTop === to) return; 22 | scrollTo(element, to, duration - 10); 23 | }); 24 | }; 25 | 26 | class Select extends Component { 27 | state = { 28 | active: false, 29 | }; 30 | 31 | componentDidMount() { 32 | // jump to selected option 33 | this.scrollToSelected(0); 34 | } 35 | 36 | componentDidUpdate(prevProps) { 37 | const { selectedIndex } = this.props; 38 | // smooth scroll to selected option 39 | if (prevProps.selectedIndex !== selectedIndex) { 40 | this.scrollToSelected(120); 41 | } 42 | } 43 | 44 | onSelect = value => { 45 | const { onSelect, type } = this.props; 46 | onSelect(type, value); 47 | }; 48 | 49 | getOptions() { 50 | const { options, selectedIndex, prefixCls, onEsc } = this.props; 51 | return options.map((item, index) => { 52 | const cls = classNames({ 53 | [`${prefixCls}-select-option-selected`]: selectedIndex === index, 54 | [`${prefixCls}-select-option-disabled`]: item.disabled, 55 | }); 56 | const onClick = item.disabled 57 | ? undefined 58 | : () => { 59 | this.onSelect(item.value); 60 | }; 61 | const onKeyDown = e => { 62 | if (e.keyCode === 13) onClick(); 63 | else if (e.keyCode === 27) onEsc(); 64 | }; 65 | return ( 66 |
  • 75 | {item.value} 76 |
  • 77 | ); 78 | }); 79 | } 80 | 81 | handleMouseEnter = e => { 82 | const { onMouseEnter } = this.props; 83 | this.setState({ active: true }); 84 | onMouseEnter(e); 85 | }; 86 | 87 | handleMouseLeave = () => { 88 | this.setState({ active: false }); 89 | }; 90 | 91 | saveRoot = node => { 92 | this.root = node; 93 | }; 94 | 95 | saveList = node => { 96 | this.list = node; 97 | }; 98 | 99 | scrollToSelected(duration) { 100 | // move to selected item 101 | const { selectedIndex } = this.props; 102 | if (!this.list) { 103 | return; 104 | } 105 | let index = selectedIndex; 106 | if (index < 0) { 107 | index = 0; 108 | } 109 | const topOption = this.list.children[index]; 110 | const to = topOption.offsetTop; 111 | scrollTo(this.root, to, duration); 112 | } 113 | 114 | render() { 115 | const { prefixCls, options } = this.props; 116 | const { active } = this.state; 117 | if (options.length === 0) { 118 | return null; 119 | } 120 | const cls = classNames(`${prefixCls}-select`, { 121 | [`${prefixCls}-select-active`]: active, 122 | }); 123 | return ( 124 |
    130 |
      {this.getOptions()}
    131 |
    132 | ); 133 | } 134 | } 135 | 136 | export default Select; 137 | -------------------------------------------------------------------------------- /src/TimePicker.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Trigger from 'rc-trigger'; 3 | import moment from 'moment'; 4 | import classNames from 'classnames'; 5 | import Panel from './Panel'; 6 | import placements from './placements'; 7 | 8 | function noop() {} 9 | 10 | function refFn(field, component) { 11 | this[field] = component; 12 | } 13 | 14 | class Picker extends Component { 15 | static defaultProps = { 16 | clearText: 'clear', 17 | prefixCls: 'rc-time-picker', 18 | defaultOpen: false, 19 | inputReadOnly: false, 20 | style: {}, 21 | className: '', 22 | inputClassName: '', 23 | popupClassName: '', 24 | popupStyle: {}, 25 | align: {}, 26 | defaultOpenValue: moment(), 27 | allowEmpty: true, 28 | showHour: true, 29 | showMinute: true, 30 | showSecond: true, 31 | disabledHours: noop, 32 | disabledMinutes: noop, 33 | disabledSeconds: noop, 34 | hideDisabledOptions: false, 35 | placement: 'bottomLeft', 36 | onChange: noop, 37 | onAmPmChange: noop, 38 | onOpen: noop, 39 | onClose: noop, 40 | onFocus: noop, 41 | onBlur: noop, 42 | addon: noop, 43 | use12Hours: false, 44 | focusOnOpen: false, 45 | onKeyDown: noop, 46 | }; 47 | 48 | constructor(props) { 49 | super(props); 50 | this.saveInputRef = refFn.bind(this, 'picker'); 51 | this.savePanelRef = refFn.bind(this, 'panelInstance'); 52 | const { defaultOpen, defaultValue, open = defaultOpen, value = defaultValue } = props; 53 | this.state = { 54 | open, 55 | value, 56 | }; 57 | } 58 | 59 | static getDerivedStateFromProps(props, state) { 60 | const newState = {}; 61 | if ('value' in props) { 62 | newState.value = props.value; 63 | } 64 | if (props.open !== undefined) { 65 | newState.open = props.open; 66 | } 67 | return Object.keys(newState).length > 0 68 | ? { 69 | ...state, 70 | ...newState, 71 | } 72 | : null; 73 | } 74 | 75 | onPanelChange = value => { 76 | this.setValue(value); 77 | }; 78 | 79 | onAmPmChange = ampm => { 80 | const { onAmPmChange } = this.props; 81 | onAmPmChange(ampm); 82 | }; 83 | 84 | onClear = event => { 85 | event.stopPropagation(); 86 | this.setValue(null); 87 | this.setOpen(false); 88 | }; 89 | 90 | onVisibleChange = open => { 91 | this.setOpen(open); 92 | }; 93 | 94 | onEsc = () => { 95 | this.setOpen(false); 96 | this.focus(); 97 | }; 98 | 99 | onKeyDown = e => { 100 | if (e.keyCode === 40) { 101 | this.setOpen(true); 102 | } 103 | }; 104 | 105 | setValue(value) { 106 | const { onChange } = this.props; 107 | if (!('value' in this.props)) { 108 | this.setState({ 109 | value, 110 | }); 111 | } 112 | onChange(value); 113 | } 114 | 115 | getFormat() { 116 | const { format, showHour, showMinute, showSecond, use12Hours } = this.props; 117 | if (format) { 118 | return format; 119 | } 120 | 121 | if (use12Hours) { 122 | const fmtString = [showHour ? 'h' : '', showMinute ? 'mm' : '', showSecond ? 'ss' : ''] 123 | .filter(item => !!item) 124 | .join(':'); 125 | 126 | return fmtString.concat(' a'); 127 | } 128 | 129 | return [showHour ? 'HH' : '', showMinute ? 'mm' : '', showSecond ? 'ss' : ''] 130 | .filter(item => !!item) 131 | .join(':'); 132 | } 133 | 134 | getPanelElement() { 135 | const { 136 | prefixCls, 137 | placeholder, 138 | disabledHours, 139 | disabledMinutes, 140 | disabledSeconds, 141 | hideDisabledOptions, 142 | inputReadOnly, 143 | showHour, 144 | showMinute, 145 | showSecond, 146 | defaultOpenValue, 147 | clearText, 148 | addon, 149 | use12Hours, 150 | focusOnOpen, 151 | onKeyDown, 152 | hourStep, 153 | minuteStep, 154 | secondStep, 155 | clearIcon, 156 | } = this.props; 157 | const { value } = this.state; 158 | return ( 159 | 187 | ); 188 | } 189 | 190 | getPopupClassName() { 191 | const { showHour, showMinute, showSecond, use12Hours, prefixCls, popupClassName } = this.props; 192 | let selectColumnCount = 0; 193 | if (showHour) { 194 | selectColumnCount += 1; 195 | } 196 | if (showMinute) { 197 | selectColumnCount += 1; 198 | } 199 | if (showSecond) { 200 | selectColumnCount += 1; 201 | } 202 | if (use12Hours) { 203 | selectColumnCount += 1; 204 | } 205 | // Keep it for old compatibility 206 | return classNames( 207 | popupClassName, 208 | { 209 | [`${prefixCls}-panel-narrow`]: (!showHour || !showMinute || !showSecond) && !use12Hours, 210 | }, 211 | `${prefixCls}-panel-column-${selectColumnCount}`, 212 | ); 213 | } 214 | 215 | setOpen(open) { 216 | const { onOpen, onClose } = this.props; 217 | const { open: currentOpen } = this.state; 218 | if (currentOpen !== open) { 219 | if (!('open' in this.props)) { 220 | this.setState({ open }); 221 | } 222 | if (open) { 223 | onOpen({ open }); 224 | } else { 225 | onClose({ open }); 226 | } 227 | } 228 | } 229 | 230 | focus() { 231 | this.picker.focus(); 232 | } 233 | 234 | blur() { 235 | this.picker.blur(); 236 | } 237 | 238 | renderClearButton() { 239 | const { value } = this.state; 240 | const { prefixCls, allowEmpty, clearIcon, clearText, disabled } = this.props; 241 | if (!allowEmpty || !value || disabled) { 242 | return null; 243 | } 244 | 245 | if (React.isValidElement(clearIcon)) { 246 | const { onClick } = clearIcon.props || {}; 247 | return React.cloneElement(clearIcon, { 248 | onClick: (...args) => { 249 | if (onClick) onClick(...args); 250 | this.onClear(...args); 251 | }, 252 | }); 253 | } 254 | 255 | return ( 256 | 263 | {clearIcon || } 264 | 265 | ); 266 | } 267 | 268 | render() { 269 | const { 270 | prefixCls, 271 | placeholder, 272 | placement, 273 | align, 274 | id, 275 | disabled, 276 | transitionName, 277 | style, 278 | className, 279 | inputClassName, 280 | getPopupContainer, 281 | name, 282 | autoComplete, 283 | onFocus, 284 | onBlur, 285 | autoFocus, 286 | inputReadOnly, 287 | inputIcon, 288 | popupStyle, 289 | } = this.props; 290 | const { open, value } = this.state; 291 | const popupClassName = this.getPopupClassName(); 292 | return ( 293 | 308 | 309 | 326 | {inputIcon || } 327 | {this.renderClearButton()} 328 | 329 | 330 | ); 331 | } 332 | } 333 | 334 | export default Picker; 335 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './TimePicker'; 2 | -------------------------------------------------------------------------------- /src/placements.js: -------------------------------------------------------------------------------- 1 | const autoAdjustOverflow = { 2 | adjustX: 1, 3 | adjustY: 1, 4 | }; 5 | 6 | const targetOffset = [0, 0]; 7 | 8 | const placements = { 9 | bottomLeft: { 10 | points: ['tl', 'tl'], 11 | overflow: autoAdjustOverflow, 12 | offset: [0, -3], 13 | targetOffset, 14 | }, 15 | bottomRight: { 16 | points: ['tr', 'tr'], 17 | overflow: autoAdjustOverflow, 18 | offset: [0, -3], 19 | targetOffset, 20 | }, 21 | topRight: { 22 | points: ['br', 'br'], 23 | overflow: autoAdjustOverflow, 24 | offset: [0, 3], 25 | targetOffset, 26 | }, 27 | topLeft: { 28 | points: ['bl', 'bl'], 29 | overflow: autoAdjustOverflow, 30 | offset: [0, 3], 31 | targetOffset, 32 | }, 33 | }; 34 | 35 | export default placements; 36 | -------------------------------------------------------------------------------- /tests/Header.spec.jsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from 'react-dom'; 2 | import React from 'react'; 3 | import KeyCode from 'rc-util/lib/KeyCode'; 4 | import { mount } from 'enzyme'; 5 | import moment from 'moment'; 6 | import TimePicker from '../src/TimePicker'; 7 | import { findHeader, clickInput, blurInput, matchAll } from './util'; 8 | 9 | describe('Header', () => { 10 | let container; 11 | 12 | function renderPicker(props) { 13 | const showSecond = true; 14 | const format = 'HH:mm:ss'; 15 | return mount( 16 | , 22 | ); 23 | } 24 | 25 | function changeTime(picker, value) { 26 | picker.find('.rc-time-picker-panel-input').simulate('change', { 27 | target: { 28 | value, 29 | }, 30 | }); 31 | } 32 | 33 | function headerContains(picker, str) { 34 | expect(findHeader(picker).instance().className).toContain(str); 35 | } 36 | 37 | beforeEach(() => { 38 | container = document.createElement('div'); 39 | document.body.appendChild(container); 40 | }); 41 | 42 | afterEach(() => { 43 | ReactDOM.unmountComponentAtNode(container); 44 | document.body.removeChild(container); 45 | }); 46 | 47 | describe('input to change value', () => { 48 | it('input correctly', async () => { 49 | const picker = renderPicker(); 50 | expect(picker.state().open).toBeFalsy(); 51 | clickInput(picker); 52 | 53 | expect(picker.state().open).toBeTruthy(); 54 | matchAll(picker, '01:02:03'); 55 | 56 | changeTime(picker, '12:34:56'); 57 | 58 | expect(picker.state().open).toBeTruthy(); 59 | matchAll(picker, '12:34:56'); 60 | }); 61 | 62 | it('carry correctly', async () => { 63 | const picker = renderPicker(); 64 | expect(picker.state().open).toBeFalsy(); 65 | clickInput(picker); 66 | expect(picker.state().open).toBeTruthy(); 67 | 68 | matchAll(picker, '01:02:03'); 69 | 70 | changeTime(picker, '33:44:55'); 71 | expect(picker.state().open).toBeTruthy(); 72 | matchAll(picker, '33:44:55', '01:02:03'); 73 | 74 | changeTime(picker, '10:90:30'); 75 | expect(picker.state().open).toBeTruthy(); 76 | matchAll(picker, '10:90:30', '01:02:03'); 77 | 78 | changeTime(picker, '34:56:78'); 79 | expect(picker.state().open).toBeTruthy(); 80 | matchAll(picker, '34:56:78', '01:02:03'); 81 | }); 82 | 83 | it('carry disabled correctly', async () => { 84 | const picker = renderPicker({ 85 | disabledMinutes(h) { 86 | return [h]; 87 | }, 88 | disabledSeconds(h, m) { 89 | return [h + (m % 60)]; 90 | }, 91 | }); 92 | expect(picker.state().open).toBeFalsy(); 93 | clickInput(picker); 94 | expect(picker.state().open).toBeTruthy(); 95 | matchAll(picker, '01:02:03'); 96 | 97 | changeTime(picker, '10:09:78'); 98 | expect(picker.state().open).toBeTruthy(); 99 | headerContains(picker, 'rc-time-picker-panel-input-invalid'); 100 | matchAll(picker, '10:09:78', '01:02:03'); 101 | 102 | changeTime(picker, '10:10:78'); 103 | expect(picker.state().open).toBeTruthy(); 104 | matchAll(picker, '10:10:78', '01:02:03'); 105 | 106 | changeTime(picker, '10:09:19'); 107 | expect(picker.state().open).toBeTruthy(); 108 | headerContains(picker, 'rc-time-picker-panel-input-invalid'); 109 | matchAll(picker, '10:09:19', '01:02:03'); 110 | 111 | changeTime(picker, '10:09:20'); 112 | expect(picker.state().open).toBeTruthy(); 113 | matchAll(picker, '10:09:20'); 114 | }); 115 | 116 | it('carry hidden correctly', async () => { 117 | const picker = renderPicker({ 118 | disabledMinutes(h) { 119 | return [h]; 120 | }, 121 | disabledSeconds(h, m) { 122 | return [h + (m % 60)]; 123 | }, 124 | hideDisabledOptions: true, 125 | }); 126 | expect(picker.state().open).toBeFalsy(); 127 | clickInput(picker); 128 | expect(picker.state().open).toBeTruthy(); 129 | 130 | matchAll(picker, '01:02:03'); 131 | 132 | changeTime(picker, '10:09:78'); 133 | expect(picker.state().open).toBeTruthy(); 134 | headerContains(picker, 'rc-time-picker-panel-input-invalid'); 135 | matchAll(picker, '10:09:78', '01:02:03'); 136 | 137 | changeTime(picker, '10:10:78'); 138 | expect(picker.state().open).toBeTruthy(); 139 | matchAll(picker, '10:10:78', '01:02:03'); 140 | 141 | changeTime(picker, '10:09:19'); 142 | expect(picker.state().open).toBeTruthy(); 143 | headerContains(picker, 'rc-time-picker-panel-input-invalid'); 144 | matchAll(picker, '10:09:19', '01:02:03'); 145 | 146 | changeTime(picker, '10:09:20'); 147 | expect(picker.state().open).toBeTruthy(); 148 | matchAll(picker, '10:09:20'); 149 | }); 150 | 151 | it('check correctly', async () => { 152 | const picker = renderPicker(); 153 | expect(picker.state().open).toBeFalsy(); 154 | clickInput(picker); 155 | expect(picker.state().open).toBeTruthy(); 156 | 157 | matchAll(picker, '01:02:03'); 158 | 159 | changeTime(picker, '3:34:56'); 160 | expect(picker.state().open).toBeTruthy(); 161 | matchAll(picker, '3:34:56', '01:02:03'); 162 | headerContains(picker, 'rc-time-picker-panel-input-invalid'); 163 | 164 | changeTime(picker, '13:3:56'); 165 | expect(picker.state().open).toBeTruthy(); 166 | matchAll(picker, '13:3:56', '01:02:03'); 167 | headerContains(picker, 'rc-time-picker-panel-input-invalid'); 168 | 169 | changeTime(picker, '13:34:5'); 170 | expect(picker.state().open).toBeTruthy(); 171 | matchAll(picker, '13:34:5', '01:02:03'); 172 | headerContains(picker, 'rc-time-picker-panel-input-invalid'); 173 | }); 174 | }); 175 | 176 | describe('other operations', () => { 177 | it('exit correctly', async () => { 178 | const picker = renderPicker(); 179 | expect(picker.state().open).toBeFalsy(); 180 | clickInput(picker); 181 | expect(picker.state().open).toBeTruthy(); 182 | 183 | matchAll(picker, '01:02:03'); 184 | 185 | findHeader(picker).simulate('keyDown', { 186 | keyCode: KeyCode.ESC, 187 | }); 188 | 189 | expect(picker.state().open).toBeFalsy(); 190 | 191 | clickInput(picker); 192 | matchAll(picker, '01:02:03'); 193 | }); 194 | 195 | it('focus on open', async () => { 196 | const picker = renderPicker({ 197 | focusOnOpen: true, 198 | }); 199 | expect(picker.state().open).toBeFalsy(); 200 | clickInput(picker); 201 | 202 | // this touches the focusOnOpen code, but we cannot verify the input is in focus 203 | expect(picker.state().open).toBeTruthy(); 204 | matchAll(picker, '01:02:03'); 205 | }); 206 | 207 | it('can be clear mannually even allowEmpty is false', async () => { 208 | const picker = renderPicker({ 209 | allowEmpty: false, 210 | }); 211 | clickInput(picker); 212 | expect(picker.state().open).toBeTruthy(); 213 | matchAll(picker, '01:02:03'); 214 | changeTime(picker, ''); 215 | blurInput(picker); 216 | matchAll(picker, ''); 217 | }); 218 | }); 219 | }); 220 | -------------------------------------------------------------------------------- /tests/Select.spec.jsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from 'react-dom'; 2 | import React from 'react'; 3 | import { mount } from 'enzyme'; 4 | import moment from 'moment'; 5 | import TimePicker from '../src/TimePicker'; 6 | import { clickInput, clickSelectItem, matchValue, matchAll, escapeSelected } from './util'; 7 | 8 | describe('Select', () => { 9 | let container; 10 | 11 | function renderPicker(props) { 12 | const showSecond = true; 13 | const format = 'HH:mm:ss'; 14 | 15 | return mount( 16 | , 22 | ); 23 | } 24 | 25 | beforeEach(() => { 26 | container = document.createElement('div'); 27 | document.body.appendChild(container); 28 | }); 29 | 30 | afterEach(() => { 31 | ReactDOM.unmountComponentAtNode(container); 32 | document.body.removeChild(container); 33 | }); 34 | 35 | describe('select panel', () => { 36 | it('select panel reacts to mouseenter and mouseleave correctly', async () => { 37 | const picker = renderPicker(); 38 | clickInput(picker); 39 | 40 | const re = /(^|\s+)rc-time-picker-panel-select-active(\s+|$)/; 41 | 42 | expect( 43 | re.test( 44 | picker 45 | .find('.rc-time-picker-panel-select') 46 | .at(0) 47 | .instance().className, 48 | ), 49 | ).toBeFalsy(); 50 | 51 | picker 52 | .find('.rc-time-picker-panel-select') 53 | .at(0) 54 | .simulate('mouseEnter'); 55 | expect( 56 | re.test( 57 | picker 58 | .find('.rc-time-picker-panel-select') 59 | .at(0) 60 | .instance().className, 61 | ), 62 | ).toBeTruthy(); 63 | 64 | picker 65 | .find('.rc-time-picker-panel-select') 66 | .at(0) 67 | .simulate('mouseLeave'); 68 | expect( 69 | re.test( 70 | picker 71 | .find('.rc-time-picker-panel-select') 72 | .at(0) 73 | .instance().className, 74 | ), 75 | ).toBeFalsy(); 76 | }); 77 | 78 | it('shows only numbers according to step props', async () => { 79 | const picker = renderPicker({ 80 | hourStep: 5, 81 | minuteStep: 15, 82 | secondStep: 21, 83 | }); 84 | clickInput(picker); 85 | 86 | const selectors = picker.find('.rc-time-picker-panel-select'); 87 | 88 | const hourSelector = selectors.at(0); 89 | const minuteSelector = selectors.at(1); 90 | const secondSelector = selectors.at(2); 91 | 92 | const hours = hourSelector.find('li').map(node => node.text()); 93 | expect(hours).toEqual(['00', '05', '10', '15', '20']); 94 | 95 | const minutes = minuteSelector.find('li').map(node => node.text()); 96 | expect(minutes).toEqual(['00', '15', '30', '45']); 97 | 98 | const seconds = secondSelector.find('li').map(node => node.text()); 99 | expect(seconds).toEqual(['00', '21', '42']); 100 | }); 101 | }); 102 | 103 | describe('select number', () => { 104 | it('select number correctly', async () => { 105 | const picker = renderPicker(); 106 | expect(picker.state().open).toBeFalsy(); 107 | 108 | clickInput(picker); 109 | expect(picker.state().open).toBeTruthy(); 110 | 111 | expect(picker.find('.rc-time-picker-panel-select').length).toBe(3); 112 | }); 113 | }); 114 | 115 | describe('select to change value', () => { 116 | it('hour correctly', async () => { 117 | const onChange = jest.fn(); 118 | const picker = renderPicker({ 119 | onChange, 120 | }); 121 | expect(picker.state().open).toBeFalsy(); 122 | 123 | clickInput(picker); 124 | 125 | expect(picker.state().open).toBeTruthy(); 126 | matchAll(picker, '01:02:04'); 127 | 128 | clickSelectItem(picker, 0, 19); 129 | 130 | expect(onChange).toBeCalled(); 131 | expect(onChange.mock.calls[0][0].hour()).toBe(19); 132 | matchAll(picker, '19:02:04'); 133 | expect(picker.state().open).toBeTruthy(); 134 | }); 135 | 136 | it('minute correctly', async () => { 137 | const onChange = jest.fn(); 138 | const picker = renderPicker({ 139 | onChange, 140 | }); 141 | expect(picker.state().open).toBeFalsy(); 142 | 143 | clickInput(picker); 144 | 145 | expect(picker.state().open).toBeTruthy(); 146 | matchAll(picker, '01:02:04'); 147 | 148 | clickSelectItem(picker, 1, 19); 149 | 150 | expect(onChange).toBeCalled(); 151 | expect(onChange.mock.calls[0][0].minute()).toBe(19); 152 | matchAll(picker, '01:19:04'); 153 | expect(picker.state().open).toBeTruthy(); 154 | }); 155 | 156 | it('second correctly', async () => { 157 | const onChange = jest.fn(); 158 | const picker = renderPicker({ 159 | onChange, 160 | }); 161 | expect(picker.state().open).toBeFalsy(); 162 | 163 | clickInput(picker); 164 | 165 | expect(picker.state().open).toBeTruthy(); 166 | matchAll(picker, '01:02:04'); 167 | 168 | clickSelectItem(picker, 2, 19); 169 | 170 | expect(onChange).toBeCalled(); 171 | expect(onChange.mock.calls[0][0].second()).toBe(19); 172 | matchAll(picker, '01:02:19'); 173 | expect(picker.state().open).toBeTruthy(); 174 | }); 175 | 176 | it('ampm correctly', async () => { 177 | const onAmPmChange = jest.fn(); 178 | const picker = renderPicker({ 179 | onAmPmChange, 180 | defaultValue: moment() 181 | .hour(0) 182 | .minute(0) 183 | .second(0), 184 | format: undefined, 185 | showSecond: false, 186 | use12Hours: true, 187 | }); 188 | expect(picker.state().open).toBeFalsy(); 189 | clickInput(picker); 190 | 191 | expect(picker.state().open).toBeTruthy(); 192 | 193 | matchValue(picker, '12:00 am'); 194 | clickSelectItem(picker, 2, 1); 195 | 196 | expect(onAmPmChange).toBeCalled(); 197 | expect(onAmPmChange.mock.calls[0][0]).toBe('PM'); 198 | matchValue(picker, '12:00 pm'); 199 | expect(picker.state().open).toBeTruthy(); 200 | }); 201 | 202 | it('disabled correctly', async () => { 203 | const onChange = jest.fn(); 204 | const picker = renderPicker({ 205 | onChange, 206 | disabledMinutes(h) { 207 | return [h]; 208 | }, 209 | disabledSeconds(h, m) { 210 | return [h + (m % 60)]; 211 | }, 212 | }); 213 | expect(picker.state().open).toBeFalsy(); 214 | clickInput(picker); 215 | 216 | expect(picker.state().open).toBeTruthy(); 217 | 218 | matchAll(picker, '01:02:04'); 219 | 220 | clickSelectItem(picker, 1, 1); 221 | 222 | expect(onChange).not.toBeCalled(); 223 | matchAll(picker, '01:02:04'); 224 | expect(picker.state().open).toBeTruthy(); 225 | 226 | clickSelectItem(picker, 2, 3); 227 | 228 | expect(onChange).not.toBeCalled(); 229 | matchAll(picker, '01:02:04'); 230 | expect(picker.state().open).toBeTruthy(); 231 | 232 | clickSelectItem(picker, 1, 7); 233 | 234 | expect(onChange).toBeCalled(); 235 | expect(onChange.mock.calls[0][0].minute()).toBe(7); 236 | matchAll(picker, '01:07:04'); 237 | expect(picker.state().open).toBeTruthy(); 238 | }); 239 | 240 | it('hidden correctly', async () => { 241 | const onChange = jest.fn(); 242 | const picker = renderPicker({ 243 | onChange, 244 | disabledHours() { 245 | return [1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23]; 246 | }, 247 | hideDisabledOptions: true, 248 | }); 249 | expect(picker.state().open).toBeFalsy(); 250 | clickInput(picker); 251 | expect(picker.state().open).toBeTruthy(); 252 | 253 | matchAll(picker, '01:02:04'); 254 | 255 | clickSelectItem(picker, 0, 3); 256 | 257 | expect(onChange).toBeCalled(); 258 | expect(onChange.mock.calls[0][0].hour()).toBe(6); 259 | matchAll(picker, '06:02:04'); 260 | expect(picker.state().open).toBeTruthy(); 261 | onChange.mockReset(); 262 | 263 | clickSelectItem(picker, 0, 4); 264 | 265 | expect(onChange).toBeCalled(); 266 | expect(onChange.mock.calls[0][0].hour()).toBe(8); 267 | matchAll(picker, '08:02:04'); 268 | expect(picker.state().open).toBeTruthy(); 269 | }); 270 | }); 271 | 272 | describe('select in 12 hours mode', () => { 273 | it('renders correctly', async () => { 274 | const picker = renderPicker({ 275 | use12Hours: true, 276 | defaultValue: moment() 277 | .hour(14) 278 | .minute(0) 279 | .second(0), 280 | showSecond: false, 281 | format: undefined, 282 | }); 283 | 284 | expect(picker.state().open).toBeFalsy(); 285 | clickInput(picker); 286 | expect(picker.state().open).toBeTruthy(); 287 | 288 | matchValue(picker, '2:00 pm'); 289 | 290 | expect(picker.find('.rc-time-picker-panel-select').length).toBe(3); 291 | }); 292 | 293 | it('renders 12am correctly', async () => { 294 | const picker = renderPicker({ 295 | use12Hours: true, 296 | defaultValue: moment() 297 | .hour(0) 298 | .minute(0) 299 | .second(0), 300 | showSecond: false, 301 | format: undefined, 302 | }); 303 | expect(picker.state().open).toBeFalsy(); 304 | clickInput(picker); 305 | expect(picker.state().open).toBeTruthy(); 306 | 307 | expect(picker.find('.rc-time-picker-panel-select').length).toBe(3); 308 | }); 309 | 310 | it('renders 5am correctly', async () => { 311 | const picker = renderPicker({ 312 | use12Hours: true, 313 | defaultValue: moment() 314 | .hour(0) 315 | .minute(0) 316 | .second(0), 317 | showSecond: false, 318 | format: undefined, 319 | }); 320 | expect(picker.state().open).toBeFalsy(); 321 | clickInput(picker); 322 | expect(picker.state().open).toBeTruthy(); 323 | 324 | matchValue(picker, '12:00 am'); 325 | clickSelectItem(picker, 0, 3); 326 | 327 | matchValue(picker, '3:00 am'); 328 | }); 329 | 330 | it('renders 12am/pm correctly', async () => { 331 | const picker = renderPicker({ 332 | use12Hours: true, 333 | defaultValue: moment() 334 | .hour(0) 335 | .minute(0) 336 | .second(0), 337 | showSecond: false, 338 | format: undefined, 339 | }); 340 | 341 | expect(picker.state().open).toBeFalsy(); 342 | clickInput(picker); 343 | expect(picker.state().open).toBeTruthy(); 344 | 345 | matchValue(picker, '12:00 am'); 346 | 347 | clickSelectItem(picker, 2, 1); 348 | matchValue(picker, '12:00 pm'); 349 | 350 | clickSelectItem(picker, 2, 0); 351 | matchValue(picker, '12:00 am'); 352 | }); 353 | 354 | it('renders uppercase AM correctly', async () => { 355 | const picker = renderPicker({ 356 | use12Hours: true, 357 | defaultValue: moment() 358 | .hour(0) 359 | .minute(0) 360 | .second(0), 361 | showSecond: false, 362 | format: 'h:mm A', 363 | }); 364 | 365 | expect(picker.state().open).toBeFalsy(); 366 | clickInput(picker); 367 | expect(picker.state().open).toBeTruthy(); 368 | 369 | matchValue(picker, '12:00 AM'); 370 | 371 | clickSelectItem(picker, 2, 1); 372 | matchValue(picker, '12:00 PM'); 373 | 374 | clickSelectItem(picker, 2, 0); 375 | matchValue(picker, '12:00 AM'); 376 | }); 377 | 378 | it('disabled correctly', async () => { 379 | const onChange = jest.fn(); 380 | const picker = renderPicker({ 381 | use12Hours: true, 382 | format: undefined, 383 | onChange, 384 | disabledHours() { 385 | return [0, 2, 6, 18, 12]; 386 | }, 387 | defaultValue: moment() 388 | .hour(0) 389 | .minute(0) 390 | .second(0), 391 | showSecond: false, 392 | }); 393 | 394 | expect(picker.state().open).toBeFalsy(); 395 | clickInput(picker); 396 | expect(picker.state().open).toBeTruthy(); 397 | 398 | matchAll(picker, '12:00 am'); 399 | 400 | clickSelectItem(picker, 0, 2); 401 | expect(onChange).not.toBeCalled(); 402 | matchAll(picker, '12:00 am'); 403 | expect(picker.state().open).toBeTruthy(); 404 | 405 | clickSelectItem(picker, 0, 5); 406 | expect(onChange).toBeCalled(); 407 | expect(onChange.mock.calls[0][0].hour()).toBe(5); 408 | matchAll(picker, '5:00 am'); 409 | expect(picker.state().open).toBeTruthy(); 410 | onChange.mockReset(); 411 | 412 | clickSelectItem(picker, 2, 1); 413 | expect(onChange).toBeCalled(); 414 | matchAll(picker, '5:00 pm'); 415 | expect(picker.state().open).toBeTruthy(); 416 | onChange.mockReset(); 417 | 418 | clickSelectItem(picker, 0, 0); 419 | expect(onChange).not.toBeCalled(); 420 | matchAll(picker, '5:00 pm'); 421 | expect(picker.state().open).toBeTruthy(); 422 | onChange.mockReset(); 423 | 424 | clickSelectItem(picker, 0, 5); 425 | expect(onChange).toBeCalled(); 426 | expect(onChange.mock.calls[0][0].hour()).toBe(17); 427 | matchAll(picker, '5:00 pm'); 428 | expect(picker.state().open).toBeTruthy(); 429 | }); 430 | }); 431 | 432 | describe('other operations', () => { 433 | function testClearIcon(name, clearIcon, findClearFunc) { 434 | it(name, async () => { 435 | const onChange = jest.fn(); 436 | const picker = renderPicker({ 437 | clearIcon, 438 | onChange, 439 | }); 440 | 441 | const clearButton = findClearFunc(picker); 442 | matchValue(picker, '01:02:04'); 443 | 444 | clearButton.simulate('click'); 445 | expect(picker.state().open).toBeFalsy(); 446 | expect(onChange.mock.calls[0][0]).toBe(null); 447 | 448 | clickInput(picker); 449 | matchValue(picker, ''); 450 | }); 451 | } 452 | 453 | testClearIcon('clear correctly', 'test-clear', picker => { 454 | const clearButton = picker.find('.rc-time-picker-clear'); 455 | expect(clearButton.text()).toBe('test-clear'); 456 | return clearButton; 457 | }); 458 | testClearIcon( 459 | 'customize element clear icon correctly', 460 | Clear Me, 461 | picker => picker.find('.test-clear-element'), 462 | ); 463 | }); 464 | 465 | it('escape closes popup', async () => { 466 | const picker = renderPicker(); 467 | 468 | expect(picker.state().open).toBeFalsy(); 469 | clickInput(picker); 470 | expect(picker.state().open).toBeTruthy(); 471 | 472 | clickSelectItem(picker, 1, 1); 473 | escapeSelected(picker); 474 | 475 | expect(picker.state().open).toBeFalsy(); 476 | }); 477 | }); 478 | -------------------------------------------------------------------------------- /tests/TimePicker.spec.jsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from 'react-dom'; 2 | import React from 'react'; 3 | import { mount } from 'enzyme'; 4 | import moment from 'moment'; 5 | import TimePicker from '../src/TimePicker'; 6 | import { clickInput, clickSelectItem, matchValue } from './util'; 7 | 8 | describe('TimePicker', () => { 9 | let container; 10 | 11 | function renderPicker(props, options) { 12 | const showSecond = true; 13 | const format = 'HH:mm:ss'; 14 | 15 | return mount( 16 | , 22 | options, 23 | ); 24 | } 25 | 26 | function renderPickerWithoutSeconds(props) { 27 | const showSecond = false; 28 | const format = 'HH:mm'; 29 | 30 | return mount( 31 | , 37 | ); 38 | } 39 | 40 | beforeEach(() => { 41 | container = document.createElement('div'); 42 | document.body.appendChild(container); 43 | }); 44 | 45 | afterEach(() => { 46 | ReactDOM.unmountComponentAtNode(container); 47 | document.body.removeChild(container); 48 | }); 49 | 50 | describe('render panel to body', () => { 51 | it('popup correctly', async () => { 52 | const onChange = jest.fn(); 53 | const picker = renderPicker({ 54 | onChange, 55 | }); 56 | expect(picker.state().open).toBeFalsy(); 57 | matchValue(picker, '12:57:58'); 58 | clickInput(picker); 59 | 60 | expect(picker.state().open).toBeTruthy(); 61 | clickSelectItem(picker, 0, 1); 62 | 63 | expect(onChange).toBeCalled(); 64 | expect(onChange.mock.calls[0][0].hour()).toBe(1); 65 | expect(onChange.mock.calls[0][0].minute()).toBe(57); 66 | expect(onChange.mock.calls[0][0].second()).toBe(58); 67 | matchValue(picker, '01:57:58'); 68 | expect(picker.state().open).toBeTruthy(); 69 | }); 70 | 71 | it('destroy correctly', async () => { 72 | const picker = renderPicker({}, { attachTo: container }); 73 | expect(picker.state().open).toBeFalsy(); 74 | clickInput(picker); 75 | expect(picker.state().open).toBeTruthy(); 76 | 77 | expect(document.querySelectorAll('.rc-time-picker').length).not.toBe(0); 78 | expect(picker.find('Panel li').length).toBeTruthy(); 79 | picker.detach(); 80 | 81 | expect(document.querySelectorAll('.rc-time-picker').length).toBe(0); 82 | expect(picker.instance().panelInstance).toBeFalsy(); 83 | }); 84 | 85 | it('support name', () => { 86 | const picker = renderPicker({ 87 | name: 'time-picker-form-name', 88 | }); 89 | expect(picker.find('.rc-time-picker-input').instance().name).toBe('time-picker-form-name'); 90 | }); 91 | 92 | it('support focus', () => { 93 | const picker = renderPicker({ 94 | name: 'time-picker-form-name', 95 | }); 96 | expect(typeof picker.instance().focus).toBe('function'); 97 | }); 98 | 99 | it('should be controlled by open', () => { 100 | const picker = renderPicker({ 101 | open: false, 102 | }); 103 | expect(picker.state().open).toBeFalsy(); 104 | clickInput(picker); 105 | expect(picker.state().open).toBeFalsy(); 106 | }); 107 | 108 | it('support custom icon', () => { 109 | const picker = renderPicker({ 110 | inputIcon: 'test-select', 111 | }); 112 | expect(picker.find('.rc-time-picker').text()).toBe('test-select'); 113 | }); 114 | }); 115 | 116 | describe('render panel to body (without seconds)', () => { 117 | it('popup correctly', async () => { 118 | const onChange = jest.fn(); 119 | const picker = renderPickerWithoutSeconds({ 120 | onChange, 121 | }); 122 | expect(picker.state().open).toBeFalsy(); 123 | matchValue(picker, '08:24'); 124 | clickInput(picker); 125 | 126 | expect(picker.find('.rc-time-picker-panel-inner').length).toBeTruthy(); 127 | expect(picker.state().open).toBeTruthy(); 128 | clickSelectItem(picker, 0, 1); 129 | 130 | expect(onChange).toBeCalled(); 131 | expect(onChange.mock.calls[0][0].hour()).toBe(1); 132 | expect(onChange.mock.calls[0][0].minute()).toBe(24); 133 | matchValue(picker, '01:24'); 134 | expect(picker.state().open).toBeTruthy(); 135 | }); 136 | }); 137 | 138 | describe('render panel to body 12pm mode', () => { 139 | it('popup correctly', async () => { 140 | const onChange = jest.fn(); 141 | const picker = renderPickerWithoutSeconds({ 142 | use12Hours: true, 143 | value: null, 144 | onChange, 145 | }); 146 | expect(picker.state().open).toBeFalsy(); 147 | matchValue(picker, ''); 148 | clickInput(picker); 149 | 150 | expect(picker.find('.rc-time-picker-panel-inner').length).toBeTruthy(); 151 | expect(picker.state().open).toBeTruthy(); 152 | clickSelectItem(picker, 0, 1); 153 | 154 | expect(onChange).toBeCalled(); 155 | expect(picker.state().open).toBeTruthy(); 156 | }); 157 | }); 158 | 159 | describe('other operations', () => { 160 | it('focus/blur correctly', async () => { 161 | let focus = false; 162 | let blur = false; 163 | 164 | const picker = renderPicker({ 165 | onFocus: () => { 166 | focus = true; 167 | }, 168 | onBlur: () => { 169 | blur = true; 170 | }, 171 | }); 172 | expect(picker.state().open).toBeFalsy(); 173 | picker.find('.rc-time-picker-input').simulate('focus'); 174 | expect(picker.state().open).toBeFalsy(); 175 | picker.find('.rc-time-picker-input').simulate('blur'); 176 | 177 | expect(focus).toBeTruthy(); 178 | expect(blur).toBeTruthy(); 179 | }); 180 | }); 181 | 182 | describe('allowEmpty', () => { 183 | it('should allow clear', async () => { 184 | const picker = renderPicker({ 185 | allowEmpty: true, 186 | }); 187 | expect(picker.render()).toMatchSnapshot(); 188 | }); 189 | 190 | it('cannot allow clear when disabled', async () => { 191 | const picker = renderPicker({ 192 | allowEmpty: true, 193 | disabled: true, 194 | }); 195 | expect(picker.render()).toMatchSnapshot(); 196 | }); 197 | }); 198 | }); 199 | -------------------------------------------------------------------------------- /tests/__snapshots__/TimePicker.spec.jsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`TimePicker allowEmpty cannot allow clear when disabled 1`] = ` 4 | 7 | 13 | 16 | 17 | `; 18 | 19 | exports[`TimePicker allowEmpty should allow clear 1`] = ` 20 | 23 | 28 | 31 | 37 | 40 | 41 | 42 | `; 43 | -------------------------------------------------------------------------------- /tests/index.js: -------------------------------------------------------------------------------- 1 | import '../assets/index.less'; 2 | import './TimePicker.spec'; 3 | import './Header.spec'; 4 | import './Select.spec'; 5 | -------------------------------------------------------------------------------- /tests/util.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | export function clickInput(picker) { 4 | picker.find('.rc-time-picker-input').simulate('click'); 5 | } 6 | 7 | export function blurInput(picker) { 8 | picker.find('.rc-time-picker-input').simulate('blur'); 9 | } 10 | 11 | export function escapeSelected(picker) { 12 | picker 13 | .find('.rc-time-picker-panel-select-option-selected') 14 | .first() 15 | .simulate('keydown', { keyCode: 27 }); 16 | } 17 | 18 | export function clickSelectItem(picker, select, index) { 19 | const selector = picker.find('.rc-time-picker-panel-select').at(select); 20 | selector 21 | .find('li') 22 | .at(index) 23 | .simulate('click'); 24 | } 25 | 26 | export function findHeader(picker) { 27 | return picker.find('.rc-time-picker-panel-input'); 28 | } 29 | 30 | export function matchValue(picker, str) { 31 | // Input 32 | expect(picker.find('.rc-time-picker-input').instance().value).toBe(str); 33 | } 34 | 35 | export function matchAll(picker, str, str2) { 36 | if (typeof picker !== 'object') { 37 | console.log('`picker` of `matchAll` is not an object!'); 38 | expect(true).toBe(false); 39 | return; 40 | } 41 | 42 | // Header 43 | expect(findHeader(picker).instance().value).toBe(str); 44 | matchValue(picker, str2 || str); 45 | } 46 | --------------------------------------------------------------------------------