-
Notifications
You must be signed in to change notification settings - Fork 2.8k
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
Layer: Reintroduce Portals with Event Blocking #6211
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
@@ -11,6 +11,8 @@ interface IVirtualElement extends HTMLElement { | |||
}; | |||
} | |||
|
|||
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?
const files: string[] = glob.sync(path.resolve(process.cwd(), 'src/components/**/examples/*Example*.tsx')); | ||
|
||
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?
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 change
Description of changes
Layer
currently 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 newLayer
propeventBubblingEnabled
and is disabled by default for backwards compatibility.However, portals and this PR do introduce a couple of known behavior changes regarding the events
onMouseEnter
andonMouseLeave
.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
onMouseEnter
handler 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
Tooltip
contains aModal
and theModal
is open, theTooltip
will appear over the Modal when the mouse enters the browser window. This happens becauseTooltip
'sonMouseEnter
handler is now triggered with portals where it wasn't before. For these types of cases, autilities
helperportalContainsElement
was added to help work around this specific example:If both
TooltipHost
andev.target
are contained within the same portal, theTooltip
will continue to appear as expected.Likewise,
onMouseLeave
used to be generated when Layered components rendered. Now that Layered components are more accurately viewed as part of the hierarchy,onMouseLeave
events may not get generated when rendering Layered content. Additionally,onMouseLeave
events 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