Skip to content

Commit 37dd1a5

Browse files
authored
Merge branch 'trunk' into bundled-themes-7-0
2 parents d4ed049 + 0fced49 commit 37dd1a5

21 files changed

Lines changed: 1092 additions & 51 deletions

composer.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@
3232
},
3333
"lock": false
3434
},
35+
"autoload-dev": {
36+
"psr-4": {
37+
"WordPress\\PHPStan\\": "tests/phpstan/"
38+
}
39+
},
3540
"scripts": {
3641
"phpstan": "@php ./vendor/bin/phpstan analyse --memory-limit=2G",
3742
"compat": "@php ./vendor/squizlabs/php_codesniffer/bin/phpcs --standard=phpcompat.xml.dist --report=summary,source",

src/wp-admin/css/common.css

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -829,7 +829,7 @@ img.emoji {
829829
border-radius: 50%;
830830
color: var(--wp-admin-theme-color, #3858e9);
831831
/* vertically center the icon cross browsers */
832-
line-height: 1.28;
832+
line-height: 1.1;
833833
}
834834

835835
.tagchecklist .ntdelbutton:focus {
@@ -961,7 +961,6 @@ a#remove-post-thumbnail:hover,
961961

962962
#publishing-action .spinner {
963963
float: none;
964-
margin-top: 5px;
965964
}
966965

967966
#misc-publishing-actions {
@@ -2460,7 +2459,7 @@ h1.nav-tab-wrapper, /* Back-compat for pre-4.4 */
24602459
filter: alpha(opacity=70);
24612460
width: 20px;
24622461
height: 20px;
2463-
margin: 4px 10px 0;
2462+
margin: 10px 10px 0;
24642463
}
24652464

24662465
.spinner.is-active,
@@ -2480,6 +2479,7 @@ h1.nav-tab-wrapper, /* Back-compat for pre-4.4 */
24802479
}
24812480
#template .submit .spinner {
24822481
float: none;
2482+
vertical-align: top;
24832483
}
24842484

24852485
.metabox-holder .stuffbox > h3, /* Back-compat for pre-4.4 */

src/wp-admin/css/customize-controls.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -572,6 +572,10 @@ body.trashing #publish-settings {
572572
display: block;
573573
}
574574

575+
.accordion-section-title button.accordion-trigger .spinner {
576+
margin-top: 0;
577+
}
578+
575579
@media (prefers-reduced-motion: reduce) {
576580
#customize-theme-controls .accordion-section-title,
577581
#customize-outer-theme-controls .accordion-section-title {

src/wp-admin/css/dashboard.css

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,6 @@
6060
/* Required min-height to make the jQuery UI Sortable drop zone work. */
6161
min-height: 0;
6262
margin: 0 8px 20px;
63-
padding: 0;
6463
}
6564

6665
#dashboard-widgets .meta-box-sortables:not(:empty) {

src/wp-admin/css/edit.css

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -173,10 +173,6 @@ body.post-type-wp_navigation .inline-edit-status {
173173

174174
/* Post Screen */
175175

176-
.metabox-holder .postbox-container .meta-box-sortables {
177-
padding: 4px;
178-
}
179-
180176
/* Only highlight drop zones when dragging and only in the 2 columns layout. */
181177
.is-dragging-metaboxes .metabox-holder .postbox-container .meta-box-sortables {
182178
border-radius: 8px;

src/wp-admin/css/list-tables.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1609,7 +1609,7 @@ div.action-links,
16091609
}
16101610

16111611
.plugin-card h3 {
1612-
margin: 0 12px 12px 0;
1612+
margin: 0 12px 16px 0;
16131613
font-size: 18px;
16141614
line-height: 1.3;
16151615
}

src/wp-includes/admin-bar.php

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -949,16 +949,40 @@ function wp_admin_bar_command_palette_menu( WP_Admin_Bar $wp_admin_bar ): void {
949949
return;
950950
}
951951

952-
$is_apple_os = (bool) preg_match( '/Macintosh|Mac OS X|Mac_PowerPC/i', $_SERVER['HTTP_USER_AGENT'] ?? '' );
953-
$shortcut_label = $is_apple_os
954-
? _x( '⌘K', 'keyboard shortcut to open the command palette' )
955-
: _x( 'Ctrl+K', 'keyboard shortcut to open the command palette' );
956-
$title = sprintf(
952+
$shortcut_labels = array(
953+
'appleOS' => _x( '⌘K', 'keyboard shortcut to open the command palette' ),
954+
'default' => _x( 'Ctrl+K', 'keyboard shortcut to open the command palette' ),
955+
);
956+
$apple_pattern = 'Macintosh|Mac OS X|Mac_PowerPC';
957+
$is_apple_os = (bool) preg_match( "/{$apple_pattern}/i", $_SERVER['HTTP_USER_AGENT'] ?? '' );
958+
$shortcut_label = $is_apple_os ? $shortcut_labels['appleOS'] : $shortcut_labels['default'];
959+
$title = sprintf(
957960
'<span class="ab-icon" aria-hidden="true"></span><span class="ab-label"><kbd>%s</kbd><span class="screen-reader-text"> %s</span></span>',
958961
$shortcut_label,
959962
/* translators: Hidden accessibility text. */
960963
__( 'Open command palette' ),
961964
);
965+
/*
966+
* Detect Apple OS via JavaScript for sites behind a CDN blocking the UA header.
967+
*
968+
* Running the script as the admin bar is rendered avoids a flash of incorrect content
969+
* for users with Apple OS when the UA header is blocked. It also prevents the need for
970+
* wp-i18n to be loaded as a dependency.
971+
*/
972+
$function = <<<'JS'
973+
( applePattern, appleOSLabel ) => {
974+
if ( ( new RegExp( applePattern ) ).test( navigator.userAgent ) ) {
975+
document.querySelector( '#wp-admin-bar-command-palette .ab-label kbd' ).textContent = appleOSLabel;
976+
}
977+
}
978+
JS;
979+
$script = sprintf(
980+
'( %s )( %s, %s );',
981+
$function,
982+
wp_json_encode( $apple_pattern, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ),
983+
wp_json_encode( $shortcut_labels['appleOS'], JSON_HEX_TAG | JSON_UNESCAPED_SLASHES )
984+
);
985+
$script .= "\n//# sourceURL=" . rawurlencode( __FUNCTION__ );
962986
$wp_admin_bar->add_node(
963987
array(
964988
'id' => 'command-palette',
@@ -967,6 +991,7 @@ function wp_admin_bar_command_palette_menu( WP_Admin_Bar $wp_admin_bar ): void {
967991
'meta' => array(
968992
'class' => 'hide-if-no-js',
969993
'onclick' => 'wp.data.dispatch( "core/commands" ).open(); return false;',
994+
'html' => wp_get_inline_script_tag( $script ),
970995
),
971996
)
972997
);

src/wp-includes/class-wp-connector-registry.php

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@
4040
* env_var_name?: non-empty-string
4141
* },
4242
* plugin?: array{
43-
* file: non-empty-string
43+
* file: non-empty-string,
44+
* is_active?: callable(): bool
4445
* }
4546
* }
4647
*/
@@ -109,8 +110,12 @@ final class WP_Connector_Registry {
109110
* @type array $plugin {
110111
* Optional. Plugin data for install/activate UI.
111112
*
112-
* @type string $file The plugin's main file path relative to the plugins
113-
* directory (e.g. 'my-plugin/my-plugin.php' or 'hello.php').
113+
* @type string $file Optional. The plugin's main file path relative to the
114+
* plugins directory (e.g. 'my-plugin/my-plugin.php' or
115+
* 'hello.php').
116+
* @type callable $is_active Optional callback to determine whether the plugin
117+
* is active. Receives no arguments and must return bool.
118+
* Defaults to `__return_true`.
114119
* }
115120
* }
116121
* @return array|null The registered connector data on success, null on failure.
@@ -243,8 +248,30 @@ public function register( string $id, array $args ): ?array {
243248
}
244249
}
245250

246-
if ( ! empty( $args['plugin'] ) && is_array( $args['plugin'] ) && ! empty( $args['plugin']['file'] ) ) {
247-
$connector['plugin'] = array( 'file' => $args['plugin']['file'] );
251+
$connector['plugin'] = array();
252+
253+
if ( ! empty( $args['plugin'] ) && is_array( $args['plugin'] ) ) {
254+
if ( ! empty( $args['plugin']['file'] ) ) {
255+
$connector['plugin']['file'] = $args['plugin']['file'];
256+
}
257+
258+
if ( isset( $args['plugin']['is_active'] ) ) {
259+
if ( ! is_callable( $args['plugin']['is_active'] ) ) {
260+
_doing_it_wrong(
261+
__METHOD__,
262+
/* translators: %s: Connector ID. */
263+
sprintf( __( 'Connector "%s" plugin is_active must be callable.' ), esc_html( $id ) ),
264+
'7.0.0'
265+
);
266+
return null;
267+
}
268+
269+
$connector['plugin']['is_active'] = $args['plugin']['is_active'];
270+
}
271+
}
272+
273+
if ( ! isset( $connector['plugin']['is_active'] ) ) {
274+
$connector['plugin']['is_active'] = '__return_true';
248275
}
249276

250277
$this->registered_connectors[ $id ] = $connector;

src/wp-includes/class-wp-script-modules.php

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,24 @@
1212
* Core class used to register script modules.
1313
*
1414
* @since 6.5.0
15+
*
16+
* @phpstan-type ScriptModule array{
17+
* src: string,
18+
* version: string|false|null,
19+
* dependencies: array<int, array{ id: string, import: 'static'|'dynamic' }>,
20+
* in_footer: bool,
21+
* fetchpriority: 'auto'|'low'|'high',
22+
* textdomain?: string,
23+
* translations_path?: string,
24+
* }
1525
*/
1626
class WP_Script_Modules {
1727
/**
1828
* Holds the registered script modules, keyed by script module identifier.
1929
*
2030
* @since 6.5.0
2131
* @var array<string, array<string, mixed>>
32+
* @phpstan-var array<string, ScriptModule>
2233
*/
2334
private $registered = array();
2435

@@ -328,6 +339,87 @@ public function deregister( string $id ) {
328339
unset( $this->registered[ $id ] );
329340
}
330341

342+
/**
343+
* Overrides the text domain and path used to load translations for a script module.
344+
*
345+
* This is only needed for modules whose text domain differs from 'default'
346+
* or whose translation files live outside the standard locations, for
347+
* example plugin modules that register their own text domain. Translations
348+
* for modules that use the default domain are loaded automatically by
349+
* {@see WP_Script_Modules::print_script_module_translations()}.
350+
*
351+
* @since 7.0.0
352+
*
353+
* @param string $id The identifier of the script module.
354+
* @param string $domain Optional. Text domain. Default 'default'.
355+
* @param string $path Optional. The full file path to the directory containing translation files.
356+
* @return bool True if the text domain was registered, false if the module is not registered.
357+
*/
358+
public function set_translations( string $id, string $domain = 'default', string $path = '' ): bool {
359+
if ( ! isset( $this->registered[ $id ] ) ) {
360+
return false;
361+
}
362+
363+
$this->registered[ $id ]['textdomain'] = $domain;
364+
$this->registered[ $id ]['translations_path'] = $path;
365+
366+
return true;
367+
}
368+
369+
/**
370+
* Prints translations for all enqueued script modules.
371+
*
372+
* Outputs inline `<script>` tags that call `wp.i18n.setLocaleData()` with
373+
* the translated strings for each script module. This must run before
374+
* the script modules execute.
375+
*
376+
* Auto-detects the text domain and translation path for each module from
377+
* its source URL. Modules whose text domain or path differs from the
378+
* defaults can opt into a specific domain/path via
379+
* {@see WP_Script_Modules::set_translations()}.
380+
*
381+
* @since 7.0.0
382+
*/
383+
public function print_script_module_translations(): void {
384+
// Collect all module IDs that will be on the page (enqueued + their dependencies).
385+
$module_ids = $this->get_sorted_dependencies( $this->queue );
386+
387+
$set_locale_data_js_function = <<<'JS'
388+
( domain, translations ) => {
389+
const localeData = translations.locale_data[ domain ] || translations.locale_data.messages;
390+
localeData[""].domain = domain;
391+
wp.i18n.setLocaleData( localeData, domain );
392+
}
393+
JS;
394+
395+
foreach ( $module_ids as $id ) {
396+
$domain = $this->registered[ $id ]['textdomain'] ?? 'default';
397+
$path = $this->registered[ $id ]['translations_path'] ?? '';
398+
399+
$json_translations = load_script_module_textdomain( $id, $domain, $path );
400+
401+
if ( ! $json_translations ) {
402+
continue;
403+
}
404+
405+
$output = sprintf(
406+
'( %s )( %s, %s );',
407+
$set_locale_data_js_function,
408+
wp_json_encode( $domain, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ),
409+
$json_translations
410+
);
411+
$script_id = "wp-script-module-translation-data-{$id}";
412+
$output .= "\n//# sourceURL=" . rawurlencode( $script_id );
413+
414+
// Ensure wp-i18n is printed; the inline script below relies on wp.i18n.setLocaleData().
415+
if ( ! wp_script_is( 'wp-i18n', 'done' ) ) {
416+
wp_scripts()->do_items( array( 'wp-i18n' ) );
417+
}
418+
419+
wp_print_inline_script_tag( $output, array( 'id' => $script_id ) );
420+
}
421+
}
422+
331423
/**
332424
* Adds the hooks to print the import map, enqueued script modules and script
333425
* module preloads.
@@ -359,6 +451,15 @@ public function add_hooks() {
359451
add_action( 'admin_print_footer_scripts', array( $this, 'print_enqueued_script_modules' ) );
360452
add_action( 'admin_print_footer_scripts', array( $this, 'print_script_module_preloads' ) );
361453

454+
/*
455+
* Print translations after classic scripts like wp-i18n are loaded (at
456+
* priority 10 via _wp_footer_scripts), but before the script modules
457+
* execute. Script modules with type="module" are deferred by default,
458+
* so inline translation scripts at priority 11 will execute before them.
459+
*/
460+
add_action( 'wp_footer', array( $this, 'print_script_module_translations' ), 21 );
461+
add_action( 'admin_print_footer_scripts', array( $this, 'print_script_module_translations' ), 11 );
462+
362463
add_action( 'wp_footer', array( $this, 'print_script_module_data' ) );
363464
add_action( 'admin_print_footer_scripts', array( $this, 'print_script_module_data' ) );
364465
add_action( 'wp_footer', array( $this, 'print_a11y_script_module_html' ), 20 );
@@ -631,6 +732,7 @@ private function get_import_map(): array {
631732
* @since 6.5.0
632733
*
633734
* @return array<string, array<string, mixed>> Script modules marked for enqueue, keyed by script module identifier.
735+
* @phpstan-return array<string, ScriptModule>
634736
*/
635737
private function get_marked_for_enqueue(): array {
636738
return wp_array_slice_assoc(
@@ -652,6 +754,7 @@ private function get_marked_for_enqueue(): array {
652754
* @param string[] $import_types Optional. Import types of dependencies to retrieve: 'static', 'dynamic', or both.
653755
* Default is both.
654756
* @return array<string, array<string, mixed>> List of dependencies, keyed by script module identifier.
757+
* @phpstan-return array<string, ScriptModule>
655758
*/
656759
private function get_dependencies( array $ids, array $import_types = array( 'static', 'dynamic' ) ): array {
657760
$all_dependencies = array();
@@ -840,6 +943,19 @@ private function sort_item_dependencies( string $id, array $import_types, array
840943
return true;
841944
}
842945

946+
/**
947+
* Gets the data for a registered script module.
948+
*
949+
* @since 7.0.0
950+
*
951+
* @param string $id The script module identifier.
952+
* @return array|null The script module data, or null if not registered.
953+
* @phpstan-return ScriptModule|null
954+
*/
955+
public function get_registered( string $id ): ?array {
956+
return $this->registered[ $id ] ?? null;
957+
}
958+
843959
/**
844960
* Gets the versioned URL for a script module src.
845961
*

0 commit comments

Comments
 (0)