@wordpress/global-styles-engine 1.17.0 → 1.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/build/core/render.cjs +36 -16
  3. package/build/core/render.cjs.map +2 -2
  4. package/build/index.cjs +3 -0
  5. package/build/index.cjs.map +2 -2
  6. package/build/lock-unlock.cjs +37 -0
  7. package/build/lock-unlock.cjs.map +7 -0
  8. package/build/private-apis.cjs +38 -0
  9. package/build/private-apis.cjs.map +7 -0
  10. package/build/settings/get-setting.cjs +3 -1
  11. package/build/settings/get-setting.cjs.map +2 -2
  12. package/build/types.cjs.map +1 -1
  13. package/build/utils/common.cjs +2 -4
  14. package/build/utils/common.cjs.map +2 -2
  15. package/build/utils/viewport.cjs +108 -0
  16. package/build/utils/viewport.cjs.map +7 -0
  17. package/build-module/core/render.mjs +36 -16
  18. package/build-module/core/render.mjs.map +2 -2
  19. package/build-module/index.mjs +2 -0
  20. package/build-module/index.mjs.map +2 -2
  21. package/build-module/lock-unlock.mjs +11 -0
  22. package/build-module/lock-unlock.mjs.map +7 -0
  23. package/build-module/private-apis.mjs +17 -0
  24. package/build-module/private-apis.mjs.map +7 -0
  25. package/build-module/settings/get-setting.mjs +3 -1
  26. package/build-module/settings/get-setting.mjs.map +2 -2
  27. package/build-module/utils/common.mjs +2 -4
  28. package/build-module/utils/common.mjs.map +2 -2
  29. package/build-module/utils/viewport.mjs +81 -0
  30. package/build-module/utils/viewport.mjs.map +7 -0
  31. package/build-types/core/render.d.ts +1 -1
  32. package/build-types/core/render.d.ts.map +1 -1
  33. package/build-types/index.d.ts +1 -0
  34. package/build-types/index.d.ts.map +1 -1
  35. package/build-types/lock-unlock.d.ts +2 -0
  36. package/build-types/lock-unlock.d.ts.map +1 -0
  37. package/build-types/private-apis.d.ts +2 -0
  38. package/build-types/private-apis.d.ts.map +1 -0
  39. package/build-types/settings/get-setting.d.ts.map +1 -1
  40. package/build-types/style-state-back-compat.d.ts.map +1 -1
  41. package/build-types/types.d.ts +4 -0
  42. package/build-types/types.d.ts.map +1 -1
  43. package/build-types/utils/common.d.ts.map +1 -1
  44. package/build-types/utils/fluid.d.ts +1 -1
  45. package/build-types/utils/fluid.d.ts.map +1 -1
  46. package/build-types/utils/viewport.d.ts +41 -0
  47. package/build-types/utils/viewport.d.ts.map +1 -0
  48. package/package.json +7 -6
  49. package/src/core/render.tsx +45 -21
  50. package/src/index.ts +1 -0
  51. package/src/lock-unlock.ts +10 -0
  52. package/src/private-apis.ts +16 -0
  53. package/src/settings/get-setting.ts +2 -0
  54. package/src/test/render.test.ts +182 -0
  55. package/src/test/viewport-utils.test.ts +168 -0
  56. package/src/types.ts +4 -0
  57. package/src/utils/common.ts +13 -8
  58. package/src/utils/viewport.ts +153 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wordpress/global-styles-engine",
3
- "version": "1.17.0",
3
+ "version": "1.18.0",
4
4
  "description": "Pure CSS generation engine for WordPress global styles.",
5
5
  "author": "The WordPress Contributors",
6
6
  "license": "GPL-2.0-or-later",
@@ -42,10 +42,11 @@
42
42
  "types": "build-types/index.d.ts",
43
43
  "sideEffects": false,
44
44
  "dependencies": {
45
- "@wordpress/blocks": "^15.23.0",
46
- "@wordpress/data": "^10.50.0",
47
- "@wordpress/i18n": "^6.23.0",
48
- "@wordpress/style-engine": "^2.50.0",
45
+ "@wordpress/blocks": "^15.24.0",
46
+ "@wordpress/data": "^10.51.0",
47
+ "@wordpress/i18n": "^6.24.0",
48
+ "@wordpress/private-apis": "^1.51.0",
49
+ "@wordpress/style-engine": "^2.51.0",
49
50
  "colord": "^2.9.3",
50
51
  "deepmerge": "^4.3.1",
51
52
  "fast-deep-equal": "^3.1.3",
@@ -59,5 +60,5 @@
59
60
  "publishConfig": {
60
61
  "access": "public"
61
62
  },
62
- "gitHead": "fee6e24e8e63d36f6bbcf487c193461e9bd79753"
63
+ "gitHead": "e9a74f9c14095a34398ecd4d1f7a908e55051205"
63
64
  }
@@ -34,6 +34,7 @@ import { getGapCSSValue } from '../utils/gap';
34
34
  import { setBackgroundStyleDefaults } from '../utils/background';
35
35
  import { LAYOUT_DEFINITIONS } from '../utils/layout';
36
36
  import { getValueFromObjectPath, setImmutably } from '../utils/object';
37
+ import { getResponsiveMediaQueries } from '../utils/viewport';
37
38
  import { getSetting } from '../settings/get-setting';
38
39
  import type { GlobalStylesConfig, GlobalStylesStyles } from '../types';
39
40
  import { normalizeStyleStateAliases } from '../style-state-back-compat';
@@ -228,15 +229,6 @@ const VALID_ELEMENT_PSEUDO_SELECTORS: Record< string, string[] > = {
228
229
  ],
229
230
  };
230
231
 
231
- /**
232
- * Responsive breakpoint state keys and their corresponding CSS media queries.
233
- * Keep in sync with WP_Theme_JSON_Gutenberg::RESPONSIVE_BREAKPOINTS.
234
- */
235
- const RESPONSIVE_BREAKPOINTS: Record< string, string > = {
236
- '@mobile': '@media (width <= 480px)',
237
- '@tablet': '@media (480px < width <= 782px)',
238
- };
239
-
240
232
  /**
241
233
  * Transform given preset tree into a set of preset class declarations.
242
234
  *
@@ -913,13 +905,21 @@ const STYLE_KEYS = [
913
905
  'background',
914
906
  ];
915
907
 
916
- function pickStyleKeys( treeToPickFrom: any ): any {
917
- return pickStyleAndPseudoKeys( treeToPickFrom );
908
+ function pickStyleKeys(
909
+ treeToPickFrom: any,
910
+ responsiveMediaQueries: Record< string, string >
911
+ ): any {
912
+ return pickStyleAndPseudoKeys(
913
+ treeToPickFrom,
914
+ undefined,
915
+ responsiveMediaQueries
916
+ );
918
917
  }
919
918
 
920
919
  function pickStyleAndPseudoKeys(
921
920
  treeToPickFrom: any,
922
- blockName?: string
921
+ blockName: string | undefined,
922
+ responsiveMediaQueries: Record< string, string >
923
923
  ): any {
924
924
  if ( ! treeToPickFrom ) {
925
925
  return {};
@@ -933,7 +933,7 @@ function pickStyleAndPseudoKeys(
933
933
  ( [ key ] ) =>
934
934
  STYLE_KEYS.includes( key ) ||
935
935
  allowedPseudoSelectors.includes( key ) ||
936
- RESPONSIVE_BREAKPOINTS[ key ]
936
+ responsiveMediaQueries[ key ]
937
937
  );
938
938
  // clone the style objects so that `getFeatureDeclarations` can remove consumed keys from it
939
939
  const clonedEntries = pickedEntries.map( ( [ key, style ] ) => [
@@ -1001,14 +1001,22 @@ function getPseudoStyleNodes( node: StylesNode ): StylesNode[] {
1001
1001
  * Breakpoint nodes render feature-level, base, and pseudo declarations through
1002
1002
  * the normal node renderer.
1003
1003
  *
1004
- * @param node Style node that may contain configured responsive state styles.
1004
+ * @param node Style node that may contain configured responsive state styles.
1005
+ * @param responsiveMediaQueries Media queries keyed by responsive state name.
1005
1006
  * @return Responsive style nodes in configured breakpoint order.
1006
1007
  */
1007
- function getResponsiveStyleNodes( node: StylesNode ): StylesNode[] {
1008
+ function getResponsiveStyleNodes(
1009
+ node: StylesNode,
1010
+ responsiveMediaQueries: Record< string, string >
1011
+ ): StylesNode[] {
1008
1012
  const {
1009
1013
  styles,
1010
1014
  selector,
1015
+ fallbackGapValue,
1011
1016
  featureSelectors,
1017
+ hasLayoutSupport,
1018
+ layoutHasBlockGapSupport,
1019
+ layoutSelector,
1012
1020
  name,
1013
1021
  elementName,
1014
1022
  isStyleVariation,
@@ -1019,7 +1027,7 @@ function getResponsiveStyleNodes( node: StylesNode ): StylesNode[] {
1019
1027
  return [];
1020
1028
  }
1021
1029
 
1022
- return Object.entries( RESPONSIVE_BREAKPOINTS ).flatMap(
1030
+ return Object.entries( responsiveMediaQueries ).flatMap(
1023
1031
  ( [ breakpointKey, mediaQuery ] ) => {
1024
1032
  const breakpointStyles = styles?.[ breakpointKey ];
1025
1033
  if ( ! breakpointStyles || typeof breakpointStyles !== 'object' ) {
@@ -1035,10 +1043,14 @@ function getResponsiveStyleNodes( node: StylesNode ): StylesNode[] {
1035
1043
  featureSelectors && typeof featureSelectors !== 'string'
1036
1044
  ? featureSelectors
1037
1045
  : undefined,
1046
+ fallbackGapValue,
1047
+ hasLayoutSupport,
1038
1048
  name,
1039
1049
  elementName,
1040
1050
  isStyleVariation,
1041
1051
  variationName,
1052
+ layoutSelector,
1053
+ layoutHasBlockGapSupport,
1042
1054
  },
1043
1055
  ];
1044
1056
  }
@@ -1055,8 +1067,10 @@ export const getNodesWithStyles = (
1055
1067
  return nodes;
1056
1068
  }
1057
1069
 
1070
+ const responsiveMediaQueries = getResponsiveMediaQueries( tree );
1071
+
1058
1072
  // Top-level.
1059
- const styles = pickStyleKeys( tree.styles );
1073
+ const styles = pickStyleKeys( tree.styles, responsiveMediaQueries );
1060
1074
  if ( styles ) {
1061
1075
  nodes.push( {
1062
1076
  styles,
@@ -1085,7 +1099,11 @@ export const getNodesWithStyles = (
1085
1099
  // Iterate over blocks: they can have styles & elements.
1086
1100
  Object.entries( tree.styles?.blocks ?? {} ).forEach(
1087
1101
  ( [ blockName, node ] ) => {
1088
- const blockStyles = pickStyleAndPseudoKeys( node, blockName );
1102
+ const blockStyles = pickStyleAndPseudoKeys(
1103
+ node,
1104
+ blockName,
1105
+ responsiveMediaQueries
1106
+ );
1089
1107
  const typedNode = node as BlockNode;
1090
1108
 
1091
1109
  // Store variation child nodes so they can be inserted after the block's own elements.
@@ -1098,7 +1116,8 @@ export const getNodesWithStyles = (
1098
1116
  const typedVariation = variation as BlockVariation;
1099
1117
  const variationStyles = pickStyleAndPseudoKeys(
1100
1118
  typedVariation,
1101
- blockName
1119
+ blockName,
1120
+ responsiveMediaQueries
1102
1121
  );
1103
1122
  if ( typedVariation?.css ) {
1104
1123
  variationStyles.css = typedVariation.css;
@@ -1193,7 +1212,8 @@ export const getNodesWithStyles = (
1193
1212
  const variationBlockStyleNodes =
1194
1213
  pickStyleAndPseudoKeys(
1195
1214
  variationBlockStyles,
1196
- variationBlockName
1215
+ variationBlockName,
1216
+ responsiveMediaQueries
1197
1217
  );
1198
1218
 
1199
1219
  if ( variationBlockStyles?.css ) {
@@ -1757,12 +1777,16 @@ export const transformToStyles = (
1757
1777
  }
1758
1778
 
1759
1779
  if ( options.blockStyles ) {
1780
+ const responsiveMediaQueries = getResponsiveMediaQueries( tree );
1760
1781
  nodesWithStyles.forEach( ( node ) => {
1761
1782
  if ( node.isStyleVariation && ! options.variationStyles ) {
1762
1783
  return;
1763
1784
  }
1764
1785
 
1765
- const responsiveNodes = getResponsiveStyleNodes( node );
1786
+ const responsiveNodes = getResponsiveStyleNodes(
1787
+ node,
1788
+ responsiveMediaQueries
1789
+ );
1766
1790
  // Match PHP node order: base, responsive base, pseudo, responsive pseudo.
1767
1791
  [
1768
1792
  node,
package/src/index.ts CHANGED
@@ -29,6 +29,7 @@ export {
29
29
  getResolvedValue,
30
30
  splitSelectorList,
31
31
  } from './utils/common';
32
+ export { privateApis } from './private-apis';
32
33
 
33
34
  // Types
34
35
  export type * from './types';
@@ -0,0 +1,10 @@
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import { __dangerousOptInToUnstableAPIsOnlyForCoreModules } from '@wordpress/private-apis';
5
+
6
+ export const { lock, unlock } =
7
+ __dangerousOptInToUnstableAPIsOnlyForCoreModules(
8
+ 'I acknowledge private features are not for use in themes or plugins and doing so will break in the next version of WordPress.',
9
+ '@wordpress/global-styles-engine'
10
+ );
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Internal dependencies
3
+ */
4
+ import {
5
+ getResponsiveMediaQueries,
6
+ getViewportBreakpoints,
7
+ getViewportBreakpointValueInPixels,
8
+ } from './utils/viewport';
9
+ import { lock } from './lock-unlock';
10
+
11
+ export const privateApis = {};
12
+ lock( privateApis, {
13
+ getResponsiveMediaQueries,
14
+ getViewportBreakpoints,
15
+ getViewportBreakpointValueInPixels,
16
+ } );
@@ -72,6 +72,8 @@ const VALID_SETTINGS = [
72
72
  'typography.textIndent',
73
73
  'typography.textTransform',
74
74
  'typography.writingMode',
75
+ 'viewport.mobile',
76
+ 'viewport.tablet',
75
77
  ];
76
78
 
77
79
  export function getSetting< T = any >(
@@ -1040,6 +1040,47 @@ describe( 'global styles renderer', () => {
1040
1040
  );
1041
1041
  } );
1042
1042
 
1043
+ it( 'handles responsive block gap styles', () => {
1044
+ const tree = {
1045
+ styles: {
1046
+ blocks: {
1047
+ 'core/group': {
1048
+ '@mobile': {
1049
+ spacing: {
1050
+ blockGap: '24px',
1051
+ },
1052
+ },
1053
+ },
1054
+ },
1055
+ },
1056
+ } as unknown as GlobalStylesConfig;
1057
+
1058
+ const blockSelectors = {
1059
+ 'core/group': {
1060
+ selector: '.wp-block-group',
1061
+ hasLayoutSupport: true,
1062
+ },
1063
+ };
1064
+
1065
+ const result = transformToStyles(
1066
+ Object.freeze( tree ),
1067
+ blockSelectors,
1068
+ true,
1069
+ false,
1070
+ false,
1071
+ true,
1072
+ minimalStyleOptions
1073
+ );
1074
+
1075
+ expect( result ).toContain( '@media (width <= 480px)' );
1076
+ expect( result ).toContain(
1077
+ ':root :where(.wp-block-group-is-layout-flow) > * { margin-block-start: 24px; margin-block-end: 0; }'
1078
+ );
1079
+ expect( result ).toContain(
1080
+ ':root :where(.wp-block-group-is-layout-flex) { gap: 24px; }'
1081
+ );
1082
+ } );
1083
+
1043
1084
  it( 'handles legacy responsive block styles', () => {
1044
1085
  const tree = {
1045
1086
  styles: {
@@ -1079,6 +1120,147 @@ describe( 'global styles renderer', () => {
1079
1120
  );
1080
1121
  } );
1081
1122
 
1123
+ it( 'uses custom viewport breakpoints for responsive block styles', () => {
1124
+ const tree = {
1125
+ settings: {
1126
+ viewport: {
1127
+ mobile: '640px',
1128
+ tablet: '960px',
1129
+ },
1130
+ },
1131
+ styles: {
1132
+ blocks: {
1133
+ 'core/button': {
1134
+ '@mobile': {
1135
+ color: {
1136
+ text: 'blue',
1137
+ },
1138
+ },
1139
+ '@tablet': {
1140
+ color: {
1141
+ text: 'green',
1142
+ },
1143
+ },
1144
+ },
1145
+ },
1146
+ },
1147
+ } as unknown as GlobalStylesConfig;
1148
+
1149
+ const blockSelectors = {
1150
+ 'core/button': {
1151
+ selector: '.wp-block-button',
1152
+ },
1153
+ };
1154
+
1155
+ const result = transformToStyles(
1156
+ Object.freeze( tree ),
1157
+ blockSelectors,
1158
+ false,
1159
+ false,
1160
+ true,
1161
+ true,
1162
+ minimalStyleOptions
1163
+ );
1164
+
1165
+ expect( result ).toEqual(
1166
+ '@media (width <= 640px){:root :where(.wp-block-button){color: blue;}}@media (640px < width <= 960px){:root :where(.wp-block-button){color: green;}}'
1167
+ );
1168
+ } );
1169
+
1170
+ it( 'omits tablet responsive styles when the tablet breakpoint is not larger than mobile', () => {
1171
+ const tree = {
1172
+ settings: {
1173
+ viewport: {
1174
+ mobile: '960px',
1175
+ tablet: '640px',
1176
+ },
1177
+ },
1178
+ styles: {
1179
+ blocks: {
1180
+ 'core/button': {
1181
+ '@mobile': {
1182
+ color: {
1183
+ text: 'blue',
1184
+ },
1185
+ },
1186
+ '@tablet': {
1187
+ color: {
1188
+ text: 'green',
1189
+ },
1190
+ },
1191
+ },
1192
+ },
1193
+ },
1194
+ } as unknown as GlobalStylesConfig;
1195
+
1196
+ const blockSelectors = {
1197
+ 'core/button': {
1198
+ selector: '.wp-block-button',
1199
+ },
1200
+ };
1201
+
1202
+ const result = transformToStyles(
1203
+ Object.freeze( tree ),
1204
+ blockSelectors,
1205
+ false,
1206
+ false,
1207
+ true,
1208
+ true,
1209
+ minimalStyleOptions
1210
+ );
1211
+
1212
+ expect( result ).toEqual(
1213
+ '@media (width <= 960px){:root :where(.wp-block-button){color: blue;}}'
1214
+ );
1215
+ } );
1216
+
1217
+ it( 'uses a single max-width tablet query when only the tablet breakpoint is valid', () => {
1218
+ const tree = {
1219
+ settings: {
1220
+ viewport: {
1221
+ mobile: '100%',
1222
+ tablet: '64rem',
1223
+ },
1224
+ },
1225
+ styles: {
1226
+ blocks: {
1227
+ 'core/button': {
1228
+ '@mobile': {
1229
+ color: {
1230
+ text: 'blue',
1231
+ },
1232
+ },
1233
+ '@tablet': {
1234
+ color: {
1235
+ text: 'green',
1236
+ },
1237
+ },
1238
+ },
1239
+ },
1240
+ },
1241
+ } as unknown as GlobalStylesConfig;
1242
+
1243
+ const blockSelectors = {
1244
+ 'core/button': {
1245
+ selector: '.wp-block-button',
1246
+ },
1247
+ };
1248
+
1249
+ const result = transformToStyles(
1250
+ Object.freeze( tree ),
1251
+ blockSelectors,
1252
+ false,
1253
+ false,
1254
+ true,
1255
+ true,
1256
+ minimalStyleOptions
1257
+ );
1258
+
1259
+ expect( result ).toEqual(
1260
+ '@media (width <= 64rem){:root :where(.wp-block-button){color: green;}}'
1261
+ );
1262
+ } );
1263
+
1082
1264
  it( 'handles responsive pseudo selector styles', () => {
1083
1265
  const tree = {
1084
1266
  styles: {
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Internal dependencies
3
+ */
4
+ import {
5
+ getResponsiveMediaQueries,
6
+ getViewportBreakpoints,
7
+ getViewportBreakpointValueInPixels,
8
+ } from '../utils/viewport';
9
+
10
+ describe( 'viewport utils', () => {
11
+ describe( 'getViewportBreakpointValueInPixels', () => {
12
+ it( 'returns numbers unchanged', () => {
13
+ expect( getViewportBreakpointValueInPixels( 640 ) ).toBe( 640 );
14
+ } );
15
+
16
+ it( 'returns pixel values as numbers', () => {
17
+ expect( getViewportBreakpointValueInPixels( '640px' ) ).toBe( 640 );
18
+ } );
19
+
20
+ it( 'converts em and rem values using a 16px base font size', () => {
21
+ expect( getViewportBreakpointValueInPixels( '40em' ) ).toBe( 640 );
22
+ expect( getViewportBreakpointValueInPixels( '64rem' ) ).toBe(
23
+ 1024
24
+ );
25
+ } );
26
+
27
+ it( 'returns undefined for unsupported values', () => {
28
+ expect( getViewportBreakpointValueInPixels( undefined ) ).toBe(
29
+ undefined
30
+ );
31
+ expect( getViewportBreakpointValueInPixels( '100%' ) ).toBe(
32
+ undefined
33
+ );
34
+ expect( getViewportBreakpointValueInPixels( 'auto' ) ).toBe(
35
+ undefined
36
+ );
37
+ } );
38
+ } );
39
+
40
+ describe( 'getViewportBreakpoints', () => {
41
+ it( 'returns custom viewport breakpoints when they are ordered', () => {
42
+ expect(
43
+ getViewportBreakpoints( {
44
+ mobile: '40rem',
45
+ tablet: '64rem',
46
+ } )
47
+ ).toEqual( {
48
+ mobile: '40rem',
49
+ tablet: '64rem',
50
+ } );
51
+ } );
52
+
53
+ it( 'returns default viewport breakpoints when no custom breakpoints are valid', () => {
54
+ expect(
55
+ getViewportBreakpoints( {
56
+ mobile: '100%',
57
+ tablet: 'auto',
58
+ } )
59
+ ).toEqual( {
60
+ mobile: '480px',
61
+ tablet: '782px',
62
+ } );
63
+ } );
64
+
65
+ it( 'uses a valid custom breakpoint without merging defaults', () => {
66
+ const viewportSettings = {
67
+ mobile: ' 640px ',
68
+ tablet: 'calc(100% - 1rem)',
69
+ desktop: '1200px',
70
+ };
71
+
72
+ expect( getViewportBreakpoints( viewportSettings ) ).toEqual( {
73
+ mobile: '640px',
74
+ } );
75
+ } );
76
+
77
+ it( 'returns the configured viewport for a single breakpoint', () => {
78
+ expect(
79
+ getViewportBreakpoints( {
80
+ tablet: '64rem',
81
+ } )
82
+ ).toEqual( {
83
+ tablet: '64rem',
84
+ } );
85
+ } );
86
+
87
+ it( 'preserves the tablet key when mobile is invalid', () => {
88
+ expect(
89
+ getViewportBreakpoints( {
90
+ mobile: '100%',
91
+ tablet: '64rem',
92
+ } )
93
+ ).toEqual( {
94
+ tablet: '64rem',
95
+ } );
96
+ } );
97
+
98
+ it( 'omits tablet when it is not larger than mobile', () => {
99
+ expect(
100
+ getViewportBreakpoints( {
101
+ mobile: '64rem',
102
+ tablet: '40rem',
103
+ } )
104
+ ).toEqual( {
105
+ mobile: '64rem',
106
+ } );
107
+ } );
108
+ } );
109
+
110
+ describe( 'getResponsiveMediaQueries', () => {
111
+ it( 'returns custom media queries when viewport breakpoints are ordered', () => {
112
+ expect(
113
+ getResponsiveMediaQueries( {
114
+ mobile: '640px',
115
+ tablet: '960px',
116
+ } )
117
+ ).toEqual( {
118
+ '@mobile': '@media (width <= 640px)',
119
+ '@tablet': '@media (640px < width <= 960px)',
120
+ } );
121
+ } );
122
+
123
+ it( 'returns default media queries when no custom breakpoints are valid', () => {
124
+ expect(
125
+ getResponsiveMediaQueries( {
126
+ mobile: '100%',
127
+ tablet: 'auto',
128
+ } )
129
+ ).toEqual( {
130
+ '@mobile': '@media (width <= 480px)',
131
+ '@tablet': '@media (480px < width <= 782px)',
132
+ } );
133
+ } );
134
+
135
+ it( 'uses a valid custom media query without merging defaults', () => {
136
+ expect(
137
+ getResponsiveMediaQueries( {
138
+ mobile: ' 640px ',
139
+ tablet: 'calc(100% - 1rem)',
140
+ } )
141
+ ).toEqual( {
142
+ '@mobile': '@media (width <= 640px)',
143
+ } );
144
+ } );
145
+
146
+ it( 'uses a single max-width tablet media query when only tablet is valid', () => {
147
+ expect(
148
+ getResponsiveMediaQueries( {
149
+ mobile: '100%',
150
+ tablet: '64rem',
151
+ } )
152
+ ).toEqual( {
153
+ '@tablet': '@media (width <= 64rem)',
154
+ } );
155
+ } );
156
+
157
+ it( 'omits the tablet media query when the tablet breakpoint is not larger than mobile', () => {
158
+ expect(
159
+ getResponsiveMediaQueries( {
160
+ mobile: '960px',
161
+ tablet: '640px',
162
+ } )
163
+ ).toEqual( {
164
+ '@mobile': '@media (width <= 960px)',
165
+ } );
166
+ } );
167
+ } );
168
+ } );
package/src/types.ts CHANGED
@@ -193,6 +193,10 @@ export interface SpacingSettings {
193
193
  */
194
194
  export interface GlobalStylesSettings {
195
195
  useRootPaddingAwareAlignments?: boolean;
196
+ viewport?: {
197
+ mobile?: string;
198
+ tablet?: string;
199
+ };
196
200
  typography?: TypographySettings;
197
201
  layout?: LayoutSettings;
198
202
  spacing?: SpacingSettings;
@@ -317,14 +317,19 @@ export function getBlockStyleVariationSelector(
317
317
  return variationClass;
318
318
  }
319
319
 
320
- const ancestorRegex = /((?::\([^)]+\))?\s*)([^\s:]+)/;
321
- const addVariationClass = (
322
- _match: string,
323
- group1: string,
324
- group2: string
325
- ) => {
326
- return group1 + group2 + variationClass;
327
- };
320
+ /*
321
+ * Append the variation class to each selector's ancestor: the first run
322
+ * of characters before any combinator (whitespace) or pseudo-class (`:`).
323
+ * `String.prototype.replace` only replaces the first match.
324
+ *
325
+ * Examples ("custom" variation):
326
+ * - `.wp-block` => `.wp-block.is-style-custom`
327
+ * - `.wp-block .inner` => `.wp-block.is-style-custom .inner`
328
+ * - `.wp-block:where(.a .b)` => `.wp-block.is-style-custom:where(.a .b)`
329
+ * - `:where(.outer .inner)` => `:where(.outer.is-style-custom .inner)`
330
+ */
331
+ const ancestorRegex = /[^\s:]+/;
332
+ const addVariationClass = ( match: string ) => match + variationClass;
328
333
 
329
334
  const result = splitSelectorList( blockSelector ).map( ( part ) =>
330
335
  part.replace( ancestorRegex, addVariationClass )