Skip to content

Commit 0891f4a

Browse files
giuseppegsimonsmith
authored andcommitted
Add encapsulate feature
Resets CSS properties to their default values to effectively allow a component to opt out of CSS inheritance and be encapsulated from the rest of the application similar to the Shadow DOM. * Add `encapsulate` option to the Node API (false by default) * Add `-s` flag to the CLI to enable See #53 Commits from feature branch: * Styles encapsulation should happen after imports * Fix tests after adding encapsulation * Add encapsulation plugins before autoprefixer * Add non inherited props for encapsulation * Enforce core plugins order * Add minimal in-browser tests for Encapsulation * Simplify test runner for the browser * Add browserify * Run encapsulation tests in Electron * Make encapsulation fixture/tests a bit more explicit * Add encapsulate option to CLI * Add documentation for encapsulateStyles option * Add autoreset to README plugin list * Add link to the subset of properties used by the preprocessor
1 parent 1b92673 commit 0891f4a

17 files changed

+622
-40
lines changed

.travis.yml

+8
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,11 @@ node_js:
44
- "4"
55
- "5"
66
- "6"
7+
addons:
8+
apt:
9+
packages:
10+
- xvfb
11+
install:
12+
- export DISPLAY=':99.0'
13+
- Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &
14+
- npm install

README.md

+134-7
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Compiles CSS packages with:
1212
* [postcss-easy-import](https://github.com/TrySound/postcss-easy-import)
1313
* [postcss-custom-properties](https://github.com/postcss/postcss-custom-properties)
1414
* [postcss-calc](https://github.com/postcss/postcss-calc)
15+
* [postcss-autoreset](https://github.com/maximkoretskiy/postcss-autoreset)
1516
* [postcss-color-function](https://github.com/postcss/postcss-color-function)
1617
* [postcss-apply](https://github.com/pascalduez/postcss-apply)
1718
* [postcss-custom-media](https://github.com/postcss/postcss-custom-media)
@@ -37,6 +38,8 @@ suitcss input.css output.css
3738

3839
### Command Line
3940

41+
Options are [documented below](#options)
42+
4043
```
4144
Usage: suitcss [<input>] [<output>]
4245
@@ -45,6 +48,7 @@ Options:
4548
-h, --help output usage information
4649
-c, --config [path] a custom PostCSS config file
4750
-i, --import-root [path] the root directory for imported css files
51+
-s, --encapsulate encapsulate component styles
4852
-w, --watch watch the input file and any imports for changes
4953
-m, --minify minify output with cssnano
5054
-e, --throw-error throw an error when any warnings are found
@@ -99,16 +103,16 @@ preprocessor(css, {
99103
});
100104
```
101105

102-
#### Options
106+
### Options
103107

104-
##### `root`
108+
#### `root`
105109

106110
* Type: `String`
107111
* Default: `process.cwd()`
108112

109113
Where to resolve imports from. Passed to [`postcss-import`](https://github.com/postcss/postcss-import/blob/master/README.md#root).
110114

111-
##### `debug`
115+
#### `debug`
112116

113117
* Type: `Function`
114118
* Default: identity (it does nothing)
@@ -137,7 +141,130 @@ function debug(plugins) {
137141
}
138142
```
139143

140-
##### `lint`
144+
#### `encapsulate`
145+
146+
_(experimental)_
147+
148+
* Type: `Boolean`
149+
* Default: `false`
150+
151+
Resets CSS properties to their [initial values](https://developer.mozilla.org/en-US/docs/Web/CSS/initial_value)
152+
to effectively allow a component to opt out of CSS inheritance and be
153+
encapsulated from the rest of the application similar to [the Shadow DOM](https://www.w3.org/TR/shadow-dom/).
154+
There are two types of CSS properties that affect components, inherited (e.g.
155+
`font-size,` `color`) and non-inherited (e.g. `margin`, `background`). This
156+
option works so that:
157+
158+
* Root elements (e.g. `.Component`) have both inherited and non-inherited
159+
properties reset to default values.
160+
* Descendants (e.g. `.Component-item`) only have non-inherited properties reset
161+
as this allows properties set on the root element to be inherited by its
162+
descendants.
163+
164+
This means that components are isolated from styles outside the component root
165+
element but should an inheritable property such as `font-size` be applied on the
166+
component root element it will be inherited by the component descendants as
167+
normal. This prevents the need to redeclare properties on every descendant in a
168+
component.
169+
170+
The same rules also apply to nested components.
171+
172+
**Rationale**
173+
174+
One of the difficulties with CSS components is predictably. Unwanted styles
175+
can be inherited from parent components and this can make it difficult to
176+
reuse components in different contexts.
177+
178+
Methodologies such as SUIT and BEM exist to solve problems around the cascade
179+
and specificity but they cannot protect components from inheriting unwanted
180+
styles. What would really help is to allow inheritance to be 'opt-in' and let
181+
component authors decide what properties are inherited. This creates a more
182+
predictable baseline for styling components and promoting easier
183+
reuse.
184+
185+
* [Component Based Style Reuse](https://youtu.be/_70Yp8KPXH8?t=27m45s)
186+
* [React: CSS in JS](http://blog.vjeux.com/2014/javascript/react-css-in-js-nationjs.html)
187+
188+
**Examples**
189+
190+
* [CodePen encapsulate](http://codepen.io/simonsmith/pen/BLOyAX) - Demonstrates
191+
how components are encapsulated from global and parent styles.
192+
* [CodePen encapsulate inheritance](http://codepen.io/simonsmith/pen/LRgxdp) -
193+
Similar to above but shows how components can opt-in to inheritance.
194+
195+
**What about `all: initial`?**
196+
197+
The `all: initial` declaration will reset both inherited and non-inherited
198+
properties but this can be too forceful. For example `display` is reset to
199+
`inline` on block elements and as mentioned earlier, descendants of a component
200+
should only have non-inherited properties reset to allow declarations to be
201+
inherited from the root element.
202+
203+
> For example, if an author specifies `all: initial` on an element it will block
204+
all inheritance and reset all properties, as if no rules appeared in the
205+
author, user, or user-agent levels of the cascade.
206+
207+
https://www.w3.org/TR/css3-cascade/#all-shorthand
208+
209+
Instead a subset of properties are reset to allow more
210+
granular control over what parts of a component use inheritance.
211+
212+
To achieve this the preprocessor uses
213+
[postcss-autoreset](https://github.com/maximkoretskiy/postcss-autoreset) with
214+
the SUIT preset and a [custom set of CSS properties](lib/encapsulation.js) that
215+
are reset to their initial values. **Only selectors conforming** to the SUIT naming
216+
conventions are affected.
217+
218+
**Caveats**
219+
220+
##### Selectors must be present in the component CSS
221+
222+
If an element is present in the HTML but not styled in the component CSS
223+
(perhaps relying on utility classes) it will not be reset. In
224+
this instance an empty ruleset can be added to ensure it is correctly reset:
225+
226+
```html
227+
<div class="Component u-posRelative u-textCenter">
228+
<div class="Component-item"></div>
229+
</div>
230+
```
231+
```css
232+
/* Empty ruleset required */
233+
.Component {}
234+
235+
.Component-item {
236+
color: red;
237+
}
238+
```
239+
240+
##### Global styles can still override descendants
241+
242+
Because component descendants only have non-inheritable properties reset it can
243+
lead to specific global rules still applying:
244+
245+
```css
246+
/* global.css */
247+
span {
248+
color: red;
249+
}
250+
251+
/* component.css */
252+
.Component-text {
253+
font-style: bold;
254+
}
255+
```
256+
```html
257+
<div class="Component">
258+
<span class="Component-text">
259+
<!-- this text is red -->
260+
<span>
261+
</div>
262+
```
263+
264+
The solution to this is to minimise or avoid entirely the use of global styles
265+
which is the recommended approach in a SUIT CSS application.
266+
267+
#### `lint`
141268

142269
* Type: `Boolean`
143270
* Default: `true`
@@ -161,14 +288,14 @@ locally in your package.
161288
}
162289
```
163290

164-
##### `minify`
291+
#### `minify`
165292

166293
* Type: `Boolean`
167294
* Default: `false`
168295

169296
If set to `true` then the output is minified by [`cssnano`](http://cssnano.co/).
170297

171-
##### `postcss`
298+
#### `postcss`
172299

173300
* Type: `Object`
174301
* Default: `undefined`
@@ -194,7 +321,7 @@ A list of plugins that are passed to PostCSS. This can be used to add new plugin
194321
}
195322
```
196323

197-
##### `<plugin-name>`
324+
#### `<plugin-name>`
198325

199326
* Type: `Object`
200327
* Default: `undefined`

bin/suitcss

+2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ program
2020
.usage('[<input>] [<output>]')
2121
.option('-c, --config [path]', 'a custom PostCSS config file')
2222
.option('-i, --import-root [path]', 'the root directory for imported css files')
23+
.option('-s, --encapsulate', 'encapsulate component styles')
2324
.option('-w, --watch', 'watch the input file and any imports for changes')
2425
.option('-m, --minify', 'minify output with cssnano')
2526
.option('-e, --throw-error', 'throw an error when any warnings are found')
@@ -112,6 +113,7 @@ function run() {
112113
var opts = assign({}, config, {
113114
minify: program.minify,
114115
root: program.importRoot,
116+
encapsulate: program.encapsulate,
115117
lint: program.lint
116118
});
117119

lib/encapsulation.js

+128
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/* eslint-disable quote-props */
2+
var autoreset = require('postcss-autoreset');
3+
4+
var rules = {
5+
inherited: {
6+
'border-collapse': 'separate',
7+
'border-spacing': 0,
8+
'caption-side': 'top',
9+
'color': 'initial',
10+
'cursor': 'auto',
11+
'direction': 'initial',
12+
'empty-cells': 'show',
13+
'font-size-adjust': 'none',
14+
'font-family': 'initial',
15+
'font-size': 'medium',
16+
'font-style': 'normal',
17+
'font-stretch': 'normal',
18+
'font-variant': 'normal',
19+
'font-weight': 'normal',
20+
'font': 'initial',
21+
'letter-spacing': 'normal',
22+
'line-height': 'normal',
23+
'list-style-image': 'none',
24+
'list-style-position': 'outside',
25+
'list-style-type': 'disc',
26+
'list-style': 'initial',
27+
'orphans': 2,
28+
'quotes': 'initial',
29+
'tab-size': 8,
30+
'text-align': 'initial',
31+
'text-align-last': 'auto',
32+
'text-decoration-color': 'initial',
33+
'text-indent': 0,
34+
'text-justify': 'auto',
35+
'text-shadow': 'none',
36+
'text-transform': 'none',
37+
'visibility': 'visible',
38+
'white-space': 'normal',
39+
'widows': 2,
40+
'word-break': 'normal',
41+
'word-spacing': 'normal',
42+
'word-wrap': 'normal'
43+
},
44+
nonInherited: {
45+
'animation': 'none 0s ease 0s 1 normal none running',
46+
'backface-visibility': 'visible',
47+
'background': 'transparent none repeat 0 0 / auto auto padding-box border-box scroll',
48+
'border': 'medium none currentColor',
49+
'border-image': 'none',
50+
'border-radius': '0',
51+
'bottom': 'auto',
52+
'box-shadow': 'none',
53+
'clear': 'none',
54+
'clip': 'auto',
55+
'columns': 'auto',
56+
'column-count': 'auto',
57+
'column-fill': 'balance',
58+
'column-gap': 'normal',
59+
'column-rule': 'medium none currentColor',
60+
'column-span': '1',
61+
'column-width': 'auto',
62+
'content': 'normal',
63+
'counter-increment': 'none',
64+
'counter-reset': 'none',
65+
'float': 'none',
66+
'height': 'auto',
67+
'hyphens': 'none',
68+
'left': 'auto',
69+
'margin': '0',
70+
'max-height': 'none',
71+
'max-width': 'none',
72+
'min-height': '0',
73+
'min-width': '0',
74+
'opacity': '1',
75+
'outline': 'medium none invert',
76+
'overflow': 'visible',
77+
'overflow-x': 'visible',
78+
'overflow-y': 'visible',
79+
'padding': '0',
80+
'page-break-after': 'auto',
81+
'page-break-before': 'auto',
82+
'page-break-inside': 'auto',
83+
'perspective': 'none',
84+
'perspective-origin': '50% 50%',
85+
'position': 'static',
86+
'right': 'auto',
87+
'table-layout': 'auto',
88+
'text-decoration': 'none',
89+
'top': 'auto',
90+
'transform': 'none',
91+
'transform-origin': '50% 50% 0',
92+
'transform-style': 'flat',
93+
'transition': 'none 0s ease 0s',
94+
'unicode-bidi': 'normal',
95+
'vertical-align': 'baseline',
96+
'width': 'auto',
97+
'z-index': 'auto'
98+
}
99+
};
100+
101+
// This applies only to the Component Root
102+
// to stop inheritance and ensure
103+
// styles encapsulation
104+
var resetInherited = autoreset({
105+
reset: rules.inherited,
106+
rulesMatcher: function (rule) {
107+
var selector = rule.selector;
108+
return (
109+
selector.charAt(0) === '.' &&
110+
/^\.(?:[a-z0-9]*-)?[A-Z](?:[a-zA-Z0-9]+)$/.test(selector)
111+
);
112+
}
113+
});
114+
115+
resetInherited.postcssPlugin = 'autoreset-suitcss-encapsulation-inherited';
116+
117+
// This applies to the Component Root and Descendants
118+
var resetGeneric = autoreset({
119+
reset: rules.nonInherited,
120+
rulesMatcher: 'suit'
121+
});
122+
123+
resetGeneric.postcssPlugin = 'autoreset-suitcss-encapsulation-nonInherited';
124+
125+
module.exports = {
126+
resetInherited: resetInherited,
127+
resetGeneric: resetGeneric
128+
};

0 commit comments

Comments
 (0)