From d9dc32631afdb19a9a7a8de64c4be47e5be0fe22 Mon Sep 17 00:00:00 2001 From: Aki Hamano Date: Fri, 29 May 2026 15:43:37 +0900 Subject: [PATCH 1/2] Icons API: Add wp_get_icon() to render registered icons as inline SVG. Introduce a procedural wrapper around `WP_Icons_Registry` so themes and plugins can render any registered core icon as an inline SVG from PHP, mirroring the React `` component. This bridges the gap for PHP contexts that otherwise depend on the deprecated dashicons font. `wp_get_icon()` looks up the icon content via the registry and uses `WP_HTML_Tag_Processor` to apply configurable `size` and `class` attributes, plus accessibility handling: a `label` produces `role="img"` and `aria-label`, while its absence marks the icon `aria-hidden="true"`. The function lives in a new `icons.php`, following the established registry-class-plus-procedural-API pairing used by connectors.php and abilities.php. Props ... Co-Authored-By: Claude Opus 4.8 (1M context) --- src/wp-includes/icons.php | 68 +++++++++++++++++ src/wp-settings.php | 1 + tests/phpunit/tests/icons/wpGetIcon.php | 97 +++++++++++++++++++++++++ 3 files changed, 166 insertions(+) create mode 100644 src/wp-includes/icons.php create mode 100644 tests/phpunit/tests/icons/wpGetIcon.php diff --git a/src/wp-includes/icons.php b/src/wp-includes/icons.php new file mode 100644 index 0000000000000..33f6498d50f23 --- /dev/null +++ b/src/wp-includes/icons.php @@ -0,0 +1,68 @@ +get_registered_icon( $name ); + if ( is_null( $icon ) ) { + return ''; + } + + $svg = $icon['content']; + if ( empty( $svg ) ) { + return ''; + } + + $args = wp_parse_args( + $args, + array( + 'size' => 24, + 'class' => '', + 'label' => '', + ) + ); + + $processor = new WP_HTML_Tag_Processor( $svg ); + if ( ! $processor->next_tag( 'svg' ) ) { + return ''; + } + + $processor->set_attribute( 'width', (string) $args['size'] ); + $processor->set_attribute( 'height', (string) $args['size'] ); + $processor->add_class( 'wp-icon' ); + + if ( ! empty( $args['class'] ) ) { + $processor->add_class( $args['class'] ); + } + + if ( ! empty( $args['label'] ) ) { + $processor->set_attribute( 'role', 'img' ); + $processor->set_attribute( 'aria-label', $args['label'] ); + } else { + $processor->set_attribute( 'aria-hidden', 'true' ); + } + + return $processor->get_updated_html(); +} diff --git a/src/wp-settings.php b/src/wp-settings.php index b2736bddadc3c..ac831c864c285 100644 --- a/src/wp-settings.php +++ b/src/wp-settings.php @@ -297,6 +297,7 @@ require ABSPATH . WPINC . '/class-wp-connector-registry.php'; require ABSPATH . WPINC . '/connectors.php'; require ABSPATH . WPINC . '/class-wp-icons-registry.php'; +require ABSPATH . WPINC . '/icons.php'; require ABSPATH . WPINC . '/widgets.php'; require ABSPATH . WPINC . '/class-wp-widget.php'; require ABSPATH . WPINC . '/class-wp-widget-factory.php'; diff --git a/tests/phpunit/tests/icons/wpGetIcon.php b/tests/phpunit/tests/icons/wpGetIcon.php new file mode 100644 index 0000000000000..9cf36b31e6fbb --- /dev/null +++ b/tests/phpunit/tests/icons/wpGetIcon.php @@ -0,0 +1,97 @@ +assertStringStartsWith( 'assertStringContainsString( '', $output ); + } + + /** + * @ticket 64847 + */ + public function test_wp_get_icon_returns_empty_string_for_unknown_icon() { + $output = wp_get_icon( 'this-icon-does-not-exist' ); + $this->assertSame( '', $output ); + } + + /** + * @ticket 64847 + */ + public function test_wp_get_icon_default_attributes() { + $output = wp_get_icon( 'core/plus' ); + // WP_HTML_Tag_Processor lowercases attribute names. + $this->assertStringContainsString( 'viewbox="0 0 24 24"', $output ); + $this->assertStringContainsString( 'width="24"', $output ); + $this->assertStringContainsString( 'height="24"', $output ); + $this->assertStringContainsString( 'class="wp-icon"', $output ); + $this->assertStringContainsString( 'aria-hidden="true"', $output ); + } + + /** + * @ticket 64847 + */ + public function test_wp_get_icon_custom_size() { + $output = wp_get_icon( 'core/plus', array( 'size' => 32 ) ); + $this->assertStringContainsString( 'width="32"', $output ); + $this->assertStringContainsString( 'height="32"', $output ); + } + + /** + * @ticket 64847 + */ + public function test_wp_get_icon_custom_class() { + $output = wp_get_icon( 'core/plus', array( 'class' => 'my-button-icon' ) ); + $this->assertStringContainsString( 'class="wp-icon my-button-icon"', $output ); + } + + /** + * @ticket 64847 + */ + public function test_wp_get_icon_with_label() { + $output = wp_get_icon( 'core/plus', array( 'label' => 'Add item' ) ); + $this->assertStringContainsString( 'role="img"', $output ); + $this->assertStringContainsString( 'aria-label="Add item"', $output ); + $this->assertStringNotContainsString( 'aria-hidden', $output ); + } + + /** + * @ticket 64847 + */ + public function test_wp_get_icon_without_label_is_hidden() { + $output = wp_get_icon( 'core/plus' ); + $this->assertStringContainsString( 'aria-hidden="true"', $output ); + $this->assertStringNotContainsString( 'role="img"', $output ); + $this->assertStringNotContainsString( 'aria-label', $output ); + } + + /** + * @ticket 64847 + */ + public function test_wp_get_icon_contains_svg_content() { + $output = wp_get_icon( 'core/plus' ); + $this->assertStringContainsString( ' '">' ) ); + $this->assertStringNotContainsString( '