Skip to content

Implement textContentType modifier #129

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
16 changes: 16 additions & 0 deletions Sources/AppKitBackend/AppKitBackend.swift
Original file line number Diff line number Diff line change
Expand Up @@ -641,6 +641,22 @@ public final class AppKitBackend: AppBackend {
onChange(textField.stringValue)
}
textField.onSubmit = onSubmit

if #available(macOS 14, *) {
textField.contentType =
switch environment.textContentType {
case .url:
.URL
case .phoneNumber:
.telephoneNumber
case .name:
.name
case .emailAddress:
.emailAddress
default:
nil
}
}
}

public func getContent(ofTextField textField: Widget) -> String {
Expand Down
4 changes: 4 additions & 0 deletions Sources/SwiftCrossUI/Environment/EnvironmentValues.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ public struct EnvironmentValues {
/// The scale factor of the current window.
public var windowScaleFactor: Double

/// The type of on-screen keyboard to show when a text field is focused.
public var textContentType: TextContentType

/// Called by view graph nodes when they resize due to an internal state
/// change and end up changing size. Each view graph node sets its own
/// handler when passing the environment on to its children, setting up
Expand Down Expand Up @@ -143,6 +146,7 @@ public struct EnvironmentValues {
multilineTextAlignment = .leading
colorScheme = .light
windowScaleFactor = 1
textContentType = .text
window = nil
extraValues = [:]
}
Expand Down
11 changes: 11 additions & 0 deletions Sources/SwiftCrossUI/Modifiers/TextContentTypeModifier.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
extension View {
/// Set the content type of text fields.
///
/// This controls autocomplete suggestions, and on mobile devices, which on-screen keyboard
/// is shown.
public func textContentType(_ type: TextContentType) -> some View {
EnvironmentModifier(self) { environment in
environment.with(\.textContentType, type)
}
}
}
38 changes: 38 additions & 0 deletions Sources/SwiftCrossUI/TextContentType.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
public enum TextContentType {
/// Plain text.
///
/// This is the default value.
case text
/// Just digits.
///
/// For numbers that may include decimals or negative numbers, see ``decimal(signed:)``.
///
/// If `ascii` is true, the user should only enter the ASCII digits 0-9. If `ascii` is
/// false, on mobile devices they may see a different numeric keypad depending on their
/// locale settings (for example, they may see the digits ० १ २ ३ ४ ५ ६ ७ ८ ९ instead
/// if the language is set to Hindi).
case digits(ascii: Bool)
/// A URL.
///
/// On mobile devices, this type shows a keyboard with prominent buttons for "/" and ".com",
/// and might not include a spacebar.
case url
/// A phone number.
case phoneNumber
/// A person's name.
///
/// This typically uses the default keyboard, but informs autocomplete to use contact
/// names rather than regular words.
case name
/// A number.
///
/// If `signed` is false, on mobile devices it shows a numeric keypad with a decimal point,
/// but not necessarily plus and minus signs. If `signed` is true then more punctuation can
/// be entered.
case decimal(signed: Bool)
/// An email address.
///
/// This informs autocomplete that the input is an email address, and on mobile devices,
/// displays a keyboard with prominent "@" and "." buttons.
case emailAddress
}
30 changes: 30 additions & 0 deletions Sources/UIKitBackend/UIKitBackend+Control.swift
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,36 @@ extension UIKitBackend {
textFieldWidget.child.textColor = UIColor(color: environment.suggestedForegroundColor)
textFieldWidget.onChange = onChange
textFieldWidget.onSubmit = onSubmit

switch environment.textContentType {
case .text:
textFieldWidget.child.keyboardType = .default
textFieldWidget.child.textContentType = nil
case .digits(ascii: false):
textFieldWidget.child.keyboardType = .numberPad
textFieldWidget.child.textContentType = nil
case .digits(ascii: true):
textFieldWidget.child.keyboardType = .asciiCapableNumberPad
textFieldWidget.child.textContentType = nil
case .url:
textFieldWidget.child.keyboardType = .URL
textFieldWidget.child.textContentType = .URL
case .phoneNumber:
textFieldWidget.child.keyboardType = .phonePad
textFieldWidget.child.textContentType = .telephoneNumber
case .name:
textFieldWidget.child.keyboardType = .namePhonePad
textFieldWidget.child.textContentType = .name
case .decimal(signed: false):
textFieldWidget.child.keyboardType = .decimalPad
textFieldWidget.child.textContentType = nil
case .decimal(signed: true):
textFieldWidget.child.keyboardType = .numbersAndPunctuation
textFieldWidget.child.textContentType = nil
case .emailAddress:
textFieldWidget.child.keyboardType = .emailAddress
textFieldWidget.child.textContentType = .emailAddress
}

#if os(iOS)
if let updateToolbar = environment.updateToolbar {
Expand Down
32 changes: 32 additions & 0 deletions Sources/WinUIBackend/WinUIBackend.swift
Original file line number Diff line number Diff line change
Expand Up @@ -801,6 +801,38 @@ public final class WinUIBackend: AppBackend {
}

missing("text field font handling")

let inputScope: InputScopeNameValue =
switch environment.textContentType {
case .decimal(_):
.number
case .digits(_):
.digits
case .emailAddress:
.emailSmtpAddress
case .name:
.personalFullName
case .phoneNumber:
.telephoneNumber
case .text:
.default
case .url:
.url
}

setInputScope(for: textField, to: inputScope)
}

private func setInputScope(for textField: TextBox, to value: InputScopeNameValue) {
if let inputScope = textField.inputScope,
inputScope.names.count == 1
{
inputScope.names[0] = value
} else {
let inputScope = InputScope()
inputScope.names.add(value)
textField.inputScope = inputScope
}
}

public func setContent(ofTextField textField: Widget, to content: String) {
Expand Down
Loading