diff --git a/static/app/components/badge/groupPriority.tsx b/static/app/components/badge/groupPriority.tsx
index b57ce7809be134..00622e02a288de 100644
--- a/static/app/components/badge/groupPriority.tsx
+++ b/static/app/components/badge/groupPriority.tsx
@@ -247,19 +247,6 @@ export function GroupPriorityDropdown({
);
}
-const DropdownButton = styled(Button)`
- font-weight: ${p => p.theme.font.weight.sans.regular};
- border: none;
- padding: 0;
- height: unset;
- border-radius: 20px;
- box-shadow: none;
-
- > span > div {
- border-radius: 20px;
- }
-`;
-
const StyledTag = styled(Tag)`
gap: ${p => p.theme.space['2xs']};
position: relative;
@@ -267,6 +254,15 @@ const StyledTag = styled(Tag)`
overflow: hidden;
`;
+const DropdownButton = styled(Button)`
+ padding: 0;
+ border-radius: ${p => p.theme.radius.full};
+
+ ${StyledTag} {
+ border-radius: ${p => p.theme.radius.full};
+ }
+`;
+
const InlinePlaceholder = styled(Placeholder)`
display: inline-block;
vertical-align: middle;
diff --git a/static/app/components/core/button/button.spec.tsx b/static/app/components/core/button/button.spec.tsx
index fb99d7c1d8c592..1ed0e958195088 100644
--- a/static/app/components/core/button/button.spec.tsx
+++ b/static/app/components/core/button/button.spec.tsx
@@ -26,6 +26,37 @@ describe('Button', () => {
expect(spy).not.toHaveBeenCalled();
});
+
+ it('does not call `onClick` on busy buttons', async () => {
+ const spy = jest.fn();
+ render(
+
+ );
+ await userEvent.click(screen.getByText('Click me'));
+
+ expect(spy).not.toHaveBeenCalled();
+ });
+
+ it('shows spinner when busy', () => {
+ render();
+
+ const button = screen.getByRole('button', {name: 'Busy Button'});
+ expect(button).toHaveAttribute('aria-busy', 'true');
+ const spinner = button.querySelector('[aria-hidden="true"]');
+ expect(spinner).toBeInTheDocument();
+ });
+
+ it('hides spinner when not busy', () => {
+ render();
+
+ const button = screen.getByRole('button', {name: 'Normal Button'});
+ expect(button).not.toHaveAttribute('aria-busy');
+
+ const spinner = button.querySelector('[aria-hidden="true"]');
+ expect(spinner).not.toBeInTheDocument();
+ });
});
describe('LinkButton', () => {
diff --git a/static/app/components/core/button/button.tsx b/static/app/components/core/button/button.tsx
index b84cf750c79b20..5ece056c82a57a 100644
--- a/static/app/components/core/button/button.tsx
+++ b/static/app/components/core/button/button.tsx
@@ -1,3 +1,4 @@
+import {keyframes} from '@emotion/react';
import styled from '@emotion/styled';
import {Flex} from '@sentry/scraps/layout';
@@ -55,6 +56,7 @@ export function Button({
minWidth="0"
height="100%"
whiteSpace="nowrap"
+ visibility={busy ? 'hidden' : undefined}
>
{props.icon && (
)}
{props.children}
+ {busy && (
+
+ {({className}) => }
+
+ )}
@@ -80,3 +93,22 @@ export function Button({
const StyledButton = styled('button')`
${p => getButtonStyles(p as any)}
`;
+
+const spin = keyframes`
+ to {
+ transform: rotate(360deg);
+ }
+`;
+
+const BusySpinner = styled('span')`
+ &::after {
+ content: '';
+ display: block;
+ width: 1em;
+ height: 1em;
+ border-radius: 50%;
+ border: 2px solid currentColor;
+ border-top-color: transparent;
+ animation: ${spin} 0.6s linear infinite;
+ }
+`;
diff --git a/static/app/components/core/layout/container.tsx b/static/app/components/core/layout/container.tsx
index ac395b2476a0f4..3aaf34a011b475 100644
--- a/static/app/components/core/layout/container.tsx
+++ b/static/app/components/core/layout/container.tsx
@@ -86,6 +86,8 @@ interface ContainerLayoutProps {
alignSelf?: Responsive;
justifySelf?: Responsive;
+ visibility?: Responsive<'visible' | 'hidden' | 'collapse'>;
+
// Text Wrapping
whiteSpace?: Responsive<
'break-spaces' | 'normal' | 'nowrap' | 'pre' | 'pre-line' | 'pre-wrap'
@@ -225,6 +227,7 @@ const omitContainerProps = new Set([
'right',
'row',
'top',
+ 'visibility',
'width',
'whiteSpace',
]);
@@ -314,6 +317,7 @@ export const Container = styled(
${p => rc('border-left', p.borderLeft, p.theme, getBorder)};
${p => rc('border-right', p.borderRight, p.theme, getBorder)};
+ ${p => rc('visibility', p.visibility, p.theme)};
${p => rc('white-space', p.whiteSpace, p.theme)};
/**