diff --git a/CHANGELOG.md b/CHANGELOG.md index bf6c555..d45cbdf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # CHANGELOG +## 1.3.0 (2017-xx-xx) + +- Resize Observer can be enabled (will be the default behaviour in 2.0.0) +- Resize observation can be switched on / off. +- First adjustment on Container instantiation can be switched off +- Dependency upgrades + ## 1.2.6 (2017-03-26) - Fixed exports for non es-6 environments diff --git a/README.md b/README.md index 39ecf57..c49ec40 100644 --- a/README.md +++ b/README.md @@ -6,64 +6,588 @@ [![Greenkeeper badge](https://badges.greenkeeper.io/ZeeCoder/container-query.svg)](https://greenkeeper.io/) A PostCSS plugin and Javascript runtime combination, which allows you to write -@container queries in your CSS the same way you would write @media queries. +**container queries** in your CSS the same way you would write **media queries**. -## screenshot +## Installation -## How to use +`yarn add --dev @zeecoder/container-query` -### Install +or `npm install --save-dev @zeecoder/container-query` -## PublicAPI -- `Container.js` - JS Runtime -- `containerQuery.js` - PostCSS Plugin -- `initialiseAllContainers.js` - helper function - -The rest in build/ is not considered to be a part of the Public API, which means -anything in it can change at any time. (Including minor / patch releases.) - -## Limitations -- `@container` queries cannot be nested - -## Supported Browsers -- Works with all modern browsers and IE9+ - -## Notes -- Lead with ## WHAT (image) followed by ## WHY -- list supported browsers -- BEM-inspiration: Block -> inside of which are elements (unique classnames!) -- @container queries must always be preceded by a @define-container -- All @containers following a @defined-container will be related to that -- @define-container declarations will be ignored inside @container declarations -- Example function to save all container configurations in separate JSON files in a dir -- Gulp example - - PostCSS example - - SASS example: works out of the box - - Less -> separate pipeline -- Containers can have more than one instance of their elements -- The container units are relative to the container's "inner" height / width. -(Without the borders.) -- Note possible circular issues - - A container should not use container units for properties that would affect - its width / height. These situations are not currently handled, so try to - avoid them. -- To avoid circular deps, use overflow: hidden and avoid using container units on defined containers -- Use native CSS techniques to achieve your goal whenever possible (css grid, flexbox) +## Introduction + +The way it works: + +> PostCSS plugin => JSON => Runtime + +Container queries work the same way media queriesdo: they allow you to apply +styles to elements (and their descendants) when certain conditions are met. + +While media queries are relative to the viewport's size, container queries are +relative to a container element's size. + +**What is a container?** + +A container is just an HTML element, which may contain other elements. +You may want to think of them as "**Blocks**" ([BEM](http://getbem.com/naming/)) +or "**Components**" ([React](https://facebook.github.io/react/docs/components-and-props.html)). + +### Highlights + +- Built with webpack / React in mind +- Uses a [ResizeObserver polyfill](https://github.com/que-etc/resize-observer-polyfill) +to detect size changes. Once the [spec](https://wicg.github.io/ResizeObserver/) +is implemented by browsers, it's going to be [even more performant](https://developers.google.com/web/updates/2016/10/resizeobserver#out_now). +- Uses media query like syntax: `@container (...) { /* ... */ }` +- Supports container units: chpx, cwpx, cminpx, cmaxpx. (Useful to set font-size +and other properties to a value that's changing with the container's size.) + +### Browser Support + +Works with all modern browsers and IE9+ + +### In action + +```pcss +.User { + @define-container; + // All container queries and container units must be preceded by a container + // definition. The rest of the classes generated here are expected to be + // "descendants" of the container. + + background: red; + + @container (width >= 200px) and (height >= 200px) { + // Container queries are relative to the previous @defined-container. + background: green; + } + + &__name { + font-size: 10chpx; + // The above is a container unit. + // It resolves to 10 percent of the container's height in pixels. + // Resolves to 12px, if the container's height is 120px. + } + + &__avatar { + display: none; + + @container (width >= 200px) and (height >= 200px) { + display: block; + } + } +} +``` + +```html + +
+
+ +
+ + +
+
+ +
+``` + +## How to use + +This solution consists of a PostCSS plugin and a JS (`Container`) class. + +> PostCSS plugin => JSON => Runtime + +The plugin analyses the given CSS, and extracts all container-query related +lines, producing a JSON file. Depending on your setup (Gulp / webpack, etc) +this file may or may not contain more than one container's data. + +Once the JSON file is generated, a new Container instance needs to be created +for all container HTML Elements, with the right json stats. + +### JSON structure + +```json +{ + ".User": {}, + ".Post": {}, + ".Avatar": {} +} +``` + +As you can see, selectors are considered to be the unique identifiers of +defined containers. While technically nothing will stop you from having +`.page .container .User` as a container's selector, it is *not recommended*. + +Instead, use the BEM methodology or something similar. + +Support for [CSS Modules](https://github.com/css-modules/css-modules#user-content-implementations) +and [CSS-in-JS](https://github.com/MicheleBertoli/css-in-js#user-content-features) +is planned, to automate this pattern. + +(You might want to watch Mark Dalgleish's talk called +"[A Unified Styling Language](https://www.youtube.com/watch?v=X_uTCnaRe94)" to +have an idea why the latter might be a good thing.) + +### webpack + React + +I recommend you to set up [postcss-loader](https://github.com/postcss/postcss-loader) +with [postcss-nested](https://github.com/postcss/postcss-nested) with +`bubble: ['container']` option, or to use SASS. + +**Avatar.pcss** +```pcss +.Avatar { + @define-container; + /* ... */ + + @container (aspect-ratio: > 3) { + /* ... */ + } + + @container (width > 100px) and (height > 100px) { + /* ... */ + } +} +``` + +**Avatar.js** +```js +import React, {Component} from 'react'; +import ReactDOM from 'react-dom'; +import Container from "@zeecoder/container-query/Container"; + +// Generates `Avatar.json` in the same folder. +require('./Avatar.pcss'); +// We only defined the Avatar container in the pcss file, so there's nothing +// else there apart from the '.Avatar' property. +const containerStats = require('./Avatar.json')['.Avatar']; +// `['.Avatar']` will be unnecessary once Issue#17 is done + +export default class Avatar extends Component { + componentDidMount() { + new Container( + ReactDOM.findDOMNode(this), + containerStats, + {adjustOnResize: true} + ); + // adjustOnResize will be the default behaviour in 2.0 + } + + render() { + return ( +
+ ); + } +} +``` + +**And that's it!** + +Now all new *Avatar* components will automatically adjust to the component's size. + +### Gulp setup + +If you're not a fan of processing styles with webpack, then you can use a task +runner instead, like Gulp. + +Your task could look something like this: + +```js +const gulp = require('gulp'); +const postcss = require('gulp-postcss'); +const rename = require('gulp-rename'); +const postcssImport = require('postcss-import'); +const postcssNested = require('postcss-nested'); +const containerQuery = require('@zeecoder/container-query/containerQuery'); + +gulp.task('styles', () => { + return gulp.src('styles/main.pcss') + .pipe(postcss([ + postcssImport(), + postcssNested({ bubble: ['container'] }), + containerQuery(), + ])) + .pipe(rename('main.css')) + .pipe(gulp.dest('web/dist')); +}); + +``` + +Now you'll have both main.css and main.json. The CSS can then be served separately +from the JS, while webpack could still `require()` the JSON and do its thing. + +### Without webpack + +Even though the library was made with webpack in mind, there's no reason why +other bundlers wouldn't work, or other UI libraries for that matter. (Instead +of React.) + +**Just follow the same steps** + +1) Process your styles with the PostCSS plugin, to extract all container-related +information +2) Save the JSON(s) somewhere +3) Serve the JSON(s) to the JS some way +4) Create a Container instance for all container html elements + +For instance, imagine you have a main.pcss file, which imports all other +components.(Using Gulp as described above.) + +Then, you can serve the JSON from the backend, and bundle the JS with your +favourite JS bundler, to grab that JSON and instantiate the Container class for +all elements found: + +```js +// Assumptions: +// Browserify as the bundler +// JSON is available in a script tag, served by the backend: +// `` + +import Container from "@zeecoder/container-query/Container"; + +const containerStats = JSON.parse( + document.getElementById('container-stats').innerHTML +); + +// Initialising all containers by finding all instances based on the selectors +for (let containerSelector in containerStats) { + document.querySelectorAll(containerSelector).forEach(element => { + // Initialising all HTML Elements with the right json stats + new Container( + element, + containerStats[containerSelector], + {adjustOnResize: true} + ); + }); +} +``` + +The above doesn't cover dynamically created elements, but you get the idea. + +## Syntax + +### Declaration + +As previous examples show, containers can be declared by adding +`@define-container;` inside a rule that's meant to be used as a container. + +Multiple such definitions in a single CSS file are allowed. All container +queries and units will be relative to the previous declaration. + +Like so: + +```pcss +.User { + @define-container; + + &__name { + display: none; + font-size: 10chpx; + } + + @container (width > 200px) { + display: block; + } +} + +.Avatar { + @define-container; + + border-radius: 100%; + border: 1chpx solid; + + @container (width < 30px), (height < 30px) { + background: grey; + } +} +``` + +Note that for container queries and container units to work, all elements must +be descendants of the container. + +Using the above example, an element with the `.User__name` class will not have +its font-size adjusted, unless it's a descendant of a container element with +the `.User` class. + +### Queries + +Container queries have the same syntax media queries do: + +```pcss +@container (condition) and (condition), (condition) { + // styles +} +``` + +However, instead of writing min-width, min-height you can use the following +operators: `<`, `<=`, `>`, `>=`. + +(In accordance width [CSS Media Queries Level 4](https://drafts.csswg.org/mediaqueries/#mq-range-context)) + +The following conditions are supported: width, height, aspect-ratio, orientation. + +**Examples** + +```pcss +@condition (orientation: landscape) {} +@condition (orientation: portrait) {} +@condition (width > 100px) {} +@condition (height < 100px) {} +@condition (aspect-ratio > 3) {} +@condition (orientation: landscape) and (aspect-ratio > 3) {} +``` + +If you want the same syntax for your media queries, then I recommend [this plugin](https://github.com/postcss/postcss-media-minmax). + +### Units + +Container units are like viewport units (vh, vw, vmin and vmax), only relative +to the container. They are useful to generate values based on the container's +size. + +The supported units are: **chpx**, **cwpx**, **cminpx**, **cmaxpx**. + +**Syntax**: `px` + +Depending on whether ch or cw is used, value stands for a percentage of the +container's width or height. + +If a container's size is: + +- width: 120px +- height: 130px + +then + +- 100cwpx => 120px +- 100chpx => 130px +- 100cminpx => 120px +- 100cmaxpx => 130px +- 15chpx => 11.53846px +- 15cwpx => 12.5px + +And so on. + +**Example** + +```pcss +.User { + @define-container; + + &__name { + font-size: 10chpx; + } + + &__avatar { + border-radius: 100%; + border: 1vminpx solid; + } + + @container (height > 150px) { + font-size: 15chpx; + border: 5vminpx solid; + } +} +``` + +Technically, you can produce any CSS units, like: chem/cwem/cminem/cmaxem, +chrem/cwrem/cminrem/cmaxrem), but they're planned to be [phased out](https://github.com/ZeeCoder/container-query/issues/16). + +Also note that recalculating and applying these values is costly, since it's +done on each resize event (or `adjust` call). + +**Example** + +You might be tempted to use container units to set an aspect ratio between the +container's width / height: + +```pcss +.Container { + @define-container; + height: 50cwpx; + // Height will now be 50% of it's width +} +``` + +While this works, there's a [pure CSS solution too](https://codepen.io/ZeeCaptein/pen/ZyEowo). + +Admittedly more boilerplate, but it might worth avoiding JS when it's not really +necessary by using flexbox, CSS grid and other vanilla CSS solutions instead. + +## API + +### Container (Runtime) + +**Instantiation** + +`new Container(Element, statsJSON, options)` + +Where `Element` is an HTMLElement, `statsJSON` is a json object from the PostCSS +plugin, and options are extra options about how the instance should behave. + +**Default options:** + +```js +{ + adjustOnResize: false, // Will be true by default in ^2.0.0 + adjustOnInstantiation: true +} +``` + +- *adjustOnResize*: If true, then the container will readjust itself based on the +element's height automatically. +This is done by using a [ResizeObserver](https://github.com/que-etc/resize-observer-polyfill) polyfill. +- *adjustOnInstantiation*: Whether to do an initial adjustment call on instantiation. + +These options may be useful to you, if you want to fine-tune when readjustments +should happen. + +*For example*: You could optimise animations, or only readjust containers on window +resize, if that fits your needs. + +**Instance methods** + +- `adjust(containerDimensions)`: Calling `adjust()` will readjust the container +based on it's size. You might have the containers size already, however, in which +case you can just pass that in, so you can save the browser the layout / repaint +work: `{ width: , height: }`. This could be useful if you're +animating the container's size, and on each "tick" you know what the dimensions +are already. +- `observeResize()`: Makes the container observe resize events and readjust +itself automatically. Passing in the {adjustOnResize: true} option has the same +effect. +- `unobserveResize()`: Stops a container observing resize events. + +### containerQuery (PostCSS plugin) + +```js +postCSS([ + containerQuery({ + getJSON: function(cssPath, jsonStats) { + // `cssPath`: the original css' path that was processed. Useful if + // you want to save the JSON relative to the original css. + // + // `jsonStats`: the json stats having all the container-related data + // needed for the Container instances. + // Structural Reminder: + // { + // ".SomeComponent": {}, + // ".OtherComponent": {}, + // } + // Keys here are the selectors having the `@define-container` + // declaration. + } + }) +]) +``` + +## Compatibility with other CSS preprocessors + +From the examples above, you can see that I recommend using PostCSS. +However, other css preprocessors would work too, as long as they support custom +at-rules. + +### SASS + +Sass works out of the box with at-rules, even when they're nested. + +They behave the same way media queries, which is great! + +You can write things like: + +```pcss +.Avatar { + @define-container; + /* ... */ + + @container (width > 200px) { + /* ... */ + } + + @container (height > 200px) { + /* ... */ + } +} +``` + +Which compiles to: + +```css +.Avatar { + @define-container; + /* ... */ +} + +@container (width > 200px) { + .Avatar { + /* ... */ + } +} + +@container (height > 200px) { + .Avatar { + /* ... */ + } +} +``` + +### LESS + +Support for at-rules is limited, but it'll work fine with v2.6.0 and above as +long as you avoid nesting. + +## Caveats / Notes + +There are some things to look out for when using this library. + +- Resize Observer reacts in ~20ms. Should be good for animation even, but if not, +it can be switched off to use requestAnimationFrame() instead. Also: the more +you nest containers, the slower change propagates from top to bottom. This is +due to the fact that a container's size cannot be checked without having a +layout / repaint first. +- Currently, styles are applied through the Element.style object. I'll probably +replace this mechanic with [Styletron](https://github.com/rtsao/styletron), or +something similar in the future. +- With element / container query solutions, circularity issues may arise. While +[an attempt](https://github.com/ZeeCoder/container-query/issues/20) to tackle +this was made, the same is still unfortunately true to this library as well. +Use your best judgement when setting up queries / units to avoid these issues. ## Thoughts on design -Here is a list of goals I started with, in case you're wondering about the -tool's design: - -- Should be thoroughly unit tested -- Use containers (as opposed to "element query"), -- Resemble @media queries so that it's familiar and easy to use, -- Uses PostCSS for preprocessing instead of a JS runtime parser, -- Modular, so it plays nicely with js bundlers and "Component-based" UI -libraries (Webpack / Browserify / React etc.) -- Doesn't need to be valid CSS syntax (since it's based on PostCSS) -- Be easy enough to use, but a transpiling step in the frontend build -process would be assumed -- Should work especially well with css component naming methodologies, like BEM or SUIT +In case you're wondering about the tool's design, here is a list of goals I +started with: + +- Should be tested. +- Use containers instead of elements. +- Use media query syntax so that it's familiar and easy to use. +- Should be easy enough to use, but a transpiling step would be assumed. +- Uses PostCSS for preprocessing instead of having a JS runtime parser. +- Use JS modules, so it plays nicely with js bundlers (webpack, Browserify, +etc.) and Component-oriented UI libraries (React, Vue, etc.) +- Don't limit the tool to CSS syntax. With PostCSS, it's easy to parse custom +at-rules instead. The end result will still be valid CSS. +- Should work with component naming methodologies - like BEM or SUIT - the best. + +## Next up + +[Ideas for enhancement](https://goo.gl/7XtjDe) + +## Alternatives + +Finally, if you like the idea of container queries, but are not particularly +convinced by this solution, then I encourage you to look at these alternatives: + +- [EQCSS](https://github.com/eqcss/eqcss) +- [CSS Element Queries](https://github.com/marcj/css-element-queries) +- [CQ Prolyfill](https://github.com/ausi/cq-prolyfill) +- [React Container Query](https://github.com/d6u/react-container-query) diff --git a/demo/gulpfile.js b/demo/gulpfile.js index acfd58e..aa22b6c 100644 --- a/demo/gulpfile.js +++ b/demo/gulpfile.js @@ -1,49 +1,60 @@ -'use strict'; +"use strict"; -const gulp = require('gulp'); -const fs = require('fs'); -const path = require('path'); -const postcss = require('gulp-postcss'); -const rename = require('gulp-rename'); -const nested = require('postcss-nested'); -const autoprefixer = require('autoprefixer'); -const postcssImport = require('postcss-import'); -const containerQuery = require('@zeecoder/container-query/containerQuery'); -const writeFileSync = require('fs').writeFileSync; +const gulp = require("gulp"); +const fs = require("fs"); +const path = require("path"); +const postcss = require("gulp-postcss"); +const rename = require("gulp-rename"); +const nested = require("postcss-nested"); +const autoprefixer = require("autoprefixer"); +const postcssImport = require("postcss-import"); +const containerQuery = require("@zeecoder/container-query/containerQuery"); +const writeFileSync = require("fs").writeFileSync; -function containerSelectorToFilename (selector) { +function containerSelectorToFilename(selector) { return selector.substr(1); } -gulp.task('css', function () { - return gulp.src('src/css/main.pcss') - .pipe(postcss([ - postcssImport(), - nested(), - autoprefixer(), - containerQuery({ - getJSON: (cssPath, containers) => { - // Saving the container query stats individually - for (let containerSelector in containers) { - let component = containerSelectorToFilename(containerSelector); +gulp.task("css", function() { + return gulp + .src("src/css/main.pcss") + .pipe( + postcss([ + postcssImport(), + nested({ + bubble: ["container"] + }), + autoprefixer(), + containerQuery({ + getJSON: (cssPath, containers) => { + // Saving the container query stats individually + for (let containerSelector in containers) { + let component = containerSelectorToFilename( + containerSelector + ); + writeFileSync( + `${__dirname}/src/css/components/${component}/${component}.json`, + JSON.stringify(containers[containerSelector]) + ); + } + + // Then saving the container names writeFileSync( - `${__dirname}/src/css/components/${component}/${component}.json`, - JSON.stringify(containers[containerSelector]) + `${__dirname}/src/js/containers.json`, + JSON.stringify( + Object.keys(containers).map( + containerSelectorToFilename + ) + ) ); } - - // Then saving the container names - writeFileSync( - `${__dirname}/src/js/containers.json`, - JSON.stringify(Object.keys(containers).map(containerSelectorToFilename)) - ); - } - }), - ])) - .pipe( rename('main.css') ) - .pipe( gulp.dest('web/dist') ); + }) + ]) + ) + .pipe(rename("main.css")) + .pipe(gulp.dest("web/dist")); }); -gulp.task('watch', function() { - gulp.watch('src/**/*.pcss', ['css']); +gulp.task("watch", function() { + gulp.watch("src/**/*.pcss", ["css"]); }); diff --git a/demo/package.json b/demo/package.json index c698a5b..a945b38 100644 --- a/demo/package.json +++ b/demo/package.json @@ -18,7 +18,8 @@ }, "scripts": { "build": "gulp css && webpack", - "watch": "gulp watch & webpack --watch &" + "watch:gulp": "gulp watch", + "watch:webpack": "webpack --watch" }, "dependencies": { "autoprefixer": "^6.7.6" diff --git a/demo/src/css/components/social-container/social-container.json b/demo/src/css/components/social-container/social-container.json index b8a4a7a..ea8e74f 100644 --- a/demo/src/css/components/social-container/social-container.json +++ b/demo/src/css/components/social-container/social-container.json @@ -1 +1 @@ -{"selector":".social-container","queries":[{"elements":[{"selector":".social-container","styles":{}},{"selector":".social-container__cell","styles":{}},{"selector":".social-container__cell:nth-child(1)","styles":{"width":""}},{"selector":".social-container__cell:nth-child(2)","styles":{"width":""}},{"selector":".social-container__cell:nth-child(3)","styles":{"width":""}},{"selector":".social-container__cell:nth-child(4)","styles":{"width":""}},{"selector":".social-container__cell:nth-child(5)","styles":{"width":""}}]},{"conditions":[[["width",">",250]]],"elements":[{"selector":".social-container__cell:nth-child(1)","styles":{"width":"50%"}},{"selector":".social-container__cell:nth-child(2)","styles":{"width":"50%"}},{"selector":".social-container__cell:nth-child(3)","styles":{"width":"33%"}},{"selector":".social-container__cell:nth-child(4)","styles":{"width":"34%"}},{"selector":".social-container__cell:nth-child(5)","styles":{"width":"33%"}}]}]} \ No newline at end of file +{"selector":".social-container","queries":[{"elements":[{"selector":".social-container","styles":{}},{"selector":".social-container__cell","styles":{}},{"selector":".social-container__cell:nth-child(1)","styles":{"width":""}},{"selector":".social-container__cell:nth-child(2)","styles":{"width":""}},{"selector":".social-container__cell:nth-child(3)","styles":{"width":""}},{"selector":".social-container__cell:nth-child(4)","styles":{"width":""}},{"selector":".social-container__cell:nth-child(5)","styles":{"width":""}},{"selector":".social-container__cell:nth-child(6)","styles":{"width":""}}]},{"conditions":[[["width",">",250]]],"elements":[{"selector":".social-container__cell:nth-child(1)","styles":{"width":"50%"}},{"selector":".social-container__cell:nth-child(2)","styles":{"width":"50%"}},{"selector":".social-container__cell:nth-child(3)","styles":{"width":"33%"}},{"selector":".social-container__cell:nth-child(4)","styles":{"width":"34%"}},{"selector":".social-container__cell:nth-child(5)","styles":{"width":"33%"}},{"selector":".social-container__cell:nth-child(6)","styles":{"width":"40%"}}]}]} \ No newline at end of file diff --git a/demo/src/css/components/social-container/social-container.pcss b/demo/src/css/components/social-container/social-container.pcss index c91a38b..6ec8630 100644 --- a/demo/src/css/components/social-container/social-container.pcss +++ b/demo/src/css/components/social-container/social-container.pcss @@ -1,16 +1,12 @@ .social-container { @define-container; - display: flex; - flex-direction: column; overflow: hidden; position: absolute; top: 0; left: 0; width: 100%; height: 100%; -} -.social-container { flex-direction: row; flex-wrap: wrap; } @@ -18,22 +14,25 @@ .social-container__cell { width: 100%; position: relative; -} -@container (width > 250px) { - .social-container__cell:nth-child(1) { - width: 50%; - } - .social-container__cell:nth-child(2) { - width: 50%; - } - .social-container__cell:nth-child(3) { - width: 33%; - } - .social-container__cell:nth-child(4) { - width: 34%; - } - .social-container__cell:nth-child(5) { - width: 33%; + @container (width > 250px) { + &:nth-child(1) { + width: 50%; + } + &:nth-child(2) { + width: 50%; + } + &:nth-child(3) { + width: 33%; + } + &:nth-child(4) { + width: 34%; + } + &:nth-child(5) { + width: 33%; + } + &:nth-child(6) { + width: 40%; + } } } diff --git a/demo/src/css/components/social-link/social-link.json b/demo/src/css/components/social-link/social-link.json index 8499165..d2b8db8 100644 --- a/demo/src/css/components/social-link/social-link.json +++ b/demo/src/css/components/social-link/social-link.json @@ -1 +1 @@ -{"selector":".social-link","queries":[{"elements":[{"selector":".social-link","styles":{"border":"calc(0.2chpx + 0.2cwpx) solid #999","borderRadius":"calc(0.3chpx + 0.3cwpx)","fontSize":"85cminpx"}},{"selector":".social-link:hover","styles":{}},{"selector":".social-link__icon","styles":{"marginLeft":""}},{"selector":".social-link__name","styles":{"display":"","marginLeft":"","marginRight":""}}]},{"conditions":[[["aspect-ratio",">",3]]],"elements":[{"selector":".social-link","styles":{"fontSize":"60chpx"}},{"selector":".social-link__icon","styles":{"marginLeft":"20chpx"}},{"selector":".social-link__name","styles":{"display":"block","marginLeft":"5cwpx","marginRight":"20chpx"}}]}]} \ No newline at end of file +{"selector":".social-link","queries":[{"elements":[{"selector":".social-link","styles":{"border":"calc(0.2chpx + 0.2cwpx) solid #999","borderRadius":"calc(0.3chpx + 0.3cwpx)","fontSize":"85cminpx"}},{"selector":".social-link:hover","styles":{}},{"selector":".social-link__icon","styles":{"marginLeft":""}},{"selector":".social-link__name","styles":{"display":"","marginLeft":"","marginRight":""}}]},{"conditions":[[["aspect-ratio",">",3]]],"elements":[{"selector":".social-link","styles":{"fontSize":"60chpx"}}]},{"conditions":[[["aspect-ratio",">",3]]],"elements":[{"selector":".social-link__icon","styles":{"marginLeft":"20chpx"}}]},{"conditions":[[["aspect-ratio",">",3]]],"elements":[{"selector":".social-link__name","styles":{"display":"block","marginLeft":"5cwpx","marginRight":"20chpx"}}]}]} \ No newline at end of file diff --git a/demo/src/css/components/social-link/social-link.pcss b/demo/src/css/components/social-link/social-link.pcss index 3c3dad5..944e257 100644 --- a/demo/src/css/components/social-link/social-link.pcss +++ b/demo/src/css/components/social-link/social-link.pcss @@ -1,6 +1,5 @@ .social-link { @define-container; - overflow: hidden; background: #ccc; border: calc(0.2chpx + 0.2cwpx) solid #999; @@ -18,12 +17,19 @@ height: 100%; transition: background-color .3s, color .3s; + @container (aspect-ratio > 3) { + font-size: 60chpx; + } + &:hover { background-color: #333; color: #eee; } &__icon { + @container (aspect-ratio > 3) { + margin-left: 20chpx; + } } &__name { @@ -31,21 +37,11 @@ overflow: hidden; text-overflow: ellipsis; font-size: 0.5em; - } -} - -@container (aspect-ratio > 3) { - .social-link { - font-size: 60chpx; - } - - .social-link__icon { - margin-left: 20chpx; - } - .social-link__name { - display: block; - margin-left: 5cwpx; - margin-right: 20chpx; + @container (aspect-ratio > 3) { + display: block; + margin-left: 5cwpx; + margin-right: 20chpx; + } } } diff --git a/demo/src/css/components/user/user.json b/demo/src/css/components/user/user.json index d069e54..60b5e63 100644 --- a/demo/src/css/components/user/user.json +++ b/demo/src/css/components/user/user.json @@ -1 +1 @@ -{"selector":".user","queries":[{"elements":[{"selector":".user","styles":{"border":"0.5cmaxpx solid","borderRadius":"3cminpx"}},{"selector":".user__info","styles":{"padding":"1cmaxpx","webkitBoxOrient":"","webkitBoxDirection":"","msFlexDirection":"","flexDirection":""}},{"selector":".user__image","styles":{"height":"30cwpx"}},{"selector":".user__name","styles":{"paddingLeft":"5cwpx","display":""}},{"selector":".user__social","styles":{}},{"selector":".user--1","styles":{}},{"selector":".user--2","styles":{}}]},{"conditions":[[["width",">",200]]],"elements":[{"selector":".user__name","styles":{"display":"block"}}]},{"conditions":[[["width",">",300]]],"elements":[{"selector":".user__info","styles":{"webkitBoxOrient":"horizontal","webkitBoxDirection":"normal","msFlexDirection":"row","flexDirection":"row"}}]}]} \ No newline at end of file +{"selector":".user","queries":[{"elements":[{"selector":".user","styles":{"border":"0.5cmaxpx solid","borderRadius":"3cminpx"}},{"selector":".user__info","styles":{"padding":"1cmaxpx","webkitBoxOrient":"","webkitBoxDirection":"","msFlexDirection":"","flexDirection":""}},{"selector":".user__image","styles":{"height":"30cwpx"}},{"selector":".user__name","styles":{"paddingLeft":"5cwpx","display":""}},{"selector":".user__social","styles":{}},{"selector":".user--1","styles":{}},{"selector":".user--2","styles":{}}]},{"conditions":[[["width",">",300]]],"elements":[{"selector":".user__info","styles":{"webkitBoxOrient":"horizontal","webkitBoxDirection":"normal","msFlexDirection":"row","flexDirection":"row"}}]},{"conditions":[[["width",">",200]]],"elements":[{"selector":".user__name","styles":{"display":"block"}}]}]} \ No newline at end of file diff --git a/demo/src/css/components/user/user.pcss b/demo/src/css/components/user/user.pcss index d25f046..cb84707 100644 --- a/demo/src/css/components/user/user.pcss +++ b/demo/src/css/components/user/user.pcss @@ -11,13 +11,18 @@ max-height: 300px; position: fixed; top: 50%; - transform: translateY(-50%); + transform: translateY(-50%) translateZ(0); + transition: width 1s; &__info { display: flex; flex-direction: column; align-items: center; padding: 1cmaxpx; + + @container (width > 300px) { + flex-direction: row; + } } &__image { @@ -30,6 +35,10 @@ flex: 1 0 0; padding-left: 5cwpx; display: none; + + @container (width > 200px) { + display: block; + } } &__social { @@ -49,15 +58,3 @@ height: 50%; } } - -@container (width > 200px) { - .user__name { - display: block; - } -} - -@container (width > 300px) { - .user__info { - flex-direction: row; - } -} diff --git a/demo/src/js/main.js b/demo/src/js/main.js index 1535d8e..78805c4 100644 --- a/demo/src/js/main.js +++ b/demo/src/js/main.js @@ -1,8 +1,8 @@ -import Container from '@zeecoder/container-query/Container'; +import Container from "@zeecoder/container-query/Container"; -const containers = require('./containers.json'); +const containers = require("./containers.json"); -function initialiseContainer (jsonData) { +function initialiseContainer(jsonData) { /** * @type NodeList */ @@ -10,18 +10,45 @@ function initialiseContainer (jsonData) { const htmlElementsLength = htmlElements.length; for (let i = 0; i < htmlElementsLength; i++) { - const containerInstance = new Container(htmlElements[i], jsonData); - window.addEventListener('resize', containerInstance.adjust); - requestAnimationFrame(() => { - containerInstance.adjust(); + // console.log(htmlElements[i]); + const containerInstance = new Container(htmlElements[i], jsonData, { + adjustOnResize: true }); + + // const containerInstance = new Container(htmlElements[i], jsonData); + // window.addEventListener('resize', containerInstance.adjust); + // requestAnimationFrame(() => { + // containerInstance.adjust(); + // }); } } -containers.forEach((containerFileName) => { - initialiseContainer(require(`../css/components/${containerFileName}/${containerFileName}.json`)); +containers.forEach(containerFileName => { + initialiseContainer( + require(`../css/components/${containerFileName}/${containerFileName}.json`) + ); }); +function startAnimating() { + let isWide = false; + const element = document.getElementById("to-animate"); + + function doAnimate() { + if (isWide) { + element.style.width = "100px"; + } else { + element.style.width = "700px"; + } + + isWide = !isWide; + + setTimeout(doAnimate, 1000); + } + + doAnimate(); +} + +setTimeout(startAnimating, 3000); // import initialiseAllContainers from '../initialiseAllContainers'; diff --git a/demo/web/index.html b/demo/web/index.html index 47e92fd..1d0c076 100644 --- a/demo/web/index.html +++ b/demo/web/index.html @@ -9,7 +9,7 @@ -
+
Avatar diff --git a/package.json b/package.json index ba3b640..dcf67b6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@zeecoder/container-query", - "version": "1.2.6", + "version": "1.3.0", "description": "A PostCSS plugin and Javascript runtime combination, which allows you to write @container queries in your CSS the same way you would write @media queries.", "main": "index.js", "author": "Viktor Hubert ", @@ -25,10 +25,14 @@ ], "license": "MIT", "dependencies": { + "es6-weak-map": "^2.0.2", "lodash.camelcase": "^4.3.0", "lodash.trim": "^4.5.1", + "mutation-observer": "^1.0.2", "object-assign": "^4.1.1", - "postcss": "^6.0.0" + "postcss": "^6.0.0", + "raf": "^3.3.2", + "resize-observer-polyfill": "^1.4.2" }, "repository": { "type": "git", @@ -58,11 +62,12 @@ "lint": "eslint src", "coveralls": "cat ./coverage/lcov.info | ./node_modules/.bin/coveralls", "build": "babel src --out-dir build --ignore spec.js", + "watch:build": "babel src --watch --out-dir build --ignore spec.js", "watch:test": "jest --watch --notify", "prepublish": "yarn run build", "precommit": "lint-staged", "eslint-check": "eslint --print-config .eslintrc.js | eslint-config-prettier-check", - "prettify": "prettier --write --tab-width=4 '{src|__mocks__}/**/*.js'" + "prettify": "prettier --write --tab-width 4 '{src,__mocks__}/**/*.js'" }, "lint-staged": { "*.js": [ diff --git a/src/postcss/containerQuery.js b/src/postcss/containerQuery.js index 462b991..702f90f 100644 --- a/src/postcss/containerQuery.js +++ b/src/postcss/containerQuery.js @@ -34,6 +34,7 @@ function shouldProcessNode(node) { } /** + * @todo refactor a bit to make testing easier * @param {{ getJSON: function }} options */ function containerQuery(options = {}) { diff --git a/src/runtime/Container.js b/src/runtime/Container.js index 7e0d9b7..0eb611a 100644 --- a/src/runtime/Container.js +++ b/src/runtime/Container.js @@ -1,19 +1,115 @@ import processConfig from "./processConfig"; import adjustContainer from "./adjustContainer"; +import objectAssign from "object-assign"; +import ResizeObserver from "resize-observer-polyfill"; +import MutationObserver from "mutation-observer"; +import WeakMap from "es6-weak-map"; +import raf from "raf"; + +const containerRegistry = new WeakMap(); + +const resizeObserver = new ResizeObserver(entries => { + if (!Array.isArray(entries)) { + return; + } + + entries.forEach(entry => { + const container = containerRegistry.get(entry.target); + + if ( + typeof container === "undefined" || + typeof container !== "object" || + typeof container.adjust !== "function" + ) { + console.warn( + "Could not find Container instance for element:", + entry.target + ); + return; + } + + container.adjust({ + width: entry.contentRect.width, + height: entry.contentRect.height + }); + }); +}); + +const mutationObserver = new MutationObserver(mutationsRecords => { + mutationsRecords.forEach(mutationsRecord => { + // Remove container element from registry and unobserve resize changes + mutationsRecord.removedNodes.forEach(node => { + if (containerRegistry.has(node) === false) { + return; + } + + resizeObserver.unobserve(node); + containerRegistry.delete(node); + }); + }); +}); /** * @class - * @property {HTMLElement} container - * @property {Object} config + * @property {Element} containerElement + * @property {Object} jsonStats + * @property {Object} opts */ export default class Container { - constructor(container, config) { - this.adjust = adjustContainer.bind( - this, - container, - processConfig(config) + constructor(containerElement, jsonStats, opts = {}) { + this.containerElement = containerElement; + this.processedJsonStats = processConfig(jsonStats); + + this.opts = objectAssign( + { + adjustOnResize: false, + adjustOnInstantiation: true + }, + opts ); - this.adjust(); + this.observeResize = this.observeResize.bind(this); + this.unobserveResize = this.unobserveResize.bind(this); + this.adjust = this.adjust.bind(this); + + containerRegistry.set(containerElement, this); + mutationObserver.observe(this.containerElement.parentNode, { + childList: true + }); + + if (this.opts.adjustOnResize) { + this.observeResize(); + } + + if (this.opts.adjustOnInstantiation) { + raf(this.adjust); + } + } + + /** + * Starts observing resize changes. + */ + observeResize() { + resizeObserver.observe(this.containerElement); + } + + /** + * Stops observing resize changes. + */ + unobserveResize() { + resizeObserver.unobserve(this.containerElement); + } + + /** + * Adjusts the container to it's current dimensions, or to the ones given. + * + * @param {ContainerDimensions} containerDimensions + */ + adjust(containerDimensions = null) { + adjustContainer( + this.containerElement, + this.processedJsonStats, + containerDimensions + ); } } diff --git a/src/runtime/Container.spec.js b/src/runtime/Container.spec.js index 5118e7d..f0d5dac 100644 --- a/src/runtime/Container.spec.js +++ b/src/runtime/Container.spec.js @@ -1,26 +1,219 @@ import Container from "./Container"; -jest.mock("./processConfig"); -jest.mock("./adjustContainer"); +console.warn = jest.fn(); +jest.mock("./processConfig", () => jest.fn(config => config)); +jest.mock("./adjustContainer", () => jest.fn()); +jest.mock("raf", () => jest.fn(cb => cb())); +jest.mock("es6-weak-map", () => { + const mock = jest.fn(); -test("appropriate instantiation", () => { - const processConfig = require("./processConfig").default; - const adjustContainer = require("./adjustContainer").default; + mock.prototype.set = jest.fn(); + mock.prototype.get = jest.fn(); + mock.prototype.has = jest.fn(); + mock.prototype.delete = jest.fn(); + + return mock; +}); + +jest.mock("resize-observer-polyfill", () => { + const mock = jest.fn(cb => { + mock.triggerEvent = cb; + }); + + mock.prototype.observe = jest.fn(); + mock.prototype.unobserve = jest.fn(); + + return mock; +}); + +jest.mock("mutation-observer", () => { + const mock = jest.fn(cb => { + mock.triggerEvent = cb; + }); + + mock.prototype.observe = jest.fn(); + mock.prototype.unobserve = jest.fn(); + + return mock; +}); + +beforeEach(() => { + require("raf").mockClear(); + require("./adjustContainer").mockClear(); +}); + +test("should instantiate properly", () => { + const ResizeObserver = require("resize-observer-polyfill"); + const processConfig = require("./processConfig"); + const adjustContainer = require("./adjustContainer"); + const raf = require("raf"); + + const containerElement = { + parentNode: document.createElement("div") + }; - const containerElement = {}; const config = {}; const processedConfig = {}; - processConfig.mockImplementation(() => processedConfig); const containerInstance = new Container(containerElement, config); containerInstance.adjust(); containerInstance.adjust(); containerInstance.adjust(); + expect(ResizeObserver).toHaveBeenCalledTimes(1); + expect(ResizeObserver.prototype.observe).toHaveBeenCalledTimes(0); + expect(raf).toHaveBeenCalledTimes(1); expect(processConfig).toHaveBeenCalledTimes(1); + expect(processConfig.mock.calls[0][0]).toBe(config); expect(adjustContainer).toHaveBeenCalledTimes(4); expect(adjustContainer.mock.calls[0][0]).toBe(containerElement); - expect(adjustContainer.mock.calls[0][1]).toBe(processedConfig); + expect(adjustContainer.mock.calls[0][1]).toEqual(processedConfig); expect(adjustContainer.mock.calls[1][0]).toBe(containerElement); - expect(adjustContainer.mock.calls[1][1]).toBe(processedConfig); + expect(adjustContainer.mock.calls[1][1]).toEqual(processedConfig); +}); + +test("should be able to observe resize events and switch off initial adjust call", () => { + const ResizeObserver = require("resize-observer-polyfill"); + const raf = require("raf"); + const adjustContainer = require("./adjustContainer"); + + const containerElement = { + parentNode: document.createElement("div") + }; + const config = {}; + + const containerInstance = new Container(containerElement, config, { + adjustOnInstantiation: false, + adjustOnResize: true + }); + containerInstance.observeResize(); + containerInstance.unobserveResize(); + containerInstance.observeResize(); + + expect(raf).toHaveBeenCalledTimes(0); + expect(adjustContainer).toHaveBeenCalledTimes(0); + expect(ResizeObserver).toHaveBeenCalledTimes(1); + expect(ResizeObserver.prototype.observe).toHaveBeenCalledTimes(3); + expect(ResizeObserver.prototype.unobserve).toHaveBeenCalledTimes(1); + expect(ResizeObserver.prototype.observe.mock.calls[0][0]).toBe( + containerElement + ); + expect(ResizeObserver.prototype.observe.mock.calls[1][0]).toBe( + containerElement + ); + expect(ResizeObserver.prototype.observe.mock.calls[2][0]).toBe( + containerElement + ); + expect(ResizeObserver.prototype.unobserve.mock.calls[0][0]).toBe( + containerElement + ); +}); + +test("should call adjust() on resize changes", () => { + const WeakMap = require("es6-weak-map"); + WeakMap.prototype.set = jest.fn(); + WeakMap.prototype.get.mockImplementationOnce(() => undefined); + WeakMap.prototype.get.mockImplementationOnce(element => { + expect(element).toBe(containerElement); + + return containerInstance; + }); + const ResizeObserver = require("resize-observer-polyfill"); + const parentElement = document.createElement("div"); + const containerElement = document.createElement("div"); + parentElement.appendChild(containerElement); + const config = {}; + const containerInstance = new Container(containerElement, config, { + adjustOnInstantiation: false, + adjustOnResize: true + }); + expect(WeakMap.prototype.set).toHaveBeenCalledTimes(1); + expect(WeakMap.prototype.set).toHaveBeenCalledWith( + containerElement, + containerInstance + ); + + expect(ResizeObserver).toHaveBeenCalledTimes(1); + expect(typeof ResizeObserver.triggerEvent).toBe("function"); + expect(() => ResizeObserver.triggerEvent()).not.toThrow(); + expect(() => { + ResizeObserver.triggerEvent([ + { + target: "" + } + ]); + }).not.toThrow(); + expect(console.warn).toHaveBeenCalledTimes(1); + + containerInstance.adjust = jest.fn(); + expect(() => { + ResizeObserver.triggerEvent([ + { + target: containerElement, + contentRect: { + width: 1, + height: 2 + } + } + ]); + }).not.toThrow(); + expect(console.warn).toHaveBeenCalledTimes(1); + expect(WeakMap.prototype.get).toHaveBeenCalledTimes(2); + expect(containerInstance.adjust).toHaveBeenCalledTimes(1); + expect(containerInstance.adjust).toHaveBeenCalledWith({ + width: 1, + height: 2 + }); +}); + +test("should clean up after container element is detached from the DOM", () => { + const WeakMap = require("es6-weak-map"); + WeakMap.prototype.set = jest.fn(); + WeakMap.prototype.has = jest.fn(() => true); + const MutationObserver = require("mutation-observer"); + const ResizeObserver = require("resize-observer-polyfill"); + ResizeObserver.prototype.unobserve = jest.fn(); + const parentElement = document.createElement("div"); + const containerElement = document.createElement("div"); + parentElement.appendChild(containerElement); + const config = {}; + const containerInstance = new Container(containerElement, config, { + adjustOnInstantiation: false, + adjustOnResize: false + }); + expect(WeakMap.prototype.set).toHaveBeenCalledTimes(1); + expect(WeakMap.prototype.set).toHaveBeenCalledWith( + containerElement, + containerInstance + ); + + let mutationRecords = [ + { + removedNodes: [containerElement] + } + ]; + + MutationObserver.triggerEvent(mutationRecords); + + expect(WeakMap.prototype.has).toHaveBeenCalledTimes(1); + expect(WeakMap.prototype.delete).toHaveBeenCalledTimes(1); + expect(ResizeObserver.prototype.unobserve).toHaveBeenCalledTimes(1); + expect(WeakMap.prototype.delete).toHaveBeenCalledWith(containerElement); + expect(ResizeObserver.prototype.unobserve).toHaveBeenCalledWith( + containerElement + ); + + // Should not clean up after non-container elements + mutationRecords = [ + { + removedNodes: [document.createElement("div")] + } + ]; + + WeakMap.prototype.has = jest.fn(() => false); + MutationObserver.triggerEvent(mutationRecords); + + expect(WeakMap.prototype.has).toHaveBeenCalledTimes(1); + expect(WeakMap.prototype.delete).toHaveBeenCalledTimes(1); + expect(ResizeObserver.prototype.unobserve).toHaveBeenCalledTimes(1); }); diff --git a/src/runtime/adjustContainer.js b/src/runtime/adjustContainer.js index 204be2b..a074d8d 100644 --- a/src/runtime/adjustContainer.js +++ b/src/runtime/adjustContainer.js @@ -11,13 +11,21 @@ import applyStylesToElements from "./applyStylesToElements"; * @param {HTMLElement} container * @param {Object} [config] Expects a configuration object that was processed * (and validated) by `processConfig` + * @param {ContainerDimensions} [containerDimensions] */ -export default function adjustContainer(container, config = null) { +export default function adjustContainer( + container, + config = null, + containerDimensions = null +) { if (config === null) { return; } - const containerDimensions = getContainerDimensions(container); + if (!containerDimensions) { + containerDimensions = getContainerDimensions(container); + } + const queriesLength = config.queries.length; const changeSets = {}; diff --git a/src/runtime/adjustContainer.spec.js b/src/runtime/adjustContainer.spec.js index e2e5dca..3d1f791 100644 --- a/src/runtime/adjustContainer.spec.js +++ b/src/runtime/adjustContainer.spec.js @@ -77,6 +77,20 @@ beforeEach(() => { require("./getContainerDimensions").default.mockClear(); }); +test("should accept container dimensions", () => { + const getContainerDimensions = require("./getContainerDimensions").default; + let config = { + queries: [] + }; + + const container = {}; + const containerDimensions = { width: 1, height: 2 }; + + adjustContainer(container, config, containerDimensions); + + expect(getContainerDimensions).toHaveBeenCalledTimes(0); +}); + test("The container and its elements should be properly adjusted with the defaults", () => { const getContainerDimensionsMock = require( "./getContainerDimensions" diff --git a/yarn.lock b/yarn.lock index 89396bc..803109f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1065,7 +1065,7 @@ es6-symbol@3.1.1, es6-symbol@^3.1, es6-symbol@^3.1.1, es6-symbol@~3.1, es6-symbo d "1" es5-ext "~0.10.14" -es6-weak-map@^2.0.1: +es6-weak-map@^2.0.1, es6-weak-map@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/es6-weak-map/-/es6-weak-map-2.0.2.tgz#5e3ab32251ffd1538a1f8e5ffa1357772f92d96f" dependencies: @@ -2407,6 +2407,10 @@ ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" +mutation-observer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/mutation-observer/-/mutation-observer-1.0.2.tgz#5059b3836180cced1d8f74efd7b3aaf7fa678841" + mute-stream@0.0.5: version "0.0.5" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.5.tgz#8fbfabb0a98a253d3184331f9e8deb7372fac6c0" @@ -2665,6 +2669,10 @@ performance-now@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5" +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + pify@^2.0.0, pify@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" @@ -2759,6 +2767,12 @@ qs@~6.4.0: version "6.4.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" +raf@^3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/raf/-/raf-3.3.2.tgz#0c13be0b5b49b46f76d6669248d527cf2b02fe27" + dependencies: + performance-now "^2.1.0" + randomatic@^1.1.3: version "1.1.6" resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-1.1.6.tgz#110dcabff397e9dcff7c0789ccc0a49adf1ec5bb" @@ -2955,6 +2969,10 @@ require-uncached@^1.0.2: caller-path "^0.1.0" resolve-from "^1.0.0" +resize-observer-polyfill@^1.4.2: + version "1.4.2" + resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.4.2.tgz#a37198e6209e888acb1532a9968e06d38b6788e5" + resolve-from@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-1.0.1.tgz#26cbfe935d1aeeeabb29bc3fe5aeb01e93d44226"