diff --git a/php-transformer/src/HtmlToBlocks/HtmlTransformer.php b/php-transformer/src/HtmlToBlocks/HtmlTransformer.php
index a1139bbf..22324f09 100644
--- a/php-transformer/src/HtmlToBlocks/HtmlTransformer.php
+++ b/php-transformer/src/HtmlToBlocks/HtmlTransformer.php
@@ -3514,10 +3514,9 @@ private function readableFormBlockFromForm(DOMElement $form, bool $allowFormEven
continue;
}
- $summary = $this->readableFormControlText($control);
- if ( '' !== $summary ) {
- $attrs = $this->isRuntimeDomTarget($control) ? $this->presentationAttributes($control) : array();
- $contentBlocks[] = $this->createBlock('core/paragraph', array_merge($attrs, array( 'content' => $summary )), array(), $control);
+ $readableControlBlock = $this->readableFormControlBlockFromElement($control);
+ if ( null !== $readableControlBlock ) {
+ $contentBlocks[] = $readableControlBlock;
}
}
@@ -3597,6 +3596,13 @@ private function readableFormControlBlockFromElement(DOMElement $element): ?arra
return $this->createBlock('core/html', array( 'content' => $this->safeFallbackHtml($element) ), array(), $element);
}
+ if ( 'select' === $tagName ) {
+ $selectBlock = $this->readableSelectBlockFromElement($element);
+ if ( null !== $selectBlock ) {
+ return $selectBlock;
+ }
+ }
+
$summary = $this->readableFormControlText($element);
if ( '' === $summary ) {
return null;
@@ -3605,6 +3611,37 @@ private function readableFormControlBlockFromElement(DOMElement $element): ?arra
return $this->createBlock('core/paragraph', array( 'content' => $summary ), array(), $element);
}
+ /**
+ * @return array|null
+ */
+ private function readableSelectBlockFromElement(DOMElement $select): ?array
+ {
+ $label = $this->readableFormControlLabel($select);
+ $optionBlocks = array();
+
+ foreach ( $this->selectOptions($select) as $option ) {
+ $optionLabel = trim((string) ($option['label'] ?? ''));
+ if ( '' === $optionLabel ) {
+ continue;
+ }
+
+ if ( true === ($option['selected'] ?? false) ) {
+ $optionLabel .= ' (selected)';
+ }
+
+ $optionBlocks[] = $this->createBlock('core/list-item', array( 'content' => $this->runtime->escapeHtml($optionLabel) ));
+ }
+
+ if ( array() === $optionBlocks ) {
+ return null;
+ }
+
+ return $this->createBlock('core/group', array(), array(
+ $this->createBlock('core/paragraph', array( 'content' => $this->runtime->escapeHtml($label) ), array(), $select),
+ $this->createBlock('core/list', array(), $optionBlocks, $select),
+ ), $select);
+ }
+
/**
* @return array
*/
@@ -3638,16 +3675,7 @@ private function isReadableFormControl(DOMElement $control): bool
private function readableFormControlText(DOMElement $control): string
{
- $label = $this->formControlLabel($control);
- if ( '' === $label ) {
- $label = $this->attr($control, 'aria-label');
- }
- if ( '' === $label ) {
- $label = $this->attr($control, 'placeholder');
- }
- if ( '' === $label ) {
- $label = $this->attr($control, 'name');
- }
+ $label = $this->readableFormControlLabel($control);
$type = $this->formControlType($control);
if ( '' === $label ) {
@@ -3711,6 +3739,27 @@ private function readableFormControlText(DOMElement $control): string
return $this->runtime->escapeHtml($text);
}
+ private function readableFormControlLabel(DOMElement $control): string
+ {
+ $label = $this->formControlLabel($control);
+ if ( '' === $label ) {
+ $label = $this->attr($control, 'aria-label');
+ }
+ if ( '' === $label ) {
+ $label = $this->attr($control, 'placeholder');
+ }
+ if ( '' === $label ) {
+ $label = $this->attr($control, 'name');
+ }
+
+ $type = $this->formControlType($control);
+ if ( '' === $label ) {
+ return 'select' === $type ? 'Select option' : ucfirst($type);
+ }
+
+ return $label;
+ }
+
private function readableSubmitText(DOMElement $control): string
{
$text = trim(preg_replace('/\s+/', ' ', $control->textContent ?? '') ?? '');
diff --git a/php-transformer/tests/contract/run.php b/php-transformer/tests/contract/run.php
index ca693efb..4bca7c5b 100644
--- a/php-transformer/tests/contract/run.php
+++ b/php-transformer/tests/contract/run.php
@@ -263,6 +263,18 @@ function serialize_blocks(array $blocks): string
$assert(str_contains($rangeControlText, 'Density: 28'), 'range input summary preserves current value');
$assert(str_contains($rangeControlText, 'min 6, max 60, step 2'), 'range input summary preserves bounds');
+$standaloneControls = ( new HtmlTransformer() )->transform(
+ '',
+ array('runtime_dom_selectors' => array('.js-sort-select'))
+)->toArray();
+$standaloneControlBlocks = $standaloneControls['blocks'][0]['innerBlocks'] ?? array();
+$assert(array() === ($standaloneControls['fallbacks'] ?? array()), 'standalone readable controls convert without unsupported-element fallback');
+$assert('core/paragraph' === ($standaloneControlBlocks[0]['blockName'] ?? ''), 'standalone non-runtime input converts to readable paragraph');
+$assert('core/list' === ($standaloneControlBlocks[1]['innerBlocks'][1]['blockName'] ?? ''), 'standalone non-runtime select options convert to readable list');
+$assert('core/html' === ($standaloneControlBlocks[2]['blockName'] ?? ''), 'runtime-targeted select preserves native DOM markup');
+$assert(str_contains((string) ($standaloneControls['serialized_blocks'] ?? ''), 'Featured (selected)'), 'readable select summary preserves selected option state');
+$assert(str_contains((string) ($standaloneControls['serialized_blocks'] ?? ''), '