mirror of
https://github.com/bitwarden/ios.git
synced 2025-12-11 23:33:36 -06:00
[PM-26063] Update Authenticator's settings view to latest designs (#2113)
This commit is contained in:
parent
f9ae28de1d
commit
73fc2d84b8
@ -60,7 +60,7 @@ class SettingsViewTests: BitwardenTestCase {
|
|||||||
func test_defaultSaveOptionChanged_updateValue() throws {
|
func test_defaultSaveOptionChanged_updateValue() throws {
|
||||||
processor.state.shouldShowDefaultSaveOption = true
|
processor.state.shouldShowDefaultSaveOption = true
|
||||||
processor.state.defaultSaveOption = .none
|
processor.state.defaultSaveOption = .none
|
||||||
let menuField = try subject.inspect().find(settingsMenuField: Localizations.defaultSaveOption)
|
let menuField = try subject.inspect().find(bitwardenMenuField: Localizations.defaultSaveOption)
|
||||||
try menuField.select(newValue: DefaultSaveOption.saveToBitwarden)
|
try menuField.select(newValue: DefaultSaveOption.saveToBitwarden)
|
||||||
XCTAssertEqual(processor.dispatchedActions.last, .defaultSaveChanged(.saveToBitwarden))
|
XCTAssertEqual(processor.dispatchedActions.last, .defaultSaveChanged(.saveToBitwarden))
|
||||||
}
|
}
|
||||||
@ -102,7 +102,7 @@ class SettingsViewTests: BitwardenTestCase {
|
|||||||
func test_sessionTimeoutValue_updateValue() throws {
|
func test_sessionTimeoutValue_updateValue() throws {
|
||||||
processor.state.biometricUnlockStatus = .available(.faceID, enabled: false, hasValidIntegrity: true)
|
processor.state.biometricUnlockStatus = .available(.faceID, enabled: false, hasValidIntegrity: true)
|
||||||
processor.state.sessionTimeoutValue = .never
|
processor.state.sessionTimeoutValue = .never
|
||||||
let menuField = try subject.inspect().find(settingsMenuField: Localizations.sessionTimeout)
|
let menuField = try subject.inspect().find(bitwardenMenuField: Localizations.sessionTimeout)
|
||||||
try menuField.select(newValue: SessionTimeoutValue.fifteenMinutes)
|
try menuField.select(newValue: SessionTimeoutValue.fifteenMinutes)
|
||||||
|
|
||||||
waitFor(!processor.effects.isEmpty)
|
waitFor(!processor.effects.isEmpty)
|
||||||
|
|||||||
@ -27,59 +27,54 @@ struct SettingsView: View {
|
|||||||
// MARK: View
|
// MARK: View
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
settingsItems
|
VStack(spacing: 16) {
|
||||||
.scrollView()
|
securitySection
|
||||||
.navigationBar(title: Localizations.settings, titleDisplayMode: titleDisplayMode)
|
dataSection
|
||||||
.toast(store.binding(
|
appearanceSection
|
||||||
get: \.toast,
|
helpSection
|
||||||
send: SettingsAction.toastShown,
|
aboutSection
|
||||||
))
|
copyrightNotice
|
||||||
.onChange(of: store.state.url) { newValue in
|
}
|
||||||
guard let url = newValue else { return }
|
.scrollView()
|
||||||
openURL(url)
|
.navigationBar(title: Localizations.settings, titleDisplayMode: titleDisplayMode)
|
||||||
store.send(.clearURL)
|
.toast(store.binding(
|
||||||
}
|
get: \.toast,
|
||||||
.task {
|
send: SettingsAction.toastShown,
|
||||||
await store.perform(.loadData)
|
))
|
||||||
}
|
.onChange(of: store.state.url) { newValue in
|
||||||
|
guard let url = newValue else { return }
|
||||||
|
openURL(url)
|
||||||
|
store.send(.clearURL)
|
||||||
|
}
|
||||||
|
.task {
|
||||||
|
await store.perform(.loadData)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Private views
|
// MARK: Private views
|
||||||
|
|
||||||
/// A view for the user's biometrics setting
|
/// The about section containing privacy policy and version information.
|
||||||
///
|
@ViewBuilder private var aboutSection: some View {
|
||||||
@ViewBuilder private var biometricsSetting: some View {
|
SectionView(Localizations.about) {
|
||||||
switch store.state.biometricUnlockStatus {
|
ContentBlock(dividerLeadingPadding: 16) {
|
||||||
case let .available(type, enabled: enabled, _):
|
externalLinkRow(Localizations.privacyPolicy, action: .privacyPolicyTapped)
|
||||||
SectionView(Localizations.security) {
|
|
||||||
VStack(spacing: 8) {
|
SettingsListItem(store.state.version) {
|
||||||
biometricUnlockToggle(enabled: enabled, type: type)
|
store.send(.versionTapped)
|
||||||
SettingsMenuField(
|
} trailingContent: {
|
||||||
title: Localizations.sessionTimeout,
|
SharedAsset.Icons.copy24.swiftUIImage
|
||||||
options: SessionTimeoutValue.allCases,
|
.imageStyle(.rowIcon)
|
||||||
hasDivider: false,
|
|
||||||
accessibilityIdentifier: "VaultTimeoutChooser",
|
|
||||||
selectionAccessibilityID: "SessionTimeoutStatusLabel",
|
|
||||||
selection: store.bindingAsync(
|
|
||||||
get: \.sessionTimeoutValue,
|
|
||||||
perform: SettingsEffect.sessionTimeoutValueChanged,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 10))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.bottom, 32)
|
|
||||||
default:
|
|
||||||
EmptyView()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The chevron shown in the settings list item.
|
/// The appearance section containing language and theme settings.
|
||||||
private var chevron: some View {
|
@ViewBuilder private var appearanceSection: some View {
|
||||||
Image(asset: SharedAsset.Icons.chevronRight16)
|
SectionView(Localizations.appearance, contentSpacing: 8) {
|
||||||
.resizable()
|
language
|
||||||
.scaledFrame(width: 12, height: 12)
|
theme
|
||||||
.foregroundColor(Color(asset: Asset.Colors.textSecondary))
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The copyright notice.
|
/// The copyright notice.
|
||||||
@ -91,31 +86,9 @@ struct SettingsView: View {
|
|||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The language picker view
|
/// The data section containing import, export, backup, and sync options.
|
||||||
private var language: some View {
|
@ViewBuilder private var dataSection: some View {
|
||||||
Button {
|
SectionView(Localizations.data) {
|
||||||
store.send(.languageTapped)
|
|
||||||
} label: {
|
|
||||||
BitwardenField(
|
|
||||||
title: Localizations.language,
|
|
||||||
footer: Localizations.languageChangeRequiresAppRestart,
|
|
||||||
) {
|
|
||||||
Text(store.state.currentLanguage.title)
|
|
||||||
.styleGuide(.body)
|
|
||||||
.foregroundColor(Color(asset: SharedAsset.Colors.textPrimary))
|
|
||||||
.multilineTextAlignment(.leading)
|
|
||||||
} accessoryContent: {
|
|
||||||
SharedAsset.Icons.chevronDown24.swiftUIImage
|
|
||||||
.imageStyle(.rowIcon)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The settings items.
|
|
||||||
private var settingsItems: some View {
|
|
||||||
VStack(spacing: 0) {
|
|
||||||
biometricsSetting
|
|
||||||
|
|
||||||
ContentBlock(dividerLeadingPadding: 16) {
|
ContentBlock(dividerLeadingPadding: 16) {
|
||||||
SettingsListItem(Localizations.import) {
|
SettingsListItem(Localizations.import) {
|
||||||
store.send(.importItemsTapped)
|
store.send(.importItemsTapped)
|
||||||
@ -135,14 +108,12 @@ struct SettingsView: View {
|
|||||||
defaultSaveOption
|
defaultSaveOption
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.bottom, 32)
|
}
|
||||||
|
}
|
||||||
SectionView(Localizations.appearance) {
|
|
||||||
language
|
|
||||||
theme
|
|
||||||
}
|
|
||||||
.padding(.bottom, 32)
|
|
||||||
|
|
||||||
|
/// The help section containing tutorial and help center links.
|
||||||
|
@ViewBuilder private var helpSection: some View {
|
||||||
|
SectionView(Localizations.help) {
|
||||||
ContentBlock(dividerLeadingPadding: 16) {
|
ContentBlock(dividerLeadingPadding: 16) {
|
||||||
SettingsListItem(Localizations.launchTutorial) {
|
SettingsListItem(Localizations.launchTutorial) {
|
||||||
store.send(.tutorialTapped)
|
store.send(.tutorialTapped)
|
||||||
@ -150,31 +121,34 @@ struct SettingsView: View {
|
|||||||
|
|
||||||
externalLinkRow(Localizations.bitwardenHelpCenter, action: .helpCenterTapped)
|
externalLinkRow(Localizations.bitwardenHelpCenter, action: .helpCenterTapped)
|
||||||
}
|
}
|
||||||
.padding(.bottom, 32)
|
|
||||||
|
|
||||||
ContentBlock(dividerLeadingPadding: 16) {
|
|
||||||
externalLinkRow(Localizations.privacyPolicy, action: .privacyPolicyTapped)
|
|
||||||
|
|
||||||
SettingsListItem(store.state.version) {
|
|
||||||
store.send(.versionTapped)
|
|
||||||
} trailingContent: {
|
|
||||||
SharedAsset.Icons.copy24.swiftUIImage
|
|
||||||
.imageStyle(.rowIcon)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(.bottom, 16)
|
|
||||||
|
|
||||||
copyrightNotice
|
|
||||||
}
|
}
|
||||||
.cornerRadius(10)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The application's default save option picker view
|
/// The language picker view.
|
||||||
|
private var language: some View {
|
||||||
|
Button {
|
||||||
|
store.send(.languageTapped)
|
||||||
|
} label: {
|
||||||
|
BitwardenField(
|
||||||
|
title: Localizations.language,
|
||||||
|
footer: Localizations.languageChangeRequiresAppRestart,
|
||||||
|
) {
|
||||||
|
Text(store.state.currentLanguage.title)
|
||||||
|
.styleGuide(.body)
|
||||||
|
.foregroundColor(Color(asset: SharedAsset.Colors.textPrimary))
|
||||||
|
.multilineTextAlignment(.leading)
|
||||||
|
} accessoryContent: {
|
||||||
|
SharedAsset.Icons.chevronDown24.swiftUIImage
|
||||||
|
.imageStyle(.rowIcon)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The application's default save option picker view.
|
||||||
@ViewBuilder private var defaultSaveOption: some View {
|
@ViewBuilder private var defaultSaveOption: some View {
|
||||||
SettingsMenuField(
|
BitwardenMenuField(
|
||||||
title: Localizations.defaultSaveOption,
|
title: Localizations.defaultSaveOption,
|
||||||
options: DefaultSaveOption.allCases,
|
options: DefaultSaveOption.allCases,
|
||||||
hasDivider: false,
|
|
||||||
selection: store.binding(
|
selection: store.binding(
|
||||||
get: \.defaultSaveOption,
|
get: \.defaultSaveOption,
|
||||||
send: SettingsAction.defaultSaveChanged,
|
send: SettingsAction.defaultSaveChanged,
|
||||||
@ -183,7 +157,31 @@ struct SettingsView: View {
|
|||||||
.accessibilityIdentifier("DefaultSaveOptionChooser")
|
.accessibilityIdentifier("DefaultSaveOptionChooser")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The application's color theme picker view
|
/// The security section containing biometric unlock and session timeout settings.
|
||||||
|
@ViewBuilder private var securitySection: some View {
|
||||||
|
switch store.state.biometricUnlockStatus {
|
||||||
|
case let .available(type, enabled: enabled, _):
|
||||||
|
SectionView(Localizations.security) {
|
||||||
|
ContentBlock {
|
||||||
|
biometricUnlockToggle(enabled: enabled, type: type)
|
||||||
|
|
||||||
|
BitwardenMenuField(
|
||||||
|
title: Localizations.sessionTimeout,
|
||||||
|
accessibilityIdentifier: "VaultTimeoutChooser",
|
||||||
|
options: SessionTimeoutValue.allCases,
|
||||||
|
selection: store.bindingAsync(
|
||||||
|
get: \.sessionTimeoutValue,
|
||||||
|
perform: SettingsEffect.sessionTimeoutValueChanged,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The application's color theme picker view.
|
||||||
private var theme: some View {
|
private var theme: some View {
|
||||||
BitwardenMenuField(
|
BitwardenMenuField(
|
||||||
title: Localizations.theme,
|
title: Localizations.theme,
|
||||||
@ -202,16 +200,15 @@ struct SettingsView: View {
|
|||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private func biometricUnlockToggle(enabled: Bool, type: BiometricAuthenticationType) -> some View {
|
private func biometricUnlockToggle(enabled: Bool, type: BiometricAuthenticationType) -> some View {
|
||||||
let toggleText = biometricsToggleText(type)
|
let toggleText = biometricsToggleText(type)
|
||||||
Toggle(isOn: store.bindingAsync(
|
BitwardenToggle(
|
||||||
get: { _ in enabled },
|
toggleText,
|
||||||
perform: SettingsEffect.toggleUnlockWithBiometrics,
|
isOn: store.bindingAsync(
|
||||||
)) {
|
get: { _ in enabled },
|
||||||
Text(toggleText)
|
perform: SettingsEffect.toggleUnlockWithBiometrics,
|
||||||
}
|
),
|
||||||
.padding(.trailing, 3)
|
)
|
||||||
.accessibilityIdentifier("UnlockWithBiometricsSwitch")
|
.accessibilityIdentifier("UnlockWithBiometricsSwitch")
|
||||||
.accessibilityLabel(toggleText)
|
.accessibilityLabel(toggleText)
|
||||||
.toggleStyle(.bitwarden)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func biometricsToggleText(_ biometryType: BiometricAuthenticationType) -> String {
|
private func biometricsToggleText(_ biometryType: BiometricAuthenticationType) -> String {
|
||||||
|
|||||||
@ -120,10 +120,11 @@ public struct BitwardenMenuField<
|
|||||||
)
|
)
|
||||||
.foregroundColor(isEnabled
|
.foregroundColor(isEnabled
|
||||||
? SharedAsset.Colors.textSecondary.swiftUIColor
|
? SharedAsset.Colors.textSecondary.swiftUIColor
|
||||||
: SharedAsset.Colors.buttonFilledDisabledForeground.swiftUIColor)
|
: SharedAsset.Colors.buttonFilledDisabledForeground.swiftUIColor
|
||||||
.onSizeChanged { size in
|
)
|
||||||
titleWidth = size.width
|
.onSizeChanged { size in
|
||||||
}
|
titleWidth = size.width
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(selection.localizedName)
|
Text(selection.localizedName)
|
||||||
@ -150,20 +151,21 @@ public struct BitwardenMenuField<
|
|||||||
.styleGuide(.body)
|
.styleGuide(.body)
|
||||||
.foregroundColor(isEnabled
|
.foregroundColor(isEnabled
|
||||||
? SharedAsset.Colors.textPrimary.swiftUIColor
|
? SharedAsset.Colors.textPrimary.swiftUIColor
|
||||||
: SharedAsset.Colors.buttonFilledDisabledForeground.swiftUIColor)
|
: SharedAsset.Colors.buttonFilledDisabledForeground.swiftUIColor
|
||||||
.frame(minHeight: 64)
|
)
|
||||||
.accessibilityIdentifier(accessibilityIdentifier ?? "")
|
.frame(minHeight: 64)
|
||||||
.overlay {
|
.accessibilityIdentifier(accessibilityIdentifier ?? "")
|
||||||
if let titleAccessoryContent {
|
.overlay {
|
||||||
titleAccessoryContent
|
if let titleAccessoryContent {
|
||||||
.frame(
|
titleAccessoryContent
|
||||||
maxWidth: .infinity,
|
.frame(
|
||||||
maxHeight: .infinity,
|
maxWidth: .infinity,
|
||||||
alignment: .topLeading,
|
maxHeight: .infinity,
|
||||||
)
|
alignment: .topLeading,
|
||||||
.offset(x: titleWidth + 4, y: 12)
|
)
|
||||||
}
|
.offset(x: titleWidth + 4, y: 12)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Initialization
|
// MARK: Initialization
|
||||||
|
|||||||
@ -1,60 +0,0 @@
|
|||||||
// swiftlint:disable:this file_name
|
|
||||||
import BitwardenKit
|
|
||||||
import SwiftUI
|
|
||||||
import ViewInspector
|
|
||||||
import XCTest
|
|
||||||
|
|
||||||
class SettingsMenuFieldTests: BitwardenTestCase {
|
|
||||||
// MARK: Types
|
|
||||||
|
|
||||||
enum TestValue: String, CaseIterable, Menuable {
|
|
||||||
case value1
|
|
||||||
case value2
|
|
||||||
|
|
||||||
var localizedName: String {
|
|
||||||
rawValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: Properties
|
|
||||||
|
|
||||||
var selection: TestValue!
|
|
||||||
var subject: SettingsMenuField<TestValue>!
|
|
||||||
|
|
||||||
// MARK: Setup & Teardown
|
|
||||||
|
|
||||||
override func setUp() {
|
|
||||||
super.setUp()
|
|
||||||
selection = .value1
|
|
||||||
let binding = Binding {
|
|
||||||
self.selection!
|
|
||||||
} set: { newValue in
|
|
||||||
self.selection = newValue
|
|
||||||
}
|
|
||||||
subject = SettingsMenuField(
|
|
||||||
title: "Title",
|
|
||||||
options: TestValue.allCases,
|
|
||||||
selection: binding,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override func tearDown() {
|
|
||||||
super.tearDown()
|
|
||||||
selection = nil
|
|
||||||
subject = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: Tests
|
|
||||||
|
|
||||||
func test_newSelection() throws {
|
|
||||||
let picker = try subject.inspect().find(ViewType.Picker.self)
|
|
||||||
try picker.select(value: TestValue.value2)
|
|
||||||
XCTAssertEqual(selection, .value2)
|
|
||||||
|
|
||||||
let menu = try subject.inspect().find(ViewType.Menu.self)
|
|
||||||
let title = try menu.labelView().find(ViewType.Text.self).string()
|
|
||||||
let pickerValue = try menu.find(ViewType.HStack.self).find(text: "value2").string()
|
|
||||||
XCTAssertEqual(title, "Title")
|
|
||||||
XCTAssertEqual(pickerValue, "value2")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,147 +0,0 @@
|
|||||||
import BitwardenResources
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
// MARK: - SettingsMenuField
|
|
||||||
|
|
||||||
/// A standard input field that allows the user to select between a predefined set of
|
|
||||||
/// options.
|
|
||||||
///
|
|
||||||
public struct SettingsMenuField<T>: View where T: Menuable {
|
|
||||||
// MARK: Properties
|
|
||||||
|
|
||||||
/// The accessibility ID for the menu field.
|
|
||||||
let accessibilityIdentifier: String?
|
|
||||||
|
|
||||||
/// Whether the menu field should have a bottom divider.
|
|
||||||
let hasDivider: Bool
|
|
||||||
|
|
||||||
/// Whether the view allows user interaction.
|
|
||||||
@Environment(\.isEnabled) var isEnabled: Bool
|
|
||||||
|
|
||||||
/// The selection chosen from the menu.
|
|
||||||
@Binding var selection: T
|
|
||||||
|
|
||||||
/// The accessibility ID for the picker selection.
|
|
||||||
let selectionAccessibilityID: String?
|
|
||||||
|
|
||||||
/// The options displayed in the menu.
|
|
||||||
let options: [T]
|
|
||||||
|
|
||||||
/// The title of the menu field.
|
|
||||||
let title: String
|
|
||||||
|
|
||||||
// MARK: View
|
|
||||||
|
|
||||||
public var body: some View {
|
|
||||||
VStack(spacing: 0) {
|
|
||||||
Menu {
|
|
||||||
Picker(selection: $selection) {
|
|
||||||
ForEach(options, id: \.hashValue) { option in
|
|
||||||
Text(option.localizedName).tag(option)
|
|
||||||
}
|
|
||||||
} label: {
|
|
||||||
Text("")
|
|
||||||
}
|
|
||||||
} label: {
|
|
||||||
HStack {
|
|
||||||
Text(title)
|
|
||||||
.multilineTextAlignment(.leading)
|
|
||||||
.foregroundColor(
|
|
||||||
(isEnabled
|
|
||||||
? SharedAsset.Colors.textPrimary
|
|
||||||
: SharedAsset.Colors.textSecondary
|
|
||||||
).swiftUIColor,
|
|
||||||
)
|
|
||||||
.padding(.vertical, 19)
|
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
Text(selection.localizedName)
|
|
||||||
.accessibilityIdentifier(selectionAccessibilityID ?? "")
|
|
||||||
.multilineTextAlignment(.trailing)
|
|
||||||
.foregroundColor(SharedAsset.Colors.textSecondary.swiftUIColor)
|
|
||||||
.accessibilityIdentifier(selectionAccessibilityID ?? "")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.styleGuide(.body)
|
|
||||||
.accessibilityIdentifier(accessibilityIdentifier ?? "")
|
|
||||||
.id(title)
|
|
||||||
.padding(.horizontal, 16)
|
|
||||||
|
|
||||||
if hasDivider {
|
|
||||||
Divider()
|
|
||||||
.padding(.leading, 16)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.background(
|
|
||||||
isEnabled
|
|
||||||
? SharedAsset.Colors.backgroundSecondary.swiftUIColor
|
|
||||||
: SharedAsset.Colors.backgroundSecondaryDisabled.swiftUIColor,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Initializes a new `SettingsMenuField`.
|
|
||||||
///
|
|
||||||
/// - Parameters:
|
|
||||||
/// - title: The title of the menu field.
|
|
||||||
/// - options: The options that the user can choose between.
|
|
||||||
/// - hasDivider: Whether the menu field should have a bottom divider.
|
|
||||||
/// - accessibilityIdentifier: The accessibility ID for the menu field.
|
|
||||||
/// - selectionAccessibilityID: The accessibility ID for the picker selection.
|
|
||||||
/// - selection: A `Binding` for the currently selected option.
|
|
||||||
///
|
|
||||||
public init(
|
|
||||||
title: String,
|
|
||||||
options: [T],
|
|
||||||
hasDivider: Bool = true,
|
|
||||||
accessibilityIdentifier: String? = nil,
|
|
||||||
selectionAccessibilityID: String? = nil,
|
|
||||||
selection: Binding<T>,
|
|
||||||
) {
|
|
||||||
self.accessibilityIdentifier = accessibilityIdentifier
|
|
||||||
self.hasDivider = hasDivider
|
|
||||||
self.options = options
|
|
||||||
_selection = selection
|
|
||||||
self.selectionAccessibilityID = selectionAccessibilityID
|
|
||||||
self.title = title
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: Previews
|
|
||||||
|
|
||||||
#if DEBUG
|
|
||||||
private enum MenuPreviewOptions: CaseIterable, Menuable {
|
|
||||||
case bear, bird, dog
|
|
||||||
|
|
||||||
var localizedName: String {
|
|
||||||
switch self {
|
|
||||||
case .bear: "🧸"
|
|
||||||
case .bird: "🪿"
|
|
||||||
case .dog: "🐕"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#Preview {
|
|
||||||
Group {
|
|
||||||
VStack(spacing: 0) {
|
|
||||||
SettingsMenuField(
|
|
||||||
title: "Bear",
|
|
||||||
options: MenuPreviewOptions.allCases,
|
|
||||||
selection: .constant(.bear),
|
|
||||||
)
|
|
||||||
|
|
||||||
SettingsMenuField(
|
|
||||||
title: "Dog",
|
|
||||||
options: MenuPreviewOptions.allCases,
|
|
||||||
hasDivider: false,
|
|
||||||
selection: .constant(.dog),
|
|
||||||
)
|
|
||||||
.disabled(true)
|
|
||||||
}
|
|
||||||
.padding(8)
|
|
||||||
}
|
|
||||||
.background(Color(.systemGroupedBackground))
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
@ -117,17 +117,6 @@ public struct LoadingViewType: BaseViewType {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A generic type wrapper around `SettingsMenuField` to allow `ViewInspector` to find instances of
|
|
||||||
/// `SettingsMenuField` without needing to know the details of its implementation.
|
|
||||||
///
|
|
||||||
public struct SettingsMenuFieldType: BaseViewType {
|
|
||||||
public static var typePrefix: String = "SettingsMenuField"
|
|
||||||
|
|
||||||
public static var namespacedPrefixes: [String] = [
|
|
||||||
"BitwardenKit.SettingsMenuField",
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: InspectableView
|
// MARK: InspectableView
|
||||||
|
|
||||||
public extension InspectableView {
|
public extension InspectableView {
|
||||||
@ -300,21 +289,6 @@ public extension InspectableView {
|
|||||||
try find(ViewType.SecureField.self, containing: label)
|
try find(ViewType.SecureField.self, containing: label)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Attempts to locate a settings menu field with the provided title.
|
|
||||||
///
|
|
||||||
/// - Parameters:
|
|
||||||
/// - title: The title to use while searching for a menu field.
|
|
||||||
/// - locale: The locale for text extraction.
|
|
||||||
/// - Returns: A `SettingsMenuField`, if one can be located.
|
|
||||||
/// - Throws: Throws an error if a view was unable to be located.
|
|
||||||
///
|
|
||||||
func find(
|
|
||||||
settingsMenuField title: String,
|
|
||||||
locale: Locale = .testsDefault,
|
|
||||||
) throws -> InspectableView<SettingsMenuFieldType> {
|
|
||||||
try find(SettingsMenuFieldType.self, containing: title, locale: locale)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Attempts to locate a slider with the provided accessibility label.
|
/// Attempts to locate a slider with the provided accessibility label.
|
||||||
///
|
///
|
||||||
/// - Parameter accessibilityLabel: The accessibility label to use while searching for a slider.
|
/// - Parameter accessibilityLabel: The accessibility label to use while searching for a slider.
|
||||||
@ -478,15 +452,6 @@ public extension InspectableView where View == BitwardenMenuFieldType {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension InspectableView where View == SettingsMenuFieldType {
|
|
||||||
/// Selects a new value in the menu field.
|
|
||||||
///
|
|
||||||
func select(newValue: any Hashable) throws {
|
|
||||||
let picker = try find(ViewType.Picker.self)
|
|
||||||
try picker.select(value: newValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public extension InspectableView where View == BitwardenStepperType {
|
public extension InspectableView where View == BitwardenStepperType {
|
||||||
/// Decrements the stepper.
|
/// Decrements the stepper.
|
||||||
///
|
///
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user