Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,21 @@ Make sure to setup the following Providers at the root of your app:

The `EchoesProvider` is required to provide theming and configuration context for all Echoes components.

If your application uses a strict Content Security Policy (CSP), you can provide a nonce to the `EchoesProvider` to enable inline styles required by Echoes components:

```tsx
const nonce = document.querySelector('meta[name="csp-nonce"]')?.getAttribute('content');

<EchoesProvider nonce={nonce}>{/* your app */}</EchoesProvider>;
```

This nonce will be automatically applied to:

- Emotion's CSS-in-JS style tags
- react-joyride inline styles used in the Spotlight component

For more information about CSP configuration, see the [Content Security Policy](#content-security-policy) section.

#### Intl Provider

The `IntlProvider` from `react-intl` is necessary for translations. See [this page](https://formatjs.github.io/docs/react-intl/components#intlprovider) for more information.
Expand Down Expand Up @@ -205,6 +220,61 @@ If tooltips or other overlay components don't appear correctly, ensure you have

If you encounter errors about missing router context when using Link components or other routing-related components, make sure you have wrapped your app with a Router provider from `react-router-dom` (e.g., `BrowserRouter`, `HashRouter`, or `MemoryRouter`).

## Content Security Policy

Echoes React supports strict Content Security Policy (CSP) configurations. To use Echoes with a CSP that restricts inline styles, you need to configure nonces.

### CSP Configuration

Configure your CSP header as follows:

```http
Content-Security-Policy:
style-src
'self'
'nonce-RANDOM_VALUE'
'unsafe-hashes'
'sha256-47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU='
'sha256-qixoDh78J8vISHKC3rLI7qSXmTShr8mhsUgjJL7W7aU='
'sha256-3gJFr3n77fnX5qwQpGju/zCOsoHW5RMqQd5XOb9WFcA=';
Comment on lines +237 to +239
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

did you try this solution instead: emilkowalski/sonner#449 (comment) ?

```

Where:

- `'nonce-RANDOM_VALUE'` - A cryptographically secure random nonce generated per request (required for Emotion CSS-in-JS and Spotlight/react-joyride inline styles)
- `'unsafe-hashes'` with three SHA-256 hashes - Required only for sonner toast notification animations

### Application Setup

Pass the nonce to the `EchoesProvider`:

```tsx
import { EchoesProvider } from '@sonarsource/echoes-react';

function App() {
// Get nonce from meta tag or server-rendered context
const nonce = document.querySelector('meta[name="csp-nonce"]')?.getAttribute('content');

return (
<EchoesProvider nonce={nonce}>
<YourApp />
</EchoesProvider>
);
}
```

The nonce will be automatically applied to:

- All Emotion CSS-in-JS style tags
- Spotlight component's inline styles (via react-joyride)

### Notes

- **The three SHA-256 hashes are specific to sonner v2.0.7** (the toast notification library). If you upgrade sonner, these hashes may need to be updated. See the [sonner CSP issue](https://github.com/emilkowalski/sonner/issues/449) for details on how to generate new hashes.
- **Automated dependency updates**: Renovate is configured to require manual approval for sonner updates to ensure CSP hashes are verified before upgrading.
- Without the nonce configuration, applications with strict CSP will block Echoes components from rendering correctly.
- For development environments without CSP, the nonce prop is optional and can be omitted.

## License

Copyright 2023-2025 SonarSource.
Expand Down
5 changes: 5 additions & 0 deletions renovate.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@
"description": "React-intl minor patch version upgrade to 6.7.3 has broken types",
"matchPackageNames": ["react-intl"],
"dependencyDashboardApproval": true
},
{
"description": "Sonner updates require manual CSP hash verification. The CSP configuration includes version-specific SHA-256 hashes for inline styles. See README.md Content Security Policy section.",
"matchPackageNames": ["sonner"],
"dependencyDashboardApproval": true
}
],
"ignoreDeps": []
Expand Down
15 changes: 12 additions & 3 deletions src/common/helpers/test-utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import createCache from '@emotion/cache';
import { CacheProvider } from '@emotion/react';
import { RenderOptions, RenderResult, render as rtlRender } from '@testing-library/react';
import userEvent, { UserEvent, Options as UserEventsOptions } from '@testing-library/user-event';
import React, { ComponentProps, PropsWithChildren } from 'react';
Expand All @@ -25,6 +27,11 @@ import { MemoryRouter, Route, Routes, useLocation } from 'react-router-dom';
import { PropsWithLabels, PropsWithLabelsAndHelpText } from '~types/utils';
import { EchoesProvider } from '../../components/echoes-provider';

// Create a singleton Emotion cache for all tests to ensure consistent class name generation
// This matches the key used in EchoesProvider when a nonce is provided
const testEmotionCache = createCache({ key: 'echoes' });
testEmotionCache.compat = true;
Comment on lines +30 to +33
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should that really be handled by Echoes? The CacheProvider is gonna change the behavior of emotion for the whole app, that should be setup at the app level not at Echoes level.


type RenderResultWithUser = RenderResult & { user: UserEvent };

export function render(
Expand Down Expand Up @@ -87,8 +94,10 @@ function ShowPath() {

function ContextWrapper({ children }: PropsWithChildren<{}>) {
return (
<IntlProvider defaultLocale="en-us" locale="en-us">
<EchoesProvider tooltipsDelayDuration={0}>{children}</EchoesProvider>
</IntlProvider>
<CacheProvider value={testEmotionCache}>
<IntlProvider defaultLocale="en-us" locale="en-us">
<EchoesProvider tooltipsDelayDuration={0}>{children}</EchoesProvider>
</IntlProvider>
</CacheProvider>
);
}
65 changes: 59 additions & 6 deletions src/components/echoes-provider/EchoesProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,29 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

import createCache from '@emotion/cache';
import { CacheProvider } from '@emotion/react';
import { HeadlessMantineProvider } from '@mantine/core';
import { PropsWithChildren } from 'react';
import { PropsWithChildren, useMemo } from 'react';
import { useIntl } from 'react-intl';
import { Toaster as ToastContainer } from 'sonner';
import { ToastGlobalStyles } from '~common/components/Toast';
import { TooltipProvider, TooltipProviderProps, TypographyGlobalStyles } from '..';
import { SelectGlobalStyles } from '../select/SelectCommons';
import { NonceContext } from './NonceContext';

export interface EchoesProviderProps {
/**
* A nonce value for inline styles (Content Security Policy - CSP) (optional).
* When provided, this nonce will be:
* - Applied to Emotion's CSS-in-JS style tags
* - Made available to components like Spotlight that need it for inline styles
* - Used to comply with strict Content Security Policy requirements
*
* This should be set once at the application root and will automatically
* propagate to all Echoes components that require it.
*/
nonce?: string;
/**
* Custom class name for all the toasts (optional).
*/
Expand Down Expand Up @@ -55,7 +69,8 @@ export interface EchoesProviderProps {
* It must be placed at the root of your application (or at least wrap all
* components that use the Echoes design system). To ensure all Echoes components work properly,
* the EchoesProvider should be placed inside the react-intl provider and react-router provider.
* Ideally, you should also wrap your application with a div that reset the [Stacking Context](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_positioned_layout/Understanding_z-index/Stacking_context)
* Ideally, you should also wrap your application with a div that reset the
* [Stacking Context](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_positioned_layout/Understanding_z-index/Stacking_context)
* for your app to ensure that tooltips and toasts from Echoes appear above the rest of the UI.
*
* **Usage**
Expand Down Expand Up @@ -85,13 +100,44 @@ export interface EchoesProviderProps {
* );
* }
* ```
*
* **Content Security Policy (CSP) Support**
*
* If your application uses a strict Content Security Policy, you can provide a nonce
* to enable inline styles required by Echoes components:
*
* ```tsx
* function App() {
* // Get nonce from meta tag or server context
* const nonce = document.querySelector('meta[name="csp-nonce"]')?.getAttribute('content');
*
* return (
* <EchoesProvider nonce={nonce}>
* {children}
* </EchoesProvider>
* );
* }
* ```
*/
export function EchoesProvider(props: PropsWithChildren<EchoesProviderProps>) {
const { children, tooltipsDelayDuration, toastsClassName, toastsVisibleNb = 5 } = props;
const { children, nonce, tooltipsDelayDuration, toastsClassName, toastsVisibleNb = 5 } = props;
const intl = useIntl();

return (
<>
// Create Emotion cache with nonce support for CSP compliance
// Use 'echoes' as the key for better namespace isolation and debugging
const emotionCache = useMemo(() => {
if (!nonce) {
return undefined;
}

const cache = createCache({ key: 'echoes', nonce });
cache.compat = true;

return cache;
}, [nonce]);
Comment on lines +126 to +137
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the emotion cache provider shouldn't be handled by Echoes


const providerContent = (
<NonceContext.Provider value={nonce}>
<TypographyGlobalStyles />
<SelectGlobalStyles />
<ToastGlobalStyles />
Expand All @@ -108,7 +154,14 @@ export function EchoesProvider(props: PropsWithChildren<EchoesProviderProps>) {
visibleToasts={toastsVisibleNb}
/>
</TooltipProvider>
</>
</NonceContext.Provider>
);

// Only wrap with CacheProvider if a nonce is provided
return emotionCache ? (
<CacheProvider value={emotionCache}>{providerContent}</CacheProvider>
) : (
providerContent
);
}

Expand Down
39 changes: 39 additions & 0 deletions src/components/echoes-provider/NonceContext.ts
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's be a bit more explicite and call that CSPNonceContext?

Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Echoes React
* Copyright (C) 2023-2025 SonarSource Sàrl
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

import { createContext, useContext } from 'react';

/**
* Context for providing CSP nonce value to components that need it.
* This is used internally by Echoes components to access the nonce
* configured in the EchoesProvider.
*/
export const NonceContext = createContext<string | undefined>(undefined);

/**
* Hook to access the CSP nonce from the EchoesProvider.
* Components that need to apply nonces to inline styles for CSP compliance
* can use this hook to retrieve the globally configured nonce value.
*
* @returns The nonce string if configured, undefined otherwise
*/
export function useNonce(): string | undefined {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here too let's be more explicit

return useContext(NonceContext);
}
93 changes: 93 additions & 0 deletions src/components/echoes-provider/__tests__/EchoesProvider-test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
* Echoes React
* Copyright (C) 2023-2025 SonarSource Sàrl
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

import { render as rtlRender, screen } from '@testing-library/react';
import { IntlProvider } from 'react-intl';
import { EchoesProvider } from '../EchoesProvider';
import { useNonce } from '../NonceContext';

describe('EchoesProvider', () => {
it('should render children without nonce', () => {
rtlRender(
<IntlProvider defaultLocale="en-us" locale="en-us">
<EchoesProvider>
<div>Test content</div>
</EchoesProvider>
</IntlProvider>,
);

expect(screen.getByText('Test content')).toBeInTheDocument();
});

it('should render children with nonce', () => {
const testNonce = 'test-nonce-12345';

rtlRender(
<IntlProvider defaultLocale="en-us" locale="en-us">
<EchoesProvider nonce={testNonce}>
<div>Test content with nonce</div>
</EchoesProvider>
</IntlProvider>,
);

expect(screen.getByText('Test content with nonce')).toBeInTheDocument();
});

it('should provide nonce through context', () => {
const testNonce = 'test-nonce-67890';
let capturedNonce: string | undefined;

function TestComponent() {
capturedNonce = useNonce();

return <div>Test</div>;
}

rtlRender(
<IntlProvider defaultLocale="en-us" locale="en-us">
<EchoesProvider nonce={testNonce}>
<TestComponent />
</EchoesProvider>
</IntlProvider>,
);

expect(capturedNonce).toBe(testNonce);
});

it('should provide undefined nonce when not specified', () => {
let capturedNonce: string | undefined = 'initial-value';

function TestComponent() {
capturedNonce = useNonce();

return <div>Test</div>;
}

rtlRender(
<IntlProvider defaultLocale="en-us" locale="en-us">
<EchoesProvider>
<TestComponent />
</EchoesProvider>
</IntlProvider>,
);

expect(capturedNonce).toBeUndefined();
});
});
1 change: 1 addition & 0 deletions src/components/echoes-provider/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@
*/

export { EchoesProvider, type EchoesProviderProps } from './EchoesProvider';
export { useNonce } from './NonceContext';
Loading