Skip to content

Commit

Permalink
[add] support for CSP policy requiring 'nonce' on <style>
Browse files Browse the repository at this point in the history
CSP policy may prevent writing to `<style>` unless a `nonce` attribute
is set. This change makes that possible by moving the modality-related
styles into the main style sheet, and allowing additional props to be
provided to the `<style>` element when rendering on the server. For
example:

```
const { element, getStyleElement } = AppRegistry.getApplication('App');
const html = renderToString(element);
const css = renderToStaticMarkup(getStyleElement({ nonce }));
```
  • Loading branch information
necolas committed May 18, 2018
1 parent 3e4d8d6 commit ee5e800
Show file tree
Hide file tree
Showing 7 changed files with 37 additions and 36 deletions.
Expand Up @@ -39,6 +39,14 @@ describe('AppRegistry', () => {
expect(styleElement).toMatchSnapshot();
});

test('"getStyleElement" adds props to <style>', () => {
const nonce = '2Bz9RM/UHvBbmo3jK/PbYZ==';
AppRegistry.registerComponent('App', () => RootComponent);
const { getStyleElement } = AppRegistry.getApplication('App', {});
const styleElement = getStyleElement({ nonce });
expect(styleElement.props.nonce).toBe(nonce);
});

test('"getStyleElement" produces styles that are a function of rendering "element"', () => {
const getApplicationStyles = appName => {
const { element, getStyleElement } = AppRegistry.getApplication(appName, {});
Expand Down
Expand Up @@ -46,9 +46,11 @@ export function getApplication(
</AppContainer>
);
// Don't escape CSS text
const getStyleElement = () => {
const getStyleElement = props => {
const sheet = styleResolver.getStyleSheet();
return <style dangerouslySetInnerHTML={{ __html: sheet.textContent }} id={sheet.id} />;
return (
<style {...props} dangerouslySetInnerHTML={{ __html: sheet.textContent }} id={sheet.id} />
);
};
return { element, getStyleElement };
}
Expand Up @@ -69,10 +69,6 @@ export default class StyleSheetManager {
return className;
}

injectKeyframe(): string {
// return identifier;
}

_addToCache(className, prop, value) {
const cache = this._cache;
if (!cache.byProp[prop]) {
Expand Down
Expand Up @@ -8,6 +8,7 @@
*/

import { canUseDOM } from 'fbjs/lib/ExecutionEnvironment';
import modality from './modality';

export default class WebStyleSheet {
_cssRules = [];
Expand All @@ -29,6 +30,7 @@ export default class WebStyleSheet {
}

if (domStyleElement) {
modality(domStyleElement);
// $FlowFixMe
this._sheet = domStyleElement.sheet;
this._textContent = domStyleElement.textContent;
Expand Down
4 changes: 0 additions & 4 deletions packages/react-native-web/src/exports/StyleSheet/index.js
@@ -1,9 +1,5 @@
import modality from '../../modules/modality';
import StyleSheet from './StyleSheet';

// initialize focus-ring fix
modality();

// allow component styles to be editable in React Dev Tools
if (process.env.NODE_ENV !== 'production') {
const { canUseDOM } = require('fbjs/lib/ExecutionEnvironment');
Expand Down
Expand Up @@ -18,12 +18,14 @@

import { canUseDOM } from 'fbjs/lib/ExecutionEnvironment';

const modality = () => {
const rule = ':focus { outline: none; }';
let ruleExists = false;

const modality = styleElement => {
if (!canUseDOM) {
return;
}

let styleElement;
let hadKeyboardEvent = false;
let keyboardThrottleTimeoutID = 0;

Expand Down Expand Up @@ -55,21 +57,6 @@ const modality = () => {
'[role=textbox]'
].join(',');

/**
* Disable the focus ring by default
*/
const initialize = () => {
// check if the style sheet needs to be created
const id = 'react-native-modality';
styleElement = document.getElementById(id);
if (!styleElement) {
// removes focus styles by default
const style = `<style id="${id}">:focus { outline: none; }</style>`;
document.head.insertAdjacentHTML('afterbegin', style);
styleElement = document.getElementById(id);
}
};

/**
* Computes whether the given element should automatically trigger the
* `focus-ring`.
Expand All @@ -83,20 +70,22 @@ const modality = () => {
};

/**
* Add the focus ring to the focused element
* Add the focus ring style
*/
const addFocusRing = () => {
if (styleElement) {
styleElement.disabled = true;
if (styleElement && ruleExists) {
styleElement.sheet.deleteRule(0);
ruleExists = false;
}
};

/**
* Remove the focus ring
* Remove the focus ring style
*/
const removeFocusRing = () => {
if (styleElement) {
styleElement.disabled = false;
if (styleElement && !ruleExists) {
styleElement.sheet.insertRule(rule, 0);
ruleExists = true;
}
};

Expand Down Expand Up @@ -136,7 +125,7 @@ const modality = () => {
};

if (document.body && document.body.addEventListener) {
initialize();
removeFocusRing();
document.body.addEventListener('keydown', handleKeyDown, true);
document.body.addEventListener('focus', handleFocus, true);
document.body.addEventListener('blur', handleBlur, true);
Expand Down
12 changes: 10 additions & 2 deletions website/storybook/2-apis/AppRegistry/AppRegistryScreen.js
Expand Up @@ -31,10 +31,18 @@ const AppRegistryScreen = () => (
/>

<DocItem
description="Use this for server-side rendering to HTML. Returns a object of the given application's element, and a function to get styles once the element is rendered."
description={
<AppText>
Use this for server-side rendering to HTML. Returns an object containing the given
application's element and a function to get styles once the element is rendered.
Additional props can be passed to the <Code>getStyleElement</Code> function, e.g., your
CSP policy may require a <Code>nonce</Code> to be set on <Code>style</Code>
elements.
</AppText>
}
label="web"
name="static getApplication"
typeInfo="(appKey: string, appParameters: ?object) => { element: ReactElement; getStyleElement: () => ReactElement }"
typeInfo="(appKey: string, appParameters: ?object) => { element: ReactElement; getStyleElement: (props) => ReactElement }"
/>

<DocItem
Expand Down

0 comments on commit ee5e800

Please sign in to comment.