@wordpress/global-styles-engine 1.17.1-next.v.202607070741.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 (52) hide show
  1. package/CHANGELOG.md +2 -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/viewport.cjs +108 -0
  14. package/build/utils/viewport.cjs.map +7 -0
  15. package/build-module/core/render.mjs +36 -16
  16. package/build-module/core/render.mjs.map +2 -2
  17. package/build-module/index.mjs +2 -0
  18. package/build-module/index.mjs.map +2 -2
  19. package/build-module/lock-unlock.mjs +11 -0
  20. package/build-module/lock-unlock.mjs.map +7 -0
  21. package/build-module/private-apis.mjs +17 -0
  22. package/build-module/private-apis.mjs.map +7 -0
  23. package/build-module/settings/get-setting.mjs +3 -1
  24. package/build-module/settings/get-setting.mjs.map +2 -2
  25. package/build-module/utils/viewport.mjs +81 -0
  26. package/build-module/utils/viewport.mjs.map +7 -0
  27. package/build-types/core/render.d.ts +1 -1
  28. package/build-types/core/render.d.ts.map +1 -1
  29. package/build-types/index.d.ts +1 -0
  30. package/build-types/index.d.ts.map +1 -1
  31. package/build-types/lock-unlock.d.ts +2 -0
  32. package/build-types/lock-unlock.d.ts.map +1 -0
  33. package/build-types/private-apis.d.ts +2 -0
  34. package/build-types/private-apis.d.ts.map +1 -0
  35. package/build-types/settings/get-setting.d.ts.map +1 -1
  36. package/build-types/style-state-back-compat.d.ts.map +1 -1
  37. package/build-types/types.d.ts +4 -0
  38. package/build-types/types.d.ts.map +1 -1
  39. package/build-types/utils/fluid.d.ts +1 -1
  40. package/build-types/utils/fluid.d.ts.map +1 -1
  41. package/build-types/utils/viewport.d.ts +41 -0
  42. package/build-types/utils/viewport.d.ts.map +1 -0
  43. package/package.json +7 -6
  44. package/src/core/render.tsx +45 -21
  45. package/src/index.ts +1 -0
  46. package/src/lock-unlock.ts +10 -0
  47. package/src/private-apis.ts +16 -0
  48. package/src/settings/get-setting.ts +2 -0
  49. package/src/test/render.test.ts +182 -0
  50. package/src/test/viewport-utils.test.ts +168 -0
  51. package/src/types.ts +4 -0
  52. package/src/utils/viewport.ts +153 -0
@@ -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;
@@ -0,0 +1,153 @@
1
+ import type { GlobalStylesConfig } from '../types';
2
+
3
+ const DEFAULT_VIEWPORT_BREAKPOINTS = {
4
+ mobile: '480px',
5
+ tablet: '782px',
6
+ };
7
+
8
+ type ViewportBreakpoint = keyof typeof DEFAULT_VIEWPORT_BREAKPOINTS;
9
+ type ViewportSettings = Partial< Record< ViewportBreakpoint, string > >;
10
+ type ViewportBreakpoints = Partial< Record< ViewportBreakpoint, string > >;
11
+
12
+ // Matches positive CSS length values supported for viewport breakpoints, and
13
+ // captures the numeric value and unit for conversion to pixels.
14
+ const VIEWPORT_SIZE_REGEXP = /^(\d+|\d*\.\d+)(px|em|rem)$/;
15
+ const DEFAULT_FONT_SIZE = 16;
16
+
17
+ function isViewportSettings(
18
+ configOrSettings: GlobalStylesConfig | ViewportSettings
19
+ ): configOrSettings is ViewportSettings {
20
+ return 'mobile' in configOrSettings || 'tablet' in configOrSettings;
21
+ }
22
+
23
+ function getViewportSettings(
24
+ configOrSettings?: GlobalStylesConfig | ViewportSettings
25
+ ): ViewportSettings {
26
+ if ( ! configOrSettings || typeof configOrSettings !== 'object' ) {
27
+ return {};
28
+ }
29
+
30
+ if ( isViewportSettings( configOrSettings ) ) {
31
+ return configOrSettings;
32
+ }
33
+
34
+ return configOrSettings.settings?.viewport ?? {};
35
+ }
36
+
37
+ function isValidViewportSize( value: unknown ): value is string {
38
+ return (
39
+ typeof value === 'string' && VIEWPORT_SIZE_REGEXP.test( value.trim() )
40
+ );
41
+ }
42
+
43
+ /**
44
+ * Converts a viewport breakpoint value to pixels.
45
+ *
46
+ * Numeric values are returned as-is. CSS length values are only supported when
47
+ * expressed in px, em, or rem units.
48
+ *
49
+ * @param value Breakpoint value as a number or CSS length.
50
+ * @return The breakpoint value in pixels, or undefined when invalid.
51
+ */
52
+ export function getViewportBreakpointValueInPixels(
53
+ value: number | string | undefined
54
+ ): number | undefined {
55
+ if ( typeof value === 'number' ) {
56
+ return value;
57
+ }
58
+
59
+ if ( typeof value !== 'string' ) {
60
+ return undefined;
61
+ }
62
+
63
+ const match = value.trim().match( VIEWPORT_SIZE_REGEXP );
64
+ if ( ! match ) {
65
+ return undefined;
66
+ }
67
+
68
+ const numericValue = Number.parseFloat( match[ 1 ] );
69
+ const unit = match[ 2 ];
70
+
71
+ // Use the most common browser default font size as the base for em/rem
72
+ // media query conversions. This pixel value is used to compare breakpoint
73
+ // order and calculate preview sizes; generated media queries keep the
74
+ // original units.
75
+ return unit === 'px' ? numericValue : numericValue * DEFAULT_FONT_SIZE;
76
+ }
77
+
78
+ /**
79
+ * Returns sanitized viewport breakpoints keyed by viewport state.
80
+ *
81
+ * Falls back to default breakpoints when no valid values are provided. If the
82
+ * mobile breakpoint is missing, a valid tablet breakpoint remains keyed as
83
+ * tablet so the editor can present it as a tablet breakpoint. If the tablet
84
+ * breakpoint is missing, invalid, or not larger than the mobile breakpoint, the
85
+ * result only includes the mobile breakpoint.
86
+ *
87
+ * @param configOrSettings Global styles config or viewport settings.
88
+ * @return Sanitized viewport breakpoints.
89
+ */
90
+ export function getViewportBreakpoints(
91
+ configOrSettings?: GlobalStylesConfig | ViewportSettings
92
+ ): ViewportBreakpoints {
93
+ const viewportSettings = getViewportSettings( configOrSettings );
94
+ const breakpoints: ViewportBreakpoints = {};
95
+ const breakpointValuesInPixels: Partial<
96
+ Record< ViewportBreakpoint, number >
97
+ > = {};
98
+
99
+ Object.keys( DEFAULT_VIEWPORT_BREAKPOINTS ).forEach( ( breakpoint ) => {
100
+ const key = breakpoint as ViewportBreakpoint;
101
+ const value = viewportSettings[ key ];
102
+ const px = getViewportBreakpointValueInPixels( value );
103
+ if ( px !== undefined && isValidViewportSize( value ) ) {
104
+ breakpoints[ key ] = value.trim();
105
+ breakpointValuesInPixels[ key ] = px;
106
+ }
107
+ } );
108
+
109
+ const breakpointNames = Object.keys( breakpoints );
110
+ if ( ! breakpointNames.length ) {
111
+ return { ...DEFAULT_VIEWPORT_BREAKPOINTS };
112
+ }
113
+
114
+ if ( 1 === breakpointNames.length ) {
115
+ return breakpoints;
116
+ }
117
+
118
+ const mobile = breakpoints.mobile!;
119
+ const tablet = breakpoints.tablet!;
120
+ if (
121
+ breakpointValuesInPixels.mobile! >= breakpointValuesInPixels.tablet!
122
+ ) {
123
+ return { mobile };
124
+ }
125
+
126
+ return { mobile, tablet };
127
+ }
128
+
129
+ /**
130
+ * Returns responsive media query aliases for the configured viewport
131
+ * breakpoints.
132
+ *
133
+ * @param configOrSettings Global styles config or viewport settings.
134
+ * @return Responsive media queries keyed by alias.
135
+ */
136
+ export function getResponsiveMediaQueries(
137
+ configOrSettings?: GlobalStylesConfig | ViewportSettings
138
+ ): Record< string, string > {
139
+ const breakpoints = getViewportBreakpoints( configOrSettings );
140
+ const mediaQueries: Record< string, string > = {};
141
+
142
+ if ( breakpoints.mobile ) {
143
+ mediaQueries[ '@mobile' ] = `@media (width <= ${ breakpoints.mobile })`;
144
+ }
145
+
146
+ if ( breakpoints.tablet ) {
147
+ mediaQueries[ '@tablet' ] = breakpoints.mobile
148
+ ? `@media (${ breakpoints.mobile } < width <= ${ breakpoints.tablet })`
149
+ : `@media (width <= ${ breakpoints.tablet })`;
150
+ }
151
+
152
+ return mediaQueries;
153
+ }