Skip to content

Allow overridden styles to re-order in generated CSS#279

Merged
lencioni merged 3 commits into
masterfrom
media-queries
Oct 11, 2017
Merged

Allow overridden styles to re-order in generated CSS#279
lencioni merged 3 commits into
masterfrom
media-queries

Conversation

@lencioni
Copy link
Copy Markdown
Collaborator

We ran into a weird scenario, where if you passed the following two
objects to css() in this order:

{
  '@media all and (min-width: 1128px)': {},
},
{
  '@media all and (min-width: 744px)': {
    backgroundColor: 'red',
  },
  '@media all and (min-width: 1128px)': {
    backgroundColor: 'blue',
  },
}

you would normally expect it to produce a style that has the 744px
min-width media query first. Unfortunately, because the first object
already had that media query as an ordered key in its OrderedElements
object, it maintained its original position. This caused the 1128px
media query to be output first, which is unexpected and ended up causing
a visual bug.

To fix this, I modified OrderedElements to always move overridden keys
to the end. This now behaves more like you would expect regular CSS to
work.

I needed to add a special case for string handlers, since they aren't
actually overriding a style but rather replacing a property with a newly
computed value.

cc @DanielGarcia-Carrillo

This method works by mutating its first argument. The return value is
never used here, so I am removing it to simplify.
@lencioni lencioni added the bug label Oct 11, 2017
@lencioni lencioni requested a review from xymostech October 11, 2017 22:58
Comment thread src/ordered-elements.js Outdated
}

set(key /* : string */, value /* : any */) {
set(key /* : string */, value /* : any */, preserveOrder /* : boolean */) {
Copy link
Copy Markdown
Contributor

@xymostech xymostech Oct 11, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, could we maybe have preserveOrder default to true and set it to false in all of the places we explicitly want it to override? I think I'd assume that preserveOrder would be the default in something called OrderedElements...

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We want this to be false in every place except one: string handlers. I'm happy to flip it to be reorder or shouldReorder and make sure that it is always passed in though if you think that is better.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright, I added a commit that inverts this!

Comment thread src/ordered-elements.js Outdated
this.keyOrder.push(key);
} else if (!preserveOrder) {
const index = this.keyOrder.indexOf(key);
this.keyOrder.push(this.keyOrder.splice(index, 1)[0]);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think splitting this into two lines would be clearer

this.keyOrder.splice(index, 1);
this.keyOrder.push(key);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, good call.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oops, argh, I meant to put this into a review. Other comments incoming...

Comment thread tests/ordered-elements_test.js Outdated

elems.set("a", 1);
elems.set("b", 1);
elems.set("a", 2, { preserveOrder: true });
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this isn't how the API works, right? It should just be a true here

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, right. This was an earlier revision. I'll fix now.

We ran into a weird scenario, where if you passed the following two
objects to css() in this order:

```js
{
  '@media all and (min-width: 1128px)': {},
},
{
  '@media all and (min-width: 744px)': {
    backgroundColor: 'red',
  },
  '@media all and (min-width: 1128px)': {
    backgroundColor: 'blue',
  },
}
```

you would normally expect it to produce a style that has the 744px
min-width media query first. Unfortunately, because the first object
already had that media query as an ordered key in its OrderedElements
object, it maintained its original position. This caused the 1128px
media query to be output first, which is unexpected and ended up causing
a visual bug.

To fix this, I modified OrderedElements to always move overridden keys
to the end. This now behaves more like you would expect regular CSS to
work.

I needed to add a special case for string handlers, since they aren't
actually overriding a style but rather replacing a property with a newly
computed value.
As @xymostech pointed out in code review

> I'd assume that preserveOrder would be the default in something called
> OrderedElements

This makes sense to me, so I am inverting this boolean and explicitly
passing it in everywhere it is used now.
Comment thread src/ordered-elements.js Outdated
}

set(key /* : string */, value /* : any */) {
set(key /* : string */, value /* : any */, preserveOrder /* : ?boolean */) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we make preserveOrder default to true and then explicitly set it to false in the places where we're overriding? I think it makes more sense for something called OrderedElements to preserve order by default.

Comment thread src/generate.js
stringHandlers /* : StringHandlers */,
selectorHandlers /* : SelectorHandler[] */
) /* : OrderedElements */ => {
) /* : void */ => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Comment thread tests/generate_test.js
}
}`, defaultSelectorHandlers);
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we have a test to make sure string handlers still work correctly?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are some tests for string handlers. Is there a specific test you think I should add?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to make sure that string handlers don't end up at the end of style declarations. I'm less worried about this now that the argument is inverted, though. :)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, there is a test that verifies that already. That's actually how I ended up making this an argument in the first place since I broke that test in my initial pass.

@lencioni lencioni merged commit 1ca597d into master Oct 11, 2017
@lencioni lencioni deleted the media-queries branch October 11, 2017 23:49
@lencioni
Copy link
Copy Markdown
Collaborator Author

Thanks for the quick review!

@xymostech
Copy link
Copy Markdown
Contributor

You're welcome! Thanks for fixing bugs! ❤️

@Soreine
Copy link
Copy Markdown
Contributor

Soreine commented Dec 29, 2017

@lencioni I have run into this behavior just now. Let me state a related question:

I am making a component <Custom /> that uses a component <Base /> like so:

class Base extends React.Component {
    props: {
        styles: SheetDefinitions
    };

    render() {
        return (
            <div
                className={css(
                    {
                        '@media x': {
                            A
                        },
                        '@media y': {
                            B
                        }
                    },
                    this.props.styles
                )}
            />
        );
    }
}

class Custom extends React.Component {
    render() {
        return (
            <Base
                styles={{
                    '@media x': {
                        C
                    }
                }}
            />
        );
    }
}

I would like the generated CSS to be in this order (not to merge the media queries):

@media x {
  A
}

@media y {
  B
}

@media x {
  C
}

Instead, I get:

@media y {
  B
}

@media x {
  A,
  C
}

How does one avoid the merging of similar media queries ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants