From 90ea3e6f79c837fdaa22e000c8185e9f5638a99b Mon Sep 17 00:00:00 2001 From: RachelRadford21 Date: Mon, 24 Jun 2024 15:14:19 -0400 Subject: [PATCH 01/20] fix colors for pill kit --- .../Color.xcassets/Status/Neutral.colorset/Contents.json | 6 +++--- .../Color.xcassets/Status/Warning.colorset/Contents.json | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/Playbook/Design Elements/Colors/Color.xcassets/Status/Neutral.colorset/Contents.json b/Sources/Playbook/Design Elements/Colors/Color.xcassets/Status/Neutral.colorset/Contents.json index 7fee7e0f2..bb6df9bb5 100644 --- a/Sources/Playbook/Design Elements/Colors/Color.xcassets/Status/Neutral.colorset/Contents.json +++ b/Sources/Playbook/Design Elements/Colors/Color.xcassets/Status/Neutral.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "214", - "green" : "205", - "red" : "193" + "blue" : "135", + "green" : "120", + "red" : "104" } }, "idiom" : "universal" diff --git a/Sources/Playbook/Design Elements/Colors/Color.xcassets/Status/Warning.colorset/Contents.json b/Sources/Playbook/Design Elements/Colors/Color.xcassets/Status/Warning.colorset/Contents.json index d2de33ecc..d3537b960 100644 --- a/Sources/Playbook/Design Elements/Colors/Color.xcassets/Status/Warning.colorset/Contents.json +++ b/Sources/Playbook/Design Elements/Colors/Color.xcassets/Status/Warning.colorset/Contents.json @@ -6,8 +6,8 @@ "components" : { "alpha" : "1.000", "blue" : "0x00", - "green" : "0xBB", - "red" : "0xF9" + "green" : "0x95", + "red" : "0xC6" } }, "idiom" : "universal" From 119e5a1a9cdf2198a6ac6e0f934d10c041360900 Mon Sep 17 00:00:00 2001 From: RachelRadford21 Date: Wed, 2 Oct 2024 10:19:54 -0400 Subject: [PATCH 02/20] add no options to dropdown --- .../Components/Typeahead/PBTypeahead.swift | 239 +++++++++--------- 1 file changed, 125 insertions(+), 114 deletions(-) diff --git a/Sources/Playbook/Components/Typeahead/PBTypeahead.swift b/Sources/Playbook/Components/Typeahead/PBTypeahead.swift index ad10be9e1..e9e1ab26f 100644 --- a/Sources/Playbook/Components/Typeahead/PBTypeahead.swift +++ b/Sources/Playbook/Components/Typeahead/PBTypeahead.swift @@ -59,134 +59,145 @@ public struct PBTypeahead: View { self.listOffset = listOffset self._isFocused = isFocused self.clearAction = clearAction - self.onSelection = onSelection + self.onSelection = onSelection } - - public var body: some View { - VStack(alignment: .leading, spacing: Spacing.xSmall) { - Text(title).pbFont(.caption) - .padding(.bottom, Spacing.xxSmall) - GridInputField( - placeholder: placeholder, - searchText: $searchText, - selection: optionsSelected, - isFocused: $isFocused, - clearAction: { clear }, - onItemTap: { removeSelected($0) }, - onViewTap: { onViewTap } - ) - .sizeReader { contentSize = $0 } - .pbPopover( - isPresented: $showList, - id: id, - position: .bottom(listOffset.x, listOffset.y), - variant: .dropdown, - refreshView: $isHovering - ) { - listView - } - .onTapGesture { - isFocused = false - } - } - .onChange(of: options.count) { _ in - listOptions = options - } - .onAppear { - focused = isFocused - listOptions = options - if debounce.numberOfCharacters == 0 { - showList = isFocused - } - setKeyboardControls - } - .onChange(of: isFocused) { newValue in - Timer.scheduledTimer(withTimeInterval: 0.03, repeats: false) { _ in - showList = newValue - } - } - .onChange(of: searchText, debounce: debounce) { _ in - _ = searchResults - reloadList - } - .onChange(of: listOptions.count) { _ in - reloadList - } - .onChange(of: contentSize) { _ in - reloadList - } - .onChange(of: hoveringIndex) { index in - reloadList - } - .onChange(of: searchText, debounce: debounce) { _ in - if !searchText.isEmpty { - showList = true - } - } + + public var body: some View { + VStack(alignment: .leading, spacing: Spacing.xSmall) { + Text(title).pbFont(.caption) + .padding(.bottom, Spacing.xxSmall) + GridInputField( + placeholder: placeholder, + searchText: $searchText, + selection: optionsSelected, + isFocused: $isFocused, + clearAction: { clear }, + onItemTap: { removeSelected($0) }, + onViewTap: { onViewTap } + ) + .sizeReader { contentSize = $0 } + .pbPopover( + isPresented: $showList, + id: id, + position: .bottom(listOffset.x, listOffset.y), + variant: .dropdown, + refreshView: $isHovering + ) { + listView + } + .onTapGesture { + isFocused = false + } + } + .onChange(of: options.count) { _ in + listOptions = options + } + .onAppear { + focused = isFocused + listOptions = options + if debounce.numberOfCharacters == 0 { + showList = isFocused + } + setKeyboardControls + } + .onChange(of: isFocused) { newValue in + Timer.scheduledTimer(withTimeInterval: 0.03, repeats: false) { _ in + showList = newValue + } } + .onChange(of: searchText, debounce: debounce) { _ in + _ = searchResults + reloadList + } + .onChange(of: listOptions.count) { _ in + reloadList + } + .onChange(of: contentSize) { _ in + reloadList + } + .onChange(of: hoveringIndex) { index in + reloadList + } + .onChange(of: searchText, debounce: debounce) { _ in + if !searchText.isEmpty { + showList = true + } + } + } } @MainActor private extension PBTypeahead { - @ViewBuilder - var listView: some View { - PBCard(alignment: .leading, padding: Spacing.none, shadow: .deeper) { - ScrollView { - VStack(spacing: 0) { - ForEach(Array(zip(searchResults.indices, searchResults)), id: \.0) { index, result in - HStack { - if let customView = result.1?.1?() { - customView - } else { - Text(result.1?.0 ?? result.0) - .pbFont(.body, color: listTextolor(index)) - } - } - .padding(.horizontal, Spacing.xSmall + 4) - .padding(.vertical, Spacing.xSmall + 4) - .frame(maxWidth: .infinity, alignment: .leading) - .background(listBackgroundColor(index)) - .onHover(disabled: false) { hover in - isHovering = hover - hoveringIndex = index - hoveringOption = result - } - .onTapGesture { - onListSelection(index: index, option: result) - } - } + @ViewBuilder + var listView: some View { + PBCard(alignment: .leading, padding: Spacing.none, shadow: .deeper) { + ScrollView { + VStack(spacing: 0) { + ForEach(Array(zip(searchResults.indices, searchResults)), id: \.0) { index, result in + HStack { + if result.0 == "No Options" { + Spacer() + Text("No Options") + .pbFont(.body, color: .text(.light)) + Spacer() + } else { + if let customView = result.1?.1?() { + customView + } else { + Text(result.1?.0 ?? result.0) + .pbFont(.body, color: listTextolor(index)) } + } } - .scrollDismissesKeyboard(.immediately) - .frame(maxHeight: dropdownMaxHeight) - .fixedSize(horizontal: false, vertical: true) - } - .frame(maxWidth: .infinity, alignment: .top) - .transition(.opacity) - } - - var searchResults: [Option] { - switch selection{ - case .multiple: - return searchText.isEmpty && debounce.numberOfCharacters == 0 ? listOptions : listOptions.filter { - if let text = $0.1?.0 { - text.localizedCaseInsensitiveContains(searchText) - } else { - $0.0.localizedCaseInsensitiveContains(searchText) + .padding(.horizontal, Spacing.xSmall + 4) + .padding(.vertical, Spacing.xSmall + 4) + .frame(maxWidth: .infinity, alignment: .leading) + .background(listBackgroundColor(index)) + .onHover(disabled: false) { hover in + isHovering = hover + hoveringIndex = index + hoveringOption = result } - } - case .single: - return searchText.isEmpty && debounce.numberOfCharacters == 0 ? options : options.filter { - if let text = $0.1?.0 { - text.localizedCaseInsensitiveContains(searchText) - } else { - $0.0.localizedCaseInsensitiveContains(searchText) + .onTapGesture { + if result.0 != "No Options" { + onListSelection(index: index, option: result) + } } } + } + } + .scrollDismissesKeyboard(.immediately) + .frame(maxHeight: dropdownMaxHeight) + .fixedSize(horizontal: false, vertical: true) + } + .frame(maxWidth: .infinity, alignment: .top) + .transition(.opacity) + } + + var searchResults: [Option] { + let results: [Option] + switch selection { + case .multiple: + results = searchText.isEmpty && debounce.numberOfCharacters == 0 ? listOptions : listOptions.filter { + if let text = $0.1?.0 { + return text.localizedCaseInsensitiveContains(searchText) + } else { + return $0.0.localizedCaseInsensitiveContains(searchText) + } + } + case .single: + results = searchText.isEmpty && debounce.numberOfCharacters == 0 ? options : options.filter { + if let text = $0.1?.0 { + return text.localizedCaseInsensitiveContains(searchText) + } else { + return $0.0.localizedCaseInsensitiveContains(searchText) + } } } + return results.isEmpty ? [("No Options", nil)] : results + } - var optionsSelected: GridInputField.Selection { + var optionsSelected: GridInputField.Selection { let optionsSelected = selectedOptions.map { value in if let content = value.1 { return content.0 From f63e123fade44c64376b33c6380a2cd78ae02a33 Mon Sep 17 00:00:00 2001 From: RachelRadford21 Date: Tue, 8 Oct 2024 15:17:12 -0400 Subject: [PATCH 03/20] commit to allow user no options text --- .../Components/Typeahead/PBTypeahead.swift | 65 ++++++++++--------- 1 file changed, 34 insertions(+), 31 deletions(-) diff --git a/Sources/Playbook/Components/Typeahead/PBTypeahead.swift b/Sources/Playbook/Components/Typeahead/PBTypeahead.swift index e9e1ab26f..e17895895 100644 --- a/Sources/Playbook/Components/Typeahead/PBTypeahead.swift +++ b/Sources/Playbook/Components/Typeahead/PBTypeahead.swift @@ -21,6 +21,7 @@ public struct PBTypeahead: View { private let popoverManager = PopoverManager() private let onSelection: (([Option]) -> Void)? private let clearAction: (() -> Void)? + let noOptionsText: String @State private var listOptions: [Option] = [] @State private var showList: Bool = false @State private var hoveringIndex: Int? @@ -31,36 +32,38 @@ public struct PBTypeahead: View { @State private var selectedOptions: [Option] = [] @State private var focused: Bool = false @Binding var options: [Option] - @Binding var searchText: String - @FocusState.Binding private var isFocused: Bool - - public init( - id: Int, - title: String, - placeholder: String = "Select", - searchText: Binding, - options: Binding<[Option]>, - selection: Selection, - debounce: (time: TimeInterval, numberOfCharacters: Int) = (0, 0), - dropdownMaxHeight: CGFloat? = nil, - listOffset: (x: CGFloat, y: CGFloat) = (0, 0), - isFocused: FocusState.Binding, - onSelection: @escaping (([Option]) -> Void), - clearAction: (() -> Void)? = nil - ) { - self.id = id - self.title = title - self.placeholder = placeholder - self._searchText = searchText - self.selection = selection - self._options = options - self.debounce = debounce - self.dropdownMaxHeight = dropdownMaxHeight - self.listOffset = listOffset - self._isFocused = isFocused - self.clearAction = clearAction - self.onSelection = onSelection - } + @Binding var searchText: String + @FocusState.Binding private var isFocused: Bool + + public init( + id: Int, + title: String, + placeholder: String = "Select", + searchText: Binding, + options: Binding<[Option]>, + selection: Selection, + debounce: (time: TimeInterval, numberOfCharacters: Int) = (0, 0), + dropdownMaxHeight: CGFloat? = nil, + listOffset: (x: CGFloat, y: CGFloat) = (0, 0), + isFocused: FocusState.Binding, + onSelection: @escaping (([Option]) -> Void), + clearAction: (() -> Void)? = nil, + noOptionsText: String = "No options" + ) { + self.id = id + self.title = title + self.placeholder = placeholder + self._searchText = searchText + self.selection = selection + self._options = options + self.debounce = debounce + self.dropdownMaxHeight = dropdownMaxHeight + self.listOffset = listOffset + self._isFocused = isFocused + self.clearAction = clearAction + self.noOptionsText = noOptionsText + self.onSelection = onSelection + } public var body: some View { VStack(alignment: .leading, spacing: Spacing.xSmall) { @@ -137,7 +140,7 @@ private extension PBTypeahead { HStack { if result.0 == "No Options" { Spacer() - Text("No Options") + Text(noOptionsText) .pbFont(.body, color: .text(.light)) Spacer() } else { From 9daaed47ecaf3ad79ba30557243b882640d7c21b Mon Sep 17 00:00:00 2001 From: "isis.silva" Date: Wed, 9 Oct 2024 16:44:15 -0400 Subject: [PATCH 04/20] no message --- .../Components/Typeahead/PBTypeahead.swift | 18 +- .../Typeahead/PBTypeaheadTemplate.swift | 397 ++++++++++++++++++ .../Typeahead/TypeaheadCatalog.swift | 29 +- .../Resources/Helper Files/Mocks.swift | 8 +- 4 files changed, 442 insertions(+), 10 deletions(-) create mode 100644 Sources/Playbook/Components/Typeahead/PBTypeaheadTemplate.swift diff --git a/Sources/Playbook/Components/Typeahead/PBTypeahead.swift b/Sources/Playbook/Components/Typeahead/PBTypeahead.swift index ad10be9e1..f34d6ecaa 100644 --- a/Sources/Playbook/Components/Typeahead/PBTypeahead.swift +++ b/Sources/Playbook/Components/Typeahead/PBTypeahead.swift @@ -23,6 +23,7 @@ public struct PBTypeahead: View { private let clearAction: (() -> Void)? @State private var listOptions: [Option] = [] @State private var showList: Bool = false + @State private var isCollapsed = false @State private var hoveringIndex: Int? @State private var hoveringOption: Option? @State private var isHovering: Bool = false @@ -162,7 +163,6 @@ private extension PBTypeahead { .fixedSize(horizontal: false, vertical: true) } .frame(maxWidth: .infinity, alignment: .top) - .transition(.opacity) } var searchResults: [Option] { @@ -270,9 +270,6 @@ private extension PBTypeahead { var reloadList: Void { if showList { isHovering.toggle() - Timer.scheduledTimer(withTimeInterval: 0.001, repeats: false) { _ in - isHovering.toggle() - } } } @@ -355,3 +352,16 @@ public extension PBTypeahead { registerFonts() return TypeaheadCatalog() } + +struct AnimatingCellHeight: AnimatableModifier { + var height: CGFloat? = 0 + + var animatableData: CGFloat? { + get { height } + set { height = newValue } + } + + func body(content: Content) -> some View { + content.frame(height: height) + } +} diff --git a/Sources/Playbook/Components/Typeahead/PBTypeaheadTemplate.swift b/Sources/Playbook/Components/Typeahead/PBTypeaheadTemplate.swift new file mode 100644 index 000000000..071a9a813 --- /dev/null +++ b/Sources/Playbook/Components/Typeahead/PBTypeaheadTemplate.swift @@ -0,0 +1,397 @@ +// +// Playbook Swift Design System +// +// Copyright © 2024 Power Home Remodeling Group +// This software is distributed under the ISC License +// +// PBTypeaheadTemplate.swift +// + +import SwiftUI + +public struct PBTypeaheadTemplate: View { + public enum OptionType: Identifiable { + public var id: String { + switch self { + case .section(let str): + return str + case .item(let item): + return item.0 + } + } + case section(String) + case item(PBTypeaheadTemplate.Option) + } + + public typealias Option = (String, (String, (() -> Content?)?)?) + private let id: Int + private let title: String + private let placeholder: String + private let selection: Selection + private let debounce: (time: TimeInterval, numberOfCharacters: Int) + private let dropdownMaxHeight: CGFloat? + private let listOffset: (x: CGFloat, y: CGFloat) + private let popoverManager = PopoverManager() + private let onSelection: (([OptionType]) -> Void)? + private let clearAction: (() -> Void)? + @State private var listOptions: [OptionType] = [] + @State private var showList: Bool = false + @State private var isCollapsed = false + @State private var hoveringIndex: Int? + @State private var hoveringOption: Option? + @State private var isHovering: Bool = false + @State private var contentSize: CGSize = .zero + @State private var selectedIndex: Int? + @State private var selectedOptions: [OptionType] = [] + @State private var focused: Bool = false + @Binding var options: [OptionType] + @Binding var searchText: String + @FocusState.Binding private var isFocused: Bool + + public init( + id: Int, + title: String, + placeholder: String = "Select", + searchText: Binding, + options: Binding<[OptionType]>, + selection: Selection, + debounce: (time: TimeInterval, numberOfCharacters: Int) = (0, 0), + dropdownMaxHeight: CGFloat? = nil, + listOffset: (x: CGFloat, y: CGFloat) = (0, 0), + isFocused: FocusState.Binding, + onSelection: @escaping (([OptionType]) -> Void), + clearAction: (() -> Void)? = nil + ) { + self.id = id + self.title = title + self.placeholder = placeholder + self._searchText = searchText + self.selection = selection + self._options = options + self.debounce = debounce + self.dropdownMaxHeight = dropdownMaxHeight + self.listOffset = listOffset + self._isFocused = isFocused + self.clearAction = clearAction + self.onSelection = onSelection + } + + public var body: some View { + VStack(alignment: .leading, spacing: Spacing.xSmall) { + Text(title).pbFont(.caption) + .padding(.bottom, Spacing.xxSmall) + GridInputField( + placeholder: placeholder, + searchText: $searchText, + selection: optionsSelected, + isFocused: $isFocused, + clearAction: { clear }, + onItemTap: { removeSelected($0) }, + onViewTap: { onViewTap } + ) + .sizeReader { contentSize = $0 } + .pbPopover( + isPresented: $showList, + id: id, + position: .bottom(listOffset.x, listOffset.y), + variant: .dropdown, + refreshView: $isHovering + ) { + listView + } + .onTapGesture { + isFocused = false + } + } + .onChange(of: options.count) { _ in + listOptions = options + } + .onAppear { + focused = isFocused + listOptions = options + if debounce.numberOfCharacters == 0 { + showList = isFocused + } + setKeyboardControls + } + .onChange(of: isFocused) { newValue in + Timer.scheduledTimer(withTimeInterval: 0.03, repeats: false) { _ in + showList = newValue + } + } + .onChange(of: searchText, debounce: debounce) { _ in + _ = searchResults + reloadList + } + .onChange(of: listOptions.count) { _ in + reloadList + } + .onChange(of: contentSize) { _ in + reloadList + } + .onChange(of: hoveringIndex) { index in + reloadList + } + .onChange(of: searchText, debounce: debounce) { _ in + if !searchText.isEmpty { + showList = true + } + } + } +} + +@MainActor +private extension PBTypeaheadTemplate { + func listItemView(item: Option, index: Int, optionType: OptionType) -> some View { + HStack { + if let customView = item.1?.1?() { + customView + } else { + Text(item.1?.0 ?? item.0) + .pbFont(.body, color: listTextolor(index)) + } + } + .padding(.horizontal, Spacing.xSmall + 4) + .padding(.vertical, Spacing.xSmall + 4) + .frame(maxWidth: .infinity, alignment: .leading) + .background(listBackgroundColor(index)) + .onHover(disabled: false) { hover in + isHovering = hover + hoveringIndex = index + hoveringOption = item + } + .onTapGesture { + onListSelection(index: index, option: optionType) + } + } + + @ViewBuilder + var listView: some View { + PBCard(alignment: .leading, padding: Spacing.none, shadow: .deeper) { + ScrollView { + VStack(alignment: .leading, spacing: 0) { + ForEach(Array(zip(searchResults.indices, searchResults)), id: \.0) { index, result in + switch result { + case .section(let section): + Text(section).pbFont(.caption) + .padding(.top) + .padding(.leading) + case .item(let item): listItemView(item: item, index: index, optionType: result) + } + } + } + } + .scrollDismissesKeyboard(.immediately) + .frame(maxHeight: dropdownMaxHeight) + .fixedSize(horizontal: false, vertical: true) + } + .frame(maxWidth: .infinity, alignment: .top) + } + + var searchResults: [OptionType] { + switch selection{ + case .multiple: + let selectedIds = Set(selectedOptions.map { $0.id }) + let arr = searchText.isEmpty && debounce.numberOfCharacters == 0 ? listOptions : listOptions.filter { + switch $0 { + case .item(let item): + if let text = item.1?.0 { + return text.localizedCaseInsensitiveContains(searchText) + } else { + return item.0.localizedCaseInsensitiveContains(searchText) + } + case .section(_): + return true + } + } + let arr2 = arr.filter { !selectedIds.contains($0.id) } + print("@@@ >>> arr1 \(arr.count), arr2 \(arr2.count)") + return arr2 + + + case .single: + return searchText.isEmpty && debounce.numberOfCharacters == 0 ? options : options.filter { + switch $0 { + case .item(let item): + if let text = item.1?.0 { + return text.localizedCaseInsensitiveContains(searchText) + } else { + return item.0.localizedCaseInsensitiveContains(searchText) + } + case .section(_): + return true + } + } + } + } + + var optionsSelected: GridInputField.Selection { + let optionsSelected = selectedOptions.map { value in + switch value { + case .item(let item): + if let content = item.1 { + return content.0 + } else { + return item.0 + } + default: return "" + } + } + return selection.selectedOptions(options: optionsSelected, placeholder: placeholder) + } + + var clear: Void { + if let action = clearAction { + clearText + action() + } else { + clearText + } + } + + var clearText: Void { + searchText = "" + selectedOptions.removeAll() + onSelection?([]) + listOptions = options + selectedIndex = nil + hoveringIndex = nil + showList = false + } + + var setKeyboardControls: Void { + #if os(macOS) + NSEvent.addLocalMonitorForEvents(matching: .keyDown) { event in + if event.keyCode == 48 { // tab + focused = true + } + if event.keyCode == 36 { // return bar + if let index = hoveringIndex, index <= searchResults.count-1, showList { + onListSelection(index: index, option: searchResults[index]) + } + } + if event.keyCode == 49 { // space + if isFocused { + if let index = hoveringIndex, index <= searchResults.count-1, showList, searchText.isEmpty { + onListSelection(index: index, option: searchResults[index]) + } else { + showList = true + } + } + } + if event.keyCode == 51 { // delete + if let lastElementIndex = selectedOptions.indices.last, isFocused, searchText.isEmpty, !selectedOptions.isEmpty { + removeSelected(lastElementIndex) + } + } + if event.keyCode == 125 { // arrow down + if isFocused { + if let index = hoveringIndex { + hoveringIndex = index < searchResults.count ? (index + 1) : 0 + } else { + hoveringIndex = 0 + } + } + } + else { + if event.keyCode == 126 { // arrow up + if isFocused, let index = hoveringIndex { + hoveringIndex = index > 1 ? (index - 1) : 0 + } + } + } + return event + } + #endif + } + + var onViewTap: Void { + showList.toggle() + isFocused = true + } + + var reloadList: Void { + if showList { + isHovering.toggle() + } + } + + func onListSelection(index: Int, option: OptionType) { + if showList { + switch selection { + case .single: + onSingleSelection(index: index, option) + case .multiple: + onMultipleSelection(option) + } + } + showList = false + searchText = "" + } + + func onSingleSelection(index: Int, _ option: OptionType) { + selectedOptions.removeAll() + selectedOptions.append(option) + selectedIndex = index + hoveringIndex = index + onSelection?(selectedOptions) + } + + func onMultipleSelection(_ option: OptionType) { + selectedOptions.append(option) + onSelection?(selectedOptions) +// listOptions.removeAll(where: { $0.id == option.id }) + hoveringIndex = nil + selectedIndex = nil + } + + func removeSelected(_ index: Int) { + if let selectedElementIndex = selectedOptions.indices.first(where: { $0 == index }) { + let selectedElement = selectedOptions.remove(at: selectedElementIndex) + onSelection?(selectedOptions) +// listOptions.append(selectedElement) + selectedIndex = nil + } + } + + func listBackgroundColor(_ index: Int?) -> Color { + switch selection { + case .single: + if selectedIndex != nil, selectedIndex == index { + return .pbPrimary + } + default: break + } +#if os(macOS) + return hoveringIndex == index ? .hover : .card +#elseif os(iOS) + return .card +#endif + } + + func listTextolor(_ index: Int?) -> Color { + if selectedIndex != nil, selectedIndex == index { + return .white + } else { + return .text(.default) + } + } +} + +public extension PBTypeaheadTemplate { + enum Selection { + case single, multiple(variant: GridInputField.Selection.Variant) + + func selectedOptions(options: [String], placeholder: String) -> GridInputField.Selection { + switch self { + case .single: return .single(options.first) + case .multiple(let variant): return .multiple(variant, options) + } + } + } +} + +//#Preview { +// registerFonts() +// return TypeaheadCatalogTemplate() +//} diff --git a/Sources/Playbook/Components/Typeahead/TypeaheadCatalog.swift b/Sources/Playbook/Components/Typeahead/TypeaheadCatalog.swift index 715deb2e7..9f0023cce 100644 --- a/Sources/Playbook/Components/Typeahead/TypeaheadCatalog.swift +++ b/Sources/Playbook/Components/Typeahead/TypeaheadCatalog.swift @@ -15,6 +15,7 @@ public struct TypeaheadCatalog: View { @State private var selectedUsers: [(String, (String, (() -> PBUser?)?)?)] = [] @State private var searchTextUsers: String = "" @State private var searchTextColors: String = "" + @State private var searchTextSections: String = "" @State private var searchText: String = "" @State private var searchTextDebounce: String = "" @State private var didTapOutside: Bool? = false @@ -24,6 +25,15 @@ public struct TypeaheadCatalog: View { @State private var assetsUser = Mocks.multipleUsersDictionary @FocusState var isFocused1 @FocusState var isFocused2 + @FocusState var isFocused3 + @State private var sectionUsers: [PBTypeaheadTemplate.OptionType] = [ + .section("section 1"), + .item(("1", (Mocks.andrew.name, { Mocks.andrew }))), + .item(("2", (Mocks.ana.name, { Mocks.ana }))), + .section("section 2"), + .item(("3", (Mocks.patric.name, { Mocks.patric }))), + .item(("4", (Mocks.luccile.name, { Mocks.luccile }))) + ] var popoverManager = PopoverManager() @@ -34,13 +44,18 @@ public struct TypeaheadCatalog: View { #if os(macOS) PBDoc(title: "Dialog") { dialog } #endif + PBDoc(title: "Sections", spacing: Spacing.small) { sections } + .padding(.bottom, 500) + } .onTapGesture { isFocused1 = false isFocused2 = false + isFocused3 = false } .popoverHandler(id: 1) .popoverHandler(id: 2) + .popoverHandler(id: 3) } } @@ -69,9 +84,19 @@ extension TypeaheadCatalog { selectedUsers = selected print(selected) } - } - + + var sections: some View { + PBTypeaheadTemplate( + id: 3, + title: "Sections", + searchText: $searchTextSections, + options: $sectionUsers, + selection: .multiple(variant: .pill), + isFocused: $isFocused3 + ) { _ in } + } + func closeToast() { presentDialog = false } diff --git a/Sources/Playbook/Resources/Helper Files/Mocks.swift b/Sources/Playbook/Resources/Helper Files/Mocks.swift index d726f43fa..74b948aaa 100644 --- a/Sources/Playbook/Resources/Helper Files/Mocks.swift +++ b/Sources/Playbook/Resources/Helper Files/Mocks.swift @@ -36,12 +36,12 @@ enum Mocks { static let assetsColors: [(String, (String, (() -> AnyView?)?)?)] = [ ("Orange", nil), ("Red", nil), - ("Green", nil), +// ("Green", nil), ("Blue", nil), ("Pink", nil), - ("Yellow", nil), - ("Violet", nil), - ("Indigo", nil), +// ("Yellow", nil), +// ("Violet", nil), +// ("Indigo", nil), ("Magenta", nil) ] From 5e57925a33d6e4ea885fd4115b7393d2a28dc62b Mon Sep 17 00:00:00 2001 From: "isis.silva" Date: Tue, 15 Oct 2024 09:17:05 -0400 Subject: [PATCH 05/20] converted data --- .../Components/Typeahead/PBTypeahead.swift | 37 ++--- .../Typeahead/PBTypeaheadTemplate.swift | 143 +++++++++++------- .../Typeahead/TypeaheadCatalog.swift | 4 + .../Resources/Helper Files/Mocks.swift | 6 +- 4 files changed, 111 insertions(+), 79 deletions(-) diff --git a/Sources/Playbook/Components/Typeahead/PBTypeahead.swift b/Sources/Playbook/Components/Typeahead/PBTypeahead.swift index f34d6ecaa..b9e9041ea 100644 --- a/Sources/Playbook/Components/Typeahead/PBTypeahead.swift +++ b/Sources/Playbook/Components/Typeahead/PBTypeahead.swift @@ -21,7 +21,6 @@ public struct PBTypeahead: View { private let popoverManager = PopoverManager() private let onSelection: (([Option]) -> Void)? private let clearAction: (() -> Void)? - @State private var listOptions: [Option] = [] @State private var showList: Bool = false @State private var isCollapsed = false @State private var hoveringIndex: Int? @@ -90,12 +89,8 @@ public struct PBTypeahead: View { isFocused = false } } - .onChange(of: options.count) { _ in - listOptions = options - } .onAppear { focused = isFocused - listOptions = options if debounce.numberOfCharacters == 0 { showList = isFocused } @@ -110,7 +105,7 @@ public struct PBTypeahead: View { _ = searchResults reloadList } - .onChange(of: listOptions.count) { _ in + .onChange(of: searchResults.count) { _ in reloadList } .onChange(of: contentSize) { _ in @@ -166,24 +161,19 @@ private extension PBTypeahead { } var searchResults: [Option] { - switch selection{ - case .multiple: - return searchText.isEmpty && debounce.numberOfCharacters == 0 ? listOptions : listOptions.filter { - if let text = $0.1?.0 { - text.localizedCaseInsensitiveContains(searchText) - } else { - $0.0.localizedCaseInsensitiveContains(searchText) - } - } - case .single: - return searchText.isEmpty && debounce.numberOfCharacters == 0 ? options : options.filter { + let filteredOptions = searchText.isEmpty && debounce.numberOfCharacters == 0 ? options : options.filter { if let text = $0.1?.0 { - text.localizedCaseInsensitiveContains(searchText) + return text.localizedCaseInsensitiveContains(searchText) } else { - $0.0.localizedCaseInsensitiveContains(searchText) + return $0.0.localizedCaseInsensitiveContains(searchText) } - } - } + } + let selectedIds = Set(selectedOptions.map { $0.0 }) + let filteredSelectedOptions = filteredOptions.filter { !selectedIds.contains($0.0) } + switch selection{ + case .multiple: return filteredSelectedOptions + case .single: return filteredOptions + } } var optionsSelected: GridInputField.Selection { @@ -210,7 +200,6 @@ private extension PBTypeahead { searchText = "" selectedOptions.removeAll() onSelection?([]) - listOptions = options selectedIndex = nil hoveringIndex = nil showList = false @@ -297,16 +286,14 @@ private extension PBTypeahead { func onMultipleSelection(_ option: Option) { selectedOptions.append(option) onSelection?(selectedOptions) - listOptions.removeAll(where: { $0.0 == option.0 }) hoveringIndex = nil selectedIndex = nil } func removeSelected(_ index: Int) { if let selectedElementIndex = selectedOptions.indices.first(where: { $0 == index }) { - let selectedElement = selectedOptions.remove(at: selectedElementIndex) + let _ = selectedOptions.remove(at: selectedElementIndex) onSelection?(selectedOptions) - listOptions.append(selectedElement) selectedIndex = nil } } diff --git a/Sources/Playbook/Components/Typeahead/PBTypeaheadTemplate.swift b/Sources/Playbook/Components/Typeahead/PBTypeaheadTemplate.swift index 071a9a813..60275e919 100644 --- a/Sources/Playbook/Components/Typeahead/PBTypeaheadTemplate.swift +++ b/Sources/Playbook/Components/Typeahead/PBTypeaheadTemplate.swift @@ -34,7 +34,7 @@ public struct PBTypeaheadTemplate: View { private let popoverManager = PopoverManager() private let onSelection: (([OptionType]) -> Void)? private let clearAction: (() -> Void)? - @State private var listOptions: [OptionType] = [] + @State private var numberOfLoadedItems: Int = 6 @State private var showList: Bool = false @State private var isCollapsed = false @State private var hoveringIndex: Int? @@ -103,12 +103,8 @@ public struct PBTypeaheadTemplate: View { isFocused = false } } - .onChange(of: options.count) { _ in - listOptions = options - } .onAppear { focused = isFocused - listOptions = options if debounce.numberOfCharacters == 0 { showList = isFocused } @@ -123,13 +119,16 @@ public struct PBTypeaheadTemplate: View { _ = searchResults reloadList } - .onChange(of: listOptions.count) { _ in + .onChange(of: searchResults.count) { _ in reloadList } .onChange(of: contentSize) { _ in reloadList } - .onChange(of: hoveringIndex) { index in + .onChange(of: hoveringIndex) { _ in + reloadList + } + .onChange(of: numberOfLoadedItems) { _ in reloadList } .onChange(of: searchText, debounce: debounce) { _ in @@ -142,7 +141,7 @@ public struct PBTypeaheadTemplate: View { @MainActor private extension PBTypeaheadTemplate { - func listItemView(item: Option, index: Int, optionType: OptionType) -> some View { + func listItemView(item: Option, index: Int) -> some View { HStack { if let customView = item.1?.1?() { customView @@ -161,24 +160,81 @@ private extension PBTypeaheadTemplate { hoveringOption = item } .onTapGesture { - onListSelection(index: index, option: optionType) +// onListSelection(index: index, option: optionType) + } + } + + var mapResults: [(String?, [Option])] { + var array: [(String?, [Option])] = [] + var currentSection: String? = nil + var currentOptions: [Option] = [] + + for result in searchResults { + switch result { + case .section(let section): + if !currentOptions.isEmpty || currentSection != nil { + array.append((currentSection, currentOptions)) + currentOptions = [] + } + currentSection = section + + case .item(let option): + currentOptions.append(option) + } + } + // Append the last section if it has items + if !currentOptions.isEmpty || currentSection != nil { + array.append((currentSection, currentOptions)) } + + return array } + @ViewBuilder var listView: some View { PBCard(alignment: .leading, padding: Spacing.none, shadow: .deeper) { ScrollView { VStack(alignment: .leading, spacing: 0) { - ForEach(Array(zip(searchResults.indices, searchResults)), id: \.0) { index, result in - switch result { - case .section(let section): - Text(section).pbFont(.caption) + + + ForEach(mapResults , id: \.0) { result in + + + Text(result.0 ?? "").pbFont(.caption) .padding(.top) .padding(.leading) - case .item(let item): listItemView(item: item, index: index, optionType: result) + + + + ForEach(Array(zip(result.1.indices, result.1)), id: \.0) { index, item in + listItemView(item: item, index: index) } + } + + + + + + + + +// ForEach(Array(zip(searchResults.indices, searchResults)).prefix(numberOfLoadedItems), id: \.0) { index, result in +// switch result { +// case .section(let section): +// Text(section).pbFont(.caption) +// .padding(.top) +// .padding(.leading) +// case .item(let item): listItemView(item: item, index: index, optionType: result) +// } +// } +// PBButton(variant: .link, title: "View More") { +// numberOfLoadedItems += 1 +// } +// .padding() + + } } .scrollDismissesKeyboard(.immediately) @@ -189,39 +245,23 @@ private extension PBTypeaheadTemplate { } var searchResults: [OptionType] { - switch selection{ - case .multiple: - let selectedIds = Set(selectedOptions.map { $0.id }) - let arr = searchText.isEmpty && debounce.numberOfCharacters == 0 ? listOptions : listOptions.filter { - switch $0 { - case .item(let item): - if let text = item.1?.0 { - return text.localizedCaseInsensitiveContains(searchText) - } else { - return item.0.localizedCaseInsensitiveContains(searchText) - } - case .section(_): - return true - } - } - let arr2 = arr.filter { !selectedIds.contains($0.id) } - print("@@@ >>> arr1 \(arr.count), arr2 \(arr2.count)") - return arr2 - - - case .single: - return searchText.isEmpty && debounce.numberOfCharacters == 0 ? options : options.filter { - switch $0 { - case .item(let item): - if let text = item.1?.0 { - return text.localizedCaseInsensitiveContains(searchText) - } else { - return item.0.localizedCaseInsensitiveContains(searchText) - } - case .section(_): - return true + let filteredOptions = searchText.isEmpty && debounce.numberOfCharacters == 0 ? options : options.filter { + switch $0 { + case .item(let item): + if let text = item.1?.0 { + return text.localizedCaseInsensitiveContains(searchText) + } else { + return item.0.localizedCaseInsensitiveContains(searchText) } - } + case .section(_): + return true + } + } + let selectedIds = Set(selectedOptions.map { $0.id }) + let filteredSelectedOptions = filteredOptions.filter { !selectedIds.contains($0.id) } + switch selection{ + case .multiple: return filteredSelectedOptions + case .single: return filteredOptions } } @@ -253,7 +293,6 @@ private extension PBTypeaheadTemplate { searchText = "" selectedOptions.removeAll() onSelection?([]) - listOptions = options selectedIndex = nil hoveringIndex = nil showList = false @@ -340,7 +379,6 @@ private extension PBTypeaheadTemplate { func onMultipleSelection(_ option: OptionType) { selectedOptions.append(option) onSelection?(selectedOptions) -// listOptions.removeAll(where: { $0.id == option.id }) hoveringIndex = nil selectedIndex = nil } @@ -349,7 +387,6 @@ private extension PBTypeaheadTemplate { if let selectedElementIndex = selectedOptions.indices.first(where: { $0 == index }) { let selectedElement = selectedOptions.remove(at: selectedElementIndex) onSelection?(selectedOptions) -// listOptions.append(selectedElement) selectedIndex = nil } } @@ -391,7 +428,7 @@ public extension PBTypeaheadTemplate { } } -//#Preview { -// registerFonts() -// return TypeaheadCatalogTemplate() -//} +#Preview { + registerFonts() + return TypeaheadCatalog() +} diff --git a/Sources/Playbook/Components/Typeahead/TypeaheadCatalog.swift b/Sources/Playbook/Components/Typeahead/TypeaheadCatalog.swift index 9f0023cce..debd9f066 100644 --- a/Sources/Playbook/Components/Typeahead/TypeaheadCatalog.swift +++ b/Sources/Playbook/Components/Typeahead/TypeaheadCatalog.swift @@ -31,6 +31,10 @@ public struct TypeaheadCatalog: View { .item(("1", (Mocks.andrew.name, { Mocks.andrew }))), .item(("2", (Mocks.ana.name, { Mocks.ana }))), .section("section 2"), + .item(("4", (Mocks.luccile.name, { Mocks.luccile }))), + .section("section 3"), + .item(("1", (Mocks.andrew.name, { Mocks.andrew }))), + .item(("2", (Mocks.ana.name, { Mocks.ana }))), .item(("3", (Mocks.patric.name, { Mocks.patric }))), .item(("4", (Mocks.luccile.name, { Mocks.luccile }))) ] diff --git a/Sources/Playbook/Resources/Helper Files/Mocks.swift b/Sources/Playbook/Resources/Helper Files/Mocks.swift index 74b948aaa..dd3bd73ba 100644 --- a/Sources/Playbook/Resources/Helper Files/Mocks.swift +++ b/Sources/Playbook/Resources/Helper Files/Mocks.swift @@ -23,7 +23,11 @@ enum Mocks { ("1", (andrew.name, { andrew })), ("2", (ana.name, { ana })), ("3", (patric.name, { patric })), - ("4", (luccile.name, { luccile })) + ("4", (luccile.name, { luccile })), + ("5", (andrew.name, { andrew })), + ("6", (ana.name, { ana })), + ("7", (patric.name, { patric })), + ("8", (luccile.name, { luccile })) ] static let avatarXSmall = PBAvatar(image: Image("andrew", bundle: .module), size: .xSmall) static let avatarXSmallStatus = PBAvatar(image: Image("andrew", bundle: .module), size: .xSmall, status: .online) From dda8321e002ca9bca89a20fdbd52e03661647191 Mon Sep 17 00:00:00 2001 From: "isis.silva" Date: Tue, 15 Oct 2024 12:28:35 -0400 Subject: [PATCH 06/20] fix pbuser --- Sources/Playbook/Components/User/PBUser.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Playbook/Components/User/PBUser.swift b/Sources/Playbook/Components/User/PBUser.swift index cec32fc9f..e2aa97284 100644 --- a/Sources/Playbook/Components/User/PBUser.swift +++ b/Sources/Playbook/Components/User/PBUser.swift @@ -85,7 +85,7 @@ public extension PBUser { Text(name) .pbFont(nameFont.font, variant: nameFont.variant) .foregroundColor(.text(.default)) - bodyText.pbFont(.body, color: .text(.light)) + bodyText.pbFont(.body, color: .text(.light)).lineLimit(1) if let content = subtitle { content } From 8ce8fb7ba48faf9caca2e952e74b298debb38cd6 Mon Sep 17 00:00:00 2001 From: "isis.silva" Date: Wed, 16 Oct 2024 11:13:08 -0400 Subject: [PATCH 07/20] add view more section button --- .../Components/Typeahead/PBTypeahead.swift | 13 -- .../Typeahead/PBTypeaheadTemplate.swift | 203 +++++++++--------- .../Typeahead/TypeaheadCatalog.swift | 5 +- .../Extensions/View+DisableAnimation.swift | 2 +- .../Resources/Helper Files/Mocks.swift | 6 +- 5 files changed, 105 insertions(+), 124 deletions(-) diff --git a/Sources/Playbook/Components/Typeahead/PBTypeahead.swift b/Sources/Playbook/Components/Typeahead/PBTypeahead.swift index b9e9041ea..055db37af 100644 --- a/Sources/Playbook/Components/Typeahead/PBTypeahead.swift +++ b/Sources/Playbook/Components/Typeahead/PBTypeahead.swift @@ -339,16 +339,3 @@ public extension PBTypeahead { registerFonts() return TypeaheadCatalog() } - -struct AnimatingCellHeight: AnimatableModifier { - var height: CGFloat? = 0 - - var animatableData: CGFloat? { - get { height } - set { height = newValue } - } - - func body(content: Content) -> some View { - content.frame(height: height) - } -} diff --git a/Sources/Playbook/Components/Typeahead/PBTypeaheadTemplate.swift b/Sources/Playbook/Components/Typeahead/PBTypeaheadTemplate.swift index 60275e919..811f23f33 100644 --- a/Sources/Playbook/Components/Typeahead/PBTypeaheadTemplate.swift +++ b/Sources/Playbook/Components/Typeahead/PBTypeaheadTemplate.swift @@ -10,19 +10,6 @@ import SwiftUI public struct PBTypeaheadTemplate: View { - public enum OptionType: Identifiable { - public var id: String { - switch self { - case .section(let str): - return str - case .item(let item): - return item.0 - } - } - case section(String) - case item(PBTypeaheadTemplate.Option) - } - public typealias Option = (String, (String, (() -> Content?)?)?) private let id: Int private let title: String @@ -32,9 +19,9 @@ public struct PBTypeaheadTemplate: View { private let dropdownMaxHeight: CGFloat? private let listOffset: (x: CGFloat, y: CGFloat) private let popoverManager = PopoverManager() - private let onSelection: (([OptionType]) -> Void)? + private let onSelection: (([Option]) -> Void)? private let clearAction: (() -> Void)? - @State private var numberOfLoadedItems: Int = 6 + @State private var showList: Bool = false @State private var isCollapsed = false @State private var hoveringIndex: Int? @@ -42,8 +29,9 @@ public struct PBTypeaheadTemplate: View { @State private var isHovering: Bool = false @State private var contentSize: CGSize = .zero @State private var selectedIndex: Int? - @State private var selectedOptions: [OptionType] = [] + @State private var selectedOptions: [Option] = [] @State private var focused: Bool = false + @State var numberOfItemsShown: [String?: Int] = [:] @Binding var options: [OptionType] @Binding var searchText: String @FocusState.Binding private var isFocused: Bool @@ -59,7 +47,7 @@ public struct PBTypeaheadTemplate: View { dropdownMaxHeight: CGFloat? = nil, listOffset: (x: CGFloat, y: CGFloat) = (0, 0), isFocused: FocusState.Binding, - onSelection: @escaping (([OptionType]) -> Void), + onSelection: @escaping (([Option]) -> Void), clearAction: (() -> Void)? = nil ) { self.id = id @@ -128,9 +116,6 @@ public struct PBTypeaheadTemplate: View { .onChange(of: hoveringIndex) { _ in reloadList } - .onChange(of: numberOfLoadedItems) { _ in - reloadList - } .onChange(of: searchText, debounce: debounce) { _ in if !searchText.isEmpty { showList = true @@ -141,6 +126,36 @@ public struct PBTypeaheadTemplate: View { @MainActor private extension PBTypeaheadTemplate { + @ViewBuilder + var listView: some View { + PBCard(alignment: .leading, padding: Spacing.none, shadow: .deeper) { + ScrollView { + VStack(alignment: .leading, spacing: 0) { + ForEach(mapResults, id: \.0) { result in + if let section = result.0 { + sectionView(section) + } + ForEach(Array(zip(result.1.indices, result.1)), id: \.0) { index, item in + listItemView(item: item, index: index) + } + result.2 + .padding() + } + } + } + .scrollDismissesKeyboard(.immediately) + .frame(maxHeight: dropdownMaxHeight) + .fixedSize(horizontal: false, vertical: true) + } + .frame(maxWidth: .infinity, alignment: .top) + } + + func sectionView(_ section: String) -> some View { + Text(section).pbFont(.caption) + .padding(.top) + .padding(.leading) + } + func listItemView(item: Option, index: Int) -> some View { HStack { if let customView = item.1?.1?() { @@ -160,88 +175,59 @@ private extension PBTypeaheadTemplate { hoveringOption = item } .onTapGesture { -// onListSelection(index: index, option: optionType) + onListSelection(index: index, option: item) } } - var mapResults: [(String?, [Option])] { - var array: [(String?, [Option])] = [] + var mapResults: [(String?, [Option], PBButton)] { + var array: [(String?, [Option], PBButton)] = [] var currentSection: String? = nil var currentOptions: [Option] = [] - for result in searchResults { switch result { - case .section(let section): - if !currentOptions.isEmpty || currentSection != nil { - array.append((currentSection, currentOptions)) - currentOptions = [] - } - currentSection = section + case .section(let section): + if !currentOptions.isEmpty || currentSection != nil { + appendSectionToArray( + section: currentSection, + options: currentOptions, + to: &array + ) + currentOptions = [] + } + currentSection = section - case .item(let option): - currentOptions.append(option) + case .item(let option): + currentOptions.append(option) } } - // Append the last section if it has items if !currentOptions.isEmpty || currentSection != nil { - array.append((currentSection, currentOptions)) + appendSectionToArray( + section: currentSection, + options: currentOptions, + to: &array + ) } - return array } - - @ViewBuilder - var listView: some View { - PBCard(alignment: .leading, padding: Spacing.none, shadow: .deeper) { - ScrollView { - VStack(alignment: .leading, spacing: 0) { - - - ForEach(mapResults , id: \.0) { result in - - - Text(result.0 ?? "").pbFont(.caption) - .padding(.top) - .padding(.leading) - - - - ForEach(Array(zip(result.1.indices, result.1)), id: \.0) { index, item in - listItemView(item: item, index: index) - } - - } - - - - - - - - -// ForEach(Array(zip(searchResults.indices, searchResults)).prefix(numberOfLoadedItems), id: \.0) { index, result in -// switch result { -// case .section(let section): -// Text(section).pbFont(.caption) -// .padding(.top) -// .padding(.leading) -// case .item(let item): listItemView(item: item, index: index, optionType: result) -// } -// } -// PBButton(variant: .link, title: "View More") { -// numberOfLoadedItems += 1 -// } -// .padding() - - - } + private func appendSectionToArray( + section: String?, + options: [Option], + to array: inout [(String?, [Option], PBButton)] + ) { + let numberOfItems = numberOfItemsShown[section] ?? 2 + array.append(( + section, + Array(options.prefix(numberOfItems)), + PBButton( + variant: .link, + title: (numberOfItems == 2) ? "View More" : "View Less", + icon: (numberOfItems == 2) ? .fontAwesome(.chevronDown) : .fontAwesome(.chevronUp) + ) { + numberOfItemsShown[section] = (numberOfItems == 2) ? 4 : 2 + reloadList } - .scrollDismissesKeyboard(.immediately) - .frame(maxHeight: dropdownMaxHeight) - .fixedSize(horizontal: false, vertical: true) - } - .frame(maxWidth: .infinity, alignment: .top) + )) } var searchResults: [OptionType] { @@ -257,7 +243,7 @@ private extension PBTypeaheadTemplate { return true } } - let selectedIds = Set(selectedOptions.map { $0.id }) + let selectedIds = Set(selectedOptions.map { $0.0 }) let filteredSelectedOptions = filteredOptions.filter { !selectedIds.contains($0.id) } switch selection{ case .multiple: return filteredSelectedOptions @@ -267,14 +253,10 @@ private extension PBTypeaheadTemplate { var optionsSelected: GridInputField.Selection { let optionsSelected = selectedOptions.map { value in - switch value { - case .item(let item): - if let content = item.1 { - return content.0 - } else { - return item.0 - } - default: return "" + if let content = value.1 { + return content.0 + } else { + return value.0 } } return selection.selectedOptions(options: optionsSelected, placeholder: placeholder) @@ -341,7 +323,7 @@ private extension PBTypeaheadTemplate { } return event } - #endif + #endif } var onViewTap: Void { @@ -355,7 +337,7 @@ private extension PBTypeaheadTemplate { } } - func onListSelection(index: Int, option: OptionType) { + func onListSelection(index: Int, option: Option) { if showList { switch selection { case .single: @@ -368,7 +350,7 @@ private extension PBTypeaheadTemplate { searchText = "" } - func onSingleSelection(index: Int, _ option: OptionType) { + func onSingleSelection(index: Int, _ option: Option) { selectedOptions.removeAll() selectedOptions.append(option) selectedIndex = index @@ -376,7 +358,7 @@ private extension PBTypeaheadTemplate { onSelection?(selectedOptions) } - func onMultipleSelection(_ option: OptionType) { + func onMultipleSelection(_ option: Option) { selectedOptions.append(option) onSelection?(selectedOptions) hoveringIndex = nil @@ -385,7 +367,7 @@ private extension PBTypeaheadTemplate { func removeSelected(_ index: Int) { if let selectedElementIndex = selectedOptions.indices.first(where: { $0 == index }) { - let selectedElement = selectedOptions.remove(at: selectedElementIndex) + _ = selectedOptions.remove(at: selectedElementIndex) onSelection?(selectedOptions) selectedIndex = nil } @@ -399,11 +381,11 @@ private extension PBTypeaheadTemplate { } default: break } -#if os(macOS) + #if os(macOS) return hoveringIndex == index ? .hover : .card -#elseif os(iOS) + #elseif os(iOS) return .card -#endif + #endif } func listTextolor(_ index: Int?) -> Color { @@ -428,6 +410,21 @@ public extension PBTypeaheadTemplate { } } +public extension PBTypeaheadTemplate { + enum OptionType: Identifiable { + public var id: String { + switch self { + case .section(let str): + return str + case .item(let item): + return item.0 + } + } + case section(String) + case item(PBTypeaheadTemplate.Option) + } +} + #Preview { registerFonts() return TypeaheadCatalog() diff --git a/Sources/Playbook/Components/Typeahead/TypeaheadCatalog.swift b/Sources/Playbook/Components/Typeahead/TypeaheadCatalog.swift index debd9f066..c70bee8dc 100644 --- a/Sources/Playbook/Components/Typeahead/TypeaheadCatalog.swift +++ b/Sources/Playbook/Components/Typeahead/TypeaheadCatalog.swift @@ -30,9 +30,9 @@ public struct TypeaheadCatalog: View { .section("section 1"), .item(("1", (Mocks.andrew.name, { Mocks.andrew }))), .item(("2", (Mocks.ana.name, { Mocks.ana }))), - .section("section 2"), + .item(("3", (Mocks.patric.name, { Mocks.patric }))), .item(("4", (Mocks.luccile.name, { Mocks.luccile }))), - .section("section 3"), + .section("section 2"), .item(("1", (Mocks.andrew.name, { Mocks.andrew }))), .item(("2", (Mocks.ana.name, { Mocks.ana }))), .item(("3", (Mocks.patric.name, { Mocks.patric }))), @@ -97,6 +97,7 @@ extension TypeaheadCatalog { searchText: $searchTextSections, options: $sectionUsers, selection: .multiple(variant: .pill), + dropdownMaxHeight: 400, isFocused: $isFocused3 ) { _ in } } diff --git a/Sources/Playbook/Resources/Extensions/View+DisableAnimation.swift b/Sources/Playbook/Resources/Extensions/View+DisableAnimation.swift index d4eb77656..0fd82ece6 100644 --- a/Sources/Playbook/Resources/Extensions/View+DisableAnimation.swift +++ b/Sources/Playbook/Resources/Extensions/View+DisableAnimation.swift @@ -4,7 +4,7 @@ // Copyright © 2024 Power Home Remodeling Group // This software is distributed under the ISC License // -// SwiftUIView.swift +// View+DisableAnimation.swift // import SwiftUI diff --git a/Sources/Playbook/Resources/Helper Files/Mocks.swift b/Sources/Playbook/Resources/Helper Files/Mocks.swift index dd3bd73ba..74b948aaa 100644 --- a/Sources/Playbook/Resources/Helper Files/Mocks.swift +++ b/Sources/Playbook/Resources/Helper Files/Mocks.swift @@ -23,11 +23,7 @@ enum Mocks { ("1", (andrew.name, { andrew })), ("2", (ana.name, { ana })), ("3", (patric.name, { patric })), - ("4", (luccile.name, { luccile })), - ("5", (andrew.name, { andrew })), - ("6", (ana.name, { ana })), - ("7", (patric.name, { patric })), - ("8", (luccile.name, { luccile })) + ("4", (luccile.name, { luccile })) ] static let avatarXSmall = PBAvatar(image: Image("andrew", bundle: .module), size: .xSmall) static let avatarXSmallStatus = PBAvatar(image: Image("andrew", bundle: .module), size: .xSmall, status: .online) From 434ebcf4d35cf790e5a617966763d36b9ea9529a Mon Sep 17 00:00:00 2001 From: "isis.silva" Date: Thu, 17 Oct 2024 13:19:16 -0400 Subject: [PATCH 08/20] fix macos version --- .../Typeahead/PBTypeaheadTemplate.swift | 75 +++++++++++-------- 1 file changed, 43 insertions(+), 32 deletions(-) diff --git a/Sources/Playbook/Components/Typeahead/PBTypeaheadTemplate.swift b/Sources/Playbook/Components/Typeahead/PBTypeaheadTemplate.swift index 811f23f33..1fb737e6c 100644 --- a/Sources/Playbook/Components/Typeahead/PBTypeaheadTemplate.swift +++ b/Sources/Playbook/Components/Typeahead/PBTypeaheadTemplate.swift @@ -24,7 +24,7 @@ public struct PBTypeaheadTemplate: View { @State private var showList: Bool = false @State private var isCollapsed = false - @State private var hoveringIndex: Int? + @State private var hoveringIndex: (Int?, String?) @State private var hoveringOption: Option? @State private var isHovering: Bool = false @State private var contentSize: CGSize = .zero @@ -113,7 +113,7 @@ public struct PBTypeaheadTemplate: View { .onChange(of: contentSize) { _ in reloadList } - .onChange(of: hoveringIndex) { _ in + .onChange(of: hoveringIndex.0) { _ in reloadList } .onChange(of: searchText, debounce: debounce) { _ in @@ -136,10 +136,9 @@ private extension PBTypeaheadTemplate { sectionView(section) } ForEach(Array(zip(result.1.indices, result.1)), id: \.0) { index, item in - listItemView(item: item, index: index) + listItemView(item: item, index: index, for: result.0) } - result.2 - .padding() + result.2.padding() } } } @@ -156,7 +155,7 @@ private extension PBTypeaheadTemplate { .padding(.leading) } - func listItemView(item: Option, index: Int) -> some View { + func listItemView(item: Option, index: Int, for section: String?) -> some View { HStack { if let customView = item.1?.1?() { customView @@ -168,14 +167,14 @@ private extension PBTypeaheadTemplate { .padding(.horizontal, Spacing.xSmall + 4) .padding(.vertical, Spacing.xSmall + 4) .frame(maxWidth: .infinity, alignment: .leading) - .background(listBackgroundColor(index)) + .background(listBackgroundColor(index, section: section)) .onHover(disabled: false) { hover in isHovering = hover - hoveringIndex = index + hoveringIndex = (index, section) hoveringOption = item } .onTapGesture { - onListSelection(index: index, option: item) + onListSelection(index: index, section: section, option: item) } } @@ -276,7 +275,7 @@ private extension PBTypeaheadTemplate { selectedOptions.removeAll() onSelection?([]) selectedIndex = nil - hoveringIndex = nil + hoveringIndex = (nil, nil) showList = false } @@ -287,14 +286,22 @@ private extension PBTypeaheadTemplate { focused = true } if event.keyCode == 36 { // return bar - if let index = hoveringIndex, index <= searchResults.count-1, showList { - onListSelection(index: index, option: searchResults[index]) + if let index = hoveringIndex.0, + let section = hoveringIndex.1, + let results = mapResults.first?.1, + index <= results.count-1, + showList { + onListSelection(index: index, section: section, option: results[index]) } } if event.keyCode == 49 { // space if isFocused { - if let index = hoveringIndex, index <= searchResults.count-1, showList, searchText.isEmpty { - onListSelection(index: index, option: searchResults[index]) + if let index = hoveringIndex.0, + let section = hoveringIndex.1, + let results = mapResults.first?.1, + index <= results.count-1, + showList, searchText.isEmpty { + onListSelection(index: index, section: section, option: results[index]) } else { showList = true } @@ -307,17 +314,17 @@ private extension PBTypeaheadTemplate { } if event.keyCode == 125 { // arrow down if isFocused { - if let index = hoveringIndex { - hoveringIndex = index < searchResults.count ? (index + 1) : 0 + if let index = hoveringIndex.0, let section = hoveringIndex.1 { + hoveringIndex.0 = index < searchResults.count ? (index + 1) : 0 } else { - hoveringIndex = 0 + hoveringIndex.0 = 0 } } } else { if event.keyCode == 126 { // arrow up - if isFocused, let index = hoveringIndex { - hoveringIndex = index > 1 ? (index - 1) : 0 + if isFocused, let index = hoveringIndex.0 { + hoveringIndex.0 = index > 1 ? (index - 1) : 0 } } } @@ -337,31 +344,35 @@ private extension PBTypeaheadTemplate { } } - func onListSelection(index: Int, option: Option) { + func onListSelection(index: Int, section: String?, option: Option) { if showList { switch selection { case .single: - onSingleSelection(index: index, option) + onSingleSelection(index: index, section: section, option) case .multiple: - onMultipleSelection(option) + onMultipleSelection(index: index, section: section, option) } } showList = false searchText = "" } - func onSingleSelection(index: Int, _ option: Option) { + func onSingleSelection(index: Int, section: String?, _ option: Option) { selectedOptions.removeAll() - selectedOptions.append(option) + if hoveringIndex.0 == index && hoveringIndex.1 == section { + selectedOptions.append(option) + onSelection?(selectedOptions) + } selectedIndex = index - hoveringIndex = index - onSelection?(selectedOptions) + hoveringIndex = (index, section) } - func onMultipleSelection(_ option: Option) { - selectedOptions.append(option) - onSelection?(selectedOptions) - hoveringIndex = nil + func onMultipleSelection(index: Int, section: String?, _ option: Option) { + if hoveringIndex.0 == index && hoveringIndex.1 == section { + selectedOptions.append(option) + onSelection?(selectedOptions) + } + hoveringIndex = (index, section) selectedIndex = nil } @@ -373,7 +384,7 @@ private extension PBTypeaheadTemplate { } } - func listBackgroundColor(_ index: Int?) -> Color { + func listBackgroundColor(_ index: Int?, section: String?) -> Color { switch selection { case .single: if selectedIndex != nil, selectedIndex == index { @@ -382,7 +393,7 @@ private extension PBTypeaheadTemplate { default: break } #if os(macOS) - return hoveringIndex == index ? .hover : .card + return hoveringIndex.0 == index && hoveringIndex.1 == section ? .hover : .card #elseif os(iOS) return .card #endif From ff487601eaf23a1079b43ead19febd8dfb1e7f4f Mon Sep 17 00:00:00 2001 From: "isis.silva" Date: Tue, 10 Dec 2024 14:37:59 -0500 Subject: [PATCH 09/20] removed warnings --- .../Fixed Confirmation Toast/PBToast.swift | 3 +- .../Components/Popover/PBPopover.swift | 10 +- .../Components/Popover/Popover+Position.swift | 10 +- .../Progress Step/PBProgressStep.swift | 2 +- .../Playbook/Components/Select/PBSelect.swift | 4 +- .../Selectable Card/PBSelectableCard.swift | 2 +- .../Components/Text Area/PBTextArea.swift | 2 +- .../Components/Tooltip/PBTooltip.swift | 4 +- .../Components/Typeahead/GridInputField.swift | 2 +- .../Components/Typeahead/PBTypeahead.swift | 43 +++--- .../Typeahead/PBTypeaheadTemplate.swift | 122 +++++++++++------- .../Typeahead/Pill/TypeaheadPill.swift | 4 +- .../Extensions/OnChange+Debounce.swift | 4 +- .../Extensions/OnScrollDetection.swift | 2 +- .../Resources/Extensions/View+Reader.swift | 4 +- 15 files changed, 125 insertions(+), 93 deletions(-) diff --git a/Sources/Playbook/Components/Fixed Confirmation Toast/PBToast.swift b/Sources/Playbook/Components/Fixed Confirmation Toast/PBToast.swift index 7ca74fcd1..bae038338 100644 --- a/Sources/Playbook/Components/Fixed Confirmation Toast/PBToast.swift +++ b/Sources/Playbook/Components/Fixed Confirmation Toast/PBToast.swift @@ -56,8 +56,7 @@ public struct PBToast: View { if let dismiss = actionView { switch dismiss { case .withTimer(let time): - _ = DispatchQueue.main.asyncAfter(deadline: .now() + time) { dismissAction() - } + DispatchQueue.main.asyncAfter(deadline: .now() + time) { dismissAction() } default: break } } diff --git a/Sources/Playbook/Components/Popover/PBPopover.swift b/Sources/Playbook/Components/Popover/PBPopover.swift index 40311472e..cb95f08a0 100644 --- a/Sources/Playbook/Components/Popover/PBPopover.swift +++ b/Sources/Playbook/Components/Popover/PBPopover.swift @@ -55,20 +55,20 @@ public struct Popover: ViewModifier { close: clickToClose ) } - .onChange(of: isPresented) { newValue in + .onChange(of: isPresented) { _, newValue in popoverManager.presentPopover(with: id, value: newValue) updateViewFrame() } - .onChange(of: popoverPosition) { position in + .onChange(of: popoverPosition) { updateViewFrame() } - .onChange(of: contentFrame) { frame in + .onChange(of: contentFrame) { updateViewFrame() } - .onChange(of: refreshView) { _ in + .onChange(of: refreshView) { updateViewFrame() } - .onChange(of: isHovering) { _ in + .onChange(of: isHovering) { updateViewFrame() } .onReceive(popoverManager.$isPresented) { newValue in diff --git a/Sources/Playbook/Components/Popover/Popover+Position.swift b/Sources/Playbook/Components/Popover/Popover+Position.swift index eecc3c384..2bbbd6e85 100644 --- a/Sources/Playbook/Components/Popover/Popover+Position.swift +++ b/Sources/Playbook/Components/Popover/Popover+Position.swift @@ -44,15 +44,15 @@ public extension Position { } func calculateFrame(from originFrame: CGRect?, size: CGSize?) -> CGRect { - var popoverFrame = self.absoluteFrame( + let popoverFrame = self.absoluteFrame( position: self, originFrame: originFrame ?? .zero, popoverSize: size ?? .zero ) - let screenEdgePadding = EdgeInsets(top: 8, leading: 8, bottom: 8, trailing: 8) - let safeWindowFrame = Screen.rect - let maxX = safeWindowFrame.maxX - screenEdgePadding.trailing - let maxY = safeWindowFrame.maxY - screenEdgePadding.bottom +// let screenEdgePadding = EdgeInsets(top: 8, leading: 8, bottom: 8, trailing: 8) +// let safeWindowFrame = Screen.rect +// let maxX = safeWindowFrame.maxX - screenEdgePadding.trailing +// let maxY = safeWindowFrame.maxY - screenEdgePadding.bottom // #if os(macOS) // if popoverFrame.origin.x < screenEdgePadding.leading { // popoverFrame.origin.x = screenEdgePadding.leading diff --git a/Sources/Playbook/Components/Progress Step/PBProgressStep.swift b/Sources/Playbook/Components/Progress Step/PBProgressStep.swift index b4b0a66e7..4d4923c63 100644 --- a/Sources/Playbook/Components/Progress Step/PBProgressStep.swift +++ b/Sources/Playbook/Components/Progress Step/PBProgressStep.swift @@ -46,7 +46,7 @@ public struct PBProgressStep: View { public var body: some View { progressVariantView - .onChange(of: progress) { newValue in + .onChange(of: progress) { _, newValue in if newValue >= steps { progress = steps } diff --git a/Sources/Playbook/Components/Select/PBSelect.swift b/Sources/Playbook/Components/Select/PBSelect.swift index 9f9aefc35..26956a93d 100644 --- a/Sources/Playbook/Components/Select/PBSelect.swift +++ b/Sources/Playbook/Components/Select/PBSelect.swift @@ -62,9 +62,9 @@ public struct PBSelect: View { } .buttonStyle(.plain) .disabled(style == .disabled) - .onChange(of: selected, perform: { newValue in + .onChange(of: selected) { _, newValue in selectedOption(newValue) - }) + } if let errorMessage = style.errorMessage { Text(errorMessage) diff --git a/Sources/Playbook/Components/Selectable Card/PBSelectableCard.swift b/Sources/Playbook/Components/Selectable Card/PBSelectableCard.swift index 13c27821a..4a93ade9a 100644 --- a/Sources/Playbook/Components/Selectable Card/PBSelectableCard.swift +++ b/Sources/Playbook/Components/Selectable Card/PBSelectableCard.swift @@ -132,7 +132,7 @@ extension PBSelectableCard { } } } - .onChange(of: radioId) { newValue in + .onChange(of: radioId) { _, newValue in isSelected = (newValue == id) } } diff --git a/Sources/Playbook/Components/Text Area/PBTextArea.swift b/Sources/Playbook/Components/Text Area/PBTextArea.swift index 30a98f673..d0a633f20 100644 --- a/Sources/Playbook/Components/Text Area/PBTextArea.swift +++ b/Sources/Playbook/Components/Text Area/PBTextArea.swift @@ -66,7 +66,7 @@ public struct PBTextArea: View { error = text.count >= maxCount ? message : nil } } - .onChange(of: text) { text in + .onChange(of: text) { _, text in if case let .maxCharacterCountError(maxCount, message) = characterCount { error = text.count >= maxCount ? message : nil } diff --git a/Sources/Playbook/Components/Tooltip/PBTooltip.swift b/Sources/Playbook/Components/Tooltip/PBTooltip.swift index 772ef641b..ff4444e1f 100644 --- a/Sources/Playbook/Components/Tooltip/PBTooltip.swift +++ b/Sources/Playbook/Components/Tooltip/PBTooltip.swift @@ -43,7 +43,7 @@ public struct PBTooltip: ViewModifier { if self.canPresent { content .onHover(disabled: false) { handleOnHover(hovering: $0) } - .onChange(of: shouldPresentPopover, perform: handleOnChange) + .onChange(of: shouldPresentPopover) { handleOnChange() } .popover(isPresented: $presentPopover, arrowEdge: self.placement, content: { popoverView }) } else { content @@ -63,7 +63,7 @@ public extension PBTooltip { shouldPresentPopover = hovering } - private func handleOnChange(_: Bool) { + private func handleOnChange() { if delay > 0.0 { handleDelay } else { diff --git a/Sources/Playbook/Components/Typeahead/GridInputField.swift b/Sources/Playbook/Components/Typeahead/GridInputField.swift index 3de49fc71..8f15c449a 100644 --- a/Sources/Playbook/Components/Typeahead/GridInputField.swift +++ b/Sources/Playbook/Components/Typeahead/GridInputField.swift @@ -94,7 +94,7 @@ private extension GridInputField { .pbFont(.body, color: placeholderTextColor) } TextField("", text: $searchText) - .onChange(of: searchText) { _ in + .onChange(of: searchText) { if searchText.first == " " { searchText = searchText.replacingOccurrences(of: " ", with: "") } diff --git a/Sources/Playbook/Components/Typeahead/PBTypeahead.swift b/Sources/Playbook/Components/Typeahead/PBTypeahead.swift index 226046f81..d65e3754b 100644 --- a/Sources/Playbook/Components/Typeahead/PBTypeahead.swift +++ b/Sources/Playbook/Components/Typeahead/PBTypeahead.swift @@ -22,7 +22,6 @@ public struct PBTypeahead: View { private let clearAction: (() -> Void)? @State private var hoveringIndex: Int? - @State private var hoveringOption: PBTypeahead.Option? @State private var isHovering: Bool = false @State private var selectedIndex: Int? @State private var focused: Bool = false @@ -75,18 +74,7 @@ public struct PBTypeahead: View { onViewTap: { onViewTap } ) .pbPopover( - isPresented: Binding( - get: { - popoverManager.isPopoverActive(for: id) - }, - set: { isActive in - if isActive { - popoverManager.showPopover(for: id) - } else { - popoverManager.hidePopover(for: id) - } - } - ), + isPresented: showPopover, id: id, position: .bottom(listOffset.x, listOffset.y), variant: .dropdown, @@ -94,7 +82,6 @@ public struct PBTypeahead: View { ) { listView } - } .onTapGesture { isFocused = false @@ -114,18 +101,18 @@ public struct PBTypeahead: View { selectedIndex = options.firstIndex(of: selectedOptions[0]) } } - .onChange(of: isFocused) { newValue in + .onChange(of: isFocused) { _, newValue in if newValue { popoverManager.showPopover(for: id) } } - .onChange(of: selectedOptions.count) { _ in + .onChange(of: selectedOptions.count) { reloadList } - .onChange(of: hoveringIndex) { index in + .onChange(of: hoveringIndex) { reloadList } - .onChange(of: popoverManager.isPopoverActive(for: id)) { newValue in + .onChange(of: popoverManager.isPopoverActive(for: id)) { _, newValue in if newValue { isFocused = true } @@ -142,6 +129,22 @@ public struct PBTypeahead: View { @MainActor private extension PBTypeahead { + + var showPopover: Binding { + .init( + get: { + popoverManager.isPopoverActive(for: id) + }, + set: { isActive in + if isActive { + popoverManager.showPopover(for: id) + } else { + popoverManager.hidePopover(for: id) + } + } + ) + } + private func togglePopover() { if popoverManager.isPopoverActive(for: id) { popoverManager.hidePopover(for: id) @@ -219,7 +222,6 @@ private extension PBTypeahead { .onHover(disabled: false) { hover in isHovering = hover hoveringIndex = index - hoveringOption = option } .onTapGesture { onListSelection(index: index, option: option) @@ -275,7 +277,8 @@ private extension PBTypeahead { selectedOptions = [] selectedIndex = nil hoveringIndex = nil - popoverManager.hidePopover(for: id) } + popoverManager.hidePopover(for: id) + } var onViewTap: Void { togglePopover() diff --git a/Sources/Playbook/Components/Typeahead/PBTypeaheadTemplate.swift b/Sources/Playbook/Components/Typeahead/PBTypeaheadTemplate.swift index 5deeaf263..8b85d1f9e 100644 --- a/Sources/Playbook/Components/Typeahead/PBTypeaheadTemplate.swift +++ b/Sources/Playbook/Components/Typeahead/PBTypeaheadTemplate.swift @@ -15,15 +15,13 @@ public struct PBTypeaheadTemplate: View { private let placeholder: String private var options: [PBTypeahead.OptionType] private let selection: PBTypeahead.Selection + private let noOptionsText: String private let debounce: (time: TimeInterval, numberOfCharacters: Int) private let dropdownMaxHeight: CGFloat? private let listOffset: (x: CGFloat, y: CGFloat) private let clearAction: (() -> Void)? - private let popoverManager = PopoverManager() - @State private var showList: Bool = false @State private var hoveringIndex: (Int?, String?) - @State private var hoveringOption: PBTypeahead.Option? @State private var isHovering: Bool = false @State private var contentSize: CGSize = .zero @State private var selectedIndex: Int? @@ -32,6 +30,7 @@ public struct PBTypeaheadTemplate: View { @Binding var selectedOptions: [PBTypeahead.Option] @Binding var searchText: String @FocusState.Binding private var isFocused: Bool + @State private var popoverManager = PopoverManager.shared public init( id: Int, @@ -45,6 +44,7 @@ public struct PBTypeaheadTemplate: View { listOffset: (x: CGFloat, y: CGFloat) = (0, 0), isFocused: FocusState.Binding, selectedOptions: Binding<[PBTypeahead.Option]>, + noOptionsText: String = "No options", clearAction: (() -> Void)? = nil ) { self.id = id @@ -57,6 +57,7 @@ public struct PBTypeaheadTemplate: View { self.dropdownMaxHeight = dropdownMaxHeight self.listOffset = listOffset self._isFocused = isFocused + self.noOptionsText = noOptionsText self.clearAction = clearAction self._selectedOptions = selectedOptions } @@ -76,7 +77,7 @@ public struct PBTypeaheadTemplate: View { ) .sizeReader { contentSize = $0 } .pbPopover( - isPresented: $showList, + isPresented: showPopover, id: id, position: .bottom(listOffset.x, listOffset.y), variant: .dropdown, @@ -84,45 +85,68 @@ public struct PBTypeaheadTemplate: View { ) { listView } + } .onTapGesture { isFocused = false } - } - .onAppear { - focused = isFocused - if debounce.numberOfCharacters == 0 { - showList = isFocused + .onAppear { + focused = isFocused + if debounce.numberOfCharacters == 0 { + if isFocused { + popoverManager.showPopover(for: id) + reloadList + } else { + popoverManager.hidePopover(for: id) + } + } + setKeyboardControls + if !selectedOptions.isEmpty { +// selectedIndex = options.first?.id.firstIndex(of: selectedOptions[0].id) + } } - setKeyboardControls - } - .onChange(of: isFocused) { newValue in - Timer.scheduledTimer(withTimeInterval: 0.03, repeats: false) { _ in - showList = newValue + .onChange(of: isFocused) { _, newValue in + if newValue { + popoverManager.showPopover(for: id) + } } - } - .onChange(of: searchText, debounce: debounce) { _ in - _ = searchResults - reloadList - } - .onChange(of: searchResults.count) { _ in - reloadList - } - .onChange(of: contentSize) { _ in - reloadList - } - .onChange(of: hoveringIndex.0) { _ in - reloadList - } - .onChange(of: searchText, debounce: debounce) { _ in - if !searchText.isEmpty { - showList = true + .onChange(of: selectedOptions.count) { + reloadList + } + .onChange(of: hoveringIndex.0) { + reloadList + } + .onChange(of: popoverManager.isPopoverActive(for: id)) { _, newValue in + if newValue { + isFocused = true + } + } + .onChange(of: searchText, debounce: debounce) { _ in + _ = searchResults + reloadList + if !searchText.isEmpty { + popoverManager.showPopover(for: id) + } } } } -} @MainActor private extension PBTypeaheadTemplate { + var showPopover: Binding { + .init( + get: { + popoverManager.isPopoverActive(for: id) + }, + set: { isActive in + if isActive { + popoverManager.showPopover(for: id) + } else { + popoverManager.hidePopover(for: id) + } + } + ) + } + @ViewBuilder var listView: some View { PBCard(alignment: .leading, padding: Spacing.none, shadow: .deeper) { @@ -168,7 +192,6 @@ private extension PBTypeaheadTemplate { .onHover(disabled: false) { hover in isHovering = hover hoveringIndex = (index, section) - hoveringOption = item } .onTapGesture { onListSelection(index: index, section: section, option: item) @@ -266,7 +289,7 @@ private extension PBTypeaheadTemplate { selectedOptions.removeAll() selectedIndex = nil hoveringIndex = (nil, nil) - showList = false + popoverManager.hidePopover(for: id) } var setKeyboardControls: Void { @@ -279,8 +302,7 @@ private extension PBTypeaheadTemplate { if let index = hoveringIndex.0, let section = hoveringIndex.1, let results = mapResults.first?.1, - index <= results.count-1, - showList { + index <= results.count-1, isFocused { onListSelection(index: index, section: section, option: results[index]) } } @@ -289,11 +311,10 @@ private extension PBTypeaheadTemplate { if let index = hoveringIndex.0, let section = hoveringIndex.1, let results = mapResults.first?.1, - index <= results.count-1, - showList, searchText.isEmpty { + index <= results.count-1, searchText.isEmpty { onListSelection(index: index, section: section, option: results[index]) } else { - showList = true + popoverManager.showPopover(for: id) } } } @@ -304,7 +325,7 @@ private extension PBTypeaheadTemplate { } if event.keyCode == 125 { // arrow down if isFocused { - if let index = hoveringIndex.0, let section = hoveringIndex.1 { + if let index = hoveringIndex.0 { hoveringIndex.0 = index < searchResults.count ? (index + 1) : 0 } else { hoveringIndex.0 = 0 @@ -323,19 +344,26 @@ private extension PBTypeaheadTemplate { #endif } + private func togglePopover() { + if popoverManager.isPopoverActive(for: id) { + popoverManager.hidePopover(for: id) + } else { + popoverManager.showPopover(for: id) + } + } + var onViewTap: Void { - showList.toggle() + togglePopover() isFocused = true } var reloadList: Void { - if showList { - isHovering.toggle() - } + isHovering.toggle() + popoverManager.update(with: id) } func onListSelection(index: Int, section: String?, option: PBTypeahead.Option) { - if showList { + if option.text != noOptionsText { switch selection { case .single: onSingleSelection(index: index, section: section, option) @@ -343,8 +371,10 @@ private extension PBTypeaheadTemplate { onMultipleSelection(index: index, section: section, option) } } - showList = false + popoverManager.hidePopover(for: id) searchText = "" + reloadList + isFocused = true } func onSingleSelection(index: Int, section: String?, _ option: PBTypeahead.Option) { diff --git a/Sources/Playbook/Components/Typeahead/Pill/TypeaheadPill.swift b/Sources/Playbook/Components/Typeahead/Pill/TypeaheadPill.swift index 39c3963e7..b0de1b08c 100644 --- a/Sources/Playbook/Components/Typeahead/Pill/TypeaheadPill.swift +++ b/Sources/Playbook/Components/Typeahead/Pill/TypeaheadPill.swift @@ -10,8 +10,8 @@ import SwiftUI struct TypeaheadPill: View { - @Environment (\.active) var isActive: Bool - @Environment (\.focus) var isFocus: Bool + @Environment(\.active) var isActive: Bool + @Environment(\.focus) var isFocus: Bool @Environment(\.hovering) var hovering: Bool @State private var isHovering: Bool = false private var shape = Capsule() diff --git a/Sources/Playbook/Resources/Extensions/OnChange+Debounce.swift b/Sources/Playbook/Resources/Extensions/OnChange+Debounce.swift index 907a7a832..047217475 100644 --- a/Sources/Playbook/Resources/Extensions/OnChange+Debounce.swift +++ b/Sources/Playbook/Resources/Extensions/OnChange+Debounce.swift @@ -27,7 +27,7 @@ private struct DebouncedChangeViewModifier: ViewModifier where Value: Equ func body(content: Content) -> some View { if debounce.numberOfCharacters != 0 { - content.onChange(of: trigger) { newValue in + content.onChange(of: trigger) { _, newValue in debouncedTask?.cancel() if newValue.count >= debounce.numberOfCharacters { debouncedTask = Task.delayed(seconds: debounce.time) { @MainActor in @@ -48,7 +48,7 @@ private struct DebouncedChangeViewModifier: ViewModifier where Value: Equ action(nil) } } - .onChange(of: trigger) { value in + .onChange(of: trigger) { _, value in debouncedTask?.cancel() debouncedTask = Task.delayed(seconds: debounce.time) { @MainActor in action(value) diff --git a/Sources/Playbook/Resources/Extensions/OnScrollDetection.swift b/Sources/Playbook/Resources/Extensions/OnScrollDetection.swift index 13bc24760..0dfb6e7dc 100644 --- a/Sources/Playbook/Resources/Extensions/OnScrollDetection.swift +++ b/Sources/Playbook/Resources/Extensions/OnScrollDetection.swift @@ -24,7 +24,7 @@ struct OnScrollDetection: ViewModifier { return Color.clear } ) - .onChange(of: scrollOffset) { newValue in + .onChange(of: scrollOffset) { action() } } diff --git a/Sources/Playbook/Resources/Extensions/View+Reader.swift b/Sources/Playbook/Resources/Extensions/View+Reader.swift index 07a16345f..3732d81ef 100644 --- a/Sources/Playbook/Resources/Extensions/View+Reader.swift +++ b/Sources/Playbook/Resources/Extensions/View+Reader.swift @@ -22,7 +22,7 @@ public extension View { return background( GeometryReader { geometry in Color.clear - .onChange(of: coordinateSpace(in: geometry)) { rect($0) } + .onChange(of: coordinateSpace(in: geometry)) { _, space in rect(space) } .onAppear { rect(coordinateSpace(in: geometry)) } @@ -41,7 +41,7 @@ public extension View { size(newValue) } } - .onChange(of: transaction?.animation) { _ in + .onChange(of: transaction?.animation) { DispatchQueue.main.async { size(geometry.size) } From ac4153147a78eb7eee89f99ef621881221f8207a Mon Sep 17 00:00:00 2001 From: "isis.silva" Date: Wed, 11 Dec 2024 14:51:59 -0500 Subject: [PATCH 10/20] turn data flat --- .../Typeahead/PBTypeaheadTemplate.swift | 121 +++++++++++++----- 1 file changed, 87 insertions(+), 34 deletions(-) diff --git a/Sources/Playbook/Components/Typeahead/PBTypeaheadTemplate.swift b/Sources/Playbook/Components/Typeahead/PBTypeaheadTemplate.swift index 8b85d1f9e..f80d58fc4 100644 --- a/Sources/Playbook/Components/Typeahead/PBTypeaheadTemplate.swift +++ b/Sources/Playbook/Components/Typeahead/PBTypeaheadTemplate.swift @@ -9,6 +9,32 @@ import SwiftUI +struct SectionList: Identifiable { + let id: UUID = UUID() + let section: String? + let items: [PBTypeahead.Option] + let button: PBButton? + static func == (lhs: SectionList, rhs: SectionList) -> Bool { lhs.id == rhs.id } +} + +enum ListElement: Identifiable { + case section(String) + case item(PBTypeahead.Option) + case button(PBButton) + + var id: UUID { + switch self { + case .section(let title): + return UUID(uuidString: title.hashValue.description) ?? UUID() + case .item(let option): + return UUID(uuidString: option.id) ?? UUID() + case .button(let button): + return UUID(uuidString: button.title ?? "id") ?? UUID() + } + } +} + + public struct PBTypeaheadTemplate: View { private let id: Int private let title: String @@ -128,38 +154,35 @@ public struct PBTypeaheadTemplate: View { } } } - } +} @MainActor private extension PBTypeaheadTemplate { var showPopover: Binding { - .init( - get: { - popoverManager.isPopoverActive(for: id) - }, - set: { isActive in - if isActive { - popoverManager.showPopover(for: id) - } else { - popoverManager.hidePopover(for: id) - } - } - ) - } + .init( + get: { + popoverManager.isPopoverActive(for: id) + }, + set: { isActive in + if isActive { + popoverManager.showPopover(for: id) + } else { + popoverManager.hidePopover(for: id) + } + } + ) + } @ViewBuilder var listView: some View { PBCard(alignment: .leading, padding: Spacing.none, shadow: .deeper) { + let flat = Array(zip(flattenedResults.indices, flattenedResults)) ScrollView { VStack(alignment: .leading, spacing: 0) { - ForEach(mapResults, id: \.0) { result in - if let section = result.0 { - sectionView(section) + Group { + ForEach(flat, id: \.0) { index, element in + listElementView(index: index, element: element) } - ForEach(Array(zip(result.1.indices, result.1)), id: \.0) { index, item in - listItemView(item: item, index: index, for: result.0) - } - result.2.padding() } } } @@ -170,13 +193,25 @@ private extension PBTypeaheadTemplate { .frame(maxWidth: .infinity, alignment: .top) } + @ViewBuilder + func listElementView(index: Int, element: ListElement) -> some View { + switch element { + case .section(let title): + sectionView(title) + case .item(let option): + listItemView(item: option, index: index) + case .button(let button): + button.padding() + } + } + func sectionView(_ section: String) -> some View { Text(section).pbFont(.caption) .padding(.top) .padding(.leading) } - func listItemView(item: PBTypeahead.Option, index: Int, for section: String?) -> some View { + func listItemView(item: PBTypeahead.Option, index: Int) -> some View { HStack { if let customView = item.customView?() { customView @@ -188,18 +223,18 @@ private extension PBTypeaheadTemplate { .padding(.horizontal, Spacing.xSmall + 4) .padding(.vertical, Spacing.xSmall + 4) .frame(maxWidth: .infinity, alignment: .leading) - .background(listBackgroundColor(index, section: section)) + .background(listBackgroundColor(index, section: "")) .onHover(disabled: false) { hover in isHovering = hover - hoveringIndex = (index, section) + hoveringIndex = (index, "") } .onTapGesture { - onListSelection(index: index, section: section, option: item) + onListSelection(index: index, section: "", option: item) } } - var mapResults: [(String?, [PBTypeahead.Option], PBButton)] { - var array: [(String?, [PBTypeahead.Option], PBButton)] = [] + var mapResults: [SectionList] { + var array: [SectionList] = [] var currentSection: String? = nil var currentOptions: [PBTypeahead.Option] = [] for result in searchResults { @@ -229,16 +264,34 @@ private extension PBTypeaheadTemplate { return array } + var flattenedResults: [ListElement] { + var elements: [ListElement] = [] + + for section in mapResults { + if let sectionTitle = section.section { + elements.append(.section(sectionTitle)) + } + + elements.append(contentsOf: section.items.map { .item($0) }) + + if let button = section.button { + elements.append(.button(button)) + } + } + + return elements + } + private func appendSectionToArray( section: String?, options: [PBTypeahead.Option], - to array: inout [(String?, [PBTypeahead.Option], PBButton)] + to array: inout [SectionList] ) { let numberOfItems = numberOfItemsShown[section] ?? 2 - array.append(( - section, - Array(options.prefix(numberOfItems)), - PBButton( + array.append(SectionList( + section: section, + items: Array(options.prefix(numberOfItems)), + button: PBButton( variant: .link, title: (numberOfItems == 2) ? "View More" : "View Less", icon: (numberOfItems == 2) ? .fontAwesome(.chevronDown) : .fontAwesome(.chevronUp) @@ -301,7 +354,7 @@ private extension PBTypeaheadTemplate { if event.keyCode == 36 { // return bar if let index = hoveringIndex.0, let section = hoveringIndex.1, - let results = mapResults.first?.1, + let results = mapResults.first?.items, index <= results.count-1, isFocused { onListSelection(index: index, section: section, option: results[index]) } @@ -310,7 +363,7 @@ private extension PBTypeaheadTemplate { if isFocused { if let index = hoveringIndex.0, let section = hoveringIndex.1, - let results = mapResults.first?.1, + let results = mapResults.first?.items, index <= results.count-1, searchText.isEmpty { onListSelection(index: index, section: section, option: results[index]) } else { From 60c6a3f247590dddb44c0b0bc36a9a26f3838e99 Mon Sep 17 00:00:00 2001 From: "isis.silva" Date: Wed, 11 Dec 2024 15:02:51 -0500 Subject: [PATCH 11/20] removed duplicated code --- .../Typeahead/PBTypeaheadTemplate.swift | 30 +++++-------------- .../Components/Typeahead/Typeahead.swift | 3 ++ 2 files changed, 10 insertions(+), 23 deletions(-) diff --git a/Sources/Playbook/Components/Typeahead/PBTypeaheadTemplate.swift b/Sources/Playbook/Components/Typeahead/PBTypeaheadTemplate.swift index f80d58fc4..2cde10e0e 100644 --- a/Sources/Playbook/Components/Typeahead/PBTypeaheadTemplate.swift +++ b/Sources/Playbook/Components/Typeahead/PBTypeaheadTemplate.swift @@ -17,24 +17,6 @@ struct SectionList: Identifiable { static func == (lhs: SectionList, rhs: SectionList) -> Bool { lhs.id == rhs.id } } -enum ListElement: Identifiable { - case section(String) - case item(PBTypeahead.Option) - case button(PBButton) - - var id: UUID { - switch self { - case .section(let title): - return UUID(uuidString: title.hashValue.description) ?? UUID() - case .item(let option): - return UUID(uuidString: option.id) ?? UUID() - case .button(let button): - return UUID(uuidString: button.title ?? "id") ?? UUID() - } - } -} - - public struct PBTypeaheadTemplate: View { private let id: Int private let title: String @@ -194,7 +176,7 @@ private extension PBTypeaheadTemplate { } @ViewBuilder - func listElementView(index: Int, element: ListElement) -> some View { + func listElementView(index: Int, element: PBTypeahead.OptionType) -> some View { switch element { case .section(let title): sectionView(title) @@ -252,6 +234,9 @@ private extension PBTypeaheadTemplate { case .item(let option): currentOptions.append(option) + + case .button(_): break + } } if !currentOptions.isEmpty || currentSection != nil { @@ -264,8 +249,8 @@ private extension PBTypeaheadTemplate { return array } - var flattenedResults: [ListElement] { - var elements: [ListElement] = [] + var flattenedResults: [PBTypeahead.OptionType] { + var elements: [PBTypeahead.OptionType] = [] for section in mapResults { if let sectionTitle = section.section { @@ -311,8 +296,7 @@ private extension PBTypeaheadTemplate { } else { return item.id.localizedCaseInsensitiveContains(searchText) } - case .section(_): - return true + default: return true } } let selectedIds = Set(selectedOptions.map { $0.id }) diff --git a/Sources/Playbook/Components/Typeahead/Typeahead.swift b/Sources/Playbook/Components/Typeahead/Typeahead.swift index b0b022e8a..24810389c 100644 --- a/Sources/Playbook/Components/Typeahead/Typeahead.swift +++ b/Sources/Playbook/Components/Typeahead/Typeahead.swift @@ -34,10 +34,13 @@ public extension PBTypeahead { return str case .item(let item): return item.id + case .button(let button): + return button.title ?? "" } } case section(String) case item(Option) + case button(PBButton) } enum Selection { From 21f31adcf5125c7acb60d0f3fd57dfaf1c6589e1 Mon Sep 17 00:00:00 2001 From: "isis.silva" Date: Wed, 11 Dec 2024 15:26:15 -0500 Subject: [PATCH 12/20] removed section from hover index --- .../Typeahead/PBTypeaheadTemplate.swift | 53 +++++++++---------- 1 file changed, 24 insertions(+), 29 deletions(-) diff --git a/Sources/Playbook/Components/Typeahead/PBTypeaheadTemplate.swift b/Sources/Playbook/Components/Typeahead/PBTypeaheadTemplate.swift index 2cde10e0e..881a64dea 100644 --- a/Sources/Playbook/Components/Typeahead/PBTypeaheadTemplate.swift +++ b/Sources/Playbook/Components/Typeahead/PBTypeaheadTemplate.swift @@ -29,7 +29,7 @@ public struct PBTypeaheadTemplate: View { private let listOffset: (x: CGFloat, y: CGFloat) private let clearAction: (() -> Void)? - @State private var hoveringIndex: (Int?, String?) + @State private var hoveringIndex: Int? @State private var isHovering: Bool = false @State private var contentSize: CGSize = .zero @State private var selectedIndex: Int? @@ -120,7 +120,7 @@ public struct PBTypeaheadTemplate: View { .onChange(of: selectedOptions.count) { reloadList } - .onChange(of: hoveringIndex.0) { + .onChange(of: hoveringIndex) { reloadList } .onChange(of: popoverManager.isPopoverActive(for: id)) { _, newValue in @@ -208,10 +208,10 @@ private extension PBTypeaheadTemplate { .background(listBackgroundColor(index, section: "")) .onHover(disabled: false) { hover in isHovering = hover - hoveringIndex = (index, "") + hoveringIndex = index } .onTapGesture { - onListSelection(index: index, section: "", option: item) + onListSelection(index: index, option: item) } } @@ -231,12 +231,9 @@ private extension PBTypeaheadTemplate { currentOptions = [] } currentSection = section - case .item(let option): currentOptions.append(option) - case .button(_): break - } } if !currentOptions.isEmpty || currentSection != nil { @@ -325,7 +322,7 @@ private extension PBTypeaheadTemplate { searchText = "" selectedOptions.removeAll() selectedIndex = nil - hoveringIndex = (nil, nil) + hoveringIndex = nil popoverManager.hidePopover(for: id) } @@ -336,20 +333,18 @@ private extension PBTypeaheadTemplate { focused = true } if event.keyCode == 36 { // return bar - if let index = hoveringIndex.0, - let section = hoveringIndex.1, + if let index = hoveringIndex, let results = mapResults.first?.items, index <= results.count-1, isFocused { - onListSelection(index: index, section: section, option: results[index]) + onListSelection(index: index, option: results[index]) } } if event.keyCode == 49 { // space if isFocused { - if let index = hoveringIndex.0, - let section = hoveringIndex.1, + if let index = hoveringIndex, let results = mapResults.first?.items, index <= results.count-1, searchText.isEmpty { - onListSelection(index: index, section: section, option: results[index]) + onListSelection(index: index, option: results[index]) } else { popoverManager.showPopover(for: id) } @@ -362,17 +357,17 @@ private extension PBTypeaheadTemplate { } if event.keyCode == 125 { // arrow down if isFocused { - if let index = hoveringIndex.0 { - hoveringIndex.0 = index < searchResults.count ? (index + 1) : 0 + if let index = hoveringIndex { + hoveringIndex = index < searchResults.count ? (index + 1) : 0 } else { - hoveringIndex.0 = 0 + hoveringIndex = 0 } } } else { if event.keyCode == 126 { // arrow up - if isFocused, let index = hoveringIndex.0 { - hoveringIndex.0 = index > 1 ? (index - 1) : 0 + if isFocused, let index = hoveringIndex { + hoveringIndex = index > 1 ? (index - 1) : 0 } } } @@ -399,13 +394,13 @@ private extension PBTypeaheadTemplate { popoverManager.update(with: id) } - func onListSelection(index: Int, section: String?, option: PBTypeahead.Option) { + func onListSelection(index: Int, option: PBTypeahead.Option) { if option.text != noOptionsText { switch selection { case .single: - onSingleSelection(index: index, section: section, option) + onSingleSelection(index: index, option) case .multiple: - onMultipleSelection(index: index, section: section, option) + onMultipleSelection(index: index, option) } } popoverManager.hidePopover(for: id) @@ -414,20 +409,20 @@ private extension PBTypeaheadTemplate { isFocused = true } - func onSingleSelection(index: Int, section: String?, _ option: PBTypeahead.Option) { + func onSingleSelection(index: Int, _ option: PBTypeahead.Option) { selectedOptions.removeAll() - if hoveringIndex.0 == index && hoveringIndex.1 == section { + if hoveringIndex == index { selectedOptions.append(option) } selectedIndex = index - hoveringIndex = (index, section) + hoveringIndex = index } - func onMultipleSelection(index: Int, section: String?, _ option: PBTypeahead.Option) { - if hoveringIndex.0 == index && hoveringIndex.1 == section { + func onMultipleSelection(index: Int, _ option: PBTypeahead.Option) { + if hoveringIndex == index { selectedOptions.append(option) } - hoveringIndex = (index, section) + hoveringIndex = index selectedIndex = nil } @@ -447,7 +442,7 @@ private extension PBTypeaheadTemplate { default: break } #if os(macOS) - return hoveringIndex.0 == index && hoveringIndex.1 == section ? .hover : .card + return hoveringIndex == index ? .hover : .card #elseif os(iOS) return .card #endif From 5c08e05a1fec567420599dcef790eae6d2ba4d9e Mon Sep 17 00:00:00 2001 From: "isis.silva" Date: Mon, 16 Dec 2024 09:41:29 -0500 Subject: [PATCH 13/20] wip --- .../Components/Typeahead/PBTypeahead.swift | 30 +-- .../Typeahead/PBTypeaheadTemplate.swift | 175 +++++++++--------- .../Components/Typeahead/Typeahead.swift | 15 ++ .../Typeahead/TypeaheadCatalog.swift | 2 +- 4 files changed, 116 insertions(+), 106 deletions(-) diff --git a/Sources/Playbook/Components/Typeahead/PBTypeahead.swift b/Sources/Playbook/Components/Typeahead/PBTypeahead.swift index d65e3754b..f5228eb39 100644 --- a/Sources/Playbook/Components/Typeahead/PBTypeahead.swift +++ b/Sources/Playbook/Components/Typeahead/PBTypeahead.swift @@ -129,19 +129,18 @@ public struct PBTypeahead: View { @MainActor private extension PBTypeahead { - - var showPopover: Binding { + var showPopover: Binding { .init( - get: { - popoverManager.isPopoverActive(for: id) - }, - set: { isActive in - if isActive { - popoverManager.showPopover(for: id) - } else { - popoverManager.hidePopover(for: id) - } + get: { + popoverManager.isPopoverActive(for: id) + }, + set: { isActive in + if isActive { + popoverManager.showPopover(for: id) + } else { + popoverManager.hidePopover(for: id) } + } ) } @@ -160,7 +159,6 @@ private extension PBTypeahead { ScrollView { VStack(spacing: 0) { ForEach(Array(zip(searchResults.indices, searchResults)), id: \.0) { index, result in - if #available(iOS 17.0, *), #available(macOS 14.0, *) { listItemView(index: index, option: result) .focusable() .focused($isFocused) @@ -181,14 +179,6 @@ private extension PBTypeahead { isFocused = true hoveringIndex = 0 } - } else { - listItemView(index: index, option: result) - .focused($isFocused) - .onAppear { - isFocused = true - hoveringIndex = 0 - } - } } } } diff --git a/Sources/Playbook/Components/Typeahead/PBTypeaheadTemplate.swift b/Sources/Playbook/Components/Typeahead/PBTypeaheadTemplate.swift index 881a64dea..b0aa73b27 100644 --- a/Sources/Playbook/Components/Typeahead/PBTypeaheadTemplate.swift +++ b/Sources/Playbook/Components/Typeahead/PBTypeaheadTemplate.swift @@ -9,14 +9,6 @@ import SwiftUI -struct SectionList: Identifiable { - let id: UUID = UUID() - let section: String? - let items: [PBTypeahead.Option] - let button: PBButton? - static func == (lhs: SectionList, rhs: SectionList) -> Bool { lhs.id == rhs.id } -} - public struct PBTypeaheadTemplate: View { private let id: Int private let title: String @@ -31,10 +23,9 @@ public struct PBTypeaheadTemplate: View { @State private var hoveringIndex: Int? @State private var isHovering: Bool = false - @State private var contentSize: CGSize = .zero @State private var selectedIndex: Int? @State private var focused: Bool = false - @State var numberOfItemsShown: [String?: Int] = [:] + @State private var numberOfItemsShown: [String?: Int] = [:] @Binding var selectedOptions: [PBTypeahead.Option] @Binding var searchText: String @FocusState.Binding private var isFocused: Bool @@ -83,7 +74,6 @@ public struct PBTypeaheadTemplate: View { onItemTap: { removeSelected($0) }, onViewTap: { onViewTap } ) - .sizeReader { contentSize = $0 } .pbPopover( isPresented: showPopover, id: id, @@ -159,20 +149,41 @@ private extension PBTypeaheadTemplate { var listView: some View { PBCard(alignment: .leading, padding: Spacing.none, shadow: .deeper) { let flat = Array(zip(flattenedResults.indices, flattenedResults)) - ScrollView { - VStack(alignment: .leading, spacing: 0) { - Group { + ScrollViewReader { proxy in + ScrollView { + VStack(alignment: .leading, spacing: 0) { ForEach(flat, id: \.0) { index, element in listElementView(index: index, element: element) + .focusable() + .focused($isFocused) + .focusEffectDisabled() + .onKeyPress(.upArrow, action: { + if let index = hoveringIndex, index > 0 { + proxy.scrollTo(index > 1 ? (index - 1) : 0) + } + return .handled + }) + .onKeyPress(.downArrow) { + if let index = hoveringIndex, index != searchResults.count-1 { + proxy.scrollTo(index < searchResults.count ? (index + 1) : 0) + } + return .handled + } + .onAppear { + isFocused = true + hoveringIndex = 0 + } } } } + .scrollDismissesKeyboard(.immediately) + .frame(maxHeight: dropdownMaxHeight) + .fixedSize(horizontal: false, vertical: true) } - .scrollDismissesKeyboard(.immediately) - .frame(maxHeight: dropdownMaxHeight) - .fixedSize(horizontal: false, vertical: true) } - .frame(maxWidth: .infinity, alignment: .top) + .onAppear { + isFocused = true + } } @ViewBuilder @@ -215,8 +226,8 @@ private extension PBTypeaheadTemplate { } } - var mapResults: [SectionList] { - var array: [SectionList] = [] + var mapResults: [PBTypeahead.SectionList] { + var array: [PBTypeahead.SectionList] = [] var currentSection: String? = nil var currentOptions: [PBTypeahead.Option] = [] for result in searchResults { @@ -248,29 +259,25 @@ private extension PBTypeaheadTemplate { var flattenedResults: [PBTypeahead.OptionType] { var elements: [PBTypeahead.OptionType] = [] - for section in mapResults { if let sectionTitle = section.section { elements.append(.section(sectionTitle)) } - elements.append(contentsOf: section.items.map { .item($0) }) - if let button = section.button { elements.append(.button(button)) } } - return elements } private func appendSectionToArray( section: String?, options: [PBTypeahead.Option], - to array: inout [SectionList] + to array: inout [PBTypeahead.SectionList] ) { let numberOfItems = numberOfItemsShown[section] ?? 2 - array.append(SectionList( + array.append(PBTypeahead.SectionList( section: section, items: Array(options.prefix(numberOfItems)), button: PBButton( @@ -318,7 +325,7 @@ private extension PBTypeaheadTemplate { var clear: Void { if let action = clearAction { action() - } + } searchText = "" selectedOptions.removeAll() selectedIndex = nil @@ -326,56 +333,6 @@ private extension PBTypeaheadTemplate { popoverManager.hidePopover(for: id) } - var setKeyboardControls: Void { - #if os(macOS) - NSEvent.addLocalMonitorForEvents(matching: .keyDown) { event in - if event.keyCode == 48 { // tab - focused = true - } - if event.keyCode == 36 { // return bar - if let index = hoveringIndex, - let results = mapResults.first?.items, - index <= results.count-1, isFocused { - onListSelection(index: index, option: results[index]) - } - } - if event.keyCode == 49 { // space - if isFocused { - if let index = hoveringIndex, - let results = mapResults.first?.items, - index <= results.count-1, searchText.isEmpty { - onListSelection(index: index, option: results[index]) - } else { - popoverManager.showPopover(for: id) - } - } - } - if event.keyCode == 51 { // delete - if let lastElementIndex = selectedOptions.indices.last, isFocused, searchText.isEmpty, !selectedOptions.isEmpty { - removeSelected(lastElementIndex) - } - } - if event.keyCode == 125 { // arrow down - if isFocused { - if let index = hoveringIndex { - hoveringIndex = index < searchResults.count ? (index + 1) : 0 - } else { - hoveringIndex = 0 - } - } - } - else { - if event.keyCode == 126 { // arrow up - if isFocused, let index = hoveringIndex { - hoveringIndex = index > 1 ? (index - 1) : 0 - } - } - } - return event - } - #endif - } - private func togglePopover() { if popoverManager.isPopoverActive(for: id) { popoverManager.hidePopover(for: id) @@ -411,25 +368,26 @@ private extension PBTypeaheadTemplate { func onSingleSelection(index: Int, _ option: PBTypeahead.Option) { selectedOptions.removeAll() - if hoveringIndex == index { - selectedOptions.append(option) - } + selectedOptions = [option] selectedIndex = index hoveringIndex = index + selectedOptions.append(option) + reloadList } func onMultipleSelection(index: Int, _ option: PBTypeahead.Option) { - if hoveringIndex == index { - selectedOptions.append(option) - } - hoveringIndex = index + selectedOptions.append(option) + hoveringIndex = nil selectedIndex = nil + reloadList } func removeSelected(_ index: Int) { if let selectedElementIndex = selectedOptions.indices.first(where: { $0 == index }) { - _ = selectedOptions.remove(at: selectedElementIndex) + let _ = selectedOptions.remove(at: selectedElementIndex) selectedIndex = nil + hoveringIndex = 0 + reloadList } } @@ -455,6 +413,53 @@ private extension PBTypeaheadTemplate { return .text(.default) } } + + var setKeyboardControls: Void { + #if os(macOS) + NSEvent.addLocalMonitorForEvents(matching: .keyDown) { event in + if event.keyCode == 48 { // tab + focused = true + } + if event.keyCode == 36 { // return bar + if isFocused, let index = hoveringIndex, index <= searchResults.count-1, let results = mapResults.first?.items, popoverManager.isPopoverActive(for: id) { + onListSelection(index: index, option: results[index-1]) + } + } + if event.keyCode == 49 { // space + if isFocused { + if let index = hoveringIndex, + let results = mapResults.first?.items, + popoverManager.isPopoverActive(for: id), + index <= results.count-1, searchText.isEmpty { + onListSelection(index: index, option: results[index]) + } else { + popoverManager.showPopover(for: id) + } + } + } + if event.keyCode == 51 { // delete + if let lastElementIndex = selectedOptions.indices.last, isFocused, searchText.isEmpty, !selectedOptions.isEmpty { + removeSelected(lastElementIndex) + } + } + if event.keyCode == 125 { // arrow down + if popoverManager.isPopoverActive(for: id), let index = hoveringIndex, index < searchResults.count-1 { + isFocused = true + hoveringIndex = index < searchResults.count ? (index + 1) : 0 + } + } + else { + if event.keyCode == 126 { // arrow up + if popoverManager.isPopoverActive(for: id), let index = hoveringIndex { + isFocused = true + hoveringIndex = index > 1 ? (index - 1) : 0 + } + } + } + return event + } + #endif + } } #Preview { diff --git a/Sources/Playbook/Components/Typeahead/Typeahead.swift b/Sources/Playbook/Components/Typeahead/Typeahead.swift index 24810389c..52cb304c2 100644 --- a/Sources/Playbook/Components/Typeahead/Typeahead.swift +++ b/Sources/Playbook/Components/Typeahead/Typeahead.swift @@ -53,5 +53,20 @@ public extension PBTypeahead { } } } + + struct SectionList: Identifiable { + public let id: UUID + let section: String? + let items: [PBTypeahead.Option] + let button: PBButton? + static func == (lhs: SectionList, rhs: SectionList) -> Bool { lhs.id == rhs.id } + + public init(id: UUID = UUID(), section: String?, items: [PBTypeahead.Option], button: PBButton?) { + self.id = id + self.section = section + self.items = items + self.button = button + } + } } diff --git a/Sources/Playbook/Components/Typeahead/TypeaheadCatalog.swift b/Sources/Playbook/Components/Typeahead/TypeaheadCatalog.swift index b4d6e5a25..23d521c37 100644 --- a/Sources/Playbook/Components/Typeahead/TypeaheadCatalog.swift +++ b/Sources/Playbook/Components/Typeahead/TypeaheadCatalog.swift @@ -31,7 +31,7 @@ public struct TypeaheadCatalog: View { @State private var presentDialog: Bool = false - @StateObject private var popoverManager = PopoverManager() + @StateObject private var popoverManager = PopoverManager.shared public var body: some View { PBDocStack(title: "Typeahead") { From de7694ab4b99a48d5c94b01dd6378c734fc55519 Mon Sep 17 00:00:00 2001 From: "isis.silva" Date: Tue, 17 Dec 2024 06:35:26 -0500 Subject: [PATCH 14/20] Fix show typeahead popover --- .../Components/Typeahead/PBTypeahead.swift | 83 +++++++-------- .../Typeahead/PBTypeaheadTemplate.swift | 100 ++++++++---------- .../Typeahead/TypeaheadCatalog.swift | 9 +- 3 files changed, 89 insertions(+), 103 deletions(-) diff --git a/Sources/Playbook/Components/Typeahead/PBTypeahead.swift b/Sources/Playbook/Components/Typeahead/PBTypeahead.swift index f5228eb39..bd73fc5e6 100644 --- a/Sources/Playbook/Components/Typeahead/PBTypeahead.swift +++ b/Sources/Playbook/Components/Typeahead/PBTypeahead.swift @@ -21,6 +21,7 @@ public struct PBTypeahead: View { private let listOffset: (x: CGFloat, y: CGFloat) private let clearAction: (() -> Void)? + @State private var showPopover: Bool = false @State private var hoveringIndex: Int? @State private var isHovering: Bool = false @State private var selectedIndex: Int? @@ -28,7 +29,7 @@ public struct PBTypeahead: View { @Binding var selectedOptions: [PBTypeahead.Option] @Binding var searchText: String @FocusState.Binding private var isFocused: Bool - @State private var popoverManager = PopoverManager.shared + private var popoverManager = PopoverManager.shared public init( id: Int, @@ -74,7 +75,7 @@ public struct PBTypeahead: View { onViewTap: { onViewTap } ) .pbPopover( - isPresented: showPopover, + isPresented: $showPopover, id: id, position: .bottom(listOffset.x, listOffset.y), variant: .dropdown, @@ -90,10 +91,10 @@ public struct PBTypeahead: View { focused = isFocused if debounce.numberOfCharacters == 0 { if isFocused { - popoverManager.showPopover(for: id) + showPopover = true reloadList } else { - popoverManager.hidePopover(for: id) + showPopover = false } } setKeyboardControls @@ -103,7 +104,7 @@ public struct PBTypeahead: View { } .onChange(of: isFocused) { _, newValue in if newValue { - popoverManager.showPopover(for: id) + showPopover = true } } .onChange(of: selectedOptions.count) { @@ -112,16 +113,26 @@ public struct PBTypeahead: View { .onChange(of: hoveringIndex) { reloadList } - .onChange(of: popoverManager.isPopoverActive(for: id)) { _, newValue in + .onChange(of: showPopover) { _, newValue in if newValue { isFocused = true + popoverManager.showPopover(for: id) + } else { + isFocused = false + popoverManager.hidePopover(for: id) + } + } + .onChange(of: popoverManager.isPopoverActive(for: id)) { _, newValue in + if !newValue { + showPopover = false + popoverManager.hidePopover(for: id) } } .onChange(of: searchText, debounce: debounce) { _ in _ = searchResults reloadList if !searchText.isEmpty { - popoverManager.showPopover(for: id) + showPopover = true } } } @@ -129,29 +140,6 @@ public struct PBTypeahead: View { @MainActor private extension PBTypeahead { - var showPopover: Binding { - .init( - get: { - popoverManager.isPopoverActive(for: id) - }, - set: { isActive in - if isActive { - popoverManager.showPopover(for: id) - } else { - popoverManager.hidePopover(for: id) - } - } - ) - } - - private func togglePopover() { - if popoverManager.isPopoverActive(for: id) { - popoverManager.hidePopover(for: id) - } else { - popoverManager.showPopover(for: id) - } - } - @ViewBuilder var listView: some View { PBCard(alignment: .leading, padding: Spacing.none, shadow: .deeper) { @@ -159,7 +147,7 @@ private extension PBTypeahead { ScrollView { VStack(spacing: 0) { ForEach(Array(zip(searchResults.indices, searchResults)), id: \.0) { index, result in - listItemView(index: index, option: result) + listItemView(option: result, index: index) .focusable() .focused($isFocused) .focusEffectDisabled() @@ -176,7 +164,7 @@ private extension PBTypeahead { return .handled } .onAppear { - isFocused = true +// isFocused = true hoveringIndex = 0 } } @@ -187,12 +175,9 @@ private extension PBTypeahead { .fixedSize(horizontal: false, vertical: true) } } - .onAppear { - isFocused = true - } } - func listItemView(index: Int, option: PBTypeahead.Option) -> some View { + func listItemView(option: PBTypeahead.Option, index: Int) -> some View { HStack { if option.text == noOptionsText { emptyView @@ -264,10 +249,13 @@ private extension PBTypeahead { } searchText = "" selectedOptions.removeAll() - selectedOptions = [] selectedIndex = nil hoveringIndex = nil - popoverManager.hidePopover(for: id) + showPopover = false + } + + private func togglePopover() { + showPopover.toggle() } var onViewTap: Void { @@ -277,7 +265,6 @@ private extension PBTypeahead { var reloadList: Void { isHovering.toggle() - popoverManager.update(with: id) } func onListSelection(index: Int, option: PBTypeahead.Option) { @@ -289,7 +276,7 @@ private extension PBTypeahead { onMultipleSelection(option) } } - popoverManager.hidePopover(for: id) + showPopover = false searchText = "" reloadList isFocused = true @@ -350,16 +337,21 @@ private extension PBTypeahead { focused = true } if event.keyCode == 36 { // return bar - if isFocused, let index = hoveringIndex, index <= searchResults.count-1 { + if isFocused, + let index = hoveringIndex, + index <= searchResults.count-1 { onListSelection(index: index, option: searchResults[index]) } } if event.keyCode == 49 { // space if isFocused { - if let index = hoveringIndex, index <= searchResults.count-1, searchText.isEmpty, popoverManager.isPopoverActive(for: id) { + if let index = hoveringIndex, + index <= searchResults.count-1, + searchText.isEmpty, + showPopover { onListSelection(index: index, option: searchResults[index]) } else { - popoverManager.showPopover(for: id) + showPopover = true } } } @@ -369,14 +361,14 @@ private extension PBTypeahead { } } if event.keyCode == 125 { // arrow down - if popoverManager.isPopoverActive(for: id), let index = hoveringIndex, index < searchResults.count-1 { + if showPopover, let index = hoveringIndex, index < searchResults.count-1 { isFocused = true hoveringIndex = index < searchResults.count ? (index + 1) : 0 } } else { if event.keyCode == 126 { // arrow up - if popoverManager.isPopoverActive(for: id), let index = hoveringIndex { + if showPopover, let index = hoveringIndex { isFocused = true hoveringIndex = index > 1 ? (index - 1) : 0 } @@ -392,3 +384,4 @@ private extension PBTypeahead { registerFonts() return TypeaheadCatalog() } + diff --git a/Sources/Playbook/Components/Typeahead/PBTypeaheadTemplate.swift b/Sources/Playbook/Components/Typeahead/PBTypeaheadTemplate.swift index b0aa73b27..be7b52990 100644 --- a/Sources/Playbook/Components/Typeahead/PBTypeaheadTemplate.swift +++ b/Sources/Playbook/Components/Typeahead/PBTypeaheadTemplate.swift @@ -21,15 +21,16 @@ public struct PBTypeaheadTemplate: View { private let listOffset: (x: CGFloat, y: CGFloat) private let clearAction: (() -> Void)? + @State private var showPopover: Bool = false @State private var hoveringIndex: Int? @State private var isHovering: Bool = false @State private var selectedIndex: Int? @State private var focused: Bool = false - @State private var numberOfItemsShown: [String?: Int] = [:] @Binding var selectedOptions: [PBTypeahead.Option] @Binding var searchText: String @FocusState.Binding private var isFocused: Bool - @State private var popoverManager = PopoverManager.shared + @State private var numberOfItemsShown: [String?: Int] = [:] + private var popoverManager = PopoverManager.shared public init( id: Int, @@ -56,8 +57,8 @@ public struct PBTypeaheadTemplate: View { self.dropdownMaxHeight = dropdownMaxHeight self.listOffset = listOffset self._isFocused = isFocused + self.clearAction = clearAction self.noOptionsText = noOptionsText - self.clearAction = clearAction self._selectedOptions = selectedOptions } @@ -75,7 +76,7 @@ public struct PBTypeaheadTemplate: View { onViewTap: { onViewTap } ) .pbPopover( - isPresented: showPopover, + isPresented: $showPopover, id: id, position: .bottom(listOffset.x, listOffset.y), variant: .dropdown, @@ -91,10 +92,10 @@ public struct PBTypeaheadTemplate: View { focused = isFocused if debounce.numberOfCharacters == 0 { if isFocused { - popoverManager.showPopover(for: id) + showPopover = true reloadList } else { - popoverManager.hidePopover(for: id) + showPopover = false } } setKeyboardControls @@ -104,7 +105,7 @@ public struct PBTypeaheadTemplate: View { } .onChange(of: isFocused) { _, newValue in if newValue { - popoverManager.showPopover(for: id) + showPopover = true } } .onChange(of: selectedOptions.count) { @@ -113,16 +114,26 @@ public struct PBTypeaheadTemplate: View { .onChange(of: hoveringIndex) { reloadList } - .onChange(of: popoverManager.isPopoverActive(for: id)) { _, newValue in - if newValue { - isFocused = true - } + .onChange(of: showPopover) { _, newValue in + if newValue { + isFocused = true + popoverManager.showPopover(for: id) + } else { + isFocused = false + popoverManager.hidePopover(for: id) + } + } + .onChange(of: popoverManager.isPopoverActive(for: id)) { _, newValue in + if !newValue { + showPopover = false + popoverManager.hidePopover(for: id) } + } .onChange(of: searchText, debounce: debounce) { _ in _ = searchResults reloadList if !searchText.isEmpty { - popoverManager.showPopover(for: id) + showPopover = true } } } @@ -130,21 +141,6 @@ public struct PBTypeaheadTemplate: View { @MainActor private extension PBTypeaheadTemplate { - var showPopover: Binding { - .init( - get: { - popoverManager.isPopoverActive(for: id) - }, - set: { isActive in - if isActive { - popoverManager.showPopover(for: id) - } else { - popoverManager.hidePopover(for: id) - } - } - ) - } - @ViewBuilder var listView: some View { PBCard(alignment: .leading, padding: Spacing.none, shadow: .deeper) { @@ -170,7 +166,6 @@ private extension PBTypeaheadTemplate { return .handled } .onAppear { - isFocused = true hoveringIndex = 0 } } @@ -181,9 +176,6 @@ private extension PBTypeaheadTemplate { .fixedSize(horizontal: false, vertical: true) } } - .onAppear { - isFocused = true - } } @ViewBuilder @@ -192,7 +184,7 @@ private extension PBTypeaheadTemplate { case .section(let title): sectionView(title) case .item(let option): - listItemView(item: option, index: index) + listItemView(option: option, index: index) case .button(let button): button.padding() } @@ -204,25 +196,25 @@ private extension PBTypeaheadTemplate { .padding(.leading) } - func listItemView(item: PBTypeahead.Option, index: Int) -> some View { + func listItemView(option: PBTypeahead.Option, index: Int) -> some View { HStack { - if let customView = item.customView?() { + if let customView = option.customView?() { customView } else { - Text(item.text ?? item.id) + Text(option.text ?? option.id) .pbFont(.body, color: listTextolor(index)) } } .padding(.horizontal, Spacing.xSmall + 4) .padding(.vertical, Spacing.xSmall + 4) .frame(maxWidth: .infinity, alignment: .leading) - .background(listBackgroundColor(index, section: "")) + .background(listBackgroundColor(index)) .onHover(disabled: false) { hover in isHovering = hover hoveringIndex = index } .onTapGesture { - onListSelection(index: index, option: item) + onListSelection(index: index, option: option) } } @@ -330,15 +322,11 @@ private extension PBTypeaheadTemplate { selectedOptions.removeAll() selectedIndex = nil hoveringIndex = nil - popoverManager.hidePopover(for: id) + showPopover = false } private func togglePopover() { - if popoverManager.isPopoverActive(for: id) { - popoverManager.hidePopover(for: id) - } else { - popoverManager.showPopover(for: id) - } + showPopover.toggle() } var onViewTap: Void { @@ -348,7 +336,6 @@ private extension PBTypeaheadTemplate { var reloadList: Void { isHovering.toggle() - popoverManager.update(with: id) } func onListSelection(index: Int, option: PBTypeahead.Option) { @@ -357,10 +344,10 @@ private extension PBTypeaheadTemplate { case .single: onSingleSelection(index: index, option) case .multiple: - onMultipleSelection(index: index, option) + onMultipleSelection(option) } } - popoverManager.hidePopover(for: id) + showPopover = false searchText = "" reloadList isFocused = true @@ -375,7 +362,7 @@ private extension PBTypeaheadTemplate { reloadList } - func onMultipleSelection(index: Int, _ option: PBTypeahead.Option) { + func onMultipleSelection(_ option: PBTypeahead.Option) { selectedOptions.append(option) hoveringIndex = nil selectedIndex = nil @@ -391,7 +378,7 @@ private extension PBTypeaheadTemplate { } } - func listBackgroundColor(_ index: Int?, section: String?) -> Color { + func listBackgroundColor(_ index: Int?) -> Color { switch selection { case .single: if selectedIndex != nil, selectedIndex == index { @@ -421,7 +408,11 @@ private extension PBTypeaheadTemplate { focused = true } if event.keyCode == 36 { // return bar - if isFocused, let index = hoveringIndex, index <= searchResults.count-1, let results = mapResults.first?.items, popoverManager.isPopoverActive(for: id) { + if isFocused, + let index = hoveringIndex, + index <= searchResults.count-1, + let results = mapResults.first?.items, + showPopover { onListSelection(index: index, option: results[index-1]) } } @@ -429,11 +420,12 @@ private extension PBTypeaheadTemplate { if isFocused { if let index = hoveringIndex, let results = mapResults.first?.items, - popoverManager.isPopoverActive(for: id), - index <= results.count-1, searchText.isEmpty { + index <= results.count-1, + searchText.isEmpty, + showPopover { onListSelection(index: index, option: results[index]) } else { - popoverManager.showPopover(for: id) + showPopover = true } } } @@ -443,14 +435,14 @@ private extension PBTypeaheadTemplate { } } if event.keyCode == 125 { // arrow down - if popoverManager.isPopoverActive(for: id), let index = hoveringIndex, index < searchResults.count-1 { + if showPopover, let index = hoveringIndex, index < searchResults.count-1 { isFocused = true hoveringIndex = index < searchResults.count ? (index + 1) : 0 } } else { if event.keyCode == 126 { // arrow up - if popoverManager.isPopoverActive(for: id), let index = hoveringIndex { + if showPopover, let index = hoveringIndex { isFocused = true hoveringIndex = index > 1 ? (index - 1) : 0 } diff --git a/Sources/Playbook/Components/Typeahead/TypeaheadCatalog.swift b/Sources/Playbook/Components/Typeahead/TypeaheadCatalog.swift index 23d521c37..403723dcd 100644 --- a/Sources/Playbook/Components/Typeahead/TypeaheadCatalog.swift +++ b/Sources/Playbook/Components/Typeahead/TypeaheadCatalog.swift @@ -48,10 +48,10 @@ public struct TypeaheadCatalog: View { .onTapGesture { dismissFocus() } - .popoverHandler(id: 1) - .popoverHandler(id: 2) - .popoverHandler(id: 3) - .popoverHandler(id: 4) + .popoverHandler(id: 1, blockBackgroundInteractions: true) + .popoverHandler(id: 2, blockBackgroundInteractions: true) + .popoverHandler(id: 3, blockBackgroundInteractions: true) + .popoverHandler(id: 4, blockBackgroundInteractions: true) } } @@ -102,6 +102,7 @@ extension TypeaheadCatalog { searchText: $searchTextSections, options: assetsSection, selection: .multiple(variant: .pill), + dropdownMaxHeight: 200, isFocused: $isFocusedSection, selectedOptions: $selectedSections ) From 2a4d1e3753e9542545882147536873412c9a76ad Mon Sep 17 00:00:00 2001 From: "isis.silva" Date: Tue, 17 Dec 2024 06:42:47 -0500 Subject: [PATCH 15/20] code polishes --- .../Components/Typeahead/PBTypeahead.swift | 95 ++++++------- .../Typeahead/PBTypeaheadTemplate.swift | 127 ++++++++++-------- 2 files changed, 121 insertions(+), 101 deletions(-) diff --git a/Sources/Playbook/Components/Typeahead/PBTypeahead.swift b/Sources/Playbook/Components/Typeahead/PBTypeahead.swift index bd73fc5e6..258e041a5 100644 --- a/Sources/Playbook/Components/Typeahead/PBTypeahead.swift +++ b/Sources/Playbook/Components/Typeahead/PBTypeahead.swift @@ -132,7 +132,7 @@ public struct PBTypeahead: View { _ = searchResults reloadList if !searchText.isEmpty { - showPopover = true + showPopover = true } } } @@ -147,26 +147,25 @@ private extension PBTypeahead { ScrollView { VStack(spacing: 0) { ForEach(Array(zip(searchResults.indices, searchResults)), id: \.0) { index, result in - listItemView(option: result, index: index) - .focusable() - .focused($isFocused) - .focusEffectDisabled() - .onKeyPress(.upArrow, action: { - if let index = hoveringIndex, index > 0 { - proxy.scrollTo(index > 1 ? (index - 1) : 0) - } - return .handled - }) - .onKeyPress(.downArrow) { - if let index = hoveringIndex, index != searchResults.count-1 { - proxy.scrollTo(index < searchResults.count ? (index + 1) : 0) - } - return .handled + listItemView(option: result, index: index) + .focusable() + .focused($isFocused) + .focusEffectDisabled() + .onKeyPress(.upArrow, action: { + if let index = hoveringIndex, index > 0 { + proxy.scrollTo(index > 1 ? (index - 1) : 0) } - .onAppear { -// isFocused = true - hoveringIndex = 0 + return .handled + }) + .onKeyPress(.downArrow) { + if let index = hoveringIndex, index != searchResults.count-1 { + proxy.scrollTo(index < searchResults.count ? (index + 1) : 0) } + return .handled + } + .onAppear { + hoveringIndex = 0 + } } } } @@ -216,6 +215,31 @@ private extension PBTypeahead { .padding(.vertical, Spacing.xSmall + 4) } + func listBackgroundColor(_ index: Int?) -> Color { + switch selection { + case .single: + if selectedIndex != nil, selectedIndex == index { + return .pbPrimary + } + default: break + } + #if os(macOS) + return hoveringIndex == index ? .hover : .card + #elseif os(iOS) + return .card + #endif + } + + func listTextolor(_ index: Int?) -> Color { + if selectedIndex != nil, selectedIndex == index { + return .white + } else { + return .text(.default) + } + } +} + +private extension PBTypeahead { var searchResults: [PBTypeahead.Option] { let filteredOptions = searchText.isEmpty && debounce.numberOfCharacters == 0 ? options : options.filter { if let text = $0.text { @@ -307,29 +331,6 @@ private extension PBTypeahead { } } - func listBackgroundColor(_ index: Int?) -> Color { - switch selection { - case .single: - if selectedIndex != nil, selectedIndex == index { - return .pbPrimary - } - default: break - } - #if os(macOS) - return hoveringIndex == index ? .hover : .card - #elseif os(iOS) - return .card - #endif - } - - func listTextolor(_ index: Int?) -> Color { - if selectedIndex != nil, selectedIndex == index { - return .white - } else { - return .text(.default) - } - } - var setKeyboardControls: Void { #if os(macOS) NSEvent.addLocalMonitorForEvents(matching: .keyDown) { event in @@ -338,17 +339,17 @@ private extension PBTypeahead { } if event.keyCode == 36 { // return bar if isFocused, - let index = hoveringIndex, - index <= searchResults.count-1 { + let index = hoveringIndex, + index <= searchResults.count-1 { onListSelection(index: index, option: searchResults[index]) } } if event.keyCode == 49 { // space if isFocused { if let index = hoveringIndex, - index <= searchResults.count-1, - searchText.isEmpty, - showPopover { + index <= searchResults.count-1, + searchText.isEmpty, + showPopover { onListSelection(index: index, option: searchResults[index]) } else { showPopover = true diff --git a/Sources/Playbook/Components/Typeahead/PBTypeaheadTemplate.swift b/Sources/Playbook/Components/Typeahead/PBTypeaheadTemplate.swift index be7b52990..633ac6ff9 100644 --- a/Sources/Playbook/Components/Typeahead/PBTypeaheadTemplate.swift +++ b/Sources/Playbook/Components/Typeahead/PBTypeaheadTemplate.swift @@ -198,23 +198,85 @@ private extension PBTypeaheadTemplate { func listItemView(option: PBTypeahead.Option, index: Int) -> some View { HStack { - if let customView = option.customView?() { - customView + if option.text == noOptionsText { + emptyView } else { - Text(option.text ?? option.id) - .pbFont(.body, color: listTextolor(index)) + Group { + if let customView = option.customView?() { + customView + } else { + Text(option.text ?? option.id) + .pbFont(.body, color: listTextolor(index)) + } + } + .padding(.horizontal, Spacing.xSmall + 4) + .padding(.vertical, Spacing.xSmall + 4) + .frame(maxWidth: .infinity, alignment: .leading) + .background(listBackgroundColor(index)) + .onHover(disabled: false) { hover in + isHovering = hover + hoveringIndex = index + } + .onTapGesture { + onListSelection(index: index, option: option) + } } } + } + + var emptyView: some View { + HStack { + Spacer() + Text(noOptionsText) + .pbFont(.body, color: .text(.light)) + Spacer() + } .padding(.horizontal, Spacing.xSmall + 4) .padding(.vertical, Spacing.xSmall + 4) - .frame(maxWidth: .infinity, alignment: .leading) - .background(listBackgroundColor(index)) - .onHover(disabled: false) { hover in - isHovering = hover - hoveringIndex = index + } + + func listBackgroundColor(_ index: Int?) -> Color { + switch selection { + case .single: + if selectedIndex != nil, selectedIndex == index { + return .pbPrimary + } + default: break } - .onTapGesture { - onListSelection(index: index, option: option) + #if os(macOS) + return hoveringIndex == index ? .hover : .card + #elseif os(iOS) + return .card + #endif + } + + func listTextolor(_ index: Int?) -> Color { + if selectedIndex != nil, selectedIndex == index { + return .white + } else { + return .text(.default) + } + } +} + +private extension PBTypeaheadTemplate { + var searchResults: [PBTypeahead.OptionType] { + let filteredOptions = searchText.isEmpty && debounce.numberOfCharacters == 0 ? options : options.filter { + switch $0 { + case .item(let item): + if let text = item.text { + return text.localizedCaseInsensitiveContains(searchText) + } else { + return item.id.localizedCaseInsensitiveContains(searchText) + } + default: return true + } + } + let selectedIds = Set(selectedOptions.map { $0.id }) + let filteredSelectedOptions = filteredOptions.filter { !selectedIds.contains($0.id) } + switch selection{ + case .multiple: return filteredSelectedOptions + case .single: return filteredOptions } } @@ -283,26 +345,6 @@ private extension PBTypeaheadTemplate { )) } - var searchResults: [PBTypeahead.OptionType] { - let filteredOptions = searchText.isEmpty && debounce.numberOfCharacters == 0 ? options : options.filter { - switch $0 { - case .item(let item): - if let text = item.text { - return text.localizedCaseInsensitiveContains(searchText) - } else { - return item.id.localizedCaseInsensitiveContains(searchText) - } - default: return true - } - } - let selectedIds = Set(selectedOptions.map { $0.id }) - let filteredSelectedOptions = filteredOptions.filter { !selectedIds.contains($0.id) } - switch selection{ - case .multiple: return filteredSelectedOptions - case .single: return filteredOptions - } - } - var optionsSelected: GridInputField.Selection { let optionsSelected = selectedOptions.map { value in if let content = value.text { @@ -378,29 +420,6 @@ private extension PBTypeaheadTemplate { } } - func listBackgroundColor(_ index: Int?) -> Color { - switch selection { - case .single: - if selectedIndex != nil, selectedIndex == index { - return .pbPrimary - } - default: break - } - #if os(macOS) - return hoveringIndex == index ? .hover : .card - #elseif os(iOS) - return .card - #endif - } - - func listTextolor(_ index: Int?) -> Color { - if selectedIndex != nil, selectedIndex == index { - return .white - } else { - return .text(.default) - } - } - var setKeyboardControls: Void { #if os(macOS) NSEvent.addLocalMonitorForEvents(matching: .keyDown) { event in From c065bc3b83a50dcafa3ecd2b1b607eae910845ff Mon Sep 17 00:00:00 2001 From: "isis.silva" Date: Tue, 17 Dec 2024 16:22:12 -0500 Subject: [PATCH 16/20] wip --- .../Typeahead/PBTypeaheadTemplate.swift | 86 ++++++++++++++++--- 1 file changed, 73 insertions(+), 13 deletions(-) diff --git a/Sources/Playbook/Components/Typeahead/PBTypeaheadTemplate.swift b/Sources/Playbook/Components/Typeahead/PBTypeaheadTemplate.swift index 633ac6ff9..cd91cc0f0 100644 --- a/Sources/Playbook/Components/Typeahead/PBTypeaheadTemplate.swift +++ b/Sources/Playbook/Components/Typeahead/PBTypeaheadTemplate.swift @@ -19,6 +19,7 @@ public struct PBTypeaheadTemplate: View { private let debounce: (time: TimeInterval, numberOfCharacters: Int) private let dropdownMaxHeight: CGFloat? private let listOffset: (x: CGFloat, y: CGFloat) + @State private var numberOfItems: Int = 2 private let clearAction: (() -> Void)? @State private var showPopover: Bool = false @@ -29,7 +30,7 @@ public struct PBTypeaheadTemplate: View { @Binding var selectedOptions: [PBTypeahead.Option] @Binding var searchText: String @FocusState.Binding private var isFocused: Bool - @State private var numberOfItemsShown: [String?: Int] = [:] + @State private var numberOfItemsShow: [String?: Int] = [:] private var popoverManager = PopoverManager.shared public init( @@ -45,6 +46,7 @@ public struct PBTypeaheadTemplate: View { isFocused: FocusState.Binding, selectedOptions: Binding<[PBTypeahead.Option]>, noOptionsText: String = "No options", + numberOfItemsShowForSection: Int = 3, clearAction: (() -> Void)? = nil ) { self.id = id @@ -57,8 +59,9 @@ public struct PBTypeaheadTemplate: View { self.dropdownMaxHeight = dropdownMaxHeight self.listOffset = listOffset self._isFocused = isFocused - self.clearAction = clearAction + self.clearAction = clearAction self.noOptionsText = noOptionsText +// self.numberOfItemsShowForSection = numberOfItemsShowForSection self._selectedOptions = selectedOptions } @@ -284,13 +287,48 @@ private extension PBTypeaheadTemplate { var array: [PBTypeahead.SectionList] = [] var currentSection: String? = nil var currentOptions: [PBTypeahead.Option] = [] + var currentButton: PBButton? = nil + var currentButtonoutro: PBButton? = nil + var buttonAction: () -> Void = {} + + var button1: PBButton = PBButton(title: "1") { if numberOfItemsShow["section 1"] == 2 { + numberOfItemsShow["section 1"] = 4 + } else { + numberOfItemsShow["section 1"] = 2 + } + reloadList + } + + var button2: PBButton = PBButton(title: "2") { + if numberOfItemsShow["section 2"] == 2 { + numberOfItemsShow["section 2"] = 4 + } else { + numberOfItemsShow["section 2"] = 2 + } + reloadList + } + + currentButton = button1 + for result in searchResults { + switch result { + case .section(let section): + + buttonAction = { if numberOfItemsShow[section] == 2 { + numberOfItemsShow[section] = 4 + } else { + numberOfItemsShow[section] = 2 + } + reloadList + } + if !currentOptions.isEmpty || currentSection != nil { appendSectionToArray( section: currentSection, options: currentOptions, + button: currentButtonoutro, to: &array ) currentOptions = [] @@ -298,13 +336,40 @@ private extension PBTypeaheadTemplate { currentSection = section case .item(let option): currentOptions.append(option) - case .button(_): break + case .button(let button): + currentButtonoutro = currentSection == "section 1" ? button1 : button2 + +// currentButtonoutro = PBButton(title: "isis", action: currentButtonoutro?.action) + + +// if !currentOptions.isEmpty || currentButton != nil { +// appendSectionToArray( +// section: nil, +// options: [], +// button: currentButton, +// to: &array +// ) +//// } + + currentButtonoutro = PBButton( + fullWidth: button.fullWidth, + variant: button.variant, + size: button.size, + shape: button.shape, + title: currentSection, + icon: button.icon, + iconPosition: button.iconPosition, + isLoading: button.$isLoading, + action: buttonAction + ) + } } - if !currentOptions.isEmpty || currentSection != nil { + if currentOptions.isEmpty { appendSectionToArray( section: currentSection, options: currentOptions, + button: currentButtonoutro, to: &array ) } @@ -328,21 +393,16 @@ private extension PBTypeaheadTemplate { private func appendSectionToArray( section: String?, options: [PBTypeahead.Option], + button: PBButton?, to array: inout [PBTypeahead.SectionList] ) { - let numberOfItems = numberOfItemsShown[section] ?? 2 + let numberOfItems = numberOfItemsShow[section] ?? 2 array.append(PBTypeahead.SectionList( section: section, items: Array(options.prefix(numberOfItems)), - button: PBButton( - variant: .link, - title: (numberOfItems == 2) ? "View More" : "View Less", - icon: (numberOfItems == 2) ? .fontAwesome(.chevronDown) : .fontAwesome(.chevronUp) - ) { - numberOfItemsShown[section] = (numberOfItems == 2) ? 4 : 2 - reloadList - } + button: button )) + print("button: \(button?.title ?? "0")") } var optionsSelected: GridInputField.Selection { From 6e3a65892030cacc9b02cae146778e084d8b8442 Mon Sep 17 00:00:00 2001 From: "isis.silva" Date: Wed, 18 Dec 2024 07:28:51 -0500 Subject: [PATCH 17/20] add list expanding logic. --- .../Components/Popover/PBPopover.swift | 1 + .../Components/Popover/PopoverManager.swift | 21 ++++--- .../Typeahead/PBTypeaheadTemplate.swift | 61 ++++--------------- .../Typeahead/TypeaheadCatalog.swift | 3 +- .../Resources/Helper Files/Mocks.swift | 10 ++- 5 files changed, 37 insertions(+), 59 deletions(-) diff --git a/Sources/Playbook/Components/Popover/PBPopover.swift b/Sources/Playbook/Components/Popover/PBPopover.swift index cb95f08a0..5930fe997 100644 --- a/Sources/Playbook/Components/Popover/PBPopover.swift +++ b/Sources/Playbook/Components/Popover/PBPopover.swift @@ -75,6 +75,7 @@ public struct Popover: ViewModifier { if let value = newValue[id] { isPresented = value } + updateViewFrame() } .onDisappear { isPresented = false diff --git a/Sources/Playbook/Components/Popover/PopoverManager.swift b/Sources/Playbook/Components/Popover/PopoverManager.swift index cfc414879..172cecdf0 100644 --- a/Sources/Playbook/Components/Popover/PopoverManager.swift +++ b/Sources/Playbook/Components/Popover/PopoverManager.swift @@ -46,23 +46,28 @@ public class PopoverManager: ObservableObject { isPresented[id] = nil } - func removeValues() { - popovers.removeAll() - isPresented.removeAll() - } - func presentPopover(with id: Int, value: Bool) { - isPresented.updateValue(value, forKey: id) + DispatchQueue.main.async { + self.isPresented.updateValue(value, forKey: id) + } } private func dismissPopover(with id: Int) { isPresented[id] = false } - + + func dismissPopovers() { + isPresented.keys.forEach { + isPresented[$0] = false + } + } + func updatePopover(with id: Int, view: AnyView, position: CGPoint?) { if let popover = popovers.first(where: { $0.key == id })?.value, let position = position { let newPopover = Popover(view: view, position: position, close: popover.close) - popovers.updateValue(newPopover, forKey: id) + DispatchQueue.main.async { + self.popovers.updateValue(newPopover, forKey: id) + } } } diff --git a/Sources/Playbook/Components/Typeahead/PBTypeaheadTemplate.swift b/Sources/Playbook/Components/Typeahead/PBTypeaheadTemplate.swift index cd91cc0f0..703a0e193 100644 --- a/Sources/Playbook/Components/Typeahead/PBTypeaheadTemplate.swift +++ b/Sources/Playbook/Components/Typeahead/PBTypeaheadTemplate.swift @@ -19,7 +19,6 @@ public struct PBTypeaheadTemplate: View { private let debounce: (time: TimeInterval, numberOfCharacters: Int) private let dropdownMaxHeight: CGFloat? private let listOffset: (x: CGFloat, y: CGFloat) - @State private var numberOfItems: Int = 2 private let clearAction: (() -> Void)? @State private var showPopover: Bool = false @@ -31,6 +30,7 @@ public struct PBTypeaheadTemplate: View { @Binding var searchText: String @FocusState.Binding private var isFocused: Bool @State private var numberOfItemsShow: [String?: Int] = [:] + private var numberOfItemsToShowInSection: Int private var popoverManager = PopoverManager.shared public init( @@ -46,7 +46,7 @@ public struct PBTypeaheadTemplate: View { isFocused: FocusState.Binding, selectedOptions: Binding<[PBTypeahead.Option]>, noOptionsText: String = "No options", - numberOfItemsShowForSection: Int = 3, + numberOfItemsToShowInSection: Int = 2, clearAction: (() -> Void)? = nil ) { self.id = id @@ -61,7 +61,7 @@ public struct PBTypeaheadTemplate: View { self._isFocused = isFocused self.clearAction = clearAction self.noOptionsText = noOptionsText -// self.numberOfItemsShowForSection = numberOfItemsShowForSection + self.numberOfItemsToShowInSection = numberOfItemsToShowInSection self._selectedOptions = selectedOptions } @@ -129,7 +129,8 @@ public struct PBTypeaheadTemplate: View { .onChange(of: popoverManager.isPopoverActive(for: id)) { _, newValue in if !newValue { showPopover = false - popoverManager.hidePopover(for: id) + popoverManager.dismissPopovers() + reloadList } } .onChange(of: searchText, debounce: debounce) { _ in @@ -288,38 +289,15 @@ private extension PBTypeaheadTemplate { var currentSection: String? = nil var currentOptions: [PBTypeahead.Option] = [] var currentButton: PBButton? = nil - var currentButtonoutro: PBButton? = nil var buttonAction: () -> Void = {} - var button1: PBButton = PBButton(title: "1") { if numberOfItemsShow["section 1"] == 2 { - numberOfItemsShow["section 1"] = 4 - } else { - numberOfItemsShow["section 1"] = 2 - } - reloadList - } - - var button2: PBButton = PBButton(title: "2") { - if numberOfItemsShow["section 2"] == 2 { - numberOfItemsShow["section 2"] = 4 - } else { - numberOfItemsShow["section 2"] = 2 - } - reloadList - } - - currentButton = button1 - for result in searchResults { - switch result { - case .section(let section): - - buttonAction = { if numberOfItemsShow[section] == 2 { - numberOfItemsShow[section] = 4 + buttonAction = { if numberOfItemsShow[section] == numberOfItemsToShowInSection { + numberOfItemsShow[section] = numberOfItemsToShowInSection*2 } else { - numberOfItemsShow[section] = 2 + numberOfItemsShow[section] = numberOfItemsToShowInSection } reloadList } @@ -328,7 +306,7 @@ private extension PBTypeaheadTemplate { appendSectionToArray( section: currentSection, options: currentOptions, - button: currentButtonoutro, + button: currentButton, to: &array ) currentOptions = [] @@ -337,21 +315,7 @@ private extension PBTypeaheadTemplate { case .item(let option): currentOptions.append(option) case .button(let button): - currentButtonoutro = currentSection == "section 1" ? button1 : button2 - -// currentButtonoutro = PBButton(title: "isis", action: currentButtonoutro?.action) - - -// if !currentOptions.isEmpty || currentButton != nil { -// appendSectionToArray( -// section: nil, -// options: [], -// button: currentButton, -// to: &array -// ) -//// } - - currentButtonoutro = PBButton( + currentButton = PBButton( fullWidth: button.fullWidth, variant: button.variant, size: button.size, @@ -362,14 +326,13 @@ private extension PBTypeaheadTemplate { isLoading: button.$isLoading, action: buttonAction ) - } } if currentOptions.isEmpty { appendSectionToArray( section: currentSection, options: currentOptions, - button: currentButtonoutro, + button: currentButton, to: &array ) } @@ -396,7 +359,7 @@ private extension PBTypeaheadTemplate { button: PBButton?, to array: inout [PBTypeahead.SectionList] ) { - let numberOfItems = numberOfItemsShow[section] ?? 2 + let numberOfItems = numberOfItemsShow[section] ?? numberOfItemsToShowInSection array.append(PBTypeahead.SectionList( section: section, items: Array(options.prefix(numberOfItems)), diff --git a/Sources/Playbook/Components/Typeahead/TypeaheadCatalog.swift b/Sources/Playbook/Components/Typeahead/TypeaheadCatalog.swift index 403723dcd..c7ce87493 100644 --- a/Sources/Playbook/Components/Typeahead/TypeaheadCatalog.swift +++ b/Sources/Playbook/Components/Typeahead/TypeaheadCatalog.swift @@ -102,7 +102,7 @@ extension TypeaheadCatalog { searchText: $searchTextSections, options: assetsSection, selection: .multiple(variant: .pill), - dropdownMaxHeight: 200, + dropdownMaxHeight: 400, isFocused: $isFocusedSection, selectedOptions: $selectedSections ) @@ -174,6 +174,7 @@ extension TypeaheadCatalog { popoverManager.hidePopover(for: 2) popoverManager.hidePopover(for: 3) popoverManager.hidePopover(for: 4) + popoverManager.dismissPopovers() } } diff --git a/Sources/Playbook/Resources/Helper Files/Mocks.swift b/Sources/Playbook/Resources/Helper Files/Mocks.swift index 44b1a5d24..2263423fa 100644 --- a/Sources/Playbook/Resources/Helper Files/Mocks.swift +++ b/Sources/Playbook/Resources/Helper Files/Mocks.swift @@ -48,11 +48,19 @@ enum Mocks { .item(.init(id: "2", text: ana.name, customView: { AnyView(ana) })), .item(.init(id: "3", text: patric.name, customView: { AnyView(patric) })), .item(.init(id: "4", text: luccile.name, customView: { AnyView(luccile) })), + .button(PBButton(variant: .link, title: "view more")), .section("section 2"), .item(.init(id: "5", text: andrew.name, customView: { AnyView(andrew) })), .item(.init(id: "6", text: ana.name, customView: { AnyView(ana) })), .item(.init(id: "7", text: patric.name, customView: { AnyView(patric) })), - .item(.init(id: "8", text: luccile.name, customView: { AnyView(luccile) })) + .item(.init(id: "8", text: luccile.name, customView: { AnyView(luccile) })), + .button(PBButton(variant: .primary, title: "view more")), + .section("section 3"), + .item(.init(id: "9", text: andrew.name, customView: { AnyView(andrew) })), + .item(.init(id: "10", text: ana.name, customView: { AnyView(ana) })), + .item(.init(id: "11", text: patric.name, customView: { AnyView(patric) })), + .item(.init(id: "12", text: luccile.name, customView: { AnyView(luccile) })), + .button(PBButton(variant: .primary, title: "view more")) ] static let cities: [String] = [ From c57050821f3f9cfe88f3f5c505723528b08e31b8 Mon Sep 17 00:00:00 2001 From: "isis.silva" Date: Wed, 18 Dec 2024 08:01:39 -0500 Subject: [PATCH 18/20] Release --- .../PlaybookShowcase.xcodeproj/project.pbxproj | 8 ++++---- project.yml | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/PlaybookShowcase/PlaybookShowcase.xcodeproj/project.pbxproj b/PlaybookShowcase/PlaybookShowcase.xcodeproj/project.pbxproj index 7420e8d8b..f3188a0dd 100644 --- a/PlaybookShowcase/PlaybookShowcase.xcodeproj/project.pbxproj +++ b/PlaybookShowcase/PlaybookShowcase.xcodeproj/project.pbxproj @@ -298,7 +298,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 6.5.0; + MARKETING_VERSION = ; PRODUCT_BUNDLE_IDENTIFIER = com.powerhrg.PlaybookShowcase; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -336,7 +336,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 6.5.0; + MARKETING_VERSION = ; PRODUCT_BUNDLE_IDENTIFIER = com.powerhrg.PlaybookShowcase; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "Playbook In House"; @@ -376,7 +376,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.6; - MARKETING_VERSION = 6.5.0; + MARKETING_VERSION = ; PRODUCT_BUNDLE_IDENTIFIER = com.powerhrg.PlaybookShowcase; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -411,7 +411,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.6; - MARKETING_VERSION = 6.5.0; + MARKETING_VERSION = ; PRODUCT_BUNDLE_IDENTIFIER = com.powerhrg.PlaybookShowcase; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "Playbook Showcase Dev ID"; diff --git a/project.yml b/project.yml index 07bce84c9..67759995f 100644 --- a/project.yml +++ b/project.yml @@ -22,7 +22,7 @@ targets: GENERATE_INFOPLIST_FILE: false PRODUCT_BUNDLE_IDENTIFIER: com.powerhrg.PlaybookShowcase CURRENT_PROJECT_VERSION: 1 - MARKETING_VERSION: "6.5.0" + MARKETING_VERSION: "" Playbook-macOS: type: application platform: macOS @@ -35,4 +35,4 @@ targets: GENERATE_INFOPLIST_FILE: false PRODUCT_BUNDLE_IDENTIFIER: com.powerhrg.PlaybookShowcase CURRENT_PROJECT_VERSION: 1 - MARKETING_VERSION: "6.5.0" + MARKETING_VERSION: "" From fd695fd4ad068cb13fd9243e64d2f4372b8cabde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8Dsis?= Date: Mon, 6 Jan 2025 12:54:53 -0500 Subject: [PATCH 19/20] [PBIOS-569] Typeahead section list (#459) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **What does this PR do?** A clear and concise description with your runway ticket url. Add sections to the Typeahead list **Screenshots:** Screenshots to visualize your addition/change Screenshot 2024-10-09 at 4 46 17 PM **How to test?** Steps to confirm the desired behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See addition/change ### Checklist - [ ] **LABELS** - Add a label: `breaking`, `bug`, `improvement`, `documentation`, or `enhancement`. See [Labels](https://github.com/powerhome/playbook-apple/labels) for descriptions. - [ ] **RELEASES** - Add the appropriate label: `Ready for Testing` / `Ready for Release` - [ ] **TESTING** - Have you tested your story? --- Sources/Playbook/Components/Typeahead/Typeahead.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Playbook/Components/Typeahead/Typeahead.swift b/Sources/Playbook/Components/Typeahead/Typeahead.swift index 52cb304c2..226bf5346 100644 --- a/Sources/Playbook/Components/Typeahead/Typeahead.swift +++ b/Sources/Playbook/Components/Typeahead/Typeahead.swift @@ -27,7 +27,7 @@ public extension PBTypeahead { } } - enum OptionType: Identifiable { + enum OptionType: Identifiable, Equatable { public var id: String { switch self { case .section(let str): From ad2edb9c83600c4be784c616f3070d841b176dcb Mon Sep 17 00:00:00 2001 From: "isis.silva" Date: Mon, 6 Jan 2025 13:16:24 -0500 Subject: [PATCH 20/20] conform to equatable --- Sources/Playbook/Components/Typeahead/Typeahead.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Sources/Playbook/Components/Typeahead/Typeahead.swift b/Sources/Playbook/Components/Typeahead/Typeahead.swift index 226bf5346..7469d9e1e 100644 --- a/Sources/Playbook/Components/Typeahead/Typeahead.swift +++ b/Sources/Playbook/Components/Typeahead/Typeahead.swift @@ -28,6 +28,10 @@ public extension PBTypeahead { } enum OptionType: Identifiable, Equatable { + public static func == (lhs: PBTypeahead.OptionType, rhs: PBTypeahead.OptionType) -> Bool { + lhs.id == rhs.id + } + public var id: String { switch self { case .section(let str):