diff --git a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/DatePicker_With_Switch_spec.js b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/DatePicker_With_Switch_spec.js index 7d96af076b..32b0b760fb 100644 --- a/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/DatePicker_With_Switch_spec.js +++ b/app/client/cypress/integration/Smoke_TestSuite/ClientSideTests/FormWidgets/DatePicker_With_Switch_spec.js @@ -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") diff --git a/app/client/src/components/ads/DatePickerComponent.tsx b/app/client/src/components/ads/DatePickerComponent.tsx index e9cde36d92..eab135c208 100644 --- a/app/client/src/components/ads/DatePickerComponent.tsx +++ b/app/client/src/components/ads/DatePickerComponent.tsx @@ -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(null); + const inputRef = useRef(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 (