Skip to content

Commit e12afbe

Browse files
committed
add option to use laceholder before rendering ContextOverlay, activate it by default for ContextMenu
1 parent cd2f035 commit e12afbe

2 files changed

Lines changed: 102 additions & 18 deletions

File tree

src/components/ContextOverlay/ContextMenu.tsx

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,15 @@ export interface ContextMenuProps extends TestableComponent {
4040
* Props to spread to `ContextOverlay` that is used to display the dropdown.
4141
*/
4242
contextOverlayProps?: Partial<Omit<ContextOverlayProps, "content" | "children" | "className">>;
43-
/** Disables the button to open the menu. */
43+
/**
44+
* Disables the button to open the menu.
45+
*/
4446
disabled?: boolean;
47+
/**
48+
* We use the target as placeholder before the real `<ContextMenu /` is rendered on first hover or focus event.
49+
* In case of problems set this property to `true`.
50+
*/
51+
preventPlaceholder?: boolean;
4552
}
4653

4754
/**
@@ -58,27 +65,33 @@ export const ContextMenu = ({
5865
/* FIXME: The Tooltip component can interfere with the opened menu, since it is implemented via portal and may cover the menu,
5966
so by default we use the title attribute instead of Tooltip. */
6067
tooltipAsTitle = true,
68+
preventPlaceholder = false,
6169
...restProps
6270
}: ContextMenuProps) => {
71+
const toggleButton =
72+
typeof togglerElement === "string" ? (
73+
<IconButton
74+
tooltipAsTitle={tooltipAsTitle}
75+
name={[togglerElement]}
76+
text={togglerText}
77+
size={togglerLarge ? "large" : "medium"}
78+
disabled={!!disabled}
79+
data-test-id={restProps["data-test-id"]}
80+
/>
81+
) : (
82+
(togglerElement as ReactElement)
83+
);
84+
6385
return (
6486
<ContextOverlay
6587
{...restProps}
6688
{...contextOverlayProps}
6789
className={`${eccgui}-contextmenu ` + className}
6890
content={<Menu>{children}</Menu>}
91+
disabled={!!disabled}
92+
usePlaceholder={!preventPlaceholder}
6993
>
70-
{typeof togglerElement === "string" ? (
71-
<IconButton
72-
tooltipAsTitle={tooltipAsTitle}
73-
name={[togglerElement]}
74-
text={togglerText}
75-
large={togglerLarge}
76-
disabled={!!disabled}
77-
data-test-id={restProps["data-test-id"]}
78-
/>
79-
) : (
80-
(togglerElement as ReactElement)
81-
)}
94+
{toggleButton}
8295
</ContextOverlay>
8396
);
8497
};

src/components/ContextOverlay/ContextOverlay.tsx

Lines changed: 76 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import React from "react";
2-
import { Popover as BlueprintPopover, PopoverProps as BlueprintPopoverProps } from "@blueprintjs/core";
2+
import {
3+
Classes as BlueprintClasses,
4+
Popover as BlueprintPopover,
5+
PopoverProps as BlueprintPopoverProps,
6+
Utils as BlueprintUtils,
7+
} from "@blueprintjs/core";
38

49
import { CLASSPREFIX as eccgui } from "../../configuration/constants";
510

@@ -13,6 +18,11 @@ export interface ContextOverlayProps extends Omit<BlueprintPopoverProps, "positi
1318
* Use it when you need to display modal dialogs out of the context overlay.
1419
*/
1520
preventTopPosition?: boolean;
21+
/**
22+
* Use the overlay target as placeholder before the real `<ContextOverlay /` is rendered on first hover or focus event.
23+
* Currently experimental.
24+
*/
25+
usePlaceholder?: boolean;
1626
}
1727

1828
/**
@@ -24,18 +34,79 @@ export const ContextOverlay = ({
2434
portalClassName,
2535
preventTopPosition,
2636
className = "",
27-
...restProps
37+
usePlaceholder = false,
38+
...otherPopoverProps
2839
}: ContextOverlayProps) => {
40+
const placeholderRef = React.useRef(null);
41+
const eventmemory = React.useRef<undefined | "afterhover" | "afterfocus">(undefined);
42+
const [placeholder, setPlaceholder] = React.useState<boolean>(
43+
// use placeholder only for "simple" overlays without special states
44+
(!otherPopoverProps?.disabled ||
45+
!!otherPopoverProps?.defaultIsOpen ||
46+
!!otherPopoverProps?.isOpen ||
47+
!otherPopoverProps?.renderTarget) &&
48+
usePlaceholder
49+
);
50+
51+
const targetClassName = `${eccgui}-contextoverlay` + (className ? ` ${className}` : "");
52+
53+
React.useEffect(() => {
54+
if (placeholderRef.current) {
55+
const swap = (ev: MouseEvent | globalThis.FocusEvent) => {
56+
eventmemory.current = ev.type === "focusin" ? "afterfocus" : "afterhover";
57+
setPlaceholder(false);
58+
};
59+
(placeholderRef.current as HTMLElement).addEventListener("mouseenter", swap);
60+
(placeholderRef.current as HTMLElement).addEventListener("focusin", swap);
61+
}
62+
}, [!!placeholderRef.current]);
63+
64+
const refocus = React.useCallback((node) => {
65+
if (eventmemory.current === "afterfocus" && node) {
66+
const target = node.targetRef.current.children[0];
67+
if (target) {
68+
target.focus();
69+
}
70+
}
71+
}, []);
72+
73+
const displayPlaceholder = () => {
74+
const PlaceholderElement = otherPopoverProps?.targetTagName ?? (otherPopoverProps?.fill ? "div" : "span");
75+
const childTarget = BlueprintUtils.ensureElement(React.Children.toArray(children)[0]);
76+
if (!childTarget) {
77+
return null;
78+
}
79+
return React.createElement(
80+
PlaceholderElement,
81+
{
82+
...otherPopoverProps?.targetProps,
83+
className: `${BlueprintClasses.POPOVER_TARGET} ${targetClassName}`,
84+
ref: placeholderRef,
85+
},
86+
React.cloneElement(childTarget, {
87+
...childTarget.props,
88+
className:
89+
childTarget.props.className ?? "" + (otherPopoverProps.fill ? ` ${BlueprintClasses.FILL}` : ""),
90+
tabIndex:
91+
childTarget.props.tabIndex ??
92+
(!otherPopoverProps?.disabled && otherPopoverProps?.openOnTargetFocus ? 0 : undefined),
93+
})
94+
);
95+
};
96+
2997
const portalClassNameFinal =
3098
(preventTopPosition ? `${eccgui}-contextoverlay__portal--lowertop` : "") +
3199
(portalClassName ? ` ${portalClassName}` : "");
32100

33-
return (
101+
return placeholder ? (
102+
displayPlaceholder()
103+
) : (
34104
<BlueprintPopover
35105
placement="bottom"
36-
{...restProps}
37-
className={`${eccgui}-contextoverlay` + (className ? ` ${className}` : "")}
106+
{...otherPopoverProps}
107+
className={targetClassName}
38108
portalClassName={portalClassNameFinal.trim() ?? undefined}
109+
ref={refocus}
39110
>
40111
{children}
41112
</BlueprintPopover>

0 commit comments

Comments
 (0)