Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions php-transformer/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -497,7 +506,7 @@ function (array $selector) use ($controlsForCustomProperties): bool {
// or `<span>` 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 ) {
Expand Down Expand Up @@ -698,8 +707,9 @@ private static function assertRole(mixed $role, ?array $condition, bool $allowEm
* @param list<array<string, mixed>> $customPropertyRules
* @return array<string, mixed>
*/
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
Expand All @@ -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);
Expand Down Expand Up @@ -759,6 +775,20 @@ private function carrierTypography(array $roles, DOMElement $label, array $rules
* @param list<array<string, mixed>> $rules
* @param list<array<string, mixed>> $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<string, string> $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']) ) {
Expand Down
53 changes: 53 additions & 0 deletions php-transformer/tests/unit/form-submit-caption-box.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);

/**
* A submit whose caption is painted by a block-level inner carrier takes its
* box from that carrier: an unpadded `<button>` around a padded block
* `<span>` 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('<?xml encoding="utf-8" ?><body><form class="f" method="post"><input type="text" name="name"><button type="submit" class="btn"><span class="inner">Send</span></button></form></body>', 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";
Loading