diff --git a/apps/vr-tests-react-components/src/stories/Tooltip/Tooltip.stories.tsx b/apps/vr-tests-react-components/src/stories/Tooltip/Tooltip.stories.tsx
index ce840bb11acef..25733bfbd31fc 100644
--- a/apps/vr-tests-react-components/src/stories/Tooltip/Tooltip.stories.tsx
+++ b/apps/vr-tests-react-components/src/stories/Tooltip/Tooltip.stories.tsx
@@ -1,6 +1,7 @@
import * as React from 'react';
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';
@@ -29,6 +30,21 @@ export const BasicDarkMode = getStoryVariant(Basic, DARK_MODE);
export const BasicHighContrast = getStoryVariant(Basic, HIGH_CONTRAST);
+export const SecondaryContent = () => (
+
+
+
+
+
+);
+SecondaryContent.storyName = 'secondary content';
+
+export const SecondaryContentDarkMode = getStoryVariant(SecondaryContent, DARK_MODE);
+
+export const SecondaryContentHighContrast = getStoryVariant(SecondaryContent, HIGH_CONTRAST);
+
export const Inverted = () => (
diff --git a/change/@fluentui-react-tooltip-d906f048-c7cf-41ab-bb86-27edec248fed.json b/change/@fluentui-react-tooltip-d906f048-c7cf-41ab-bb86-27edec248fed.json
new file mode 100644
index 0000000000000..8def9b2b399ea
--- /dev/null
+++ b/change/@fluentui-react-tooltip-d906f048-c7cf-41ab-bb86-27edec248fed.json
@@ -0,0 +1,7 @@
+{
+ "type": "minor",
+ "comment": "feat: add secondary content to Tooltip",
+ "packageName": "@fluentui/react-tooltip",
+ "email": "petrduda@microsoft.com",
+ "dependentChangeType": "patch"
+}
diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Tooltip/Tooltip.test.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Tooltip/Tooltip.test.tsx
index 5137f1b26b42c..6e42a086bd6a9 100644
--- a/packages/react-components/react-headless-components-preview/library/src/components/Tooltip/Tooltip.test.tsx
+++ b/packages/react-components/react-headless-components-preview/library/src/components/Tooltip/Tooltip.test.tsx
@@ -1,6 +1,7 @@
import * as React from 'react';
+import { renderToStaticMarkup } from 'react-dom/server';
import { render, screen } from '@testing-library/react';
-import { resetIdsForTests } from '@fluentui/react-utilities';
+import { resetIdsForTests, SSRProvider } from '@fluentui/react-utilities';
import { isConformant } from '../../testing/isConformant';
import type { IsConformantOptions } from '@fluentui/react-conformance';
import { Tooltip } from './Tooltip';
@@ -100,6 +101,80 @@ describe('Tooltip', () => {
expect(target).toHaveAttribute('aria-describedby', tooltip.id);
});
+ it('renders secondary content', () => {
+ render(
+
+
+ ,
+ );
+
+ const tooltip = screen.getByRole('tooltip');
+ const target = screen.getByRole('button');
+ expect(tooltip).toHaveTextContent('BoldCtrl+B');
+ expect(tooltip.querySelector('span')).toHaveTextContent('Ctrl+B');
+ expect(target).toHaveAttribute('aria-label', 'Bold Ctrl+B');
+ expect(target).not.toHaveAttribute('aria-labelledby');
+ });
+
+ it('includes secondary content in a rich accessible label', () => {
+ render(
+ Bold} secondaryContent="Ctrl+B" relationship="label">
+
+ ,
+ );
+
+ const tooltip = screen.getByRole('tooltip');
+ const target = screen.getByRole('button');
+ const secondaryContent = tooltip.querySelector(':scope > span');
+
+ expect(target).toHaveAttribute('aria-labelledby', tooltip.id);
+ expect(secondaryContent).not.toHaveAttribute('aria-hidden');
+ });
+
+ it('keeps a string label with secondary content when the trigger popup is expanded', () => {
+ render(
+
+
+ ,
+ );
+
+ expect(screen.queryByRole('tooltip')).toBeNull();
+ expect(screen.getByRole('button')).toHaveAttribute('aria-label', 'Bold Ctrl+B');
+ });
+
+ it('keeps a generic secondary content label when the trigger popup is expanded', () => {
+ render(
+ Ctrl+B} relationship="label" visible>
+
+ ,
+ );
+
+ const tooltip = document.querySelector('[role="tooltip"]');
+ const target = screen.getByRole('button');
+
+ expect(tooltip).not.toBeNull();
+ expect(target).not.toHaveAttribute('aria-label');
+ expect(target).toHaveAttribute('aria-labelledby', tooltip?.id);
+ expect(tooltip).toHaveTextContent('BoldCtrl+B');
+ });
+
+ it('keeps the primary string label during SSR with generic secondary content', () => {
+ const html = renderToStaticMarkup(
+
+ Ctrl+B} relationship="label">
+
+
+ ,
+ );
+
+ expect(html).toContain('aria-label="Bold"');
+ expect(html).not.toContain('aria-labelledby');
+ });
+
it('renders arrow element when withArrow is true', () => {
render(
diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Tooltip/renderTooltip.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Tooltip/renderTooltip.tsx
index 4e8d795e2ae94..564aab58d5f66 100644
--- a/packages/react-components/react-headless-components-preview/library/src/components/Tooltip/renderTooltip.tsx
+++ b/packages/react-components/react-headless-components-preview/library/src/components/Tooltip/renderTooltip.tsx
@@ -17,7 +17,14 @@ export const renderTooltip = (state: TooltipState): JSXElement => {
{state.shouldRenderTooltip && (
{state.withArrow && }
- {state.content.children}
+ {state.secondaryContent ? (
+ <>
+ {state.content.children}
+
+ >
+ ) : (
+ state.content.children
+ )}
)}
>
diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Tooltip/useTooltip.ts b/packages/react-components/react-headless-components-preview/library/src/components/Tooltip/useTooltip.ts
index 5fe5f4204bf11..f03b8c0a405ee 100644
--- a/packages/react-components/react-headless-components-preview/library/src/components/Tooltip/useTooltip.ts
+++ b/packages/react-components/react-headless-components-preview/library/src/components/Tooltip/useTooltip.ts
@@ -36,6 +36,7 @@ export const useTooltip = (props: TooltipProps): TooltipState => {
const {
children,
content,
+ secondaryContent,
positioning = 'above',
withArrow = false,
onVisibleChange,
@@ -44,17 +45,24 @@ export const useTooltip = (props: TooltipProps): TooltipState => {
hideDelay = 250,
} = props;
+ const child = getTriggerChild(children);
+ const isPopupExpanded =
+ child?.props?.['aria-haspopup'] &&
+ (child?.props?.['aria-expanded'] === true || child?.props?.['aria-expanded'] === 'true');
+ const renderedVisible = visible && !isPopupExpanded;
+
const state: TooltipState = {
positioning,
showDelay,
hideDelay,
relationship,
- visible,
- shouldRenderTooltip: visible,
+ visible: renderedVisible,
+ shouldRenderTooltip: renderedVisible,
withArrow,
// Slots
components: {
content: 'div',
+ secondaryContent: 'span',
},
content: slot.always(content, {
defaultProps: {
@@ -63,11 +71,13 @@ export const useTooltip = (props: TooltipProps): TooltipState => {
},
elementType: 'div',
}),
+ secondaryContent: slot.optional(secondaryContent, {
+ elementType: 'span',
+ }),
};
const positioningOptions = resolvePositioningShorthand(positioning);
const { targetRef, containerRef } = usePositioning(positioningOptions);
-
state.content.id = useId('tooltip-', state.content.id);
const contentRef = useMergedRefs(state.content.ref, containerRef);
@@ -105,7 +115,7 @@ export const useTooltip = (props: TooltipProps): TooltipState => {
el.addEventListener('toggle', onToggle);
try {
- if (visible) {
+ if (state.visible) {
el.showPopover();
} else if (el.matches(':popover-open')) {
el.hidePopover();
@@ -126,7 +136,7 @@ export const useTooltip = (props: TooltipProps): TooltipState => {
return () => {
el.removeEventListener('toggle', onToggle);
};
- }, [contentRef, visible, setVisible, onToggle]);
+ }, [contentRef, state.visible, setVisible, onToggle]);
// Used to skip showing the tooltip in certain situations when the trigger is focused.
// See comments where this is set for more info.
@@ -213,16 +223,18 @@ export const useTooltip = (props: TooltipProps): TooltipState => {
// eslint-disable-next-line react-hooks/immutability, react-hooks/refs
state.content.onBlur = mergeCallbacks(state.content.onBlur, onLeaveTrigger);
- const child = getTriggerChild(children);
-
const triggerAriaProps: Pick = {};
- const isPopupExpanded =
- child?.props?.['aria-haspopup'] &&
- (child?.props?.['aria-expanded'] === true || child?.props?.['aria-expanded'] === 'true');
if (relationship === 'label') {
// aria-label only works if the content is a string. Otherwise, need to use aria-labelledby.
- if (typeof state.content.children === 'string') {
+ if (
+ typeof state.content.children === 'string' &&
+ (!state.secondaryContent || typeof state.secondaryContent.children === 'string')
+ ) {
+ triggerAriaProps['aria-label'] = state.secondaryContent
+ ? `${state.content.children} ${state.secondaryContent.children}`
+ : state.content.children;
+ } else if (isServerSideRender && typeof state.content.children === 'string') {
triggerAriaProps['aria-label'] = state.content.children;
} else {
triggerAriaProps['aria-labelledby'] = state.content.id;
@@ -237,9 +249,8 @@ export const useTooltip = (props: TooltipProps): TooltipState => {
state.shouldRenderTooltip = true;
}
- // Case 1: Don't render the Tooltip in SSR to avoid hydration errors
- // Case 2: Don't render the Tooltip, if it triggers Menu or another popup and it's already opened
- if (isServerSideRender || isPopupExpanded) {
+ // Don't render the Tooltip in SSR to avoid hydration errors.
+ if (isServerSideRender) {
// eslint-disable-next-line react-hooks/immutability
state.shouldRenderTooltip = false;
}
diff --git a/packages/react-components/react-tooltip/library/etc/react-tooltip.api.md b/packages/react-components/react-tooltip/library/etc/react-tooltip.api.md
index 902ae0bd236b0..fde29a8cbdfe9 100644
--- a/packages/react-components/react-tooltip/library/etc/react-tooltip.api.md
+++ b/packages/react-components/react-tooltip/library/etc/react-tooltip.api.md
@@ -50,6 +50,7 @@ export type TooltipProps = ComponentProps & TriggerProps>;
+ secondaryContent?: Slot<'span'>;
};
// @public
@@ -59,6 +60,8 @@ export type TooltipState = ComponentState & Pick;
arrowClassName?: string;
+ contentLayoutClassName?: string;
+ primaryContentClassName?: string;
};
// @public
diff --git a/packages/react-components/react-tooltip/library/src/components/Tooltip/Tooltip.test.tsx b/packages/react-components/react-tooltip/library/src/components/Tooltip/Tooltip.test.tsx
index 1c74c0f9b3035..47cc2a76cfc26 100644
--- a/packages/react-components/react-tooltip/library/src/components/Tooltip/Tooltip.test.tsx
+++ b/packages/react-components/react-tooltip/library/src/components/Tooltip/Tooltip.test.tsx
@@ -1,10 +1,11 @@
import * as React from 'react';
+import { renderToStaticMarkup } from 'react-dom/server';
import { Tooltip } from './Tooltip';
import { isConformant } from '../../testing/isConformant';
import type { IsConformantOptions } from '@fluentui/react-conformance';
import type { RenderResult } from '@testing-library/react';
import { act, fireEvent, render } from '@testing-library/react';
-import { resetIdsForTests } from '@fluentui/react-utilities';
+import { resetIdsForTests, SSRProvider } from '@fluentui/react-utilities';
// testing-library's queryByRole function doesn't look inside portals
function queryByRoleTooltip(result: RenderResult) {
@@ -44,6 +45,13 @@ describe('Tooltip', () => {
'consistent-callback-args': {
legacyCallbacks: ['onVisibleChange'],
},
+ 'has-static-classnames': [
+ {
+ props: {
+ secondaryContent: 'Test secondary content',
+ },
+ },
+ ],
},
});
@@ -88,6 +96,107 @@ describe('Tooltip', () => {
expect(target.getAttribute('aria-labelledby')).toBe('the-tooltip-id');
});
+ it('includes secondary content in the accessible label', () => {
+ const result = render(
+
+
+ ,
+ );
+
+ 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 Ctrl+B');
+ expect(target.getAttribute('aria-labelledby')).toBeNull();
+ expect(secondaryContent?.hasAttribute('aria-hidden')).toBe(false);
+ });
+
+ it('includes secondary content in a rich accessible label', () => {
+ const result = render(
+ Bold} secondaryContent="Ctrl+B" relationship="label">
+
+ ,
+ );
+
+ const tooltip = getByRoleTooltip(result);
+ const target = result.getByRole('button');
+ const secondaryContent = tooltip.querySelector('.fui-Tooltip__secondaryContent');
+
+ expect(target.getAttribute('aria-labelledby')).toBe(tooltip.id);
+ expect(secondaryContent?.hasAttribute('aria-hidden')).toBe(false);
+ });
+
+ it('keeps a string label with secondary content when the trigger popup is expanded', () => {
+ const result = render(
+
+
+ ,
+ );
+
+ expect(queryByRoleTooltip(result)).toBeNull();
+ expect(result.getByRole('button').getAttribute('aria-label')).toBe('Bold Ctrl+B');
+ });
+
+ it('keeps a generic secondary content label when the trigger popup is expanded', () => {
+ const result = render(
+ Ctrl+B} relationship="label" visible>
+
+ ,
+ );
+
+ const tooltip = getByRoleTooltip(result);
+ const target = result.getByRole('button');
+
+ expect(target.getAttribute('aria-label')).toBeNull();
+ expect(target.getAttribute('aria-labelledby')).toBe(tooltip.id);
+ expect(tooltip.textContent).toBe('BoldCtrl+B');
+ });
+
+ it('dismisses internal visibility with Escape while the trigger popup is expanded', () => {
+ const onVisibleChange = jest.fn();
+ render(
+
+
+ ,
+ );
+
+ act(() => {
+ fireEvent.keyDown(document, { key: 'Escape' });
+ });
+
+ expect(onVisibleChange).toHaveBeenCalledWith(undefined, expect.objectContaining({ visible: false }));
+ });
+
+ it('keeps the primary string label during SSR with generic secondary content', () => {
+ const html = renderToStaticMarkup(
+
+ Ctrl+B} relationship="label">
+
+
+ ,
+ );
+
+ expect(html).toContain('aria-label="Bold"');
+ expect(html).not.toContain('aria-labelledby');
+ });
+
+ it('includes secondary content in the accessible description', () => {
+ const result = render(
+
+
+ ,
+ );
+
+ 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(
diff --git a/packages/react-components/react-tooltip/library/src/components/Tooltip/Tooltip.types.ts b/packages/react-components/react-tooltip/library/src/components/Tooltip/Tooltip.types.ts
index e3792ae308cbb..8035091749e5c 100644
--- a/packages/react-components/react-tooltip/library/src/components/Tooltip/Tooltip.types.ts
+++ b/packages/react-components/react-tooltip/library/src/components/Tooltip/Tooltip.types.ts
@@ -11,6 +11,11 @@ export type TooltipSlots = {
* The text or JSX content of the tooltip.
*/
content: NonNullable>;
+
+ /**
+ * Secondary content rendered opposite the primary content (e.g. shortcut text).
+ */
+ secondaryContent?: Slot<'span'>;
};
/**
@@ -151,6 +156,16 @@ export type TooltipState = ComponentState &
* 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;
diff --git a/packages/react-components/react-tooltip/library/src/components/Tooltip/renderTooltip.tsx b/packages/react-components/react-tooltip/library/src/components/Tooltip/renderTooltip.tsx
index bc9e4a8a89e87..b4513050dc934 100644
--- a/packages/react-components/react-tooltip/library/src/components/Tooltip/renderTooltip.tsx
+++ b/packages/react-components/react-tooltip/library/src/components/Tooltip/renderTooltip.tsx
@@ -19,7 +19,14 @@ export const renderTooltip_unstable = (state: TooltipBaseState): JSXElement => {
{state.withArrow && }
- {state.content.children}
+ {state.secondaryContent ? (
+
+
{state.content.children}
+
+
+ ) : (
+ state.content.children
+ )}
)}
diff --git a/packages/react-components/react-tooltip/library/src/components/Tooltip/useTooltipBase.tsx b/packages/react-components/react-tooltip/library/src/components/Tooltip/useTooltipBase.tsx
index 998bdc9bbc094..a911a83375708 100644
--- a/packages/react-components/react-tooltip/library/src/components/Tooltip/useTooltipBase.tsx
+++ b/packages/react-components/react-tooltip/library/src/components/Tooltip/useTooltipBase.tsx
@@ -48,6 +48,7 @@ export const useTooltipBase_unstable = (props: TooltipBaseProps): TooltipBaseSta
const {
children,
content,
+ secondaryContent,
withArrow = false,
positioning = 'above',
onVisibleChange,
@@ -57,19 +58,26 @@ export const useTooltipBase_unstable = (props: TooltipBaseProps): TooltipBaseSta
mountNode,
} = props;
+ const child = getTriggerChild(children);
+ const isPopupExpanded =
+ child?.props?.['aria-haspopup'] &&
+ (child?.props?.['aria-expanded'] === true || child?.props?.['aria-expanded'] === 'true');
+ const renderedVisible = visible && !isPopupExpanded;
+
const state: TooltipBaseState = {
withArrow,
positioning,
showDelay,
hideDelay,
relationship,
- visible,
+ visible: renderedVisible,
hidden,
- shouldRenderTooltip: visible,
+ shouldRenderTooltip: renderedVisible,
mountNode,
// Slots
components: {
content: 'div',
+ secondaryContent: 'span',
},
content: slot.always(content, {
defaultProps: {
@@ -77,8 +85,10 @@ export const useTooltipBase_unstable = (props: TooltipBaseProps): TooltipBaseSta
},
elementType: 'div',
}),
+ secondaryContent: slot.optional(secondaryContent, {
+ elementType: 'span',
+ }),
};
-
state.content.id = useId('tooltip-', state.content.id);
const resolvedPositioning = resolvePositioningShorthand(state.positioning);
@@ -272,16 +282,18 @@ export const useTooltipBase_unstable = (props: TooltipBaseProps): TooltipBaseSta
// eslint-disable-next-line react-hooks/immutability, react-hooks/refs
state.content.onBlur = mergeCallbacks(state.content.onBlur, onLeaveTrigger);
- const child = getTriggerChild(children);
-
const triggerAriaProps: Pick = {};
- const isPopupExpanded =
- child?.props?.['aria-haspopup'] &&
- (child?.props?.['aria-expanded'] === true || child?.props?.['aria-expanded'] === 'true');
if (relationship === 'label') {
// aria-label only works if the content is a string. Otherwise, need to use aria-labelledby.
- if (typeof state.content.children === 'string') {
+ if (
+ typeof state.content.children === 'string' &&
+ (!state.secondaryContent || typeof state.secondaryContent.children === 'string')
+ ) {
+ triggerAriaProps['aria-label'] = state.secondaryContent
+ ? `${state.content.children} ${state.secondaryContent.children}`
+ : state.content.children;
+ } else if (isServerSideRender && typeof state.content.children === 'string') {
triggerAriaProps['aria-label'] = state.content.children;
} else {
triggerAriaProps['aria-labelledby'] = state.content.id;
@@ -296,9 +308,8 @@ export const useTooltipBase_unstable = (props: TooltipBaseProps): TooltipBaseSta
state.shouldRenderTooltip = true;
}
- // Case 1: Don't render the Tooltip in SSR to avoid hydration errors
- // Case 2: Don't render the Tooltip, if it triggers Menu or another popup and it's already opened
- if (isServerSideRender || isPopupExpanded) {
+ // Don't render the Tooltip in SSR to avoid hydration errors.
+ if (isServerSideRender) {
// eslint-disable-next-line react-hooks/immutability
state.shouldRenderTooltip = false;
}
diff --git a/packages/react-components/react-tooltip/library/src/components/Tooltip/useTooltipStyles.styles.ts b/packages/react-components/react-tooltip/library/src/components/Tooltip/useTooltipStyles.styles.ts
index bcd26b042bab3..73a951d7b3ab9 100644
--- a/packages/react-components/react-tooltip/library/src/components/Tooltip/useTooltipStyles.styles.ts
+++ b/packages/react-components/react-tooltip/library/src/components/Tooltip/useTooltipStyles.styles.ts
@@ -9,6 +9,7 @@ import type { SlotClassNames } from '@fluentui/react-utilities';
export const tooltipClassNames: SlotClassNames = {
content: 'fui-Tooltip__content',
+ secondaryContent: 'fui-Tooltip__secondaryContent',
};
/**
@@ -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,
+ },
});
/**
@@ -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;
};
diff --git a/packages/react-components/react-tooltip/stories/src/Tooltip/TooltipAccessibility.md b/packages/react-components/react-tooltip/stories/src/Tooltip/TooltipAccessibility.md
index 45019a84d6aa1..ca46699e1136d 100644
--- a/packages/react-components/react-tooltip/stories/src/Tooltip/TooltipAccessibility.md
+++ b/packages/react-components/react-tooltip/stories/src/Tooltip/TooltipAccessibility.md
@@ -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 displayed opposite the primary content, such as a keyboard shortcut. Secondary content is included in the trigger's accessible label or description.
diff --git a/packages/react-components/react-tooltip/stories/src/Tooltip/TooltipSecondaryContent.stories.tsx b/packages/react-components/react-tooltip/stories/src/Tooltip/TooltipSecondaryContent.stories.tsx
new file mode 100644
index 0000000000000..7c1dcc3309eee
--- /dev/null
+++ b/packages/react-components/react-tooltip/stories/src/Tooltip/TooltipSecondaryContent.stories.tsx
@@ -0,0 +1,19 @@
+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 => (
+
+ } />
+
+);
+
+SecondaryContent.parameters = {
+ docs: {
+ description: {
+ story:
+ 'Use `secondaryContent` for short supporting information displayed opposite the primary content, such as a keyboard shortcut.',
+ },
+ },
+};
diff --git a/packages/react-components/react-tooltip/stories/src/Tooltip/index.stories.tsx b/packages/react-components/react-tooltip/stories/src/Tooltip/index.stories.tsx
index a055ba3993f54..26685c2639e12 100644
--- a/packages/react-components/react-tooltip/stories/src/Tooltip/index.stories.tsx
+++ b/packages/react-components/react-tooltip/stories/src/Tooltip/index.stories.tsx
@@ -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';