Skip to content

Commit 20bf77e

Browse files
priscilawebdevcodex
andcommitted
fix(issues): Add tooltips to icon-only feed actions
Restore tooltip affordances for the issue feed auto-refresh control and align the adjacent issue view icon actions with the same interaction pattern. Also teach icon-only feedback buttons to expose the same hover affordance without changing them into text buttons, so page-frame issue surfaces stay discoverable while remaining compact. Fixes DE-1104 Co-Authored-By: OpenAI Codex <noreply@openai.com>
1 parent 484df9d commit 20bf77e

File tree

6 files changed

+125
-25
lines changed

6 files changed

+125
-25
lines changed
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
2+
3+
import {FeedbackButton} from 'sentry/components/feedbackButton/feedbackButton';
4+
5+
jest.mock('sentry/utils/useFeedbackForm', () => ({
6+
useFeedbackForm: () => jest.fn(),
7+
}));
8+
9+
describe('FeedbackButton', () => {
10+
it('adds a tooltip for icon-only feedback buttons', async () => {
11+
render(<FeedbackButton>{null}</FeedbackButton>);
12+
13+
const button = screen.getByRole('button', {name: 'Give Feedback'});
14+
15+
await userEvent.hover(button);
16+
17+
expect(screen.getByText('Give Feedback')).toBeInTheDocument();
18+
});
19+
20+
it('does not add a tooltip for text feedback buttons', async () => {
21+
render(<FeedbackButton />);
22+
23+
const button = screen.getByRole('button', {name: 'Give Feedback'});
24+
25+
await userEvent.hover(button);
26+
27+
expect(button).not.toHaveAttribute('aria-describedby');
28+
});
29+
30+
it('uses the custom aria-label for icon-only tooltip text', async () => {
31+
render(<FeedbackButton aria-label="Give feedback on issues">{null}</FeedbackButton>);
32+
33+
const button = screen.getByRole('button', {name: 'Give feedback on issues'});
34+
35+
await userEvent.hover(button);
36+
37+
expect(screen.getByText('Give feedback on issues')).toBeInTheDocument();
38+
});
39+
});

static/app/components/feedbackButton/feedbackButton.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,14 +60,23 @@ export function FeedbackButton({feedbackOptions, ...buttonProps}: Props) {
6060
return null;
6161
}
6262

63-
const children = 'children' in buttonProps ? buttonProps.children : t('Give Feedback');
63+
const defaultLabel = t('Give Feedback');
64+
const children = 'children' in buttonProps ? buttonProps.children : defaultLabel;
65+
const isIconOnly = children === null || children === undefined || children === false;
66+
const ariaLabel = buttonProps['aria-label'] ?? (isIconOnly ? defaultLabel : undefined);
67+
const tooltipProps =
68+
isIconOnly && !buttonProps.tooltipProps?.title
69+
? {...buttonProps.tooltipProps, title: ariaLabel}
70+
: buttonProps.tooltipProps;
6471

6572
return (
6673
<Button
6774
ref={buttonRef}
6875
size="sm"
6976
icon={<IconMegaphone />}
7077
{...buttonProps}
78+
tooltipProps={tooltipProps}
79+
aria-label={ariaLabel}
7180
onClick={e => {
7281
openForm?.(feedbackOptions);
7382
buttonProps.onClick?.(e);

static/app/views/issueList/issueViews/issueViewSaveButton.spec.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,21 @@ describe('IssueViewSaveButton', () => {
325325
expect(screen.getByTestId('save-button')).toBeInTheDocument();
326326
});
327327

328+
it('renders a tooltip for the save options trigger', async () => {
329+
render(<IssueViewSaveButton {...defaultProps} />, {
330+
initialRouterConfig: initialRouterConfigView,
331+
organization,
332+
});
333+
334+
const moreSaveOptionsButton = await screen.findByRole('button', {
335+
name: 'More save options',
336+
});
337+
338+
await userEvent.hover(moreSaveOptionsButton);
339+
340+
expect(screen.getByText('More save options')).toBeInTheDocument();
341+
});
342+
328343
it('shows a feature disabled hovercard when the feature is disabled', async () => {
329344
render(<IssueViewSaveButton {...defaultProps} />, {
330345
organization: OrganizationFixture({

static/app/views/issueList/issueViews/issueViewSaveButton.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ function SegmentedIssueViewSaveButton({
132132
<DropdownTrigger
133133
{...props}
134134
disabled={!hasFeature || isSaving}
135+
tooltipProps={{title: t('More save options')}}
135136
icon={
136137
<IconChevron
137138
direction="down"

static/app/views/issueList/issueViewsHeader.spec.tsx

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,47 @@ describe('IssueViewsHeader', () => {
5151
route: '/organizations/:orgId/issues/',
5252
};
5353

54+
describe('realtime button', () => {
55+
it('renders a tooltip on the issue feed', async () => {
56+
render(<IssueViewsHeader {...defaultProps} />, {
57+
organization,
58+
59+
initialRouterConfig: onIssueFeedRouterConfig,
60+
});
61+
62+
const realtimeButton = await screen.findByRole('button', {
63+
name: 'Enable real-time updates',
64+
});
65+
66+
await userEvent.hover(realtimeButton);
67+
68+
expect(screen.getByText('Enable real-time updates')).toBeInTheDocument();
69+
});
70+
71+
it('renders tooltips for the issue view icon buttons', async () => {
72+
render(<IssueViewsHeader {...defaultProps} />, {
73+
organization,
74+
75+
initialRouterConfig: onIssueViewRouterConfig,
76+
});
77+
78+
const starButton = await screen.findByRole('button', {name: 'Star view'});
79+
await userEvent.hover(starButton);
80+
expect(screen.getByText('Star view')).toBeInTheDocument();
81+
82+
await userEvent.unhover(starButton);
83+
await waitFor(() => {
84+
expect(screen.queryByText('Star view')).not.toBeInTheDocument();
85+
});
86+
87+
const moreOptionsButton = await screen.findByRole('button', {
88+
name: 'More issue view options',
89+
});
90+
await userEvent.hover(moreOptionsButton);
91+
expect(screen.getByText('More issue view options')).toBeInTheDocument();
92+
});
93+
});
94+
5495
describe('edit menu', () => {
5596
it('does not render if not on a view', async () => {
5697
render(<IssueViewsHeader {...defaultProps} />, {

static/app/views/issueList/issueViewsHeader.tsx

Lines changed: 19 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ function IssueViewStarButton() {
7171
const queryClient = useQueryClient();
7272

7373
const {data: groupSearchView} = useSelectedGroupSearchView();
74+
const starViewLabel = groupSearchView?.starred ? t('Unstar view') : t('Star view');
7475
const {mutate: mutateViewStarred} = useUpdateGroupSearchViewStarred({
7576
onMutate: variables => {
7677
setApiQueryData<GroupSearchView>(
@@ -128,7 +129,8 @@ function IssueViewStarButton() {
128129
}
129130
);
130131
}}
131-
aria-label={groupSearchView?.starred ? t('Unstar view') : t('Star view')}
132+
tooltipProps={{title: starViewLabel}}
133+
aria-label={starViewLabel}
132134
icon={
133135
<IconStar
134136
isSolid={groupSearchView?.starred}
@@ -146,6 +148,7 @@ function IssueViewEditMenu() {
146148
const user = useUser();
147149
const {mutateAsync: deleteIssueView} = useDeleteGroupSearchView();
148150
const navigate = useNavigate();
151+
const moreOptionsLabel = t('More issue view options');
149152

150153
if (!groupSearchView) {
151154
return null;
@@ -194,8 +197,9 @@ function IssueViewEditMenu() {
194197
<Button
195198
size="sm"
196199
{...props}
200+
tooltipProps={{title: moreOptionsLabel}}
197201
icon={<IconEllipsis />}
198-
aria-label={t('More issue view options')}
202+
aria-label={moreOptionsLabel}
199203
/>
200204
)}
201205
position="bottom-end"
@@ -217,6 +221,17 @@ export function IssueViewsHeader({
217221
const realtimeLabel = realtimeActive
218222
? t('Pause real-time updates')
219223
: t('Enable real-time updates');
224+
const realtimeButton = viewId ? null : (
225+
<DisableInDemoMode>
226+
<Button
227+
size="sm"
228+
tooltipProps={{title: realtimeLabel}}
229+
aria-label={realtimeLabel}
230+
icon={realtimeActive ? <IconPause /> : <IconPlay />}
231+
onClick={() => onRealtimeChange(!realtimeActive)}
232+
/>
233+
</DisableInDemoMode>
234+
);
220235

221236
if (hasPageFrameFeature) {
222237
return (
@@ -226,17 +241,7 @@ export function IssueViewsHeader({
226241
</Layout.HeaderContent>
227242
<TopBar.Slot name="actions">
228243
{headerActions}
229-
{!viewId && (
230-
<DisableInDemoMode>
231-
<Button
232-
size="sm"
233-
tooltipProps={{title: realtimeLabel}}
234-
aria-label={realtimeLabel}
235-
icon={realtimeActive ? <IconPause /> : <IconPlay />}
236-
onClick={() => onRealtimeChange(!realtimeActive)}
237-
/>
238-
</DisableInDemoMode>
239-
)}
244+
{realtimeButton}
240245
<IssueViewStarButton />
241246
<IssueViewEditMenu />
242247
</TopBar.Slot>
@@ -251,17 +256,7 @@ export function IssueViewsHeader({
251256
<PageTitle title={title} description={description} />
252257
<Flex align="center" gap="md">
253258
{headerActions}
254-
{!viewId && (
255-
<DisableInDemoMode>
256-
<Button
257-
size="sm"
258-
tooltipProps={{title: realtimeLabel}}
259-
aria-label={realtimeLabel}
260-
icon={realtimeActive ? <IconPause /> : <IconPlay />}
261-
onClick={() => onRealtimeChange(!realtimeActive)}
262-
/>
263-
</DisableInDemoMode>
264-
)}
259+
{realtimeButton}
265260
<IssueViewStarButton />
266261
<IssueViewEditMenu />
267262
</Flex>

0 commit comments

Comments
 (0)