From 4a0118431231259aae3049ae00619e4ff7846b80 Mon Sep 17 00:00:00 2001 From: Simon Hamp Date: Sun, 27 Sep 2026 18:38:19 +0100 Subject: [PATCH] Add headline-line-through to list items A done todo usually shows a struck-through title, and the list row had no way to do it, so people rebuilt rows by hand. The new headline-line-through attribute (fluent headlineLineThrough()) follows the line-through naming used by native:text and draws a strikethrough on the headline on iOS and Android. Co-Authored-By: Claude Opus 5.5 (1M context) --- resources/android/ListItemRenderer.kt | 5 +- resources/ios/NativeUIListItemRenderer.swift | 4 +- src/Elements/ListItem.php | 16 +++++++ tests/ListItemLineThroughTest.php | 49 ++++++++++++++++++++ 4 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 tests/ListItemLineThroughTest.php diff --git a/resources/android/ListItemRenderer.kt b/resources/android/ListItemRenderer.kt index 29dc703..d98a52d 100644 --- a/resources/android/ListItemRenderer.kt +++ b/resources/android/ListItemRenderer.kt @@ -31,6 +31,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import coil3.compose.SubcomposeAsyncImage @@ -49,6 +50,7 @@ object ListItemRenderer { // Colors val headlineColor = p.getColor("headline_color", 0) + val headlineLineThrough = p.getBool("headline_line_through") val supportingColor = p.getColor("supporting_color", 0) val overlineColor = p.getColor("overline_color", 0) val containerColor = p.getColor("container_color", 0) @@ -117,7 +119,8 @@ object ListItemRenderer { Text( text = headline, fontFamily = nuiDefaultFontFamily(), - color = if (headlineColor != 0) Color(headlineColor) else Color.Unspecified + color = if (headlineColor != 0) Color(headlineColor) else Color.Unspecified, + textDecoration = if (headlineLineThrough) TextDecoration.LineThrough else null ) }, modifier = clickModifier, diff --git a/resources/ios/NativeUIListItemRenderer.swift b/resources/ios/NativeUIListItemRenderer.swift index f89ba7d..15c657b 100644 --- a/resources/ios/NativeUIListItemRenderer.swift +++ b/resources/ios/NativeUIListItemRenderer.swift @@ -39,6 +39,7 @@ struct NativeUIListItemRenderer: View { // Colors let headlineColor = p.getColor("headline_color", default: 0) + let headlineLineThrough = p.getBool("headline_line_through") let supportingColor = p.getColor("supporting_color", default: 0) let overlineColor = p.getColor("overline_color", default: 0) let containerColor = p.getColor("container_color", default: 0) @@ -79,7 +80,8 @@ struct NativeUIListItemRenderer: View { .nuiScaledFont(size: 12) .foregroundColor(overlineColor != 0 ? Color(argb: overlineColor) : .secondary) } - Text(headline) + // Text-level strikethrough (iOS 13+), same as NativeUITextRenderer. + (headlineLineThrough ? Text(headline).strikethrough() : Text(headline)) .nuiScaledFont(size: 17) .foregroundColor(headlineColor != 0 ? Color(argb: headlineColor) : .primary) if !supporting.isEmpty { diff --git a/src/Elements/ListItem.php b/src/Elements/ListItem.php index e4bb74d..d22bd9e 100644 --- a/src/Elements/ListItem.php +++ b/src/Elements/ListItem.php @@ -154,6 +154,12 @@ public function applyAttributes(array $attrs): void if (isset($attrs['headlineColor'])) { $this->headlineColor($attrs['headlineColor']); } + // Struck-through headline, e.g. a completed todo. Same name as the + // `line-through` text decoration on ``. + $lineThrough = $attrs['headline-line-through'] ?? $attrs['headlineLineThrough'] ?? null; + if ($lineThrough !== null) { + $this->headlineLineThrough(filter_var($lineThrough, FILTER_VALIDATE_BOOLEAN)); + } if (isset($attrs['supportingColor'])) { $this->supportingColor($attrs['supportingColor']); } @@ -470,6 +476,16 @@ public function headlineColor(string $color): static return $this; } + /** + * Draw a line through the headline, e.g. to mark a todo as done. + */ + public function headlineLineThrough(bool $lineThrough = true): static + { + $this->listItemProps['headline_line_through'] = $lineThrough; + + return $this; + } + public function supportingColor(string $color): static { $this->listItemProps['supporting_color'] = $this->resolveColorValue($color); diff --git a/tests/ListItemLineThroughTest.php b/tests/ListItemLineThroughTest.php new file mode 100644 index 0000000..3e0b48c --- /dev/null +++ b/tests/ListItemLineThroughTest.php @@ -0,0 +1,49 @@ + 'Buy milk'] + $attrs); + + return NativeElementCollector::collect()->toArray(new CallbackRegistry)['props'] ?? []; +} + +it('leaves the headline undecorated by default', function () { + expect(lineThroughProps([]))->not->toHaveKey('headline_line_through'); +}); + +it('strikes through the headline from either Blade spelling', function (string $attr) { + expect(lineThroughProps([$attr => true])['headline_line_through'])->toBeTrue(); +})->with(['headline-line-through', 'headlineLineThrough']); + +it('follows a bound value so done and not-done rows share one tag', function (mixed $value, bool $expected) { + expect(lineThroughProps(['headline-line-through' => $value])['headline_line_through'])->toBe($expected); +})->with([ + [true, true], + [false, false], + ['true', true], + ['false', false], + [1, true], + [0, false], +]); + +it('exposes a fluent builder', function () { + $props = ListItem::make('Buy milk')->headlineLineThrough()->toArray(new CallbackRegistry)['props']; + + expect($props['headline_line_through'])->toBeTrue(); +});