Skip to content
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

Improve addVariant API #5809

Merged
merged 15 commits into from
Oct 18, 2021
Merged

Improve addVariant API #5809

merged 15 commits into from
Oct 18, 2021

Conversation

RobinMalfait
Copy link
Member

@RobinMalfait RobinMalfait commented Oct 16, 2021

This PR will improve and drastically simplify the addVariant API. This is an API that is important to us internally, and also for plugin authors. If you don't write plugins yourself, then this PR doesn't really matter to you.

The API we had was very verbose, but it made sense if you think about how AOT mode worked. In AOT mode, we generated all the css you could ever need. This means that we didn't know what you were actually using. Plugin authors then had to keep multiple things into account. For starters they need to know how a selector is structured, for example first-letter${config('separator')}${className}. For JIT mode this doesn't really make sense, because we start from the content files. This means that we already know the final className (because we extracted it from the content files), and rebuilding this className by hand means that you can only do it perfect, or introduce errors if you for example forget the old className, or if you forget to read the separator from the tailwind.config.js file.

Long story short, we can hugely simplify this...

I will get into more API details soon, but let's just look at some before/after implementations for existing variants.

First Letter

A "simple" variant, that adds a pseudo element.

Before:

addVariant(
   'first-letter',
   transformAllSelectors((selector) => {
     return updateAllClasses(selector, (className, { withPseudo }) => {
       return withPseudo(`first-letter${config('separator')}${className}`, '::first-letter')
     })
   })
 )

After:

addVariant('first-letter', '&::first-letter')

Marker

A variant that has parallel variants.

Before:

addVariant('marker', [
   transformAllSelectors((selector) => {
     let variantSelector = updateAllClasses(selector, (className) => {
       return `marker${config('separator')}${className}`
     })

     return `${variantSelector} *::marker`
   }),
   transformAllSelectors((selector) => {
     return updateAllClasses(selector, (className, { withPseudo }) => {
       return withPseudo(`marker${config('separator')}${className}`, '::marker')
     })
   }),
 ])

After:

addVariant('marker', ['& *::marker', '&::marker'])

Group hover

A variant that adds a parent selector, but should also re-use he existing .group if it already exists.

Before:

let groupMarker = prefixSelector(config('prefix'), '.group')

addVariant(
 'group-hover',
 transformAllSelectors((selector) => {
   let variantSelector = updateAllClasses(selector, (className) => {
     if (`.${className}` === groupMarker) return className
     return `group-hover${config('separator')}${className}`
   })

   if (variantSelector === selector) {
     return null
   }

   return applyStateToMarker(
     variantSelector,
     groupMarker,
     ':hover',
     (marker, selector) => `${marker} ${selector}`
   )
 })
)

After:

addVariant(`group-hover`, `:merge(.group):hover &`)

API

The verbose addVariant now has new functions it exposes in the callback.

  • format(/* format string */)
    • The format string has 2 API's that plugin authors need to understand.
    • First, the &, this is a symbol that references the selector it is applying to. We don't need to rebuild the full selector manually, we already have all that information. But now you can prepend/append anything to it.
    • Second, we have a :merge() pseudo, which allows you to merge states together in case you are using multiple variants. Let's explain this with an example instead (1).
  • wrap(/* PostCSS Node, E.g.: AtRule */)

Full example:

addVariant('name', ({ format, wrap }) => {
  // Format the selector, by prepending or appending
  format('.dark &') // Dark mode using `class` implementation
  format('&:hover') // Hover implementation

  // Wrap the current node in an AtRule
  wrap(
    postcss.atRule({
      name: 'media',
      params: '(prefers-reduced-motion: reduce)',
    })
  )
})

Format

The format function accepts a string where you can prepend or append other parts of a selector to the existing selector.

&

The & symbol in the format function is referring to the "previous" or "parent" selector. You don't have to rebuild this full selector yourself, because we already know what the final className should look like.

For example, the hover variant, is implemented like this:

addVariant('hover', ({ format }) => format('&:hover'))

This means that if you are using hover:text-center, that the final result would be .hover\:text-center:hover

Another example is dark mode, using the class mode.

addVariant('dark', ({ format }) => format('.dark &'))

This means that if you are using dark:text-center, that the final result would be .dark .dark\:text-center

:merge()

The :merge(.class) pseudo/function inside your format string allows you to merge with existing classes if they already exist, if not, it would be inserted. Let's explain it with an example.

Imagine you want to implement the group-hover variant:

addVariant('group-hover', ({ format }) => format('.group:hover &'))

This means, that if you are using group-hover:text-center, that your final resul would be .group:hover .group-hover\:text-center. This is correct and what you expect.

However, let's imagine you want to implement the group-focus variant:

addVariant('group-focus', ({ format }) => format('.group:focus &'))

But this time, you are using group-hover:group-focus:text-center, this means that the final result would look like this:
.group:hover .group:focus .group-hover\:group-focus\:text-center, this is incorrect, we don't want two .group classes at the front. What we expect is .group:hover:focus .group-hover\:group-focus\:text-center.
This is where the :merge comes in. We want to merge the .group class in this case.

addVariant('group-hover', ({ format }) => format(':merge(.group):hover &'))
addVariant('group-focus', ({ format }) => format(':merge(.group):focus &'))

We also have two addVariant shorthands.

The current API looks like this:

addVariant('name', ({ format, wrap }) => {
  // Wrap in an atRule
  wrap(postcss.atRule({ name: 'media', params: '(prefers-reduced-motion: reduce)' }))

  // "Mutate" the selector, for example prepend `.dark`
  format('.dark &')
})

It is also pretty common to have this:

addVariant('name', ({ format }) => format('.dark &'))

So we simplified this to:

addVariant('name', '.dark &')

It is also pretty common to have this:

addVariant('name', ({ wrap }) => wrap(postcss.atRule({ name: 'media', params: '(prefers-reduced-motion: reduce)' })))

So we simplified this to:

addVariant('name', '@media (prefers-reduced-motion: reduce)')

The last shorthand is to combine @media and & related formats.

addVariant(
  'magic',
  `
    @supports (hover: hover) {
      @media (print) {
        &:disabled
      }
    }
  `
)
// Or as a one-liner
addVariant('magic', '@supports (hover: hover) { @media (print) { &:disabled } }')

@RobinMalfait RobinMalfait changed the title improve add variant api Improve addVariant API Oct 16, 2021
Probably messed this up in another PR, so just a bit of cleaning.
This will be used to eventually simplify the addVariant API.

The idea is that it can take a list of strings that define a certain
format. Then it squashes everything to a single format how you would
expect it.

E.g.:

Input:
  - '&:hover'
  - '&:focus'
  - '.dark &'
  - ':merge(.group):hover &'
  - ':merge(.group):focus &'
Output:
  - ':merge(.group):focus:hover .dark &:focus:hover'

The API here is:
  - `&`, this means "The parent" or "The previous selector" (you can
    think of it like if you are using nested selectors)
  - `:merge(.group)`, this means insert a `.group` if it doesn't exist
    yet, but if it does exist already, then merge the new value with the
    old value. This allows us to merge group-focus, group-hover into a
    single `.group:focus:hover ...`
This will ensure that the backwards compatibility for `modifySelectors`
and direct mutations to the `container` will still work.

We will try to capture the changes made to the `rule.selector`, we will
also "backup" the existing selector. This allows us to diff the old and
new selectors and determine what actually happened.

Once we know this, we can restore the selector to the "old" selector and
add the diffed string e.g.: `.foo &`, to the `collectedFormats` as if
you called `format()` directly. This is a bunch of extra work, but it
allows us to be backwards compatible.

In the future we could also warn if you are using `modifySelectors`, but
it is going to be a little bit tricky, because usually that's
implemented by plugin authors and therefore you don't have direct
control over this. Maybe we can figure out the plugin this is used in
and change the warning somehow?
This was clearly a bug, keyframes should not include escaped variants at
all. The reason this is here in the first place is because the nodes in
a keyframe are also "rule" nodes.
The current implementation had a strange side effect, that resulted in
incorrect class definitions. When you are combining the `:hover` and
`:focus` event, then there is no difference between `:hover:focus` and
`:focus:hover`.

However, when you use `:hover::file-selector-button` or `::file-selector-button:hover`,
then there is a big difference. In the first place, you can hover over the full file input
to apply changes to the `File selector button`.
In the second scenario you have to hover over the `File selector button` itself to apply changes.

You can think of it as function calls:
- focus(hover(text-center))

What you would expect is something like this:
`.focus\:hover\:text-center:hover:focus`, where `hover` is on the
inside, and `focus` is on the outside. However in the current
implementation this is implemented as
`.focus\:hover\:text-cener:focus:hover`
We can get rid of this because we drastically simplified the new
addVariant API.
The current API looks like this:

```js
addVariant('name', ({ format, wrap }) => {
  // Wrap in an atRule
  wrap(postcss.atRule({ name: 'media', params: '(prefers-reduced-motion: reduce)' }))

  // "Mutate" the selector, for example prepend `.dark`
  format('.dark &')
})
```

It is also pretty common to have this:
```js
addVariant('name', ({ format }) => format('.dark &'))
```
So we simplified this to:
```js
addVariant('name', '.dark &')
```

It is also pretty common to have this:
```js
addVariant('name', ({ wrap }) => wrap(postcss.atRule({ name: 'media', params: '(prefers-reduced-motion: reduce)' })))
```
So we simplified this to:
```js
addVariant('name', '@media (prefers-reduced-motion: reduce)')
```
We will use `@defaults`, so that only the resets are injected for the
utilities we actually use.
This will allow to write something like:

```js
addVariant('name', `
  @supports (hover: hover) {
    @media (print) {
      &:hover
    }
  }
`)
// Or as a one-liner
addVariant('name', '@supports (hover: hover) { @media (print) { &:hover } }')
```
@RobinMalfait RobinMalfait merged commit 5809c4d into master Oct 18, 2021
@RobinMalfait RobinMalfait deleted the improve-add-variant-api branch October 18, 2021 09:26
david-crespo added a commit to oxidecomputer/console that referenced this pull request Nov 16, 2021
david-crespo added a commit to oxidecomputer/console that referenced this pull request Nov 17, 2021
* upgrade to tailwind 3.0 alpha, make config changes

* rewrite svg: w/ new addVariant API tailwindlabs/tailwindcss#5809

* first first-of-type:before: order issue (tw fix in next alpha)

tailwindlabs/tailwindcss#6112
tailwindlabs/tailwindcss#6016
tailwindlabs/tailwindcss#6018

* replace tailwindcss-children plugin with new custom addVariant

* oh yeah... use stroke-green-500

* each variant doesn't need its own plugin

* update yarn.lock after merging main
seanpdoyle added a commit to thoughtbot/tailwindcss-aria-attributes that referenced this pull request Jul 28, 2022
Re-implement variants using the newly String-based `addVariant` plugin
method changed in [tailwindlabs/tailwindcss#5809][].

[tailwindlabs/tailwindcss#5809]: tailwindlabs/tailwindcss#5809
seanpdoyle added a commit to thoughtbot/tailwindcss-aria-attributes that referenced this pull request Jul 28, 2022
Re-implement variants using the newly String-based `addVariant` plugin
method changed in [tailwindlabs/tailwindcss#5809][].

[tailwindlabs/tailwindcss#5809]: tailwindlabs/tailwindcss#5809
seanpdoyle added a commit to thoughtbot/tailwindcss-aria-attributes that referenced this pull request Jul 28, 2022
Re-implement variants using the newly String-based `addVariant` plugin
method changed in [tailwindlabs/tailwindcss#5809][].

[tailwindlabs/tailwindcss#5809]: tailwindlabs/tailwindcss#5809
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant