Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as React from 'react';

@github-actions github-actions Bot Aug 2, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🕵🏾‍♀️ visual changes to review in the Visual Change Report

vr-tests-react-components/CalendarCompat 4 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/CalendarCompat.multiDayView - High Contrast.default.chromium.png 1228 Changed
vr-tests-react-components/CalendarCompat.multiDayView.default.chromium_1.png 489 Changed
vr-tests-react-components/CalendarCompat.multiDayView - Dark Mode.default.chromium.png 1103 Changed
vr-tests-react-components/CalendarCompat.multiDayView - RTL.default.chromium.png 494 Changed
vr-tests-react-components/Menu 1 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/Menu.Nested Submenus Small Viewport Stacked.nested menu.chromium.png 841 Changed
vr-tests-react-components/Menu Converged - submenuIndicator slotted content 1 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/Menu Converged - submenuIndicator slotted content.default.submenus open.chromium.png 413 Changed
vr-tests-react-components/Positioning 2 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/Positioning.Positioning end.chromium.png 957 Changed
vr-tests-react-components/Positioning.Positioning end.updated 2 times.chromium.png 744 Changed
vr-tests-react-components/ProgressBar converged 3 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/ProgressBar converged.Indeterminate + thickness - Dark Mode.default.chromium.png 40 Changed
vr-tests-react-components/ProgressBar converged.Indeterminate + thickness.default.chromium.png 27 Changed
vr-tests-react-components/ProgressBar converged.Indeterminate + thickness - High Contrast.default.chromium.png 35 Changed
vr-tests-react-components/TagPicker 2 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/TagPicker.disabled - Dark Mode.chromium.png 658 Changed
vr-tests-react-components/TagPicker.disabled.chromium.png 677 Changed
vr-tests-react-components/Tooltip Converged 3 screenshots
Image Name Diff(in Pixels) Image Type
vr-tests-react-components/Tooltip Converged.secondary content - Dark Mode.default.chromium.png 0 Added
vr-tests-react-components/Tooltip Converged.secondary content - High Contrast.default.chromium.png 0 Added
vr-tests-react-components/Tooltip Converged.secondary content.default.chromium.png 0 Added

There were 3 duplicate changes discarded. Check the build logs for more information.

import type { Meta } from '@storybook/react-webpack5';
import { Tooltip } from '@fluentui/react-tooltip';
import { TextBoldRegular } from '@fluentui/react-icons';

import { useStyles } from './utils';
import { DARK_MODE, getStoryVariant, HIGH_CONTRAST, TestWrapperDecorator } from '../../utilities';
Expand Down Expand Up @@ -29,6 +30,21 @@ export const BasicDarkMode = getStoryVariant(Basic, DARK_MODE);

export const BasicHighContrast = getStoryVariant(Basic, HIGH_CONTRAST);

export const SecondaryContent = () => (
<div className={useStyles().wrapper}>
<Tooltip visible content="Bold" secondaryContent="Ctrl+B" relationship="label">
<button aria-keyshortcuts="Control+B">
<TextBoldRegular />
</button>
</Tooltip>
</div>
);
SecondaryContent.storyName = 'secondary content';

export const SecondaryContentDarkMode = getStoryVariant(SecondaryContent, DARK_MODE);

export const SecondaryContentHighContrast = getStoryVariant(SecondaryContent, HIGH_CONTRAST);

export const Inverted = () => (
<div className={useStyles().wrapper}>
<Tooltip visible appearance="inverted" content="Inverted tooltip" relationship="description">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "feat: add secondary content to Tooltip",
"packageName": "@fluentui/react-tooltip",
"email": "petrduda@microsoft.com",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export type TooltipProps = ComponentProps<TooltipSlots> & TriggerProps<TooltipTr
// @public
export type TooltipSlots = {
content: NonNullable<Slot<'div'>>;
secondaryContent?: Slot<'span'>;
};

// @public
Expand All @@ -59,6 +60,8 @@ export type TooltipState = ComponentState<TooltipSlots> & Pick<TooltipProps, 'mo
shouldRenderTooltip?: boolean;
arrowRef?: React_2.Ref<HTMLDivElement>;
arrowClassName?: string;
contentLayoutClassName?: string;
primaryContentClassName?: string;
};

// @public
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,13 @@ describe('Tooltip', () => {
'consistent-callback-args': {
legacyCallbacks: ['onVisibleChange'],
},
'has-static-classnames': [
{
props: {
secondaryContent: 'Test secondary content',
},
},
],
},
});

Expand Down Expand Up @@ -88,6 +95,38 @@ describe('Tooltip', () => {
expect(target.getAttribute('aria-labelledby')).toBe('the-tooltip-id');
});

it('renders secondary content without including it in the accessible label', () => {
const result = render(
<Tooltip content="Bold" secondaryContent="Ctrl+B" relationship="label" visible>
<button aria-keyshortcuts="Control+B" />
</Tooltip>,
);

const tooltip = getByRoleTooltip(result);
const target = result.getByRole('button');
const secondaryContent = tooltip.querySelector('.fui-Tooltip__secondaryContent');

expect(tooltip.textContent).toBe('BoldCtrl+B');
expect(target.getAttribute('aria-label')).toBe('Bold');
expect(target.getAttribute('aria-keyshortcuts')).toBe('Control+B');
expect(secondaryContent?.hasAttribute('aria-hidden')).toBe(false);
});

it('renders secondary content without hiding it from assistive technologies', () => {
const result = render(
<Tooltip content="Bold" secondaryContent="Ctrl+B" relationship="description">
<button aria-keyshortcuts="Control+B" />
</Tooltip>,
);

const tooltip = getByRoleTooltip(result);
const target = result.getByRole('button');
const secondaryContent = tooltip.querySelector('.fui-Tooltip__secondaryContent');

expect(target.getAttribute('aria-describedby')).toBe(tooltip.id);
expect(secondaryContent?.hasAttribute('aria-hidden')).toBe(false);
});

it('renders a description tooltip content always', () => {
const result = render(
<Tooltip content="Description tooltip" relationship="description">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ export type TooltipSlots = {
* The text or JSX content of the tooltip.
*/
content: NonNullable<Slot<'div'>>;

/**
* Secondary content rendered opposite the primary content (e.g. shortcut text).
*/
secondaryContent?: Slot<'span'>;
};

/**
Expand Down Expand Up @@ -151,6 +156,16 @@ export type TooltipState = ComponentState<TooltipSlots> &
* CSS class for the arrow element
*/
arrowClassName?: string;

/**
* CSS class for the content layout when secondary content is present.
*/
contentLayoutClassName?: string;

/**
* CSS class for the primary content when secondary content is present.
*/
primaryContentClassName?: string;
};

export type TooltipBaseState = Omit<TooltipState, 'appearance'>;
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,14 @@ export const renderTooltip_unstable = (state: TooltipBaseState): JSXElement => {
<Portal mountNode={state.mountNode}>
<state.content>
{state.withArrow && <div ref={state.arrowRef} className={state.arrowClassName} />}
{state.content.children}
{state.secondaryContent ? (
<div className={state.contentLayoutClassName}>
<div className={state.primaryContentClassName}>{state.content.children}</div>
<state.secondaryContent />
</div>
) : (
state.content.children
)}
</state.content>
</Portal>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export const useTooltipBase_unstable = (props: TooltipBaseProps): TooltipBaseSta
const {
children,
content,
secondaryContent,
withArrow = false,
positioning = 'above',
onVisibleChange,
Expand All @@ -70,13 +71,17 @@ export const useTooltipBase_unstable = (props: TooltipBaseProps): TooltipBaseSta
// Slots
components: {
content: 'div',
secondaryContent: 'span',
},
content: slot.always(content, {
defaultProps: {
role: 'tooltip',
},
elementType: 'div',
}),
secondaryContent: slot.optional(secondaryContent, {
elementType: 'span',
}),
};

state.content.id = useId('tooltip-', state.content.id);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type { SlotClassNames } from '@fluentui/react-utilities';

export const tooltipClassNames: SlotClassNames<TooltipSlots> = {
content: 'fui-Tooltip__content',
secondaryContent: 'fui-Tooltip__secondaryContent',
};

/**
Expand Down Expand Up @@ -51,6 +52,33 @@ const useStyles = makeStyles({
},

arrow: createArrowStyles({ arrowHeight }),

contentLayout: {
display: 'flex',
flexWrap: 'wrap',
columnGap: tokens.spacingHorizontalS,
rowGap: tokens.spacingVerticalXXS,
},

primaryContent: {
minWidth: 0,
flexGrow: 1,
flexShrink: 1,
},

secondaryContent: {
marginInlineStart: 'auto',
whiteSpace: 'nowrap',
color: tokens.colorNeutralForeground3,

'@media (forced-colors: active)': {
color: 'inherit',
},
},

secondaryContentInverted: {
color: tokens.colorNeutralForegroundInverted2,
},
});

/**
Expand All @@ -72,5 +100,19 @@ export const useTooltipStyles_unstable = (state: TooltipState): TooltipState =>
// eslint-disable-next-line react-hooks/immutability
state.arrowClassName = styles.arrow;

if (state.secondaryContent) {
// eslint-disable-next-line react-hooks/immutability
state.contentLayoutClassName = styles.contentLayout;
// eslint-disable-next-line react-hooks/immutability
state.primaryContentClassName = styles.primaryContent;
// eslint-disable-next-line react-hooks/immutability
state.secondaryContent.className = mergeClasses(
tooltipClassNames.secondaryContent,
styles.secondaryContent,
state.appearance === 'inverted' && styles.secondaryContentInverted,
state.secondaryContent.className,
);
}

return state;
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@
Tooltips should always wrap interactive controls like buttons, links, form fields, gridcells, etc. For tooltips that should be triggered by static icons, the `InfoLabel` control is available as an accessible tooltip-like pattern.

Tooltips should not be used to provide a full-text alternative to truncated content. For more infomation on the accessibility of truncated content, see our [truncation docs](https://react.fluentui.dev/?path=/docs/concepts-developer-accessibility-truncation--docs).

Use `secondaryContent` for short supporting information such as a keyboard shortcut. Set `aria-keyshortcuts` on the trigger to expose keyboard shortcuts semantically to assistive technologies.
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import * as React from 'react';
import type { JSXElement } from '@fluentui/react-components';
import { Button, Tooltip } from '@fluentui/react-components';
import { TextBoldRegular } from '@fluentui/react-icons';

export const SecondaryContent = (): JSXElement => (
<Tooltip content="Bold" secondaryContent="Ctrl+B" relationship="label">
<Button aria-keyshortcuts="Control+B" icon={<TextBoldRegular />} />
</Tooltip>
);

SecondaryContent.parameters = {
docs: {
description: {
story:
'Use `secondaryContent` for short supporting information such as a keyboard shortcut. ' +
'Use `aria-keyshortcuts` on the trigger to expose the keyboard shortcut semantically.',
},
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Tooltip } from '@fluentui/react-components';
import descriptionMd from './TooltipDescription.md';
import accessibilityMd from './TooltipAccessibility.md';
export { Default } from './TooltipDefault.stories';
export { SecondaryContent } from './TooltipSecondaryContent.stories';
export { RelationshipLabel } from './TooltipRelationshipLabel.stories';
export { RelationshipDescription } from './TooltipRelationshipDescription.stories';
export { Inverted } from './TooltipInverted.stories';
Expand Down
Loading