diff --git a/docs/demo/getScrollBarSize.md b/docs/demo/getScrollBarSize.md index 9dfc8138..3d36f914 100644 --- a/docs/demo/getScrollBarSize.md +++ b/docs/demo/getScrollBarSize.md @@ -6,3 +6,14 @@ nav: --- + +When using a Content Security Policy that requires a style nonce, pass the CSP +config as the second argument to `getTargetScrollBarSize`. The nonce is applied +to the temporary styles used for measurement: + +```ts +getTargetScrollBarSize(element, { nonce: 'your-nonce' }); +``` + +Components using Ant Design's `ConfigProvider` should forward its `csp` config +to this argument. This utility does not read React context automatically. diff --git a/src/getScrollBarSize.tsx b/src/getScrollBarSize.tsx index 35709437..9e9fb0d5 100644 --- a/src/getScrollBarSize.tsx +++ b/src/getScrollBarSize.tsx @@ -2,6 +2,7 @@ import { removeCSS, updateCSS } from './Dom/dynamicCSS'; type ScrollBarSize = { width: number; height: number }; +type CSPConfig = { nonce?: string }; type ExtendCSSStyleDeclaration = CSSStyleDeclaration & { scrollbarColor?: string; @@ -10,7 +11,10 @@ type ExtendCSSStyleDeclaration = CSSStyleDeclaration & { let cached: ScrollBarSize; -function measureScrollbarSize(ele?: HTMLElement): ScrollBarSize { +function measureScrollbarSize( + ele?: HTMLElement, + csp?: CSPConfig, +): ScrollBarSize { const randomId = `rc-scrollbar-measure-${Math.random() .toString(36) .substring(7)}`; @@ -53,6 +57,7 @@ ${widthStyle} ${heightStyle} }`, randomId, + { csp }, ); } catch (e) { // Can't wrap, just log error @@ -98,7 +103,8 @@ export default function getScrollBarSize(fresh?: boolean): number { return cached.width; } -export function getTargetScrollBarSize(target: HTMLElement) { +/** Pass the application's CSP config when measuring a target's scrollbar. */ +export function getTargetScrollBarSize(target: HTMLElement, csp?: CSPConfig) { if ( typeof document === 'undefined' || !target || @@ -107,5 +113,5 @@ export function getTargetScrollBarSize(target: HTMLElement) { return { width: 0, height: 0 }; } - return measureScrollbarSize(target); + return measureScrollbarSize(target, csp); } diff --git a/tests/getScrollBarSize.test.ts b/tests/getScrollBarSize.test.ts index 9c03b945..67e966a5 100644 --- a/tests/getScrollBarSize.test.ts +++ b/tests/getScrollBarSize.test.ts @@ -2,6 +2,7 @@ import getScrollBarSize, { getTargetScrollBarSize, } from '../src/getScrollBarSize'; import { spyElementPrototypes } from '../src/test/domHook'; +import { clearContainerCache } from '../src/Dom/dynamicCSS'; const DEFAULT_SIZE = 16; @@ -39,6 +40,52 @@ describe('getScrollBarSize', () => { }); describe('getTargetScrollBarSize', () => { + afterEach(() => { + jest.restoreAllMocks(); + clearContainerCache(); + }); + + it.each([undefined, {}, { nonce: 'test-nonce' }])( + 'measures and cleans up with CSP config %p', + csp => { + const target = document.createElement('div'); + jest.spyOn(window, 'getComputedStyle').mockImplementation( + (_, pseudoElement) => + ({ + width: pseudoElement ? '12px' : '100px', + height: pseudoElement ? '8px' : '100px', + scrollbarColor: 'auto', + scrollbarWidth: 'auto', + }) as CSSStyleDeclaration, + ); + + const insertedStyles: HTMLStyleElement[] = []; + const appendChild = document.head.appendChild; + jest.spyOn(document.head, 'appendChild').mockImplementation(node => { + if (node instanceof HTMLStyleElement) { + // CSP checks the nonce at insertion, including the placeholder style. + expect(node.nonce).toBe(csp?.nonce || ''); + insertedStyles.push(node); + } + return appendChild.call(document.head, node) as typeof node; + }); + + expect(getTargetScrollBarSize(target, csp)).toEqual({ + width: DEFAULT_SIZE, + height: 0, + }); + expect(insertedStyles).toHaveLength(2); + expect(insertedStyles[1].innerHTML).toContain('width: 12px;'); + expect(insertedStyles[1].innerHTML).toContain('height: 8px;'); + insertedStyles.forEach(style => { + expect(style.parentNode).toBeNull(); + }); + expect( + document.querySelector('[id^="rc-scrollbar-measure-"]'), + ).toBeNull(); + }, + ); + it('invalidate', () => { expect( getTargetScrollBarSize({ notValidateObject: true } as any),