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");
|
cy.setDate(1, "ddd MMM DD YYYY");
|
||||||
const nextDay = dayjs().format("DD/MM/YYYY");
|
const nextDay = dayjs().format("DD/MM/YYYY");
|
||||||
cy.log(nextDay);
|
cy.log(nextDay);
|
||||||
cy.get(widgetsPage.actionSelect).click();
|
cy.get(widgetsPage.actionSelect).click({ force: true });
|
||||||
cy.get(commonlocators.chooseAction)
|
cy.get(commonlocators.chooseAction)
|
||||||
.children()
|
.children()
|
||||||
.contains("Reset widget")
|
.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 { DateInput, TimePrecision } from "@blueprintjs/datetime";
|
||||||
import styled from "constants/DefaultTheme";
|
import styled from "constants/DefaultTheme";
|
||||||
import { Classes } from "./common";
|
import { Classes } from "./common";
|
||||||
|
|
@ -12,8 +12,8 @@ const StyledDateInput = styled(DateInput)`
|
||||||
props.theme.colors.propertyPane.buttonText};
|
props.theme.colors.propertyPane.buttonText};
|
||||||
border: 1px solid ${Colors.ALTO2};
|
border: 1px solid ${Colors.ALTO2};
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
padding: 0px 8px;
|
padding: 6px 8px;
|
||||||
height: 32px;
|
height: 36px;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
|
|
||||||
&:focus {
|
&: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} {
|
.${Classes.DATE_PICKER_OVARLAY} {
|
||||||
background-color: ${(props) =>
|
background-color: ${(props) =>
|
||||||
props.theme.colors.propertyPane.radioGroupBg};
|
props.theme.colors.propertyPane.radioGroupBg};
|
||||||
|
|
@ -101,20 +113,195 @@ interface DatePickerComponentProps {
|
||||||
parseDate?: (dateStr: string) => Date | null;
|
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) {
|
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 (
|
return (
|
||||||
<StyledDateInput
|
<StyledDateInput
|
||||||
className={Classes.DATE_PICKER_OVARLAY}
|
className={Classes.DATE_PICKER_OVARLAY}
|
||||||
|
clearButtonText={clearButtonText}
|
||||||
closeOnSelection={props.closeOnSelection}
|
closeOnSelection={props.closeOnSelection}
|
||||||
formatDate={props.formatDate}
|
formatDate={props.formatDate}
|
||||||
highlightCurrentDay={props.highlightCurrentDay}
|
highlightCurrentDay={props.highlightCurrentDay}
|
||||||
|
inputProps={{
|
||||||
|
inputRef: inputRef,
|
||||||
|
onClick: handleDateInputClick,
|
||||||
|
}}
|
||||||
maxDate={props.maxDate}
|
maxDate={props.maxDate}
|
||||||
minDate={props.minDate}
|
minDate={props.minDate}
|
||||||
onChange={props.onChange}
|
onChange={props.onChange}
|
||||||
parseDate={props.parseDate}
|
parseDate={props.parseDate}
|
||||||
placeholder={props.placeholder}
|
placeholder={props.placeholder}
|
||||||
popoverProps={{ usePortal: true }}
|
popoverProps={{
|
||||||
|
popoverRef: popoverRef,
|
||||||
|
onInteraction: handleInteraction,
|
||||||
|
usePortal: true,
|
||||||
|
isOpen: isDatePickerVisible,
|
||||||
|
}}
|
||||||
showActionsBar={props.showActionsBar}
|
showActionsBar={props.showActionsBar}
|
||||||
|
timePickerProps={{
|
||||||
|
onKeyDown: handleTimePickerKeydown,
|
||||||
|
}}
|
||||||
timePrecision={props.timePrecision}
|
timePrecision={props.timePrecision}
|
||||||
value={props.value}
|
value={props.value}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user