From aa48899717ebe60a4b5ecae7dface49951c2d39e Mon Sep 17 00:00:00 2001 From: Nico Hinderling Date: Thu, 2 Apr 2026 16:18:56 -0700 Subject: [PATCH 1/5] feat(dashboards): Add ESC key to dismiss widget builder slideout Pressing Escape while the widget builder slideout is open now triggers the same close flow as the Close button, including the unsaved-changes confirmation modal when edits are pending. Co-Authored-By: Claude Opus 4.6 --- .../widgetBuilder/components/widgetBuilderSlideout.tsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/static/app/views/dashboards/widgetBuilder/components/widgetBuilderSlideout.tsx b/static/app/views/dashboards/widgetBuilder/components/widgetBuilderSlideout.tsx index b17494c071277b..02a0684c8f440c 100644 --- a/static/app/views/dashboards/widgetBuilder/components/widgetBuilderSlideout.tsx +++ b/static/app/views/dashboards/widgetBuilder/components/widgetBuilderSlideout.tsx @@ -234,6 +234,16 @@ export function WidgetBuilderSlideout({ }); }, [initialState, onClose, state]); + useEffect(() => { + function handleKeyDown(event: KeyboardEvent) { + if (event.key === 'Escape') { + onCloseWithModal(); + } + } + document.addEventListener('keydown', handleKeyDown); + return () => document.removeEventListener('keydown', handleKeyDown); + }, [onCloseWithModal]); + const breadcrumbs = customizeFromLibrary ? [ { From 0373efb751ba9ca5c5413b79993c025129bb196b Mon Sep 17 00:00:00 2001 From: Nico Hinderling Date: Thu, 2 Apr 2026 16:32:50 -0700 Subject: [PATCH 2/5] fix(dashboards): Skip ESC handler when confirmation modal is open MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The document-level keydown listener for Escape fired even when the confirmation modal was open, causing it to immediately reopen after being dismissed — trapping the user in a loop. Check global modal visibility and defaultPrevented before triggering the close handler. Co-Authored-By: Claude Opus 4.6 --- .../widgetBuilder/components/widgetBuilderSlideout.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/static/app/views/dashboards/widgetBuilder/components/widgetBuilderSlideout.tsx b/static/app/views/dashboards/widgetBuilder/components/widgetBuilderSlideout.tsx index 02a0684c8f440c..34e1252b7ac7fe 100644 --- a/static/app/views/dashboards/widgetBuilder/components/widgetBuilderSlideout.tsx +++ b/static/app/views/dashboards/widgetBuilder/components/widgetBuilderSlideout.tsx @@ -19,6 +19,7 @@ import {SlideOverPanel} from '@sentry/scraps/slideOverPanel'; import {Breadcrumbs} from 'sentry/components/breadcrumbs'; import {openConfirmModal} from 'sentry/components/confirm'; import {ErrorBoundary} from 'sentry/components/errorBoundary'; +import {useGlobalModal} from 'sentry/components/globalModal/useGlobalModal'; import {Placeholder} from 'sentry/components/placeholder'; import {IconClose} from 'sentry/icons'; import {t, tctCode} from 'sentry/locale'; @@ -99,6 +100,7 @@ export function WidgetBuilderSlideout({ }: WidgetBuilderSlideoutProps) { const organization = useOrganization(); const location = useLocation(); + const {visible: isModalVisible} = useGlobalModal(); const {state, dispatch} = useWidgetBuilderContext(); const [initialState] = useState(state); const [customizeFromLibrary, setCustomizeFromLibrary] = useState(false); @@ -236,13 +238,13 @@ export function WidgetBuilderSlideout({ useEffect(() => { function handleKeyDown(event: KeyboardEvent) { - if (event.key === 'Escape') { + if (event.key === 'Escape' && !event.defaultPrevented && !isModalVisible) { onCloseWithModal(); } } document.addEventListener('keydown', handleKeyDown); return () => document.removeEventListener('keydown', handleKeyDown); - }, [onCloseWithModal]); + }, [onCloseWithModal, isModalVisible]); const breadcrumbs = customizeFromLibrary ? [ From cab9f4f69bb73eee7b8fbbf2bcf90f631996aa6c Mon Sep 17 00:00:00 2001 From: Nico Hinderling Date: Tue, 7 Apr 2026 10:22:18 -0700 Subject: [PATCH 3/5] ref(dashboards): Use useHotkeys for ESC key handling in widget builder Replace raw useEffect + addEventListener with the useHotkeys hook from @sentry/scraps/hotkey, matching the pattern used by the global drawer. Co-Authored-By: Claude Opus 4.6 --- .../components/widgetBuilderSlideout.tsx | 29 ++++++++----------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/static/app/views/dashboards/widgetBuilder/components/widgetBuilderSlideout.tsx b/static/app/views/dashboards/widgetBuilder/components/widgetBuilderSlideout.tsx index 34e1252b7ac7fe..3d5036907e92b8 100644 --- a/static/app/views/dashboards/widgetBuilder/components/widgetBuilderSlideout.tsx +++ b/static/app/views/dashboards/widgetBuilder/components/widgetBuilderSlideout.tsx @@ -1,17 +1,11 @@ -import { - Fragment, - useCallback, - useEffect, - useMemo, - useState, - type RefCallback, -} from 'react'; +import {Fragment, useCallback, useMemo, useState, type RefCallback} from 'react'; import {useTheme} from '@emotion/react'; import styled from '@emotion/styled'; import isEqual from 'lodash/isEqual'; import {Alert} from '@sentry/scraps/alert'; import {Button} from '@sentry/scraps/button'; +import {useHotkeys} from '@sentry/scraps/hotkey'; import {Flex} from '@sentry/scraps/layout'; import {ExternalLink, Link} from '@sentry/scraps/link'; import {SlideOverPanel} from '@sentry/scraps/slideOverPanel'; @@ -236,15 +230,16 @@ export function WidgetBuilderSlideout({ }); }, [initialState, onClose, state]); - useEffect(() => { - function handleKeyDown(event: KeyboardEvent) { - if (event.key === 'Escape' && !event.defaultPrevented && !isModalVisible) { - onCloseWithModal(); - } - } - document.addEventListener('keydown', handleKeyDown); - return () => document.removeEventListener('keydown', handleKeyDown); - }, [onCloseWithModal, isModalVisible]); + useHotkeys([ + { + match: 'Escape', + callback: () => { + if (!isModalVisible) { + onCloseWithModal(); + } + }, + }, + ]); const breadcrumbs = customizeFromLibrary ? [ From 8dd28842b9f01825a93f4a20df79a1a177bf7fb8 Mon Sep 17 00:00:00 2001 From: Nico Hinderling Date: Tue, 7 Apr 2026 10:29:01 -0700 Subject: [PATCH 4/5] fix(dashboards): Restore missing useEffect import in widget builder The useHotkeys refactor accidentally removed useEffect from the React import while it's still used elsewhere in the component, causing a ReferenceError crash when opening the widget builder. Co-Authored-By: Claude Opus 4.6 --- .../widgetBuilder/components/widgetBuilderSlideout.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/static/app/views/dashboards/widgetBuilder/components/widgetBuilderSlideout.tsx b/static/app/views/dashboards/widgetBuilder/components/widgetBuilderSlideout.tsx index 3d5036907e92b8..0b917baac4e1e9 100644 --- a/static/app/views/dashboards/widgetBuilder/components/widgetBuilderSlideout.tsx +++ b/static/app/views/dashboards/widgetBuilder/components/widgetBuilderSlideout.tsx @@ -1,4 +1,11 @@ -import {Fragment, useCallback, useMemo, useState, type RefCallback} from 'react'; +import { + Fragment, + useCallback, + useEffect, + useMemo, + useState, + type RefCallback, +} from 'react'; import {useTheme} from '@emotion/react'; import styled from '@emotion/styled'; import isEqual from 'lodash/isEqual'; From ac660efa3e2504a145a8bad4b320430ec9f6520e Mon Sep 17 00:00:00 2001 From: Nico Hinderling Date: Tue, 7 Apr 2026 12:03:25 -0700 Subject: [PATCH 5/5] fix(dashboards): Prevent ESC from blocking confirmation modal dismiss useHotkeys calls preventDefault before the callback, so when the confirmation modal is open the event gets swallowed before GlobalModal can handle it. Use skipPreventDefault and call preventDefault only when we actually close the slideout. --- .../widgetBuilder/components/widgetBuilderSlideout.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/static/app/views/dashboards/widgetBuilder/components/widgetBuilderSlideout.tsx b/static/app/views/dashboards/widgetBuilder/components/widgetBuilderSlideout.tsx index 0b917baac4e1e9..9f2a3bd39d9bd6 100644 --- a/static/app/views/dashboards/widgetBuilder/components/widgetBuilderSlideout.tsx +++ b/static/app/views/dashboards/widgetBuilder/components/widgetBuilderSlideout.tsx @@ -240,8 +240,10 @@ export function WidgetBuilderSlideout({ useHotkeys([ { match: 'Escape', - callback: () => { + skipPreventDefault: true, + callback: (evt: KeyboardEvent) => { if (!isModalVisible) { + evt.preventDefault(); onCloseWithModal(); } },