From 44aa9d46f49e5cb6e2d22f3bfbfd82ac1deaf3a2 Mon Sep 17 00:00:00 2001 From: Adam Wathan <4323180+adamwathan@users.noreply.github.com> Date: Mon, 23 Sep 2024 16:41:56 -0400 Subject: [PATCH 1/6] Only apply hover on devices that support hover --- .../tailwindcss/src/compat/plugin-api.test.ts | 6 +- packages/tailwindcss/src/index.test.ts | 136 ++++++++++++------ packages/tailwindcss/src/utilities.test.ts | 10 +- packages/tailwindcss/src/variants.test.ts | 122 ++++++++++------ packages/tailwindcss/src/variants.ts | 18 ++- 5 files changed, 189 insertions(+), 103 deletions(-) diff --git a/packages/tailwindcss/src/compat/plugin-api.test.ts b/packages/tailwindcss/src/compat/plugin-api.test.ts index 7d732ca1ccf1..cbf4b29ece79 100644 --- a/packages/tailwindcss/src/compat/plugin-api.test.ts +++ b/packages/tailwindcss/src/compat/plugin-api.test.ts @@ -1919,8 +1919,10 @@ describe('matchVariant', () => { "@layer utilities { @media (width >= 100px) { @media (width <= 200px) { - .testmin-\\[100px\\]\\:testmax-\\[200px\\]\\:hover\\:underline:hover { - text-decoration-line: underline; + @media (hover: hover) { + .testmin-\\[100px\\]\\:testmax-\\[200px\\]\\:hover\\:underline:hover { + text-decoration-line: underline; + } } } } diff --git a/packages/tailwindcss/src/index.test.ts b/packages/tailwindcss/src/index.test.ts index 0f56d3430d3a..1305b9415fe6 100644 --- a/packages/tailwindcss/src/index.test.ts +++ b/packages/tailwindcss/src/index.test.ts @@ -35,8 +35,10 @@ describe('compiling CSS', () => { display: flex; } - .hover\\:underline:hover { - text-decoration-line: underline; + @media (hover: hover) { + .hover\\:underline:hover { + text-decoration-line: underline; + } } @media (width >= 768px) { @@ -193,8 +195,10 @@ describe('@apply', () => { text-decoration-line: underline; } - .foo:hover { - background-color: var(--color-blue-500, #3b82f6); + @media (hover: hover) { + .foo:hover { + background-color: var(--color-blue-500, #3b82f6); + } } @media (width >= 768px) { @@ -390,16 +394,20 @@ describe('arbitrary variants', () => { describe('variant stacking', () => { it('should stack simple variants', async () => { expect(await run(['focus:hover:flex'])).toMatchInlineSnapshot(` - ".focus\\:hover\\:flex:focus:hover { - display: flex; + "@media (hover: hover) { + .focus\\:hover\\:flex:focus:hover { + display: flex; + } }" `) }) it('should stack arbitrary variants and simple variants', async () => { expect(await run(['[&_p]:hover:flex'])).toMatchInlineSnapshot(` - ".\\[\\&_p\\]\\:hover\\:flex p:hover { - display: flex; + "@media (hover: hover) { + .\\[\\&_p\\]\\:hover\\:flex p:hover { + display: flex; + } }" `) }) @@ -420,13 +428,17 @@ describe('variant stacking', () => { content: var(--tw-content); } - .before\\:hover\\:flex:before:hover { - display: flex; + @media (hover: hover) { + .before\\:hover\\:flex:before:hover { + display: flex; + } } - .hover\\:before\\:flex:hover:before { - content: var(--tw-content); - display: flex; + @media (hover: hover) { + .hover\\:before\\:flex:hover:before { + content: var(--tw-content); + display: flex; + } } @supports (-moz-orient: inline) { @@ -627,22 +639,24 @@ describe('sorting', () => { ), ), ).toMatchInlineSnapshot(` - ".pointer-events-none { - pointer-events: none; - } + ".pointer-events-none { + pointer-events: none; + } - .flex { - display: flex; - } + .flex { + display: flex; + } + @media (hover: hover) { .hover\\:flex:hover { display: flex; } + } - .focus\\:pointer-events-none:focus { - pointer-events: none; - }" - `) + .focus\\:pointer-events-none:focus { + pointer-events: none; + }" + `) }) /** @@ -672,16 +686,20 @@ describe('sorting', () => { display: flex; } - .hover\\:flex:hover { - display: flex; + @media (hover: hover) { + .hover\\:flex:hover { + display: flex; + } } .focus\\:flex:focus { display: flex; } - .hover\\:focus\\:flex:hover:focus { - display: flex; + @media (hover: hover) { + .hover\\:focus\\:flex:hover:focus { + display: flex; + } } .disabled\\:flex:disabled { @@ -715,44 +733,64 @@ describe('sorting', () => { ].sort(() => Math.random() - 0.5), ), ).toMatchInlineSnapshot(` - ".group-hover\\:flex:is(:where(.group):hover *) { - display: flex; + "@media (hover: hover) { + .group-hover\\:flex:is(:where(.group):hover *) { + display: flex; + } } .group-focus\\:flex:is(:where(.group):focus *) { display: flex; } - .peer-hover\\:flex:is(:where(.peer):hover ~ *) { - display: flex; + @media (hover: hover) { + .peer-hover\\:flex:is(:where(.peer):hover ~ *) { + display: flex; + } } - .group-hover\\:peer-hover\\:flex:is(:where(.group):hover *):is(:where(.peer):hover ~ *) { - display: flex; + @media (hover: hover) { + @media (hover: hover) { + .group-hover\\:peer-hover\\:flex:is(:where(.group):hover *):is(:where(.peer):hover ~ *) { + display: flex; + } + } } - .peer-hover\\:group-hover\\:flex:is(:where(.peer):hover ~ *):is(:where(.group):hover *) { - display: flex; + @media (hover: hover) { + @media (hover: hover) { + .peer-hover\\:group-hover\\:flex:is(:where(.peer):hover ~ *):is(:where(.group):hover *) { + display: flex; + } + } } - .group-focus\\:peer-hover\\:flex:is(:where(.group):focus *):is(:where(.peer):hover ~ *) { - display: flex; + @media (hover: hover) { + .group-focus\\:peer-hover\\:flex:is(:where(.group):focus *):is(:where(.peer):hover ~ *) { + display: flex; + } } - .peer-hover\\:group-focus\\:flex:is(:where(.peer):hover ~ *):is(:where(.group):focus *) { - display: flex; + @media (hover: hover) { + .peer-hover\\:group-focus\\:flex:is(:where(.peer):hover ~ *):is(:where(.group):focus *) { + display: flex; + } } .peer-focus\\:flex:is(:where(.peer):focus ~ *) { display: flex; } - .group-hover\\:peer-focus\\:flex:is(:where(.group):hover *):is(:where(.peer):focus ~ *) { - display: flex; + @media (hover: hover) { + .group-hover\\:peer-focus\\:flex:is(:where(.group):hover *):is(:where(.peer):focus ~ *) { + display: flex; + } } - .peer-focus\\:group-hover\\:flex:is(:where(.peer):focus ~ *):is(:where(.group):hover *) { - display: flex; + @media (hover: hover) { + .peer-focus\\:group-hover\\:flex:is(:where(.peer):focus ~ *):is(:where(.group):hover *) { + display: flex; + } } .group-focus\\:peer-focus\\:flex:is(:where(.group):focus *):is(:where(.peer):focus ~ *) { @@ -763,8 +801,10 @@ describe('sorting', () => { display: flex; } - .hover\\:flex:hover { - display: flex; + @media (hover: hover) { + .hover\\:flex:hover { + display: flex; + } }" `) }) @@ -2104,8 +2144,10 @@ describe('@variant', () => { expect(optimizeCss(compiled).trim()).toMatchInlineSnapshot(` "@layer utilities { @media (any-hover: hover) { - .any-hover\\:hover\\:underline:hover { - text-decoration-line: underline; + @media (hover: hover) { + .any-hover\\:hover\\:underline:hover { + text-decoration-line: underline; + } } } }" diff --git a/packages/tailwindcss/src/utilities.test.ts b/packages/tailwindcss/src/utilities.test.ts index 209bcd70178b..014c4973212a 100644 --- a/packages/tailwindcss/src/utilities.test.ts +++ b/packages/tailwindcss/src/utilities.test.ts @@ -15660,10 +15660,12 @@ describe('custom utilities', () => { display: flex; } - .hover\\:foo:hover { - flex-direction: column; - text-decoration-line: underline; - display: flex; + @media (hover: hover) { + .hover\\:foo:hover { + flex-direction: column; + text-decoration-line: underline; + display: flex; + } }" `) }) diff --git a/packages/tailwindcss/src/variants.test.ts b/packages/tailwindcss/src/variants.test.ts index 169f3b2750bf..f43945f890fd 100644 --- a/packages/tailwindcss/src/variants.test.ts +++ b/packages/tailwindcss/src/variants.test.ts @@ -581,16 +581,22 @@ test('focus-within', async () => { test('hover', async () => { expect(await run(['hover:flex', 'group-hover:flex', 'peer-hover:flex'])).toMatchInlineSnapshot(` - ".group-hover\\:flex:is(:where(.group):hover *) { - display: flex; + "@media (hover: hover) { + .group-hover\\:flex:is(:where(.group):hover *) { + display: flex; + } } - .peer-hover\\:flex:is(:where(.peer):hover ~ *) { - display: flex; + @media (hover: hover) { + .peer-hover\\:flex:is(:where(.peer):hover ~ *) { + display: flex; + } } - .hover\\:flex:hover { - display: flex; + @media (hover: hover) { + .hover\\:flex:hover { + display: flex; + } }" `) expect(await run(['hover/foo:flex'])).toEqual('') @@ -615,8 +621,10 @@ test('focus', async () => { test('group-hover group-focus sorting', async () => { expect(await run(['group-hover:flex', 'group-focus:flex'])).toMatchInlineSnapshot(` - ".group-hover\\:flex:is(:where(.group):hover *) { - display: flex; + "@media (hover: hover) { + .group-hover\\:flex:is(:where(.group):hover *) { + display: flex; + } } .group-focus\\:flex:is(:where(.group):focus *) { @@ -624,8 +632,10 @@ test('group-hover group-focus sorting', async () => { }" `) expect(await run(['group-focus:flex', 'group-hover:flex'])).toMatchInlineSnapshot(` - ".group-hover\\:flex:is(:where(.group):hover *) { - display: flex; + "@media (hover: hover) { + .group-hover\\:flex:is(:where(.group):hover *) { + display: flex; + } } .group-focus\\:flex:is(:where(.group):focus *) { @@ -741,16 +751,24 @@ test('group-[...]', async () => { display: flex; } - .group-\\[\\&_p\\]\\:hover\\:flex:is(:where(.group) p *):hover { - display: flex; + @media (hover: hover) { + .group-\\[\\&_p\\]\\:hover\\:flex:is(:where(.group) p *):hover { + display: flex; + } } - .hover\\:group-\\[\\&_p\\]\\:flex:hover:is(:where(.group) p *) { - display: flex; + @media (hover: hover) { + .hover\\:group-\\[\\&_p\\]\\:flex:hover:is(:where(.group) p *) { + display: flex; + } } - .hover\\:group-\\[\\&_p\\]\\:hover\\:flex:hover:is(:where(.group) p *):hover { - display: flex; + @media (hover: hover) { + @media (hover: hover) { + .hover\\:group-\\[\\&_p\\]\\:hover\\:flex:hover:is(:where(.group) p *):hover { + display: flex; + } + } }" `) @@ -786,20 +804,26 @@ test('group-*', async () => { ], ), ).toMatchInlineSnapshot(` - ".group-hover\\:flex:is(:where(.group):hover *) { - display: flex; + "@media (hover: hover) { + .group-hover\\:flex:is(:where(.group):hover *) { + display: flex; + } } .group-focus\\:flex:is(:where(.group):focus *) { display: flex; } - .group-focus\\:group-hover\\:flex:is(:where(.group):focus *):is(:where(.group):hover *) { - display: flex; + @media (hover: hover) { + .group-focus\\:group-hover\\:flex:is(:where(.group):focus *):is(:where(.group):hover *) { + display: flex; + } } - .group-hover\\:group-focus\\:flex:is(:where(.group):hover *):is(:where(.group):focus *) { - display: flex; + @media (hover: hover) { + .group-hover\\:group-focus\\:flex:is(:where(.group):hover *):is(:where(.group):focus *) { + display: flex; + } } .group-hocus\\:flex:is(:is(:where(.group):hover, :where(.group):focus) *) { @@ -843,16 +867,22 @@ test('peer-[...]', async () => { display: flex; } - .hover\\:peer-\\[\\&_p\\]\\:flex:hover:is(:where(.peer) p ~ *) { - display: flex; + @media (hover: hover) { + .hover\\:peer-\\[\\&_p\\]\\:flex:hover:is(:where(.peer) p ~ *) { + display: flex; + } } - .peer-\\[\\&_p\\]\\:hover\\:flex:is(:where(.peer) p ~ *):hover { - display: flex; + @media (hover: hover) { + .peer-\\[\\&_p\\]\\:hover\\:flex:is(:where(.peer) p ~ *):hover { + display: flex; + } } - .hover\\:peer-\\[\\&_p\\]\\:focus\\:flex:hover:is(:where(.peer) p ~ *):focus { - display: flex; + @media (hover: hover) { + .hover\\:peer-\\[\\&_p\\]\\:focus\\:flex:hover:is(:where(.peer) p ~ *):focus { + display: flex; + } }" `) @@ -887,20 +917,26 @@ test('peer-*', async () => { ], ), ).toMatchInlineSnapshot(` - ".peer-hover\\:flex:is(:where(.peer):hover ~ *) { - display: flex; + "@media (hover: hover) { + .peer-hover\\:flex:is(:where(.peer):hover ~ *) { + display: flex; + } } .peer-focus\\:flex:is(:where(.peer):focus ~ *) { display: flex; } - .peer-focus\\:peer-hover\\:flex:is(:where(.peer):focus ~ *):is(:where(.peer):hover ~ *) { - display: flex; + @media (hover: hover) { + .peer-focus\\:peer-hover\\:flex:is(:where(.peer):focus ~ *):is(:where(.peer):hover ~ *) { + display: flex; + } } - .peer-hover\\:peer-focus\\:flex:is(:where(.peer):hover ~ *):is(:where(.peer):focus ~ *) { - display: flex; + @media (hover: hover) { + .peer-hover\\:peer-focus\\:flex:is(:where(.peer):hover ~ *):is(:where(.peer):focus ~ *) { + display: flex; + } } .peer-hocus\\:flex:is(:is(:where(.peer):hover, :where(.peer):focus) ~ *) { @@ -2479,12 +2515,16 @@ test('variant order', async () => { --breakpoint-2xl: 1536px; } - .group-hover\\:flex:is(:where(.group):hover *) { - display: flex; + @media (hover: hover) { + .group-hover\\:flex:is(:where(.group):hover *) { + display: flex; + } } - .peer-hover\\:flex:is(:where(.peer):hover ~ *) { - display: flex; + @media (hover: hover) { + .peer-hover\\:flex:is(:where(.peer):hover ~ *) { + display: flex; + } } .first-letter\\:flex:first-letter { @@ -2625,8 +2665,10 @@ test('variant order', async () => { display: flex; } - .hover\\:flex:hover { - display: flex; + @media (hover: hover) { + .hover\\:flex:hover { + display: flex; + } } .focus\\:flex:focus { diff --git a/packages/tailwindcss/src/variants.ts b/packages/tailwindcss/src/variants.ts index 7dfcc4046bb3..38f141c666b1 100644 --- a/packages/tailwindcss/src/variants.ts +++ b/packages/tailwindcss/src/variants.ts @@ -452,15 +452,9 @@ export function createVariants(theme: Theme): Variants { ['focus-within', '&:focus-within'], [ 'hover', - '&:hover', - // TODO: Update tests for this: - // v => { - // v.nodes = [ - // rule('@media (hover: hover) and (pointer: fine)', [ - // rule('&:hover', v.nodes), - // ]), - // ] - // } + (r) => { + r.nodes = [rule('&:hover', [rule('@media (hover: hover)', r.nodes)])] + }, ], ['focus', '&:focus'], ['focus-visible', '&:focus-visible'], @@ -470,7 +464,11 @@ export function createVariants(theme: Theme): Variants { ] for (let [key, value] of pseudos) { - staticVariant(key, [value]) + if (typeof value === 'string') { + staticVariant(key, [value]) + } else { + variants.static(key, value) + } } staticVariant('inert', ['&:is([inert], [inert] *)']) From 04f6b8bf388fc40e13be79670f28479d10ecf6b0 Mon Sep 17 00:00:00 2001 From: Adam Wathan <4323180+adamwathan@users.noreply.github.com> Date: Mon, 23 Sep 2024 16:55:13 -0400 Subject: [PATCH 2/6] Remove loop --- packages/tailwindcss/src/variants.ts | 99 ++++++++++++---------------- 1 file changed, 42 insertions(+), 57 deletions(-) diff --git a/packages/tailwindcss/src/variants.ts b/packages/tailwindcss/src/variants.ts index 38f141c666b1..968f8d7a16d5 100644 --- a/packages/tailwindcss/src/variants.ts +++ b/packages/tailwindcss/src/variants.ts @@ -413,63 +413,48 @@ export function createVariants(theme: Theme): Variants { ) } - let pseudos: [name: string, selector: string][] = [ - // Positional - ['first', '&:first-child'], - ['last', '&:last-child'], - ['only', '&:only-child'], - ['odd', '&:nth-child(odd)'], - ['even', '&:nth-child(even)'], - ['first-of-type', '&:first-of-type'], - ['last-of-type', '&:last-of-type'], - ['only-of-type', '&:only-of-type'], - - // State - // TODO: Remove alpha vars or no? - ['visited', '&:visited'], - - ['target', '&:target'], - ['open', '&:is([open], :popover-open)'], - - // Forms - ['default', '&:default'], - ['checked', '&:checked'], - ['indeterminate', '&:indeterminate'], - ['placeholder-shown', '&:placeholder-shown'], - ['autofill', '&:autofill'], - ['optional', '&:optional'], - ['required', '&:required'], - ['valid', '&:valid'], - ['invalid', '&:invalid'], - ['in-range', '&:in-range'], - ['out-of-range', '&:out-of-range'], - ['read-only', '&:read-only'], - - // Content - ['empty', '&:empty'], - - // Interactive - ['focus-within', '&:focus-within'], - [ - 'hover', - (r) => { - r.nodes = [rule('&:hover', [rule('@media (hover: hover)', r.nodes)])] - }, - ], - ['focus', '&:focus'], - ['focus-visible', '&:focus-visible'], - ['active', '&:active'], - ['enabled', '&:enabled'], - ['disabled', '&:disabled'], - ] - - for (let [key, value] of pseudos) { - if (typeof value === 'string') { - staticVariant(key, [value]) - } else { - variants.static(key, value) - } - } + // Positional + staticVariant('first', ['&:first-child']) + staticVariant('last', ['&:last-child']) + staticVariant('only', ['&:only-child']) + staticVariant('odd', ['&:nth-child(odd)']) + staticVariant('even', ['&:nth-child(even)']) + staticVariant('first-of-type', ['&:first-of-type']) + staticVariant('last-of-type', ['&:last-of-type']) + staticVariant('only-of-type', ['&:only-of-type']) + + // State + staticVariant('visited', ['&:visited']) + staticVariant('target', ['&:target']) + staticVariant('open', ['&:is([open], :popover-open)']) + + // Forms + staticVariant('default', ['&:default']) + staticVariant('checked', ['&:checked']) + staticVariant('indeterminate', ['&:indeterminate']) + staticVariant('placeholder-shown', ['&:placeholder-shown']) + staticVariant('autofill', ['&:autofill']) + staticVariant('optional', ['&:optional']) + staticVariant('required', ['&:required']) + staticVariant('valid', ['&:valid']) + staticVariant('invalid', ['&:invalid']) + staticVariant('in-range', ['&:in-range']) + staticVariant('out-of-range', ['&:out-of-range']) + staticVariant('read-only', ['&:read-only']) + + // Content + staticVariant('empty', ['&:empty']) + + // Interactive + staticVariant('focus-within', ['&:focus-within']) + variants.static('hover', (r) => { + r.nodes = [rule('&:hover', [rule('@media (hover: hover)', r.nodes)])] + }) + staticVariant('focus', ['&:focus']) + staticVariant('focus-visible', ['&:focus-visible']) + staticVariant('active', ['&:active']) + staticVariant('enabled', ['&:enabled']) + staticVariant('disabled', ['&:disabled']) staticVariant('inert', ['&:is([inert], [inert] *)']) From f7995776b65ae46f6ddb15568d739e4b445a085c Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 24 Sep 2024 12:47:43 +0200 Subject: [PATCH 3/6] use `data-test:` instead of `hover:` This allows us to just change the data attribute when making a change instead of relying on `hover:`. In this PR `hover:` will use `@media (hover: hover)` which is not implemented in Firefox on Linux at the time of writing this PR. --- packages/tailwindcss/tests/ui.spec.ts | 107 ++++++++++++++------------ 1 file changed, 56 insertions(+), 51 deletions(-) diff --git a/packages/tailwindcss/tests/ui.spec.ts b/packages/tailwindcss/tests/ui.spec.ts index b7580d2f310a..9d351d6d8bfd 100644 --- a/packages/tailwindcss/tests/ui.spec.ts +++ b/packages/tailwindcss/tests/ui.spec.ts @@ -11,12 +11,12 @@ const css = String.raw test('touch action', async ({ page }) => { let { getPropertyValue } = await render( page, - html`
Hello world
`, + html`
Hello world
`, ) expect(await getPropertyValue('#x', 'touch-action')).toEqual('pan-x pan-y') - await page.locator('#x').hover() + await page.locator('#x').evaluate((el) => (el.dataset.test = '')) expect([ // `manipulation` is an alias for `pan-x pan-y pinch-zoom` and some engines @@ -71,7 +71,7 @@ test('background gradient, going from 2 to 3', async ({ page }) => { let { getPropertyValue } = await render( page, html` -
+
Hello world
`, @@ -81,7 +81,7 @@ test('background gradient, going from 2 to 3', async ({ page }) => { 'linear-gradient(to right, rgb(239, 68, 68) 0%, rgb(59, 130, 246) 100%)', ) - await page.locator('#x').hover() + await page.locator('#x').evaluate((el) => (el.dataset.test = '')) expect(await getPropertyValue('#x', 'background-image')).toEqual( 'linear-gradient(to right, rgb(239, 68, 68) 0%, rgb(34, 197, 94) 50%, rgb(59, 130, 246) 100%)', @@ -92,7 +92,10 @@ test('background gradient, going from 3 to 2', async ({ page }) => { let { getPropertyValue } = await render( page, html` -
+
Hello world
`, @@ -102,7 +105,7 @@ test('background gradient, going from 3 to 2', async ({ page }) => { 'linear-gradient(to right, rgb(239, 68, 68) 0%, rgb(34, 197, 94) 50%, rgb(59, 130, 246) 100%)', ) - await page.locator('#x').hover() + await page.locator('#x').evaluate((el) => (el.dataset.test = '')) expect(await getPropertyValue('#x', 'background-image')).toEqual( 'linear-gradient(to right, rgb(239, 68, 68) 0%, rgb(59, 130, 246) 100%)', @@ -223,8 +226,8 @@ test('shadow colors', async ({ page }) => {
-
Hello world
-
+
Hello world
+
Hello world
`, @@ -268,7 +271,7 @@ test('shadow colors', async ({ page }) => { ].join(', '), ) - await page.locator('#d').hover() + await page.locator('#d').evaluate((el) => (el.dataset.test = '')) expect(await getPropertyValue('#d', 'box-shadow')).toEqual( [ @@ -290,7 +293,7 @@ test('shadow colors', async ({ page }) => { ].join(', '), ) - await page.locator('#e').hover() + await page.locator('#e').evaluate((el) => (el.dataset.test = '')) expect(await getPropertyValue('#e', 'box-shadow')).toEqual( [ @@ -310,10 +313,12 @@ test('inset shadow colors', async ({ page }) => {
-
Hello world
+
+ Hello world +
Hello world
@@ -358,7 +363,7 @@ test('inset shadow colors', async ({ page }) => { ].join(', '), ) - await page.locator('#d').hover() + await page.locator('#d').evaluate((el) => (el.dataset.test = '')) expect(await getPropertyValue('#d', 'box-shadow')).toEqual( [ @@ -380,7 +385,7 @@ test('inset shadow colors', async ({ page }) => { ].join(', '), ) - await page.locator('#e').hover() + await page.locator('#e').evaluate((el) => (el.dataset.test = '')) expect(await getPropertyValue('#e', 'box-shadow')).toEqual( [ @@ -405,14 +410,14 @@ test('outline style is optional', async ({ page }) => { test('outline style is preserved when changing outline width', async ({ page }) => { let { getPropertyValue } = await render( page, - html`
+ html`
Hello world
`, ) expect(await getPropertyValue('#x', 'outline')).toEqual('rgb(255, 255, 255) dotted 2px') - await page.locator('#x').hover() + await page.locator('#x').evaluate((el) => (el.dataset.test = '')) expect(await getPropertyValue('#x', 'outline')).toEqual('rgb(255, 255, 255) dotted 4px') }) @@ -429,13 +434,13 @@ test('borders can be added without a border-style utility', async ({ page }) => test('borders can be added to a single side without a border-style utility', async ({ page }) => { let { getPropertyValue } = await render( page, - html`
+ html`
Hello world
`, ) expect(await getPropertyValue('#x', 'border-right')).toEqual('2px dashed rgb(0, 0, 0)') - await page.locator('#x').hover() + await page.locator('#x').evaluate((el) => (el.dataset.test = '')) expect(await getPropertyValue('#x', 'border-right')).toEqual('4px dashed rgb(0, 0, 0)') }) @@ -443,14 +448,14 @@ test('borders can be added to a single side without a border-style utility', asy test('dividers can be added without setting border-style', async ({ page }) => { let { getPropertyValue } = await render( page, - html`
+ html`
First
Second
`, ) expect(await getPropertyValue('#b', 'border-bottom')).toEqual('2px dashed rgb(0, 0, 0)') - await page.locator('#a').hover() + await page.locator('#a').evaluate((el) => (el.dataset.test = '')) expect(await getPropertyValue('#b', 'border-bottom')).toEqual('4px dashed rgb(0, 0, 0)') }) @@ -458,11 +463,11 @@ test('dividers can be added without setting border-style', async ({ page }) => { test('scale can be a number or percentage', async ({ page }) => { let { getPropertyValue } = await render( page, - html`
Hello world
`, + html`
Hello world
`, ) expect(await getPropertyValue('#x', 'scale')).toEqual('0.5') - await page.locator('#x').hover() + await page.locator('#x').evaluate((el) => (el.dataset.test = '')) expect(await getPropertyValue('#x', 'scale')).toEqual('1.5') }) @@ -471,12 +476,12 @@ test('scale can be a number or percentage', async ({ page }) => { test('content-none persists when conditionally styling a pseudo-element', async ({ page }) => { let { getPropertyValue } = await render( page, - html`
Hello world
`, + html`
Hello world
`, ) expect(await getPropertyValue(['#x', '::after'], 'content')).toEqual('none') - await page.locator('#x').hover() + await page.locator('#x').evaluate((el) => (el.dataset.test = '')) expect(await getPropertyValue(['#x', '::after'], 'content')).toEqual('none') }) @@ -485,9 +490,9 @@ test('explicit leading utilities are respected when overriding font-size', async let { getPropertyValue } = await render( page, html` -
Hello world
-
Hello world
-
Hello world
+
Hello world
+
Hello world
+
Hello world
`, css` @theme { @@ -501,15 +506,15 @@ test('explicit leading utilities are respected when overriding font-size', async ) expect(await getPropertyValue('#x', 'line-height')).toEqual('16px') - await page.locator('#x').hover() + await page.locator('#x').evaluate((el) => (el.dataset.test = '')) expect(await getPropertyValue('#x', 'line-height')).toEqual('24px') expect(await getPropertyValue('#y', 'line-height')).toEqual('8px') - await page.locator('#y').hover() + await page.locator('#y').evaluate((el) => (el.dataset.test = '')) expect(await getPropertyValue('#y', 'line-height')).toEqual('8px') expect(await getPropertyValue('#z', 'line-height')).toEqual('10px') - await page.locator('#z').hover() + await page.locator('#z').evaluate((el) => (el.dataset.test = '')) expect(await getPropertyValue('#z', 'line-height')).toEqual('10px') }) @@ -517,9 +522,9 @@ test('explicit leading utilities are overridden by line-height modifiers', async let { getPropertyValue } = await render( page, html` -
Hello world
-
Hello world
-
Hello world
+
Hello world
+
Hello world
+
Hello world
`, css` @theme { @@ -533,15 +538,15 @@ test('explicit leading utilities are overridden by line-height modifiers', async ) expect(await getPropertyValue('#x', 'line-height')).toEqual('16px') - await page.locator('#x').hover() + await page.locator('#x').evaluate((el) => (el.dataset.test = '')) expect(await getPropertyValue('#x', 'line-height')).toEqual('100px') expect(await getPropertyValue('#y', 'line-height')).toEqual('8px') - await page.locator('#y').hover() + await page.locator('#y').evaluate((el) => (el.dataset.test = '')) expect(await getPropertyValue('#y', 'line-height')).toEqual('100px') expect(await getPropertyValue('#z', 'line-height')).toEqual('10px') - await page.locator('#z').hover() + await page.locator('#z').evaluate((el) => (el.dataset.test = '')) expect(await getPropertyValue('#z', 'line-height')).toEqual('100px') }) @@ -549,9 +554,9 @@ test('explicit tracking utilities are respected when overriding font-size', asyn let { getPropertyValue } = await render( page, html` -
Hello world
-
Hello world
-
Hello world
+
Hello world
+
Hello world
+
Hello world
`, css` @theme { @@ -563,15 +568,15 @@ test('explicit tracking utilities are respected when overriding font-size', asyn ) expect(await getPropertyValue('#x', 'letter-spacing')).toEqual('5px') - await page.locator('#x').hover() + await page.locator('#x').evaluate((el) => (el.dataset.test = '')) expect(await getPropertyValue('#x', 'letter-spacing')).toEqual('10px') expect(await getPropertyValue('#y', 'letter-spacing')).toEqual('1px') - await page.locator('#y').hover() + await page.locator('#y').evaluate((el) => (el.dataset.test = '')) expect(await getPropertyValue('#y', 'letter-spacing')).toEqual('1px') expect(await getPropertyValue('#z', 'letter-spacing')).toEqual('3px') - await page.locator('#z').hover() + await page.locator('#z').evaluate((el) => (el.dataset.test = '')) expect(await getPropertyValue('#z', 'letter-spacing')).toEqual('3px') }) @@ -579,9 +584,9 @@ test('explicit font-weight utilities are respected when overriding font-size', a let { getPropertyValue } = await render( page, html` -
Hello world
-
Hello world
-
Hello world
+
Hello world
+
Hello world
+
Hello world
`, css` @theme { @@ -592,15 +597,15 @@ test('explicit font-weight utilities are respected when overriding font-size', a ) expect(await getPropertyValue('#x', 'font-weight')).toEqual('100') - await page.locator('#x').hover() + await page.locator('#x').evaluate((el) => (el.dataset.test = '')) expect(await getPropertyValue('#x', 'font-weight')).toEqual('200') expect(await getPropertyValue('#y', 'font-weight')).toEqual('700') - await page.locator('#y').hover() + await page.locator('#y').evaluate((el) => (el.dataset.test = '')) expect(await getPropertyValue('#y', 'font-weight')).toEqual('700') expect(await getPropertyValue('#z', 'font-weight')).toEqual('900') - await page.locator('#z').hover() + await page.locator('#z').evaluate((el) => (el.dataset.test = '')) expect(await getPropertyValue('#z', 'font-weight')).toEqual('900') }) @@ -612,7 +617,7 @@ test('explicit duration and ease utilities are respected when overriding transit html`
Hello world
@@ -621,7 +626,7 @@ test('explicit duration and ease utilities are respected when overriding transit expect(await getPropertyValue('#x', 'transition-timing-function')).toEqual('linear') expect(await getPropertyValue('#x', 'transition-duration')).toEqual('0.5s') - await page.locator('#x').hover() + await page.locator('#x').evaluate((el) => (el.dataset.test = '')) expect(await getPropertyValue('#x', 'transition-timing-function')).toEqual('linear') expect(await getPropertyValue('#x', 'transition-duration')).toEqual('0.5s') }) @@ -663,7 +668,7 @@ async function render(page: Page, content: string, extraCss: string = '') { content: optimizeCss(build(candidates)), }) - await page.locator('#mouse-park').hover() + await page.locator('#mouse-park').evaluate((el) => (el.dataset.test = '')) return { getPropertyValue(selector: string | [string, string], property: string) { From 6d9a1eb2966884cf33ef8287eeb82f3fd38943a1 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 24 Sep 2024 13:45:38 +0200 Subject: [PATCH 4/6] Revert "use `data-test:` instead of `hover:`" This reverts commit f7995776b65ae46f6ddb15568d739e4b445a085c. --- packages/tailwindcss/tests/ui.spec.ts | 107 ++++++++++++-------------- 1 file changed, 51 insertions(+), 56 deletions(-) diff --git a/packages/tailwindcss/tests/ui.spec.ts b/packages/tailwindcss/tests/ui.spec.ts index 9d351d6d8bfd..b7580d2f310a 100644 --- a/packages/tailwindcss/tests/ui.spec.ts +++ b/packages/tailwindcss/tests/ui.spec.ts @@ -11,12 +11,12 @@ const css = String.raw test('touch action', async ({ page }) => { let { getPropertyValue } = await render( page, - html`
Hello world
`, + html`
Hello world
`, ) expect(await getPropertyValue('#x', 'touch-action')).toEqual('pan-x pan-y') - await page.locator('#x').evaluate((el) => (el.dataset.test = '')) + await page.locator('#x').hover() expect([ // `manipulation` is an alias for `pan-x pan-y pinch-zoom` and some engines @@ -71,7 +71,7 @@ test('background gradient, going from 2 to 3', async ({ page }) => { let { getPropertyValue } = await render( page, html` -
+
Hello world
`, @@ -81,7 +81,7 @@ test('background gradient, going from 2 to 3', async ({ page }) => { 'linear-gradient(to right, rgb(239, 68, 68) 0%, rgb(59, 130, 246) 100%)', ) - await page.locator('#x').evaluate((el) => (el.dataset.test = '')) + await page.locator('#x').hover() expect(await getPropertyValue('#x', 'background-image')).toEqual( 'linear-gradient(to right, rgb(239, 68, 68) 0%, rgb(34, 197, 94) 50%, rgb(59, 130, 246) 100%)', @@ -92,10 +92,7 @@ test('background gradient, going from 3 to 2', async ({ page }) => { let { getPropertyValue } = await render( page, html` -
+
Hello world
`, @@ -105,7 +102,7 @@ test('background gradient, going from 3 to 2', async ({ page }) => { 'linear-gradient(to right, rgb(239, 68, 68) 0%, rgb(34, 197, 94) 50%, rgb(59, 130, 246) 100%)', ) - await page.locator('#x').evaluate((el) => (el.dataset.test = '')) + await page.locator('#x').hover() expect(await getPropertyValue('#x', 'background-image')).toEqual( 'linear-gradient(to right, rgb(239, 68, 68) 0%, rgb(59, 130, 246) 100%)', @@ -226,8 +223,8 @@ test('shadow colors', async ({ page }) => {
-
Hello world
-
+
Hello world
+
Hello world
`, @@ -271,7 +268,7 @@ test('shadow colors', async ({ page }) => { ].join(', '), ) - await page.locator('#d').evaluate((el) => (el.dataset.test = '')) + await page.locator('#d').hover() expect(await getPropertyValue('#d', 'box-shadow')).toEqual( [ @@ -293,7 +290,7 @@ test('shadow colors', async ({ page }) => { ].join(', '), ) - await page.locator('#e').evaluate((el) => (el.dataset.test = '')) + await page.locator('#e').hover() expect(await getPropertyValue('#e', 'box-shadow')).toEqual( [ @@ -313,12 +310,10 @@ test('inset shadow colors', async ({ page }) => {
-
- Hello world -
+
Hello world
Hello world
@@ -363,7 +358,7 @@ test('inset shadow colors', async ({ page }) => { ].join(', '), ) - await page.locator('#d').evaluate((el) => (el.dataset.test = '')) + await page.locator('#d').hover() expect(await getPropertyValue('#d', 'box-shadow')).toEqual( [ @@ -385,7 +380,7 @@ test('inset shadow colors', async ({ page }) => { ].join(', '), ) - await page.locator('#e').evaluate((el) => (el.dataset.test = '')) + await page.locator('#e').hover() expect(await getPropertyValue('#e', 'box-shadow')).toEqual( [ @@ -410,14 +405,14 @@ test('outline style is optional', async ({ page }) => { test('outline style is preserved when changing outline width', async ({ page }) => { let { getPropertyValue } = await render( page, - html`
+ html`
Hello world
`, ) expect(await getPropertyValue('#x', 'outline')).toEqual('rgb(255, 255, 255) dotted 2px') - await page.locator('#x').evaluate((el) => (el.dataset.test = '')) + await page.locator('#x').hover() expect(await getPropertyValue('#x', 'outline')).toEqual('rgb(255, 255, 255) dotted 4px') }) @@ -434,13 +429,13 @@ test('borders can be added without a border-style utility', async ({ page }) => test('borders can be added to a single side without a border-style utility', async ({ page }) => { let { getPropertyValue } = await render( page, - html`
+ html`
Hello world
`, ) expect(await getPropertyValue('#x', 'border-right')).toEqual('2px dashed rgb(0, 0, 0)') - await page.locator('#x').evaluate((el) => (el.dataset.test = '')) + await page.locator('#x').hover() expect(await getPropertyValue('#x', 'border-right')).toEqual('4px dashed rgb(0, 0, 0)') }) @@ -448,14 +443,14 @@ test('borders can be added to a single side without a border-style utility', asy test('dividers can be added without setting border-style', async ({ page }) => { let { getPropertyValue } = await render( page, - html`
+ html`
First
Second
`, ) expect(await getPropertyValue('#b', 'border-bottom')).toEqual('2px dashed rgb(0, 0, 0)') - await page.locator('#a').evaluate((el) => (el.dataset.test = '')) + await page.locator('#a').hover() expect(await getPropertyValue('#b', 'border-bottom')).toEqual('4px dashed rgb(0, 0, 0)') }) @@ -463,11 +458,11 @@ test('dividers can be added without setting border-style', async ({ page }) => { test('scale can be a number or percentage', async ({ page }) => { let { getPropertyValue } = await render( page, - html`
Hello world
`, + html`
Hello world
`, ) expect(await getPropertyValue('#x', 'scale')).toEqual('0.5') - await page.locator('#x').evaluate((el) => (el.dataset.test = '')) + await page.locator('#x').hover() expect(await getPropertyValue('#x', 'scale')).toEqual('1.5') }) @@ -476,12 +471,12 @@ test('scale can be a number or percentage', async ({ page }) => { test('content-none persists when conditionally styling a pseudo-element', async ({ page }) => { let { getPropertyValue } = await render( page, - html`
Hello world
`, + html`
Hello world
`, ) expect(await getPropertyValue(['#x', '::after'], 'content')).toEqual('none') - await page.locator('#x').evaluate((el) => (el.dataset.test = '')) + await page.locator('#x').hover() expect(await getPropertyValue(['#x', '::after'], 'content')).toEqual('none') }) @@ -490,9 +485,9 @@ test('explicit leading utilities are respected when overriding font-size', async let { getPropertyValue } = await render( page, html` -
Hello world
-
Hello world
-
Hello world
+
Hello world
+
Hello world
+
Hello world
`, css` @theme { @@ -506,15 +501,15 @@ test('explicit leading utilities are respected when overriding font-size', async ) expect(await getPropertyValue('#x', 'line-height')).toEqual('16px') - await page.locator('#x').evaluate((el) => (el.dataset.test = '')) + await page.locator('#x').hover() expect(await getPropertyValue('#x', 'line-height')).toEqual('24px') expect(await getPropertyValue('#y', 'line-height')).toEqual('8px') - await page.locator('#y').evaluate((el) => (el.dataset.test = '')) + await page.locator('#y').hover() expect(await getPropertyValue('#y', 'line-height')).toEqual('8px') expect(await getPropertyValue('#z', 'line-height')).toEqual('10px') - await page.locator('#z').evaluate((el) => (el.dataset.test = '')) + await page.locator('#z').hover() expect(await getPropertyValue('#z', 'line-height')).toEqual('10px') }) @@ -522,9 +517,9 @@ test('explicit leading utilities are overridden by line-height modifiers', async let { getPropertyValue } = await render( page, html` -
Hello world
-
Hello world
-
Hello world
+
Hello world
+
Hello world
+
Hello world
`, css` @theme { @@ -538,15 +533,15 @@ test('explicit leading utilities are overridden by line-height modifiers', async ) expect(await getPropertyValue('#x', 'line-height')).toEqual('16px') - await page.locator('#x').evaluate((el) => (el.dataset.test = '')) + await page.locator('#x').hover() expect(await getPropertyValue('#x', 'line-height')).toEqual('100px') expect(await getPropertyValue('#y', 'line-height')).toEqual('8px') - await page.locator('#y').evaluate((el) => (el.dataset.test = '')) + await page.locator('#y').hover() expect(await getPropertyValue('#y', 'line-height')).toEqual('100px') expect(await getPropertyValue('#z', 'line-height')).toEqual('10px') - await page.locator('#z').evaluate((el) => (el.dataset.test = '')) + await page.locator('#z').hover() expect(await getPropertyValue('#z', 'line-height')).toEqual('100px') }) @@ -554,9 +549,9 @@ test('explicit tracking utilities are respected when overriding font-size', asyn let { getPropertyValue } = await render( page, html` -
Hello world
-
Hello world
-
Hello world
+
Hello world
+
Hello world
+
Hello world
`, css` @theme { @@ -568,15 +563,15 @@ test('explicit tracking utilities are respected when overriding font-size', asyn ) expect(await getPropertyValue('#x', 'letter-spacing')).toEqual('5px') - await page.locator('#x').evaluate((el) => (el.dataset.test = '')) + await page.locator('#x').hover() expect(await getPropertyValue('#x', 'letter-spacing')).toEqual('10px') expect(await getPropertyValue('#y', 'letter-spacing')).toEqual('1px') - await page.locator('#y').evaluate((el) => (el.dataset.test = '')) + await page.locator('#y').hover() expect(await getPropertyValue('#y', 'letter-spacing')).toEqual('1px') expect(await getPropertyValue('#z', 'letter-spacing')).toEqual('3px') - await page.locator('#z').evaluate((el) => (el.dataset.test = '')) + await page.locator('#z').hover() expect(await getPropertyValue('#z', 'letter-spacing')).toEqual('3px') }) @@ -584,9 +579,9 @@ test('explicit font-weight utilities are respected when overriding font-size', a let { getPropertyValue } = await render( page, html` -
Hello world
-
Hello world
-
Hello world
+
Hello world
+
Hello world
+
Hello world
`, css` @theme { @@ -597,15 +592,15 @@ test('explicit font-weight utilities are respected when overriding font-size', a ) expect(await getPropertyValue('#x', 'font-weight')).toEqual('100') - await page.locator('#x').evaluate((el) => (el.dataset.test = '')) + await page.locator('#x').hover() expect(await getPropertyValue('#x', 'font-weight')).toEqual('200') expect(await getPropertyValue('#y', 'font-weight')).toEqual('700') - await page.locator('#y').evaluate((el) => (el.dataset.test = '')) + await page.locator('#y').hover() expect(await getPropertyValue('#y', 'font-weight')).toEqual('700') expect(await getPropertyValue('#z', 'font-weight')).toEqual('900') - await page.locator('#z').evaluate((el) => (el.dataset.test = '')) + await page.locator('#z').hover() expect(await getPropertyValue('#z', 'font-weight')).toEqual('900') }) @@ -617,7 +612,7 @@ test('explicit duration and ease utilities are respected when overriding transit html`
Hello world
@@ -626,7 +621,7 @@ test('explicit duration and ease utilities are respected when overriding transit expect(await getPropertyValue('#x', 'transition-timing-function')).toEqual('linear') expect(await getPropertyValue('#x', 'transition-duration')).toEqual('0.5s') - await page.locator('#x').evaluate((el) => (el.dataset.test = '')) + await page.locator('#x').hover() expect(await getPropertyValue('#x', 'transition-timing-function')).toEqual('linear') expect(await getPropertyValue('#x', 'transition-duration')).toEqual('0.5s') }) @@ -668,7 +663,7 @@ async function render(page: Page, content: string, extraCss: string = '') { content: optimizeCss(build(candidates)), }) - await page.locator('#mouse-park').evaluate((el) => (el.dataset.test = '')) + await page.locator('#mouse-park').hover() return { getPropertyValue(selector: string | [string, string], property: string) { From fc89ad6d4e877aab1a6ebc21ab30e6571043dd52 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 24 Sep 2024 13:47:17 +0200 Subject: [PATCH 5/6] update firefox preferences --- packages/tailwindcss/playwright.config.ts | 26 ++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/packages/tailwindcss/playwright.config.ts b/packages/tailwindcss/playwright.config.ts index 8b728ca82e19..c0a123abf5db 100644 --- a/packages/tailwindcss/playwright.config.ts +++ b/packages/tailwindcss/playwright.config.ts @@ -42,7 +42,31 @@ export default defineConfig({ }, { name: 'firefox', - use: { ...devices['Desktop Firefox'] }, + use: { + ...devices['Desktop Firefox'], + // https://playwright.dev/docs/test-use-options#more-browser-and-context-options + launchOptions: { + // https://playwright.dev/docs/api/class-browsertype#browser-type-launch-option-firefox-user-prefs + firefoxUserPrefs: { + // By default, headless Firefox runs as though no pointers + // capabilities are available. + // https://github.com/microsoft/playwright/issues/7769#issuecomment-966098074 + // + // This impacts our `hover` variant implementation which uses an + // '(hover: hover)' media query to determine if hover is available. + // + // Available values for pointer capabilities: + // NO_POINTER = 0x00; + // COARSE_POINTER = 0x01; + // FINE_POINTER = 0x02; + // HOVER_CAPABLE_POINTER = 0x04; + // + // Setting to 0x02 | 0x04 says the system supports a mouse + 'ui.primaryPointerCapabilities': 0x02 | 0x04, + 'ui.allPointerCapabilities': 0x02 | 0x04, + }, + }, + }, }, /* Test against mobile viewports. */ From 64c1efdb13098f9b88eca78901d6b3106f0b43f2 Mon Sep 17 00:00:00 2001 From: Adam Wathan <4323180+adamwathan@users.noreply.github.com> Date: Tue, 24 Sep 2024 11:41:55 -0400 Subject: [PATCH 6/6] Update changelog [skip ci] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8649e22891bd..527b87fb733c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Preserve explicit transition duration and timing function when overriding transition property ([#14490](https://github.com/tailwindlabs/tailwindcss/pull/14490)) - Change the implementation for `@import` resolution to speed up initial builds ([#14446](https://github.com/tailwindlabs/tailwindcss/pull/14446)) - Remove automatic `var(…)` injection ([#13657](https://github.com/tailwindlabs/tailwindcss/pull/13657)) +- Only apply `:hover` states on devices that support `@media (hover: hover)` ([#14500](https://github.com/tailwindlabs/tailwindcss/pull/14500)) ## [4.0.0-alpha.24] - 2024-09-11