Toggletip: Ensure consistent positioning in all scenarios (#114085)
* portal toggletip content * add test to check focus trapping * kick CI
This commit is contained in:
@@ -140,7 +140,7 @@ describe('Toggletip', () => {
|
||||
expect(onOpen).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should be able to focus toggletip content next in DOM order - forwards and backwards', async () => {
|
||||
it('should trap content within the overlay', async () => {
|
||||
const onClose = jest.fn();
|
||||
const afterInDom = 'Outside of toggletip';
|
||||
|
||||
@@ -164,17 +164,39 @@ describe('Toggletip', () => {
|
||||
const closeButton = screen.getByTestId('toggletip-header-close');
|
||||
expect(closeButton).toHaveFocus();
|
||||
|
||||
// focus after
|
||||
await userEvent.tab();
|
||||
expect(afterButton).toHaveFocus();
|
||||
// tab forwards
|
||||
await userEvent.keyboard('{tab}');
|
||||
// need to waitFor here to wait for the floating-ui focus manager to take effect
|
||||
await waitFor(() => {
|
||||
expect(closeButton).toHaveFocus();
|
||||
});
|
||||
expect(afterButton).not.toHaveFocus();
|
||||
|
||||
// focus backwards
|
||||
await userEvent.tab({ shift: true });
|
||||
expect(closeButton).toHaveFocus();
|
||||
// tab forwards again
|
||||
await userEvent.keyboard('{tab}');
|
||||
// need to waitFor here to wait for the floating-ui focus manager to take effect
|
||||
await waitFor(() => {
|
||||
expect(closeButton).toHaveFocus();
|
||||
});
|
||||
expect(afterButton).not.toHaveFocus();
|
||||
|
||||
// focus back to togglebutton
|
||||
await userEvent.tab({ shift: true });
|
||||
// tab backwards
|
||||
await userEvent.keyboard('{shift}{tab}');
|
||||
// need to waitFor here to wait for the floating-ui focus manager to take effect
|
||||
await waitFor(() => {
|
||||
expect(closeButton).toHaveFocus();
|
||||
});
|
||||
expect(afterButton).not.toHaveFocus();
|
||||
|
||||
// close overlay, focus back to togglebutton
|
||||
await userEvent.keyboard('{escape}');
|
||||
expect(button).toHaveFocus();
|
||||
expect(afterButton).not.toHaveFocus();
|
||||
|
||||
// tab forwards with overlay closed
|
||||
await userEvent.tab();
|
||||
expect(closeButton).not.toHaveFocus();
|
||||
expect(afterButton).toHaveFocus();
|
||||
});
|
||||
|
||||
describe('Focus state', () => {
|
||||
|
||||
@@ -20,6 +20,7 @@ import { useStyles2, useTheme2 } from '../../themes/ThemeContext';
|
||||
import { getPositioningMiddleware } from '../../utils/floating';
|
||||
import { buildTooltipTheme, getPlacement } from '../../utils/tooltipUtils';
|
||||
import { IconButton } from '../IconButton/IconButton';
|
||||
import { Portal } from '../Portal/Portal';
|
||||
|
||||
import { ToggletipContent } from './types';
|
||||
|
||||
@@ -118,44 +119,46 @@ export const Toggletip = memo(
|
||||
...getReferenceProps(),
|
||||
})}
|
||||
{isOpen && (
|
||||
<FloatingFocusManager context={context} modal={false} closeOnFocusOut={false}>
|
||||
<div
|
||||
data-testid="toggletip-content"
|
||||
className={cx(style.container, {
|
||||
[styles.fitContent]: fitContent,
|
||||
})}
|
||||
ref={refs.setFloating}
|
||||
style={floatingStyles}
|
||||
{...getFloatingProps()}
|
||||
>
|
||||
<FloatingArrow
|
||||
strokeWidth={0.3}
|
||||
stroke={grafanaTheme.colors.border.weak}
|
||||
className={style.arrow}
|
||||
ref={arrowRef}
|
||||
context={context}
|
||||
/>
|
||||
{Boolean(title) && <div className={style.header}>{title}</div>}
|
||||
{closeButton && (
|
||||
<div className={style.headerClose}>
|
||||
<IconButton
|
||||
aria-label={t('grafana-ui.toggletip.close', 'Close')}
|
||||
name="times"
|
||||
data-testid="toggletip-header-close"
|
||||
onClick={() => {
|
||||
setControlledVisible(false);
|
||||
onClose?.();
|
||||
}}
|
||||
/>
|
||||
<Portal>
|
||||
<FloatingFocusManager context={context} modal={true}>
|
||||
<div
|
||||
data-testid="toggletip-content"
|
||||
className={cx(style.container, {
|
||||
[styles.fitContent]: fitContent,
|
||||
})}
|
||||
ref={refs.setFloating}
|
||||
style={floatingStyles}
|
||||
{...getFloatingProps()}
|
||||
>
|
||||
<FloatingArrow
|
||||
strokeWidth={0.3}
|
||||
stroke={grafanaTheme.colors.border.weak}
|
||||
className={style.arrow}
|
||||
ref={arrowRef}
|
||||
context={context}
|
||||
/>
|
||||
{Boolean(title) && <div className={style.header}>{title}</div>}
|
||||
{closeButton && (
|
||||
<div className={style.headerClose}>
|
||||
<IconButton
|
||||
aria-label={t('grafana-ui.toggletip.close', 'Close')}
|
||||
name="times"
|
||||
data-testid="toggletip-header-close"
|
||||
onClick={() => {
|
||||
setControlledVisible(false);
|
||||
onClose?.();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className={style.body}>
|
||||
{(typeof content === 'string' || isValidElement(content)) && content}
|
||||
{typeof content === 'function' && content({})}
|
||||
</div>
|
||||
)}
|
||||
<div className={style.body}>
|
||||
{(typeof content === 'string' || isValidElement(content)) && content}
|
||||
{typeof content === 'function' && content({})}
|
||||
{Boolean(footer) && <div className={style.footer}>{footer}</div>}
|
||||
</div>
|
||||
{Boolean(footer) && <div className={style.footer}>{footer}</div>}
|
||||
</div>
|
||||
</FloatingFocusManager>
|
||||
</FloatingFocusManager>
|
||||
</Portal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -51,8 +51,8 @@ export function buildTooltipTheme(
|
||||
headerClose: css({
|
||||
color: theme.colors.text.secondary,
|
||||
position: 'absolute',
|
||||
right: theme.spacing(1),
|
||||
top: theme.spacing(1.5),
|
||||
right: theme.spacing(0.5),
|
||||
top: theme.spacing(1),
|
||||
backgroundColor: 'transparent',
|
||||
border: 0,
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user