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

Always render the fallback Popover anchor within the Popover's parent element #53982

Merged
merged 4 commits into from
Aug 28, 2023
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
15 changes: 9 additions & 6 deletions packages/components/src/popover/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -569,17 +569,20 @@ const UnforwardedPopover = (

if ( shouldRenderWithinSlot ) {
content = <Fill name={ slotName }>{ content }</Fill>;
} else if ( ! inline ) {
content = createPortal( content, getPopoverFallbackContainer() );
}

if ( ! hasAnchor ) {
content = <span ref={ anchorRefFallback }>{ content }</span>;
}

if ( shouldRenderWithinSlot || inline ) {
if ( hasAnchor ) {
return content;
}

return createPortal( content, getPopoverFallbackContainer() );
return (
<>
<span ref={ anchorRefFallback } />
{ content }
</>
);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style nit: most of the time the anchor is specified, so let's avoid rendering the redundant fragment by writing it as:

if ( hasAnchor ) {
  return content;
}
return <><span/>{ content }</>;

Copy link
Contributor

@ciampo ciampo Aug 30, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to understand as I catch up with the changes happened to Popover:

previously, { content } was rendered as a child of the anchor span, but it's now rendered as a sibling. Is that on purpose for this PR ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previously, the { content }, i.e., the popover element (used as floating by Floating UI), would be rendered as a child of the span, and it would be absolutely positioned, so it's not part of the layout flow. So, layout-wise, the <span> is empty, and Floating UI ensures that the popover is positioned correctly relative to the empty <span> anchor.

Now, the { content } is rendered in a portal element, at a completely different location. The <span> is empty not only layout-wise, but also DOM-markup-wise. Again, Floating UI is managing the position.

It's true that <span>{ content }</span> would also work, because content is a portal, it doesn't affect the DOM structure of the span at all. The only difference is whether the span React element receives the events dispatched withing the portal. But as the span has no listeners, it doesn't matter.

};

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ exports[`DotTip should render correctly 1`] = `
aria-label="Editor tips"
class="components-popover nux-dot-tip is-positioned"
role="dialog"
style="position: absolute; top: 0px; left: 0px; opacity: 0; transform: translateX(0px) translateY(0px) translateX(-2em) scale(0) translateZ(0); transform-origin: 0% 50% 0;"
style="position: absolute; top: 0px; left: 0px; opacity: 1; transform: none; transform-origin: 0% 50% 0;"
tabindex="-1"
>
<div
Expand Down
26 changes: 14 additions & 12 deletions packages/nux/src/components/dot-tip/test/index.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
/**
* External dependencies
*/
import { act, render, screen } from '@testing-library/react';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

/**
* Internal dependencies
*/
import { DotTip } from '..';

const noop = () => {};

describe( 'DotTip', () => {
beforeEach( () => {
jest.useFakeTimers();
Expand All @@ -20,26 +18,26 @@ describe( 'DotTip', () => {
jest.useRealTimers();
} );

it( 'should not render anything if invisible', async () => {
it( 'should not render anything if invisible', () => {
render(
<DotTip>
It looks like you’re writing a letter. Would you like help?
</DotTip>
);

await act( () => Promise.resolve() );

expect( screen.queryByRole( 'dialog' ) ).not.toBeInTheDocument();
} );

it( 'should render correctly', async () => {
render(
<DotTip isVisible setTimeout={ noop }>
<DotTip isVisible>
It looks like you’re writing a letter. Would you like help?
</DotTip>
);

await act( () => Promise.resolve() );
await waitFor( () =>
expect( screen.getByRole( 'dialog' ) ).toBePositionedPopover()
);

expect( screen.getByRole( 'dialog' ) ).toMatchSnapshot();
} );
Expand All @@ -51,12 +49,14 @@ describe( 'DotTip', () => {
const onDismiss = jest.fn();

render(
<DotTip isVisible onDismiss={ onDismiss } setTimeout={ noop }>
<DotTip isVisible onDismiss={ onDismiss }>
It looks like you’re writing a letter. Would you like help?
</DotTip>
);

await act( () => Promise.resolve() );
await waitFor( () =>
expect( screen.getByRole( 'dialog' ) ).toBePositionedPopover()
);

await user.click( screen.getByRole( 'button', { name: 'Got it' } ) );

Expand All @@ -70,12 +70,14 @@ describe( 'DotTip', () => {
const onDisable = jest.fn();

render(
<DotTip isVisible onDisable={ onDisable } setTimeout={ noop }>
<DotTip isVisible onDisable={ onDisable }>
It looks like you’re writing a letter. Would you like help?
</DotTip>
);

await act( () => Promise.resolve() );
await waitFor( () =>
expect( screen.getByRole( 'dialog' ) ).toBePositionedPopover()
);

await user.click(
screen.getByRole( 'button', { name: 'Disable tips' } )
Expand Down
Loading