From 9e6994229e38c29ecd383ad755ef768b7dc9c87c Mon Sep 17 00:00:00 2001 From: siddharth Date: Fri, 17 Jan 2025 18:34:27 +0530 Subject: [PATCH] fix: data-table loading state and keyboard controls --- .../components/data-table/index.tsx | 97 ++++++++++++++++--- .../cypress/e2e/terms-templates.cy.ts | 16 +-- apps/admin-panel/cypress/e2e/user.cy.ts | 2 +- 3 files changed, 93 insertions(+), 22 deletions(-) diff --git a/apps/admin-panel/components/data-table/index.tsx b/apps/admin-panel/components/data-table/index.tsx index 3fc94e50d..444fde2bb 100644 --- a/apps/admin-panel/components/data-table/index.tsx +++ b/apps/admin-panel/components/data-table/index.tsx @@ -50,7 +50,9 @@ const DataTable = ({ }: DataTableProps) => { const isMobile = useBreakpointDown("md") const [focusedRowIndex, setFocusedRowIndex] = useState(-1) + const [isTableFocused, setIsTableFocused] = useState(false) const tableRef = useRef(null) + const focusTimeoutRef = useRef() const router = useRouter() const getNavigationUrl = (item: T): string | null => { @@ -63,8 +65,46 @@ const DataTable = ({ return url !== null && url !== "" } + const isNoFocusActive = () => { + const activeElement = document.activeElement + const isBaseElement = + !activeElement || + activeElement === document.body || + activeElement === document.documentElement + const isOutsideTable = !tableRef.current?.contains(activeElement) + const isInteractiveElement = activeElement?.matches( + "button, input, select, textarea, a[href], [tabindex], [contenteditable]", + ) + return (isBaseElement || isOutsideTable) && !isInteractiveElement + } + + const smartFocus = () => { + if (isNoFocusActive()) { + if (focusTimeoutRef.current) { + clearTimeout(focusTimeoutRef.current) + } + + focusTimeoutRef.current = setTimeout(() => { + if (tableRef.current) { + tableRef.current.focus() + setIsTableFocused(true) + + const targetIndex = focusedRowIndex >= 0 ? focusedRowIndex : 0 + const targetRow = document.querySelector( + `[data-testid="table-row-${targetIndex}"]`, + ) as HTMLElement + + if (targetRow) { + targetRow.focus() + setFocusedRowIndex(targetIndex) + } + } + }, 0) + } + } + const focusRow = (index: number) => { - if (index < 0 || !data.length) return + if (index < 0 || !data.length || !isTableFocused) return const validIndex = Math.min(Math.max(0, index), data.length - 1) const row = document.querySelector( `[data-testid="table-row-${validIndex}"]`, @@ -78,15 +118,14 @@ const DataTable = ({ useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { + if (!tableRef.current?.contains(document.activeElement) || !isTableFocused) return if ( document.activeElement?.tagName === "INPUT" || document.activeElement?.tagName === "TEXTAREA" || document.activeElement?.tagName === "SELECT" || document.activeElement?.tagName === "BUTTON" - ) { + ) return - } - if (!data.length) return switch (e.key) { @@ -115,19 +154,44 @@ const DataTable = ({ } } - window.addEventListener("keydown", handleKeyDown) - return () => window.removeEventListener("keydown", handleKeyDown) + if (isTableFocused) { + window.addEventListener("keydown", handleKeyDown) + return () => window.removeEventListener("keydown", handleKeyDown) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [data, focusedRowIndex, onRowClick, navigateTo, isTableFocused]) + + useEffect(() => { + const shouldAutoFocus = data && data.length > 0 && !loading + if (shouldAutoFocus) { + smartFocus() + } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [data, focusedRowIndex, onRowClick, navigateTo]) + }, [data?.length, loading]) useEffect(() => { - if (data.length && focusedRowIndex === -1) { - focusRow(0) + const handleFocusOut = (e: FocusEvent) => { + if (!tableRef.current?.contains(e.relatedTarget as Node)) { + if (isNoFocusActive()) { + smartFocus() + } + } } + + document.addEventListener("focusout", handleFocusOut) + return () => document.removeEventListener("focusout", handleFocusOut) // eslint-disable-next-line react-hooks/exhaustive-deps - }, [data.length]) + }, []) + + useEffect(() => { + return () => { + if (focusTimeoutRef.current) { + clearTimeout(focusTimeoutRef.current) + } + } + }, []) - if (loading && !data) { + if (loading && !data.length) { return isMobile ? (
{Array.from({ length: 5 }).map((_, idx) => ( @@ -257,9 +321,16 @@ const DataTable = ({ return (
setIsTableFocused(true)} + onBlur={(e) => { + if (!tableRef.current?.contains(e.relatedTarget as Node)) { + setIsTableFocused(false) + setFocusedRowIndex(-1) + } + }} > diff --git a/apps/admin-panel/cypress/e2e/terms-templates.cy.ts b/apps/admin-panel/cypress/e2e/terms-templates.cy.ts index 231c68ede..080853991 100644 --- a/apps/admin-panel/cypress/e2e/terms-templates.cy.ts +++ b/apps/admin-panel/cypress/e2e/terms-templates.cy.ts @@ -20,17 +20,17 @@ describe("Terms Template", () => { cy.takeScreenshot("2_click_create_button") cy.get('[data-testid="terms-template-name-input"]') - .type(templateName) + .type(templateName, { delay: 0, waitForAnimations: false }) .should("have.value", templateName) cy.takeScreenshot("3_enter_template_name") cy.get('[data-testid="terms-template-annual-rate-input"]') - .type("5.5") + .type("5.5", { delay: 0, waitForAnimations: false }) .should("have.value", "5.5") cy.takeScreenshot("4_enter_annual_rate") cy.get('[data-testid="terms-template-duration-units-input"]') - .type("12") + .type("12", { delay: 0, waitForAnimations: false }) .should("have.value", "12") cy.takeScreenshot("5_enter_duration_units") @@ -47,22 +47,22 @@ describe("Terms Template", () => { cy.takeScreenshot("8_select_incurrence_interval") cy.get('[data-testid="terms-template-initial-cvl-input"]') - .type("140") + .type("140", { delay: 0, waitForAnimations: false }) .should("have.value", "140") cy.takeScreenshot("9_enter_initial_cvl") cy.get('[data-testid="terms-template-margin-call-cvl-input"]') - .type("120") + .type("120", { delay: 0, waitForAnimations: false }) .should("have.value", "120") cy.takeScreenshot("10_enter_margin_call_cvl") cy.get('[data-testid="terms-template-liquidation-cvl-input"]') - .type("110") + .type("110", { delay: 0, waitForAnimations: false }) .should("have.value", "110") cy.takeScreenshot("11_enter_liquidation_cvl") cy.get('[data-testid="terms-template-one-time-fee-rate-input"]') - .type("5") + .type("5", { delay: 0, waitForAnimations: false }) .should("have.value", "5") cy.get('[data-testid="terms-template-submit-button"]').click() @@ -98,7 +98,7 @@ describe("Terms Template", () => { cy.takeScreenshot("16_click_update_button") cy.get('[data-testid="terms-template-annual-rate-input"]') - .type("6") + .type("6", { delay: 0, waitForAnimations: false }) .should("have.value", "6") cy.takeScreenshot("17_update_annual_rate") diff --git a/apps/admin-panel/cypress/e2e/user.cy.ts b/apps/admin-panel/cypress/e2e/user.cy.ts index a8ae0abbe..8daaa46df 100644 --- a/apps/admin-panel/cypress/e2e/user.cy.ts +++ b/apps/admin-panel/cypress/e2e/user.cy.ts @@ -21,7 +21,7 @@ describe("Users", () => { cy.takeScreenshot("2_click_create_button") cy.get('[data-testid="create-user-email-input"]') - .type(userEmail) + .type(userEmail, { delay: 0, waitForAnimations: false }) .should("have.value", userEmail) cy.takeScreenshot("3_enter_email")