Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Render tooltip when trigger is disabled #637

Merged
merged 3 commits into from
May 27, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 26 additions & 4 deletions packages/react-components/src/overlay/src/useOverlayTrigger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,38 @@ export function useOverlayTrigger(isOpen: boolean, {
}
});

// Hotfix for https://bugzilla.mozilla.org/show_bug.cgi?id=1487102
// Hotfix: https://bugzilla.mozilla.org/show_bug.cgi?id=1487102
const handleKeyUp = useEventCallback((event: KeyboardEvent) => {
if (event.key === Keys.space) {
event.preventDefault();
}
});

const handleMouseEnter = useEventCallback((event: MouseEvent) => { show(event); });
const handleMouseLeave = useEventCallback((event: MouseEvent) => { hide(event); });
const handleMouseEnter = useEventCallback((event: MouseEvent) => {
show(event);

if (hideOnLeave) {
let target = event.target as HTMLElement;

// HACK: The current strategy to show an overlay for a disabled trigger is to wrap the trigger in a div.
// Strangely, when doing so, event.target is the disable trigger instead of the wrapper. This code ensure we resolve
// the target to the wrapper instead of the original disabled trigger.
if (target.hasAttribute("disabled")) {
target = target.parentElement;
}

// HACK: A mouseleave event is not fired when the element have a disabled child. For more info view: https://github.com/facebook/react/issues/10396.
// This is part of a work around to support a tooltip for a disabled button.
target.addEventListener("mouseleave", handleMouseLeave);
}
});

const handleMouseLeave = useEventCallback((event: any) => {
hide(event);

event.target.removeEventListener("mouseleave", handleMouseLeave);
});

const handleFocus = useEventCallback((event: FocusEvent) => { show(event); });
const handleBlur = useEventCallback((event: FocusEvent) => { hide(event); });

Expand All @@ -81,7 +104,6 @@ export function useOverlayTrigger(isOpen: boolean, {
// The overlay will show when the trigger is hovered with mouse or focus with keyboard.
return {
onMouseEnter: handleMouseEnter,
onMouseLeave: hideOnLeave ? handleMouseLeave : undefined,
onFocus: handleFocus,
onBlur: hideOnLeave ? handleBlur : undefined
};
Expand Down
8 changes: 4 additions & 4 deletions packages/react-components/src/overlay/src/usePopup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export function usePopup(type: "menu" | "listbox" | "dialog", {
updateIsOpen(event, false);
}
}),
hideOnLeave: isOpen && hideOnLeave,
hideOnLeave,
isDisabled: disabled
});

Expand All @@ -106,9 +106,9 @@ export function usePopup(type: "menu" | "listbox" | "dialog", {
onHide: useEventCallback((event: SyntheticEvent) => {
updateIsOpen(event, false);
}),
hideOnEscape: isOpen && hideOnEscape,
hideOnLeave: isOpen && hideOnLeave,
hideOnOutsideClick: isOpen && hideOnOutsideClick
hideOnEscape,
hideOnLeave,
hideOnOutsideClick
});

const restoreFocusProps = useRestoreFocus(focusScope, { isDisabled: !restoreFocus || !isOpen });
Expand Down
75 changes: 62 additions & 13 deletions packages/react-components/src/overlay/tests/jest/usePopup.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -157,13 +157,16 @@ describe("\"click\" trigger", () => {
test("when opened, close on trigger click", async () => {
const { getByTestId, queryByTestId } = render(
<Popup
defaultOpen
trigger="click"
data-triggertestid="trigger"
data-overlaytestid="overlay"
/>
);

act(() => {
userEvent.click(getByTestId("trigger"));
});

await waitFor(() => expect(getByTestId("overlay")).toBeInTheDocument());

act(() => {
Expand All @@ -176,12 +179,16 @@ describe("\"click\" trigger", () => {
test("when opened, close on esc keypress", async () => {
const { getByTestId, queryByTestId } = render(
<Popup
defaultOpen
trigger="click"
data-triggertestid="trigger"
data-overlaytestid="overlay"
/>
);

act(() => {
userEvent.click(getByTestId("trigger"));
});

await waitFor(() => expect(getByTestId("overlay")).toBeInTheDocument());

act(() => {
Expand All @@ -199,12 +206,16 @@ describe("\"click\" trigger", () => {
const { getByTestId } = render(
<Popup
hideOnEscape={false}
defaultOpen
trigger="click"
data-triggertestid="trigger"
data-overlaytestid="overlay"
/>
);

act(() => {
userEvent.click(getByTestId("trigger"));
});

await waitFor(() => expect(getByTestId("overlay")).toBeInTheDocument());

act(() => {
Expand All @@ -223,13 +234,17 @@ describe("\"click\" trigger", () => {
<>
<button type="button" data-testid="focusable-element">Focusable element</button>
<Popup
defaultOpen
trigger="click"
data-triggertestid="trigger"
data-overlaytestid="overlay"
/>
</>
);

act(() => {
userEvent.click(getByTestId("trigger"));
});

await waitFor(() => expect(getByTestId("overlay")).toBeInTheDocument());

act(() => {
Expand All @@ -249,13 +264,17 @@ describe("\"click\" trigger", () => {
<button type="button" data-testid="focusable-element">Focusable element</button>
<Popup
hideOnLeave={false}
defaultOpen
trigger="click"
data-triggertestid="trigger"
data-overlaytestid="overlay"
/>
</>
);

act(() => {
userEvent.click(getByTestId("trigger"));
});

await waitFor(() => expect(getByTestId("overlay")).toBeInTheDocument());

act(() => {
Expand All @@ -272,12 +291,16 @@ describe("\"click\" trigger", () => {
test("when opened, close on outside click", async () => {
const { getByTestId, queryByTestId } = render(
<Popup
defaultOpen
trigger="click"
data-triggertestid="trigger"
data-overlaytestid="overlay"
/>
);

act(() => {
userEvent.click(getByTestId("trigger"));
});

await waitFor(() => expect(getByTestId("overlay")).toBeInTheDocument());

act(() => {
Expand All @@ -295,12 +318,16 @@ describe("\"click\" trigger", () => {
const { getByTestId } = render(
<Popup
hideOnOutsideClick={false}
defaultOpen
trigger="click"
data-triggertestid="trigger"
data-overlaytestid="overlay"
/>
);

act(() => {
userEvent.click(getByTestId("trigger"));
});

await waitFor(() => expect(getByTestId("overlay")).toBeInTheDocument());

act(() => {
Expand Down Expand Up @@ -356,11 +383,15 @@ describe("\"hover\" trigger", () => {
const { getByTestId, queryByTestId } = render(
<Popup
trigger="hover"
defaultOpen
data-triggertestid="trigger"
data-overlaytestid="overlay"
/>
);

act(() => {
fireEvent.mouseEnter(getByTestId("trigger"));
});

await waitFor(() => expect(getByTestId("overlay")).toBeInTheDocument());

act(() => {
Expand Down Expand Up @@ -440,12 +471,15 @@ describe("\"hover\" trigger", () => {
const { getByTestId, queryByTestId } = render(
<Popup
trigger="hover"
defaultOpen
data-triggertestid="trigger"
data-overlaytestid="overlay"
/>
);

act(() => {
fireEvent.mouseEnter(getByTestId("trigger"));
});

await waitFor(() => expect(getByTestId("overlay")).toBeInTheDocument());

act(() => {
Expand All @@ -460,12 +494,15 @@ describe("\"hover\" trigger", () => {
<Popup
hideOnLeave={false}
trigger="hover"
defaultOpen
data-triggertestid="trigger"
data-overlaytestid="overlay"
/>
);

act(() => {
fireEvent.mouseEnter(getByTestId("trigger"));
});

await waitFor(() => expect(getByTestId("overlay")).toBeInTheDocument());

act(() => {
Expand All @@ -479,11 +516,15 @@ describe("\"hover\" trigger", () => {
const { getByTestId, queryByTestId } = render(
<Popup
trigger="hover"
defaultOpen
data-triggertestid="trigger"
data-overlaytestid="overlay"
/>
);

act(() => {
fireEvent.mouseEnter(getByTestId("trigger"));
});

await waitFor(() => expect(getByTestId("overlay")).toBeInTheDocument());

act(() => {
Expand All @@ -502,11 +543,15 @@ describe("\"hover\" trigger", () => {
<Popup
hideOnOutsideClick={false}
trigger="hover"
defaultOpen
data-triggertestid="trigger"
data-overlaytestid="overlay"
/>
);

act(() => {
fireEvent.mouseEnter(getByTestId("trigger"));
});

await waitFor(() => expect(getByTestId("overlay")).toBeInTheDocument());

act(() => {
Expand Down Expand Up @@ -617,11 +662,15 @@ test("when autoFocus is true, focus the popup element on open", async () => {
const { getByTestId } = render(
<Popup
autoFocus
defaultOpen
data-triggertestid="trigger"
data-overlaytestid="overlay"
/>
);

act(() => {
userEvent.click(getByTestId("trigger"));
});

await waitFor(() => expect(getByTestId("overlay")).toHaveFocus());
});

Expand Down
5 changes: 5 additions & 0 deletions packages/react-components/src/tooltip/src/Tooltip.css
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,8 @@
.o-ui-tooltip .o-ui-text + .o-ui-icon {
margin-left: var(--o-ui-global-scale-alpha);
}

/* DISABLED WRAPPER */
.o-ui-tooltip-disabled-wrapper {
display: inline-block;
}
16 changes: 12 additions & 4 deletions packages/react-components/src/tooltip/src/TooltipTrigger.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ interface InnerTooltipTriggerProps {
| "left"
| "left-start"
| "left-end";

/**
* Called when the open state change.
* @param {SyntheticEvent} event - React's original SyntheticEvent.
Expand Down Expand Up @@ -75,7 +74,6 @@ interface InnerTooltipTriggerProps {
forwardedRef: ForwardedRef<any>;
}


export function parseTooltipTrigger(children: ReactNode) {
const array = Children.toArray(resolveChildren(children));

Expand Down Expand Up @@ -155,10 +153,20 @@ export function InnerTooltipTrigger({

const tooltipId = useId(tooltip.props.id, "o-ui-tooltip");

const triggerMarkup = augmentElement(trigger, mergeProps(
const triggerWithDescribedBy = augmentElement(trigger, {
"aria-describedby": isOpen ? tooltipId : undefined
});

// HACK: a disabled element doesn't fire event, therefore the element is wrapped in a div.
const triggerElement = !triggerWithDescribedBy.props.disabled ? triggerWithDescribedBy : (
<div className="o-ui-tooltip-disabled-wrapper">
{triggerWithDescribedBy}
</div>
);

const triggerMarkup = augmentElement(triggerElement, mergeProps(
triggerProps,
{
"aria-describedby": isOpen ? tooltipId : undefined,
ref: triggerRef
}
));
Expand Down
Loading