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
22 changes: 8 additions & 14 deletions resources/ios/NativeUITextInputCore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,9 @@ struct NativeUITextInputCore: View {
/// chromeless `BasicTextField` doesn't have).
var supportsRevealToggle: Bool = false

/// Colour for the placeholder. Nil keeps SwiftUI's own placeholder style,
/// which is what every variant drew before. The outlined variant passes
/// its `on-input` token here, so a declared `on-input` recolours the
/// placeholder along with everything else inside the box, as it already
/// does on Android. Without it the placeholder stays system grey, which
/// all but disappears on a dark `input-fill`.
/// Optional placeholder override (for example, the outlined variant's
/// `on-input` token). Otherwise use a muted field text colour so a custom
/// surface stays readable even when it differs from the system appearance.
var placeholderColor: Color? = nil

@State private var text: String = ""
Expand Down Expand Up @@ -90,9 +87,8 @@ struct NativeUITextInputCore: View {
var body: some View {
let p = node.props
let placeholder = p.getString("placeholder")
// Nil unless the variant asked for a colour, and a nil prompt leaves the
// title as the placeholder, exactly as the prompt-less initializers do.
let prompt = placeholderColor.map { Text(placeholder).foregroundColor($0) }
let resolvedPlaceholderColor = placeholderColor ?? contentColor.opacity(0.6)
let prompt = Text(placeholder).foregroundColor(resolvedPlaceholderColor)
let serverValue = p.getString("value")
let secure = p.getBool("secure")
let multiline = p.getBool("multiline")
Expand Down Expand Up @@ -215,13 +211,11 @@ struct NativeUITextInputCore: View {
.focused($isFocused)
}
.overlay(alignment: .topLeading) {
// TextEditor has no placeholder slot. Same colour rule
// as the `prompt` the TextField branches take: the
// variant's colour when it passed one, else the system
// placeholder grey.
// TextEditor has no placeholder slot. Match the prompt
// used by the single-line and secure fields.
if text.isEmpty && !placeholder.isEmpty {
Text(placeholder)
.foregroundStyle(placeholderColor ?? Color(UIColor.placeholderText))
.foregroundStyle(resolvedPlaceholderColor)
.allowsHitTesting(false)
}
}
Expand Down
23 changes: 23 additions & 0 deletions tests/IosTextInputPlaceholderTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

it('uses the field color as the placeholder fallback while preserving explicit overrides', function () {
$source = file_get_contents(dirname(__DIR__).'/resources/ios/NativeUITextInputCore.swift');

expect($source)
->toContain('let resolvedPlaceholderColor = placeholderColor ?? contentColor.opacity(0.6)')
->toContain('Text(placeholder).foregroundColor(resolvedPlaceholderColor)')
->not->toContain('UIColor.placeholderText');
});

it('uses the resolved placeholder color for secure, revealed, selected, and multiline inputs', function () {
$source = file_get_contents(dirname(__DIR__).'/resources/ios/NativeUITextInputCore.swift');
preg_match_all('/(?:SecureField|TextField)\(placeholder, [^\n]+/', $source, $fields);

expect($fields[0])->toHaveCount(4);

foreach ($fields[0] as $field) {
expect($field)->toContain('prompt: prompt');
}

expect($source)->toMatch('/if text\.isEmpty && !placeholder\.isEmpty\s*\{\s*Text\(placeholder\)\s*\.foregroundStyle\(resolvedPlaceholderColor\)/');
});
Loading