-
Notifications
You must be signed in to change notification settings - Fork 2.9k
Layer: Reintroduce Portals with Event Blocking #6211
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
Conversation
* Use Portals when available for Callout * Update shrinkwrap * Set virtual parent for initial render * Only wait for target when required * Update shrinkwrap * update * Prevent markdown-to-jsx from upgrading * Add patch file for example app base change * Update Layer.tsx
| * Traditionally Layer has not had this behavior. This prop preserves backwards compatibility by | ||
| * default while allowing users to opt in to the new event bubbling functionality. | ||
| */ | ||
| eventBubblingEnabled?: boolean; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will this limit us going forward? Should we have a new Layer that does this by default and that we recommend people to use?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a good question. In many use cases (Modals, Panels, Dialogs, etc.) I don't think you'll ever want event bubbling behavior. While portals more accurately portray virtual hierarchy, I don't think it's how people will ever want these common use cases to behave.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I agree here.
The spirit of a layer is that you are rendering on a higher elevation than the default body. It really is an attached layer, like photoshop layers, which simply overlay and do not interact.
There are cases where you need to know the originating dom heirarchy, and we have virtual parents for that. But I've never run into a case where events should bubble into or out of layers. That would kind of break the spirit.
Now I have seen web sites which use DOM eventing to raise all sorts of custom action events, so that they can be mediated through the root dom. Kind of like redux and central state, but with respect to DOM structure to provide a framework agnostic but tree sensitive data flow. In that case? Sure I could see a need to propagate events.
| eventBubblingEnabled ? ( | ||
| <Fabric className={classNames.content}>{this.props.children}</Fabric> | ||
| ) : ( | ||
| <Fabric |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't this be its own component? Something like EventBoundary?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't see the need since it's pretty simple in implementation. It's just one set of JSX args and one handler. If there's a case for reuse it could always be broken out later pretty easily.
packages/utilities/src/dom.ts
Outdated
| }; | ||
| } | ||
|
|
||
| export const dataPortalAttribute = 'data-portal-element'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
string constants like these probably should be CAPITALIZED_LIKE_THIS
packages/utilities/src/dom.ts
Outdated
| * If both parent and child are within the same portal this function will return false. | ||
| */ | ||
| export function portalContainsElement(target: HTMLElement, parent?: HTMLElement): boolean { | ||
| let elementMatch = findElementRecursive( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
const
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder why tslint didn't catch that?
packages/utilities/src/dom.test.ts
Outdated
| setPortalAttribute(portal); | ||
| }); | ||
|
|
||
| it('works correctly', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you describe what "works correctly" means in this so that readers can understand the "spec" of this?
|
|
||
| beforeAll(() => { | ||
| // Mock createPortal to capture its component hierarchy in snapshot output. | ||
| ReactDOM.createPortal = jest.fn(element => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pretty sure you're the only one testing with portals, so not an immediate issue here. But you should restore the implementation when you're all done inside an afterAll() block.
packages/office-ui-fabric-react/src/components/ComponentExamples.test.tsx
Show resolved
Hide resolved
| const doc = getDocument(rootElement); | ||
|
|
||
| if (!doc || !rootElement) { | ||
| const doc = getDocument(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe getDocument takes an element, so if host is available, maybe pass that in?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@cschleiden Do you remember the rationale here?
kenotron
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay my comments are resolved!
|
👏 |
|
🥇 |
Pull request checklist
$ npm run changeDescription of changes
Layercurrently uses React'sunstable_renderSubtreeIntoContainer, which will soon be removed. Additionally, it's incompatible with React 16 Context and has hampered our efforts to evolve theming with Layered components. As a result, we've had to prioritize reintroducing React portals use inLayer.React Portals more accurately respect virtual hierarchy in allowing events to bubble up and trickle down as well as in processing mouse behavior. I've reintroduced
Layer's use of React portals with event blocking added to more closely simulateLayer's existing behavior. Event bubbling is controlled via a newLayerpropeventBubblingEnabledand is disabled by default for backwards compatibility.However, portals and this PR do introduce a couple of known behavior changes regarding the events
onMouseEnterandonMouseLeave.These events cannot be blocked without breaking backwards compatibility and invasive and hacky changes to how React operates. With portals, when Layered components are moused over, every
onMouseEnterhandler along the subtree from the common ancestor of the component left down to the Layered component will be triggered. (This didn't happen with old Layer because these events just traversed directly to the Layer subtree attached to root.)For example, if a
Tooltipcontains aModaland theModalis open, theTooltipwill appear over the Modal when the mouse enters the browser window. This happens becauseTooltip'sonMouseEnterhandler is now triggered with portals where it wasn't before. For these types of cases, autilitieshelperportalContainsElementwas added to help work around this specific example:If both
TooltipHostandev.targetare contained within the same portal, theTooltipwill continue to appear as expected.Likewise,
onMouseLeaveused to be generated when Layered components rendered. Now that Layered components are more accurately viewed as part of the hierarchy,onMouseLeaveevents may not get generated when rendering Layered content. Additionally,onMouseLeaveevents will now get generated for the entire root->content->Layer->Layered component subtree when the mouse leaves the browser window or any point in that subtree.Focus areas to test
Unit tests added for event blocking and utility helper.
Microsoft Reviewers: Open in CodeFlow