Merge pull request #12190 from appsmithorg/feature/pp-datepicker
feat: added keyboard interaction for datepicker
This commit is contained in:
commit
ef79f6450d
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user