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
6 changes: 4 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ package-lock.json
/packages/react-dropdown-select/src/helpers.js
.DS_Store
coverage
dist/
lib/
/packages/react-dropdown-select/dist/
/packages/react-dropdown-select/lib/
/packages/docs-site/dist/
/packages/react-dropdown-select/types/
storybook-static/
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ pnpm test
| addPlaceholder | string | "" | Secondary placeholder on search field if any value selected |
| disabled | bool | false | Disable select and all interactions |
| style | object | {} | Style object to pass to select |
| styleNonce | string | | Nonce value for the `<style>` tag injected by the component, useful to satisfy a strict Content Security Policy |
| className | string | | CSS class attribute to pass to select |
| loading | bool | false | Loading indicator |
| clearable | bool | false | Clear all indicator |
Expand Down Expand Up @@ -185,6 +186,33 @@ pnpm test
| searchFn | func | undefined | Overrides internal search function |
| handleKeyDownFn | func | undefined | Overrides internal keyDown function |

### Content Security Policy

The component serves its base styles and per-component CSS variables through a `<style>` tag added to
`document.head`. Under a strict CSP (e.g. `style-src 'self' 'nonce-…'`) that tag is blocked unless it
carries a nonce. Pass the nonce via the `styleNonce` prop:

```jsx
<Select styleNonce={window.NONCE_ID} ... />
```

The remaining runtime values (such as dropdown positioning) are applied through the DOM style API
(`element.style.setProperty`), which Content Security Policy does not block.

#### Testing against a strict CSP in the docs site

The docs dev server ships a self-contained CSP test page. It is served with a strict
`style-src 'self' 'nonce-…'` header and renders a `<Select styleNonce={…}>`, then verifies that the
injected stylesheet carried the matching nonce and that its rules actually applied:

```sh
pnpm --filter docs-site dev
# open http://localhost:5173/react-dropdown-select/csp-test.html
```

The page reports PASS/FAIL per check; a clean run shows no CSP violations in the DevTools console.
This is a dev-only entry and is not part of the static docs build.

### License

[MIT](https://github.com/sanusart/react-dropdown-select/blob/master/LICENSE)
24 changes: 24 additions & 0 deletions commitlint.config.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [
2,
'always',
[
'build',
'chore',
'ci',
'docs',
'feat',
'fix',
'perf',
'refactor',
'revert',
'style',
'test',
'typo',
'i18n',
],
],
},
};
16 changes: 0 additions & 16 deletions commitlint.config.js

This file was deleted.

7 changes: 7 additions & 0 deletions packages/docs-site/README.md
Original file line number Diff line number Diff line change
@@ -1 +1,8 @@
# React dropdown select site

## CSP test page

`pnpm --filter docs-site dev` also serves a self-contained test for the `styleNonce` prop at
`http://localhost:5173/react-dropdown-select/csp-test.html`. It applies a strict
`style-src 'self' 'nonce-…'` header and verifies the component still renders. Dev-only; not part of
the static build.
12 changes: 12 additions & 0 deletions packages/docs-site/csp-test.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>react-dropdown-select - Content Security Policy test</title>
</head>
<body>
<div id="csp-root"></div>
<script type="module" src="/src/csp-test-main.tsx"></script>
</body>
</html>
3 changes: 3 additions & 0 deletions packages/docs-site/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@
"type": "module",
"homepage": "http://sanusart.github.io/react-dropdown-select/",
"scripts": {
"predev": "pnpm --filter react-dropdown-select types",
"dev": "vite",
"prebuild": "pnpm --filter react-dropdown-select types",
"build": "vite build",
"preview": "vite preview",
"pretypecheck": "pnpm --filter react-dropdown-select types",
"typecheck": "tsc --noEmit",
"lint": "eslint src",
"lint:fix": "eslint src --fix",
Expand Down
93 changes: 93 additions & 0 deletions packages/docs-site/src/csp-test-main.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { useEffect, useState } from 'react';
import type { CSSProperties } from 'react';
import { createRoot } from 'react-dom/client';
import Select from 'react-dropdown-select';

declare global {
interface Window {
__CSP_NONCE__?: string;
}
}

const nonce = window.__CSP_NONCE__;

const options = [
{ value: 1, label: 'One' },
{ value: 2, label: 'Two' },
{ value: 3, label: 'Three' },
];

const ROOT_MIN_HEIGHT = '36px';

const pageStyles: CSSProperties = {
maxWidth: 720,
margin: '40px auto',
fontFamily: 'Inter, system-ui, sans-serif',
};

const codeStyles: CSSProperties = {
background: '#f6f8fa',
padding: '2px 6px',
borderRadius: 4,
fontSize: 13,
};

function CspTest() {
const [withNonce, setWithNonce] = useState(true);
const [stylesApplied, setStylesApplied] = useState<boolean | null>(null);

useEffect(() => {
const rootEl = document.querySelector<HTMLElement>('.react-dropdown-select');
const minHeight = rootEl ? getComputedStyle(rootEl).minHeight : '';
setStylesApplied(minHeight === ROOT_MIN_HEIGHT);
}, [withNonce]);

const blocked = stylesApplied === false;

return (
<div style={pageStyles}>
<h1 style={{ fontSize: 24, margin: '0 0 4px' }}>react-dropdown-select — CSP test</h1>
<p style={{ margin: '0 0 16px', color: '#57606a', fontSize: 14, lineHeight: 1.6 }}>
This page is served with{' '}
<code style={codeStyles}>Content-Security-Policy: style-src 'self' 'nonce-…'</code>. Without
a nonce the injected <code style={codeStyles}>&lt;style&gt;</code> tag is blocked by the
policy, so the dropdown renders unstyled. Toggle <code style={codeStyles}>styleNonce</code>{' '}
and watch.
</p>

<div style={{ marginBottom: 16 }}>
<button
onClick={() => setWithNonce((v) => !v)}
style={{ padding: '6px 12px', cursor: 'pointer', borderRadius: 4 }}>
styleNonce: {withNonce ? 'provided' : 'not provided'}
</button>
</div>

<Select
key={withNonce ? 'with-nonce' : 'without-nonce'}
styleNonce={withNonce ? nonce : undefined}
options={options}
values={[options[0]]}
multi
clearable
placeholder="Pick some options…"
/>

<p
style={{
marginTop: 24,
fontSize: 14,
color: blocked ? '#cf222e' : '#1a7f37',
fontWeight: 600,
}}>
{stylesApplied === null
? 'Checking…'
: blocked
? 'Styles blocked — the <style> tag has no matching nonce, so the dropdown is unstyled.'
: 'Styles applied — the <style> tag carries the nonce and is allowed by the CSP.'}
</p>
</div>
);
}

createRoot(document.getElementById('csp-root')!).render(<CspTest />);
Loading
Loading