Skip to content

Commit 8eabbcb

Browse files
committed
feat: add keyboard accessibility for timepicker
1 parent 3089d0b commit 8eabbcb

1 file changed

Lines changed: 56 additions & 0 deletions

File tree

src/generic/datepicker-control/DatepickerControl.jsx

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,26 @@ import classNames from 'classnames';
55
import { Form, Icon } from '@openedx/paragon';
66
import { Calendar } from '@openedx/paragon/icons';
77
import { useIntl } from '@edx/frontend-platform/i18n';
8+
import moment from 'moment';
89

910
import { convertToDateFromString, convertToStringFromDate, isValidDate } from '../../utils';
1011
import { DATE_FORMAT, TIME_FORMAT } from '../../constants';
1112
import 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+
1328
export 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

Comments
 (0)