diff --git a/RIADigiDoc/UI/Component/Container/Signing/IdCard/IdCardActionView.swift b/RIADigiDoc/UI/Component/Container/Signing/IdCard/IdCardActionView.swift index 70c8f91d..fd596053 100644 --- a/RIADigiDoc/UI/Component/Container/Signing/IdCard/IdCardActionView.swift +++ b/RIADigiDoc/UI/Component/Container/Signing/IdCard/IdCardActionView.swift @@ -64,6 +64,7 @@ struct IdCardActionView: View { isIdCardActionMessageFocused = true } } + .accessibilityElement(children: .combine) } } diff --git a/RIADigiDoc/UI/Component/Container/Signing/IdCard/IdCardInputView.swift b/RIADigiDoc/UI/Component/Container/Signing/IdCard/IdCardInputView.swift index 39a73877..0c7b19c6 100644 --- a/RIADigiDoc/UI/Component/Container/Signing/IdCard/IdCardInputView.swift +++ b/RIADigiDoc/UI/Component/Container/Signing/IdCard/IdCardInputView.swift @@ -28,6 +28,8 @@ struct IdCardInputView: View { @AppTheme private var theme @AppTypography private var typography + @AccessibilityFocusState private var isActionMessageFocused: Bool + let personIdentifier: String @Binding var pinNumber: String @Binding var pinError: String @@ -73,6 +75,7 @@ struct IdCardInputView: View { Text(verbatim: actionMessage) .font(typography.labelLarge) .foregroundStyle(theme.onSurfaceVariant) + .accessibilityFocused($isActionMessageFocused) Text(verbatim: personIdentifier) .font(typography.bodyLarge) @@ -97,6 +100,11 @@ struct IdCardInputView: View { .foregroundStyle(theme.error) } } + .onAppear { + DispatchQueue.main.async { + isActionMessageFocused = true + } + } } } diff --git a/RIADigiDoc/UI/Component/Container/Signing/IdCard/IdCardView.swift b/RIADigiDoc/UI/Component/Container/Signing/IdCard/IdCardView.swift index cea163c2..06b75038 100644 --- a/RIADigiDoc/UI/Component/Container/Signing/IdCard/IdCardView.swift +++ b/RIADigiDoc/UI/Component/Container/Signing/IdCard/IdCardView.swift @@ -205,9 +205,6 @@ struct IdCardView: View { await viewModel.stopDiscoveringReaders() cancelIdCardAction() resetIdCardAction() - isInProgress = false - isShowingPinView = false - isShowingLoadingView = false onSuccessDecrypt(container) dismiss() @@ -239,6 +236,7 @@ struct IdCardView: View { icon: "ic_m3_smart_card_reader_48pt_wght400", message: $idCardActionMessage ) + .accessibilityHidden(!isInProgress) } else if isInProgress && isShowingPinView { IdCardInputView( personIdentifier: personIdentifier, @@ -385,6 +383,10 @@ struct IdCardView: View { } } .onDisappear { + isInProgress = false + isShowingPinView = false + isShowingLoadingView = false + idCardActionMessage = "" pinNumber.removeAll() cancelIdCardAction() } @@ -491,17 +493,19 @@ struct IdCardView: View { await viewModel.stopDiscoveringReaders() cancelIdCardAction() - isInProgress = false - isShowingPinView = false - isShowingLoadingView = false - Toast.show(signatureAddedMessage, type: .success) - if voiceOverEnabled { - AccessibilityUtil.announceMessage(signatureAddedMessage) - } + await MainActor.run { + idCardActionMessage = "" - onSuccess(container) - dismiss() + Toast.show(signatureAddedMessage, type: .success) + + if voiceOverEnabled { + AccessibilityUtil.announceMessage(signatureAddedMessage) + } + + onSuccess(container) + dismiss() + } } } } diff --git a/RIADigiDoc/UI/Component/Container/Signing/NFC/NFCInputView.swift b/RIADigiDoc/UI/Component/Container/Signing/NFC/NFCInputView.swift index 88648a51..e66e6f69 100644 --- a/RIADigiDoc/UI/Component/Container/Signing/NFC/NFCInputView.swift +++ b/RIADigiDoc/UI/Component/Container/Signing/NFC/NFCInputView.swift @@ -22,6 +22,7 @@ import FactoryKit import IdCardLib struct NFCInputView: View { + @Environment(\.accessibilityVoiceOverEnabled) private var voiceOverEnabled @Environment(LanguageSettings.self) private var languageSettings @AppTheme private var theme @@ -80,27 +81,14 @@ struct NFCInputView: View { var body: some View { VStack(alignment: .leading) { VStack(alignment: .leading, spacing: Dimensions.Padding.MPadding) { - VStack(alignment: .leading, spacing: Dimensions.Padding.XSPadding) { - FloatingLabelTextField( - title: canNumberTitle, - placeholder: canNumberTitle, - text: $canNumber, - isError: !(canNumberError?.isEmpty ?? true), - errorText: canNumberError ?? "", - keyboardType: .numberPad, - sortPriority: 0 - ) - .onChange(of: canNumber) { - onInputChange() - } - - Text(verbatim: canNumberLocationLabel) - .font(typography.labelMedium) - .foregroundStyle(theme.onSecondaryContainer) - .padding(.top, Dimensions.Padding.XXSPadding) - .accessibilitySortPriority(1) + // Allow keyboard to focus on input fields + // but have custom sort priority (contain to VStack) for these elements only with VoiceOver + if voiceOverEnabled { + nfcInputFields + .accessibilityElement(children: .contain) + } else { + nfcInputFields } - .accessibilityElement(children: .contain) } .padding(.bottom, Dimensions.Padding.MPadding) @@ -138,6 +126,29 @@ struct NFCInputView: View { } } } + + private var nfcInputFields: some View { + VStack(alignment: .leading, spacing: Dimensions.Padding.XSPadding) { + FloatingLabelTextField( + title: canNumberTitle, + placeholder: canNumberTitle, + text: $canNumber, + isError: !(canNumberError?.isEmpty ?? true), + errorText: canNumberError ?? "", + keyboardType: .numberPad, + sortPriority: 0 + ) + .onChange(of: canNumber) { + onInputChange() + } + + Text(verbatim: canNumberLocationLabel) + .font(typography.labelMedium) + .foregroundStyle(theme.onSecondaryContainer) + .padding(.top, Dimensions.Padding.XXSPadding) + .accessibilitySortPriority(1) + } + } } #Preview { diff --git a/RIADigiDoc/UI/Component/Container/Signing/NFC/NFCView.swift b/RIADigiDoc/UI/Component/Container/Signing/NFC/NFCView.swift index b97170fa..bcdf1d0a 100644 --- a/RIADigiDoc/UI/Component/Container/Signing/NFC/NFCView.swift +++ b/RIADigiDoc/UI/Component/Container/Signing/NFC/NFCView.swift @@ -367,6 +367,10 @@ struct NFCView: View { Toast.show(signatureAddedMessage, type: .success) + if voiceOverEnabled { + AccessibilityUtil.announceMessage(signatureAddedMessage) + } + onSuccess(container) dismiss() } diff --git a/RIADigiDoc/UI/Component/Container/Signing/SignaturesListView.swift b/RIADigiDoc/UI/Component/Container/Signing/SignaturesListView.swift index 8e464b68..b09ce8e6 100644 --- a/RIADigiDoc/UI/Component/Container/Signing/SignaturesListView.swift +++ b/RIADigiDoc/UI/Component/Container/Signing/SignaturesListView.swift @@ -31,12 +31,15 @@ struct SignaturesListView: View { @Binding var selectedSignature: SignatureWrapper? @Binding var containerMimetype: String var dataFilesCount: Int + @Binding var focusedIndex: Int? var showRemoveSignatureButton: Bool @Binding var showRemoveSignatureModal: Bool let nameUtil: NameUtilProtocol let signatureUtil: SignatureUtilProtocol + @AccessibilityFocusState private var focusedSignatureIndex: Int? + var body: some View { VStack { if #available(iOS 26.0, *) { @@ -55,6 +58,7 @@ struct SignaturesListView: View { selectedSignature = timestamp } ) + .accessibilityFocused($focusedSignatureIndex, equals: index) } } else { ForEach(Array(timestamps.enumerated()), id: \.offset) { index, timestamp in @@ -72,8 +76,10 @@ struct SignaturesListView: View { selectedSignature = timestamp } ) + .accessibilityFocused($focusedSignatureIndex, equals: index) } } + if #available(iOS 26.0, *) { ForEach(signatures.enumerated(), id: \.offset) { index, signature in SignatureView( @@ -89,6 +95,13 @@ struct SignaturesListView: View { selectedSignature = signature } ) + .id(index) + .accessibilityFocused($focusedSignatureIndex, equals: index) + } + .onChange(of: focusedIndex) { _, newValue in + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + focusedSignatureIndex = newValue + } } } else { ForEach(Array(signatures.enumerated()), id: \.offset) { index, signature in @@ -105,6 +118,7 @@ struct SignaturesListView: View { selectedSignature = signature } ) + .accessibilityFocused($focusedSignatureIndex, equals: index) } } } @@ -140,6 +154,7 @@ struct SignaturesListView: View { selectedSignature: .constant(signature), containerMimetype: .constant("application/vnd.etsi.asic-e+zip"), dataFilesCount: 1, + focusedIndex: .constant(nil), showRemoveSignatureButton: true, showRemoveSignatureModal: .constant(false), nameUtil: Container.shared.nameUtil(), diff --git a/RIADigiDoc/UI/Component/Container/Signing/SigningRootView.swift b/RIADigiDoc/UI/Component/Container/Signing/SigningRootView.swift index 19535dea..0e68d611 100644 --- a/RIADigiDoc/UI/Component/Container/Signing/SigningRootView.swift +++ b/RIADigiDoc/UI/Component/Container/Signing/SigningRootView.swift @@ -29,6 +29,7 @@ struct SigningRootView: View { @Environment(NavigationPathManager.self) private var pathManager @State private var chosenMethod: ActionMethod = .idCardViaNFC + @State private var isSuccess = false @State private var viewModel: SigningRootViewModel @@ -58,6 +59,7 @@ struct SigningRootView: View { pinType: CodeType.pin2, signedContainer: container, onSuccess: { container in + isSuccess = true sharedContainerViewModel.removeLastContainer() sharedContainerViewModel.setSignedContainer(container) sharedContainerViewModel.setIsSignatureAdded(true) @@ -76,11 +78,13 @@ struct SigningRootView: View { ], signedContainer: container, onSuccess: { container in + isSuccess = true sharedContainerViewModel.removeLastContainer() sharedContainerViewModel.setSignedContainer(container) sharedContainerViewModel.setIsSignatureAdded(true) } ) + .accessibilityHidden(isSuccess) } case .mobileId: if let container = signedContainer as? SignedContainerProtocol { diff --git a/RIADigiDoc/UI/Component/Container/Signing/SigningView.swift b/RIADigiDoc/UI/Component/Container/Signing/SigningView.swift index e1c43e11..a1bc7587 100644 --- a/RIADigiDoc/UI/Component/Container/Signing/SigningView.swift +++ b/RIADigiDoc/UI/Component/Container/Signing/SigningView.swift @@ -25,6 +25,7 @@ import UtilsLib struct SigningView: View { @Environment(\.presentationMode) var presentationMode + @Environment(\.accessibilityVoiceOverEnabled) private var voiceOverEnabled @AppTheme private var theme @AppTypography private var typography @Environment(LanguageSettings.self) private var languageSettings @@ -56,7 +57,8 @@ struct SigningView: View { @State private var showSivaMessage = false - @AccessibilityFocusState private var focusedField: AccessibilityField? + @State private var scrollPosition: Int? + @State private var focusedSignatureIndex: Int? private var containerTitle: String { !isContainerSigned && !isNestedContainer ? @@ -282,6 +284,7 @@ struct SigningView: View { selectedSignature: $selectedSignature, containerMimetype: $viewModel.containerMimetype, dataFilesCount: viewModel.dataFiles.count, + focusedIndex: $focusedSignatureIndex, showRemoveSignatureButton: viewModel.isSignatureRemoveButtonShown(), showRemoveSignatureModal: $showRemoveSignatureModal, nameUtil: nameUtil, @@ -329,6 +332,11 @@ struct SigningView: View { } } .padding(Dimensions.Padding.SPadding) + .scrollPosition(id: $scrollPosition, anchor: .bottom) + .onChange(of: viewModel.signatures.count) { previousCount, newCount in + guard newCount > previousCount else { return } + scrollToBottom() + } if isSignedContainer { if let containerFile = viewModel.containerURL { @@ -386,8 +394,10 @@ struct SigningView: View { } } .onAppear { - if viewModel.isSignatureAdded() { - selectedTab = .signatures + DispatchQueue.main.async { + if viewModel.isSignatureAdded() { + selectedTab = .signatures + } } containerLoadingTask = Task { @@ -474,6 +484,18 @@ struct SigningView: View { } } + private func scrollToBottom() { + DispatchQueue.main.async { + guard let lastSignature = viewModel.signatures.indices.last else { return } + + scrollPosition = lastSignature + + DispatchQueue.main.async { + focusedSignatureIndex = lastSignature + } + } + } + private func updateSignAndEncryptButtonVisibility() async { let shouldShowSignButton = await viewModel .isSignButtonShown( diff --git a/RIADigiDoc/UI/Component/My eID/MyEidPinChangeView.swift b/RIADigiDoc/UI/Component/My eID/MyEidPinChangeView.swift index d1f8f068..04e0881c 100644 --- a/RIADigiDoc/UI/Component/My eID/MyEidPinChangeView.swift +++ b/RIADigiDoc/UI/Component/My eID/MyEidPinChangeView.swift @@ -230,36 +230,14 @@ struct MyEidPinChangeView: View { .padding(.bottom, Dimensions.Padding.LPadding) .frame(maxWidth: .infinity) - VStack { - FloatingLabelTextField( - title: flowCodeType, - placeholder: flowCodeType, - text: $viewModel.input, - isSecure: true, - isError: isInputError, - errorText: inputErrorMessage, - keyboardType: .numberPad, - identifier: "pinInput", - sortPriority: 0, - spellOutCharacters: true, - onDone: { - if voiceOverEnabled || (viewModel.step == .confirm || ( - viewModel.input.isEmpty || !inputErrorMessage.isEmpty - )) { return } - - Task { await viewModel.submit(nfcStringsUtil: nfcStringsUtil) } - } - ) - - Text(verbatim: flowDescription) - .font(typography.bodySmall) - .foregroundStyle(isInputError ? theme.error : theme.onSurface) - .padding(.vertical, Dimensions.Padding.MSPadding) - .multilineTextAlignment(.leading) - .frame(maxWidth: .infinity, alignment: .leading) - .accessibilitySortPriority(1) + // Allow keyboard to focus on input fields + // but have custom sort priority (contain to VStack) for these elements only with VoiceOver + if voiceOverEnabled { + pinInputField + .accessibilityElement(children: .contain) + } else { + pinInputField } - .accessibilityElement(children: .contain) } } @@ -321,34 +299,35 @@ struct MyEidPinChangeView: View { ) } -// @ViewBuilder -// private var inputGroup: some View { -// VStack { -// FloatingLabelTextField( -// title: flowCodeType, -// placeholder: flowCodeType, -// text: $viewModel.input, -// isSecure: true, -// isError: isInputError, -// errorText: inputErrorMessage, -// keyboardType: .numberPad, -// identifier: "pinInput", -// sortPriority: 0, -// spellOutCharacters: true, -// onDone: { -// if voiceOverEnabled || (viewModel.step == .confirm || ( -// viewModel.input.isEmpty || !inputErrorMessage.isEmpty -// )) { return } -// -// Task { await viewModel.submit(nfcStringsUtil: nfcStringsUtil) } -// } -// ) -// -// Text(verbatim: flowDescription) -// .font(typography.bodySmall) -// .foregroundStyle(isInputError ? theme.error : theme.onSurface) -// .padding(.vertical, Dimensions.Padding.MSPadding) -// .accessibilitySortPriority(1) -// } -// } + private var pinInputField: some View { + VStack { + FloatingLabelTextField( + title: flowCodeType, + placeholder: flowCodeType, + text: $viewModel.input, + isSecure: true, + isError: isInputError, + errorText: inputErrorMessage, + keyboardType: .numberPad, + identifier: "pinInput", + sortPriority: 0, + spellOutCharacters: true, + onDone: { + if voiceOverEnabled || (viewModel.step == .confirm || ( + viewModel.input.isEmpty || !inputErrorMessage.isEmpty + )) { return } + + Task { await viewModel.submit(nfcStringsUtil: nfcStringsUtil) } + } + ) + + Text(verbatim: flowDescription) + .font(typography.bodySmall) + .foregroundStyle(isInputError ? theme.error : theme.onSurface) + .padding(.vertical, Dimensions.Padding.MSPadding) + .multilineTextAlignment(.leading) + .frame(maxWidth: .infinity, alignment: .leading) + .accessibilitySortPriority(1) + } + } } diff --git a/RIADigiDoc/UI/Component/Toast/ToastOverlay.swift b/RIADigiDoc/UI/Component/Toast/ToastOverlay.swift index 641cd004..f27da191 100644 --- a/RIADigiDoc/UI/Component/Toast/ToastOverlay.swift +++ b/RIADigiDoc/UI/Component/Toast/ToastOverlay.swift @@ -60,6 +60,7 @@ struct ToastOverlay: View { Text(verbatim: message) .lineLimit(nil) .minimumScaleFactor(0.5) + .accessibilityHidden(true) } .frame(maxWidth: .infinity, alignment: .leading) .padding(.horizontal, Dimensions.Padding.SPadding)