Skip to content

Commit 7d92510

Browse files
committed
switch to popover hint
1 parent 9a30810 commit 7d92510

4 files changed

Lines changed: 251 additions & 63 deletions

File tree

packages/react-components/react-headless-components-preview/library/src/components/Provider/renderProvider.tsx

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,7 @@
33

44
import { assertSlots } from '@fluentui/react-utilities';
55
import type { JSXElement } from '@fluentui/react-utilities';
6-
import {
7-
Provider_unstable as Provider,
8-
TooltipVisibilityProvider_unstable as TooltipVisibilityProvider,
9-
} from '@fluentui/react-shared-contexts';
6+
import { Provider_unstable as Provider } from '@fluentui/react-shared-contexts';
107
import type { FluentProviderContextValues, FluentProviderSlots } from '@fluentui/react-provider';
118
import type { ProviderState } from './Provider.types';
129

@@ -18,9 +15,7 @@ export const renderProvider = (state: ProviderState, contextValues: FluentProvid
1815

1916
return (
2017
<Provider value={contextValues.provider}>
21-
<TooltipVisibilityProvider value={contextValues.tooltip}>
22-
<state.root />
23-
</TooltipVisibilityProvider>
18+
<state.root />
2419
</Provider>
2520
);
2621
};
Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
import * as React from 'react';
2+
import { mount as mountBase } from '@fluentui/scripts-cypress';
3+
import { Tooltip } from './Tooltip';
4+
import type { TooltipProps } from './Tooltip.types';
5+
import type { JSXElement } from '@fluentui/react-utilities';
6+
7+
const mount = (element: JSXElement) => mountBase(element);
8+
9+
const tooltipSelector = '[role="tooltip"]';
10+
11+
const Example = (props: Partial<TooltipProps>) => (
12+
<Tooltip content="Tooltip content" relationship="description" {...props}>
13+
<button>Trigger</button>
14+
</Tooltip>
15+
);
16+
17+
describe('Tooltip', () => {
18+
describe('visibility', () => {
19+
it('should be hidden by default', () => {
20+
mount(<Example />);
21+
cy.get(tooltipSelector).should('not.be.visible');
22+
});
23+
24+
it('should show after pointer enters trigger', () => {
25+
mount(<Example />);
26+
cy.get('button').trigger('pointerover');
27+
cy.get(tooltipSelector).should('be.visible');
28+
});
29+
30+
it('should hide after pointer leaves trigger', () => {
31+
mount(<Example />);
32+
cy.get('button').trigger('pointerover');
33+
cy.get(tooltipSelector).should('be.visible');
34+
cy.get('button').trigger('pointerout', { force: true });
35+
cy.get(tooltipSelector).should('not.be.visible');
36+
});
37+
38+
it('should remain visible when pointer moves to tooltip content', () => {
39+
cy.clock();
40+
mount(<Example showDelay={0} hideDelay={300} />);
41+
cy.get('button').trigger('pointerover');
42+
cy.tick(10);
43+
cy.get('button').trigger('pointerout', { force: true });
44+
cy.get(tooltipSelector).trigger('pointerover');
45+
cy.tick(500); // Well past hideDelay; timer was cancelled
46+
cy.get(tooltipSelector).should('be.visible');
47+
});
48+
49+
it('should hide after pointer leaves tooltip content', () => {
50+
mount(<Example />);
51+
cy.get('button').trigger('pointerover');
52+
cy.get(tooltipSelector).should('be.visible');
53+
cy.get(tooltipSelector).trigger('pointerout');
54+
cy.get(tooltipSelector).should('not.be.visible');
55+
});
56+
});
57+
58+
describe('keyboard and focus', () => {
59+
it('should show on trigger focus', () => {
60+
mount(<Example />);
61+
cy.get('button').focus();
62+
cy.get(tooltipSelector).should('be.visible');
63+
});
64+
65+
it('should hide immediately on trigger blur', () => {
66+
mount(<Example />);
67+
cy.get('button').focus();
68+
cy.get(tooltipSelector).should('be.visible');
69+
cy.get('button').blur();
70+
cy.get(tooltipSelector).should('not.be.visible');
71+
});
72+
73+
it('should hide when browser dismisses the popover', () => {
74+
mount(<Example />);
75+
cy.get('button').trigger('pointerover');
76+
cy.get(tooltipSelector).should('be.visible');
77+
cy.get(tooltipSelector).then($tooltip => {
78+
($tooltip[0] as HTMLElement).hidePopover();
79+
});
80+
cy.get(tooltipSelector).should('not.be.visible');
81+
});
82+
});
83+
84+
describe('show and hide delays', () => {
85+
it('should not show before showDelay has elapsed', () => {
86+
cy.clock();
87+
mount(<Example showDelay={400} />);
88+
cy.get('button').trigger('pointerover');
89+
cy.tick(200);
90+
cy.get(tooltipSelector).should('not.be.visible');
91+
cy.tick(200);
92+
cy.get(tooltipSelector).should('be.visible');
93+
});
94+
95+
it('should not hide before hideDelay has elapsed', () => {
96+
cy.clock();
97+
mount(<Example showDelay={0} hideDelay={400} />);
98+
cy.get('button').trigger('pointerover');
99+
cy.tick(10);
100+
cy.get('button').trigger('pointerout', { force: true });
101+
cy.tick(200);
102+
cy.get(tooltipSelector).should('be.visible');
103+
cy.tick(200);
104+
cy.get(tooltipSelector).should('not.be.visible');
105+
});
106+
107+
it('should cancel the hide timer when pointer re-enters trigger', () => {
108+
cy.clock();
109+
mount(<Example showDelay={0} hideDelay={300} />);
110+
cy.get('button').trigger('pointerover');
111+
cy.tick(10);
112+
cy.get('button').trigger('pointerout', { force: true });
113+
cy.tick(150); // Partway through hideDelay
114+
cy.get('button').trigger('pointerover', { force: true }); // Re-enter — cancels hide timer
115+
cy.tick(500);
116+
cy.get(tooltipSelector).should('be.visible');
117+
});
118+
});
119+
120+
describe('controlled', () => {
121+
const ControlledExample = () => {
122+
const [visible, setVisible] = React.useState(false);
123+
return (
124+
<>
125+
<Tooltip
126+
content="Controlled tooltip"
127+
relationship="description"
128+
visible={visible}
129+
onVisibleChange={(_, data) => setVisible(data.visible)}
130+
>
131+
<button id="trigger">Trigger</button>
132+
</Tooltip>
133+
<button id="toggle" onClick={() => setVisible(v => !v)}>
134+
Toggle
135+
</button>
136+
</>
137+
);
138+
};
139+
140+
it('should show when visible is set to true', () => {
141+
mount(<ControlledExample />);
142+
cy.get('#toggle').click();
143+
cy.get(tooltipSelector).should('be.visible');
144+
});
145+
146+
it('should hide when visible is set to false', () => {
147+
mount(<ControlledExample />);
148+
cy.get('#toggle').click();
149+
cy.get(tooltipSelector).should('be.visible');
150+
cy.get('#toggle').click({ force: true });
151+
cy.get(tooltipSelector).should('not.be.visible');
152+
});
153+
154+
it('should call onVisibleChange with visible=true when shown', () => {
155+
const onVisibleChange = cy.stub().as('onVisibleChange');
156+
mount(<Example onVisibleChange={onVisibleChange} />);
157+
cy.get('button').trigger('pointerover');
158+
cy.get(tooltipSelector).should('be.visible');
159+
cy.get('@onVisibleChange').should(
160+
'have.been.calledWith',
161+
Cypress.sinon.match.any,
162+
Cypress.sinon.match({ visible: true }),
163+
);
164+
});
165+
166+
it('should call onVisibleChange with visible=false when hidden', () => {
167+
const onVisibleChange = cy.stub().as('onVisibleChange');
168+
mount(<Example onVisibleChange={onVisibleChange} />);
169+
cy.get('button').trigger('pointerover');
170+
cy.get(tooltipSelector).should('be.visible');
171+
cy.get('button').trigger('pointerout', { force: true });
172+
cy.get(tooltipSelector).should('not.be.visible');
173+
cy.get('@onVisibleChange').should(
174+
'have.been.calledWith',
175+
Cypress.sinon.match.any,
176+
Cypress.sinon.match({ visible: false }),
177+
);
178+
});
179+
});
180+
181+
describe('aria relationship', () => {
182+
it('should set aria-label when relationship="label" with string content', () => {
183+
mount(
184+
<Tooltip content="Label text" relationship="label">
185+
<button>Trigger</button>
186+
</Tooltip>,
187+
);
188+
cy.get('button').should('have.attr', 'aria-label', 'Label text');
189+
// Not rendered since aria-label is sufficient
190+
cy.get(tooltipSelector).should('not.exist');
191+
});
192+
193+
it('should set aria-labelledby when relationship="label" with non-string content', () => {
194+
mount(
195+
<Tooltip content={<span>Complex label</span>} relationship="label">
196+
<button>Trigger</button>
197+
</Tooltip>,
198+
);
199+
cy.get('button')
200+
.invoke('attr', 'aria-labelledby')
201+
.then(id => {
202+
cy.get(`#${id}`).should('exist');
203+
});
204+
});
205+
206+
it('should set aria-describedby when relationship="description"', () => {
207+
mount(
208+
<Tooltip content="Description text" relationship="description">
209+
<button>Trigger</button>
210+
</Tooltip>,
211+
);
212+
cy.get('button')
213+
.invoke('attr', 'aria-describedby')
214+
.then(id => {
215+
cy.get(`#${id}`).should('exist');
216+
});
217+
});
218+
219+
it('should always render tooltip for aria-describedby even when hidden', () => {
220+
mount(
221+
<Tooltip content="Always here" relationship="description">
222+
<button>Trigger</button>
223+
</Tooltip>,
224+
);
225+
cy.get(tooltipSelector).should('exist');
226+
});
227+
});
228+
});

packages/react-components/react-headless-components-preview/library/src/components/Tooltip/Tooltip.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ describe('Tooltip', () => {
5050
);
5151

5252
expect(screen.getByLabelText('Default Tooltip')).toBeInTheDocument();
53-
expect(screen.getByRole('tooltip')).toHaveAttribute('popover', 'manual');
53+
expect(screen.getByRole('tooltip')).toHaveAttribute('popover', 'hint');
5454
});
5555

5656
it('renders only aria-label for a simple label tooltip', () => {

0 commit comments

Comments
 (0)