Prevent Tooltip from overriding any aria-label, etc. props on its child#21532
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 a84ca7b:
|
📊 Bundle size report
Unchanged fixtures
|
Asset size changesSize Auditor did not detect a change in bundle size for any component! Baseline commit: a21bfaceecf201d0c984fa84704148a24f956901 (build) |
Perf Analysis (
|
| Scenario | Render type | Master Ticks | PR Ticks | Iterations | Status |
|---|---|---|---|---|---|
| ContextualMenu | mount | 14598 | 7280 | 1000 | Possible regression |
All results
| Scenario | Render type | Master Ticks | PR Ticks | Iterations | Status |
|---|---|---|---|---|---|
| Avatar | mount | 808 | 838 | 5000 | |
| BaseButton | mount | 804 | 798 | 5000 | |
| Breadcrumb | mount | 2257 | 2297 | 1000 | |
| ButtonNext | mount | 449 | 439 | 5000 | |
| Checkbox | mount | 1307 | 1331 | 5000 | |
| CheckboxBase | mount | 1056 | 1075 | 5000 | |
| ChoiceGroup | mount | 4116 | 4043 | 5000 | |
| ComboBox | mount | 844 | 857 | 1000 | |
| CommandBar | mount | 8779 | 8813 | 1000 | |
| ContextualMenu | mount | 14598 | 7280 | 1000 | Possible regression |
| DefaultButton | mount | 968 | 993 | 5000 | |
| DetailsRow | mount | 3153 | 3078 | 5000 | |
| DetailsRowFast | mount | 3224 | 3246 | 5000 | |
| DetailsRowNoStyles | mount | 2938 | 3046 | 5000 | |
| Dialog | mount | 2271 | 2249 | 1000 | |
| DocumentCardTitle | mount | 156 | 162 | 1000 | |
| Dropdown | mount | 2787 | 2668 | 5000 | |
| FluentProviderNext | mount | 1641 | 1670 | 5000 | |
| FluentProviderWithTheme | mount | 161 | 153 | 10 | |
| FluentProviderWithTheme | virtual-rerender | 87 | 98 | 10 | |
| FluentProviderWithTheme | virtual-rerender-with-unmount | 164 | 153 | 10 | |
| FocusTrapZone | mount | 1591 | 1606 | 5000 | |
| FocusZone | mount | 1566 | 1543 | 5000 | |
| IconButton | mount | 1521 | 1513 | 5000 | |
| Label | mount | 331 | 332 | 5000 | |
| Layer | mount | 2476 | 2576 | 5000 | |
| Link | mount | 413 | 430 | 5000 | |
| MakeStyles | mount | 1428 | 1335 | 50000 | |
| MenuButton | mount | 1221 | 1204 | 5000 | |
| MessageBar | mount | 1730 | 1733 | 5000 | |
| Nav | mount | 2796 | 2808 | 1000 | |
| OverflowSet | mount | 984 | 870 | 5000 | |
| Panel | mount | 2190 | 2023 | 1000 | |
| Persona | mount | 770 | 754 | 1000 | |
| Pivot | mount | 1247 | 1263 | 1000 | |
| PrimaryButton | mount | 1147 | 1028 | 5000 | |
| Rating | mount | 6433 | 6363 | 5000 | |
| SearchBox | mount | 1110 | 1074 | 5000 | |
| Shimmer | mount | 2167 | 2009 | 5000 | |
| Slider | mount | 1571 | 1628 | 5000 | |
| SpinButton | mount | 4257 | 4001 | 5000 | |
| Spinner | mount | 393 | 398 | 5000 | |
| SplitButton | mount | 2697 | 2557 | 5000 | |
| Stack | mount | 468 | 429 | 5000 | |
| StackWithIntrinsicChildren | mount | 1980 | 2007 | 5000 | |
| StackWithTextChildren | mount | 4398 | 4393 | 5000 | |
| SwatchColorPicker | mount | 9891 | 9532 | 5000 | |
| TagPicker | mount | 2241 | 2193 | 5000 | |
| TeachingBubble | mount | 11108 | 11223 | 5000 | |
| Text | mount | 395 | 398 | 5000 | |
| TextField | mount | 1228 | 1113 | 5000 | |
| ThemeProvider | mount | 1040 | 1046 | 5000 | |
| ThemeProvider | virtual-rerender | 498 | 547 | 5000 | |
| ThemeProvider | virtual-rerender-with-unmount | 1631 | 1549 | 5000 | |
| Toggle | mount | 727 | 729 | 5000 | |
| buttonNative | mount | 130 | 145 | 5000 |
Perf Analysis (@fluentui/react-northstar)
| triggerProps['aria-labelledby'] = state.content.id; | ||
| // Always render the tooltip even if hidden, so that aria-labelledby refers to a valid element | ||
| state.shouldRenderTooltip = true; | ||
| const hasLabel = child?.props && ('aria-label' in child.props || 'aria-labelledby' in child.props); |
There was a problem hiding this comment.
IMO this solution is error prone and requires a lot of responsibility from library developers as we will need to check existence of every prop that we are trying to apply. We will also need to test these cases to ensure that it works. For example, this PR already adds 3 additional tests.
Taking into account that it affects all components that use trigger pattern (Popup, Menu) I still think that it's better to update applyTriggerPropsToElement() to handle it inside .cloneElement():
React.cloneElement(children, {
...nonMergeableProps, // aria, id, etc.
...children.props,
...mergeableProps // callbacks & ref
});We also cannot rely on reading props in examples like this as we don't really traverse React tree (which is much more complex). In that example MenuTrigger will not be able to read props of Button.
And even if it works properly in the snippet below it's a side-effect.
const CallButton = React.forwardRef((props, ref) => {
return (
<Tooltip content="bar" relationship="label">
<Button
{...props}
ref={ref}
// works as Tooltip reads props
aria-label="foo"
// works as we override `props` passed by "Menu"
aria-haspopup="false"
>
Toggle menu
</Button>
</Tooltip>
);
});There was a problem hiding this comment.
Discussed in a meeting. In summary, the issues @layershifter described are nuances of prop merging in general. These trigger props are just one instance of prop merging in our library, but we do it in many places, in every component (for native props on component's root, as well as any slot).
It's probably worthwhile creating a prop merging helper that can make sure it's done safely in all cases. However, it seems out of scope to me to include that in applyTriggerPropsToChildren, which is just a wrapper around React.cloneElement.
Current Behavior
Tooltip currently overrides aria props set on their child component. For example, the Button ends up with
aria-label="Foo"in this scenario:New Behavior
Have tooltip check for the existence of
aria-label/aria-labelledbybefore setting either prop. Likewise, check foraria-description/aria-describedbybefore setting a description.Related Issue(s)