Tooltip: Replace triggerAriaAttribute prop with relationship and make it required with no default#21331
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 f40285e:
|
📊 Bundle size report
Unchanged fixtures
|
Asset size changesSize Auditor did not detect a change in bundle size for any component! Baseline commit: 05da13bb294d7152dc8bb4e5a5fdc72d502156d3 (build) |
Perf Analysis (
|
| Scenario | Render type | Master Ticks | PR Ticks | Iterations | Status |
|---|---|---|---|---|---|
| Avatar | mount | 987 | 988 | 5000 | |
| BaseButton | mount | 955 | 973 | 5000 | |
| Breadcrumb | mount | 2684 | 2726 | 1000 | |
| ButtonNext | mount | 525 | 519 | 5000 | |
| Checkbox | mount | 1596 | 1592 | 5000 | |
| CheckboxBase | mount | 1373 | 1390 | 5000 | |
| ChoiceGroup | mount | 4873 | 4861 | 5000 | |
| ComboBox | mount | 1046 | 1007 | 1000 | |
| CommandBar | mount | 10432 | 10419 | 1000 | |
| ContextualMenu | mount | 8491 | 8636 | 1000 | |
| DefaultButton | mount | 1201 | 1169 | 5000 | |
| DetailsRow | mount | 3864 | 3943 | 5000 | |
| DetailsRowFast | mount | 3870 | 3903 | 5000 | |
| DetailsRowNoStyles | mount | 3662 | 3635 | 5000 | |
| Dialog | mount | 2679 | 2661 | 1000 | |
| DocumentCardTitle | mount | 182 | 205 | 1000 | |
| Dropdown | mount | 3318 | 3329 | 5000 | |
| FluentProviderNext | mount | 1946 | 1912 | 5000 | |
| FluentProviderWithTheme | mount | 167 | 180 | 10 | |
| FluentProviderWithTheme | virtual-rerender | 112 | 127 | 10 | |
| FluentProviderWithTheme | virtual-rerender-with-unmount | 199 | 185 | 10 | |
| FocusTrapZone | mount | 1851 | 1857 | 5000 | |
| FocusZone | mount | 1872 | 1840 | 5000 | |
| IconButton | mount | 1778 | 1803 | 5000 | |
| Label | mount | 393 | 376 | 5000 | |
| Layer | mount | 3091 | 3165 | 5000 | |
| Link | mount | 498 | 554 | 5000 | |
| MakeStyles | mount | 1843 | 1772 | 50000 | |
| MenuButton | mount | 1497 | 1526 | 5000 | |
| MessageBar | mount | 2053 | 2046 | 5000 | |
| Nav | mount | 3305 | 3395 | 1000 | |
| OverflowSet | mount | 1172 | 1181 | 5000 | |
| Panel | mount | 2604 | 2565 | 1000 | |
| Persona | mount | 883 | 890 | 1000 | |
| Pivot | mount | 1501 | 1475 | 1000 | |
| PrimaryButton | mount | 1362 | 1376 | 5000 | |
| Rating | mount | 7765 | 7948 | 5000 | |
| SearchBox | mount | 1367 | 1331 | 5000 | |
| Shimmer | mount | 2527 | 2681 | 5000 | |
| Slider | mount | 1967 | 1959 | 5000 | |
| SpinButton | mount | 5104 | 5035 | 5000 | |
| Spinner | mount | 472 | 469 | 5000 | |
| SplitButton | mount | 3181 | 3279 | 5000 | |
| Stack | mount | 582 | 561 | 5000 | |
| StackWithIntrinsicChildren | mount | 2349 | 2296 | 5000 | |
| StackWithTextChildren | mount | 5180 | 5331 | 5000 | |
| SwatchColorPicker | mount | 11540 | 11865 | 5000 | |
| TagPicker | mount | 2682 | 2654 | 5000 | |
| TeachingBubble | mount | 13440 | 13417 | 5000 | |
| Text | mount | 491 | 465 | 5000 | |
| TextField | mount | 1433 | 1444 | 5000 | |
| ThemeProvider | mount | 1231 | 1290 | 5000 | |
| ThemeProvider | virtual-rerender | 641 | 642 | 5000 | |
| ThemeProvider | virtual-rerender-with-unmount | 1960 | 1948 | 5000 | |
| Toggle | mount | 844 | 891 | 5000 | |
| buttonNative | mount | 164 | 161 | 5000 |
Perf Analysis (@fluentui/react-northstar)
Perf tests with no regressions
| Scenario | Current PR Ticks | Baseline Ticks | Ratio |
|---|---|---|---|
| LayoutMinimalPerf.default | 389 | 356 | 1.09:1 |
| FlexMinimalPerf.default | 318 | 294 | 1.08:1 |
| AvatarMinimalPerf.default | 206 | 193 | 1.07:1 |
| SegmentMinimalPerf.default | 365 | 340 | 1.07:1 |
| TableMinimalPerf.default | 432 | 407 | 1.06:1 |
| ChatDuplicateMessagesPerf.default | 316 | 301 | 1.05:1 |
| ListCommonPerf.default | 648 | 616 | 1.05:1 |
| ListMinimalPerf.default | 521 | 498 | 1.05:1 |
| MenuMinimalPerf.default | 873 | 834 | 1.05:1 |
| PortalMinimalPerf.default | 186 | 177 | 1.05:1 |
| RefMinimalPerf.default | 254 | 243 | 1.05:1 |
| HeaderSlotsPerf.default | 791 | 762 | 1.04:1 |
| LabelMinimalPerf.default | 407 | 393 | 1.04:1 |
| VideoMinimalPerf.default | 661 | 635 | 1.04:1 |
| AttachmentSlotsPerf.default | 1110 | 1077 | 1.03:1 |
| DividerMinimalPerf.default | 374 | 363 | 1.03:1 |
| ImageMinimalPerf.default | 383 | 372 | 1.03:1 |
| ProviderMergeThemesPerf.default | 1744 | 1689 | 1.03:1 |
| TreeWith60ListItems.default | 189 | 184 | 1.03:1 |
| CheckboxMinimalPerf.default | 2728 | 2685 | 1.02:1 |
| DialogMinimalPerf.default | 783 | 768 | 1.02:1 |
| ListNestedPerf.default | 573 | 562 | 1.02:1 |
| RosterPerf.default | 1197 | 1177 | 1.02:1 |
| TextAreaMinimalPerf.default | 501 | 493 | 1.02:1 |
| AnimationMinimalPerf.default | 557 | 553 | 1.01:1 |
| BoxMinimalPerf.default | 355 | 352 | 1.01:1 |
| ButtonSlotsPerf.default | 569 | 566 | 1.01:1 |
| CardMinimalPerf.default | 574 | 566 | 1.01:1 |
| ChatMinimalPerf.default | 728 | 720 | 1.01:1 |
| ChatWithPopoverPerf.default | 360 | 356 | 1.01:1 |
| DropdownMinimalPerf.default | 3032 | 2995 | 1.01:1 |
| EmbedMinimalPerf.default | 4161 | 4136 | 1.01:1 |
| ReactionMinimalPerf.default | 387 | 383 | 1.01:1 |
| SkeletonMinimalPerf.default | 354 | 349 | 1.01:1 |
| StatusMinimalPerf.default | 692 | 686 | 1.01:1 |
| CustomToolbarPrototype.default | 4095 | 4054 | 1.01:1 |
| ToolbarMinimalPerf.default | 933 | 921 | 1.01:1 |
| TooltipMinimalPerf.default | 1067 | 1053 | 1.01:1 |
| ButtonOverridesMissPerf.default | 1719 | 1723 | 1:1 |
| DatepickerMinimalPerf.default | 5578 | 5595 | 1:1 |
| InputMinimalPerf.default | 1307 | 1301 | 1:1 |
| ItemLayoutMinimalPerf.default | 1213 | 1209 | 1:1 |
| PopupMinimalPerf.default | 615 | 616 | 1:1 |
| RadioGroupMinimalPerf.default | 457 | 459 | 1:1 |
| DropdownManyItemsPerf.default | 680 | 690 | 0.99:1 |
| HeaderMinimalPerf.default | 375 | 380 | 0.99:1 |
| MenuButtonMinimalPerf.default | 1661 | 1686 | 0.99:1 |
| ProviderMinimalPerf.default | 1119 | 1136 | 0.99:1 |
| SplitButtonMinimalPerf.default | 4292 | 4315 | 0.99:1 |
| IconMinimalPerf.default | 621 | 630 | 0.99:1 |
| TableManyItemsPerf.default | 1903 | 1921 | 0.99:1 |
| TextMinimalPerf.default | 360 | 364 | 0.99:1 |
| CarouselMinimalPerf.default | 488 | 500 | 0.98:1 |
| FormMinimalPerf.default | 403 | 413 | 0.98:1 |
| GridMinimalPerf.default | 342 | 348 | 0.98:1 |
| ListWith60ListItems.default | 646 | 660 | 0.98:1 |
| LoaderMinimalPerf.default | 683 | 699 | 0.98:1 |
| AccordionMinimalPerf.default | 152 | 156 | 0.97:1 |
| ButtonMinimalPerf.default | 168 | 174 | 0.97:1 |
| TreeMinimalPerf.default | 806 | 833 | 0.97:1 |
| AttachmentMinimalPerf.default | 158 | 165 | 0.96:1 |
| SliderMinimalPerf.default | 1648 | 1716 | 0.96:1 |
| AlertMinimalPerf.default | 276 | 292 | 0.95:1 |
|
before we merge this PR, I would like to discuss if there are at least other ways to support the consumers to do the right thing. I agree that this makes the behavior more consistent with the standards. However we are also making it much harder to use the component for the most common use case. |
|
From our tech sync discussion: I like @behowell & @jurokapsiar's suggestions of making the prop required, and maybe called something like: |
| visible?: boolean; | ||
| onVisibleChange?: (event: React_2.PointerEvent<HTMLElement> | React_2.FocusEvent<HTMLElement> | undefined, data: OnVisibleChangeData) => void; | ||
| triggerAriaAttribute: 'label' | 'labelledby' | 'describedby' | null; | ||
| relationship: 'description' | 'label' | 'none'; |
There was a problem hiding this comment.
I would propose to change none to something more disturbing like noLabelOrDescription
There was a problem hiding this comment.
plus ensuring it is a required prop without default would make the API safer to use
There was a problem hiding this comment.
I changed it to inaccessible; how does that sound? (cc @smhigley)
triggerAriaAttribute prop with relationship and make it required with no default
| } else { | ||
| state.triggerAriaAttribute = 'labelledby'; | ||
| if (relationship === 'label') { | ||
| const hasLabel = child?.props && ('aria-label' in child.props || 'aria-labelledby' in child.props); |
There was a problem hiding this comment.
This will not work with composition properly:
<Tooltip>
<MenuTrigger>
<button aria-label='foo' />
</MenuTrigger>
</Tooltip>In this case child?.props will be actually props of MenuTrigger, not button's.
Menu had the same issue, check #21225. Is there another approach?
There was a problem hiding this comment.
This should work with Menu or any composition, since the props applied to the trigger element will always have priority over trigger overrides.
In the case where Tooltip is wrapped around MenuTrigger:
- Tooltip will fail to find aria-label or aria-describedby
- Pass and aria-* attribute through prop cloning
- Menu will let the user's aria-label win
Am I missing anything ?
There was a problem hiding this comment.
The check is actually useless, and should be removed, as long as our triggers follow the constraint that user props are always respected (this is currently the case) we can spread whatever trigger props and let the useTriggerElement hook do its job
There was a problem hiding this comment.
A bigger example why it's not useful even without composition:
const TooltipButton = React.forwardRef((props, ref) => {
return <Button {...props} ref={ref} aria-label="foo" />;
});
function App() {
return (
<Tooltip content={<div />}>
{/* 👇 this does not have "aria-label" and "aria-labelledby",
* so Tooltip will apply "aria-labelledby" as "content" is not a string
*/}
<TooltipButton />
</Tooltip>
);
}After rendering that snippet we will get a button that has both aria-label and aria-labelledby. As we discussed with @ling1726 it's more reasonable to pass props always and implement a runtime check to ensure that both attributes are not defined on a trigger HTML element. As we have an access to it via refs it should be doable.
WDYT?
There was a problem hiding this comment.
I'm not sure that setting the aria props via a ref is a good idea... it seems a bit hacky for an edge case scenario.
But thinking about it further, it might be wrong to even try to check for existing aria-label, etc... Now that the relationship prop is required, the user has to choose to set the Tooltip to be the label. If your example had relationship="label" on the Tooltip, then it's reasonable to assume that it would override the existing aria-label.
And in the case where they want to preserve the TooltipButton's internal aria-label, they can set relationship="inaccessible". In fact, that is probably a good thing, since it shines a light on the fact that the tooltip's content is inaccessible in that case.
There was a problem hiding this comment.
I'm not sure that setting the aria props via a ref is a good idea... it seems a bit hacky for an edge case scenario.
Sorry, it seems that I was not enough clear, I don't suggest to set aria props via ref. My proposal is to keep existing cloning and and validate on DOM that conflicting props are not applied:
if (process.env.NODE_ENV !== 'production') {
if (triggerRef.current) {
if (triggerRef.current.hasAttribute('aria-label') && triggerRef.current.hasAttribute('aria-labelledby') {
console.warn('Please do not combine conflicting attributes')
}
}
}There was a problem hiding this comment.
Ah got it, sorry I misunderstood--so it's just adding a warning?
I'm still inclined not to add a warning here:
- There are other hypothetical ways you could unexpectedly end up with both aria-label and aria-labelledby on some of our components. It seems out of scope to check for that in Tooltip.
- E.g. from your example, a standalone
<TooltipButton aria-labelledby="..." />would have that problem, and it wouldn't benefit from the check in Tooltip.
- E.g. from your example, a standalone
- An accessibility checker would be a better utility to have a warning like that, and it'd check everything, not just Tooltip.
- It's still a very unlikely edge case.
- People don't pay attention to console warnings.
That said, if you feel strongly about it, I can add the warning 🙂
There was a problem hiding this comment.
As for the problem with that you can't check the child props for aria-label, etc. when you have nested triggers, I have an idea for how to fix that as well, by modifying the onlyChild helper function. See my comment here for more details: #21245 (comment)
That change would allow this code to work the same regardless of whether or not there is a MenuTrigger between the Tooltip and button.
There was a problem hiding this comment.
That said, if you feel strongly about it, I can add the warning 🙂
Let's add it if someone will report us an issue then :)
There was a problem hiding this comment.
@layershifter to clarify, do you mean leave it out, and only add it if/when someone reports an issue? Or do you mean add it now, and see if someone reports an issue because of it?
If it's the former, then there are no more changes needed in this PR right? Could you approve if everything looks ok?
Thanks!
layershifter
left a comment
There was a problem hiding this comment.
We discussed this in offline. For now, let's remove hasDescription & hasLabel checks and after that we are good to merge 👍
Current Behavior
aria-descriptioninstead ofaria-describedbyfor description tooltips.New Behavior
triggerAriaAttributetorelationship, and make it a required prop with no default valuerelationshipprop, since it is now requiredRelated Issue(s)
Fixes #21297