diff --git a/src/css/selectors/parser.rs b/src/css/selectors/parser.rs index 04c0fe003929..40b55bcfbbfb 100644 --- a/src/css/selectors/parser.rs +++ b/src/css/selectors/parser.rs @@ -1222,7 +1222,7 @@ impl<'a> SelectorParser<'a> { // `::View-Transition-Group(..)` fall through to `CustomFunction`, // so look up `name` verbatim with no case folding. // - // PERF: 7 entries with near-unique lengths (3/6/10/19/19/21/26) — + // PERF: 8 entries with near-unique lengths (3/6/10/19/19/21/26/30) — // a length-gated `match` rejects the overwhelmingly-common miss path // (unknown `::-webkit-foo(...)` etc.) on a single `usize` compare, // versus a hash lookup's hash + table load + slice compare. Only @@ -1269,6 +1269,11 @@ impl<'a> SelectorParser<'a> { part_name: ViewTransitionPartName::parse(self, input)?, }); } + 30 if name == b"view-transition-group-children" => { + return Ok(PseudoElement::ViewTransitionGroupChildren { + part_name: ViewTransitionPartName::parse(self, input)?, + }); + } _ => {} } if !strings::starts_with(name, b"-") { @@ -2967,6 +2972,11 @@ pub enum PseudoElement { /// A part name selector. part_name: ViewTransitionPartName, }, + /// The [::view-transition-group-children()](https://drafts.csswg.org/css-view-transitions-2/#::view-transition-group-children) functional pseudo element. + ViewTransitionGroupChildren { + /// A part name selector. + part_name: ViewTransitionPartName, + }, /// The [::details-content](https://drafts.csswg.org/css-pseudo-4/#details-content-pseudo) pseudo element. DetailsContent, /// The [::picker-icon](https://drafts.csswg.org/css-forms-1/#picker-icon-pseudo) pseudo element. @@ -3075,6 +3085,7 @@ impl PseudoElement { | PE::ViewTransitionImagePair { .. } | PE::ViewTransitionNew { .. } | PE::ViewTransitionOld { .. } + | PE::ViewTransitionGroupChildren { .. } ) } @@ -3107,6 +3118,7 @@ impl fmt::Display for PseudoElement { Self::ViewTransitionImagePair { .. } => "view_transition_image_pair", Self::ViewTransitionOld { .. } => "view_transition_old", Self::ViewTransitionNew { .. } => "view_transition_new", + Self::ViewTransitionGroupChildren { .. } => "view_transition_group_children", Self::DetailsContent => "details_content", Self::PickerIcon => "picker_icon", Self::Checkmark => "checkmark", diff --git a/src/css/selectors/selector.rs b/src/css/selectors/selector.rs index 3109fdd519d9..310f2e62a883 100644 --- a/src/css/selectors/selector.rs +++ b/src/css/selectors/selector.rs @@ -1255,6 +1255,11 @@ pub(crate) mod serialize { part_name.to_css(dest)?; dest.write_char(b')')?; } + PseudoElement::ViewTransitionGroupChildren { part_name } => { + dest.write_str(b"::view-transition-group-children(")?; + part_name.to_css(dest)?; + dest.write_char(b')')?; + } PseudoElement::DetailsContent => dest.write_str(b"::details-content")?, PseudoElement::PickerIcon => dest.write_str(b"::picker-icon")?, PseudoElement::Checkmark => dest.write_str(b"::checkmark")?, diff --git a/test/bundler/css/css-modules.test.ts b/test/bundler/css/css-modules.test.ts index d4583df7958b..d10b693721b4 100644 --- a/test/bundler/css/css-modules.test.ts +++ b/test/bundler/css/css-modules.test.ts @@ -327,6 +327,75 @@ describe("css", () => { }, }); + // `::view-transition-group-children()` (css-view-transitions-2) takes the + // same argument as `::view-transition-group()`. The name and the class get + // the module hash, the class is exported, and there is no warning. + // https://github.com/oven-sh/bun/issues/42777 + itBundled("css-module/ViewTransitionGroupChildrenScoped", { + files: { + "/entry.js": ` + import styles from './styles.module.css'; + console.log(styles); + `, + "/styles.module.css": ` + .card { + view-transition-name: hero; + view-transition-class: big; + view-transition-group: contain; + } + ::view-transition-group(hero) { animation-duration: 1s } + ::view-transition-group-children(hero) { overflow: clip } + ::view-transition-group-children(.big) { overflow: clip } + ::view-transition-group-children(*):only-child { overflow: visible } + `, + }, + entryPoints: ["/entry.js"], + outdir: "/out", + onAfterBundle(api) { + const css = api.readFile("/out/entry.css"); + const card = css.match(/\.card_([A-Za-z0-9_-]+)\s*\{/); + expect(card, ".card should be scoped").not.toBeNull(); + const hash = card![1]; + + expect(css).toEqualIgnoringWhitespace(` + /* styles.module.css */ + .card_${hash} { + view-transition-name: hero_${hash}; + view-transition-class: big_${hash}; + view-transition-group: contain; + } + + ::view-transition-group(hero_${hash}) { + animation-duration: 1s; + } + + ::view-transition-group-children(hero_${hash}) { + overflow: clip; + } + + ::view-transition-group-children(.big_${hash}) { + overflow: clip; + } + + ::view-transition-group-children(*):only-child { + overflow: visible; + } + `); + + const js = api.readFile("/out/entry.js"); + expect(js).toEqualIgnoringWhitespace(` + // styles.module.css + var styles_module_default = { + card: "card_${hash}", + big: "big_${hash}" + }; + + // entry.js + console.log(styles_module_default); + `); + }, + }); + // A module file in a nested directory: the `view-transition-class` // declaration, the `::view-transition-*(.class)` selector and the exported // value must all carry the same hash. diff --git a/test/bundler/css/view-transition-23600.test.ts b/test/bundler/css/view-transition-23600.test.ts index 3b7ad93d38c7..2331078ea938 100644 --- a/test/bundler/css/view-transition-23600.test.ts +++ b/test/bundler/css/view-transition-23600.test.ts @@ -1,3 +1,6 @@ +import { describe, expect, test } from "bun:test"; +import { tempDir } from "harness"; +import path from "node:path"; import { itBundled } from "../expectBundled"; describe("css", () => { @@ -130,4 +133,28 @@ describe("css", () => { `); }, }); + + // `::view-transition-group-children()` (css-view-transitions-2) takes the + // same argument as `::view-transition-group()`. It must not warn. + // https://github.com/oven-sh/bun/issues/42777 + test("::view-transition-group-children() is a known pseudo-element (#42777)", async () => { + using dir = tempDir("css-42777", { + "in.css": ` + ::view-transition-group-children(hero) { overflow: clip } + ::view-transition-group-children(.big) { overflow: clip } + ::view-transition-group-children(*) { overflow: visible } + ::view-transition-group-children(hero):only-child { overflow: visible } + `, + }); + const result = await Bun.build({ + entrypoints: [path.join(String(dir), "in.css")], + minify: true, + throw: true, + }); + expect(result.logs.map(String)).toEqual([]); + const out = await result.outputs[0].text(); + expect(out.trim()).toBe( + "::view-transition-group-children(hero){overflow:clip}::view-transition-group-children(.big){overflow:clip}::view-transition-group-children(*){overflow:visible}::view-transition-group-children(hero):only-child{overflow:visible}", + ); + }); });