diff --git a/superset-frontend/plugins/plugin-chart-ag-grid-table/src/AgGridTable/components/CustomPopover.tsx b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/AgGridTable/components/CustomPopover.tsx index e630986bf69a..d2e404e98c24 100644 --- a/superset-frontend/plugins/plugin-chart-ag-grid-table/src/AgGridTable/components/CustomPopover.tsx +++ b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/AgGridTable/components/CustomPopover.tsx @@ -69,15 +69,21 @@ const CustomPopover: React.FC = ({ } }; + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape') onClose(); + }; + if (isOpen) { updatePosition(); document.addEventListener('mousedown', handleClickOutside); + document.addEventListener('keydown', handleKeyDown); window.addEventListener('scroll', updatePosition); window.addEventListener('resize', updatePosition); } return () => { document.removeEventListener('mousedown', handleClickOutside); + document.removeEventListener('keydown', handleKeyDown); window.removeEventListener('scroll', updatePosition); window.removeEventListener('resize', updatePosition); }; diff --git a/superset-frontend/plugins/preset-chart-deckgl/src/components/Tooltip.tsx b/superset-frontend/plugins/preset-chart-deckgl/src/components/Tooltip.tsx index 1eca2c1a77fe..002854b9f02c 100644 --- a/superset-frontend/plugins/preset-chart-deckgl/src/components/Tooltip.tsx +++ b/superset-frontend/plugins/preset-chart-deckgl/src/components/Tooltip.tsx @@ -19,7 +19,7 @@ import { safeHtmlSpan } from '@superset-ui/core'; import { styled } from '@apache-superset/core/theme'; -import { ReactNode } from 'react'; +import { ReactNode, useEffect, useRef, useState } from 'react'; export type TooltipProps = { tooltip: @@ -43,6 +43,12 @@ const StyledDiv = styled.div<{ top: ${top}px; left: ${left}px; z-index: 9; + /* deck.gl tooltips track the cursor across the canvas — capturing pointer + events here would block layer hover/click and create flicker loops as + the cursor enters and leaves the floating tooltip. Dismissal is via the + Escape key handler below; WCAG 1.4.13 "hoverable" is satisfied because + the tooltip remains visible under the cursor while pointing at the + feature. */ pointer-events: none; ${ variant === 'default' @@ -66,7 +72,39 @@ const StyledDiv = styled.div<{ export default function Tooltip(props: TooltipProps) { const { tooltip, variant = 'default' } = props; - if (typeof tooltip === 'undefined' || tooltip === null) { + const [dismissed, setDismissed] = useState(false); + const wasVisibleRef = useRef(false); + + // Reset dismissed when the tooltip transitions from hidden to visible. This + // handles the case where the cursor leaves the chart and re-enters a + // different pickable that happens to render the same content — content + // alone wouldn't trigger a reset there. + useEffect(() => { + const isVisible = !!tooltip; + if (isVisible && !wasVisibleRef.current) { + setDismissed(false); + } + wasVisibleRef.current = isVisible; + }, [tooltip]); + + // Reset on content change too (most common new-target signal). + useEffect(() => { + setDismissed(false); + }, [tooltip?.content]); + + // Bind the Escape listener once per visibility cycle. Depending on + // `tooltip` directly would re-bind on every cursor move (x/y change). + const visible = !!tooltip && !dismissed; + useEffect(() => { + if (!visible) return undefined; + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape') setDismissed(true); + }; + document.addEventListener('keydown', handleKeyDown); + return () => document.removeEventListener('keydown', handleKeyDown); + }, [visible]); + + if (!tooltip || dismissed) { return null; } diff --git a/superset-frontend/src/features/tasks/TaskPayloadPopover.tsx b/superset-frontend/src/features/tasks/TaskPayloadPopover.tsx index b8276327e577..e419919fdfb7 100644 --- a/superset-frontend/src/features/tasks/TaskPayloadPopover.tsx +++ b/superset-frontend/src/features/tasks/TaskPayloadPopover.tsx @@ -17,7 +17,7 @@ * under the License. */ -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { styled } from '@apache-superset/core/theme'; import { Popover } from '@superset-ui/core/components'; import { Icons } from '@superset-ui/core/components/Icons'; @@ -54,6 +54,14 @@ export default function TaskPayloadPopover({ }: TaskPayloadPopoverProps) { const [visible, setVisible] = useState(false); + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape') setVisible(false); + }; + if (visible) document.addEventListener('keydown', handleKeyDown); + return () => document.removeEventListener('keydown', handleKeyDown); + }, [visible]); + const content = ( {JSON.stringify(payload, null, 2)} diff --git a/superset-frontend/src/features/tasks/TaskStackTracePopover.tsx b/superset-frontend/src/features/tasks/TaskStackTracePopover.tsx index 18b7c725132c..82a2f746676c 100644 --- a/superset-frontend/src/features/tasks/TaskStackTracePopover.tsx +++ b/superset-frontend/src/features/tasks/TaskStackTracePopover.tsx @@ -17,7 +17,7 @@ * under the License. */ -import { useState, useCallback } from 'react'; +import { useState, useCallback, useEffect } from 'react'; import { t } from '@apache-superset/core/translation'; import { styled } from '@apache-superset/core/theme'; import { Popover, Tooltip } from '@superset-ui/core/components'; @@ -90,6 +90,14 @@ export default function TaskStackTracePopover({ const [copied, setCopied] = useState(false); const { addDangerToast } = useToasts(); + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape') setVisible(false); + }; + if (visible) document.addEventListener('keydown', handleKeyDown); + return () => document.removeEventListener('keydown', handleKeyDown); + }, [visible]); + const handleCopy = useCallback(() => { copyTextToClipboard(() => Promise.resolve(stackTrace)) .then(() => {