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

use popover API in Tooltip if supported #2060

Merged
merged 14 commits into from
May 23, 2024
5 changes: 5 additions & 0 deletions .changeset/quick-ligers-serve.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@itwin/itwinui-css': minor
---

Added support for `popover` attribute in `iui-tooltip`.
5 changes: 5 additions & 0 deletions .changeset/tender-forks-visit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@itwin/itwinui-react': minor
---

`Tooltip` will now automatically use the [`popover` API](https://developer.mozilla.org/en-US/docs/Web/API/Popover_API) in supported browsers. This ensures that tooltips appear in the top layer, avoiding stacking context issues.
2 changes: 1 addition & 1 deletion apps/website/src/content/docs/tooltip.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ There are some advanced props available for more granular control over positioni

### Portals

It is important to know that before calculating the position, the tooltip gets [portaled](https://react.dev/reference/react-dom/createPortal) into the nearest [`ThemeProvider`](themeprovider) to avoid [stacking context](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_positioned_layout/Understanding_z-index/Stacking_context) issues. This behavior can be controlled using the Tooltip's `portal` prop or the ThemeProvider's [`portalContainer`](themeprovider#portals) prop.
It is important to know that before calculating the position, the tooltip gets [portaled](https://react.dev/reference/react-dom/createPortal) into the nearest [`ThemeProvider`](themeprovider). This is done to avoid [stacking context](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_positioned_layout/Understanding_z-index/Stacking_context) issues in browsers where the [`popover` API](https://developer.mozilla.org/en-US/docs/Web/API/Popover_API) is not supported. This portaling behavior can be controlled using the Tooltip's `portal` prop or the ThemeProvider's [`portalContainer`](themeprovider#portals) prop.
r100-stack marked this conversation as resolved.
Show resolved Hide resolved

## Accessibility

Expand Down
2 changes: 1 addition & 1 deletion packages/itwinui-css/src/tooltip/tooltip.scss
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@

@include mixins.iui-blur($hsl: 0 0% 0%, $opacity: 3);

&[hidden] {
&:where([hidden], [popover]:not(:popover-open)) {
r100-stack marked this conversation as resolved.
Show resolved Hide resolved
display: none !important;
}
}
2 changes: 2 additions & 0 deletions packages/itwinui-react/src/core/Tooltip/Tooltip.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ it('should toggle the visibility of tooltip on hover', () => {

const tooltip = getByText('some text');
expect(tooltip).not.toBeVisible();
expect(tooltip).toHaveAttribute('popover', 'manual');

fireEvent.mouseEnter(trigger);
expect(tooltip).toBeVisible();
Expand All @@ -45,6 +46,7 @@ it('should toggle the visibility of tooltip on focus', async () => {

const tooltip = getByText('some text');
expect(tooltip).not.toBeVisible();
expect(tooltip).toHaveAttribute('popover', 'manual');

fireEvent.focus(trigger);
act(() => void vi.advanceTimersByTime(50));
Expand Down
39 changes: 36 additions & 3 deletions packages/itwinui-react/src/core/Tooltip/Tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ import type {
PortalProps,
} from '../../utils/index.js';

// ----------------------------------------------------------------------------

type TooltipOptions = {
/**
* Placement of the Tooltip
Expand Down Expand Up @@ -117,6 +119,14 @@ type TooltipOwnProps = {
children?: React.ReactNode;
} & PortalProps;

// TODO: Remove this when types are available
type HTMLElementWithPopover = HTMLElement & {
/** @see https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/togglePopover */
togglePopover?: (force?: boolean) => void;
};

// ----------------------------------------------------------------------------

const useTooltip = (options: TooltipOptions = {}) => {
const uniqueId = useId();
const {
Expand All @@ -137,6 +147,20 @@ const useTooltip = (options: TooltipOptions = {}) => {
onVisibleChange,
);

const syncWithControlledState = React.useCallback(
(element: HTMLElementWithPopover | null) => {
// Using a microtask ensures that the popover is mounted before calling togglePopover
queueMicrotask(() => {
try {
element?.togglePopover?.(open);
} catch {
// Fail silently, to avoid crashing the page
}
});
},
[open],
);

const floating = useFloating({
placement,
open,
Expand Down Expand Up @@ -242,13 +266,15 @@ const useTooltip = (options: TooltipOptions = {}) => {
);

const floatingProps = React.useMemo(
() =>
interactions.getFloatingProps({
() => ({
...interactions.getFloatingProps({
hidden: !open,
'aria-hidden': 'true',
...props,
id,
}),
popover: 'manual',
r100-stack marked this conversation as resolved.
Show resolved Hide resolved
}),
[interactions, props, id, open],
);

Expand All @@ -257,10 +283,17 @@ const useTooltip = (options: TooltipOptions = {}) => {
getReferenceProps,
floatingProps,
...floating,
refs: {
...floating.refs,
setFloating: (element: HTMLElement | null) => {
floating.refs.setFloating(element);
syncWithControlledState(element);
},
},
// styles are not relevant when tooltip is not open
floatingStyles: floating.context.open ? floating.floatingStyles : {},
}),
[getReferenceProps, floatingProps, floating],
[getReferenceProps, floatingProps, floating, syncWithControlledState],
);
};

Expand Down
Loading