Skip to content

Commit

Permalink
feat(DateRangeInput3) Add keyboard accessibility (#7080)
Browse files Browse the repository at this point in the history
  • Loading branch information
jscheiny authored Nov 22, 2024
1 parent 1d1579d commit dc7c104
Show file tree
Hide file tree
Showing 4 changed files with 454 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,13 @@ import type {
DateRangeInput3PropsWithDefaults,
} from "./dateRangeInput3Props";
import type { DateRangeInput3State } from "./dateRangeInput3State";
import {
clampDate,
getTodayAtMidnight,
isEntireInputSelected,
shiftDateByArrowKey,
shiftDateByDays,
} from "./dateRangeInputUilts";

export type { DateRangeInput3Props };

Expand Down Expand Up @@ -465,7 +472,7 @@ export class DateRangeInput3 extends DateFnsLocalizedComponent<DateRangeInput3Pr
break;
case "keydown":
e = e as React.KeyboardEvent<HTMLInputElement>;
this.handleInputKeyDown(e);
this.handleInputKeyDown(e, boundary);
inputProps?.onKeyDown?.(e);
break;
case "mousedown":
Expand All @@ -481,14 +488,21 @@ export class DateRangeInput3 extends DateFnsLocalizedComponent<DateRangeInput3Pr
// add a keydown listener to persistently change focus when tabbing:
// - if focused in start field, Tab moves focus to end field
// - if focused in end field, Shift+Tab moves focus to start field
private handleInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
private handleInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>, boundary: Boundary) => {
const isArrowKeyPresssed =
e.key === "ArrowUp" || e.key === "ArrowDown" || e.key === "ArrowLeft" || e.key === "ArrowRight";
const isTabPressed = e.key === "Tab";
const isEnterPressed = e.key === "Enter";
const isEscapeKeyPressed = e.key === "Escape";
const isShiftPressed = e.shiftKey;

const { selectedStart, selectedEnd } = this.state;

if (isArrowKeyPresssed) {
this.handleInputArrowKeyDown(e, boundary);
return;
}

if (isEscapeKeyPressed) {
this.startInputElement?.blur();
this.endInputElement?.blur();
Expand Down Expand Up @@ -545,6 +559,79 @@ export class DateRangeInput3 extends DateFnsLocalizedComponent<DateRangeInput3Pr
}
};

private handleInputArrowKeyDown = (e: React.KeyboardEvent<HTMLInputElement>, boundary: Boundary) => {
const { minDate, maxDate } = this.props;
const { isOpen } = this.state;
const inputElement = boundary === Boundary.START ? this.startInputElement : this.endInputElement;

if (!isOpen || !isEntireInputSelected(inputElement)) {
return;
}

const shiftedDate =
this.getNextDateForArrowKeyNavigation(e.key, boundary) ??
this.getDefaultDateForArrowKeyNavigation(e.key, boundary);

if (shiftedDate == null) {
return;
}

// We've commited to moving the selection, prevent the default arrow key interactions
e.preventDefault();

const clampedDate = clampDate(shiftedDate, minDate, maxDate);
const { keys } = this.getStateKeysAndValuesForBoundary(boundary);
const nextState: Partial<DateRangeInput3State> = {
[keys.inputString]: this.formatDate(clampedDate),
shouldSelectAfterUpdate: true,
};

if (!this.isControlled()) {
nextState[keys.selectedValue] = clampedDate;
}

this.props.onChange?.(this.getDateRangeForCallback(clampedDate, boundary));
this.setState(nextState);
};

private getNextDateForArrowKeyNavigation(arrowKey: string, boundary: Boundary) {
const { allowSingleDayRange } = this.props;
const [selectedStart, selectedEnd] = this.getSelectedRange();
const initialDate = boundary === Boundary.START ? selectedStart : selectedEnd;
if (initialDate == null) {
return undefined;
}

const relativeDate = shiftDateByArrowKey(initialDate, arrowKey);

// Ensure that we don't move onto a single day range selection if that is disallowed
const adjustedStart =
selectedStart == null || allowSingleDayRange ? selectedStart : shiftDateByDays(selectedStart, 1);
const adjustedEnd = selectedEnd == null || allowSingleDayRange ? selectedEnd : shiftDateByDays(selectedEnd, -1);

return boundary === Boundary.START
? clampDate(relativeDate, undefined, adjustedEnd)
: clampDate(relativeDate, adjustedStart, undefined);
}

private getDefaultDateForArrowKeyNavigation(arrowKey: string, boundary: Boundary) {
const [selectedStart, selectedEnd] = this.getSelectedRange();
const otherBoundary = boundary === Boundary.START ? selectedEnd : selectedStart;

if (otherBoundary == null) {
return getTodayAtMidnight();
}

const isForwardArrowKey = arrowKey === "ArrowRight" || arrowKey === "ArrowDown";
// If the arrow key direction is in the same direction as the boundary, then moving that way will not create an
// overlapping date range
if (isForwardArrowKey === (boundary === Boundary.END)) {
return shiftDateByArrowKey(otherBoundary, arrowKey);
}

return undefined;
}

private handleInputMouseDown = () => {
// clicking in the field constitutes an explicit focus change. we update
// the flag on "mousedown" instead of on "click", because it needs to be
Expand Down Expand Up @@ -692,7 +779,7 @@ export class DateRangeInput3 extends DateFnsLocalizedComponent<DateRangeInput3Pr
return false;
}

const fallbackDate = new Date(new Date().setHours(0, 0, 0, 0));
const fallbackDate = getTodayAtMidnight();
const [selectedStart, selectedEnd] = this.getSelectedRange([fallbackDate, fallbackDate]);

// case to check if the user has changed TimePicker values
Expand Down Expand Up @@ -928,7 +1015,7 @@ export class DateRangeInput3 extends DateFnsLocalizedComponent<DateRangeInput3Pr
newDate = getDateFnsParser(format, locale)(dateString);
}

return newDate === false ? new Date() : newDate;
return newDate === false ? getTodayAtMidnight() : newDate;
};

// called on date hover & selection
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/* !
* (c) Copyright 2024 Palantir Technologies Inc. All rights reserved.
*/

const DAY_IN_MILLIS = 1000 * 60 * 60 * 24;
const WEEK_IN_MILLIS = 7 * DAY_IN_MILLIS;

export function getTodayAtMidnight() {
return new Date(new Date().setHours(0, 0, 0, 0));
}

export function shiftDateByDays(date: Date, days: number): Date {
return new Date(date.valueOf() + days * DAY_IN_MILLIS);
}

export function shiftDateByWeeks(date: Date, weeks: number): Date {
return new Date(date.valueOf() + weeks * WEEK_IN_MILLIS);
}

export function shiftDateByArrowKey(date: Date, key: string): Date {
switch (key) {
case "ArrowUp":
return shiftDateByWeeks(date, -1);
case "ArrowDown":
return shiftDateByWeeks(date, 1);
case "ArrowLeft":
return shiftDateByDays(date, -1);
case "ArrowRight":
return shiftDateByDays(date, 1);
default:
return date;
}
}

export function clampDate(date: Date, minDate: Date | null | undefined, maxDate: Date | null | undefined) {
let result = date;
if (minDate != null && date < minDate) {
result = minDate;
}
if (maxDate != null && date > maxDate) {
result = maxDate;
}
return result;
}

export function isEntireInputSelected(element: HTMLInputElement | null) {
if (element == null) {
return false;
}

return element.selectionStart === 0 && element.selectionEnd === element.value.length;
}
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ function useContiguousCalendarViews(

const nextRangeStart = MonthAndYear.fromDate(selectedRange[0]);
const nextRangeEnd = MonthAndYear.fromDate(selectedRange[1]);
const hasSelectionEndChanged = prevSelectedRange.current[0]?.valueOf() === selectedRange[0]?.valueOf();

if (nextRangeStart == null && nextRangeEnd != null) {
// Only end date selected.
Expand Down Expand Up @@ -175,8 +176,11 @@ function useContiguousCalendarViews(
} else {
newDisplayMonth = nextRangeStart;
}
} else if (hasSelectionEndChanged) {
// If the selection end has changed, adjust the view to show the new end date
newDisplayMonth = singleMonthOnly ? nextRangeEnd : nextRangeEnd.getPreviousMonth();
} else {
// Different start and end date months, adjust display months.
// Otherwise, the selection start must have changed, show that
newDisplayMonth = nextRangeStart;
}
}
Expand Down
Loading

1 comment on commit dc7c104

@svc-palantir-github
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

feat(DateRangeInput3) Add keyboard accessibility (#7080)

Build artifact links for this commit: documentation | landing | table | demo

This is an automated comment from the deploy-preview CircleCI job.

Please sign in to comment.