Skip to content

Scoped style attribute not attached to selector key if it's a pseudo-class/element #8868

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
kleinfreund opened this issue Jul 28, 2023 · 5 comments
Labels
has PR A pull request has already been submitted to solve the issue has workaround A workaround has been found to avoid the problem 🐞 bug Something isn't working 🔩 p2-edge-case scope: compiler scope: sfc

Comments

@kleinfreund
Copy link
Contributor

kleinfreund commented Jul 28, 2023

Vue version

3.3.9

Link to minimal reproduction

https://play.vuejs.org example

Steps to reproduce

  1. Create a scoped style block.
  2. Add a rule set with a selector which has any ancestor selectors and a bare pseudo-class (but not :is() or :where()) as the key (e.g. div :not(.test) not div *:not(.test)).

Example:

<style scoped>
div :not(.test) {
  color: tomato;
}

div :required {
  color: tomato;
}

/* workaround: add `*` to the pseudo class */
div *:required {
  color: tomato;
}
</style>

What is expected?

The generated CSS rule set's selector list is scoped to the key of the selector (i.e. div [data-v-7ba5bd90]:not(.test)) even if the key is a bare pseudo-class or pseudo-element (except in the case of :deep).

div [data-v-7ba5bd90]:not(.test) {
  color: tomato;
}

div [data-v-7ba5bd90]:required {
  color: tomato;
}

div *[data-v-7ba5bd90]:required {
  color: tomato;
}

What is actually happening?

The generated CSS rule set's selector list is scoped to the last selector part that is not a bare pseudo-class (i.e. div[data-v-7ba5bd90] :not(.test)).

div[data-v-7ba5bd90] :not(.test) {
  color: tomato;
}

div[data-v-7ba5bd90] :required {
  color: tomato;
}

div *[data-v-7ba5bd90]:required {
  color: tomato;
}

This leads to styles unintentionally leaking out of the scoped tree.

System Info

Binaries:
    Node: 19.1.0 - C:\Program Files\nodejs\node.EXE
    Yarn: 1.22.19 - C:\Program Files\nodejs\yarn.CMD
    npm: 9.8.0 - ~\dev\package\node_modules\.bin\npm.CMD
    pnpm: 8.5.1 - C:\Program Files\nodejs\pnpm.CMD
  Browsers:
    Edge: Spartan (44.22621.1992.0), Chromium (115.0.1901.183)
    Internet Explorer: 11.0.22621.1

Any additional comments?

  • This seems to be true for most pseudo-classes and pseudo-elements, but isn't true for :is(), :where(), and the special case :deep()
  • Initially posted as a question here: Should "div :is(*)" in a scoped style block produce a different selector? #8800
  • The current behavior is unexpected to me when writing scoped styles as it easily allows styles to leak out of the scoped tree (e.g. when using slots)
@kleinfreund kleinfreund changed the title Scoped style attribute not attached to pseudo-class/element selectors Scoped style attribute not attached to selector key if it's a pseudo-class/element Jul 28, 2023
@edison1105
Copy link
Member

This is as expected (align with the original behavior of :not or :required).

div [data-v-7ba5bd90]:not(.test) {
  color: tomato;
}

div[data-v-7ba5bd90] :not(.test) {
  color: tomato;
}

The above two rules are different. Whether we use scoped or not, we cannot change its original behavior.

@kleinfreund
Copy link
Contributor Author

I understand that div [data-v-7ba5bd90]:not(.test) and div[data-v-7ba5bd90] :not(.test) are different and that’s why the behavior of scoped styles with regards to pseudo classes is unexpected to me.

The two selectors div :not(.test) and div *:not(.test) are equivalent in all but structure. They have the same specificity and select the same nodes. I would therefore expect them to behave the same using Vue scoped styles, but they don’t. They generate different selectors not just in structure but also meaning.

div *:not(.test) behaves well, it generates div *[data-v-7ba5bd90]:not(.test). However, div :not(.test) doesn’t behave well: it generates div[data-v-7ba5bd90] :not(.test) when it should generate div [data-v-7ba5bd90]:not(.test).

That may lead to problems because it allows styles to be applied to a tree that’s not part of the component's scope. In other words, div :not(.test) behaves like div :deep(:not(.test)).

@Justineo
Copy link
Member

I think this is a bug. The data-v-* attribute selector should be added to the selector subjects.

@Justineo Justineo added the 🐞 bug Something isn't working label Nov 27, 2023
@kleinfreund
Copy link
Contributor Author

A few test cases for packages/compiler-sfc/__tests__/compileStyle.spec.ts that currently fail:

describe('SFC scoped CSS', () => {
  test.each([
    {
      message: 'pseudo class with class',
      source: `.foo:after { color: red; }`,
      expected: `.foo:after[data-v-test] { color: red;`
    },
    {
      message: 'bare pseudo class at root',
      source: `:after { color: red; }`,
      expected: `:after[data-v-test] { color: red;`
    },
    {
      message: 'bare pseudo class as descendent',
      source: `div :required { color: red; }`,
      expected: `div :required[data-v-test] { color: red;`
    },
    {
      message: 'pseudo class with tag as descendent',
      source: `div input:required { color: red; }`,
      expected: `div input:required[data-v-test] { color: red;`
    },
    {
      message: ':not pseudo class',
      source: `input:not(.error) { color: red; }`,
      expected: `input:not(.error)[data-v-test] { color: red;`
    },
  ])('$message', ({ source, expected }) => {
    expect(compileScoped(source)).toMatch(expected)
  })
})

Relevant code:

if (n.type !== 'pseudo' && n.type !== 'combinator') {
node = n
}

I think the following change would fix this:

-     if (n.type !== 'pseudo' && n.type !== 'combinator') {
+     if (n.type !== 'combinator') {

It comes with the caveat that the generated [data-v-...] attribute would be come after the pseudo class (e.g. input:not(.error) { color: red; } produces input:not(.error)[data-v-test] and not input[data-v-test]:not(.error)). However, this should be a cosmetic difference only as the selectors are correct and valid both ways.

I'd be happy to contribute this if you want. I’m not totally sure if the approach is correct though.

@edison1105
Copy link
Member

@kleinfreund
uh~, I got your point. PR welcome~

kleinfreund pushed a commit to kleinfreund/vue-core that referenced this issue Nov 28, 2023
Fix a scoped style leak when using pseudo classes caused by the scoped style attribute (i.e. `[data-v-...]`) not being attached to the key part of a selector when it contains pseudo class selectors.

Closes vuejs#8868.
kleinfreund pushed a commit to kleinfreund/vue-core that referenced this issue Dec 4, 2023
Fix a scoped style leak when using pseudo classes caused by the scoped style attribute (i.e. `[data-v-...]`) not being attached to the key part of a selector when it contains pseudo class selectors.

Closes vuejs#8868.
kleinfreund pushed a commit to kleinfreund/vue-core that referenced this issue Jan 3, 2024
Fix a scoped style leak when using pseudo classes caused by the scoped style attribute (i.e. `[data-v-...]`) not being attached to the key part of a selector when it contains pseudo class selectors.

Closes vuejs#8868.
@haoqunjiang haoqunjiang added has workaround A workaround has been found to avoid the problem 🔩 p2-edge-case has PR A pull request has already been submitted to solve the issue scope: sfc labels Apr 3, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
has PR A pull request has already been submitted to solve the issue has workaround A workaround has been found to avoid the problem 🐞 bug Something isn't working 🔩 p2-edge-case scope: compiler scope: sfc
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants