Skip to content

Commit

Permalink
feat: 🎸 [JIRA:IOSSDKBUG-434] filterFeedBack list mutil select (#869)
Browse files Browse the repository at this point in the history
filterFeedBack list mutil selections shown in section
  • Loading branch information
restaurantt authored Nov 7, 2024
1 parent 317a569 commit 74a403c
Show file tree
Hide file tree
Showing 7 changed files with 153 additions and 45 deletions.
23 changes: 22 additions & 1 deletion Sources/FioriSwiftUICore/DataTypes/SortFilter+DataType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand All @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions Sources/FioriSwiftUICore/Models/ModelDefinitions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
136 changes: 99 additions & 37 deletions Sources/FioriSwiftUICore/Views/SearchListPickerItem+View.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,54 +12,67 @@ 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
self.allowsEmptySelection = allowsEmptySelection
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)
}
}
}
}
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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: ""))
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.";

0 comments on commit 74a403c

Please sign in to comment.