diff --git a/Sources/FioriSwiftUICore/DataTypes/SortFilter+DataType.swift b/Sources/FioriSwiftUICore/DataTypes/SortFilter+DataType.swift index b048151c1..8e35c1f28 100644 --- a/Sources/FioriSwiftUICore/DataTypes/SortFilter+DataType.swift +++ b/Sources/FioriSwiftUICore/DataTypes/SortFilter+DataType.swift @@ -365,6 +365,7 @@ public extension SortFilterItem { public var displayMode: DisplayMode = .automatic /// If seachBar in list display mode is shown. Default is `false`. public var isSearchBarHidden: Bool = false + var disableListEntriesSection: Bool = false /// Available OptionListPicker modes. Use this enum to define picker mode to present. public enum DisplayMode { @@ -390,7 +391,18 @@ public extension SortFilterItem { case nameAndValue } - public init(id: String = UUID().uuidString, name: String, value: [Int], valueOptions: [String], allowsMultipleSelection: Bool, allowsEmptySelection: Bool, barItemDisplayMode: BarItemDisplayMode = .name, isSearchBarHidden: Bool = false, icon: String? = nil, itemLayout: OptionListPickerItemLayoutType = .fixed, displayMode: DisplayMode = .automatic) { + /// Enum for list show entries section + /// The default value is `.default`. + public enum ListEntriesSectionMode { + /// Depend on 'allowsMultipleSelection' + case `default` + /// Enable + case enable + /// Disable + case disable + } + + public init(id: String = UUID().uuidString, name: String, value: [Int], valueOptions: [String], allowsMultipleSelection: Bool, allowsEmptySelection: Bool, barItemDisplayMode: BarItemDisplayMode = .name, isSearchBarHidden: Bool = false, icon: String? = nil, itemLayout: OptionListPickerItemLayoutType = .fixed, displayMode: DisplayMode = .automatic, listEntriesSectionMode: ListEntriesSectionMode = .default) { self.id = id self.name = name self.value = value @@ -404,6 +416,15 @@ public extension SortFilterItem { self.icon = icon self.itemLayout = itemLayout self.displayMode = displayMode + + switch listEntriesSectionMode { + case .default: + self.disableListEntriesSection = allowsMultipleSelection ? false : true + case .disable: + self.disableListEntriesSection = true + case .enable: + self.disableListEntriesSection = false + } } mutating func onTap(option: String) { diff --git a/Sources/FioriSwiftUICore/Models/ModelDefinitions.swift b/Sources/FioriSwiftUICore/Models/ModelDefinitions.swift index 45f8a69b2..180aad8e4 100644 --- a/Sources/FioriSwiftUICore/Models/ModelDefinitions.swift +++ b/Sources/FioriSwiftUICore/Models/ModelDefinitions.swift @@ -536,6 +536,7 @@ public protocol OptionListPickerItemModel: OptionListPickerComponent { // sourcery: virtualPropIsSearchBarHidden = "var isSearchBarHidden: Bool = false" // sourcery: virtualPropPopoverWidth = "let popoverWidth = 393.0" // sourcery: virtualPropKeyboardHeight = "@State var _keyboardHeight: CGFloat = 0.0" +// sourcery: virtualPropDisableListEntriesSection = "var disableListEntriesSection: Bool = false" public protocol SearchListPickerItemModel: OptionListPickerComponent { // sourcery: default.value = nil // sourcery: no_view diff --git a/Sources/FioriSwiftUICore/Views/SearchListPickerItem+View.swift b/Sources/FioriSwiftUICore/Views/SearchListPickerItem+View.swift index 0f6977832..cab732a47 100644 --- a/Sources/FioriSwiftUICore/Views/SearchListPickerItem+View.swift +++ b/Sources/FioriSwiftUICore/Views/SearchListPickerItem+View.swift @@ -12,7 +12,8 @@ public extension SearchListPickerItem { /// - onTap: The closure when tap on item. /// - selectAll: The closure when click 'Select All' button. /// - updateSearchListPickerHeight: The closure to update the parent view. - init(value: Binding<[Int]>, valueOptions: [String] = [], hint: String? = nil, allowsMultipleSelection: Bool, allowsEmptySelection: Bool, isSearchBarHidden: Bool = false, onTap: ((_ index: Int) -> Void)? = nil, selectAll: ((_ isAll: Bool) -> Void)? = nil, updateSearchListPickerHeight: ((CGFloat) -> Void)? = nil) { + /// - disableListEntriesSection: A boolean value to indicate to disable entries section or not. + init(value: Binding<[Int]>, valueOptions: [String] = [], hint: String? = nil, allowsMultipleSelection: Bool, allowsEmptySelection: Bool, isSearchBarHidden: Bool = false, disableListEntriesSection: Bool, onTap: ((_ index: Int) -> Void)? = nil, selectAll: ((_ isAll: Bool) -> Void)? = nil, updateSearchListPickerHeight: ((CGFloat) -> Void)? = nil) { self.init(value: value, valueOptions: valueOptions, hint: hint, onTap: onTap) self.allowsMultipleSelection = allowsMultipleSelection @@ -20,46 +21,58 @@ public extension SearchListPickerItem { self.isSearchBarHidden = isSearchBarHidden self.selectAll = selectAll self.updateSearchListPickerHeight = updateSearchListPickerHeight + self.disableListEntriesSection = disableListEntriesSection } } extension SearchListPickerItem: View { public var body: some View { VStack(spacing: 0) { - if allowsMultipleSelection { - if _value.count != _valueOptions.count || allowsEmptySelection { - self.selectAllView() - } - } else if _value.count == _valueOptions.count { - self.selectAllView() - } - - Divider().edgesIgnoringSafeArea(.all) List { - ForEach(_valueOptions.filter { _searchText.isEmpty || $0.localizedStandardContains(_searchText) }, id: \.self) { item in - let isSelected = self.isItemSelected(item) - HStack { - Text(item) - .lineLimit(1) - .foregroundStyle(Color.preferredColor(.primaryLabel)) - .font(.fiori(forTextStyle: .body, weight: .regular)) - Spacer() - if isSelected { - Image(systemName: "checkmark") - #if !os(visionOS) - .foregroundStyle(Color.preferredColor(.tintColor)) - #else - .foregroundStyle(Color.preferredColor(.primaryLabel)) - #endif + if !disableListEntriesSection, _value.count > 0 { + self.selectionHeader() + + Section { + let selectedOptions = _value.wrappedValue.map { _valueOptions[$0] } + ForEach(selectedOptions.filter { _searchText.isEmpty || $0.localizedStandardContains(_searchText) }, id: \.self) { item in + self.rowView(value: item, isSelected: true) + .padding(0) + .contentShape(Rectangle()) + .onTapGesture { + guard let index = findIndex(of: item) else { + return + } + _onTap?(index) + } } + + Rectangle().fill(Color.preferredColor(.primaryGroupedBackground)) + .frame(height: 30) + .listRowInsets(EdgeInsets()) } - .padding(0) - .contentShape(Rectangle()) - .onTapGesture { - guard let index = findIndex(of: item) else { - return + } + + Section { + if allowsMultipleSelection { + if _value.count != _valueOptions.count || allowsEmptySelection { + self.selectAllView() } - _onTap?(index) + } else if _value.count == _valueOptions.count { + self.selectAllView() + } else { + EmptyView() + } + ForEach(_valueOptions.filter { _searchText.isEmpty || $0.localizedStandardContains(_searchText) }, id: \.self) { item in + let isSelected = self.isItemSelected(item) + self.rowView(value: item, isSelected: isSelected) + .padding(0) + .contentShape(Rectangle()) + .onTapGesture { + guard let index = findIndex(of: item) else { + return + } + _onTap?(index) + } } } } @@ -69,10 +82,10 @@ extension SearchListPickerItem: View { } DispatchQueue.main.async { let popverHeight = Screen.bounds.size.height - StatusBar.height - let totalSpacing: CGFloat = (UIDevice.current.userInterfaceIdiom == .pad ? 8 : 16) * 2 - let totalPadding: CGFloat = (UIDevice.current.userInterfaceIdiom == .pad ? 13 : 16) * 2 + let totalSpacing: CGFloat = (UIDevice.current.userInterfaceIdiom != .phone ? 8 : 16) * 2 + let totalPadding: CGFloat = (UIDevice.current.userInterfaceIdiom != .phone ? 13 : 16) * 2 let safeAreaInset = self.getSafeAreaInsets() - var maxScrollViewHeight = popverHeight - totalSpacing - totalPadding - (self.isSearchBarHidden ? 0 : 52) - 56 - safeAreaInset.top - safeAreaInset.bottom - (UIDevice.current.userInterfaceIdiom == .pad ? 250 : 30) + var maxScrollViewHeight = popverHeight - totalSpacing - totalPadding - (self.isSearchBarHidden ? 0 : 52) - 56 - safeAreaInset.top - safeAreaInset.bottom - (UIDevice.current.userInterfaceIdiom != .phone ? 250 : 30) maxScrollViewHeight -= self._keyboardHeight self._height = min(scrollView.contentSize.height, maxScrollViewHeight) var isSelectAllViewShow = false @@ -90,8 +103,10 @@ extension SearchListPickerItem: View { .frame(minWidth: UIDevice.current.userInterfaceIdiom != .phone ? popoverWidth : nil) .scrollContentBackground(.hidden) .padding(0) + .environment(\.defaultMinListRowHeight, 0) + .environment(\.defaultMinListHeaderHeight, 0) .ifApply(!isSearchBarHidden, content: { v in - v.searchable(text: $_searchText, placement: .automatic) + v.searchable(text: $_searchText, placement: .navigationBarDrawer(displayMode: .always)) .onReceive(NotificationCenter.default.publisher(for: UIApplication.keyboardDidShowNotification)) { notif in let rect = (notif.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect) ?? .zero self._keyboardHeight = rect.height @@ -103,6 +118,43 @@ extension SearchListPickerItem: View { } } + private func rowView(value: String, isSelected: Bool) -> some View { + HStack { + Text(value) + .lineLimit(1) + .foregroundStyle(Color.preferredColor(.primaryLabel)) + .font(.fiori(forTextStyle: .body, weight: .regular)) + Spacer() + if isSelected { + Image(systemName: "checkmark") + #if !os(visionOS) + .foregroundStyle(Color.preferredColor(.tintColor)) + #else + .foregroundStyle(Color.preferredColor(.primaryLabel)) + #endif + } + } + } + + private func selectionHeader() -> some View { + HStack { + Text(NSLocalizedString("Selected", tableName: "FioriSwiftUICore", bundle: Bundle.accessor, comment: "")) + .foregroundStyle(Color.preferredColor(.secondaryLabel)) + .font(.fiori(forTextStyle: .subheadline, weight: .regular)) + Spacer() + } + .padding([.leading, .trailing], UIDevice.current.userInterfaceIdiom != .phone ? 13 : 16) + .padding([.top, .bottom], 8) + .background(Color.preferredColor(.secondaryGroupedBackground)) + .listRowInsets(EdgeInsets()) + .alignmentGuide(.listRowSeparatorLeading) { dimensions in + dimensions[.leading] + } + .alignmentGuide(.listRowSeparatorTrailing) { dimensions in + dimensions[.trailing] + } + } + private func selectAllView() -> some View { HStack { Text(NSLocalizedString("All", tableName: "FioriSwiftUICore", bundle: Bundle.accessor, comment: "")) @@ -113,10 +165,20 @@ extension SearchListPickerItem: View { selectAll?(_value.count != _valueOptions.count) }) { Text(_value.count == _valueOptions.count ? NSLocalizedString("Deselect All", tableName: "FioriSwiftUICore", bundle: Bundle.accessor, comment: "") : NSLocalizedString("Select All", tableName: "FioriSwiftUICore", bundle: Bundle.accessor, comment: "")) - } + .foregroundStyle(Color.preferredColor(.tintColor)) + .font(.fiori(forTextStyle: .subheadline, weight: .regular)) + }.buttonStyle(PlainButtonStyle()) } - .padding([.leading, .trailing], UIDevice.current.userInterfaceIdiom == .pad ? 13 : 16) + .padding([.leading, .trailing], UIDevice.current.userInterfaceIdiom != .phone ? 13 : 16) .padding([.top, .bottom], 8) + .background(Color.preferredColor(.secondaryGroupedBackground)) + .listRowInsets(EdgeInsets()) + .alignmentGuide(.listRowSeparatorLeading) { dimensions in + dimensions[.leading] + } + .alignmentGuide(.listRowSeparatorTrailing) { dimensions in + dimensions[.trailing] + } } private func isItemSelected(_ item: String) -> Bool { diff --git a/Sources/FioriSwiftUICore/Views/SortFilter/FilterFeedbackBarItem+View.swift b/Sources/FioriSwiftUICore/Views/SortFilter/FilterFeedbackBarItem+View.swift index 90e2acb03..55b848b42 100644 --- a/Sources/FioriSwiftUICore/Views/SortFilter/FilterFeedbackBarItem+View.swift +++ b/Sources/FioriSwiftUICore/Views/SortFilter/FilterFeedbackBarItem+View.swift @@ -249,7 +249,7 @@ struct PickerMenuItem: View { }) .buttonStyle(ApplyButtonStyle()) } components: { - SearchListPickerItem(value: self.$item.workingValue, valueOptions: self.item.valueOptions, hint: nil, allowsMultipleSelection: self.item.allowsMultipleSelection, allowsEmptySelection: self.item.allowsEmptySelection, isSearchBarHidden: self.item.isSearchBarHidden) { index in + SearchListPickerItem(value: self.$item.workingValue, valueOptions: self.item.valueOptions, hint: nil, allowsMultipleSelection: self.item.allowsMultipleSelection, allowsEmptySelection: self.item.allowsEmptySelection, isSearchBarHidden: self.item.isSearchBarHidden, disableListEntriesSection: self.item.disableListEntriesSection) { index in self.item.onTap(option: self.item.valueOptions[index]) } selectAll: { isAll in self.item.selectAll(isAll) diff --git a/Sources/FioriSwiftUICore/Views/SortFilter/_SortFilterCFGItemContainer.swift b/Sources/FioriSwiftUICore/Views/SortFilter/_SortFilterCFGItemContainer.swift index 5075b06d8..76b71a844 100644 --- a/Sources/FioriSwiftUICore/Views/SortFilter/_SortFilterCFGItemContainer.swift +++ b/Sources/FioriSwiftUICore/Views/SortFilter/_SortFilterCFGItemContainer.swift @@ -147,7 +147,8 @@ extension _SortFilterCFGItemContainer: View { valueOptions: self._items[r][c].picker.valueOptions, allowsMultipleSelection: self._items[r][c].picker.allowsMultipleSelection, allowsEmptySelection: self._items[r][c].picker.allowsEmptySelection, - isSearchBarHidden: self._items[r][c].picker.isSearchBarHidden + isSearchBarHidden: self._items[r][c].picker.isSearchBarHidden, + disableListEntriesSection: self._items[r][c].picker.disableListEntriesSection ) { index in self._items[r][c].picker.onTap(option: self._items[r][c].picker.valueOptions[index]) } selectAll: { isAll in diff --git a/Sources/FioriSwiftUICore/_generated/ViewModels/API/SearchListPickerItem+API.generated.swift b/Sources/FioriSwiftUICore/_generated/ViewModels/API/SearchListPickerItem+API.generated.swift index e29c8b060..2c5993c6f 100644 --- a/Sources/FioriSwiftUICore/_generated/ViewModels/API/SearchListPickerItem+API.generated.swift +++ b/Sources/FioriSwiftUICore/_generated/ViewModels/API/SearchListPickerItem+API.generated.swift @@ -10,17 +10,18 @@ public struct SearchListPickerItem { var _hint: String? = nil var _onTap: ((_ index: Int) -> Void)? = nil + var disableListEntriesSection: Bool = false var updateSearchListPickerHeight: ((CGFloat) -> ())? = nil + @State var _height: CGFloat = 44 + var allowsEmptySelection: Bool = false var allowsMultipleSelection: Bool = false var isSearchBarHidden: Bool = false - var selectAll: ((Bool) -> ())? = nil - @State var _height: CGFloat = 44 - @State var _searchViewCornerRadius: CGFloat = 18 @State var _searchText: String = "" - var allowsEmptySelection: Bool = false let popoverWidth = 393.0 + var selectAll: ((Bool) -> ())? = nil @State var _keyboardHeight: CGFloat = 0.0 - + @State var _searchViewCornerRadius: CGFloat = 18 + public init(model: SearchListPickerItemModel) { self.init(value: Binding<[Int]>(get: { model.value }, set: { model.value = $0 }), valueOptions: model.valueOptions, hint: model.hint, onTap: model.onTap) } diff --git a/Sources/FioriSwiftUICore/_localization/en.lproj/FioriSwiftUICore.strings b/Sources/FioriSwiftUICore/_localization/en.lproj/FioriSwiftUICore.strings index 782701595..3ccf9b250 100644 --- a/Sources/FioriSwiftUICore/_localization/en.lproj/FioriSwiftUICore.strings +++ b/Sources/FioriSwiftUICore/_localization/en.lproj/FioriSwiftUICore.strings @@ -255,7 +255,29 @@ "Select All" = "Select All"; /* XBUT: The \"Deselect All\" button title on the List Picker header */ "Deselect All" = "Deselect All"; + /* XBUT: Reset action of filter feedback bar, see https://experience.sap.com/fiori-design-ios/article/filter-feedback-bar/#behavior-and-interaction */ "Reset" = "Reset"; /* XBUT: Apply action of filter feedback bar, see https://experience.sap.com/fiori-design-ios/article/filter-feedback-bar/#behavior-and-interaction */ "Apply" = "Apply"; + +/* XBUT: The \"Selected\" title on the List Picker header */ +"Selected" = "Selected"; + +/* XACT: The accessibility hint for Text Input Views, editable */ +"Text field, Double tap to edit" = "Text field, Double tap to edit"; + +/* XACT: The accessibility hint for Text Input Views, editing */ +"Text field, is editing" = "Text field, is editing"; + +/* XACT: The accessibility hint for Text Input Views, editing, with clear button */ +"Text field, is editing. Double tap to clear text" = "Text field, is editing. Double tap to clear text"; + +/* XACT: The default accessibility label for the custom action in TextFieldFormView if none was specified */ +"Custom Action" = "Custom Action"; + +/* XBUT: Error messages shown to users in DocumentScannerView */ +"Failed to create PDF page from images" = "Failed to create PDF page from images."; + +/* XBUT: Cancel message shown to users in DocumentScannerView */ +"User cancelled the scan" = "User cancelled the scan.";