Skip to content

Tooltip: Replace triggerAriaAttribute prop with relationship and make it required with no default#21331

Merged
behowell merged 15 commits into
microsoft:masterfrom
behowell:tooltip-aria-type
Jan 26, 2022
Merged

Tooltip: Replace triggerAriaAttribute prop with relationship and make it required with no default#21331
behowell merged 15 commits into
microsoft:masterfrom
behowell:tooltip-aria-type

Conversation

@behowell

@behowell behowell commented Jan 19, 2022

Copy link
Copy Markdown
Contributor

Current Behavior

  • Tooltip defaults to setting aria-label on its trigger element, which could unexpectedly supersede the existing label on the tooltip's trigger.
  • The triggerAriaAttribute prop is poorly named.
  • The triggerAriaAttribute property is too specific about which aria attribute we set. For example, in the future we might want to have tooltips automatically set aria-description instead of aria-describedby for description tooltips.

New Behavior

  • Change the name of triggerAriaAttribute to relationship, and make it a required prop with no default value
  • Improve tooltip unit tests and remove the snapshots, whose utility was dubious at best
  • Update tooltip stories to include the relationship prop, since it is now required

Related Issue(s)

Fixes #21297

@codesandbox-ci

codesandbox-ci Bot commented Jan 19, 2022

Copy link
Copy Markdown

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:

Sandbox Source
@fluentui/react 8 starter Configuration
@fluentui/react-components 9 starter Configuration

@fabricteam

fabricteam commented Jan 19, 2022

Copy link
Copy Markdown
Collaborator

📊 Bundle size report

Package & Exports Baseline (minified/GZIP) PR Change
react-tooltip
Tooltip
45.013 kB
15.641 kB
44.872 kB
15.617 kB
-141 B
-24 B
Unchanged fixtures
Package & Exports Size (minified/GZIP)
react-components
react-components: Accordion, Button, FluentProvider, Image, Menu, Popover
166.089 kB
47.059 kB
react-components
react-components: FluentProvider & webLightTheme
32.489 kB
10.636 kB
react-menu
Menu (including children components)
103.149 kB
31.833 kB
react-menu
Menu (including selectable components)
105.4 kB
32.191 kB
🤖 This report was generated against 05da13bb294d7152dc8bb4e5a5fdc72d502156d3

@size-auditor

size-auditor Bot commented Jan 19, 2022

Copy link
Copy Markdown

Asset size changes

Size Auditor did not detect a change in bundle size for any component!

Baseline commit: 05da13bb294d7152dc8bb4e5a5fdc72d502156d3 (build)

@fabricteam

fabricteam commented Jan 19, 2022

Copy link
Copy Markdown
Collaborator

Perf Analysis (@fluentui/react)

No significant results to display.

All results

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

@jurokapsiar

Copy link
Copy Markdown
Contributor

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.

@smhigley

smhigley commented Jan 19, 2022

Copy link
Copy Markdown
Contributor

From our tech sync discussion:

I like @behowell & @jurokapsiar's suggestions of making the prop required, and maybe called something like:
relationship: 'description' | 'label' | 'dangerousNone' (maybe workshop that last one 😂)

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';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would propose to change none to something more disturbing like noLabelOrDescription

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

plus ensuring it is a required prop without default would make the API safer to use

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed it to inaccessible; how does that sound? (cc @smhigley)

@behowell behowell marked this pull request as ready for review January 19, 2022 21:05
@behowell behowell requested review from a team and khmakoto as code owners January 19, 2022 21:05
@behowell behowell changed the title Tooltip: Default to setting the trigger's description instead of label Tooltip: Replace triggerAriaAttribute prop with relationship and make it required with no default Jan 19, 2022
@behowell behowell requested a review from smhigley January 19, 2022 21:06
} else {
state.triggerAriaAttribute = 'labelledby';
if (relationship === 'label') {
const hasLabel = child?.props && ('aria-label' in child.props || 'aria-labelledby' in child.props);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. Tooltip will fail to find aria-label or aria-describedby
  2. Pass and aria-* attribute through prop cloning
  3. Menu will let the user's aria-label win

Am I missing anything ?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@layershifter layershifter Jan 20, 2022

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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')
    }
  }
}

@behowell behowell Jan 21, 2022

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
  • 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 🙂

@behowell behowell Jan 22, 2022

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 :)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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!

@TristanWatanabe TristanWatanabe removed their request for review January 24, 2022 16:44

@layershifter layershifter left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We discussed this in offline. For now, let's remove hasDescription & hasLabel checks and after that we are good to merge 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Development

Successfully merging this pull request may close these issues.

Tooltip a11y review: Re-consider default value for triggerAriaAttribute

6 participants