4
4
*
5
5
*/
6
6
7
- import React , { useEffect , useState , useRef } from 'react' ;
7
+ import React , { useEffect , useState , useRef , useMemo } from 'react' ;
8
8
import { isInteger , toNumber } from 'lodash' ;
9
9
import PropTypes from 'prop-types' ;
10
10
@@ -18,6 +18,11 @@ import Icon from '../Icon';
18
18
import useEventListener from '../../hooks/useEventListener' ;
19
19
import useShortcutEffect from '../../hooks/useShortcutEffect' ;
20
20
21
+ const MINUTES_IN_HOUR = 60 ;
22
+
23
+ // Returns string with two digits padded at start with 0
24
+ const pad = num => `0${ num } ` . substr ( - 2 ) ;
25
+
21
26
// Convert time array to formatted time string
22
27
export const timeFormatter = time => {
23
28
const newTime = Array ( 3 )
@@ -65,27 +70,43 @@ const short = hour => {
65
70
return hour ;
66
71
} ;
67
72
73
+ // return array of minutes in hours with current step
74
+ const getMinutesArr = step => {
75
+ const length = MINUTES_IN_HOUR / step ;
76
+
77
+ return Array . from ( { length } , ( _v , i ) => step * i ) ;
78
+ } ;
79
+
68
80
// Generate options for TimeList display
69
- const getOptions = ( ) => {
70
- const hours = Array . from ( { length : 24 } , ( _ , k ) => k ) ;
81
+ const getOptions = step => {
82
+ const hours = Array . from ( { length : 24 } , ( _ , i ) => i ) ;
83
+ const minutes = getMinutesArr ( step ) ;
84
+
71
85
const options = hours . reduce ( ( acc , cur ) => {
72
- const hour = cur < 10 ? `0${ cur } ` : cur ;
86
+ const hour = pad ( cur ) ;
87
+
88
+ const hourOptions = minutes . map ( minute => {
89
+ const label = `${ hour } :${ pad ( minute ) } ` ;
73
90
74
- return acc . concat ( [
75
- { value : ` ${ hour } :00:00` , label : ` ${ hour } :00` } ,
76
- { value : ` ${ hour } :30:00` , label : ` ${ hour } :30` } ,
77
- ] ) ;
91
+ return { value : ` ${ label } :00` , label } ;
92
+ } ) ;
93
+
94
+ return acc . concat ( hourOptions ) ;
78
95
} , [ ] ) ;
79
96
80
97
return options ;
81
98
} ;
82
99
83
100
// Find the nearest time option to select a TimeList value
84
- const roundHour = time => {
101
+ const roundHour = ( time , step ) => {
85
102
const arr = splitArray ( time ) ;
86
- const nearMin = nearest ( [ 0 , 30 , 60 ] , parseInt ( arr [ 1 ] , 10 ) ) ;
103
+ const minutesArr = getMinutesArr ( step ) ;
104
+ const nearMin = nearest (
105
+ minutesArr . concat ( MINUTES_IN_HOUR ) ,
106
+ parseInt ( arr [ 1 ] , 10 )
107
+ ) ;
87
108
88
- arr [ 1 ] = nearMin !== 30 ? '00' : '30' ;
109
+ arr [ 1 ] = minutesArr . includes ( arr [ 1 ] ) ? '00' : pad ( nearMin ) ;
89
110
arr [ 2 ] = nearMin === 60 ? `${ parseInt ( arr [ 2 ] , 10 ) + 1 } ` : arr [ 2 ] ;
90
111
91
112
return format ( arr . reverse ( ) ) . join ( ':' ) ;
@@ -99,18 +120,23 @@ const nearest = (arr, val) =>
99
120
) + val ;
100
121
101
122
function TimePicker ( props ) {
102
- const { name, onChange, seconds, tabIndex, value } = props ;
123
+ const { name, onChange, seconds, tabIndex, value, step } = props ;
103
124
const [ inputVal , setInputVal ] = useState ( seconds ? value : short ( value ) ) ;
104
125
const [ isOpen , setIsOpen ] = useState ( false ) ;
126
+ const options = useMemo ( ( ) => getOptions ( step ) , [ step ] ) ;
105
127
const inputRef = useRef ( ) ;
106
128
const wrapperRef = useRef ( ) ;
107
129
const listRef = useRef ( ) ;
108
- const listRefs = getOptions ( ) . reduce ( ( acc , curr ) => {
130
+ const listRefs = options . reduce ( ( acc , curr ) => {
109
131
acc [ curr . value ] = useRef ( ) ;
110
132
111
133
return acc ;
112
134
} , { } ) ;
113
- const currentTimeSelected = roundHour ( timeFormatter ( inputVal ) ) ;
135
+
136
+ const currentTimeSelected = useMemo (
137
+ ( ) => roundHour ( timeFormatter ( inputVal ) , step ) ,
138
+ [ inputVal , step ]
139
+ ) ;
114
140
115
141
// Effect to enable scrolling
116
142
useEffect ( ( ) => {
@@ -131,14 +157,13 @@ function TimePicker(props) {
131
157
// Custom hook to select a time using the keyboard's up arrow
132
158
useShortcutEffect ( 'arrowUp' , ( ) => {
133
159
if ( isOpen ) {
134
- const currentTimeIndex = getOptions ( ) . findIndex (
160
+ const currentIndex = options . findIndex (
135
161
o => o . value === currentTimeSelected
136
162
) ;
137
- const optionsLength = getOptions ( ) . length ;
138
- const nextTime =
139
- currentTimeIndex === optionsLength - 1
140
- ? getOptions ( ) [ optionsLength - 1 ]
141
- : getOptions ( ) [ currentTimeIndex + 1 ] ;
163
+ if ( ! currentIndex ) return ;
164
+ const nextIndex = currentIndex - 1 ;
165
+
166
+ const nextTime = options [ nextIndex ] || options [ currentIndex ] ;
142
167
143
168
updateTime ( nextTime . value ) ;
144
169
}
@@ -147,13 +172,14 @@ function TimePicker(props) {
147
172
// Custom hook to select a time using the keyboard's down arrow
148
173
useShortcutEffect ( 'arrowDown' , ( ) => {
149
174
if ( isOpen ) {
150
- const currentTimeIndex = getOptions ( ) . findIndex (
175
+ const currentIndex = options . findIndex (
151
176
o => o . value === currentTimeSelected
152
177
) ;
153
- const nextTime =
154
- currentTimeIndex === 0
155
- ? getOptions ( ) [ 0 ]
156
- : getOptions ( ) [ currentTimeIndex - 1 ] ;
178
+ const lastIndex = options . length - 1 ;
179
+ if ( currentIndex >= lastIndex ) return ;
180
+ const nextIndex = currentIndex + 1 ;
181
+
182
+ const nextTime = options [ nextIndex ] || options [ lastIndex ] ;
157
183
158
184
updateTime ( nextTime . value ) ;
159
185
}
@@ -218,16 +244,16 @@ function TimePicker(props) {
218
244
< Icon icon = "time" />
219
245
</ IconWrapper >
220
246
< TimeList className = { isOpen && 'displayed' } ref = { listRef } >
221
- { getOptions ( ) . map ( option => (
247
+ { options . map ( option => (
222
248
< li key = { option . value } ref = { listRefs [ option . value ] } >
223
249
< input
224
250
type = "radio"
225
251
onChange = { handleClick }
226
252
value = { option . value }
227
253
id = { option . value }
228
254
name = "time"
229
- checked = { option . value === roundHour ( timeFormatter ( inputVal ) ) }
230
- tabIndex = "-1 "
255
+ checked = { option . value === currentTimeSelected }
256
+ tabIndex = "0 "
231
257
/>
232
258
< label htmlFor = { option . value } > { option . label } </ label >
233
259
</ li >
@@ -243,13 +269,17 @@ TimePicker.defaultProps = {
243
269
tabIndex : '0' ,
244
270
seconds : false ,
245
271
value : '' ,
272
+ step : 30 ,
246
273
} ;
247
274
248
275
TimePicker . propTypes = {
249
276
className : PropTypes . string ,
250
277
name : PropTypes . string . isRequired ,
251
278
onChange : PropTypes . func ,
252
279
seconds : PropTypes . bool ,
280
+ step : ( props , propName ) =>
281
+ MINUTES_IN_HOUR % props [ propName ] > 0 &&
282
+ new Error ( 'step should be divisible by 60' ) ,
253
283
tabIndex : PropTypes . string ,
254
284
value : PropTypes . string ,
255
285
} ;
0 commit comments