fix(Menu): handle triggers composition#21225
Conversation
This PR tries to show a potential solution to nested trigger composition through prop and ref forwarding.
|
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 e95bf61:
|
📊 Bundle size reportUnchanged fixtures
|
Asset size changesSize Auditor did not detect a change in bundle size for any component! Baseline commit: dd3b0a121181411de974c62a2797b9291cfbc134 (build) |
Perf Analysis (
|
| Scenario | Render type | Master Ticks | PR Ticks | Iterations | Status |
|---|---|---|---|---|---|
| ContextualMenu | mount | 7268 | 14728 | 1000 | Possible regression |
All results
| Scenario | Render type | Master Ticks | PR Ticks | Iterations | Status |
|---|---|---|---|---|---|
| Avatar | mount | 817 | 781 | 5000 | |
| BaseButton | mount | 789 | 815 | 5000 | |
| Breadcrumb | mount | 2343 | 2355 | 1000 | |
| ButtonNext | mount | 453 | 461 | 5000 | |
| Checkbox | mount | 1321 | 1342 | 5000 | |
| CheckboxBase | mount | 1139 | 1133 | 5000 | |
| ChoiceGroup | mount | 4126 | 4164 | 5000 | |
| ComboBox | mount | 844 | 870 | 1000 | |
| CommandBar | mount | 8811 | 8801 | 1000 | |
| ContextualMenu | mount | 7268 | 14728 | 1000 | Possible regression |
| DefaultButton | mount | 1005 | 974 | 5000 | |
| DetailsRow | mount | 3268 | 3301 | 5000 | |
| DetailsRowFast | mount | 3275 | 3275 | 5000 | |
| DetailsRowNoStyles | mount | 3103 | 3085 | 5000 | |
| Dialog | mount | 2250 | 2179 | 1000 | |
| DocumentCardTitle | mount | 174 | 170 | 1000 | |
| Dropdown | mount | 2764 | 2756 | 5000 | |
| FluentProviderNext | mount | 1689 | 1621 | 5000 | |
| FluentProviderWithTheme | mount | 167 | 126 | 10 | |
| FluentProviderWithTheme | virtual-rerender | 91 | 102 | 10 | |
| FluentProviderWithTheme | virtual-rerender-with-unmount | 167 | 165 | 10 | |
| FocusTrapZone | mount | 1641 | 1579 | 5000 | |
| FocusZone | mount | 1575 | 1565 | 5000 | |
| IconButton | mount | 1516 | 1523 | 5000 | |
| Label | mount | 319 | 328 | 5000 | |
| Layer | mount | 2484 | 2527 | 5000 | |
| Link | mount | 433 | 427 | 5000 | |
| MakeStyles | mount | 1582 | 1603 | 50000 | |
| MenuButton | mount | 1288 | 1292 | 5000 | |
| MessageBar | mount | 1753 | 1785 | 5000 | |
| Nav | mount | 2872 | 2855 | 1000 | |
| OverflowSet | mount | 989 | 979 | 5000 | |
| Panel | mount | 2114 | 2154 | 1000 | |
| Persona | mount | 736 | 730 | 1000 | |
| Pivot | mount | 1242 | 1265 | 1000 | |
| PrimaryButton | mount | 1133 | 1140 | 5000 | |
| Rating | mount | 6695 | 6599 | 5000 | |
| SearchBox | mount | 1180 | 1181 | 5000 | |
| Shimmer | mount | 2207 | 2231 | 5000 | |
| Slider | mount | 1725 | 1733 | 5000 | |
| SpinButton | mount | 4288 | 4578 | 5000 | |
| Spinner | mount | 410 | 400 | 5000 | |
| SplitButton | mount | 2738 | 2715 | 5000 | |
| Stack | mount | 465 | 477 | 5000 | |
| StackWithIntrinsicChildren | mount | 1981 | 2015 | 5000 | |
| StackWithTextChildren | mount | 4434 | 4448 | 5000 | |
| SwatchColorPicker | mount | 9644 | 9897 | 5000 | |
| TagPicker | mount | 2298 | 2291 | 5000 | |
| TeachingBubble | mount | 11187 | 11106 | 5000 | |
| Text | mount | 386 | 377 | 5000 | |
| TextField | mount | 1202 | 1216 | 5000 | |
| ThemeProvider | mount | 1031 | 1050 | 5000 | |
| ThemeProvider | virtual-rerender | 548 | 538 | 5000 | |
| ThemeProvider | virtual-rerender-with-unmount | 1608 | 1606 | 5000 | |
| Toggle | mount | 706 | 693 | 5000 | |
| buttonNative | mount | 127 | 138 | 5000 |
Perf Analysis (@fluentui/react-northstar)
Perf tests with no regressions
| Scenario | Current PR Ticks | Baseline Ticks | Ratio |
|---|---|---|---|
| TreeWith60ListItems.default | 170 | 153 | 1.11:1 |
| AttachmentMinimalPerf.default | 139 | 127 | 1.09:1 |
| RefMinimalPerf.default | 218 | 203 | 1.07:1 |
| AvatarMinimalPerf.default | 178 | 170 | 1.05:1 |
| ChatDuplicateMessagesPerf.default | 272 | 261 | 1.04:1 |
| InputMinimalPerf.default | 1136 | 1091 | 1.04:1 |
| ListWith60ListItems.default | 569 | 549 | 1.04:1 |
| PortalMinimalPerf.default | 156 | 150 | 1.04:1 |
| ReactionMinimalPerf.default | 340 | 326 | 1.04:1 |
| ButtonMinimalPerf.default | 149 | 145 | 1.03:1 |
| FormMinimalPerf.default | 360 | 349 | 1.03:1 |
| ItemLayoutMinimalPerf.default | 1031 | 997 | 1.03:1 |
| LabelMinimalPerf.default | 343 | 334 | 1.03:1 |
| LoaderMinimalPerf.default | 591 | 575 | 1.03:1 |
| SegmentMinimalPerf.default | 300 | 292 | 1.03:1 |
| TextAreaMinimalPerf.default | 434 | 422 | 1.03:1 |
| TreeMinimalPerf.default | 689 | 670 | 1.03:1 |
| AlertMinimalPerf.default | 234 | 229 | 1.02:1 |
| AnimationMinimalPerf.default | 464 | 457 | 1.02:1 |
| BoxMinimalPerf.default | 302 | 296 | 1.02:1 |
| ButtonSlotsPerf.default | 467 | 460 | 1.02:1 |
| ChatMinimalPerf.default | 634 | 619 | 1.02:1 |
| DialogMinimalPerf.default | 656 | 643 | 1.02:1 |
| FlexMinimalPerf.default | 252 | 248 | 1.02:1 |
| HeaderSlotsPerf.default | 662 | 652 | 1.02:1 |
| ListNestedPerf.default | 480 | 471 | 1.02:1 |
| StatusMinimalPerf.default | 585 | 572 | 1.02:1 |
| AttachmentSlotsPerf.default | 914 | 904 | 1.01:1 |
| CardMinimalPerf.default | 477 | 473 | 1.01:1 |
| CarouselMinimalPerf.default | 403 | 399 | 1.01:1 |
| MenuMinimalPerf.default | 720 | 716 | 1.01:1 |
| MenuButtonMinimalPerf.default | 1413 | 1402 | 1.01:1 |
| PopupMinimalPerf.default | 524 | 519 | 1.01:1 |
| SplitButtonMinimalPerf.default | 3681 | 3643 | 1.01:1 |
| TextMinimalPerf.default | 301 | 299 | 1.01:1 |
| CustomToolbarPrototype.default | 3509 | 3489 | 1.01:1 |
| ButtonOverridesMissPerf.default | 1440 | 1438 | 1:1 |
| CheckboxMinimalPerf.default | 2275 | 2272 | 1:1 |
| DropdownManyItemsPerf.default | 572 | 570 | 1:1 |
| DropdownMinimalPerf.default | 2563 | 2565 | 1:1 |
| EmbedMinimalPerf.default | 3517 | 3521 | 1:1 |
| GridMinimalPerf.default | 288 | 287 | 1:1 |
| ImageMinimalPerf.default | 324 | 323 | 1:1 |
| ListCommonPerf.default | 543 | 542 | 1:1 |
| ListMinimalPerf.default | 440 | 438 | 1:1 |
| RadioGroupMinimalPerf.default | 385 | 385 | 1:1 |
| SkeletonMinimalPerf.default | 305 | 305 | 1:1 |
| TooltipMinimalPerf.default | 878 | 877 | 1:1 |
| DatepickerMinimalPerf.default | 4745 | 4772 | 0.99:1 |
| ProviderMergeThemesPerf.default | 1458 | 1472 | 0.99:1 |
| SliderMinimalPerf.default | 1411 | 1430 | 0.99:1 |
| TableManyItemsPerf.default | 1605 | 1619 | 0.99:1 |
| ToolbarMinimalPerf.default | 792 | 801 | 0.99:1 |
| VideoMinimalPerf.default | 535 | 540 | 0.99:1 |
| DividerMinimalPerf.default | 305 | 312 | 0.98:1 |
| HeaderMinimalPerf.default | 306 | 312 | 0.98:1 |
| ProviderMinimalPerf.default | 964 | 979 | 0.98:1 |
| TableMinimalPerf.default | 339 | 346 | 0.98:1 |
| ChatWithPopoverPerf.default | 320 | 329 | 0.97:1 |
| LayoutMinimalPerf.default | 300 | 308 | 0.97:1 |
| IconMinimalPerf.default | 526 | 546 | 0.96:1 |
| RosterPerf.default | 976 | 1033 | 0.94:1 |
| AccordionMinimalPerf.default | 123 | 141 | 0.87:1 |
|
/azp run |
|
Azure Pipelines successfully started running 3 pipeline(s). |
|
|
||
| /** Custom props including callbacks. */ | ||
| overrideProps: TriggerProps; | ||
| }; |
There was a problem hiding this comment.
I am not sure that I like the interface there, so opened for ideas/suggestions.
There was a problem hiding this comment.
IMO the interface is fine, but there should be extra documentation that deals with the priority of the prop overriding children > overrideProps > outerProps
| outerProps[callbackName]?.(e as any); | ||
| /* eslint-enable @typescript-eslint/no-explicit-any */ | ||
| } | ||
| }); |
There was a problem hiding this comment.
Decided that it's clearer to duplicate (almost duplicate) rather than create a utility hook for this.
| }), | ||
| }); | ||
|
|
||
| ReactDOM.render(<Component />, element); |
There was a problem hiding this comment.
looking at this test, I can't see why testing-library can't be used, it will also deal with cleanup
There was a problem hiding this comment.
I want to use bare minimum in these tests to ensure that nothing that Enzyme/Testing Library inject can affect the result.
|
|
||
| if (callbackName) { | ||
| /* eslint-disable @typescript-eslint/no-explicit-any */ | ||
| childProps[callbackName]?.(e as any); |
There was a problem hiding this comment.
why did you need the casts to any ?
There was a problem hiding this comment.
I added a comment to code to clarify:
TS2345: Argument of type 'SyntheticEvent<unknown, Event>' is not assignable to parameter of type 'ClipboardEvent<unknown> & CompositionEvent<unknown> & FocusEvent<unknown> & FormEvent<unknown> & SyntheticEvent<unknown, Event> & ... 8 more ... & TransitionEvent<...>'.
Property 'clipboardData' is missing in type 'SyntheticEvent<unknown, Event>' but required in type 'ClipboardEvent<unknown>'.
event is always SyntheticEvent but callbacks have better typings and expect more specific types
| ref: React.Ref<unknown> | undefined; | ||
|
|
||
| /** Props that are passed to a parent component and should be forwarded down. */ | ||
| outerProps: React.HTMLProps<unknown>; |
There was a problem hiding this comment.
nit: it is not clear what outerProps mean without reading the docs, passedProps would be more understandable for me, but to be fair, it seems other libraries use outer as well.
There was a problem hiding this comment.
#21225 (comment)
Will be happy to iterate on interface there.
|
@layershifter @ling1726 seems like this change broke up |
@khmakoto thanks for reporting I'll take a look at it |
I checked the problem, it's related to newly introduced |
@khmakoto @layershifter recorded in issue #21350 and a fix #21349 |

Current behaviour
Currently the is no way to compose components that are using trigger pattern:
Related Issues
Fixes #21115, based on #21128.
Description of changes
This PR proposes a solution to the nested trigger composition problem with
refandpropsforwarding.Pros
useTriggerElementhook that can be reusedCons
disabled) easilydisabledandaria-disabledfrom the event targetsHow it works?
We need to have stable callbacks that we are passing to slots or when we are cloning elements (trigger's case) to not break memoization that could be used by customers:
It's easy to when we know them in advance:
But it becomes really problematic with triggers (mainly because we can't have random amount of hooks):
The solution is simple:
React.SyntheticEventalready contains a name of callback that should be called (seegetReactCallbackName()function), this was tested on all minor React versions including18-rc:With that we can have a single handler (actually two as before React 17 we don't know in which phase a callback is called) to handle all callbacks 🎉
This PR implements this solution for
MenuTriggercomponent.