diff --git a/packages/core/src/components/overlay/overlay.tsx b/packages/core/src/components/overlay/overlay.tsx index b7753d951d..b575063d04 100644 --- a/packages/core/src/components/overlay/overlay.tsx +++ b/packages/core/src/components/overlay/overlay.tsx @@ -283,7 +283,7 @@ export class Overlay extends AbstractPureComponent { * the `startFocusTrapElement`), depending on whether the element losing focus is inside the * Overlay. */ - private handleStartFocusTrapElementFocus = (e: React.FocusEvent) => { + private handleStartFocusTrapElementFocus = (e: React.FocusEvent) => { if (!this.props.enforceFocus || this.isAutoFocusing) { return; } @@ -293,7 +293,7 @@ export class Overlay extends AbstractPureComponent { // element in this transition group. if ( e.relatedTarget != null && - this.containerElement.current?.contains(e.relatedTarget as Element) && + this.containerElement.current?.contains(e.relatedTarget) && e.relatedTarget !== this.endFocusTrapElement.current ) { this.endFocusTrapElement.current?.focus({ preventScroll: true }); @@ -323,7 +323,7 @@ export class Overlay extends AbstractPureComponent { * `startFocusTrapElement`), depending on whether the element losing focus is inside the * Overlay. */ - private handleEndFocusTrapElementFocus = (e: React.FocusEvent) => { + private handleEndFocusTrapElementFocus = (e: React.FocusEvent) => { // No need for this.props.enforceFocus check here because this element is only rendered // when that prop is true. // During user interactions, e.relatedTarget will be defined, and we should wrap around to the @@ -332,7 +332,7 @@ export class Overlay extends AbstractPureComponent { // presses shift+tab from the first focusable element in the overlay. if ( e.relatedTarget != null && - this.containerElement.current?.contains(e.relatedTarget as Element) && + this.containerElement.current?.contains(e.relatedTarget) && e.relatedTarget !== this.startFocusTrapElement.current ) { const firstFocusableElement = getKeyboardFocusableElements(this.containerElement).shift(); diff --git a/packages/core/src/components/overlay2/overlay2.tsx b/packages/core/src/components/overlay2/overlay2.tsx index e7dbd86507..ece31dae0d 100644 --- a/packages/core/src/components/overlay2/overlay2.tsx +++ b/packages/core/src/components/overlay2/overlay2.tsx @@ -125,10 +125,10 @@ export const Overlay2 = React.forwardRef((props, /** Ref for backdrop element */ const backdropElement = React.useRef(null); - /* An empty, keyboard-focusable div at the beginning of the Overlay content */ + /** An empty, keyboard-focusable div at the beginning of the Overlay content */ const startFocusTrapElement = React.useRef(null); - /* An empty, keyboard-focusable div at the end of the Overlay content */ + /** An empty, keyboard-focusable div at the end of the Overlay content */ const endFocusTrapElement = React.useRef(null); /** @@ -477,7 +477,7 @@ export const Overlay2 = React.forwardRef((props, * Overlay. */ const handleStartFocusTrapElementFocus = React.useCallback( - (e: React.FocusEvent) => { + (e: React.FocusEvent) => { if (!enforceFocus || isAutoFocusing) { return; } @@ -487,11 +487,7 @@ export const Overlay2 = React.forwardRef((props, // element in this transition group. const container = getRef(containerElement); const endFocusTrap = getRef(endFocusTrapElement); - if ( - e.relatedTarget != null && - container?.contains(e.relatedTarget as Element) && - e.relatedTarget !== endFocusTrap - ) { + if (e.relatedTarget != null && container?.contains(e.relatedTarget) && e.relatedTarget !== endFocusTrap) { endFocusTrap?.focus({ preventScroll: true }); } }, @@ -525,7 +521,7 @@ export const Overlay2 = React.forwardRef((props, * Overlay. */ const handleEndFocusTrapElementFocus = React.useCallback( - (e: React.FocusEvent) => { + (e: React.FocusEvent) => { // No need for this.props.enforceFocus check here because this element is only rendered // when that prop is true. // During user interactions, e.relatedTarget will be defined, and we should wrap around to the @@ -535,7 +531,7 @@ export const Overlay2 = React.forwardRef((props, const startFocusTrap = getRef(startFocusTrapElement); if ( e.relatedTarget != null && - getRef(containerElement)?.contains(e.relatedTarget as Element) && + getRef(containerElement)?.contains(e.relatedTarget) && e.relatedTarget !== startFocusTrap ) { const firstFocusableElement = getKeyboardFocusableElements(containerElement).shift(); diff --git a/packages/core/src/components/popover/popover.tsx b/packages/core/src/components/popover/popover.tsx index 5b34c697e2..c7d130aafe 100644 --- a/packages/core/src/components/popover/popover.tsx +++ b/packages/core/src/components/popover/popover.tsx @@ -37,6 +37,7 @@ import { } from "../../common"; import * as Errors from "../../common/errors"; import { Overlay2 } from "../overlay2/overlay2"; +import type { OverlayInstance } from "../overlay2/overlayInstance"; import { ResizeSensor } from "../resize-sensor/resizeSensor"; // eslint-disable-next-line import/no-cycle import { Tooltip } from "../tooltip/tooltip"; @@ -198,27 +199,29 @@ export class Popover< */ public targetRef = React.createRef(); - /** - * Overlay2 transition container element ref. - */ - private transitionContainerElement = React.createRef(); + /** Overlay2 transition container element ref */ + private transitionContainerRef = React.createRef(); + + /** Overlay2 overlay container ref */ + private overlayRef = React.createRef(); private cancelOpenTimeout?: () => void; - // a flag that lets us detect mouse movement between the target and popover, - // now that mouseleave is triggered when you cross the gap between the two. + /** + * A flag that lets us detect mouse movement between the target and popover, + * now that mouseleave is triggered when you cross the gap between the two. + */ private isMouseInTargetOrPopover = false; - // a flag that indicates whether the target previously lost focus to another - // element on the same page. + /** A flag that indicates whether the target previously lost focus to another element on the same page */ private lostFocusOnSamePage = true; - // Reference to the Poppper.scheduleUpdate() function, this changes every time the popper is mounted + /** Reference to the Poppper.scheduleUpdate() function, this changes every time the popper is mounted */ private popperScheduleUpdate?: () => Promise | null>; private isControlled = () => this.props.isOpen !== undefined; - // arrow is disabled if minimal, or if the arrow modifier was explicitly disabled + /** Arrow is disabled if minimal, or if the arrow modifier was explicitly disabled */ private isArrowEnabled = () => !this.props.minimal && this.props.modifiers?.arrow?.enabled !== false; private isHoverInteractionKind = () => { @@ -503,7 +506,7 @@ export class Popover< backdropProps={backdropProps} canEscapeKeyClose={canEscapeKeyClose} canOutsideClickClose={interactionKind === PopoverInteractionKind.CLICK} - childRef={this.transitionContainerElement} + childRef={this.transitionContainerRef} enforceFocus={enforceFocus} hasBackdrop={hasBackdrop} isOpen={isOpen} @@ -513,6 +516,7 @@ export class Popover< onClosing={this.props.onClosing} onOpened={this.props.onOpened} onOpening={this.props.onOpening} + ref={this.overlayRef} transitionDuration={this.props.transitionDuration} transitionName={Classes.POPOVER} usePortal={usePortal} @@ -529,7 +533,7 @@ export class Popover< // accepts a ref object (not a callback) due to a CSSTransition API limitation. // N.B. react-popper has a wide type for this ref, but we can narrow it based on the source, // see https://github.com/floating-ui/react-popper/blob/beac280d61082852c4efc302be902911ce2d424c/src/Popper.js#L94 - ref={mergeRefs(popperProps.ref as React.RefCallback, this.transitionContainerElement)} + ref={mergeRefs(popperProps.ref as React.RefCallback, this.transitionContainerRef)} style={popperProps.style} > @@ -612,30 +616,27 @@ export class Popover< return popperModifiers; } - private handleTargetFocus = (e: React.FocusEvent) => { + private handleTargetFocus = (e: React.FocusEvent) => { if (this.props.openOnTargetFocus && this.isHoverInteractionKind()) { if (e.relatedTarget == null && !this.lostFocusOnSamePage) { // ignore this focus event -- the target was already focused but the page itself // lost focus (e.g. due to switching tabs). return; } - this.handleMouseEnter(e as unknown as React.MouseEvent); + this.handleMouseEnter(e); } }; - private handleTargetBlur = (e: React.FocusEvent) => { + private handleTargetBlur = (e: React.FocusEvent) => { if (this.props.openOnTargetFocus && this.isHoverInteractionKind()) { if (e.relatedTarget != null) { - // if the next element to receive focus is within the popover, we'll want to leave the - // popover open. - if ( - e.relatedTarget !== this.popoverElement && - !this.isElementInPopover(e.relatedTarget as HTMLElement) - ) { - this.handleMouseLeave(e as unknown as React.MouseEvent); + // if the next element to receive focus is within the overlay (such as the Overlay + // startFocusTrapElement), we'll want to leave the popover open. + if (!this.isElementInOverlay(e.relatedTarget)) { + this.handleMouseLeave(e); } } else { - this.handleMouseLeave(e as unknown as React.MouseEvent); + this.handleMouseLeave(e); } } this.lostFocusOnSamePage = e.relatedTarget != null; @@ -649,7 +650,7 @@ export class Popover< } }; - private handleMouseEnter = (e: React.MouseEvent) => { + private handleMouseEnter = (e: React.MouseEvent | React.SyntheticEvent) => { this.isMouseInTargetOrPopover = true; // if we're entering the popover, and the mode is set to be HOVER_TARGET_ONLY, we want to manually @@ -667,7 +668,7 @@ export class Popover< } }; - private handleMouseLeave = (e: React.MouseEvent) => { + private handleMouseLeave = (e: React.MouseEvent | React.SyntheticEvent) => { this.isMouseInTargetOrPopover = false; // Wait until the event queue is flushed, because we want to leave the @@ -784,6 +785,10 @@ export class Popover< return this.getPopoverElement()?.contains(element) ?? false; } + private isElementInOverlay(element: Element) { + return this.overlayRef.current?.containerElement.current?.contains(element); + } + private getIsContentEmpty() { const { content } = this.props; return content == null || Utils.isEmptyString(content);