From 2a614afa2243b746dabe8cba2dc913bc3f116ccd Mon Sep 17 00:00:00 2001 From: Jay Goss Date: Thu, 17 Sep 2026 16:24:58 -0500 Subject: [PATCH] fix(integrations): Keep disabled integration buttons reachable by keyboard AddIntegrationButton and DirectEnableButton were natively disabled when the provider cannot be added or the user lacks access, so keyboard and screen reader users could not reach the tooltip that explains why. Both buttons are now aria-disabled with the tooltip on the button itself, and the click handler bails while disabled. --- .../addIntegrationButton.spec.tsx | 29 ++++++++- .../addIntegrationButton.tsx | 65 +++++++++++-------- .../directEnableButton.spec.tsx | 17 +++-- .../directEnableButton.tsx | 33 ++++++---- 4 files changed, 98 insertions(+), 46 deletions(-) diff --git a/static/app/views/settings/organizationIntegrations/addIntegrationButton.spec.tsx b/static/app/views/settings/organizationIntegrations/addIntegrationButton.spec.tsx index f40c74a190d5..7d27acdf8e26 100644 --- a/static/app/views/settings/organizationIntegrations/addIntegrationButton.spec.tsx +++ b/static/app/views/settings/organizationIntegrations/addIntegrationButton.spec.tsx @@ -1,7 +1,7 @@ import {IntegrationProviderFixture} from 'sentry-fixture/integrationProvider'; import {OrganizationFixture} from 'sentry-fixture/organization'; -import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary'; +import {act, render, screen, userEvent} from 'sentry-test/reactTestingLibrary'; import * as pipelineModal from 'sentry/components/pipeline/modal'; import {AddIntegrationButton} from 'sentry/views/settings/organizationIntegrations/addIntegrationButton'; @@ -36,4 +36,31 @@ describe('AddIntegrationButton', () => { }) ); }); + + it('keeps the button focusable with a tooltip when the provider cannot be added', async () => { + const openPipelineModalSpy = jest + .spyOn(pipelineModal, 'openPipelineModal') + .mockImplementation(() => {}); + + render( + + ); + + const button = screen.getByRole('button', {name: 'Add integration'}); + expect(button).toHaveAttribute('aria-disabled', 'true'); + + act(() => button.focus()); + expect( + await screen.findByText( + `Integration cannot be added on Sentry. Enable this integration via the ${provider.name} instance.` + ) + ).toBeInTheDocument(); + + await userEvent.click(button); + expect(openPipelineModalSpy).not.toHaveBeenCalled(); + }); }); diff --git a/static/app/views/settings/organizationIntegrations/addIntegrationButton.tsx b/static/app/views/settings/organizationIntegrations/addIntegrationButton.tsx index f6a4b70a76e5..c9a0905e91f4 100644 --- a/static/app/views/settings/organizationIntegrations/addIntegrationButton.tsx +++ b/static/app/views/settings/organizationIntegrations/addIntegrationButton.tsx @@ -1,6 +1,5 @@ import type {ButtonProps} from '@sentry/scraps/button'; import {Button} from '@sentry/scraps/button'; -import {Tooltip} from '@sentry/scraps/tooltip'; import {t} from 'sentry/locale'; import type {IntegrationWithConfig} from 'sentry/types/integrations'; @@ -55,34 +54,44 @@ export function AddIntegrationButton({ }); return ( - - - + } + startFlow({ + provider, + organization, + onInstall: onAddIntegration, + analyticsParams, + suppressSuccessMessage, + onCancel, + onError, + }); + }} + > + {label} + ); } diff --git a/static/app/views/settings/organizationIntegrations/directEnableButton.spec.tsx b/static/app/views/settings/organizationIntegrations/directEnableButton.spec.tsx index cf6b89e08779..e131ecef3759 100644 --- a/static/app/views/settings/organizationIntegrations/directEnableButton.spec.tsx +++ b/static/app/views/settings/organizationIntegrations/directEnableButton.spec.tsx @@ -1,6 +1,6 @@ import {OrganizationFixture} from 'sentry-fixture/organization'; -import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary'; +import {act, render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary'; import {DirectEnableButton} from 'sentry/views/settings/organizationIntegrations/directEnableButton'; @@ -42,8 +42,8 @@ describe('DirectEnableButton', () => { await waitFor(() => expect(mockPost).toHaveBeenCalledTimes(1)); }); - it('disables button when user does not have access', () => { - MockApiClient.addMockResponse({ + it('keeps the button focusable with a tooltip when user does not have access', async () => { + const mockPost = MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/integrations/direct-enable/github_copilot/`, method: 'POST', body: {}, @@ -53,6 +53,15 @@ describe('DirectEnableButton', () => { organization, }); - expect(screen.getByRole('button', {name: 'Enable Integration'})).toBeDisabled(); + const button = screen.getByRole('button', {name: 'Enable Integration'}); + expect(button).toHaveAttribute('aria-disabled', 'true'); + + act(() => button.focus()); + expect( + await screen.findByText('You do not have permission to enable this integration.') + ).toBeInTheDocument(); + + await userEvent.click(button); + expect(mockPost).not.toHaveBeenCalled(); }); }); diff --git a/static/app/views/settings/organizationIntegrations/directEnableButton.tsx b/static/app/views/settings/organizationIntegrations/directEnableButton.tsx index b95a61cbf060..7c2f54022fc2 100644 --- a/static/app/views/settings/organizationIntegrations/directEnableButton.tsx +++ b/static/app/views/settings/organizationIntegrations/directEnableButton.tsx @@ -1,7 +1,6 @@ import {useMutation, useQueryClient} from '@tanstack/react-query'; import {Button} from '@sentry/scraps/button'; -import {Tooltip} from '@sentry/scraps/tooltip'; import {addErrorMessage} from 'sentry/actionCreators/indicator'; import {t} from 'sentry/locale'; @@ -58,18 +57,26 @@ export function DirectEnableButton({ }); return ( - { + if (!userHasAccess) { + return; + } + enable(); + }} > - - + {t('Enable Integration')} + ); }