Update trigger utilities to handle nested MenuTrigger/Tooltip#21495
Conversation
|
This pull request is automatically built and testable in CodeSandbox. To see build info of the built libraries, click here or the icon next to each commit SHA. Latest deployment of this branch, based on commit 49289be:
|
📊 Bundle size reportUnchanged fixtures
|
Asset size changesSize Auditor did not detect a change in bundle size for any component! Baseline commit: 332d8f47ddea3daa577c95220a11f8bf5a7ac365 (build) |
Perf Analysis (
|
| Scenario | Render type | Master Ticks | PR Ticks | Iterations | Status |
|---|---|---|---|---|---|
| Avatar | mount | 690 | 782 | 5000 | |
| BaseButton | mount | 715 | 751 | 5000 | |
| Breadcrumb | mount | 2040 | 2123 | 1000 | |
| ButtonNext | mount | 452 | 451 | 5000 | |
| Checkbox | mount | 1324 | 1269 | 5000 | |
| CheckboxBase | mount | 1074 | 1131 | 5000 | |
| ChoiceGroup | mount | 3861 | 3935 | 5000 | |
| ComboBox | mount | 839 | 843 | 1000 | |
| CommandBar | mount | 8367 | 8837 | 1000 | |
| ContextualMenu | mount | 7220 | 6877 | 1000 | |
| DefaultButton | mount | 1005 | 949 | 5000 | |
| DetailsRow | mount | 3140 | 3200 | 5000 | |
| DetailsRowFast | mount | 3323 | 2913 | 5000 | |
| DetailsRowNoStyles | mount | 2806 | 3024 | 5000 | |
| Dialog | mount | 2150 | 2034 | 1000 | |
| DocumentCardTitle | mount | 131 | 141 | 1000 | |
| Dropdown | mount | 2675 | 2839 | 5000 | |
| FluentProviderNext | mount | 1586 | 1609 | 5000 | |
| FluentProviderWithTheme | mount | 132 | 144 | 10 | |
| FluentProviderWithTheme | virtual-rerender | 87 | 103 | 10 | |
| FluentProviderWithTheme | virtual-rerender-with-unmount | 168 | 190 | 10 | |
| FocusTrapZone | mount | 1511 | 1374 | 5000 | |
| FocusZone | mount | 1543 | 1493 | 5000 | |
| IconButton | mount | 1500 | 1496 | 5000 | |
| Label | mount | 320 | 287 | 5000 | |
| Layer | mount | 2466 | 2474 | 5000 | |
| Link | mount | 386 | 439 | 5000 | |
| MakeStyles | mount | 1351 | 1336 | 50000 | |
| MenuButton | mount | 1317 | 1180 | 5000 | |
| MessageBar | mount | 1775 | 1762 | 5000 | |
| Nav | mount | 2731 | 2778 | 1000 | |
| OverflowSet | mount | 977 | 969 | 5000 | |
| Panel | mount | 2008 | 2065 | 1000 | |
| Persona | mount | 731 | 704 | 1000 | |
| Pivot | mount | 1127 | 1233 | 1000 | |
| PrimaryButton | mount | 1070 | 1022 | 5000 | |
| Rating | mount | 6345 | 6582 | 5000 | |
| SearchBox | mount | 1103 | 1068 | 5000 | |
| Shimmer | mount | 1993 | 1991 | 5000 | |
| Slider | mount | 1586 | 1566 | 5000 | |
| SpinButton | mount | 4059 | 3959 | 5000 | |
| Spinner | mount | 399 | 412 | 5000 | |
| SplitButton | mount | 2692 | 2718 | 5000 | |
| Stack | mount | 427 | 468 | 5000 | |
| StackWithIntrinsicChildren | mount | 1891 | 1843 | 5000 | |
| StackWithTextChildren | mount | 4286 | 4153 | 5000 | |
| SwatchColorPicker | mount | 9633 | 9418 | 5000 | |
| TagPicker | mount | 2240 | 2193 | 5000 | |
| TeachingBubble | mount | 11340 | 11164 | 5000 | |
| Text | mount | 411 | 355 | 5000 | |
| TextField | mount | 1238 | 1170 | 5000 | |
| ThemeProvider | mount | 1049 | 1026 | 5000 | |
| ThemeProvider | virtual-rerender | 558 | 480 | 5000 | |
| ThemeProvider | virtual-rerender-with-unmount | 1623 | 1586 | 5000 | |
| Toggle | mount | 713 | 723 | 5000 | |
| buttonNative | mount | 118 | 133 | 5000 |
Perf Analysis (@fluentui/react-northstar)
… a helper function there
There was a problem hiding this comment.
I requested changes because:
- we should fix props priority (can be done in a separate PR)
- there is no sense to support
React.Fragmentin traversal
In general it's not good idea to traverse React's tree, but as we don't rely on reading props - it's enough good for this scenario.
This approach is also better than existing as it removes event dispatching logic 🤗
| outerProps: rest, | ||
| overrideProps, | ||
| }), | ||
| children: applyTriggerPropsToChildren(children, overrideProps), |
There was a problem hiding this comment.
As we discussed in offline the behavior of applyTriggerPropsToChildren() should be fixed to respect user's input:
<Tooltip content="bar" relationship="label">
<Button aria-label="foo" />
</Tooltip>In this case button should have foo as aria-label, but it has bar:
This change could be done either in this PR or in a follow up.
There was a problem hiding this comment.
This needs to be handled by the individual trigger components to decide which props to override and which not to. applyTriggerPropsToChildren can't do it because for example, we want the merged event handlers on triggerProps to override the child's; but we don't want the aria props on triggerProps to override the child's.
I'll handle aria-label for tooltip in a separate PR.
There was a problem hiding this comment.
We can do something like that:
applyTriggerPropsToChildren(children, {
defaultProps: { /* aria props there */},
overrideProps: {/* handlers there */}
})In this case, we could handle it in a single place. WDYT?
There was a problem hiding this comment.
It seems a little overkill IMO, and it's something that the caller could easily do if they wanted:
applyTriggerPropsToChildren(children, {
...defaultProps,
...child?.props,
...overrideProps,
});Also, it still doesn't help with the case where we don't want to set aria-label on the child if it has aria-labelledby.
There was a problem hiding this comment.
applyTriggerPropsToChildren(children, {
...defaultProps, // aria props
...child?.props,
...overrideProps, // merged event handlers
});Also, it still doesn't help with the case where we don't want to set aria-label on the child if it has aria-labelledby.
Well, if overrideProps will contain only merged event handlers that it will work as expected (user's input wins).
Add an example of compositionIMO we should mention this behavior in docs somehow, via an example or something else. const CallButton = React.forwardRef((props, ref) => {
return (
<Tooltip content="bar" relationship="label">
<Button
{...props}
ref={ref}
onClick={(...args) => {
props.onClick(...args);
// custom logic
}}
>
Toggle menu
</Button>
</Tooltip>
);
});
export const MenuTriggerWithTooltip = () => (
<Menu>
<MenuTrigger>
<CallButton />
</MenuTrigger>
<MenuPopover>
<MenuList>
<MenuItem>New </MenuItem>
<MenuItem>New Window</MenuItem>
<MenuItem disabled>Open File</MenuItem>
<MenuItem>Open Folder</MenuItem>
</MenuList>
</MenuPopover>
</Menu>
);This example works fully with current approach, but there are few things that worth mentioning: |
We might want to document But I don't see anything in your example that needs explicit documentation IMO:
I suppose if anything, the potential confusing part is that MenuTrigger and Tooltip "magically" set some HTMLElement props on their child, and those props need to work as expected. We could theoretically add something that mentions that to the documentation for those components. |
layershifter
left a comment
There was a problem hiding this comment.
Approved, thanks for addressing comments 👍
But we should solve #21495 (comment) in follow up PR for both Menu & Tooltip.
|
@layershifter I created PR #21532 to fix the aria-label overriding issue |
I am not sure that it is applicable to the example that I posted as It will only work for scenarios when someone composes const LabelTooltip = (props) => {
return (
<Tooltip {...props} relationship="label" />
);
}
function App() {
return (
<LabelTooltip>
<button />
</LabelTooltip>
)
}I think that |
That's right: I was mentioning documenting In the |


Current Behavior
Tooltip and MenuTrigger don't play nicely together. They both need to add props to their child element, but that does not currently work when they're nested like this:
New Behavior
This is an alternative approach to the
useTriggerElementhook that was added here:The goal of this approach is to have any trigger be able to "see through" other triggers and apply its props to the actual control that will do the triggering.
This introduces a few new types and helper functions:
FluentTriggerComponent- Allows components to tag themselves as trigger components, so other trigger components know to ignore them when looking for their childgetTriggerChild- Replaces the formeronlyChild: looks for the first non-trigger descendant of the given element.cloneTriggerTree- Applies new props by cloning the element that's found bygetTriggerChild. Handles cloning any intermediate triggers as well, so they have the cloned element as their child.useMergedEventCallbacks- Merges multiple callbacks into a single function that calls both.This also removes the
useTriggerElementfunction, since this replaces it.Related Issues