Skip to content

Commit 578e5ce

Browse files
Fix pseudo selector block style rendering in the editor
1 parent 5f60541 commit 578e5ce

2 files changed

Lines changed: 235 additions & 46 deletions

File tree

packages/global-styles-engine/src/core/render.tsx

Lines changed: 133 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,13 @@ const BLOCK_SUPPORT_FEATURE_LEVEL_SELECTORS = {
159159
typography: 'typography',
160160
};
161161

162+
// The valid pseudo-selectors that can be used for blocks.
163+
// Keep in sync with WP_Theme_JSON_Gutenberg::VALID_BLOCK_PSEUDO_SELECTORS.
164+
const VALID_BLOCK_PSEUDO_SELECTORS: Record< string, string[] > = {
165+
'core/button': [ ':hover', ':focus', ':focus-visible', ':active' ],
166+
'core/navigation-link': [ ':hover', ':focus', ':focus-visible', ':active' ],
167+
};
168+
162169
/**
163170
* Transform given preset tree into a set of preset class declarations.
164171
*
@@ -857,12 +864,23 @@ const STYLE_KEYS = [
857864
];
858865

859866
function pickStyleKeys( treeToPickFrom: any ): any {
867+
return pickStyleAndPseudoKeys( treeToPickFrom );
868+
}
869+
870+
function pickStyleAndPseudoKeys(
871+
treeToPickFrom: any,
872+
blockName?: string
873+
): any {
860874
if ( ! treeToPickFrom ) {
861875
return {};
862876
}
863877
const entries = Object.entries( treeToPickFrom );
864-
const pickedEntries = entries.filter( ( [ key ] ) =>
865-
STYLE_KEYS.includes( key )
878+
const allowedPseudoSelectors = blockName
879+
? VALID_BLOCK_PSEUDO_SELECTORS[ blockName ] ?? []
880+
: [];
881+
const pickedEntries = entries.filter(
882+
( [ key ] ) =>
883+
STYLE_KEYS.includes( key ) || allowedPseudoSelectors.includes( key )
866884
);
867885
// clone the style objects so that `getFeatureDeclarations` can remove consumed keys from it
868886
const clonedEntries = pickedEntries.map( ( [ key, style ] ) => [
@@ -872,6 +890,92 @@ function pickStyleKeys( treeToPickFrom: any ): any {
872890
return Object.fromEntries( clonedEntries );
873891
}
874892

893+
function appendPseudoSelectorStyles(
894+
styles: Record< string, any >,
895+
selector: string,
896+
ruleset: string,
897+
featureSelectors:
898+
| string
899+
| Record< string, string | Record< string, string > >
900+
| undefined,
901+
treeSettings: Record< string, any > | undefined,
902+
blockName: string | undefined,
903+
styleVariationSelector?: string
904+
): string {
905+
const pseudoSelectorStyles = Object.entries( styles ).filter( ( [ key ] ) =>
906+
key.startsWith( ':' )
907+
);
908+
909+
if ( ! pseudoSelectorStyles.length ) {
910+
return ruleset;
911+
}
912+
913+
pseudoSelectorStyles.forEach( ( [ pseudoKey, pseudoStyle ] ) => {
914+
if ( ! pseudoStyle || typeof pseudoStyle !== 'object' ) {
915+
return;
916+
}
917+
918+
const remainingPseudoStyles = JSON.parse(
919+
JSON.stringify( pseudoStyle )
920+
);
921+
922+
if ( featureSelectors && typeof featureSelectors !== 'string' ) {
923+
let pseudoFeatureDeclarations = getFeatureDeclarations(
924+
featureSelectors,
925+
remainingPseudoStyles
926+
);
927+
928+
pseudoFeatureDeclarations = updateParagraphTextIndentSelector(
929+
pseudoFeatureDeclarations,
930+
treeSettings,
931+
blockName
932+
);
933+
934+
pseudoFeatureDeclarations = updateButtonWidthDeclarations(
935+
pseudoFeatureDeclarations,
936+
treeSettings
937+
);
938+
939+
Object.entries( pseudoFeatureDeclarations ).forEach(
940+
( [ baseSelector, declarations ] ) => {
941+
if ( ! declarations.length ) {
942+
return;
943+
}
944+
const pseudoFeatureSelector = appendToSelector(
945+
baseSelector,
946+
pseudoKey
947+
);
948+
const cssSelector = styleVariationSelector
949+
? concatFeatureVariationSelectorString(
950+
pseudoFeatureSelector,
951+
styleVariationSelector
952+
)
953+
: pseudoFeatureSelector;
954+
const rules = declarations.join( ';' );
955+
ruleset += `:root :where(${ cssSelector }){${ rules };}`;
956+
}
957+
);
958+
}
959+
960+
const pseudoDeclarations = getStylesDeclarations(
961+
remainingPseudoStyles
962+
);
963+
964+
if ( ! pseudoDeclarations.length ) {
965+
return;
966+
}
967+
968+
const pseudoSelector = appendToSelector( selector, pseudoKey );
969+
const pseudoRule = `:root :where(${ pseudoSelector }){${ pseudoDeclarations.join(
970+
';'
971+
) };}`;
972+
973+
ruleset += pseudoRule;
974+
} );
975+
976+
return ruleset;
977+
}
978+
875979
export const getNodesWithStyles = (
876980
tree: GlobalStylesConfig,
877981
blockSelectors: string | BlockSelectors
@@ -923,7 +1027,7 @@ export const getNodesWithStyles = (
9231027
// Iterate over blocks: they can have styles & elements.
9241028
Object.entries( tree.styles?.blocks ?? {} ).forEach(
9251029
( [ blockName, node ] ) => {
926-
const blockStyles = pickStyleKeys( node );
1030+
const blockStyles = pickStyleAndPseudoKeys( node, blockName );
9271031
const typedNode = node as BlockNode;
9281032

9291033
// Store variation data for later processing, but don't add to nodes yet.
@@ -935,8 +1039,10 @@ export const getNodesWithStyles = (
9351039
Object.entries( typedNode.variations ).forEach(
9361040
( [ variationName, variation ] ) => {
9371041
const typedVariation = variation as BlockVariation;
938-
variations[ variationName ] =
939-
pickStyleKeys( typedVariation );
1042+
variations[ variationName ] = pickStyleAndPseudoKeys(
1043+
typedVariation,
1044+
blockName
1045+
);
9401046
if ( typedVariation?.css ) {
9411047
variations[ variationName ].css =
9421048
typedVariation.css;
@@ -1002,7 +1108,10 @@ export const getNodesWithStyles = (
10021108
: undefined;
10031109

10041110
const variationBlockStyleNodes =
1005-
pickStyleKeys( variationBlockStyles );
1111+
pickStyleAndPseudoKeys(
1112+
variationBlockStyles,
1113+
variationBlockName
1114+
);
10061115

10071116
if ( variationBlockStyles?.css ) {
10081117
variationBlockStyleNodes.css =
@@ -1558,6 +1667,17 @@ export const transformToStyles = (
15581667
`:root :where(${ styleVariationSelector })`
15591668
);
15601669
}
1670+
1671+
ruleset = appendPseudoSelectorStyles(
1672+
styleVariations,
1673+
styleVariationSelector as string,
1674+
ruleset,
1675+
featureSelectors,
1676+
tree.settings,
1677+
name,
1678+
styleVariationSelector as string
1679+
);
1680+
15611681
// Generate layout styles for the variation if it supports layout and has blockGap defined.
15621682
if (
15631683
hasLayoutSupport &&
@@ -1579,45 +1699,14 @@ export const transformToStyles = (
15791699
);
15801700
}
15811701

1582-
// Check for pseudo selector in `styles` and handle separately.
1583-
const pseudoSelectorStyles = Object.entries( styles ).filter(
1584-
( [ key ] ) => key.startsWith( ':' )
1702+
ruleset = appendPseudoSelectorStyles(
1703+
styles,
1704+
selector,
1705+
ruleset,
1706+
featureSelectors,
1707+
tree.settings,
1708+
name
15851709
);
1586-
1587-
if ( pseudoSelectorStyles?.length ) {
1588-
pseudoSelectorStyles.forEach(
1589-
( [ pseudoKey, pseudoStyle ] ) => {
1590-
const pseudoDeclarations =
1591-
getStylesDeclarations( pseudoStyle );
1592-
1593-
if ( ! pseudoDeclarations?.length ) {
1594-
return;
1595-
}
1596-
1597-
// `selector` may be provided in a form
1598-
// where block level selectors have sub element
1599-
// selectors appended to them as a comma separated
1600-
// string.
1601-
// e.g. `h1 a,h2 a,h3 a,h4 a,h5 a,h6 a`;
1602-
// Split and append pseudo selector to create
1603-
// the proper rules to target the elements.
1604-
const _selector = selector
1605-
.split( ',' )
1606-
.map( ( sel: string ) => sel + pseudoKey )
1607-
.join( ',' );
1608-
1609-
// As pseudo classes such as :hover, :focus etc. have class-level
1610-
// specificity, they must use the `:root :where()` wrapper. This.
1611-
// caps the specificity at `0-1-0` to allow proper nesting of variations
1612-
// and block type element styles.
1613-
const pseudoRule = `:root :where(${ _selector }){${ pseudoDeclarations.join(
1614-
';'
1615-
) };}`;
1616-
1617-
ruleset += pseudoRule;
1618-
}
1619-
);
1620-
}
16211710
}
16221711
);
16231712
}

packages/global-styles-engine/src/test/render.test.ts

Lines changed: 102 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -652,6 +652,106 @@ describe( 'global styles renderer', () => {
652652
);
653653
} );
654654

655+
it( 'should handle block pseudo selectors', () => {
656+
const tree = {
657+
styles: {
658+
blocks: {
659+
'core/button': {
660+
color: {
661+
text: 'red',
662+
},
663+
':hover': {
664+
color: {
665+
text: 'blue',
666+
},
667+
},
668+
},
669+
},
670+
},
671+
} as unknown as GlobalStylesConfig;
672+
673+
const blockSelectors = {
674+
'core/button': {
675+
selector: '.wp-block-button',
676+
},
677+
};
678+
679+
const result = transformToStyles(
680+
Object.freeze( tree ),
681+
blockSelectors,
682+
false,
683+
false,
684+
true,
685+
true,
686+
{
687+
blockGap: false,
688+
blockStyles: true,
689+
layoutStyles: false,
690+
marginReset: false,
691+
presets: false,
692+
rootPadding: false,
693+
}
694+
);
695+
696+
expect( result ).toEqual(
697+
':root :where(.wp-block-button){color: red;}:root :where(.wp-block-button:hover){color: blue;}'
698+
);
699+
} );
700+
701+
it( 'should handle style variation pseudo selectors', () => {
702+
const tree = {
703+
styles: {
704+
blocks: {
705+
'core/button': {
706+
variations: {
707+
foo: {
708+
color: {
709+
text: 'green',
710+
},
711+
':hover': {
712+
color: {
713+
text: 'yellow',
714+
},
715+
},
716+
},
717+
},
718+
},
719+
},
720+
},
721+
} as unknown as GlobalStylesConfig;
722+
723+
const blockSelectors = {
724+
'core/button': {
725+
selector: '.wp-block-button',
726+
styleVariationSelectors: {
727+
foo: '.is-style-foo.wp-block-button',
728+
},
729+
},
730+
};
731+
732+
const result = transformToStyles(
733+
Object.freeze( tree ),
734+
blockSelectors,
735+
false,
736+
false,
737+
true,
738+
true,
739+
{
740+
blockGap: false,
741+
blockStyles: true,
742+
layoutStyles: false,
743+
marginReset: false,
744+
presets: false,
745+
rootPadding: false,
746+
variationStyles: true,
747+
}
748+
);
749+
750+
expect( result ).toEqual(
751+
':root :where(.is-style-foo.wp-block-button){color: green;}:root :where(.is-style-foo.wp-block-button:hover){color: yellow;}'
752+
);
753+
} );
754+
655755
it( 'should handle duotone filter', () => {
656756
const tree = {
657757
styles: {
@@ -944,7 +1044,7 @@ describe( 'global styles renderer', () => {
9441044
} );
9451045

9461046
it( 'should convert preset percentage width to calc() formula', () => {
947-
const tree: GlobalStylesConfig = {
1047+
const tree = {
9481048
settings: {
9491049
blocks: {
9501050
'core/button': {
@@ -971,7 +1071,7 @@ describe( 'global styles renderer', () => {
9711071
},
9721072
},
9731073
},
974-
};
1074+
} as unknown as GlobalStylesConfig;
9751075

9761076
const blockSelectors = {
9771077
'core/button': {

0 commit comments

Comments
 (0)