Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(DateRangeInput3) Add keyboard accessibility #7080

Merged
merged 3 commits into from
Nov 22, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import type {
DateRangeInput3PropsWithDefaults,
} from "./dateRangeInput3Props";
import type { DateRangeInput3State } from "./dateRangeInput3State";
import { clampDate, isEntireInputSelected, shiftDateByArrowKey, shiftDateByDays } from "./dateRangeInputUilts";

export type { DateRangeInput3Props };

Expand Down Expand Up @@ -465,7 +466,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 +482,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 +553,64 @@ export class DateRangeInput3 extends DateFnsLocalizedComponent<DateRangeInput3Pr
}
};

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

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

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

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

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

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

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

private getNextDateForArrowKeyNavigation(arrowKey: string, boundary: Boundary) {
const { allowSingleDayRange, maxDate, minDate } = 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, minDate, adjustedEnd)
: clampDate(relativeDate, adjustedStart, maxDate);
}

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

const selectedDate = otherBoundary == null ? new Date() : shiftDateByArrowKey(otherBoundary, arrowKey);
return clampDate(selectedDate, minDate, maxDate);
}

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
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/* !
* (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 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