From fd09783a450f262cf68fccba555551a70b5da1f2 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Tue, 28 Dec 2021 18:48:07 -0600 Subject: [PATCH] toolbar: work around tooltip bugs This works around several bugs where tooltips don't close when expected. Fixes: https://github.com/pybricks/pybricks-code/issues/275 --- CHANGELOG.md | 7 +++ src/toolbar/ActionButton.tsx | 20 ++++++--- src/toolbar/OpenFileButton.tsx | 21 ++++++--- src/utils/monkey-patch.test.tsx | 76 +++++++++++++++++++++++++++++++++ src/utils/monkey-patch.ts | 68 +++++++++++++++++++++++++++++ 5 files changed, 179 insertions(+), 13 deletions(-) create mode 100644 src/utils/monkey-patch.test.tsx create mode 100644 src/utils/monkey-patch.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index d4302e63..797ddc16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ # Changelog +## [Unreleased] + +### Fixed +- Fix tooltips not closing when expected ([pybricks-code#275]). + +[pybricks-code#275]: https://github.com/pybricks/pybricks-code/issues/275 + ## [1.2.0-beta.1] - 2021-12-27 ### Added diff --git a/src/toolbar/ActionButton.tsx b/src/toolbar/ActionButton.tsx index 9f893e6d..8de557c3 100644 --- a/src/toolbar/ActionButton.tsx +++ b/src/toolbar/ActionButton.tsx @@ -13,6 +13,7 @@ import { Tooltip2 } from '@blueprintjs/popover2'; import { useI18n } from '@shopify/react-i18n'; import React, { useEffect, useMemo, useState } from 'react'; import { tooltipDelay } from '../app/constants'; +import { closeTooltip2, useTooltip2MonkeyPatch } from '../utils/monkey-patch'; import { TooltipId } from './i18n'; import en from './i18n.en.json'; @@ -90,22 +91,29 @@ const ActionButton: React.FC = (props) => { useHotkeys(hotkeys); + const tooltipRef = useTooltip2MonkeyPatch(); + return ( ( + )} + /> + ); +}; + +it.each([false, true])( + 'should work around https://github.com/palantir/blueprint/issues/4503', + async (monkeyPatch) => { + render(); + + expect(screen.queryByText(testTooltipText)).not.toBeInTheDocument(); + + // pressing the tab key should focus the button and open the tooltip + userEvent.tab(); + + await waitFor(() => { + expect(screen.getByText(testTooltipText)).toBeInTheDocument(); + }); + + // a blur event with renderTarget=null should trigger the bug and workaround + fireEvent.blur(screen.getByTestId(testButtonId)); + + if (monkeyPatch) { + // if the patch was applied, there should not be any error + await waitForElementToBeRemoved(screen.getByText(testTooltipText)); + } else { + // if the patch was not applied, the bug should be triggered + try { + await waitForElementToBeRemoved(screen.getByText(testTooltipText)); + fail('bug was not triggered'); + } catch (err) { + // error was expected + } + } + }, +); diff --git a/src/utils/monkey-patch.ts b/src/utils/monkey-patch.ts new file mode 100644 index 00000000..4d16252b --- /dev/null +++ b/src/utils/monkey-patch.ts @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2021 The Pybricks Authors + +import { Tooltip2 } from '@blueprintjs/popover2'; +import React, { useEffect, useRef } from 'react'; + +/** Hack to access private members of Popover2. */ +interface Popover2Private { + handleTargetBlur: (e: React.FocusEvent) => void; + handleMouseLeave: (e: React.MouseEvent) => void; +} +/** Hack to access private members of Tooltip2. */ +interface Tooltip2Private { + popover: Popover2Private; +} + +/** + * Monkey patch Popover2 blur event handler to make tooltips close when + * focus is lost. + * + * https://github.com/palantir/blueprint/issues/4503 + * + * @returns A React ref to be attached to the Tooltip2 component. + */ +export function useTooltip2MonkeyPatch(): React.RefObject> { + const tooltipRef = useRef>(null); + + useEffect(() => { + if (!tooltipRef.current) { + return; + } + + const tooltip = tooltipRef.current as unknown as Tooltip2Private; + + const oldHandleTargetBlur = tooltip.popover.handleTargetBlur; + + tooltip.popover.handleTargetBlur = (e) => { + if (e.relatedTarget) { + oldHandleTargetBlur(e); + } else { + closeTooltip2(tooltipRef); + } + }; + + return () => { + tooltip.popover.handleTargetBlur = oldHandleTargetBlur; + }; + }, [tooltipRef]); + + return tooltipRef; +} + +/** + * Hack to access private members of Tooltip2 to programmatically close the tooltip. + * @param tooltipRef The reference returned from useTooltip2MonkeyPatch() + */ +export function closeTooltip2(tooltipRef: React.RefObject>): void { + if (!tooltipRef.current) { + return; + } + + const tooltip = tooltipRef.current as unknown as Tooltip2Private; + + // Currently, the event arg is not used so it should be safe to pass undefined. + tooltip.popover.handleMouseLeave( + undefined as unknown as React.MouseEvent, + ); +}