Initial commit 🎉
fbshipit-source-id: b6fc29740c6875d2e78953b8a7123890a67930f2 Co-authored-by: Sebastian McKenzie <sebmck@fb.com> Co-authored-by: John Knox <jknox@fb.com> Co-authored-by: Emil Sjölander <emilsj@fb.com> Co-authored-by: Pritesh Nandgaonkar <prit91@fb.com>
This commit is contained in:
145
src/ui/styled/__tests__/gc.node.js
Normal file
145
src/ui/styled/__tests__/gc.node.js
Normal file
@@ -0,0 +1,145 @@
|
||||
/**
|
||||
* Copyright 2018-present Facebook.
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
* @format
|
||||
*/
|
||||
|
||||
import type {Tracker} from '../index.js';
|
||||
import {GarbageCollector} from '../gc.js';
|
||||
import {StyleSheet} from '../sheet.js';
|
||||
|
||||
function createGC(): {|
|
||||
gc: GarbageCollector,
|
||||
tracker: Tracker,
|
||||
|} {
|
||||
const sheet = new StyleSheet();
|
||||
const tracker = new Map();
|
||||
const rulesToClass = new WeakMap();
|
||||
|
||||
const gc = new GarbageCollector(sheet, tracker, rulesToClass);
|
||||
return {gc, tracker};
|
||||
}
|
||||
|
||||
test('register classes to be garbage collected when no references exist', () => {
|
||||
const {gc} = createGC();
|
||||
|
||||
gc.registerClassUse('foo');
|
||||
expect(gc.getCollectionQueue()).toEqual([]);
|
||||
|
||||
gc.deregisterClassUse('foo');
|
||||
expect(gc.getCollectionQueue()).toEqual(['foo']);
|
||||
});
|
||||
|
||||
test('cancel garbage collection for classes used before actual collection happens', () => {
|
||||
const {gc} = createGC();
|
||||
|
||||
gc.registerClassUse('foo');
|
||||
expect(gc.getCollectionQueue()).toEqual([]);
|
||||
|
||||
gc.deregisterClassUse('foo');
|
||||
expect(gc.getCollectionQueue()).toEqual(['foo']);
|
||||
|
||||
gc.registerClassUse('foo');
|
||||
expect(gc.getCollectionQueue()).toEqual([]);
|
||||
});
|
||||
|
||||
test('garbage collector removes unreferenced classes', () => {
|
||||
const {gc, tracker} = createGC();
|
||||
|
||||
tracker.set('foo', {
|
||||
displayName: 'foo',
|
||||
namespace: '',
|
||||
selector: '',
|
||||
style: {},
|
||||
rules: {},
|
||||
});
|
||||
|
||||
gc.registerClassUse('foo');
|
||||
expect(gc.getCollectionQueue()).toEqual([]);
|
||||
|
||||
gc.deregisterClassUse('foo');
|
||||
expect(gc.getCollectionQueue()).toEqual(['foo']);
|
||||
expect(gc.hasQueuedCollection()).toBe(true);
|
||||
expect(tracker.has('foo')).toBe(true);
|
||||
|
||||
gc.collectGarbage();
|
||||
expect(gc.hasQueuedCollection()).toBe(false);
|
||||
expect(gc.getCollectionQueue()).toEqual([]);
|
||||
expect(tracker.has('foo')).toBe(false);
|
||||
});
|
||||
|
||||
test('properly tracks reference counts', () => {
|
||||
const {gc} = createGC();
|
||||
|
||||
gc.registerClassUse('foo');
|
||||
gc.registerClassUse('foo');
|
||||
gc.registerClassUse('bar');
|
||||
expect(gc.getReferenceCount('foo')).toBe(2);
|
||||
expect(gc.getReferenceCount('bar')).toBe(1);
|
||||
|
||||
gc.deregisterClassUse('bar');
|
||||
expect(gc.getReferenceCount('bar')).toBe(0);
|
||||
|
||||
gc.deregisterClassUse('foo');
|
||||
expect(gc.getReferenceCount('foo')).toBe(1);
|
||||
|
||||
gc.deregisterClassUse('foo');
|
||||
expect(gc.getReferenceCount('foo')).toBe(0);
|
||||
});
|
||||
|
||||
test("gracefully handle deregistering classes we don't have a count for", () => {
|
||||
const {gc} = createGC();
|
||||
gc.deregisterClassUse('not-tracking');
|
||||
});
|
||||
|
||||
test('only halt garbage collection if there is nothing left in the queue', () => {
|
||||
const {gc} = createGC();
|
||||
|
||||
gc.registerClassUse('foo');
|
||||
expect(gc.hasQueuedCollection()).toBe(false);
|
||||
|
||||
gc.deregisterClassUse('foo');
|
||||
expect(gc.hasQueuedCollection()).toBe(true);
|
||||
|
||||
gc.registerClassUse('bar');
|
||||
expect(gc.hasQueuedCollection()).toBe(true);
|
||||
|
||||
gc.deregisterClassUse('bar');
|
||||
expect(gc.hasQueuedCollection()).toBe(true);
|
||||
|
||||
gc.registerClassUse('bar');
|
||||
expect(gc.hasQueuedCollection()).toBe(true);
|
||||
|
||||
gc.registerClassUse('foo');
|
||||
expect(gc.hasQueuedCollection()).toBe(false);
|
||||
});
|
||||
|
||||
test('ensure garbage collection happens', () => {
|
||||
const {gc} = createGC();
|
||||
|
||||
gc.registerClassUse('foo');
|
||||
gc.deregisterClassUse('foo');
|
||||
expect(gc.hasQueuedCollection()).toBe(true);
|
||||
expect(gc.getCollectionQueue()).toEqual(['foo']);
|
||||
|
||||
jest.runAllTimers();
|
||||
expect(gc.hasQueuedCollection()).toBe(false);
|
||||
expect(gc.getCollectionQueue()).toEqual([]);
|
||||
});
|
||||
|
||||
test('flush', () => {
|
||||
const {gc} = createGC();
|
||||
|
||||
gc.registerClassUse('bar');
|
||||
gc.deregisterClassUse('bar');
|
||||
expect(gc.getCollectionQueue()).toEqual(['bar']);
|
||||
expect(gc.getReferenceCount('bar')).toBe(0);
|
||||
|
||||
gc.registerClassUse('foo');
|
||||
expect(gc.getReferenceCount('foo')).toBe(1);
|
||||
|
||||
gc.flush();
|
||||
expect(gc.getCollectionQueue()).toEqual([]);
|
||||
expect(gc.getReferenceCount('foo')).toBe(0);
|
||||
});
|
||||
14
src/ui/styled/__tests__/hash.node.js
Normal file
14
src/ui/styled/__tests__/hash.node.js
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Copyright 2018-present Facebook.
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
* @format
|
||||
*/
|
||||
|
||||
import hash from '../hash.js';
|
||||
|
||||
test('hash', () => {
|
||||
expect(hash('f')).toBe('1xwd1rk');
|
||||
expect(hash('foobar')).toBe('slolri');
|
||||
expect(hash('foobar2')).toBe('34u6r4');
|
||||
});
|
||||
387
src/ui/styled/__tests__/index.node.js
Normal file
387
src/ui/styled/__tests__/index.node.js
Normal file
@@ -0,0 +1,387 @@
|
||||
/**
|
||||
* Copyright 2018-present Facebook.
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
* @format
|
||||
*/
|
||||
|
||||
import styled, {buildKeyframes, flush, gc, tracker} from '../index.js';
|
||||
|
||||
const ReactTestRenderer = require('react-test-renderer');
|
||||
const invariant = require('invariant');
|
||||
const React = require('react'); // eslint-disable-line
|
||||
|
||||
const BasicComponent = styled.view({
|
||||
color: 'red',
|
||||
});
|
||||
|
||||
const DynamicComponent = styled.view({
|
||||
color: props => props.color,
|
||||
});
|
||||
|
||||
test('can create a basic component without any errors', () => {
|
||||
let component;
|
||||
|
||||
try {
|
||||
component = ReactTestRenderer.create(<BasicComponent />);
|
||||
component.toJSON();
|
||||
component.unmount();
|
||||
gc.flush();
|
||||
} finally {
|
||||
if (component) {
|
||||
component.unmount();
|
||||
}
|
||||
gc.flush();
|
||||
}
|
||||
});
|
||||
|
||||
test('can create a basic component and garbage collect', () => {
|
||||
let component;
|
||||
|
||||
try {
|
||||
component = ReactTestRenderer.create(<BasicComponent />);
|
||||
const tree = component.toJSON();
|
||||
|
||||
expect(tree.type).toBe('div');
|
||||
|
||||
const className = tree.props.className;
|
||||
expect(gc.hasQueuedCollection()).toBe(false);
|
||||
expect(gc.getReferenceCount(className)).toBe(1);
|
||||
|
||||
component.unmount();
|
||||
expect(gc.getReferenceCount(className)).toBe(0);
|
||||
expect(gc.hasQueuedCollection()).toBe(true);
|
||||
} finally {
|
||||
if (component) {
|
||||
component.unmount();
|
||||
}
|
||||
gc.flush();
|
||||
}
|
||||
});
|
||||
|
||||
test('remove outdated classes when updating component', () => {
|
||||
let component;
|
||||
|
||||
try {
|
||||
component = ReactTestRenderer.create(<DynamicComponent color="red" />);
|
||||
const tree = component.toJSON();
|
||||
|
||||
const className = tree.props.className;
|
||||
expect(gc.hasQueuedCollection()).toBe(false);
|
||||
expect(gc.getReferenceCount(className)).toBe(1);
|
||||
|
||||
// updating with the same props should generate the same style and not trigger a collection
|
||||
component.update(<DynamicComponent color="red" />);
|
||||
expect(gc.hasQueuedCollection()).toBe(false);
|
||||
expect(gc.getReferenceCount(className)).toBe(1);
|
||||
|
||||
// change style
|
||||
component.update(<DynamicComponent color="blue" />);
|
||||
expect(gc.hasQueuedCollection()).toBe(true);
|
||||
expect(gc.getReferenceCount(className)).toBe(0);
|
||||
} finally {
|
||||
if (component) {
|
||||
component.unmount();
|
||||
}
|
||||
gc.flush();
|
||||
}
|
||||
});
|
||||
|
||||
test('extra class names should be preserved', () => {
|
||||
let component;
|
||||
|
||||
try {
|
||||
component = ReactTestRenderer.create(<BasicComponent className="foo" />);
|
||||
const tree = component.toJSON();
|
||||
expect(tree.props.className.split(' ').includes('foo')).toBe(true);
|
||||
} finally {
|
||||
if (component) {
|
||||
component.unmount();
|
||||
}
|
||||
gc.flush();
|
||||
}
|
||||
});
|
||||
|
||||
test('should inherit component when passed as first arg to styled', () => {
|
||||
let component;
|
||||
|
||||
try {
|
||||
const InheritComponent = BasicComponent.extends({
|
||||
backgroundColor: 'black',
|
||||
});
|
||||
|
||||
component = ReactTestRenderer.create(<InheritComponent />);
|
||||
const tree = component.toJSON();
|
||||
|
||||
const rules = tracker.get(tree.props.className);
|
||||
invariant(rules, 'expected rules');
|
||||
expect(rules.style).toEqual({
|
||||
'background-color': 'black',
|
||||
color: 'red',
|
||||
});
|
||||
} finally {
|
||||
if (component) {
|
||||
component.unmount();
|
||||
}
|
||||
gc.flush();
|
||||
}
|
||||
});
|
||||
|
||||
test("when passed class name of another styled component it's rules should be inherited", () => {
|
||||
let component;
|
||||
|
||||
try {
|
||||
class BaseComponent extends styled.StylableComponent<{
|
||||
className: string,
|
||||
}> {
|
||||
render() {
|
||||
return <BasicComponent className={this.props.className} />;
|
||||
}
|
||||
}
|
||||
|
||||
const InheritComponent = BaseComponent.extends({
|
||||
backgroundColor: 'black',
|
||||
});
|
||||
|
||||
component = ReactTestRenderer.create(<InheritComponent />);
|
||||
const tree = component.toJSON();
|
||||
|
||||
const rules = tracker.get(tree.props.className);
|
||||
invariant(rules, 'expected rules');
|
||||
expect(rules.style).toEqual({
|
||||
'background-color': 'black',
|
||||
color: 'red',
|
||||
});
|
||||
} finally {
|
||||
if (component) {
|
||||
component.unmount();
|
||||
}
|
||||
gc.flush();
|
||||
}
|
||||
});
|
||||
|
||||
test('supports pseudo selectors', () => {
|
||||
let component;
|
||||
|
||||
try {
|
||||
const Component = styled.view({
|
||||
'&:hover': {
|
||||
color: 'red',
|
||||
},
|
||||
});
|
||||
|
||||
component = ReactTestRenderer.create(<Component />);
|
||||
const tree = component.toJSON();
|
||||
|
||||
const rules = tracker.get(tree.props.className);
|
||||
invariant(rules, 'expected rules');
|
||||
expect(rules.style).toEqual({
|
||||
color: 'red',
|
||||
});
|
||||
} finally {
|
||||
if (component) {
|
||||
component.unmount();
|
||||
}
|
||||
gc.flush();
|
||||
}
|
||||
});
|
||||
|
||||
test('supports multiple pseudo selectors', () => {
|
||||
let component;
|
||||
|
||||
try {
|
||||
const Component = styled.view({
|
||||
'&:active': {
|
||||
color: 'blue',
|
||||
},
|
||||
|
||||
'&:hover': {
|
||||
color: 'red',
|
||||
},
|
||||
});
|
||||
|
||||
component = ReactTestRenderer.create(<Component />);
|
||||
const tree = component.toJSON();
|
||||
|
||||
const classes = tree.props.className.split(' ');
|
||||
expect(classes.length).toBe(2);
|
||||
|
||||
const hoverRules = tracker.get(classes[1]);
|
||||
invariant(hoverRules, 'expected hoverRules');
|
||||
expect(hoverRules.style).toEqual({
|
||||
color: 'red',
|
||||
});
|
||||
expect(hoverRules.namespace).toBe('&:hover');
|
||||
expect(hoverRules.selector.endsWith(':hover')).toBe(true);
|
||||
|
||||
const activeRules = tracker.get(classes[0]);
|
||||
invariant(activeRules, 'expected activeRules');
|
||||
expect(activeRules.style).toEqual({
|
||||
color: 'blue',
|
||||
});
|
||||
expect(activeRules.namespace).toBe('&:active');
|
||||
expect(activeRules.selector.endsWith(':active')).toBe(true);
|
||||
} finally {
|
||||
if (component) {
|
||||
component.unmount();
|
||||
}
|
||||
gc.flush();
|
||||
}
|
||||
});
|
||||
|
||||
test('supports child selectors', () => {
|
||||
let component;
|
||||
|
||||
try {
|
||||
const Component = styled.view({
|
||||
'> li': {
|
||||
color: 'red',
|
||||
},
|
||||
});
|
||||
|
||||
component = ReactTestRenderer.create(<Component />);
|
||||
const tree = component.toJSON();
|
||||
|
||||
const classes = tree.props.className.split(' ');
|
||||
expect(classes.length).toBe(1);
|
||||
|
||||
const rules = tracker.get(classes[0]);
|
||||
invariant(rules, 'expected rules');
|
||||
|
||||
expect(rules.style).toEqual({
|
||||
color: 'red',
|
||||
});
|
||||
expect(rules.namespace).toBe('> li');
|
||||
expect(rules.selector.endsWith(' > li')).toBe(true);
|
||||
} finally {
|
||||
if (component) {
|
||||
component.unmount();
|
||||
}
|
||||
gc.flush();
|
||||
}
|
||||
});
|
||||
|
||||
test('flush', () => {
|
||||
flush();
|
||||
});
|
||||
|
||||
test('innerRef works on styled components', () => {
|
||||
let component;
|
||||
|
||||
try {
|
||||
const Component = styled.view({});
|
||||
|
||||
let called = false;
|
||||
const innerRef = ref => {
|
||||
called = true;
|
||||
};
|
||||
ReactTestRenderer.create(<Component innerRef={innerRef} />);
|
||||
expect(called).toBe(true);
|
||||
} finally {
|
||||
if (component) {
|
||||
component.unmount();
|
||||
}
|
||||
gc.flush();
|
||||
}
|
||||
});
|
||||
|
||||
test('ignoreAttributes', () => {
|
||||
let component;
|
||||
|
||||
try {
|
||||
const Component = styled.view(
|
||||
{
|
||||
color: props => props.color,
|
||||
},
|
||||
{
|
||||
ignoreAttributes: ['color'],
|
||||
},
|
||||
);
|
||||
|
||||
component = ReactTestRenderer.create(<Component color="red" />);
|
||||
const tree = component.toJSON();
|
||||
|
||||
expect(tree.props.color).toBe(undefined);
|
||||
|
||||
const rules = tracker.get(tree.props.className);
|
||||
invariant(rules, 'expected rules');
|
||||
expect(rules.style).toEqual({
|
||||
color: 'red',
|
||||
});
|
||||
} finally {
|
||||
if (component) {
|
||||
component.unmount();
|
||||
}
|
||||
gc.flush();
|
||||
}
|
||||
});
|
||||
test('buildKeyframes', () => {
|
||||
const css = buildKeyframes({
|
||||
'0%': {
|
||||
opacity: 0,
|
||||
},
|
||||
|
||||
'50%': {
|
||||
height: 50,
|
||||
opacity: 0.8,
|
||||
},
|
||||
|
||||
'100%': {
|
||||
opacity: 1,
|
||||
},
|
||||
});
|
||||
|
||||
expect(css).toBe(
|
||||
[
|
||||
' 0% {',
|
||||
' opacity: 0;',
|
||||
' }',
|
||||
' 50% {',
|
||||
' height: 50px;',
|
||||
' opacity: 0.8;',
|
||||
' }',
|
||||
' 100% {',
|
||||
' opacity: 1;',
|
||||
' }',
|
||||
].join('\n'),
|
||||
);
|
||||
});
|
||||
|
||||
test('keyframes', () => {
|
||||
const className = styled.keyframes({
|
||||
'0%': {
|
||||
opacity: 0,
|
||||
},
|
||||
|
||||
'50%': {
|
||||
opacity: 0.8,
|
||||
},
|
||||
|
||||
'100%': {
|
||||
opacity: 1,
|
||||
},
|
||||
});
|
||||
expect(typeof className).toBe('string');
|
||||
});
|
||||
|
||||
test('buildKeyframes only accepts string property values', () => {
|
||||
expect(() => {
|
||||
buildKeyframes({
|
||||
// $FlowFixMe: ignore
|
||||
'0%': {
|
||||
fn: () => {},
|
||||
},
|
||||
});
|
||||
}).toThrow('Keyframe objects must only have strings values');
|
||||
});
|
||||
|
||||
test('buildKeyframes only accepts object specs', () => {
|
||||
expect(() => {
|
||||
buildKeyframes({
|
||||
// $FlowFixMe: ignore
|
||||
'0%': () => {
|
||||
return '';
|
||||
},
|
||||
});
|
||||
}).toThrow('Keyframe spec must only have objects');
|
||||
});
|
||||
76
src/ui/styled/__tests__/rules.node.js
Normal file
76
src/ui/styled/__tests__/rules.node.js
Normal file
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* Copyright 2018-present Facebook.
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
* @format
|
||||
*/
|
||||
|
||||
import {buildRules, normaliseRules} from '../rules.js';
|
||||
|
||||
describe('normaliseRules', () => {
|
||||
test('ensure top level values are expanded', () => {
|
||||
const normalisedRules = normaliseRules({height: '4px'});
|
||||
expect(normalisedRules['&'].height).toBe('4px');
|
||||
});
|
||||
|
||||
test('ensure keys are dashed', () => {
|
||||
const normalisedRules = normaliseRules({
|
||||
// $FlowFixMe: ignore
|
||||
'&:hover': {
|
||||
lineHeight: '4px',
|
||||
WebkitAppRegion: 'drag',
|
||||
},
|
||||
});
|
||||
const hoverRules = normalisedRules['&:hover'];
|
||||
expect(Object.keys(hoverRules).length).toBe(2);
|
||||
expect(hoverRules['line-height']).toBe('4px');
|
||||
expect(hoverRules['-webkit-app-region']).toBe('drag');
|
||||
});
|
||||
|
||||
test('exclude empty objects', () => {
|
||||
const normalisedRules = normaliseRules({
|
||||
'&:hover': {},
|
||||
});
|
||||
|
||||
expect(normalisedRules['&:hover']).toBe(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildRules', () => {
|
||||
test('ensure null values are left out', () => {
|
||||
const builtRules = buildRules({height: (null: any)}, {}, {});
|
||||
expect('height' in builtRules).toBe(false);
|
||||
|
||||
const builtRules2 = buildRules(
|
||||
{
|
||||
height() {
|
||||
return (null: any);
|
||||
},
|
||||
},
|
||||
{},
|
||||
{},
|
||||
);
|
||||
expect('height' in builtRules2).toBe(false);
|
||||
});
|
||||
|
||||
test('ensure numbers are appended with px', () => {
|
||||
expect(buildRules({height: 40}, {}, {}).height).toBe('40px');
|
||||
});
|
||||
|
||||
test("ensure unitless numbers aren't appended with px", () => {
|
||||
expect(buildRules({'z-index': 4}, {}, {})['z-index']).toBe('4');
|
||||
});
|
||||
|
||||
test('ensure functions are called with props', () => {
|
||||
const thisProps = {};
|
||||
expect(
|
||||
buildRules(
|
||||
{
|
||||
border: props => (props === thisProps ? 'foo' : 'bar'),
|
||||
},
|
||||
thisProps,
|
||||
{},
|
||||
).border,
|
||||
).toBe('foo');
|
||||
});
|
||||
});
|
||||
74
src/ui/styled/__tests__/sheet.node.js
Normal file
74
src/ui/styled/__tests__/sheet.node.js
Normal file
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* Copyright 2018-present Facebook.
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
* @format
|
||||
*/
|
||||
|
||||
import {StyleSheet} from '../sheet.js';
|
||||
|
||||
describe('flush', () => {
|
||||
test('should remove all rules', () => {
|
||||
const sheet = new StyleSheet();
|
||||
expect(sheet.getRuleCount()).toBe(0);
|
||||
|
||||
sheet.insert('foo', 'div {color: red;}');
|
||||
expect(sheet.getRuleCount()).toBe(1);
|
||||
|
||||
sheet.flush();
|
||||
expect(sheet.getRuleCount()).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('inject', () => {
|
||||
test("throw's an error when already injected", () => {
|
||||
const sheet = new StyleSheet();
|
||||
|
||||
expect(() => {
|
||||
sheet.inject();
|
||||
sheet.inject();
|
||||
}).toThrow('already injected stylesheet!');
|
||||
});
|
||||
});
|
||||
|
||||
describe('insert', () => {
|
||||
test('non-speedy', () => {
|
||||
const sheet = new StyleSheet();
|
||||
|
||||
expect(sheet.getRuleCount()).toBe(0);
|
||||
sheet.insert('foo', 'div {color: red;}');
|
||||
expect(sheet.getRuleCount()).toBe(1);
|
||||
});
|
||||
|
||||
test('speedy', () => {
|
||||
const sheet = new StyleSheet(true);
|
||||
|
||||
expect(sheet.getRuleCount()).toBe(0);
|
||||
sheet.insert('foo', 'div {color: red;}');
|
||||
expect(sheet.getRuleCount()).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete', () => {
|
||||
test('non-speedy', () => {
|
||||
const sheet = new StyleSheet();
|
||||
|
||||
expect(sheet.getRuleCount()).toBe(0);
|
||||
sheet.insert('foo', 'div {color: red;}');
|
||||
expect(sheet.getRuleCount()).toBe(1);
|
||||
|
||||
sheet.delete('foo');
|
||||
expect(sheet.getRuleCount()).toBe(0);
|
||||
});
|
||||
|
||||
test('speedy', () => {
|
||||
const sheet = new StyleSheet(true);
|
||||
|
||||
expect(sheet.getRuleCount()).toBe(0);
|
||||
sheet.insert('foo', 'div {color: red;}');
|
||||
expect(sheet.getRuleCount()).toBe(1);
|
||||
|
||||
sheet.delete('foo');
|
||||
expect(sheet.getRuleCount()).toBe(0);
|
||||
});
|
||||
});
|
||||
114
src/ui/styled/gc.js
Normal file
114
src/ui/styled/gc.js
Normal file
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* Copyright 2018-present Facebook.
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
* @format
|
||||
*/
|
||||
|
||||
import type {Tracker, RulesToClass} from './index.js';
|
||||
import type {StyleSheet} from './sheet.js';
|
||||
|
||||
const invariant = require('invariant');
|
||||
|
||||
export class GarbageCollector {
|
||||
constructor(sheet: StyleSheet, tracker: Tracker, rulesToClass: RulesToClass) {
|
||||
this.sheet = sheet;
|
||||
this.tracker = tracker;
|
||||
|
||||
// used to keep track of what classes are actively in use
|
||||
this.usedClasses = new Map();
|
||||
|
||||
// classes to be removed, we put this in a queue and perform it in bulk rather than straight away
|
||||
// since by the time the next tick happens this style could have been reinserted
|
||||
this.classRemovalQueue = new Set();
|
||||
|
||||
this.rulesToClass = rulesToClass;
|
||||
}
|
||||
|
||||
tracker: Tracker;
|
||||
sheet: StyleSheet;
|
||||
usedClasses: Map<string, number>;
|
||||
garbageTimer: ?TimeoutID;
|
||||
classRemovalQueue: Set<string>;
|
||||
rulesToClass: RulesToClass;
|
||||
|
||||
hasQueuedCollection(): boolean {
|
||||
return Boolean(this.garbageTimer);
|
||||
}
|
||||
|
||||
getReferenceCount(key: string): number {
|
||||
return this.usedClasses.get(key) || 0;
|
||||
}
|
||||
|
||||
// component has been mounted so make sure it's being depended on
|
||||
registerClassUse(name: string) {
|
||||
const count = this.usedClasses.get(name) || 0;
|
||||
this.usedClasses.set(name, count + 1);
|
||||
if (this.classRemovalQueue.has(name)) {
|
||||
this.classRemovalQueue.delete(name);
|
||||
|
||||
if (this.classRemovalQueue.size === 0) {
|
||||
this.haltGarbage();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// component has been unmounted so remove it's dependencies
|
||||
deregisterClassUse(name: string) {
|
||||
let count = this.usedClasses.get(name);
|
||||
if (count == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
count--;
|
||||
this.usedClasses.set(name, count);
|
||||
|
||||
if (count === 0) {
|
||||
this.classRemovalQueue.add(name);
|
||||
this.scheduleGarbage();
|
||||
}
|
||||
}
|
||||
|
||||
scheduleGarbage() {
|
||||
if (this.garbageTimer != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.garbageTimer = setTimeout(() => {
|
||||
this.collectGarbage();
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
haltGarbage() {
|
||||
if (this.garbageTimer) {
|
||||
clearTimeout(this.garbageTimer);
|
||||
this.garbageTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
getCollectionQueue() {
|
||||
return Array.from(this.classRemovalQueue);
|
||||
}
|
||||
|
||||
collectGarbage() {
|
||||
this.haltGarbage();
|
||||
for (const name of this.classRemovalQueue) {
|
||||
const trackerInfo = this.tracker.get(name);
|
||||
invariant(trackerInfo != null, 'trying to remove unknown class');
|
||||
|
||||
const {rules} = trackerInfo;
|
||||
this.rulesToClass.delete(rules);
|
||||
|
||||
this.sheet.delete(name);
|
||||
this.tracker.delete(name);
|
||||
this.usedClasses.delete(name);
|
||||
}
|
||||
this.classRemovalQueue.clear();
|
||||
}
|
||||
|
||||
flush() {
|
||||
this.haltGarbage();
|
||||
this.classRemovalQueue.clear();
|
||||
this.usedClasses.clear();
|
||||
}
|
||||
}
|
||||
74
src/ui/styled/hash.js
Normal file
74
src/ui/styled/hash.js
Normal file
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* Copyright 2018-present Facebook.
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
* @format
|
||||
*/
|
||||
|
||||
export default function hash(str: string): string {
|
||||
const m = 0x5bd1e995;
|
||||
const r = 24;
|
||||
let h = str.length;
|
||||
let length = str.length;
|
||||
let currentIndex = 0;
|
||||
|
||||
while (length >= 4) {
|
||||
let k = UInt32(str, currentIndex);
|
||||
|
||||
k = Umul32(k, m);
|
||||
k ^= k >>> r;
|
||||
k = Umul32(k, m);
|
||||
|
||||
h = Umul32(h, m);
|
||||
h ^= k;
|
||||
|
||||
currentIndex += 4;
|
||||
length -= 4;
|
||||
}
|
||||
|
||||
switch (length) {
|
||||
case 3:
|
||||
h ^= UInt16(str, currentIndex);
|
||||
h ^= str.charCodeAt(currentIndex + 2) << 16;
|
||||
h = Umul32(h, m);
|
||||
break;
|
||||
|
||||
case 2:
|
||||
h ^= UInt16(str, currentIndex);
|
||||
h = Umul32(h, m);
|
||||
break;
|
||||
|
||||
case 1:
|
||||
h ^= str.charCodeAt(currentIndex);
|
||||
h = Umul32(h, m);
|
||||
break;
|
||||
}
|
||||
|
||||
h ^= h >>> 13;
|
||||
h = Umul32(h, m);
|
||||
h ^= h >>> 15;
|
||||
|
||||
return (h >>> 0).toString(36);
|
||||
}
|
||||
|
||||
function UInt32(str: string, pos: number): number {
|
||||
return (
|
||||
str.charCodeAt(pos++) +
|
||||
(str.charCodeAt(pos++) << 8) +
|
||||
(str.charCodeAt(pos++) << 16) +
|
||||
(str.charCodeAt(pos) << 24)
|
||||
);
|
||||
}
|
||||
|
||||
function UInt16(str: string, pos: number): number {
|
||||
return str.charCodeAt(pos++) + (str.charCodeAt(pos++) << 8);
|
||||
}
|
||||
|
||||
function Umul32(n: number, m: number): number {
|
||||
n |= 0;
|
||||
m |= 0;
|
||||
const nlo = n & 0xffff;
|
||||
const nhi = n >>> 16;
|
||||
const res = (nlo * m + (((nhi * m) & 0xffff) << 16)) | 0;
|
||||
return res;
|
||||
}
|
||||
440
src/ui/styled/index.js
Normal file
440
src/ui/styled/index.js
Normal file
@@ -0,0 +1,440 @@
|
||||
/**
|
||||
* Copyright 2018-present Facebook.
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
* @format
|
||||
*/
|
||||
|
||||
import type {BaseRules, KeyframeRules, RawRules} from './rules.js';
|
||||
import {buildKeyframeRules, buildRules, normaliseRules} from './rules.js';
|
||||
import assignDeep from '../../utils/assignDeep.js';
|
||||
import * as performance from '../../utils/performance.js';
|
||||
import {GarbageCollector} from './gc.js';
|
||||
import {StyleSheet} from './sheet.js';
|
||||
import hash from './hash.js';
|
||||
|
||||
const React = require('react');
|
||||
|
||||
export type Tracker = Map<
|
||||
string,
|
||||
{
|
||||
displayName: ?string,
|
||||
namespace: string,
|
||||
rules: BaseRules,
|
||||
selector: string,
|
||||
style: Object,
|
||||
},
|
||||
>;
|
||||
|
||||
export type RulesToClass = WeakMap<BaseRules, string>;
|
||||
|
||||
// map of inserted classes and metadata about them
|
||||
export const tracker: Tracker = new Map();
|
||||
|
||||
// map of rules to their class
|
||||
const rulesToClass: RulesToClass = new WeakMap();
|
||||
|
||||
export const sheet = new StyleSheet(process.env.NODE_ENV === 'production');
|
||||
export const gc = new GarbageCollector(sheet, tracker, rulesToClass);
|
||||
|
||||
function addRules(
|
||||
displayName: string,
|
||||
rules: BaseRules,
|
||||
namespace,
|
||||
props: Object,
|
||||
context: Object,
|
||||
) {
|
||||
// if these rules have been cached to a className then retrieve it
|
||||
const cachedClass = rulesToClass.get(rules);
|
||||
if (cachedClass != null) {
|
||||
return cachedClass;
|
||||
}
|
||||
|
||||
//
|
||||
const declarations = [];
|
||||
const style = buildRules(rules, props, context);
|
||||
|
||||
// generate css declarations based on the style object
|
||||
for (const key in style) {
|
||||
const val = style[key];
|
||||
declarations.push(` ${key}: ${val};`);
|
||||
}
|
||||
const css = declarations.join('\n');
|
||||
|
||||
// build the class name with the display name of the styled component and a unique id based on the css and namespace
|
||||
const className = displayName + '__' + hash(namespace + css);
|
||||
|
||||
// this is the first time we've found this className
|
||||
if (!tracker.has(className)) {
|
||||
// build up the correct selector, explode on commas to allow multiple selectors
|
||||
const selector = namespace
|
||||
.split(', ')
|
||||
.map(part => {
|
||||
if (part[0] === '&') {
|
||||
return '.' + className + part.slice(1);
|
||||
} else {
|
||||
return '.' + className + ' ' + part;
|
||||
}
|
||||
})
|
||||
.join(', ');
|
||||
|
||||
// insert the new style text
|
||||
tracker.set(className, {displayName, namespace, rules, selector, style});
|
||||
sheet.insert(className, `${selector} {\n${css}\n}`);
|
||||
|
||||
// if there's no dynamic rules then cache this
|
||||
if (hasDynamicRules(rules) === false) {
|
||||
rulesToClass.set(rules, className);
|
||||
}
|
||||
}
|
||||
|
||||
return className;
|
||||
}
|
||||
|
||||
// remove all styhles
|
||||
export function flush() {
|
||||
gc.flush();
|
||||
tracker.clear();
|
||||
sheet.flush();
|
||||
}
|
||||
|
||||
export type TagName = string | Function;
|
||||
|
||||
type StyledComponentState = {|
|
||||
extraClassNames: Array<string>,
|
||||
classNames: Array<string>,
|
||||
lastBuiltRules: ?Object,
|
||||
lastBuiltRulesIsDynamic: boolean,
|
||||
|};
|
||||
|
||||
export class StylableComponent<
|
||||
Props = void,
|
||||
State = void,
|
||||
> extends React.Component<Props, State> {
|
||||
static extends(
|
||||
rules: RawRules,
|
||||
opts?: StyledComponentOpts,
|
||||
): StyledComponent<any> {
|
||||
return createStyledComponent(this, rules, opts);
|
||||
}
|
||||
}
|
||||
|
||||
class StylablePureComponent<
|
||||
Props = void,
|
||||
State = void,
|
||||
> extends React.PureComponent<Props, State> {
|
||||
static extends(
|
||||
rules: RawRules,
|
||||
opts?: StyledComponentOpts,
|
||||
): StyledComponent<any> {
|
||||
return createStyledComponent(this, rules, opts);
|
||||
}
|
||||
}
|
||||
|
||||
class StyledComponentBase<Props> extends React.PureComponent<
|
||||
Props,
|
||||
StyledComponentState,
|
||||
> {
|
||||
constructor(props: Props, context: Object): void {
|
||||
super(props, context);
|
||||
this.state = {
|
||||
classNames: [],
|
||||
extraClassNames: [],
|
||||
lastBuiltRulesIsDynamic: false,
|
||||
lastBuiltRules: null,
|
||||
};
|
||||
}
|
||||
|
||||
static defaultProps: ?$Shape<Props>;
|
||||
|
||||
static STYLED_CONFIG: {|
|
||||
tagName: TagName,
|
||||
ignoreAttributes: ?Array<string>,
|
||||
builtRules: any,
|
||||
|};
|
||||
|
||||
static extends(
|
||||
rules: RawRules,
|
||||
opts?: StyledComponentOpts,
|
||||
): StyledComponent<any> {
|
||||
return createStyledComponent(this, rules, opts);
|
||||
}
|
||||
|
||||
componentWillMount(): void {
|
||||
this.generateClassnames(this.props, null);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps: Props): void {
|
||||
this.generateClassnames(nextProps, this.props);
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
for (const name of this.state.classNames) {
|
||||
gc.deregisterClassUse(name);
|
||||
}
|
||||
}
|
||||
|
||||
generateClassnames(props: Props, prevProps: ?Props): void {
|
||||
throw new Error('unimplemented');
|
||||
}
|
||||
}
|
||||
|
||||
function hasDynamicRules(rules: Object): boolean {
|
||||
for (const key in rules) {
|
||||
const val = rules[key];
|
||||
|
||||
if (typeof val === 'function') {
|
||||
return true;
|
||||
} else if (typeof val === 'object' && hasDynamicRules(val)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function hasEquivProps(props: Object, nextProps: Object): boolean {
|
||||
// check if the props are equivalent
|
||||
for (const key in props) {
|
||||
// ignore `children` since we do that check later
|
||||
if (key === 'children') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// check strict equality of prop value
|
||||
if (nextProps[key] !== props[key]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// check if nextProps has any values that props doesn't
|
||||
for (const key in nextProps) {
|
||||
if (!(key in props)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// check if the boolean equality of children is equivalent
|
||||
if (Boolean(props.children) !== Boolean(nextProps.children)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export type StyledComponent<Props> = Class<StyledComponentBase<Props>>;
|
||||
|
||||
type StyledComponentOpts = {
|
||||
displayName?: string,
|
||||
contextTypes?: Object,
|
||||
ignoreAttributes?: Array<string>,
|
||||
};
|
||||
|
||||
function createStyledComponent(
|
||||
tagName: TagName,
|
||||
rules: RawRules,
|
||||
opts?: StyledComponentOpts = {},
|
||||
): StyledComponent<any> {
|
||||
let {contextTypes = {}, ignoreAttributes} = opts;
|
||||
|
||||
// build up rules
|
||||
let builtRules = normaliseRules(rules);
|
||||
|
||||
// if inheriting from another styled component then take all of it's properties
|
||||
if (typeof tagName === 'function' && tagName.STYLED_CONFIG) {
|
||||
// inherit context types
|
||||
if (tagName.contextTypes) {
|
||||
contextTypes = {...contextTypes, ...tagName.contextTypes};
|
||||
}
|
||||
|
||||
const parentConfig = tagName.STYLED_CONFIG;
|
||||
|
||||
// inherit tagname
|
||||
tagName = parentConfig.tagName;
|
||||
|
||||
// inherit ignoreAttributes
|
||||
if (parentConfig.ignoreAttributes) {
|
||||
if (ignoreAttributes) {
|
||||
ignoreAttributes = ignoreAttributes.concat(
|
||||
parentConfig.ignoreAttributes,
|
||||
);
|
||||
} else {
|
||||
ignoreAttributes = parentConfig.ignoreAttributes;
|
||||
}
|
||||
}
|
||||
|
||||
// inherit rules
|
||||
builtRules = assignDeep({}, parentConfig.builtRules, builtRules);
|
||||
}
|
||||
|
||||
const displayName: string =
|
||||
opts.displayName == null ? 'StyledComponent' : opts.displayName;
|
||||
const isDOM = typeof tagName === 'string';
|
||||
|
||||
class Constructor<Props: Object> extends StyledComponentBase<Props> {
|
||||
generateClassnames(props: Props, prevProps: ?Props) {
|
||||
// if this is a secondary render then check if the props are essentially equivalent
|
||||
// NOTE: hasEquivProps is not a standard shallow equality test
|
||||
if (prevProps != null && hasEquivProps(props, prevProps)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const debugId = performance.mark();
|
||||
const extraClassNames = [];
|
||||
|
||||
let myBuiltRules = builtRules;
|
||||
|
||||
// if passed any classes from another styled component, ignore that class and merge in their
|
||||
// resolved styles
|
||||
if (props.className) {
|
||||
const propClassNames = props.className.trim().split(/[\s]+/g);
|
||||
for (const className of propClassNames) {
|
||||
const classInfo = tracker.get(className);
|
||||
if (classInfo) {
|
||||
const {namespace, style} = classInfo;
|
||||
myBuiltRules = assignDeep({}, myBuiltRules, {[namespace]: style});
|
||||
} else {
|
||||
extraClassNames.push(className);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if we had the exact same rules as last time and they weren't dynamic then we can bail out here
|
||||
if (
|
||||
myBuiltRules !== this.state.lastBuiltRules ||
|
||||
this.state.lastBuiltRulesIsDynamic !== false
|
||||
) {
|
||||
const prevClasses = this.state.classNames;
|
||||
const classNames = [];
|
||||
|
||||
// add rules
|
||||
for (const namespace in myBuiltRules) {
|
||||
const className = addRules(
|
||||
displayName,
|
||||
myBuiltRules[namespace],
|
||||
namespace,
|
||||
props,
|
||||
this.context,
|
||||
);
|
||||
classNames.push(className);
|
||||
|
||||
// if this is the first mount render or we didn't previously have this class then add it as new
|
||||
if (prevProps == null || !prevClasses.includes(className)) {
|
||||
gc.registerClassUse(className);
|
||||
}
|
||||
}
|
||||
|
||||
// check what classNames have been removed if this is a secondary render
|
||||
if (prevProps != null) {
|
||||
for (const className of prevClasses) {
|
||||
// if this previous class isn't in the current classes then deregister it
|
||||
if (!classNames.includes(className)) {
|
||||
gc.deregisterClassUse(className);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
classNames,
|
||||
lastBuiltRules: myBuiltRules,
|
||||
lastBuiltRulesIsDynamic: hasDynamicRules(myBuiltRules),
|
||||
extraClassNames,
|
||||
});
|
||||
}
|
||||
|
||||
performance.measure(
|
||||
debugId,
|
||||
`🚀 ${this.constructor.name} [style calculate]`,
|
||||
);
|
||||
}
|
||||
render() {
|
||||
const {children, innerRef, ...props} = this.props;
|
||||
|
||||
if (ignoreAttributes) {
|
||||
for (const key of ignoreAttributes) {
|
||||
delete props[key];
|
||||
}
|
||||
}
|
||||
// build class names
|
||||
const className = this.state.classNames
|
||||
.concat(this.state.extraClassNames)
|
||||
.join(' ');
|
||||
if (props.is) {
|
||||
props.class = className;
|
||||
} else {
|
||||
props.className = className;
|
||||
}
|
||||
//
|
||||
if (innerRef) {
|
||||
if (isDOM) {
|
||||
// dom ref
|
||||
props.ref = innerRef;
|
||||
} else {
|
||||
// probably another styled component so pass it down
|
||||
props.innerRef = innerRef;
|
||||
}
|
||||
}
|
||||
return React.createElement(tagName, props, children);
|
||||
}
|
||||
}
|
||||
Constructor.STYLED_CONFIG = {
|
||||
builtRules,
|
||||
ignoreAttributes,
|
||||
tagName,
|
||||
};
|
||||
|
||||
Constructor.contextTypes = {
|
||||
...contextTypes,
|
||||
};
|
||||
|
||||
Object.defineProperty(Constructor, 'name', {
|
||||
value: displayName,
|
||||
});
|
||||
|
||||
return Constructor;
|
||||
}
|
||||
export function buildKeyframes(spec: KeyframeRules) {
|
||||
let css = [];
|
||||
|
||||
const builtRules = buildKeyframeRules(spec);
|
||||
for (const key in builtRules) {
|
||||
const declarations = [];
|
||||
const rules = builtRules[key];
|
||||
|
||||
for (const key in rules) {
|
||||
declarations.push(` ${key}: ${String(rules[key])};`);
|
||||
}
|
||||
css.push(` ${key} {`);
|
||||
css = css.concat(declarations);
|
||||
css.push(' }');
|
||||
}
|
||||
css = css.join('\n');
|
||||
return css;
|
||||
}
|
||||
function createKeyframes(spec: KeyframeRules): string {
|
||||
const body = buildKeyframes(spec);
|
||||
const className = `animation-${hash(body)}`;
|
||||
|
||||
const css = `@keyframes ${className} {\n${body}\n}`;
|
||||
sheet.insert(className, css);
|
||||
return className;
|
||||
}
|
||||
type StyledComponentFactory = (
|
||||
rules: RawRules,
|
||||
opts?: StyledComponentOpts,
|
||||
) => StyledComponent<any>;
|
||||
|
||||
function createStyledComponentFactory(tagName: string): StyledComponentFactory {
|
||||
return (rules: RawRules, opts?: StyledComponentOpts) => {
|
||||
return createStyledComponent(tagName, rules, opts);
|
||||
};
|
||||
}
|
||||
export default {
|
||||
image: createStyledComponentFactory('img'),
|
||||
view: createStyledComponentFactory('div'),
|
||||
text: createStyledComponentFactory('span'),
|
||||
textInput: createStyledComponentFactory('input'),
|
||||
customHTMLTag: createStyledComponent,
|
||||
keyframes: createKeyframes,
|
||||
StylableComponent,
|
||||
StylablePureComponent,
|
||||
};
|
||||
180
src/ui/styled/rules.js
Normal file
180
src/ui/styled/rules.js
Normal file
@@ -0,0 +1,180 @@
|
||||
/**
|
||||
* Copyright 2018-present Facebook.
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
* @format
|
||||
*/
|
||||
|
||||
import type {CSSPropertySet, CSSPropertyValue} from './types.js';
|
||||
|
||||
const dashify = require('dashify');
|
||||
|
||||
export type NormalisedRules = {
|
||||
[namespace: string]: BaseRules,
|
||||
};
|
||||
|
||||
export type BaseRules = {
|
||||
[key: string]: CSSPropertyValue<string | number>,
|
||||
};
|
||||
|
||||
export type PlainRules = {
|
||||
[key: string]: string,
|
||||
};
|
||||
|
||||
export type NormalisedKeyframeRules = {
|
||||
[key: string]: PlainRules,
|
||||
};
|
||||
|
||||
export type KeyframeRules = {
|
||||
[key: string]: CSSPropertySet,
|
||||
};
|
||||
|
||||
export type RawRules = {
|
||||
...CSSPropertySet,
|
||||
[key: string]: CSSPropertySet,
|
||||
};
|
||||
|
||||
const unitlessNumberProperties = new Set([
|
||||
'animation-iteration-count',
|
||||
'border-image-outset',
|
||||
'border-image-slice',
|
||||
'border-image-width',
|
||||
'column-count',
|
||||
'flex',
|
||||
'flex-grow',
|
||||
'flex-positive',
|
||||
'flex-shrink',
|
||||
'flex-order',
|
||||
'grid-row',
|
||||
'grid-column',
|
||||
'font-weight',
|
||||
'line-clamp',
|
||||
'line-height',
|
||||
'opacity',
|
||||
'order',
|
||||
'orphans',
|
||||
'tab-size',
|
||||
'widows',
|
||||
'z-index',
|
||||
'zoom',
|
||||
'fill-opacity',
|
||||
'flood-opacity',
|
||||
'stop-opacity',
|
||||
'stroke-dasharray',
|
||||
'stroke-dashoffset',
|
||||
'stroke-miterlimit',
|
||||
'stroke-opacity',
|
||||
'stroke-width',
|
||||
]);
|
||||
|
||||
// put top level styles into an '&' object
|
||||
function expandRules(rules: RawRules): NormalisedRules {
|
||||
const expandedRules = {};
|
||||
const rootRules = {};
|
||||
|
||||
for (const key in rules) {
|
||||
const val = rules[key];
|
||||
|
||||
if (typeof val === 'object') {
|
||||
expandedRules[key] = val;
|
||||
} else {
|
||||
rootRules[key] = val;
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(rootRules).length) {
|
||||
expandedRules['&'] = rootRules;
|
||||
}
|
||||
|
||||
return expandedRules;
|
||||
}
|
||||
|
||||
function shouldAppendPixel(key: string, val: mixed): boolean {
|
||||
return (
|
||||
typeof val === 'number' && !unitlessNumberProperties.has(key) && !isNaN(val)
|
||||
);
|
||||
}
|
||||
|
||||
export function normaliseRules(rules: RawRules): NormalisedRules {
|
||||
const expandedRules = expandRules(rules);
|
||||
|
||||
const builtRules = {};
|
||||
|
||||
for (const key in expandedRules) {
|
||||
const rules = expandedRules[key];
|
||||
const myRules = {};
|
||||
|
||||
for (const key in rules) {
|
||||
const val = rules[key];
|
||||
|
||||
let dashedKey = dashify(key);
|
||||
if (/[A-Z]/.test(key[0])) {
|
||||
dashedKey = `-${dashedKey}`;
|
||||
}
|
||||
|
||||
myRules[dashedKey] = val;
|
||||
}
|
||||
|
||||
if (Object.keys(myRules).length) {
|
||||
builtRules[key] = myRules;
|
||||
}
|
||||
}
|
||||
|
||||
return builtRules;
|
||||
}
|
||||
|
||||
export function buildKeyframeRules(
|
||||
rules: KeyframeRules,
|
||||
): NormalisedKeyframeRules {
|
||||
const spec = {};
|
||||
|
||||
for (const selector in rules) {
|
||||
const newRules = {};
|
||||
|
||||
const rules2 = rules[selector];
|
||||
if (!rules2 || typeof rules2 !== 'object') {
|
||||
throw new Error('Keyframe spec must only have objects');
|
||||
}
|
||||
|
||||
for (const key in rules2) {
|
||||
let val = rules2[key];
|
||||
|
||||
if (shouldAppendPixel(key, val)) {
|
||||
val += 'px';
|
||||
} else if (typeof val === 'number') {
|
||||
val = String(val);
|
||||
}
|
||||
|
||||
if (typeof val !== 'string') {
|
||||
throw new Error('Keyframe objects must only have strings values');
|
||||
}
|
||||
|
||||
newRules[key] = val;
|
||||
}
|
||||
|
||||
spec[selector] = newRules;
|
||||
}
|
||||
|
||||
return spec;
|
||||
}
|
||||
|
||||
export function buildRules(
|
||||
rules: BaseRules,
|
||||
props: NormalisedRules,
|
||||
context: Object,
|
||||
): PlainRules {
|
||||
const style = {};
|
||||
for (const key in rules) {
|
||||
let val = rules[key];
|
||||
if (typeof val === 'function') {
|
||||
val = val(props, context);
|
||||
}
|
||||
if (val != null && shouldAppendPixel(key, val)) {
|
||||
val += 'px';
|
||||
}
|
||||
if (val != null) {
|
||||
style[key] = String(val);
|
||||
}
|
||||
}
|
||||
return style;
|
||||
}
|
||||
92
src/ui/styled/sheet.js
Normal file
92
src/ui/styled/sheet.js
Normal file
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* Copyright 2018-present Facebook.
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
* @format
|
||||
*/
|
||||
|
||||
const invariant = require('invariant');
|
||||
|
||||
function makeStyleTag(): HTMLStyleElement {
|
||||
const tag = document.createElement('style');
|
||||
tag.type = 'text/css';
|
||||
tag.appendChild(document.createTextNode(''));
|
||||
|
||||
const {head} = document;
|
||||
invariant(head, 'expected head');
|
||||
head.appendChild(tag);
|
||||
|
||||
return tag;
|
||||
}
|
||||
|
||||
export class StyleSheet {
|
||||
constructor(isSpeedy?: boolean) {
|
||||
this.injected = false;
|
||||
this.isSpeedy = Boolean(isSpeedy);
|
||||
|
||||
this.flush();
|
||||
this.inject();
|
||||
}
|
||||
|
||||
ruleIndexes: Array<string>;
|
||||
injected: boolean;
|
||||
isSpeedy: boolean;
|
||||
tag: HTMLStyleElement;
|
||||
|
||||
getRuleCount(): number {
|
||||
return this.ruleIndexes.length;
|
||||
}
|
||||
|
||||
flush() {
|
||||
this.ruleIndexes = [];
|
||||
if (this.tag) {
|
||||
this.tag.innerHTML = '';
|
||||
}
|
||||
}
|
||||
|
||||
inject() {
|
||||
if (this.injected) {
|
||||
throw new Error('already injected stylesheet!');
|
||||
}
|
||||
|
||||
this.tag = makeStyleTag();
|
||||
this.injected = true;
|
||||
}
|
||||
|
||||
delete(key: string) {
|
||||
const index = this.ruleIndexes.indexOf(key);
|
||||
if (index < 0) {
|
||||
// TODO maybe error
|
||||
return;
|
||||
}
|
||||
|
||||
this.ruleIndexes.splice(index, 1);
|
||||
|
||||
const tag = this.tag;
|
||||
if (this.isSpeedy) {
|
||||
const sheet = tag.sheet;
|
||||
invariant(sheet, 'expected sheet');
|
||||
|
||||
// $FlowFixMe: sheet is actually CSSStylesheet
|
||||
sheet.deleteRule(index);
|
||||
} else {
|
||||
tag.removeChild(tag.childNodes[index + 1]);
|
||||
}
|
||||
}
|
||||
|
||||
insert(key: string, rule: string) {
|
||||
const tag = this.tag;
|
||||
|
||||
if (this.isSpeedy) {
|
||||
const sheet = tag.sheet;
|
||||
invariant(sheet, 'expected sheet');
|
||||
|
||||
// $FlowFixMe: sheet is actually CSSStylesheet
|
||||
sheet.insertRule(rule, sheet.cssRules.length);
|
||||
} else {
|
||||
tag.appendChild(document.createTextNode(rule));
|
||||
}
|
||||
|
||||
this.ruleIndexes.push(key);
|
||||
}
|
||||
}
|
||||
1280
src/ui/styled/types.js
Normal file
1280
src/ui/styled/types.js
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user