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'] ?? ''), '", + "options": { + "runtime_dom_selectors": [".js-sort-select"] + } + }, + "expected_blocks": [ + { "path": "blocks.0", "name": "core/group" }, + { "path": "blocks.0.innerBlocks.0", "name": "core/paragraph" }, + { "path": "blocks.0.innerBlocks.1", "name": "core/group" }, + { "path": "blocks.0.innerBlocks.1.innerBlocks.1", "name": "core/list" }, + { "path": "blocks.0.innerBlocks.2", "name": "core/html" } + ], + "expected_fallbacks": [], + "expect": [ + { "path": "status", "assert": "equals", "value": "success" }, + { "path": "fallbacks", "assert": "count", "count": 0 }, + { "path": "serialized_blocks", "assert": "contains", "value": "Custom donation amount: Enter amount" }, + { "path": "serialized_blocks", "assert": "contains", "value": "Sort products" }, + { "path": "serialized_blocks", "assert": "contains", "value": "Featured (selected)" }, + { "path": "serialized_blocks", "assert": "contains", "value": "Price: Low to High" }, + { "path": "serialized_blocks", "assert": "contains", "value": "