diff --git a/src/wp-admin/includes/class-wp-list-table.php b/src/wp-admin/includes/class-wp-list-table.php
index 0b20f6be0c..8ccd02998f 100644
--- a/src/wp-admin/includes/class-wp-list-table.php
+++ b/src/wp-admin/includes/class-wp-list-table.php
@@ -419,12 +419,29 @@ class WP_List_Table {
}
/**
- * Gets the list of bulk actions available on this table.
+ * Retrieves the list of bulk actions available for this table.
*
- * The format is an associative array:
- * - `'option_name' => 'option_title'`
+ * The format is an associative array where each element represents either a top level option value and label, or
+ * an array representing an optgroup and its options.
+ *
+ * For a standard option, the array element key is the field value and the array element value is the field label.
+ *
+ * For an optgroup, the array element key is the label and the array element value is an associative array of
+ * options as above.
+ *
+ * Example:
+ *
+ * [
+ * 'edit' => 'Edit',
+ * 'delete' => 'Delete',
+ * 'Change State' => [
+ * 'feature' => 'Featured',
+ * 'sale' => 'On Sale',
+ * ]
+ * ]
*
* @since 3.1.0
+ * @since 5.6.0 A bulk action can now contain an array of options in order to create an optgroup.
*
* @return array
*/
@@ -445,14 +462,15 @@ class WP_List_Table {
$this->_actions = $this->get_bulk_actions();
/**
- * Filters the list table bulk actions drop-down.
+ * Filters the items in the bulk actions menu of the list table.
*
* The dynamic portion of the hook name, `$this->screen->id`, refers
- * to the ID of the current screen, usually a string.
+ * to the ID of the current screen.
*
* @since 3.1.0
+ * @since 5.6.0 A bulk action can now contain an array of options in order to create an optgroup.
*
- * @param string[] $actions An array of the available bulk actions.
+ * @param array $actions An array of the available bulk actions.
*/
$this->_actions = apply_filters( "bulk_actions-{$this->screen->id}", $this->_actions ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
@@ -469,10 +487,21 @@ class WP_List_Table {
echo '\n";
diff --git a/src/wp-admin/includes/class-wp-privacy-requests-table.php b/src/wp-admin/includes/class-wp-privacy-requests-table.php
index c75edcf2c0..d2772bd60a 100644
--- a/src/wp-admin/includes/class-wp-privacy-requests-table.php
+++ b/src/wp-admin/includes/class-wp-privacy-requests-table.php
@@ -206,7 +206,7 @@ abstract class WP_Privacy_Requests_Table extends WP_List_Table {
*
* @since 4.9.6
*
- * @return string[] Array of bulk action labels keyed by their action.
+ * @return array Array of bulk action labels keyed by their action.
*/
protected function get_bulk_actions() {
return array(
diff --git a/src/wp-admin/includes/class-wp-users-list-table.php b/src/wp-admin/includes/class-wp-users-list-table.php
index ae52238844..42cf19f5f8 100644
--- a/src/wp-admin/includes/class-wp-users-list-table.php
+++ b/src/wp-admin/includes/class-wp-users-list-table.php
@@ -259,7 +259,7 @@ class WP_Users_List_Table extends WP_List_Table {
*
* @since 3.1.0
*
- * @return string[] Array of bulk action labels keyed by their action.
+ * @return array Array of bulk action labels keyed by their action.
*/
protected function get_bulk_actions() {
$actions = array();
diff --git a/tests/phpunit/tests/admin/includesListTable.php b/tests/phpunit/tests/admin/includesListTable.php
index a6b77af5b8..22e5045372 100644
--- a/tests/phpunit/tests/admin/includesListTable.php
+++ b/tests/phpunit/tests/admin/includesListTable.php
@@ -352,6 +352,42 @@ class Tests_Admin_includesListTable extends WP_UnitTestCase {
$this->assertNotContains( 'id="delete_all"', $output );
}
+ /**
+ * @ticket 19278
+ */
+ public function test_bulk_action_menu_supports_options_and_optgroups() {
+ $table = _get_list_table( 'WP_Comments_List_Table', array( 'screen' => 'edit-comments' ) );
+
+ add_filter(
+ 'bulk_actions-edit-comments',
+ function() {
+ return array(
+ 'delete' => 'Delete',
+ 'Change State' => array(
+ 'feature' => 'Featured',
+ 'sale' => 'On Sale',
+ ),
+ );
+ }
+ );
+
+ ob_start();
+ $table->bulk_actions();
+ $output = ob_get_clean();
+
+ $this->assertContains(
+ <<<'OPTIONS'
+
+
+OPTIONS
+ ,
+ $output
+ );
+ }
+
/**
* @ticket 45089
*/