diff --git a/php-transformer/composer.json b/php-transformer/composer.json index 6295acf0e..2268825f1 100644 --- a/php-transformer/composer.json +++ b/php-transformer/composer.json @@ -161,6 +161,7 @@ "php tests/unit/conditional-native-button.php", "php tests/unit/form-physical-box-ownership.php", "php tests/unit/form-offscreen-submit-presenter.php", + "php tests/unit/form-submit-caption-box.php", "php tests/unit/runtime-declarations-streaming-hash.php", "php tests/unit/external-metric-declarations.php", "php tests/unit/runtime-script-evidence-analyzer.php", diff --git a/php-transformer/src/HtmlToBlocks/Style/FormPresentationGraphBuilder.php b/php-transformer/src/HtmlToBlocks/Style/FormPresentationGraphBuilder.php index 83e07aa18..1bfcc6bf1 100644 --- a/php-transformer/src/HtmlToBlocks/Style/FormPresentationGraphBuilder.php +++ b/php-transformer/src/HtmlToBlocks/Style/FormPresentationGraphBuilder.php @@ -36,6 +36,15 @@ final class FormPresentationGraphBuilder private const TYPOGRAPHY_PROPERTIES = array( 'color', 'font', 'font-family', 'font-size', 'font-weight', 'letter-spacing', 'line-height', ); + /** A button caption carrier also sets the caption's case. */ + private const CAPTION_PROPERTIES = array( + 'color', 'font', 'font-family', 'font-size', 'font-weight', 'letter-spacing', 'line-height', 'text-transform', + ); + /** A block-level caption carrier fills its button, so its padding sizes the button box. */ + private const CAPTION_BOX_PROPERTIES = array( + 'padding', 'padding-top', 'padding-right', 'padding-bottom', 'padding-left', + 'padding-block', 'padding-inline', 'padding-block-start', 'padding-block-end', 'padding-inline-start', 'padding-inline-end', + ); /** How far a text-carrier search may walk before the search, not the cascade, gives up. */ private const MAX_CARRIER_CANDIDATES = 256; private const CONTROL_CONTAINER_PROPERTIES = array( @@ -497,7 +506,7 @@ function (array $selector) use ($controlsForCustomProperties): bool { // or `` inside it). Typography that carrier declares wins // over the label's own for that text, property by property, so a // provider label that renders the text directly keeps it. - $carried = $this->carrierTypography($buttonCaption ? array() : $roles, $element, $analysis['rules'], $customPropertyAnalysis['rules'], $conditional); + $carried = $this->carrierTypography($buttonCaption ? array() : $roles, $element, $analysis['rules'], $customPropertyAnalysis['rules'], $conditional, $buttonCaption && ! $this->hasPadding($styles)); if ( array() !== $carried ) { $overridden = array_map(static fn (string $key): string => str_replace('_', '-', $key), array_keys(array_intersect_key($styles, $carried['styles']))); foreach ( $provenance as $factIndex => $fact ) { @@ -698,8 +707,9 @@ private static function assertRole(mixed $role, ?array $condition, bool $allowEm * @param list> $customPropertyRules * @return array */ - private function carrierTypography(array $roles, DOMElement $label, array $rules, array $customPropertyRules, array $labelConditional = array()): array + private function carrierTypography(array $roles, DOMElement $label, array $rules, array $customPropertyRules, array $labelConditional = array(), bool $captionBox = false): array { + $properties = array_flip(array() === $roles ? self::CAPTION_PROPERTIES : self::TYPOGRAPHY_PROPERTIES); $excluded = array_values(array_filter( array( $roles['control'] ?? null, $roles['required_marker'] ?? null ), static fn (mixed $element): bool => $element instanceof DOMElement @@ -709,13 +719,19 @@ private function carrierTypography(array $roles, DOMElement $label, array $rules $conditions = array_fill_keys(array_keys($labelConditional), true); for ( $element = $carrier; $element instanceof DOMElement && ! $element->isSameNode($label); $element = $element->parentNode instanceof DOMElement ? $element->parentNode : null ) { $matched = $this->matched($element, $rules); - $base = array_intersect_key($matched['base'], array_flip(self::TYPOGRAPHY_PROPERTIES)); + $carried = $properties; + // The caption carrier's own padding sizes an unpadded button only + // when the carrier is block-level; inline padding leaves the line box. + if ( $captionBox && $element->isSameNode($carrier) && self::isBlockLevelDisplay((string) ($matched['base']['display']['value'] ?? '')) ) { + $carried += array_flip(self::CAPTION_BOX_PROPERTIES); + } + $base = array_intersect_key($matched['base'], $carried); $conditional = $this->effectiveConditional($matched['conditional'], $matched['base']); foreach ($base as $property => $fact) { foreach (FormCustomPropertyResolver::conditionsChanging($fact['value'], $element, $customPropertyRules, true) as $condition) $conditional[json_encode($condition)][$property] ??= $fact; } foreach ( $conditional as $encoded => &$facts ) { - $facts = array_intersect_key($facts, array_flip(self::TYPOGRAPHY_PROPERTIES)); + $facts = array_intersect_key($facts, $carried); if ( array() !== $facts ) $conditions[$encoded] = true; } unset($facts); @@ -759,6 +775,20 @@ private function carrierTypography(array $roles, DOMElement $label, array $rules * @param list> $rules * @param list> $customPropertyRules */ + private static function isBlockLevelDisplay(string $display): bool + { + return in_array(CssValueInspector::comparable($display), array( 'block', 'inline-block', 'flex', 'inline-flex', 'grid', 'inline-grid', 'flow-root' ), true); + } + + /** @param array $styles */ + private function hasPadding(array $styles): bool + { + foreach ( $styles as $key => $value ) { + if ( str_starts_with($key, 'padding') && 1 !== preg_match('/^(?:0(?:\.0+)?(?:px|em|rem|%)?\s*)+$/', trim($value)) ) return true; + } + return false; + } + private function carryFieldGroupGapOntoLabel(array &$row, DOMElement $control, array $ancestors, array $rules, array $customPropertyRules): void { if ( isset($row['label']['styles']['margin_block_end']) || isset($row['label']['styles']['margin_bottom']) ) { diff --git a/php-transformer/tests/unit/form-submit-caption-box.php b/php-transformer/tests/unit/form-submit-caption-box.php new file mode 100644 index 000000000..53bf76cf0 --- /dev/null +++ b/php-transformer/tests/unit/form-submit-caption-box.php @@ -0,0 +1,53 @@ +` around a padded block + * `` is as tall as the span. The presentation graph carries the + * carrier's padding and text case onto the button so a provider button, + * which renders the caption directly, keeps the source box and case. + */ + +require dirname(__DIR__, 2) . '/vendor/autoload.php'; + +use Automattic\BlocksEngine\PhpTransformer\HtmlToBlocks\Style\FormPresentationGraphBuilder; + +$failures = 0; +$passes = 0; +$assert = static function (bool $ok, string $message, string $detail = '') use (&$failures, &$passes): void { + if ( $ok ) { + ++$passes; + return; + } + ++$failures; + fwrite(STDERR, 'FAIL: ' . $message . ( '' !== $detail ? ' - ' . $detail : '' ) . PHP_EOL); +}; + +$submit = static function (string $css): array { + $document = new DOMDocument(); + $document->loadHTML('
', LIBXML_NOERROR | LIBXML_NOWARNING); + $graph = (new FormPresentationGraphBuilder())->build($document->getElementsByTagName('form')->item(0), array(), $css); + return array( 'styles' => array_column($graph['controls'], null, 'index')[1]['control']['styles'] ?? array(), 'variants' => $graph['variants'] ?? array() ); +}; + +$block = $submit('.btn { padding: 0; background-color: #f5a623; } .btn .inner { display: block; padding: .625em 1.9375em; text-transform: uppercase; font-weight: 700; }'); +$assert('.625em 1.9375em' === ($block['styles']['padding'] ?? null), 'a block carrier pads the unpadded button', json_encode($block['styles'])); +$assert('uppercase' === ($block['styles']['text_transform'] ?? null) && '700' === ($block['styles']['font_weight'] ?? null), 'the carrier sets the caption case and weight', json_encode($block['styles'])); + +$inline = $submit('.btn { padding: 0; } .btn .inner { padding: 4px 8px; text-transform: uppercase; }'); +$assert('0' === ($inline['styles']['padding'] ?? null), 'an inline carrier leaves the button box alone', json_encode($inline['styles'])); +$assert('uppercase' === ($inline['styles']['text_transform'] ?? null), 'an inline carrier still sets the caption case', json_encode($inline['styles'])); + +$padded = $submit('.btn { padding: 10px 20px; } .btn .inner { display: block; padding: 4px; }'); +$assert('10px 20px' === ($padded['styles']['padding'] ?? null), 'a padded button keeps its own box', json_encode($padded['styles'])); + +$responsive = $submit('.btn { padding: 0; } .btn .inner { display: block; padding: 4px; } @media (min-width: 600px) { .btn .inner { padding: 12px; } }'); +$patches = array_map(static fn (array $variant): array => $variant['style_patch'] ?? array(), array_filter($responsive['variants'], static fn (array $variant): bool => 1 === ($variant['index'] ?? null) && 'control' === ($variant['role'] ?? null))); +$assert('4px' === ($responsive['styles']['padding'] ?? null) && in_array('12px', array_column($patches, 'padding'), true), 'a conditional carrier padding becomes a variant', json_encode(array( $responsive['styles'], $patches ))); + +if ( 0 < $failures ) { + fwrite(STDERR, "form submit caption box FAILED: {$passes} passed, {$failures} failed\n"); + exit(1); +} +echo "form submit caption box passed: {$passes} assertions\n";