diff options
-rw-r--r-- | README.md | 3 | ||||
-rw-r--r-- | examples/12hours.html | 0 | ||||
-rw-r--r-- | examples/12hours.js | 30 | ||||
-rw-r--r-- | src/Combobox.jsx | 76 | ||||
-rw-r--r-- | src/Panel.jsx | 5 | ||||
-rw-r--r-- | src/Select.jsx | 2 | ||||
-rw-r--r-- | src/TimePicker.jsx | 18 | ||||
-rw-r--r-- | tests/Select.spec.jsx | 176 |
8 files changed, 298 insertions, 12 deletions
@@ -60,12 +60,13 @@ API | |||
60 | | value | moment | null | current value | | 60 | | value | moment | null | current value | |
61 | | placeholder | String | '' | time input's placeholder | | 61 | | placeholder | String | '' | time input's placeholder | |
62 | | showHour | Boolean | whether show hour | | | 62 | | showHour | Boolean | whether show hour | | |
63 | | showMinute | Boolean | whether show minute | | | 63 | | showMinute | Boolean | whether show minute | | |
64 | | showSecond | Boolean | whether show second | | | 64 | | showSecond | Boolean | whether show second | | |
65 | | format | String | | | | 65 | | format | String | | | |
66 | | disabledHours | Function | disabled hour options | | | 66 | | disabledHours | Function | disabled hour options | | |
67 | | disabledMinutes | Function | disabled minute options | | | 67 | | disabledMinutes | Function | disabled minute options | | |
68 | | disabledSeconds | Function | disabled second options | | | 68 | | disabledSeconds | Function | disabled second options | | |
69 | | use12Hours | Boolean | 12 hours display mode | | | ||
69 | | hideDisabledOptions | Boolean | whether hide disabled options | | | 70 | | hideDisabledOptions | Boolean | whether hide disabled options | | |
70 | | onChange | Function | null | called when select a different value | | 71 | | onChange | Function | null | called when select a different value | |
71 | | addon | Function | nothing | 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()`.| | 72 | | addon | Function | nothing | 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()`.| |
diff --git a/examples/12hours.html b/examples/12hours.html new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/examples/12hours.html | |||
diff --git a/examples/12hours.js b/examples/12hours.js new file mode 100644 index 0000000..72d6950 --- /dev/null +++ b/examples/12hours.js | |||
@@ -0,0 +1,30 @@ | |||
1 | /* eslint no-console:0 */ | ||
2 | |||
3 | import 'rc-time-picker/assets/index.less'; | ||
4 | |||
5 | import React from 'react'; | ||
6 | import ReactDom from 'react-dom'; | ||
7 | |||
8 | import moment from 'moment'; | ||
9 | |||
10 | import TimePicker from 'rc-time-picker'; | ||
11 | |||
12 | const format = 'h:mm a'; | ||
13 | |||
14 | const now = moment().hour(0).minute(0); | ||
15 | |||
16 | function onChange(value) { | ||
17 | console.log(value && value.format(format)); | ||
18 | } | ||
19 | |||
20 | ReactDom.render( | ||
21 | <TimePicker | ||
22 | showSecond={false} | ||
23 | defaultValue={now} | ||
24 | className="xxx" | ||
25 | onChange={onChange} | ||
26 | format={format} | ||
27 | use12Hours | ||
28 | />, | ||
29 | document.getElementById('__react-content') | ||
30 | ); | ||
diff --git a/src/Combobox.jsx b/src/Combobox.jsx index 013617c..36b61cc 100644 --- a/src/Combobox.jsx +++ b/src/Combobox.jsx | |||
@@ -35,17 +35,40 @@ const Combobox = React.createClass({ | |||
35 | disabledMinutes: PropTypes.func, | 35 | disabledMinutes: PropTypes.func, |
36 | disabledSeconds: PropTypes.func, | 36 | disabledSeconds: PropTypes.func, |
37 | onCurrentSelectPanelChange: PropTypes.func, | 37 | onCurrentSelectPanelChange: PropTypes.func, |
38 | use12Hours: PropTypes.bool, | ||
38 | }, | 39 | }, |
39 | 40 | ||
40 | onItemChange(type, itemValue) { | 41 | onItemChange(type, itemValue) { |
41 | const { onChange, defaultOpenValue } = this.props; | 42 | const { onChange, defaultOpenValue, use12Hours } = this.props; |
42 | const value = (this.props.value || defaultOpenValue).clone(); | 43 | const value = (this.props.value || defaultOpenValue).clone(); |
44 | |||
43 | if (type === 'hour') { | 45 | if (type === 'hour') { |
44 | value.hour(itemValue); | 46 | if (use12Hours) { |
47 | if (this.isAM()) { | ||
48 | value.hour(+itemValue % 12); | ||
49 | } else { | ||
50 | value.hour((+itemValue % 12) + 12); | ||
51 | } | ||
52 | } else { | ||
53 | value.hour(+itemValue); | ||
54 | } | ||
45 | } else if (type === 'minute') { | 55 | } else if (type === 'minute') { |
46 | value.minute(itemValue); | 56 | value.minute(+itemValue); |
57 | } else if (type === 'ampm') { | ||
58 | const ampm = itemValue.toUpperCase(); | ||
59 | if (use12Hours) { | ||
60 | if (ampm === 'PM' && value.hour() < 12) { | ||
61 | value.hour((value.hour() % 12) + 12); | ||
62 | } | ||
63 | |||
64 | if (ampm === 'AM') { | ||
65 | if (value.hour() >= 12) { | ||
66 | value.hour(value.hour() - 12); | ||
67 | } | ||
68 | } | ||
69 | } | ||
47 | } else { | 70 | } else { |
48 | value.second(itemValue); | 71 | value.second(+itemValue); |
49 | } | 72 | } |
50 | onChange(value); | 73 | onChange(value); |
51 | }, | 74 | }, |
@@ -55,17 +78,26 @@ const Combobox = React.createClass({ | |||
55 | }, | 78 | }, |
56 | 79 | ||
57 | getHourSelect(hour) { | 80 | getHourSelect(hour) { |
58 | const { prefixCls, hourOptions, disabledHours, showHour } = this.props; | 81 | const { prefixCls, hourOptions, disabledHours, showHour, use12Hours } = this.props; |
59 | if (!showHour) { | 82 | if (!showHour) { |
60 | return null; | 83 | return null; |
61 | } | 84 | } |
62 | const disabledOptions = disabledHours(); | 85 | const disabledOptions = disabledHours(); |
86 | let hourOptionsAdj; | ||
87 | let hourAdj; | ||
88 | if (use12Hours) { | ||
89 | hourOptionsAdj = [12].concat(hourOptions.filter(h => h < 12 && h > 0)); | ||
90 | hourAdj = (hour % 12) || 12; | ||
91 | } else { | ||
92 | hourOptionsAdj = hourOptions; | ||
93 | hourAdj = hour; | ||
94 | } | ||
63 | 95 | ||
64 | return ( | 96 | return ( |
65 | <Select | 97 | <Select |
66 | prefixCls={prefixCls} | 98 | prefixCls={prefixCls} |
67 | options={hourOptions.map(option => formatOption(option, disabledOptions))} | 99 | options={hourOptionsAdj.map(option => formatOption(option, disabledOptions))} |
68 | selectedIndex={hourOptions.indexOf(hour)} | 100 | selectedIndex={hourOptionsAdj.indexOf(hourAdj)} |
69 | type="hour" | 101 | type="hour" |
70 | onSelect={this.onItemChange} | 102 | onSelect={this.onItemChange} |
71 | onMouseEnter={this.onEnterSelectPanel.bind(this, 'hour')} | 103 | onMouseEnter={this.onEnterSelectPanel.bind(this, 'hour')} |
@@ -113,6 +145,35 @@ const Combobox = React.createClass({ | |||
113 | ); | 145 | ); |
114 | }, | 146 | }, |
115 | 147 | ||
148 | getAMPMSelect() { | ||
149 | const { prefixCls, use12Hours, format } = this.props; | ||
150 | if (!use12Hours) { | ||
151 | return null; | ||
152 | } | ||
153 | |||
154 | const AMPMOptions = ['am', 'pm'] // If format has A char, then we should uppercase AM/PM | ||
155 | .map(c => format.match(/\sA/) ? c.toUpperCase() : c) | ||
156 | .map(c => ({ value: c })); | ||
157 | |||
158 | const selected = this.isAM() ? 0 : 1; | ||
159 | |||
160 | return ( | ||
161 | <Select | ||
162 | prefixCls={prefixCls} | ||
163 | options={AMPMOptions} | ||
164 | selectedIndex={selected} | ||
165 | type="ampm" | ||
166 | onSelect={this.onItemChange} | ||
167 | onMouseEnter={this.onEnterSelectPanel.bind(this, 'ampm')} | ||
168 | /> | ||
169 | ); | ||
170 | }, | ||
171 | |||
172 | isAM() { | ||
173 | const { value } = this.props; | ||
174 | return value.hour() >= 0 && value.hour() < 12; | ||
175 | }, | ||
176 | |||
116 | render() { | 177 | render() { |
117 | const { prefixCls, defaultOpenValue } = this.props; | 178 | const { prefixCls, defaultOpenValue } = this.props; |
118 | const value = this.props.value || defaultOpenValue; | 179 | const value = this.props.value || defaultOpenValue; |
@@ -121,6 +182,7 @@ const Combobox = React.createClass({ | |||
121 | {this.getHourSelect(value.hour())} | 182 | {this.getHourSelect(value.hour())} |
122 | {this.getMinuteSelect(value.minute())} | 183 | {this.getMinuteSelect(value.minute())} |
123 | {this.getSecondSelect(value.second())} | 184 | {this.getSecondSelect(value.second())} |
185 | {this.getAMPMSelect(value.hour())} | ||
124 | </div> | 186 | </div> |
125 | ); | 187 | ); |
126 | }, | 188 | }, |
diff --git a/src/Panel.jsx b/src/Panel.jsx index fddea1c..df128ff 100644 --- a/src/Panel.jsx +++ b/src/Panel.jsx | |||
@@ -37,6 +37,7 @@ const Panel = React.createClass({ | |||
37 | showMinute: PropTypes.bool, | 37 | showMinute: PropTypes.bool, |
38 | showSecond: PropTypes.bool, | 38 | showSecond: PropTypes.bool, |
39 | onClear: PropTypes.func, | 39 | onClear: PropTypes.func, |
40 | use12Hours: PropTypes.bool, | ||
40 | addon: PropTypes.func, | 41 | addon: PropTypes.func, |
41 | }, | 42 | }, |
42 | 43 | ||
@@ -49,6 +50,7 @@ const Panel = React.createClass({ | |||
49 | disabledMinutes: noop, | 50 | disabledMinutes: noop, |
50 | disabledSeconds: noop, | 51 | disabledSeconds: noop, |
51 | defaultOpenValue: moment(), | 52 | defaultOpenValue: moment(), |
53 | use12Hours: false, | ||
52 | addon: noop, | 54 | addon: noop, |
53 | }; | 55 | }; |
54 | }, | 56 | }, |
@@ -90,7 +92,7 @@ const Panel = React.createClass({ | |||
90 | const { | 92 | const { |
91 | prefixCls, className, placeholder, disabledHours, disabledMinutes, | 93 | prefixCls, className, placeholder, disabledHours, disabledMinutes, |
92 | disabledSeconds, hideDisabledOptions, allowEmpty, showHour, showMinute, showSecond, | 94 | disabledSeconds, hideDisabledOptions, allowEmpty, showHour, showMinute, showSecond, |
93 | format, defaultOpenValue, clearText, onEsc, addon, | 95 | format, defaultOpenValue, clearText, onEsc, addon, use12Hours, |
94 | } = this.props; | 96 | } = this.props; |
95 | const { | 97 | const { |
96 | value, currentSelectPanel, | 98 | value, currentSelectPanel, |
@@ -140,6 +142,7 @@ const Panel = React.createClass({ | |||
140 | disabledMinutes={disabledMinutes} | 142 | disabledMinutes={disabledMinutes} |
141 | disabledSeconds={disabledSeconds} | 143 | disabledSeconds={disabledSeconds} |
142 | onCurrentSelectPanelChange={this.onCurrentSelectPanelChange} | 144 | onCurrentSelectPanelChange={this.onCurrentSelectPanelChange} |
145 | use12Hours={use12Hours} | ||
143 | /> | 146 | /> |
144 | {addon(this)} | 147 | {addon(this)} |
145 | </div> | 148 | </div> |
diff --git a/src/Select.jsx b/src/Select.jsx index 238a776..5733b1a 100644 --- a/src/Select.jsx +++ b/src/Select.jsx | |||
@@ -58,7 +58,7 @@ const Select = React.createClass({ | |||
58 | }); | 58 | }); |
59 | let onclick = null; | 59 | let onclick = null; |
60 | if (!item.disabled) { | 60 | if (!item.disabled) { |
61 | onclick = this.onSelect.bind(this, +item.value); | 61 | onclick = this.onSelect.bind(this, item.value); |
62 | } | 62 | } |
63 | return (<li | 63 | return (<li |
64 | className={cls} | 64 | className={cls} |
diff --git a/src/TimePicker.jsx b/src/TimePicker.jsx index e9d6d15..7065333 100644 --- a/src/TimePicker.jsx +++ b/src/TimePicker.jsx | |||
@@ -43,6 +43,7 @@ const Picker = React.createClass({ | |||
43 | addon: PropTypes.func, | 43 | addon: PropTypes.func, |
44 | name: PropTypes.string, | 44 | name: PropTypes.string, |
45 | autoComplete: PropTypes.string, | 45 | autoComplete: PropTypes.string, |
46 | use12Hours: PropTypes.bool, | ||
46 | }, | 47 | }, |
47 | 48 | ||
48 | getDefaultProps() { | 49 | getDefaultProps() { |
@@ -67,6 +68,7 @@ const Picker = React.createClass({ | |||
67 | onOpen: noop, | 68 | onOpen: noop, |
68 | onClose: noop, | 69 | onClose: noop, |
69 | addon: noop, | 70 | addon: noop, |
71 | use12Hours: false, | ||
70 | }; | 72 | }; |
71 | }, | 73 | }, |
72 | 74 | ||
@@ -126,10 +128,21 @@ const Picker = React.createClass({ | |||
126 | }, | 128 | }, |
127 | 129 | ||
128 | getFormat() { | 130 | getFormat() { |
129 | const { format, showHour, showMinute, showSecond } = this.props; | 131 | const { format, showHour, showMinute, showSecond, use12Hours } = this.props; |
130 | if (format) { | 132 | if (format) { |
131 | return format; | 133 | return format; |
132 | } | 134 | } |
135 | |||
136 | if (use12Hours) { | ||
137 | const fmtString = ([ | ||
138 | showHour ? 'h' : '', | ||
139 | showMinute ? 'mm' : '', | ||
140 | showSecond ? 'ss' : '', | ||
141 | ].filter(item => !!item).join(':')); | ||
142 | |||
143 | return fmtString.concat(' a'); | ||
144 | } | ||
145 | |||
133 | return [ | 146 | return [ |
134 | showHour ? 'HH' : '', | 147 | showHour ? 'HH' : '', |
135 | showMinute ? 'mm' : '', | 148 | showMinute ? 'mm' : '', |
@@ -142,7 +155,7 @@ const Picker = React.createClass({ | |||
142 | prefixCls, placeholder, disabledHours, | 155 | prefixCls, placeholder, disabledHours, |
143 | disabledMinutes, disabledSeconds, hideDisabledOptions, | 156 | disabledMinutes, disabledSeconds, hideDisabledOptions, |
144 | allowEmpty, showHour, showMinute, showSecond, defaultOpenValue, clearText, | 157 | allowEmpty, showHour, showMinute, showSecond, defaultOpenValue, clearText, |
145 | addon, | 158 | addon, use12Hours, |
146 | } = this.props; | 159 | } = this.props; |
147 | return ( | 160 | return ( |
148 | <Panel | 161 | <Panel |
@@ -164,6 +177,7 @@ const Picker = React.createClass({ | |||
164 | disabledMinutes={disabledMinutes} | 177 | disabledMinutes={disabledMinutes} |
165 | disabledSeconds={disabledSeconds} | 178 | disabledSeconds={disabledSeconds} |
166 | hideDisabledOptions={hideDisabledOptions} | 179 | hideDisabledOptions={hideDisabledOptions} |
180 | use12Hours={use12Hours} | ||
167 | addon={addon} | 181 | addon={addon} |
168 | /> | 182 | /> |
169 | ); | 183 | ); |
diff --git a/tests/Select.spec.jsx b/tests/Select.spec.jsx index e6d32dd..fd2ec32 100644 --- a/tests/Select.spec.jsx +++ b/tests/Select.spec.jsx | |||
@@ -315,4 +315,180 @@ describe('Select', () => { | |||
315 | }); | 315 | }); |
316 | }); | 316 | }); |
317 | }); | 317 | }); |
318 | |||
319 | |||
320 | describe('select in 12 hours mode', () => { | ||
321 | it('renders correctly', (done) => { | ||
322 | const picker = renderPicker({ | ||
323 | use12Hours: true, | ||
324 | defaultValue: moment().hour(14).minute(0).second(0), | ||
325 | showSecond: false, | ||
326 | format: undefined, | ||
327 | }); | ||
328 | expect(picker.state.open).not.to.be.ok(); | ||
329 | const input = TestUtils.scryRenderedDOMComponentsWithClass(picker, | ||
330 | 'rc-time-picker-input')[0]; | ||
331 | let selector; | ||
332 | async.series([(next) => { | ||
333 | expect(picker.state.open).to.be(false); | ||
334 | |||
335 | Simulate.click(input); | ||
336 | setTimeout(next, 100); | ||
337 | }, (next) => { | ||
338 | expect(picker.state.open).to.be(true); | ||
339 | selector = TestUtils.scryRenderedDOMComponentsWithClass(picker.panelInstance, | ||
340 | 'rc-time-picker-panel-select'); | ||
341 | expect((input).value).to.be('2:00 pm'); | ||
342 | |||
343 | setTimeout(next, 100); | ||
344 | }, (next) => { | ||
345 | expect(selector.length).to.be(3); | ||
346 | |||
347 | next(); | ||
348 | }], () => { | ||
349 | done(); | ||
350 | }); | ||
351 | }); | ||
352 | |||
353 | |||
354 | it('renders 12am correctly', (done) => { | ||
355 | const picker = renderPicker({ | ||
356 | use12Hours: true, | ||
357 | defaultValue: moment().hour(0).minute(0).second(0), | ||
358 | showSecond: false, | ||
359 | format: undefined, | ||
360 | }); | ||
361 | expect(picker.state.open).not.to.be.ok(); | ||
362 | const input = TestUtils.scryRenderedDOMComponentsWithClass(picker, | ||
363 | 'rc-time-picker-input')[0]; | ||
364 | let selector; | ||
365 | async.series([(next) => { | ||
366 | expect(picker.state.open).to.be(false); | ||
367 | |||
368 | Simulate.click(input); | ||
369 | setTimeout(next, 100); | ||
370 | }, (next) => { | ||
371 | expect(picker.state.open).to.be(true); | ||
372 | selector = TestUtils.scryRenderedDOMComponentsWithClass(picker.panelInstance, | ||
373 | 'rc-time-picker-panel-select'); | ||
374 | setTimeout(next, 100); | ||
375 | }, (next) => { | ||
376 | expect(selector.length).to.be(3); | ||
377 | |||
378 | next(); | ||
379 | }], () => { | ||
380 | done(); | ||
381 | }); | ||
382 | }); | ||
383 | |||
384 | |||
385 | it('renders 5am correctly', (done) => { | ||
386 | const picker = renderPicker({ | ||
387 | use12Hours: true, | ||
388 | defaultValue: moment().hour(0).minute(0).second(0), | ||
389 | showSecond: false, | ||
390 | format: undefined, | ||
391 | }); | ||
392 | expect(picker.state.open).not.to.be.ok(); | ||
393 | const input = TestUtils.scryRenderedDOMComponentsWithClass(picker, | ||
394 | 'rc-time-picker-input')[0]; | ||
395 | let selector; | ||
396 | async.series([(next) => { | ||
397 | expect(picker.state.open).to.be(false); | ||
398 | |||
399 | Simulate.click(input); | ||
400 | setTimeout(next, 100); | ||
401 | }, (next) => { | ||
402 | expect(picker.state.open).to.be(true); | ||
403 | selector = TestUtils.scryRenderedDOMComponentsWithClass(picker.panelInstance, | ||
404 | 'rc-time-picker-panel-select')[0]; | ||
405 | expect((input).value).to.be('12:00 am'); | ||
406 | const option = selector.getElementsByTagName('li')[3]; | ||
407 | Simulate.click(option); | ||
408 | setTimeout(next, 100); | ||
409 | }, (next) => { | ||
410 | expect((input).value).to.be('3:00 am'); | ||
411 | next(); | ||
412 | }], () => { | ||
413 | done(); | ||
414 | }); | ||
415 | }); | ||
416 | |||
417 | |||
418 | it('renders 12am/pm correctly', (done) => { | ||
419 | const picker = renderPicker({ | ||
420 | use12Hours: true, | ||
421 | defaultValue: moment().hour(0).minute(0).second(0), | ||
422 | showSecond: false, | ||
423 | format: undefined, | ||
424 | }); | ||
425 | expect(picker.state.open).not.to.be.ok(); | ||
426 | const input = TestUtils.scryRenderedDOMComponentsWithClass(picker, | ||
427 | 'rc-time-picker-input')[0]; | ||
428 | let selector; | ||
429 | async.series([(next) => { | ||
430 | expect(picker.state.open).to.be(false); | ||
431 | |||
432 | Simulate.click(input); | ||
433 | setTimeout(next, 100); | ||
434 | }, (next) => { | ||
435 | expect(picker.state.open).to.be(true); | ||
436 | selector = TestUtils.scryRenderedDOMComponentsWithClass(picker.panelInstance, | ||
437 | 'rc-time-picker-panel-select')[2]; | ||
438 | expect((input).value).to.be('12:00 am'); | ||
439 | const option = selector.getElementsByTagName('li')[1]; | ||
440 | Simulate.click(option); | ||
441 | setTimeout(next, 200); | ||
442 | }, (next) => { | ||
443 | expect((input).value).to.be('12:00 pm'); | ||
444 | next(); | ||
445 | }, (next) => { | ||
446 | Simulate.click(selector.getElementsByTagName('li')[0]); | ||
447 | setTimeout(next, 200); | ||
448 | }, (next) => { | ||
449 | expect((input).value).to.be('12:00 am'); | ||
450 | next(); | ||
451 | }], () => { | ||
452 | done(); | ||
453 | }); | ||
454 | }); | ||
455 | |||
456 | it('renders uppercase AM correctly', (done) => { | ||
457 | const picker = renderPicker({ | ||
458 | use12Hours: true, | ||
459 | defaultValue: moment().hour(0).minute(0).second(0), | ||
460 | showSecond: false, | ||
461 | format: 'h:mm A', | ||
462 | }); | ||
463 | expect(picker.state.open).not.to.be.ok(); | ||
464 | const input = TestUtils.scryRenderedDOMComponentsWithClass(picker, | ||
465 | 'rc-time-picker-input')[0]; | ||
466 | let selector; | ||
467 | async.series([(next) => { | ||
468 | expect(picker.state.open).to.be(false); | ||
469 | |||
470 | Simulate.click(input); | ||
471 | setTimeout(next, 100); | ||
472 | }, (next) => { | ||
473 | expect(picker.state.open).to.be(true); | ||
474 | selector = TestUtils.scryRenderedDOMComponentsWithClass(picker.panelInstance, | ||
475 | 'rc-time-picker-panel-select')[2]; | ||
476 | expect((input).value).to.be('12:00 AM'); | ||
477 | const option = selector.getElementsByTagName('li')[1]; | ||
478 | Simulate.click(option); | ||
479 | setTimeout(next, 200); | ||
480 | }, (next) => { | ||
481 | expect((input).value).to.be('12:00 PM'); | ||
482 | next(); | ||
483 | }, (next) => { | ||
484 | Simulate.click(selector.getElementsByTagName('li')[0]); | ||
485 | setTimeout(next, 200); | ||
486 | }, (next) => { | ||
487 | expect((input).value).to.be('12:00 AM'); | ||
488 | next(); | ||
489 | }], () => { | ||
490 | done(); | ||
491 | }); | ||
492 | }); | ||
493 | }); | ||
318 | }); | 494 | }); |