Skip to content

Commit ecf7331

Browse files
committed
Implement focus management
1 parent eeaea3c commit ecf7331

File tree

8 files changed

+521
-130
lines changed

8 files changed

+521
-130
lines changed

components/dash-core-components/src/components/css/calendar.css

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@
5656
.dash-datepicker-calendar td.dash-datepicker-calendar-date-disabled {
5757
opacity: 0.6;
5858
cursor: not-allowed;
59-
background-color: inherit;
6059
pointer-events: none;
6160
}
6261

components/dash-core-components/src/fragments/DatePickerRange.tsx

Lines changed: 105 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
ArrowRightIcon,
99
} from '@radix-ui/react-icons';
1010
import AutosizeInput from 'react-input-autosize';
11-
import Calendar from '../utils/calendar/Calendar';
11+
import Calendar, {CalendarHandle} from '../utils/calendar/Calendar';
1212
import {DatePickerRangeProps, CalendarDirection} from '../types';
1313
import {
1414
dateAsStr,
@@ -59,7 +59,11 @@ const DatePickerRange = ({
5959
const direction = is_RTL
6060
? CalendarDirection.RightToLeft
6161
: CalendarDirection.LeftToRight;
62-
const initialMonth = strAsDate(initial_visible_month);
62+
const initialCalendarDate =
63+
strAsDate(initial_visible_month) ||
64+
internalStartDate ||
65+
internalEndDate;
66+
6367
const minDate = strAsDate(min_date_allowed);
6468
const maxDate = strAsDate(max_date_allowed);
6569
const disabledDates = useMemo(() => {
@@ -101,6 +105,7 @@ const DatePickerRange = ({
101105
const containerRef = useRef<HTMLDivElement>(null);
102106
const startInputRef = useRef<HTMLInputElement | null>(null);
103107
const endInputRef = useRef<HTMLInputElement | null>(null);
108+
const calendarRef = useRef<CalendarHandle>(null);
104109

105110
useEffect(() => {
106111
setInternalStartDate(strAsDate(start_date));
@@ -145,77 +150,107 @@ const DatePickerRange = ({
145150
}
146151
}, [isCalendarOpen, startInputValue]);
147152

148-
const sendStartInputAsDate = useCallback(() => {
149-
const parsed = strAsDate(startInputValue, display_format);
150-
const isValid =
151-
parsed && !isDateDisabled(parsed, minDate, maxDate, disabledDates);
153+
const sendStartInputAsDate = useCallback(
154+
(focusCalendar = false) => {
155+
if (startInputValue) {
156+
setInternalStartDate(undefined);
157+
}
158+
const parsed = strAsDate(startInputValue, display_format);
159+
const isValid =
160+
parsed &&
161+
!isDateDisabled(parsed, minDate, maxDate, disabledDates);
152162

153-
if (isValid) {
154-
setInternalStartDate(parsed);
155-
} else {
156-
// Invalid or disabled input: revert to previous valid date with proper formatting
157-
const previousDate = strAsDate(start_date);
158-
setStartInputValue(
159-
previousDate ? formatDate(previousDate, display_format) : ''
160-
);
161-
}
162-
}, [
163-
startInputValue,
164-
display_format,
165-
start_date,
166-
minDate,
167-
maxDate,
168-
disabledDates,
169-
]);
163+
if (isValid) {
164+
setInternalStartDate(parsed);
165+
if (focusCalendar) {
166+
calendarRef.current?.focusDate(parsed);
167+
} else {
168+
calendarRef.current?.setVisibleDate(parsed);
169+
}
170+
} else {
171+
// Invalid or disabled input: revert to previous valid date with proper formatting
172+
const previousDate = strAsDate(start_date);
173+
setStartInputValue(
174+
previousDate ? formatDate(previousDate, display_format) : ''
175+
);
176+
if (focusCalendar) {
177+
calendarRef.current?.focusDate(previousDate);
178+
}
179+
}
180+
},
181+
[
182+
startInputValue,
183+
display_format,
184+
start_date,
185+
minDate,
186+
maxDate,
187+
disabledDates,
188+
]
189+
);
170190

171-
const sendEndInputAsDate = useCallback(() => {
172-
const parsed = strAsDate(endInputValue, display_format);
173-
const isValid =
174-
parsed && !isDateDisabled(parsed, minDate, maxDate, disabledDates);
191+
const sendEndInputAsDate = useCallback(
192+
(focusCalendar = false) => {
193+
if (endInputValue === '') {
194+
setInternalEndDate(undefined);
195+
}
196+
const parsed = strAsDate(endInputValue, display_format);
197+
const isValid =
198+
parsed &&
199+
!isDateDisabled(parsed, minDate, maxDate, disabledDates);
175200

176-
if (isValid) {
177-
setInternalEndDate(parsed);
178-
} else {
179-
// Invalid or disabled input: revert to previous valid date with proper formatting
180-
const previousDate = strAsDate(end_date);
181-
setEndInputValue(
182-
previousDate ? formatDate(previousDate, display_format) : ''
183-
);
184-
}
185-
}, [
186-
endInputValue,
187-
display_format,
188-
end_date,
189-
minDate,
190-
maxDate,
191-
disabledDates,
192-
]);
201+
if (isValid) {
202+
setInternalEndDate(parsed);
203+
if (focusCalendar) {
204+
calendarRef.current?.focusDate(parsed);
205+
} else {
206+
calendarRef.current?.setVisibleDate(parsed);
207+
}
208+
} else {
209+
// Invalid or disabled input: revert to previous valid date with proper formatting
210+
const previousDate = strAsDate(end_date);
211+
setEndInputValue(
212+
previousDate ? formatDate(previousDate, display_format) : ''
213+
);
214+
if (focusCalendar) {
215+
calendarRef.current?.focusDate(previousDate);
216+
}
217+
}
218+
},
219+
[
220+
endInputValue,
221+
display_format,
222+
end_date,
223+
minDate,
224+
maxDate,
225+
disabledDates,
226+
]
227+
);
193228

194229
const clearSelection = useCallback(
195230
e => {
196-
e.preventDefault();
197231
setInternalStartDate(undefined);
198232
setInternalEndDate(undefined);
233+
startInputRef.current?.focus();
234+
e.preventDefault();
235+
e.stopPropagation();
199236
if (reopen_calendar_on_clear) {
200237
setIsCalendarOpen(true);
201-
} else {
202-
startInputRef.current?.focus();
203238
}
204239
},
205240
[reopen_calendar_on_clear]
206241
);
207242

208243
const handleStartInputKeyDown = useCallback(
209244
(e: React.KeyboardEvent<HTMLInputElement>) => {
210-
if (e.key === 'ArrowDown') {
245+
if (['ArrowUp', 'ArrowDown'].includes(e.key)) {
211246
e.preventDefault();
247+
sendStartInputAsDate(true);
212248
if (!isCalendarOpen) {
213-
sendStartInputAsDate();
214249
// open the calendar after resolving prop changes, so that
215250
// it opens with the correct date showing
216251
setTimeout(() => setIsCalendarOpen(true), 0);
217252
}
218-
} else if (e.key === 'Enter') {
253+
} else if (['Enter', 'Tab'].includes(e.key)) {
219254
sendStartInputAsDate();
220255
}
221256
},
@@ -224,15 +259,15 @@ const DatePickerRange = ({
224259

225260
const handleEndInputKeyDown = useCallback(
226261
(e: React.KeyboardEvent<HTMLInputElement>) => {
227-
if (e.key === 'ArrowDown') {
262+
if (['ArrowUp', 'ArrowDown'].includes(e.key)) {
228263
e.preventDefault();
264+
sendEndInputAsDate(true);
229265
if (!isCalendarOpen) {
230-
sendEndInputAsDate();
231266
// open the calendar after resolving prop changes, so that
232267
// it opens with the correct date showing
233268
setTimeout(() => setIsCalendarOpen(true), 0);
234269
}
235-
} else if (e.key === 'Enter') {
270+
} else if (['Enter', 'Tab'].includes(e.key)) {
236271
sendEndInputAsDate();
237272
}
238273
},
@@ -248,9 +283,6 @@ const DatePickerRange = ({
248283
classNames += ' ' + className;
249284
}
250285

251-
const initialCalendarDate =
252-
initialMonth || internalStartDate || internalEndDate;
253-
254286
const ArrowIcon =
255287
direction === CalendarDirection.LeftToRight
256288
? ArrowRightIcon
@@ -299,6 +331,12 @@ const DatePickerRange = ({
299331
aria-haspopup="dialog"
300332
aria-expanded={isCalendarOpen}
301333
aria-disabled={disabled}
334+
onClick={e => {
335+
e.preventDefault();
336+
if (!isCalendarOpen && !disabled) {
337+
setIsCalendarOpen(true);
338+
}
339+
}}
302340
>
303341
<CalendarIcon className="dash-datepicker-trigger-icon" />
304342
<AutosizeInput
@@ -311,10 +349,11 @@ const DatePickerRange = ({
311349
value={startInputValue}
312350
onChange={e => setStartInputValue(e.target.value)}
313351
onKeyDown={handleStartInputKeyDown}
314-
onBlur={sendStartInputAsDate}
315-
onClick={() => {
316-
if (!isCalendarOpen && !disabled) {
317-
setIsCalendarOpen(true);
352+
onFocus={() => {
353+
if (internalStartDate) {
354+
calendarRef.current?.setVisibleDate(
355+
internalStartDate
356+
);
318357
}
319358
}}
320359
placeholder={start_date_placeholder_text}
@@ -333,10 +372,11 @@ const DatePickerRange = ({
333372
value={endInputValue}
334373
onChange={e => setEndInputValue(e.target.value)}
335374
onKeyDown={handleEndInputKeyDown}
336-
onBlur={sendEndInputAsDate}
337-
onClick={() => {
338-
if (!isCalendarOpen && !disabled) {
339-
setIsCalendarOpen(true);
375+
onFocus={() => {
376+
if (internalEndDate) {
377+
calendarRef.current?.setVisibleDate(
378+
internalEndDate
379+
);
340380
}
341381
}}
342382
placeholder={end_date_placeholder_text}
@@ -365,6 +405,7 @@ const DatePickerRange = ({
365405
onOpenAutoFocus={e => e.preventDefault()}
366406
>
367407
<Calendar
408+
ref={calendarRef}
368409
initialVisibleDate={initialCalendarDate}
369410
selectionStart={internalStartDate}
370411
selectionEnd={internalEndDate}

0 commit comments

Comments
 (0)