emotion icon indicating copy to clipboard operation
emotion copied to clipboard

Don't add classname for `:is()` and `:where()` selectors

Open siriwatknp opened this issue 3 years ago • 13 comments

The problem

I want to leverage :where selector (it is good for theming because it has 0 specificity) to style a component based on the parent attribute.

However, emotion prefix the style with the generated class name in front of :where which does not make the style works:

const Component = style('div')`
  color: black
  :where([data-color-scheme="dark"]) & {
    color: white
  }
`

turned into

<style>
// to make this style work, the `.css-9qpay1` at the beginning should be removed
.css-9qpay1:where([data-color-scheme="dark"]) .css-9qpay1{color:white;}
</style>

Here is the reproduction: https://codesandbox.io/s/emotion-forked-e9fyes?file=/index.js

Proposed solution

Should :is and :where be the exceptional case to not implicitly append to the class name?

const Component = style('div')`
  :where([data-color-scheme="dark"]) & {
    color: white
  }
  // result as ':where([data-color-scheme="dark"]) .css { color: white };
`

const Component = style('div')`
  // explicitly declare the '&'
  &:where([data-color-scheme="dark"]) {
    color: white
  }
  // result as '.css:where([data-color-scheme="dark"]) .css { color: white };
`

Alternative solutions

Is there any workaround that I might have missed?

Additional context

Hi, I am from the MUI core team. We are working on the theming API to let developers theme components based on color schemes without having to read the mode from the theme.

Our API added an attribute to the html when the color scheme changes:

// light mode
<html data-color-scheme="light">

// dark mode
<html data-color-scheme="dark">

siriwatknp avatar Jul 24 '22 06:07 siriwatknp

This is a byproduct of our compat plugin - however, this matches the behavior of Emotion 10 for all pseudo-selectors. I'd like to remove this compat plugin in the next major version of Emotion but I'm worried that changing the current behavior, even for a subset of pseudo-selectors, would be a breaking change that we can't do (we can continue discussing this though, those 2 are fairly new additions and it somewhat makes sense to treat them differently).

Right now you sort of can achieve this by combining namespace plugin and :global(): https://codesandbox.io/s/emotion-forked-rc5rpb?file=/index.js . Note that the generated rule has increased specificity because it uses the class name twice - maybe this could be fixed

Andarist avatar Jul 24 '22 08:07 Andarist

@Andarist Thanks a lot for the info.

Note that the generated rule has increased specificity because it uses the class name twice - maybe this could be fixed

Can you point me to where to fix this? I can submit a PR.

Note, from what I check styled-components does not work as well.

siriwatknp avatar Jul 25 '22 03:07 siriwatknp

My guess is that the problem is in this compat plugin: https://github.com/emotion-js/emotion/blob/1554a7e264e05780b2c5bd74ccb20a92005ba61d/packages/cache/src/stylis-plugins.js#L88

But I didn't yet analyze this in full.

One alternative solution that I was thinking about (since adding the namespace plugin in MUI might be problematic in the long run) is to provide a way to "escape" a pseudo selector somehow so we could ignore it in the compat plugin (since it's the one that automatically prepends implicit & to all rules starting with :). Any ideas on what kind of a token we could use there?

Andarist avatar Jul 25 '22 09:07 Andarist

I can think of:

  • ~: it is the root dir in the terminal which could refer to global (this might not work, it refers to general sibling selectors)
  • *: this looks like global (this might not work, it refers to the universal selector in CSS)
  • !: or with the negate symbol to opt-out of implicit &

The usage would look like this, right?

const Component = style('div')`
  color: black
  ~:where([data-color-scheme="dark"]) & {
    color: white
  }
`

siriwatknp avatar Jul 25 '22 10:07 siriwatknp

I found a workaround! https://codesandbox.io/s/emotion-forked-6eco3g?file=/index.js

siriwatknp avatar Aug 08 '22 12:08 siriwatknp

Note that this might be prone to some edge cases but it probably shouldn't matter for real-life cases in which you are gonna use this.

Andarist avatar Aug 08 '22 12:08 Andarist

After thinking about this a little bit - I think that a comment annotation would be the easiest solution for this. Something like:

const Component = style('div')`
  color: black
  :where([data-color-scheme="dark"]) & {
    /* emotion-no-compat */
    color: white
  }
`

I think I'm gonna add a dev-only warning for all instances of the "compat" behavior kicking in so people can start fixing their selectors in v11 but I can't change this default right now.

Note that whatever we could do about this in Emotion right now would require a new v11 minor version - and, from what I understand, you need this in MUI and you don't control which version of Emotion people have installed there as its a peer dep. So I'm unsure if any solution from our side would actually solve your issue - unless you expect people to update Emotion frequently.

Andarist avatar Aug 22 '22 10:08 Andarist

I think that a comment annotation would be the easiest solution for this.

We have a util function that produces :where([data-color-scheme="dark"]) &. Does it mean that developers have to specify the comment all the time?:

styled('div')(({ theme }) => ({
  // theme.getColorSchemeSelector('dark') => ':where([data-color-scheme="dark"]) &'
  [theme.getColorSchemeSelector('dark')]: {
    /* emotion-no-compat */
    // ...styles
  }
}))

siriwatknp avatar Jan 04 '23 04:01 siriwatknp

It may not work for all use-cases, but in case anyone else comes looking, I've had success with using the * universal selector before the :where() pseudoclass along with the & class selector to apply the style to the desired class like so:

* :where() & {...

const  texStyle = css`
  color: red;
  * :where([data-your-theme-value="example"]) & {
    color: blue;
   }
`

() => (
  <>
    <div data-your-theme-value='example'>
      <span css={textStyle}> Blue Text Here </span>
    </div>
    <span css={textStyle}>Red Text Here</span>
  </>
)

See the codesandbox PoC here

ByronDWall avatar Jan 27 '23 18:01 ByronDWall

It may not work for all use-cases, but in case anyone else comes looking, I've had success with using the * universal selector before the :where() pseudoclass along with the & class selector to apply the style to the desired class like so:

* :where() & {...


const  texStyle = css`

  color: red;

  * :where([data-your-theme-value="example"]) & {

    color: blue;

   }

`



() => (

  <>

    <div data-your-theme-value='example'>

      <span css={textStyle}> Blue Text Here </span>

    </div>

    <span css={textStyle}>Red Text Here</span>

  </>

)

See the codesandbox PoC here

Oh, great. Thanks for letting me know.

siriwatknp avatar Jan 28 '23 02:01 siriwatknp

After thinking about this a little bit - I think that a comment annotation would be the easiest solution for this. Something like:

const Component = style('div')`
  color: black
  :where([data-color-scheme="dark"]) & {
    /* emotion-no-compat */
    color: white
  }
`

I think I'm gonna add a dev-only warning for all instances of the "compat" behavior kicking in so people can start fixing their selectors in v11 but I can't change this default right now.

Unfortunately, this solution breaks down completely when using Emotion object notation.

<div
  css={{
    color: 'black',
    ':where([data-color-scheme="dark"]) &': {
      /* emotion-no-compat */              //  <== this is just a JS comment; will not be interpreted by Emotion.
      color: 'white',
    }
  }}
>

ivanjonas avatar Jun 27 '23 13:06 ivanjonas