From 11a6b61462ffd7a9b6b1090d155e4be710dff783 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rcio?= Date: Thu, 2 Jul 2026 10:26:53 +0200 Subject: [PATCH 1/4] ECHOES-1409 Update spinner default styling --- .../spinner/SpinnerOverrideColor.tsx | 17 +------- src/components/spinner/SpinnerStyles.tsx | 21 ++++------ .../spinner/__tests__/Spinner-test.tsx | 41 +++++++++++++++++++ 3 files changed, 51 insertions(+), 28 deletions(-) diff --git a/src/components/spinner/SpinnerOverrideColor.tsx b/src/components/spinner/SpinnerOverrideColor.tsx index 680a56c66..4bfaf6056 100644 --- a/src/components/spinner/SpinnerOverrideColor.tsx +++ b/src/components/spinner/SpinnerOverrideColor.tsx @@ -19,22 +19,9 @@ */ import styled from '@emotion/styled'; import { Spinner } from './Spinner'; - -import { cssVar } from '~utils/design-tokens'; +import { SPINNER_DEFAULT_COLOR } from './SpinnerStyles'; export const SpinnerOverrideColor = styled(Spinner)` - --spinner-background: - linear-gradient( - 0deg, - var(--spinner-color-override, ${cssVar('color-background-accent-default')}) 50%, - transparent 50% 100% - ) - border-box, - linear-gradient( - 90deg, - var(--spinner-color-override, ${cssVar('color-background-accent-default')}) 25%, - transparent 75% 100% - ) - border-box; + --spinner-color: var(--spinner-color-override, ${SPINNER_DEFAULT_COLOR}); `; SpinnerOverrideColor.displayName = 'SpinnerOverrideColor'; diff --git a/src/components/spinner/SpinnerStyles.tsx b/src/components/spinner/SpinnerStyles.tsx index 15b2e1ba5..54bef4d07 100644 --- a/src/components/spinner/SpinnerStyles.tsx +++ b/src/components/spinner/SpinnerStyles.tsx @@ -68,20 +68,15 @@ const spinAnimation = keyframes` } `; +export const SPINNER_DEFAULT_COLOR = cssVar('color-surface-inverse-default'); + +const spinnerBorderWidth = '2px'; +const spinnerColor = `var(--spinner-color, ${SPINNER_DEFAULT_COLOR})`; + export const SpinnerStyled = styled.span<{ inline: boolean }>` - border: 2px solid transparent; - background: var( - --spinner-background, - linear-gradient(0deg, ${cssVar('color-background-accent-default')} 50%, transparent 50% 100%) - border-box, - linear-gradient(90deg, ${cssVar('color-background-accent-default')} 25%, transparent 75% 100%) - border-box - ); - mask: - linear-gradient(#fff 0 0) padding-box, - linear-gradient(#fff 0 0); - -webkit-mask-composite: xor; - mask-composite: exclude; + position: relative; + border: ${spinnerBorderWidth} solid ${cssVar('color-border-weak')}; + border-top-color: ${spinnerColor}; animation: ${spinAnimation} 1s infinite linear; display: ${displaySwitcher}; diff --git a/src/components/spinner/__tests__/Spinner-test.tsx b/src/components/spinner/__tests__/Spinner-test.tsx index ef9c3ad0f..e73e5761c 100644 --- a/src/components/spinner/__tests__/Spinner-test.tsx +++ b/src/components/spinner/__tests__/Spinner-test.tsx @@ -18,11 +18,18 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { matchers } from '@emotion/jest'; +import styled from '@emotion/styled'; import { screen } from '@testing-library/react'; import { ComponentProps } from 'react'; import { render } from '~common/helpers/test-utils'; import { Tooltip } from '../../tooltip'; import { Spinner } from '../Spinner'; +import { SpinnerOverrideColor } from '../SpinnerOverrideColor'; + +import { cssVar } from '~utils/design-tokens'; + +expect.extend(matchers); it('can be controlled by the isLoading prop', async () => { const { container, rerender } = setupSpinner({ isLoading: true }); @@ -84,6 +91,29 @@ it('should correctly support tooltips', async () => { expect(screen.getByRole('tooltip', { name: 'my tooltip' })).toBeInTheDocument(); }); +it('uses the inverse color as the default solid arc with a fixed weak track', () => { + const { container } = setupSpinner({ isLoading: true }); + const spinnerIcon = getSpinnerIcon(container); + + expect(spinnerIcon).toHaveStyleRule('border', `2px solid ${cssVar('color-border-weak')}`); + expect(spinnerIcon).toHaveStyleRule( + 'border-top-color', + `var(--spinner-color, ${cssVar('color-surface-inverse-default')})`, + ); +}); + +it('allows overriding the arc color without overriding the weak track', () => { + const { container } = render(); + const spinnerIcon = getSpinnerIcon(container); + + expect(screen.getByRole('status')).toHaveStyleRule('--spinner-color-override', 'hotpink'); + expect(screen.getByRole('status')).toHaveStyleRule( + '--spinner-color', + `var(--spinner-color-override, ${cssVar('color-surface-inverse-default')})`, + ); + expect(spinnerIcon).toHaveStyleRule('border', `2px solid ${cssVar('color-border-weak')}`); +}); + function setupSpinner(props: Partial> = {}) { const { rerender: rtlRerender, ...rest } = render(); return { @@ -93,3 +123,14 @@ function setupSpinner(props: Partial> = {}) { ...rest, }; } + +const CustomColorSpinner = styled(SpinnerOverrideColor)` + --spinner-color-override: hotpink; +`; + +function getSpinnerIcon(container: HTMLElement) { + // eslint-disable-next-line testing-library/no-container, testing-library/no-node-access -- this is a purely visual element that isn't accessible + const spinnerIcon = container.querySelector('output > span'); + expect(spinnerIcon).toBeInTheDocument(); + return spinnerIcon as HTMLElement; +} From 7f94acded1392e3e317b28f6f0807f6c82dd7ba9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rcio?= Date: Thu, 2 Jul 2026 14:31:31 +0200 Subject: [PATCH 2/4] ECHOES-1409 Refine spinner overrides --- src/components/badges/BadgeSeverity.tsx | 12 +++++++- .../badges/__tests__/BadgeSeverity-test.tsx | 17 ++++++++++- src/components/buttons/ButtonStyles.tsx | 9 ++++++ .../buttons/__tests__/Button-test.tsx | 26 +++++++++++++++++ .../spinner/SpinnerOverrideColor.tsx | 3 +- src/components/spinner/SpinnerStyles.tsx | 24 ++++++++++++++-- .../spinner/__tests__/Spinner-test.tsx | 28 ++++++++++++++++--- 7 files changed, 109 insertions(+), 10 deletions(-) diff --git a/src/components/badges/BadgeSeverity.tsx b/src/components/badges/BadgeSeverity.tsx index 9643b5872..5d8050021 100644 --- a/src/components/badges/BadgeSeverity.tsx +++ b/src/components/badges/BadgeSeverity.tsx @@ -348,6 +348,8 @@ const BADGE_SEVERITY_ICON = { const BADGE_SEVERITY_BLOCKER_STYLE = { '--badge-severity-color': cssVar('severity-badge-colors-foreground-blocker-text-default'), '--badge-severity-border-color': cssVar('severity-badge-colors-borders-blocker-default'), + '--spinner-color-override': cssVar('severity-badge-colors-foreground-blocker-icon-default'), + '--spinner-track-color-override': cssVar('color-border-bold'), '--badge-severity-background-color': cssVar( 'severity-badge-colors-background-severity-blocker-prefix-default', ), @@ -362,6 +364,8 @@ const BADGE_SEVERITY_BLOCKER_STYLE = { const BADGE_SEVERITY_HIGH_STYLE = { '--badge-severity-color': cssVar('severity-badge-colors-foreground-high-text-default'), '--badge-severity-border-color': cssVar('severity-badge-colors-borders-high-default'), + '--spinner-color-override': cssVar('severity-badge-colors-foreground-high-icon-default'), + '--spinner-track-color-override': cssVar('color-border-bold'), '--badge-severity-background-color': cssVar( 'severity-badge-colors-background-severity-high-prefix-default', ), @@ -376,6 +380,8 @@ const BADGE_SEVERITY_HIGH_STYLE = { const BADGE_SEVERITY_LOW_STYLE = { '--badge-severity-color': cssVar('severity-badge-colors-foreground-low-text-default'), '--badge-severity-border-color': cssVar('severity-badge-colors-borders-low-default'), + '--spinner-color-override': cssVar('severity-badge-colors-foreground-low-icon-default'), + '--spinner-track-color-override': cssVar('color-border-bold'), '--badge-severity-background-color': cssVar( 'severity-badge-colors-background-severity-low-prefix-default', ), @@ -390,6 +396,8 @@ const BADGE_SEVERITY_LOW_STYLE = { const BADGE_SEVERITY_MEDIUM_STYLE = { '--badge-severity-color': cssVar('severity-badge-colors-foreground-medium-text-default'), '--badge-severity-border-color': cssVar('severity-badge-colors-borders-medium-default'), + '--spinner-color-override': cssVar('severity-badge-colors-foreground-medium-icon-default'), + '--spinner-track-color-override': cssVar('color-border-bold'), '--badge-severity-background-color': cssVar( 'severity-badge-colors-background-severity-medium-prefix-default', ), @@ -404,6 +412,8 @@ const BADGE_SEVERITY_MEDIUM_STYLE = { const BADGE_SEVERITY_INFO_STYLE = { '--badge-severity-color': cssVar('severity-badge-colors-foreground-info-text-default'), '--badge-severity-border-color': cssVar('severity-badge-colors-borders-info-default'), + '--spinner-color-override': cssVar('severity-badge-colors-foreground-info-icon-default'), + '--spinner-track-color-override': cssVar('color-border-bold'), '--badge-severity-background-color': cssVar( 'severity-badge-colors-background-severity-info-prefix-default', ), @@ -414,7 +424,7 @@ const BADGE_SEVERITY_INFO_STYLE = { 'severity-badge-colors-background-severity-info-suffix-hover', ), }; -const BADGE_SEVERITY_STYLES = { +export const BADGE_SEVERITY_STYLES = { [BadgeSeverityLevel.Blocker]: BADGE_SEVERITY_BLOCKER_STYLE, [BadgeSeverityLevel.High]: BADGE_SEVERITY_HIGH_STYLE, [BadgeSeverityLevel.Info]: BADGE_SEVERITY_INFO_STYLE, diff --git a/src/components/badges/__tests__/BadgeSeverity-test.tsx b/src/components/badges/__tests__/BadgeSeverity-test.tsx index 7e5a74f85..0fc935cfb 100644 --- a/src/components/badges/__tests__/BadgeSeverity-test.tsx +++ b/src/components/badges/__tests__/BadgeSeverity-test.tsx @@ -21,7 +21,9 @@ import { screen } from '@testing-library/react'; import { render } from '~common/helpers/test-utils'; import { IconBug } from '../../../components/icons'; -import { BadgeSeverity } from '../BadgeSeverity'; +import { BADGE_SEVERITY_STYLES, BadgeSeverity, BadgeSeverityLevel } from '../BadgeSeverity'; + +import { cssVar } from '~utils/design-tokens'; describe('BadgeSeverity', () => { it('renders as expected', () => { @@ -58,4 +60,17 @@ describe('BadgeSeverity', () => { expect(screen.getByText('Loading...')).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'click me' })).toBeEnabled(); }); + + it.each([ + [BadgeSeverityLevel.Blocker, cssVar('severity-badge-colors-foreground-blocker-icon-default')], + [BadgeSeverityLevel.High, cssVar('severity-badge-colors-foreground-high-icon-default')], + [BadgeSeverityLevel.Medium, cssVar('severity-badge-colors-foreground-medium-icon-default')], + [BadgeSeverityLevel.Low, cssVar('severity-badge-colors-foreground-low-icon-default')], + [BadgeSeverityLevel.Info, cssVar('severity-badge-colors-foreground-info-icon-default')], + ])('defines %s severity tokens for the loading spinner', (severity, spinnerColor) => { + expect(BADGE_SEVERITY_STYLES[severity]).toMatchObject({ + '--spinner-color-override': spinnerColor, + '--spinner-track-color-override': cssVar('color-border-bold'), + }); + }); }); diff --git a/src/components/buttons/ButtonStyles.tsx b/src/components/buttons/ButtonStyles.tsx index a16759ff4..f9fd11da2 100644 --- a/src/components/buttons/ButtonStyles.tsx +++ b/src/components/buttons/ButtonStyles.tsx @@ -119,6 +119,8 @@ export const BUTTON_VARIETY_STYLES = { '--button-background-active': cssVar('color-surface-active'), '--button-background-focus': cssVar('color-surface-default'), '--button-background-disabled': cssVar('color-surface-disabled'), + '--spinner-color-override': cssVar('color-icon-default'), + '--spinner-track-color-override': cssVar('color-border-bold'), }, [ButtonVariety.DefaultGhost]: { '--button-color': cssVar('color-text-default'), @@ -128,6 +130,8 @@ export const BUTTON_VARIETY_STYLES = { '--button-background-active': cssVar('color-background-ghost-neutral-active'), '--button-background-focus': cssVar('color-background-ghost-neutral-focus'), '--button-background-disabled': cssVar('color-background-ghost-neutral-default'), + '--spinner-color-override': cssVar('color-icon-default'), + '--spinner-track-color-override': cssVar('color-border-bold'), }, [ButtonVariety.Primary]: { '--button-color': cssVar('color-text-on-color'), @@ -138,6 +142,7 @@ export const BUTTON_VARIETY_STYLES = { '--button-background-focus': cssVar('color-background-accent-focus'), '--button-background-disabled': cssVar('color-surface-disabled'), '--spinner-color-override': cssVar('color-icon-on-color'), + '--spinner-track-color-override': cssVar('color-border-bold'), }, [ButtonVariety.PrimaryGhost]: { '--button-color': cssVar('color-text-accent'), @@ -147,6 +152,7 @@ export const BUTTON_VARIETY_STYLES = { '--button-background-active': cssVar('color-background-ghost-accent-active'), '--button-background-focus': cssVar('color-background-ghost-accent-focus'), '--button-background-disabled': cssVar('color-background-ghost-accent-default'), + '--spinner-color-override': cssVar('color-border-accent-default'), }, [ButtonVariety.Danger]: { '--button-color': cssVar('color-text-on-color'), @@ -157,6 +163,7 @@ export const BUTTON_VARIETY_STYLES = { '--button-background-focus': cssVar('color-background-danger-focus'), '--button-background-disabled': cssVar('color-surface-disabled'), '--spinner-color-override': cssVar('color-icon-on-color'), + '--spinner-track-color-override': cssVar('color-background-danger-active'), }, [ButtonVariety.DangerGhost]: { '--button-color': cssVar('color-text-danger'), @@ -166,6 +173,7 @@ export const BUTTON_VARIETY_STYLES = { '--button-background-active': cssVar('color-background-ghost-danger-active'), '--button-background-focus': cssVar('color-background-ghost-danger-focus'), '--button-background-disabled': cssVar('color-background-ghost-danger-default'), + '--spinner-color-override': cssVar('color-icon-danger'), }, [ButtonVariety.DangerOutline]: { '--button-color': cssVar('color-text-danger'), @@ -175,6 +183,7 @@ export const BUTTON_VARIETY_STYLES = { '--button-background-active': cssVar('color-surface-active'), '--button-background-focus': cssVar('color-surface-default'), '--button-background-disabled': cssVar('color-surface-disabled'), + '--spinner-color-override': cssVar('color-icon-danger'), }, }; diff --git a/src/components/buttons/__tests__/Button-test.tsx b/src/components/buttons/__tests__/Button-test.tsx index 04f3f8b50..9698600ff 100644 --- a/src/components/buttons/__tests__/Button-test.tsx +++ b/src/components/buttons/__tests__/Button-test.tsx @@ -21,6 +21,10 @@ import { screen } from '@testing-library/react'; import { PointerEventsCheckLevel } from '@testing-library/user-event'; import { render, renderWithMemoryRouter } from '~common/helpers/test-utils'; import { Button } from '../Button'; +import { BUTTON_VARIETY_STYLES } from '../ButtonStyles'; +import { ButtonVariety } from '../ButtonTypes'; + +import { cssVar } from '~utils/design-tokens'; describe('Button', () => { it('should call onClick function when clicked', async () => { @@ -60,6 +64,28 @@ describe('Button', () => { expect(screen.getByRole('button', { name: 'Loading... Click me' })).toBeEnabled(); }); + it.each([ + [ButtonVariety.Default, cssVar('color-icon-default'), cssVar('color-border-bold')], + [ButtonVariety.DefaultGhost, cssVar('color-icon-default'), cssVar('color-border-bold')], + [ButtonVariety.Primary, cssVar('color-icon-on-color'), cssVar('color-border-bold')], + [ButtonVariety.PrimaryGhost, cssVar('color-border-accent-default'), undefined], + [ButtonVariety.Danger, cssVar('color-icon-on-color'), cssVar('color-background-danger-active')], + [ButtonVariety.DangerGhost, cssVar('color-icon-danger'), undefined], + [ButtonVariety.DangerOutline, cssVar('color-icon-danger'), undefined], + ])('defines %s sentiment tokens for the loading spinner', (variety, spinnerColor, trackColor) => { + expect(BUTTON_VARIETY_STYLES[variety]).toMatchObject({ + '--spinner-color-override': spinnerColor, + }); + + if (trackColor) { + expect(BUTTON_VARIETY_STYLES[variety]).toMatchObject({ + '--spinner-track-color-override': trackColor, + }); + } else { + expect(BUTTON_VARIETY_STYLES[variety]).not.toHaveProperty('--spinner-track-color-override'); + } + }); + it('should render with prefix and suffix', () => { render(