From b223463ae7fb85bf8ae2fa6d4e907a9289933c24 Mon Sep 17 00:00:00 2001 From: Sanne de Vries Date: Tue, 7 Jul 2026 14:09:44 +0200 Subject: [PATCH 1/3] Fix period arrow keys hijacking custom-range calendar - On periods with `< >` controls (Today, Month to Date, Last Month, Year to Date, specific day/month/year), the document-level ArrowLeft/ArrowRight listener kept firing while the calendar popover was open, silently shifting the period and closing the calendar. - Suppress that listener whenever a flatpickr calendar is mounted, via a new `isDateRangeCalendarOpen` predicate on `Keybind.shouldIgnoreWhen`. - Added a regression test covering both directions: arrows still shift the period when the calendar is closed, and no longer do so while it's open. --- .../query-periods/dashboard-dates.test.tsx | 34 ++++++++++++++++++- .../query-periods/date-range-calendar.tsx | 3 ++ .../query-periods/move-period-arrows.tsx | 26 +++++++++++--- 3 files changed, 57 insertions(+), 6 deletions(-) diff --git a/assets/js/dashboard/nav-menu/query-periods/dashboard-dates.test.tsx b/assets/js/dashboard/nav-menu/query-periods/dashboard-dates.test.tsx index 64d3eca180ff..4e790e8a47e1 100644 --- a/assets/js/dashboard/nav-menu/query-periods/dashboard-dates.test.tsx +++ b/assets/js/dashboard/nav-menu/query-periods/dashboard-dates.test.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { render, screen } from '@testing-library/react' +import { render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { TestContextProviders } from '../../../../test-utils/app-context-providers' import { stringifySearch } from '../../util/url-search-params' @@ -150,6 +150,38 @@ test.each([ } ) +test('arrow keys shift the period when the custom-range calendar is closed, but not while it is open', async () => { + const localDomain = 'no-arrow-hijack.test' + const startUrl = `${getRouterBasepath({ domain: localDomain, shared: false })}${stringifySearch({ period: 'month', date: '2024-08-15' })}` + + render(, { + wrapper: (props) => ( + + ) + }) + + expect(screen.getByText('August 2024')).toBeVisible() + expect(document.querySelector('.flatpickr-calendar')).toBeNull() + + await userEvent.keyboard('{ArrowLeft}') + expect(screen.getByText('July 2024')).toBeVisible() + + await userEvent.keyboard('c') + await waitFor(() => + expect(document.querySelector('.flatpickr-calendar')).not.toBeNull() + ) + + await userEvent.keyboard('{ArrowLeft}') + await userEvent.keyboard('{ArrowLeft}') + + expect(screen.getByText('July 2024')).toBeVisible() + expect(document.querySelector('.flatpickr-calendar')).not.toBeNull() +}) + test('going back resets the stored dashboardState period to previous value', async () => { const BrowserBackButton = () => { const navigate = useNavigate() diff --git a/assets/js/dashboard/nav-menu/query-periods/date-range-calendar.tsx b/assets/js/dashboard/nav-menu/query-periods/date-range-calendar.tsx index dae81d15077c..021594b3a9a6 100644 --- a/assets/js/dashboard/nav-menu/query-periods/date-range-calendar.tsx +++ b/assets/js/dashboard/nav-menu/query-periods/date-range-calendar.tsx @@ -2,6 +2,9 @@ import React, { useLayoutEffect, useRef } from 'react' import DatePicker from 'react-flatpickr' import { CustomLocale } from 'flatpickr/dist/types/locale' +export const isDateRangeCalendarOpen = (): boolean => + !!document.querySelector('.flatpickr-calendar') + const calendarLocale: Partial = { weekdays: { shorthand: ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'], diff --git a/assets/js/dashboard/nav-menu/query-periods/move-period-arrows.tsx b/assets/js/dashboard/nav-menu/query-periods/move-period-arrows.tsx index f5aa35ce0561..c3aaf04a1bbe 100644 --- a/assets/js/dashboard/nav-menu/query-periods/move-period-arrows.tsx +++ b/assets/js/dashboard/nav-menu/query-periods/move-period-arrows.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from 'react' +import React, { useCallback, useMemo } from 'react' import { shiftDashboardPeriod, getDateForShiftedPeriod @@ -6,11 +6,15 @@ import { import classNames from 'classnames' import { useDashboardStateContext } from '../../dashboard-state-context' import { useSiteContext } from '../../site-context' -import { NavigateKeybind } from '../../keybinding' -import { AppNavigationLink } from '../../navigation/use-app-navigate' +import { isModifierPressed, isTyping, Keybind } from '../../keybinding' +import { + AppNavigationLink, + useAppNavigate +} from '../../navigation/use-app-navigate' import { DashboardPeriod } from '../../dashboard-time-periods' import { useMatch } from 'react-router-dom' import { rootRoute } from '../../router' +import { isDateRangeCalendarOpen } from './date-range-calendar' const ArrowKeybind = ({ keyboardKey @@ -19,6 +23,7 @@ const ArrowKeybind = ({ }) => { const site = useSiteContext() const { dashboardState } = useDashboardStateContext() + const navigate = useAppNavigate() const search = useMemo( () => @@ -31,11 +36,22 @@ const ArrowKeybind = ({ [site, dashboardState, keyboardKey] ) + const handler = useCallback(() => { + navigate({ search }) + }, [navigate, search]) + return ( - ) } From 43b342b6cf2de656491bc6f2ed547bb0e6f89341 Mon Sep 17 00:00:00 2001 From: Sanne de Vries Date: Wed, 8 Jul 2026 08:25:02 +0200 Subject: [PATCH 2/3] Add changelog entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 58fd7d5a89d4..c878813e9c0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,7 @@ All notable changes to this project will be documented in this file. - Fixed issue with timestamps being rendered incorrectly in segment menus and modals for some timezones - Fixed main graph being clipped when the browser's root font size is smaller than the default 16px - Fixed issue with users with billing role not being able to create personal segments +- Fixed period arrow keys hijacking custom-range calendar ## v3.2.0 - 2026-01-16 From 2f3f3644bd74bfa6442e7ffa8fcf9af8e6d4d5c8 Mon Sep 17 00:00:00 2001 From: Sanne de Vries Date: Wed, 8 Jul 2026 13:57:00 +0200 Subject: [PATCH 3/3] Incorporate feedback --- assets/js/dashboard/keybinding.tsx | 6 +++-- .../query-periods/dashboard-dates.test.tsx | 6 ++--- .../query-periods/move-period-arrows.tsx | 27 +++++-------------- 3 files changed, 13 insertions(+), 26 deletions(-) diff --git a/assets/js/dashboard/keybinding.tsx b/assets/js/dashboard/keybinding.tsx index 646b35bd206c..7fa707d88432 100644 --- a/assets/js/dashboard/keybinding.tsx +++ b/assets/js/dashboard/keybinding.tsx @@ -113,11 +113,13 @@ export function Keybind(opts: KeybindOptions) { export function NavigateKeybind({ keyboardKey, type, - navigateProps + navigateProps, + shouldAlsoIgnoreWhen = [] }: { keyboardKey: string type: KeyboardEventType navigateProps: AppNavigationTarget + shouldAlsoIgnoreWhen?: Array<(event: KeyboardEvent) => boolean> }) { const navigate = useAppNavigate() const handler = useCallback(() => { @@ -129,7 +131,7 @@ export function NavigateKeybind({ keyboardKey={keyboardKey} type={type} handler={handler} - shouldIgnoreWhen={[isModifierPressed, isTyping]} + shouldIgnoreWhen={[isModifierPressed, isTyping, ...shouldAlsoIgnoreWhen]} targetRef="document" /> ) diff --git a/assets/js/dashboard/nav-menu/query-periods/dashboard-dates.test.tsx b/assets/js/dashboard/nav-menu/query-periods/dashboard-dates.test.tsx index 4e790e8a47e1..f57d6b80d552 100644 --- a/assets/js/dashboard/nav-menu/query-periods/dashboard-dates.test.tsx +++ b/assets/js/dashboard/nav-menu/query-periods/dashboard-dates.test.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { render, screen, waitFor } from '@testing-library/react' +import { render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { TestContextProviders } from '../../../../test-utils/app-context-providers' import { stringifySearch } from '../../util/url-search-params' @@ -171,9 +171,7 @@ test('arrow keys shift the period when the custom-range calendar is closed, but expect(screen.getByText('July 2024')).toBeVisible() await userEvent.keyboard('c') - await waitFor(() => - expect(document.querySelector('.flatpickr-calendar')).not.toBeNull() - ) + expect(document.querySelector('.flatpickr-calendar')).not.toBeNull() await userEvent.keyboard('{ArrowLeft}') await userEvent.keyboard('{ArrowLeft}') diff --git a/assets/js/dashboard/nav-menu/query-periods/move-period-arrows.tsx b/assets/js/dashboard/nav-menu/query-periods/move-period-arrows.tsx index c3aaf04a1bbe..bfce22d42ca3 100644 --- a/assets/js/dashboard/nav-menu/query-periods/move-period-arrows.tsx +++ b/assets/js/dashboard/nav-menu/query-periods/move-period-arrows.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useMemo } from 'react' +import React, { useMemo } from 'react' import { shiftDashboardPeriod, getDateForShiftedPeriod @@ -6,11 +6,8 @@ import { import classNames from 'classnames' import { useDashboardStateContext } from '../../dashboard-state-context' import { useSiteContext } from '../../site-context' -import { isModifierPressed, isTyping, Keybind } from '../../keybinding' -import { - AppNavigationLink, - useAppNavigate -} from '../../navigation/use-app-navigate' +import { NavigateKeybind } from '../../keybinding' +import { AppNavigationLink } from '../../navigation/use-app-navigate' import { DashboardPeriod } from '../../dashboard-time-periods' import { useMatch } from 'react-router-dom' import { rootRoute } from '../../router' @@ -23,7 +20,6 @@ const ArrowKeybind = ({ }) => { const site = useSiteContext() const { dashboardState } = useDashboardStateContext() - const navigate = useAppNavigate() const search = useMemo( () => @@ -36,22 +32,13 @@ const ArrowKeybind = ({ [site, dashboardState, keyboardKey] ) - const handler = useCallback(() => { - navigate({ search }) - }, [navigate, search]) - return ( - ) }