Merge pull request #12190 from appsmithorg/feature/pp-datepicker

feat: added keyboard interaction for datepicker
This commit is contained in:
ankurrsinghal 2022-05-04 18:10:27 +05:30 committed by GitHub
commit ef79f6450d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 192 additions and 5 deletions

View File

@ -31,7 +31,7 @@ describe("Switch Widget within Form widget Functionality", function() {
cy.setDate(1, "ddd MMM DD YYYY");
const nextDay = dayjs().format("DD/MM/YYYY");
cy.log(nextDay);
cy.get(widgetsPage.actionSelect).click();
cy.get(widgetsPage.actionSelect).click({ force: true });
cy.get(commonlocators.chooseAction)
.children()
.contains("Reset widget")

View File

@ -1,4 +1,4 @@
import React from "react";
import React, { useEffect, useRef, useState } from "react";
import { DateInput, TimePrecision } from "@blueprintjs/datetime";
import styled from "constants/DefaultTheme";
import { Classes } from "./common";
@ -12,8 +12,8 @@ const StyledDateInput = styled(DateInput)`
props.theme.colors.propertyPane.buttonText};
border: 1px solid ${Colors.ALTO2};
border-radius: 0;
padding: 0px 8px;
height: 32px;
padding: 6px 8px;
height: 36px;
box-shadow: none;
&:focus {
@ -22,6 +22,18 @@ const StyledDateInput = styled(DateInput)`
}
}
.bp3-input-group input:focus {
border-color: var(--appsmith-color-black-900);
}
button,
select,
[tabindex]:not([tabindex="-1"]) {
&:focus {
outline: #6eb9f0 auto 2px !important;
}
}
.${Classes.DATE_PICKER_OVARLAY} {
background-color: ${(props) =>
props.theme.colors.propertyPane.radioGroupBg};
@ -101,20 +113,195 @@ interface DatePickerComponentProps {
parseDate?: (dateStr: string) => Date | null;
}
function getKeyboardFocusableElements(element: HTMLDivElement) {
return [
...element.querySelectorAll(
'button, input, textarea, select, details,[tabindex]:not([tabindex="-1"])',
),
].filter(
(el) => !el.hasAttribute("disabled") && !el.getAttribute("aria-hidden"),
);
}
function whetherItIsTheLastButtonInDatepicker(
element: HTMLElement,
buttonText: string,
) {
return (
element.nodeName === "BUTTON" &&
element.className === "bp3-button bp3-minimal" &&
element.innerText === buttonText
);
}
function useKeyboardNavigation(clearButtonText: string) {
const [isDatePickerVisible, setDatePickerVisibility] = useState(false);
// to get the latest visibility value
const DatePickerVisibilityRef = useRef(isDatePickerVisible);
DatePickerVisibilityRef.current = isDatePickerVisible;
const popoverRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
function handleDateInputClick() {
setDatePickerVisibility(true);
}
function handleKeydown(e: KeyboardEvent) {
if (document.activeElement === inputRef.current) {
if (e.key === "Enter") {
setDatePickerVisibility((value) => !value);
} else if (e.key === "Escape") {
setDatePickerVisibility(false);
} else if (e.key === "Tab") {
const popoverElement = popoverRef.current;
if (popoverElement) {
e.preventDefault();
const focusableElements = getKeyboardFocusableElements(
popoverElement,
);
const firstElement = focusableElements[0];
if (firstElement) {
(firstElement as any)?.focus();
}
}
}
} else {
const popoverElement = popoverRef.current;
if (popoverElement) {
if (DatePickerVisibilityRef.current) {
// if datepicker is visible on pressing
// escape hide it and put focus back to input
if (e.key === "Escape") {
e.preventDefault();
e.stopPropagation();
setDatePickerVisibility(false);
inputRef.current?.focus();
}
const focusableElements = getKeyboardFocusableElements(
popoverElement,
);
if (e.key === "Tab") {
if (e.shiftKey) {
const lastFocusedElementIndex = focusableElements.findIndex(
(element) => document.activeElement === element,
);
if (
lastFocusedElementIndex === 1 ||
lastFocusedElementIndex === 0
) {
const lastFocusableElement = focusableElements.find((element) =>
whetherItIsTheLastButtonInDatepicker(
element as HTMLElement,
clearButtonText,
),
);
if (lastFocusableElement) {
(lastFocusableElement as HTMLElement).focus();
e.preventDefault();
}
}
} else {
const lastFocusedElement = focusableElements.find(
(element) => document.activeElement === element,
);
if (lastFocusedElement) {
if (
whetherItIsTheLastButtonInDatepicker(
lastFocusedElement as HTMLElement,
clearButtonText,
)
) {
(focusableElements[0] as HTMLElement).focus();
e.preventDefault();
}
}
}
}
}
}
}
}
useEffect(() => {
document.body.addEventListener("keydown", handleKeydown);
return () => {
document.body.removeEventListener("keydown", handleKeydown);
};
}, []);
function handleInteraction(nextOpenState: boolean) {
setDatePickerVisibility(nextOpenState);
if (!nextOpenState) {
inputRef.current?.focus();
}
}
function handleTimePickerKeydown(e: React.KeyboardEvent) {
if (e.key === "Enter") {
setDatePickerVisibility(false);
e.stopPropagation();
inputRef.current?.focus();
}
}
return {
// state
isDatePickerVisible,
// references
inputRef,
popoverRef,
// event handlers
handleTimePickerKeydown,
handleDateInputClick,
handleInteraction,
};
}
function DatePickerComponent(props: DatePickerComponentProps) {
// this was added to check the Datepickers
// footer action bar last Clear button
const clearButtonText = "Clear";
const {
handleDateInputClick,
handleInteraction,
handleTimePickerKeydown,
inputRef,
isDatePickerVisible,
popoverRef,
} = useKeyboardNavigation(clearButtonText);
return (
<StyledDateInput
className={Classes.DATE_PICKER_OVARLAY}
clearButtonText={clearButtonText}
closeOnSelection={props.closeOnSelection}
formatDate={props.formatDate}
highlightCurrentDay={props.highlightCurrentDay}
inputProps={{
inputRef: inputRef,
onClick: handleDateInputClick,
}}
maxDate={props.maxDate}
minDate={props.minDate}
onChange={props.onChange}
parseDate={props.parseDate}
placeholder={props.placeholder}
popoverProps={{ usePortal: true }}
popoverProps={{
popoverRef: popoverRef,
onInteraction: handleInteraction,
usePortal: true,
isOpen: isDatePickerVisible,
}}
showActionsBar={props.showActionsBar}
timePickerProps={{
onKeyDown: handleTimePickerKeydown,
}}
timePrecision={props.timePrecision}
value={props.value}
/>