Preserve explicit transition duration and timing function when overriding transition property (#14490)

This PR changes the behavior of the `transition-{property}` utilities to
respect any explicit timing function or duration set by the user using
the `ease-*` and `duration-*` utilities.

Say you have this HTML:

```html
<div class="transition-colors duration-500 ease-out lg:transition-all">
```

Currently, the `transition-duration` and `transition-timing-functions`
will be reset to their default values at the `lg` breakpoint even though
you've provided explicit values for them.

After this PR is merged, those values will be preserved at the `lg`
breakpoint.

This PR also adds `duration-initial` and `ease-initial` utilities to
"unset" explicit duration/timing-function values so that the defaults
from classes like `transition-all` will kick in, without having to
specify their explicit values.

---------

Co-authored-by: Adam Wathan <4323180+adamwathan@users.noreply.github.com>
This commit is contained in:
Adam Wathan 2024-09-23 10:43:28 -04:00 • committed by GitHub
parent 222a93c0a3
commit 6d43a8be99
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 139 additions and 59 deletions

View file

@ -32,6 +32,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Preserve explicit `leading-*`, `tracking-*`, and `font-{weight}` value when overriding font-size ([#14403](https://github.com/tailwindlabs/tailwindcss/pull/14403))
- Disallow negative bare values in core utilities and variants ([#14453](https://github.com/tailwindlabs/tailwindcss/pull/14453))
- Preserve explicit shadow color when overriding shadow size ([#14458](https://github.com/tailwindlabs/tailwindcss/pull/14458))
- Preserve explicit transition duration and timing function when overriding transition property ([#14490](https://github.com/tailwindlabs/tailwindcss/pull/14490))
## [4.0.0-alpha.24] - 2024-09-11

View file

@ -1908,6 +1908,8 @@ exports[`getClassList 1`] = `
"duration-500",
"duration-700",
"duration-75",
"duration-initial",
"ease-initial",
"end-0.5",
"end-1",
"end-3",

View file

@ -12898,47 +12898,47 @@ test('transition', async () => {
.transition {
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, -webkit-backdrop-filter, backdrop-filter;
transition-timing-function: var(--default-transition-timing-function, ease);
transition-duration: var(--default-transition-duration, .1s);
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function, ease));
transition-duration: var(--tw-duration, var(--default-transition-duration, .1s));
}
.transition-\\[--value\\] {
transition-property: var(--value);
transition-timing-function: var(--default-transition-timing-function, ease);
transition-duration: var(--default-transition-duration, .1s);
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function, ease));
transition-duration: var(--tw-duration, var(--default-transition-duration, .1s));
}
.transition-all {
transition-property: all;
transition-timing-function: var(--default-transition-timing-function, ease);
transition-duration: var(--default-transition-duration, .1s);
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function, ease));
transition-duration: var(--tw-duration, var(--default-transition-duration, .1s));
}
.transition-colors {
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to;
transition-timing-function: var(--default-transition-timing-function, ease);
transition-duration: var(--default-transition-duration, .1s);
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function, ease));
transition-duration: var(--tw-duration, var(--default-transition-duration, .1s));
}
.transition-opacity {
transition-property: opacity;
transition-timing-function: var(--default-transition-timing-function, ease);
transition-duration: var(--default-transition-duration, .1s);
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function, ease));
transition-duration: var(--tw-duration, var(--default-transition-duration, .1s));
transition-property: var(--transition-property-opacity, opacity);
transition-timing-function: var(--default-transition-timing-function, ease);
transition-duration: var(--default-transition-duration, .1s);
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function, ease));
transition-duration: var(--tw-duration, var(--default-transition-duration, .1s));
}
.transition-shadow {
transition-property: box-shadow;
transition-timing-function: var(--default-transition-timing-function, ease);
transition-duration: var(--default-transition-duration, .1s);
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function, ease));
transition-duration: var(--tw-duration, var(--default-transition-duration, .1s));
}
.transition-transform {
transition-property: transform, translate, scale, rotate;
transition-timing-function: var(--default-transition-timing-function, ease);
transition-duration: var(--default-transition-duration, .1s);
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function, ease));
transition-duration: var(--tw-duration, var(--default-transition-duration, .1s));
}
.transition-none {
@ -12965,20 +12965,20 @@ test('transition', async () => {
.transition {
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, -webkit-backdrop-filter, backdrop-filter;
transition-duration: .1s;
transition-timing-function: ease;
transition-timing-function: var(--tw-ease, ease);
transition-duration: var(--tw-duration, .1s);
}
.transition-all {
transition-property: all;
transition-duration: .1s;
transition-timing-function: ease;
transition-timing-function: var(--tw-ease, ease);
transition-duration: var(--tw-duration, .1s);
}
.transition-colors {
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to;
transition-duration: .1s;
transition-timing-function: ease;
transition-timing-function: var(--tw-ease, ease);
transition-duration: var(--tw-duration, .1s);
}"
`)
@ -12992,8 +12992,8 @@ test('transition', async () => {
).toMatchInlineSnapshot(`
".transition-all {
transition-property: all;
transition-duration: 0s;
transition-timing-function: ease;
transition-timing-function: var(--tw-ease, ease);
transition-duration: var(--tw-duration, 0s);
}"
`)
@ -13047,15 +13047,31 @@ test('delay', async () => {
test('duration', async () => {
expect(await run(['duration-123', 'duration-200', 'duration-[300ms]'])).toMatchInlineSnapshot(`
".duration-123 {
--tw-duration: .123s;
transition-duration: .123s;
}
.duration-200 {
--tw-duration: .2s;
transition-duration: .2s;
}
.duration-\\[300ms\\] {
--tw-duration: .3s;
transition-duration: .3s;
}
@supports (-moz-orient: inline) {
@layer base {
*, :before, :after, ::backdrop {
--tw-duration: initial;
}
}
}
@property --tw-duration {
syntax: "*";
inherits: false
}"
`)
expect(
@ -13090,15 +13106,31 @@ test('ease', async () => {
}
.ease-\\[--value\\] {
--tw-ease: var(--value);
transition-timing-function: var(--value);
}
.ease-in {
--tw-ease: var(--transition-timing-function-in, cubic-bezier(.4, 0, 1, 1));
transition-timing-function: var(--transition-timing-function-in, cubic-bezier(.4, 0, 1, 1));
}
.ease-out {
--tw-ease: var(--transition-timing-function-out, cubic-bezier(0, 0, .2, 1));
transition-timing-function: var(--transition-timing-function-out, cubic-bezier(0, 0, .2, 1));
}
@supports (-moz-orient: inline) {
@layer base {
*, :before, :after, ::backdrop {
--tw-ease: initial;
}
}
}
@property --tw-ease {
syntax: "*";
inherits: false
}"
`)
expect(

View file

@ -3701,9 +3701,8 @@ export function createUtilities(theme: Theme) {
}
{
let defaultTimingFunction =
theme.resolve(null, ['--default-transition-timing-function']) ?? 'ease'
let defaultDuration = theme.resolve(null, ['--default-transition-duration']) ?? '0s'
let defaultTimingFunction = `var(--tw-ease, ${theme.resolve(null, ['--default-transition-timing-function']) ?? 'ease'})`
let defaultDuration = `var(--tw-duration, ${theme.resolve(null, ['--default-transition-duration']) ?? '0s'})`
staticUtility('transition-none', [['transition-property', 'none']])
staticUtility('transition-all', [
@ -3755,37 +3754,49 @@ export function createUtilities(theme: Theme) {
handle: (value) => [decl('transition-delay', value)],
})
utilities.functional('duration', (candidate) => {
// This utility doesn't support negative values.
if (candidate.negative) return
// This utility doesn't support modifiers.
if (candidate.modifier) return
// This utility doesn't support `DEFAULT` values.
if (!candidate.value) return
// Find the actual CSS value that the candidate value maps to.
let value: string | null = null
if (candidate.value.kind === 'arbitrary') {
value = candidate.value.value
} else {
value = theme.resolve(candidate.value.fraction ?? candidate.value.value, [
'--transition-duration',
])
if (value === null && isPositiveInteger(candidate.value.value)) {
value = `${candidate.value.value}ms`
}
{
let transitionDurationProperty = () => {
return atRoot([property('--tw-duration')])
}
// If the candidate value (like the `sm` in `max-w-sm`) doesn't resolve to
// an actual value, don't generate any rules.
if (value === null) return
staticUtility('duration-initial', [transitionDurationProperty, ['--tw-duration', 'initial']])
return [decl('transition-duration', value)]
})
utilities.functional('duration', (candidate) => {
// This utility doesn't support negative values.
if (candidate.negative) return
// This utility doesn't support modifiers.
if (candidate.modifier) return
// This utility doesn't support `DEFAULT` values.
if (!candidate.value) return
// Find the actual CSS value that the candidate value maps to.
let value: string | null = null
if (candidate.value.kind === 'arbitrary') {
value = candidate.value.value
} else {
value = theme.resolve(candidate.value.fraction ?? candidate.value.value, [
'--transition-duration',
])
if (value === null && isPositiveInteger(candidate.value.value)) {
value = `${candidate.value.value}ms`
}
}
// If the candidate value (like the `sm` in `max-w-sm`) doesn't resolve to
// an actual value, don't generate any rules.
if (value === null) return
return [
transitionDurationProperty(),
decl('--tw-duration', value),
decl('transition-duration', value),
]
})
}
suggest('delay', () => [
{
@ -3802,10 +3813,22 @@ export function createUtilities(theme: Theme) {
])
}
functionalUtility('ease', {
themeKeys: ['--transition-timing-function'],
handle: (value) => [decl('transition-timing-function', value)],
})
{
let transitionTimingFunctionProperty = () => {
return atRoot([property('--tw-ease')])
}
staticUtility('ease-initial', [transitionTimingFunctionProperty, ['--tw-ease', 'initial']])
functionalUtility('ease', {
themeKeys: ['--transition-timing-function'],
handle: (value) => [
transitionTimingFunctionProperty(),
decl('--tw-ease', value),
decl('transition-timing-function', value),
],
})
}
staticUtility('will-change-auto', [['will-change', 'auto']])
staticUtility('will-change-scroll', [['will-change', 'scroll-position']])

View file

@ -604,6 +604,28 @@ test('explicit font-weight utilities are respected when overriding font-size', a
expect(await getPropertyValue('#z', 'font-weight')).toEqual('900')
})
test('explicit duration and ease utilities are respected when overriding transition-property', async ({
page,
}) => {
let { getPropertyValue } = await render(
page,
html`
<div
id="x"
class="ease-[linear] duration-500 transition-[opacity] hover:transition-[background-color]"
>
Hello world
</div>
`,
)
expect(await getPropertyValue('#x', 'transition-timing-function')).toEqual('linear')
expect(await getPropertyValue('#x', 'transition-duration')).toEqual('0.5s')
await page.locator('#x').hover()
expect(await getPropertyValue('#x', 'transition-timing-function')).toEqual('linear')
expect(await getPropertyValue('#x', 'transition-duration')).toEqual('0.5s')
})
// ---
const preflight = fs.readFileSync(path.resolve(__dirname, '..', 'preflight.css'), 'utf-8')