@@ -5,11 +5,26 @@ import classNames from 'classnames';
55import { Form , Icon } from '@openedx/paragon' ;
66import { Calendar } from '@openedx/paragon/icons' ;
77import { useIntl } from '@edx/frontend-platform/i18n' ;
8+ import moment from 'moment' ;
89
910import { convertToDateFromString , convertToStringFromDate , isValidDate } from '../../utils' ;
1011import { DATE_FORMAT , TIME_FORMAT } from '../../constants' ;
1112import messages from './messages' ;
1213
14+ const timeFormats = [ 'HH:mm' , 'H:mm' , 'hh:mm A' , 'h:mm A' , 'hh:mm a' , 'h:mm a' ] ;
15+ const timeStepMinutes = 30 ;
16+
17+ const scrollSelectedTimeIntoView = ( ) => {
18+ const schedule = typeof window !== 'undefined' && typeof window . requestAnimationFrame === 'function'
19+ ? window . requestAnimationFrame
20+ : ( ( cb ) => setTimeout ( cb , 0 ) ) ;
21+
22+ schedule ( ( ) => {
23+ const selectedItem = document . querySelector ( '.react-datepicker__time-list-item--selected' ) ;
24+ selectedItem ?. scrollIntoView ( { block : 'nearest' } ) ;
25+ } ) ;
26+ } ;
27+
1328export const DATEPICKER_TYPES = {
1429 date : 'date' ,
1530 time : 'time' ,
@@ -32,6 +47,46 @@ const DatepickerControl = ({
3247 [ DATEPICKER_TYPES . date ] : DATE_FORMAT ,
3348 [ DATEPICKER_TYPES . time ] : TIME_FORMAT ,
3449 } ;
50+ const isTimePicker = type === DATEPICKER_TYPES . time ;
51+
52+ const parseTimeValue = ( rawValue ) => {
53+ if ( ! rawValue ) {
54+ return null ;
55+ }
56+ const sanitized = rawValue . trim ( ) . replace ( / \s + / g, ' ' ) ;
57+ const parsed = moment ( sanitized , timeFormats , true ) ;
58+ if ( ! parsed . isValid ( ) ) {
59+ return null ;
60+ }
61+ return parsed ;
62+ } ;
63+
64+ const handleTimeKeyDown = ( event ) => {
65+ if ( ! isTimePicker || readonly ) {
66+ return ;
67+ }
68+ if ( event . key !== 'ArrowUp' && event . key !== 'ArrowDown' ) {
69+ return ;
70+ }
71+
72+ event . preventDefault ( ) ;
73+ const direction = event . key === 'ArrowUp' ? - 1 : 1 ;
74+ const parsedTime = parseTimeValue ( event . target . value ) ;
75+ const baseMoment = formattedDate ? moment ( formattedDate ) : moment ( ) . startOf ( 'day' ) ;
76+ const workingMoment = parsedTime
77+ ? baseMoment . clone ( ) . hours ( parsedTime . hours ( ) ) . minutes ( parsedTime . minutes ( ) )
78+ : baseMoment . clone ( ) ;
79+
80+ workingMoment . seconds ( 0 ) ;
81+ workingMoment . milliseconds ( 0 ) ;
82+
83+ const roundedMinutes = Math . floor ( workingMoment . minutes ( ) / timeStepMinutes ) * timeStepMinutes ;
84+ workingMoment . minutes ( roundedMinutes ) ;
85+
86+ const adjustedTime = workingMoment . add ( direction * timeStepMinutes , 'minutes' ) ;
87+ onChange ( convertToStringFromDate ( adjustedTime . toDate ( ) ) ) ;
88+ scrollSelectedTimeIntoView ( ) ;
89+ } ;
3590
3691 return (
3792 < Form . Group className = "form-group-custom datepicker-custom" >
@@ -67,6 +122,7 @@ const DatepickerControl = ({
67122 showTimeSelectOnly = { type === DATEPICKER_TYPES . time }
68123 placeholderText = { inputFormat [ type ] . toLocaleUpperCase ( ) }
69124 showPopperArrow = { false }
125+ onKeyDown = { isTimePicker ? handleTimeKeyDown : undefined }
70126 onChange = { ( date ) => {
71127 if ( isValidDate ( date ) ) {
72128 onChange ( convertToStringFromDate ( date ) ) ;
0 commit comments