diff --git a/.github/workflows/compound-ios.yml b/.github/workflows/compound-ios.yml index f063f1d0b..e940dcf3e 100644 --- a/.github/workflows/compound-ios.yml +++ b/.github/workflows/compound-ios.yml @@ -35,7 +35,7 @@ jobs: persist-credentials: false - name: Configure Xcode 26 - run: sudo xcode-select -s /Applications/Xcode_26.4.app + run: sudo xcode-select -s /Applications/Xcode_26.5.app - name: Install sourcery run: brew install sourcery @@ -45,11 +45,11 @@ jobs: run: sourcery --config Tools/Sourcery/PreviewTestsConfig.yml - name: Create simulator - run: xcrun simctl create "Snapshots" "iPhone SE (3rd generation)" "com.apple.CoreSimulator.SimRuntime.iOS-26-4" + run: xcrun simctl create "Snapshots" "iPhone SE (3rd generation)" "com.apple.CoreSimulator.SimRuntime.iOS-26-5" - name: Run tests working-directory: compound-ios - run: set -o pipefail && xcodebuild test -scheme 'Compound' -sdk iphonesimulator -destination 'platform=iOS Simulator,name=Snapshots,OS=26.4.1' -skipPackagePluginValidation -resultBundlePath Logs/CompoundTests.xcresult | xcbeautify --renderer github-actions + run: set -o pipefail && xcodebuild test -scheme 'Compound' -sdk iphonesimulator -destination 'platform=iOS Simulator,name=Snapshots,OS=26.5' -skipPackagePluginValidation -resultBundlePath Logs/CompoundTests.xcresult | xcbeautify --renderer github-actions - name: Zip artifacts if: failure() # We only care about artefacts if the tests fail diff --git a/compound-ios/Sources/Compound/BaseStyles/CompoundTextFieldStyles.swift b/compound-ios/Sources/Compound/BaseStyles/CompoundTextFieldStyles.swift index ed910e8e0..b6a402ef8 100644 --- a/compound-ios/Sources/Compound/BaseStyles/CompoundTextFieldStyles.swift +++ b/compound-ios/Sources/Compound/BaseStyles/CompoundTextFieldStyles.swift @@ -59,9 +59,49 @@ public extension TextFieldStyle where Self == CompoundTextFieldStyle { public struct CompoundTextFieldStyle: @MainActor TextFieldStyle { public enum Kind { /// The standard text field style for use on the default canvas. - case plain + public static var plain: Self { + .plain(.standard) + } + /// A style that raises the text field above the background (typically `bgSubtleSecondaryLevel0`). - case raised + public static var raised: Self { + .raised(.standard) + } + + /// The standard text field style for use on the default canvas. + case plain(TextEntry) + /// A style that raises the text field above the background (typically `bgSubtleSecondaryLevel0`). + case raised(TextEntry) + + var isRaised: Bool { + switch self { + case .plain: false + case .raised: true + } + } + + var textEntry: TextEntry { + switch self { + case .plain(let textEntry), + .raised(let textEntry): + textEntry + } + } + } + + public enum TextEntry { + /// The text entry is configured for a standard `TextField`. + case standard + /// The text entry is configured for a `SecureField`, using the supplied + /// accessibility labels on the "eye" button that reveals any entered text. + case secure(hideLabel: String, showLabel: String) + + var isSecure: Bool { + switch self { + case .standard: false + case .secure: true + } + } } public enum State { @@ -76,6 +116,10 @@ public struct CompoundTextFieldStyle: @MainActor TextFieldStyle { @Environment(\.isEnabled) private var isEnabled @FocusState private var isFocused: Bool + + @SwiftUI.State private var isSecureTextRevealed = false + @SwiftUI.State private var secureTextField: UITextField? + let kind: Kind let labelText: Text? let footerText: Text? @@ -89,7 +133,7 @@ public struct CompoundTextFieldStyle: @MainActor TextFieldStyle { /// The color of the text field's border. private var borderColor: Color { guard !isError else { return .compound.textCriticalPrimary } - return kind == .raised ? .compound.borderInteractiveSecondary : .clear + return kind.isRaised ? .compound.borderInteractiveSecondary : .clear } /// The width of the text field's border. @@ -108,7 +152,7 @@ public struct CompoundTextFieldStyle: @MainActor TextFieldStyle { /// The color of the text field's background. private var backgroundColor: Color { - if kind == .raised { + if kind.isRaised { isError ? .compound.bgCriticalSubtleHovered : .compound.bgCanvasDefaultLevel1 } else { @@ -162,26 +206,52 @@ public struct CompoundTextFieldStyle: @MainActor TextFieldStyle { .foregroundColor(labelColor) .padding(.horizontal, 16) - configuration - .focused($isFocused) - .font(.compound.bodyLG) - .foregroundColor(textColor) - .accentColor(accentColor) - .padding(.leading, 16.0) - .padding([.vertical, .trailing], 11.0) - .background { - ZStack { - shape.fill(backgroundColor) - shape.stroke(borderColor, lineWidth: borderWidth) + HStack(spacing: 0) { + configuration + .focused($isFocused) + .font(.compound.bodyLG) + .foregroundColor(textColor) + .accentColor(accentColor) + .introspect(.textField, on: .supportedVersions) { textField in + textField.clearButtonMode = kind.textEntry.isSecure ? .never : .whileEditing + textField.attributedPlaceholder = NSAttributedString(string: textField.placeholder ?? "", + attributes: [NSAttributedString.Key.foregroundColor: placeholderColor]) + textField.accessibilityIdentifier = accessibilityIdentifier + + if kind.textEntry.isSecure { + textField.isSecureTextEntry = !isSecureTextRevealed // SecureField can reset this on e.g. focus change. + + if secureTextField !== textField { + DispatchQueue.main.async { secureTextField = textField } + } + } } - .onTapGesture { isFocused = true } // Set focus with taps outside of the text field + + switch kind.textEntry { + case .secure(let hideLabel, let showLabel): + Button { + isSecureTextRevealed.toggle() + secureTextField?.isSecureTextEntry = !isSecureTextRevealed + } label: { + CompoundIcon(isSecureTextRevealed ? \.visibilityOff : \.visibilityOn, + size: .medium, + relativeTo: .compound.bodyLG) + .foregroundStyle(.compound.iconSecondary) + } + .accessibilityLabel(isSecureTextRevealed ? hideLabel : showLabel) + case .standard: + EmptyView() } - .introspect(.textField, on: .supportedVersions) { textField in - textField.clearButtonMode = .whileEditing - textField.attributedPlaceholder = NSAttributedString(string: textField.placeholder ?? "", - attributes: [NSAttributedString.Key.foregroundColor: placeholderColor]) - textField.accessibilityIdentifier = accessibilityIdentifier + } + .padding(.leading, 16.0) + .padding([.vertical, .trailing], 11.0) + .background { + ZStack { + shape.fill(backgroundColor) + shape.stroke(borderColor, lineWidth: borderWidth) } + .onTapGesture { isFocused = true } // Set focus with taps outside of the text field + } if let footerText { Label { @@ -222,7 +292,7 @@ public struct CompoundTextFieldStyle_Previews: PreviewProvider, TestablePreview ScrollView { states } - .previewLayout(.fixed(width: 390, height: 1250)) + .previewLayout(.fixed(width: 390, height: 1500)) } @ViewBuilder @@ -249,9 +319,15 @@ public struct CompoundTextFieldStyle_Previews: PreviewProvider, TestablePreview } } + @ViewBuilder static func textFields(_ kind: CompoundTextFieldStyle.Kind = .plain, labelText: String? = nil, footerText: String? = nil) -> some View { + let secureKind: CompoundTextFieldStyle.Kind = switch kind { + case .plain: .plain(.secure(hideLabel: "Hide", showLabel: "Show")) + case .raised: .raised(.secure(hideLabel: "Hide", showLabel: "Show")) + } + VStack(spacing: 20) { TextField("Placeholder", text: .constant("")) .textFieldStyle(.compound(kind, labelText: labelText, footerText: footerText)) @@ -259,6 +335,9 @@ public struct CompoundTextFieldStyle_Previews: PreviewProvider, TestablePreview TextField("Placeholder", text: .constant("Text")) .textFieldStyle(.compound(kind, labelText: labelText, footerText: footerText)) + SecureField("Placeholder", text: .constant("p4ssw0rd")) + .textFieldStyle(.compound(secureKind, labelText: labelText, footerText: footerText)) + TextField("Placeholder", text: .constant("Disabled")) .textFieldStyle(.compound(kind, labelText: labelText, footerText: footerText)) .disabled(true) @@ -267,7 +346,7 @@ public struct CompoundTextFieldStyle_Previews: PreviewProvider, TestablePreview .textFieldStyle(.compound(kind, labelText: labelText, footerText: footerText, state: .error)) } .padding() - .background(kind == .raised ? .compound.bgSubtleSecondaryLevel0 : .clear) + .background(kind.isRaised ? .compound.bgSubtleSecondaryLevel0 : .clear) } struct Header: View { diff --git a/compound-ios/Tests/CompoundTests/PreviewTests.swift b/compound-ios/Tests/CompoundTests/PreviewTests.swift index 5d92f15c0..c0c7be1f7 100644 --- a/compound-ios/Tests/CompoundTests/PreviewTests.swift +++ b/compound-ios/Tests/CompoundTests/PreviewTests.swift @@ -20,7 +20,7 @@ struct PreviewTests { private let deviceConfig: ViewImageConfig = .iPhoneX private let simulatorDevice: String? = "iPhone14,6" // iPhone SE 3rd Generation - private let requiredOSVersion = (major: 26, minor: 4) + private let requiredOSVersion = (major: 26, minor: 5) /// The key is the name we will give to the snapshot /// The value is the actual device that will be used to render the preview private let snapshotDevices: [SnapshotDevice] = [.init(name: "iPhone", device: "iPhone 17"), diff --git a/compound-ios/Tests/CompoundTests/__Snapshots__/PreviewTests/compoundTextFieldStyle.iPad-en-US-0.png b/compound-ios/Tests/CompoundTests/__Snapshots__/PreviewTests/compoundTextFieldStyle.iPad-en-US-0.png index 1d5fed85a..a66773d1d 100644 --- a/compound-ios/Tests/CompoundTests/__Snapshots__/PreviewTests/compoundTextFieldStyle.iPad-en-US-0.png +++ b/compound-ios/Tests/CompoundTests/__Snapshots__/PreviewTests/compoundTextFieldStyle.iPad-en-US-0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a0e8869897ac8ba0ee0595d2f058868d12231a7c47888304fa740525d23e2ab7 -size 198222 +oid sha256:5b50af9a6a0b85eff9a5b0806d431b76bf6ff9def2d20ac32e63af1265f87bf6 +size 234892 diff --git a/compound-ios/Tests/CompoundTests/__Snapshots__/PreviewTests/compoundTextFieldStyle.iPhone-en-US-0.png b/compound-ios/Tests/CompoundTests/__Snapshots__/PreviewTests/compoundTextFieldStyle.iPhone-en-US-0.png index 6d1992895..e90611117 100644 --- a/compound-ios/Tests/CompoundTests/__Snapshots__/PreviewTests/compoundTextFieldStyle.iPhone-en-US-0.png +++ b/compound-ios/Tests/CompoundTests/__Snapshots__/PreviewTests/compoundTextFieldStyle.iPhone-en-US-0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b20272919b574830b6941fa1153a8487a0115da11a21e32d422522215b60f37b -size 122608 +oid sha256:2f3f599dbbdaf73b51276cd5409fbb56c952f6627820d1324ce61cbeefb8b51a +size 144539