Tooltip: add support for composition with other *Trigger components#21245
Tooltip: add support for composition with other *Trigger components#21245layershifter wants to merge 3 commits into
Conversation
📊 Bundle size report
Unchanged fixtures
|
Asset size changesSize Auditor did not detect a change in bundle size for any component! Baseline commit: eba257f5078792e52dbf8c71b1fc342d607668bc (build) |
Perf Analysis (
|
| Scenario | Render type | Master Ticks | PR Ticks | Iterations | Status |
|---|---|---|---|---|---|
| ContextualMenu | mount | 16662 | 8366 | 1000 | Possible regression |
All results
| Scenario | Render type | Master Ticks | PR Ticks | Iterations | Status |
|---|---|---|---|---|---|
| Avatar | mount | 944 | 961 | 5000 | |
| BaseButton | mount | 912 | 918 | 5000 | |
| Breadcrumb | mount | 2576 | 2587 | 1000 | |
| ButtonNext | mount | 501 | 498 | 5000 | |
| Checkbox | mount | 1529 | 1522 | 5000 | |
| CheckboxBase | mount | 1290 | 1260 | 5000 | |
| ChoiceGroup | mount | 4838 | 4695 | 5000 | |
| ComboBox | mount | 1008 | 1014 | 1000 | |
| CommandBar | mount | 10404 | 10288 | 1000 | |
| ContextualMenu | mount | 16662 | 8366 | 1000 | Possible regression |
| DefaultButton | mount | 1146 | 1185 | 5000 | |
| DetailsRow | mount | 3794 | 3806 | 5000 | |
| DetailsRowFast | mount | 3807 | 3950 | 5000 | |
| DetailsRowNoStyles | mount | 3614 | 3609 | 5000 | |
| Dialog | mount | 2538 | 2583 | 1000 | |
| DocumentCardTitle | mount | 177 | 184 | 1000 | |
| Dropdown | mount | 3179 | 3164 | 5000 | |
| FluentProviderNext | mount | 1913 | 1895 | 5000 | |
| FluentProviderWithTheme | mount | 170 | 177 | 10 | |
| FluentProviderWithTheme | virtual-rerender | 124 | 127 | 10 | |
| FluentProviderWithTheme | virtual-rerender-with-unmount | 211 | 202 | 10 | |
| FocusTrapZone | mount | 1869 | 1856 | 5000 | |
| FocusZone | mount | 1835 | 1878 | 5000 | |
| IconButton | mount | 1791 | 1775 | 5000 | |
| Label | mount | 375 | 392 | 5000 | |
| Layer | mount | 3147 | 3052 | 5000 | |
| Link | mount | 508 | 499 | 5000 | |
| MakeStyles | mount | 1735 | 1705 | 50000 | |
| MenuButton | mount | 1533 | 1508 | 5000 | |
| MessageBar | mount | 2066 | 2085 | 5000 | |
| Nav | mount | 3341 | 3335 | 1000 | |
| OverflowSet | mount | 1190 | 1155 | 5000 | |
| Panel | mount | 2541 | 2559 | 1000 | |
| Persona | mount | 885 | 885 | 1000 | |
| Pivot | mount | 1504 | 1484 | 1000 | |
| PrimaryButton | mount | 1333 | 1337 | 5000 | |
| Rating | mount | 7763 | 7809 | 5000 | |
| SearchBox | mount | 1356 | 1322 | 5000 | |
| Shimmer | mount | 2544 | 2549 | 5000 | |
| Slider | mount | 1966 | 1931 | 5000 | |
| SpinButton | mount | 4875 | 4934 | 5000 | |
| Spinner | mount | 448 | 459 | 5000 | |
| SplitButton | mount | 3195 | 3070 | 5000 | |
| Stack | mount | 544 | 545 | 5000 | |
| StackWithIntrinsicChildren | mount | 2349 | 2299 | 5000 | |
| StackWithTextChildren | mount | 5422 | 5360 | 5000 | |
| SwatchColorPicker | mount | 11500 | 11584 | 5000 | |
| TagPicker | mount | 2719 | 2742 | 5000 | |
| TeachingBubble | mount | 13340 | 13478 | 5000 | |
| Text | mount | 454 | 453 | 5000 | |
| TextField | mount | 1439 | 1441 | 5000 | |
| ThemeProvider | mount | 1254 | 1251 | 5000 | |
| ThemeProvider | virtual-rerender | 654 | 651 | 5000 | |
| ThemeProvider | virtual-rerender-with-unmount | 1952 | 1923 | 5000 | |
| Toggle | mount | 877 | 854 | 5000 | |
| buttonNative | mount | 153 | 153 | 5000 |
Perf Analysis (@fluentui/react-northstar)
Perf tests with no regressions
| Scenario | Current PR Ticks | Baseline Ticks | Ratio |
|---|---|---|---|
| SkeletonMinimalPerf.default | 373 | 340 | 1.1:1 |
| PortalMinimalPerf.default | 197 | 180 | 1.09:1 |
| TableMinimalPerf.default | 414 | 388 | 1.07:1 |
| AttachmentMinimalPerf.default | 162 | 153 | 1.06:1 |
| FormMinimalPerf.default | 425 | 403 | 1.05:1 |
| GridMinimalPerf.default | 355 | 337 | 1.05:1 |
| LayoutMinimalPerf.default | 387 | 367 | 1.05:1 |
| TextMinimalPerf.default | 350 | 333 | 1.05:1 |
| ToolbarMinimalPerf.default | 970 | 926 | 1.05:1 |
| ButtonSlotsPerf.default | 566 | 546 | 1.04:1 |
| HeaderMinimalPerf.default | 381 | 367 | 1.04:1 |
| ListCommonPerf.default | 635 | 612 | 1.04:1 |
| ListWith60ListItems.default | 665 | 642 | 1.04:1 |
| PopupMinimalPerf.default | 633 | 610 | 1.04:1 |
| BoxMinimalPerf.default | 347 | 336 | 1.03:1 |
| ImageMinimalPerf.default | 400 | 389 | 1.03:1 |
| ReactionMinimalPerf.default | 391 | 380 | 1.03:1 |
| AttachmentSlotsPerf.default | 1051 | 1035 | 1.02:1 |
| DropdownManyItemsPerf.default | 691 | 677 | 1.02:1 |
| EmbedMinimalPerf.default | 4168 | 4106 | 1.02:1 |
| FlexMinimalPerf.default | 305 | 298 | 1.02:1 |
| HeaderSlotsPerf.default | 768 | 756 | 1.02:1 |
| ListMinimalPerf.default | 529 | 518 | 1.02:1 |
| ListNestedPerf.default | 573 | 561 | 1.02:1 |
| LoaderMinimalPerf.default | 691 | 676 | 1.02:1 |
| MenuMinimalPerf.default | 862 | 845 | 1.02:1 |
| RadioGroupMinimalPerf.default | 470 | 463 | 1.02:1 |
| TreeMinimalPerf.default | 804 | 789 | 1.02:1 |
| ButtonOverridesMissPerf.default | 1658 | 1646 | 1.01:1 |
| DialogMinimalPerf.default | 767 | 763 | 1.01:1 |
| InputMinimalPerf.default | 1312 | 1293 | 1.01:1 |
| ProviderMergeThemesPerf.default | 1736 | 1727 | 1.01:1 |
| SegmentMinimalPerf.default | 355 | 351 | 1.01:1 |
| StatusMinimalPerf.default | 671 | 665 | 1.01:1 |
| IconMinimalPerf.default | 606 | 600 | 1.01:1 |
| TextAreaMinimalPerf.default | 507 | 500 | 1.01:1 |
| TreeWith60ListItems.default | 178 | 176 | 1.01:1 |
| AvatarMinimalPerf.default | 199 | 200 | 1:1 |
| CardMinimalPerf.default | 537 | 536 | 1:1 |
| ChatDuplicateMessagesPerf.default | 310 | 310 | 1:1 |
| DatepickerMinimalPerf.default | 5493 | 5478 | 1:1 |
| DropdownMinimalPerf.default | 3033 | 3047 | 1:1 |
| ItemLayoutMinimalPerf.default | 1177 | 1172 | 1:1 |
| LabelMinimalPerf.default | 388 | 389 | 1:1 |
| SliderMinimalPerf.default | 1655 | 1655 | 1:1 |
| CustomToolbarPrototype.default | 3960 | 3979 | 1:1 |
| TooltipMinimalPerf.default | 1022 | 1018 | 1:1 |
| AlertMinimalPerf.default | 269 | 272 | 0.99:1 |
| ChatMinimalPerf.default | 716 | 724 | 0.99:1 |
| CheckboxMinimalPerf.default | 2650 | 2667 | 0.99:1 |
| MenuButtonMinimalPerf.default | 1688 | 1701 | 0.99:1 |
| ProviderMinimalPerf.default | 1144 | 1153 | 0.99:1 |
| TableManyItemsPerf.default | 1818 | 1838 | 0.99:1 |
| AnimationMinimalPerf.default | 535 | 547 | 0.98:1 |
| CarouselMinimalPerf.default | 467 | 477 | 0.98:1 |
| DividerMinimalPerf.default | 363 | 370 | 0.98:1 |
| SplitButtonMinimalPerf.default | 4161 | 4255 | 0.98:1 |
| VideoMinimalPerf.default | 623 | 635 | 0.98:1 |
| ButtonMinimalPerf.default | 166 | 171 | 0.97:1 |
| ChatWithPopoverPerf.default | 368 | 381 | 0.97:1 |
| RefMinimalPerf.default | 238 | 248 | 0.96:1 |
| RosterPerf.default | 1165 | 1226 | 0.95:1 |
| AccordionMinimalPerf.default | 151 | 165 | 0.92:1 |
|
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 306f677:
|
5ad0ec3 to
b69be69
Compare
| const defaultOptions: Partial<IsConformantOptions<TProps>> = { | ||
| asPropHandlesRef: true, | ||
| componentPath: module!.parent!.filename.replace('.test', ''), | ||
| extraTests: makeStylesTests as TestObject<TProps>, |
There was a problem hiding this comment.
As our tests currently perform asserts only for root slot these tests are not applicable.
| // eslint-disable-next-line @typescript-eslint/ban-ts-comment | ||
| // @ts-ignore | ||
| 'data-testid': 'the-tooltip', |
There was a problem hiding this comment.
As our typings are strict we don't allow to pass data- attributes:
src/components/Tooltip/Tooltip.test.tsx:52:11 - error TS2322: Type '{ children: JSX.Element; id: string; 'data-testid': string; }' is not assignable to type 'ShorthandProps<{ as?: "div" | undefined; } & Pick<DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "key" | keyof HTMLAttributes<...>> & { ...; } & { ...; }>'.
Object literal may only specify known properties, and ''data-testid'' does not exist in type 'ReactElement<any, string | JSXElementConstructor<any>> | ReactNodeArray | ReactPortal | ({ as?: "div" | undefined; } & Pick<...> & { ...; } & { ...; })'.
52 'data-testid': 'the-tooltip',
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This probably should be turned into an issue.
There was a problem hiding this comment.
FYI, I'm removing the data attributes from the tests with this PR: #21331.
I agree it's still an issue that our types don't allow for data attributes, but at least it won't be an issue in this particular case.
| /** | ||
| * A tooltip provides light weight contextual information on top of its target element. | ||
| */ | ||
| export const Tooltip: ForwardRefComponent<TooltipProps> = React.forwardRef((props, ref) => { |
There was a problem hiding this comment.
Keeping it as it is impossible as it causes a set of TS issues:
src/components/Tooltip/Tooltip.tsx:11:14 - error TS2322: Type 'ForwardRefExoticComponent<RefAttributes<unknown>>' is not assignable to type 'never'.
11 export const Tooltip: ForwardRefComponent<TooltipProps> = React.forwardRef((props, ref) => {
~~~~~~~
src/components/Tooltip/Tooltip.tsx:12:28 - error TS2345: Argument of type '{ children?: ReactNode; }' is not assignable to parameter of type 'TooltipProps'.
Type '{ children?: ReactNode; }' is not assignable to type '{ children?: (ReactElement<any, string | JSXElementConstructor<any>> & { ref?: Ref<unknown> | undefined; }) | ((props: TooltipTriggerProps) => ReactNode) | undefined; }'.
Types of property 'children' are incompatible.
Type 'ReactNode' is not assignable to type '(ReactElement<any, string | JSXElementConstructor<any>> & { ref?: Ref<unknown> | undefined; }) | ((props: TooltipTriggerProps) => ReactNode) | undefined'.
Type 'null' is not assignable to type '(ReactElement<any, string | JSXElementConstructor<any>> & { ref?: Ref<unknown> | undefined; }) | ((props: TooltipTriggerProps) => ReactNode) | undefined'.
12 const state = useTooltip(props, ref);
~~~~~
src/components/Tooltip/Tooltip.tsx:18:9 - error TS2339: Property 'displayName' does not exist on type 'never'.
| onPointerEnter: onEnterTrigger, | ||
| onPointerLeave: onLeaveTrigger, | ||
| onFocus: onEnterTrigger, | ||
| onBlur: onLeaveTrigger, |
There was a problem hiding this comment.
Merging is not longer requires as it's done in useTriggerElement().
behowell
left a comment
There was a problem hiding this comment.
Looks good! Just one comment about not using forwardRef with no root slot.
| * A tooltip provides light weight contextual information on top of its target element. | ||
| */ | ||
| export const Tooltip: ForwardRefComponent<TooltipProps> = React.forwardRef((props, ref) => { | ||
| export const Tooltip = React.forwardRef<HTMLElement, TooltipProps>((props, ref) => { |
There was a problem hiding this comment.
Since we don't have a root slot, this probably shouldn't use forwardRef. If someone needs a ref to the content slot, they can pass it as an attribute to that slot.
There was a problem hiding this comment.
Well, it should as we also forwarding refs for composition purpose to trigger:
<MenuTrigger>
<Tooltip>
<button />
</Tooltip>
</MenuTrigger>In this example MenuTrigger still need to pass ref to button, without ref forwarding it's not possible. As an option we can use cast to a regular React.FunctionComponent to hide this contract. WDYT?
There was a problem hiding this comment.
Just to make sure I understand, if I write the following, you're saying myRef be the <button />?
<Tooltip ref={myRef} content="...">
<button />
</Tooltip>That seems weird to me... first that the ref on Tooltip goes to another element other than the Tooltip, and second if we're forwarding ref, why aren't we forwarding other props?
Stepping back a minute, I wonder if another way to solve the problem of applying props to the trigger would be to update onlyChild to know to drill through to the children of Tooltip and MenuTrigger the same way that it currently drills through to the children of React.Fragment. That way both MenuTrigger and Tooltip would be able to "see through" each other to the trigger element, and they could apply props and refs to the trigger directly, instead of relying on the other to forward them.
E.g. maybe we could have an attribute e.g. isFluentTrigger (except with a better name) on the Tooltip and MenuTrigger types like:
// in Tooltip.tsx:
Tooltip.isFluentTrigger = true;
// in MenuTrigger.tsx:
MenuTrigger.isFluentTrigger = true;
// New utility function isFluentTrigger:
const isFluentTrigger = <P>(type: React.ReactElement<P>['type'] & { isFluentTrigger?: boolean }) => {
return typeof type !== 'string' && 'isFluentTrigger' in type && type.isFluentTrigger;
};
// in onlyChild.ts:
return child.type === React.Fragment || isFluentTrigger(child.type) ? onlyChild(child.props.children) : child;There was a problem hiding this comment.
Just to make sure I understand, if I write the following, you're saying
myRefbe the<button />?<Tooltip ref={myRef} content="..."> <button /> </Tooltip>That seems weird to me... first that the ref on Tooltip goes to another element other than the Tooltip, and second if we're forwarding
ref, why aren't we forwarding other props?
Correct and we are forwarding props. It's how composition of triggers works.
There was a problem hiding this comment.
Stepping back a minute, I wonder if another way to solve the problem of applying props to the trigger would be to update
onlyChildto know to drill through to the children ofTooltipandMenuTriggerthe same way that it currently drills through to the children ofReact.Fragment. That way both MenuTrigger and Tooltip would be able to "see through" each other to the trigger element, and they could apply props and refs to the trigger directly, instead of relying on the other to forward them.
We had similar approach in N*, but with React you cannot predict element's children:
function MyTrigger(props) {
const data = useData();
return (
<MenuTrigger {...props}>
<CustomButton>{data.count}</CustomButton>
</MenuTrigger>
);
}In this example MyTrigger.isFluentTrigger will be undefined, but it still valid code.
b69be69 to
bef7414
Compare
| useTooltipStyles_unstable(state); | ||
| return renderTooltip_unstable(state); | ||
| }; | ||
| }) as React.FC<TooltipProps>; |
There was a problem hiding this comment.
This type cast allows to hide ref from Tooltip props.
|
Closing in favour of #21495. |
PR changes
Tooltipin this PR usesuseTriggerElement()hook introduced in #21225. This adds support for composition with*Triggercomponents:With changes in this PR the order of
MenuTrigger&Tooltipin markup does matter anymore.