From ee531a2d24072ce153a053902a7ca9967b2f57aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8Dsis?= Date: Fri, 22 Nov 2024 14:11:29 -0500 Subject: [PATCH] [PBIOS-591] Fix popover closing on scroll (#469) **What does this PR do?** A clear and concise description with your runway ticket url. I want the typeahead popover to remain open when scrolling to dismiss the keyboard, So that I can continue interacting with the typeahead options without losing the popover. https://runway.powerhrg.com/backlog_items/PBIOS-591 **Screenshots:** Screenshots to visualize your addition/change **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? --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 6 +- .tool-versions | 2 +- Gemfile | 4 +- Gemfile.lock | 119 ++++++++-------- Package.swift | 4 +- .../Components/Popover/PopoverHandler.swift | 11 ++ .../Components/Popover/PopoverManager.swift | 30 +++- .../Components/Typeahead/GridInputField.swift | 1 + .../Components/Typeahead/PBTypeahead.swift | 128 +++++++++++------- .../Typeahead/TypeaheadCatalog.swift | 28 ++-- .../Extensions/OnScrollDetection.swift | 37 +++++ 11 files changed, 243 insertions(+), 127 deletions(-) create mode 100644 Sources/Playbook/Resources/Extensions/OnScrollDetection.swift diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4add2784f..03d8291f3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,14 +1,14 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 + rev: v4.6.0 hooks: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/nicklockwood/SwiftFormat - rev: 0.53.5 + rev: 0.54.6 hooks: - id: swiftformat - repo: https://github.com/realm/SwiftLint - rev: 0.54.0 + rev: 0.57.0 hooks: - id: swiftlint diff --git a/.tool-versions b/.tool-versions index 3294aeda6..5aa8e0c30 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1 +1 @@ -ruby 3.3.0 +ruby 3.3.6 diff --git a/Gemfile b/Gemfile index 11fafb338..98a22190b 100644 --- a/Gemfile +++ b/Gemfile @@ -2,8 +2,8 @@ source "https://rubygems.org" git_source(:github) { |repo_name| "https://github.com/#{repo_name}" } -gem "fastlane", "~> 2.219.0" -gem "json", "~> 2.7.0" +gem "fastlane", "~> 2.225.0" +gem "json", "~> 2.8.0" gem 'httparty' plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile') diff --git a/Gemfile.lock b/Gemfile.lock index 665a05459..170701c09 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,42 +1,47 @@ GEM remote: https://rubygems.org/ specs: - CFPropertyList (3.0.6) + CFPropertyList (3.0.7) + base64 + nkf rexml - addressable (2.8.6) - public_suffix (>= 2.0.2, < 6.0) - artifactory (3.0.15) + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) + artifactory (3.0.17) atomos (0.1.3) aws-eventstream (1.3.0) - aws-partitions (1.876.0) - aws-sdk-core (3.190.1) + aws-partitions (1.1006.0) + aws-sdk-core (3.212.0) aws-eventstream (~> 1, >= 1.3.0) - aws-partitions (~> 1, >= 1.651.0) - aws-sigv4 (~> 1.8) + aws-partitions (~> 1, >= 1.992.0) + aws-sigv4 (~> 1.9) jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.75.0) - aws-sdk-core (~> 3, >= 3.188.0) - aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.142.0) - aws-sdk-core (~> 3, >= 3.189.0) + aws-sdk-kms (1.95.0) + aws-sdk-core (~> 3, >= 3.210.0) + aws-sigv4 (~> 1.5) + aws-sdk-s3 (1.170.1) + aws-sdk-core (~> 3, >= 3.210.0) aws-sdk-kms (~> 1) - aws-sigv4 (~> 1.8) - aws-sigv4 (1.8.0) + aws-sigv4 (~> 1.5) + aws-sigv4 (1.10.1) aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.4) + base64 (0.2.0) + bigdecimal (3.1.8) claide (1.1.0) colored (1.2) colored2 (3.1.2) commander (4.6.0) highline (~> 2.0.0) + csv (0.1.0) declarative (0.0.20) digest-crc (0.6.5) rake (>= 12.0.0, < 14.0.0) - domain_name (0.6.20231109) + domain_name (0.6.20240107) dotenv (2.8.1) emoji_regex (3.2.3) - excon (0.108.0) - faraday (1.10.3) + excon (0.112.0) + faraday (1.10.4) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) faraday-excon (~> 1.1) @@ -57,22 +62,22 @@ GEM faraday-httpclient (1.0.1) faraday-multipart (1.0.4) multipart-post (~> 2) - faraday-net_http (1.0.1) + faraday-net_http (1.0.2) faraday-net_http_persistent (1.2.0) faraday-patron (1.0.0) faraday-rack (1.0.0) faraday-retry (1.0.3) - faraday_middleware (1.2.0) + faraday_middleware (1.2.1) faraday (~> 1.0) - fastimage (2.3.0) - fastlane (2.219.0) + fastimage (2.3.1) + fastlane (2.225.0) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) aws-sdk-s3 (~> 1.0) babosa (>= 1.0.3, < 2.0.0) bundler (>= 1.12.0, < 3.0.0) - colored + colored (~> 1.2) commander (~> 4.6) dotenv (>= 2.1.1, < 3.0.0) emoji_regex (>= 0.1, < 4.0) @@ -81,6 +86,7 @@ GEM faraday-cookie_jar (~> 0.0.6) faraday_middleware (~> 1.0) fastimage (>= 2.1.0, < 3.0.0) + fastlane-sirp (>= 1.0.0) gh_inspector (>= 1.1.2, < 2.0.0) google-apis-androidpublisher_v3 (~> 0.3) google-apis-playcustomapp_v1 (~> 0.1) @@ -93,10 +99,10 @@ GEM mini_magick (>= 4.9.4, < 5.0.0) multipart-post (>= 2.0.0, < 3.0.0) naturally (~> 2.2) - optparse (>= 0.1.1) + optparse (>= 0.1.1, < 1.0.0) plist (>= 3.1.0, < 4.0.0) rubyzip (>= 2.0.0, < 3.0.0) - security (= 0.1.3) + security (= 0.1.5) simctl (~> 1.6.3) terminal-notifier (>= 2.0.0, < 3.0.0) terminal-table (~> 3) @@ -105,12 +111,14 @@ GEM word_wrap (~> 1.0.0) xcodeproj (>= 1.13.0, < 2.0.0) xcpretty (~> 0.3.0) - xcpretty-travis-formatter (>= 0.0.3) + xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) fastlane-plugin-appcenter (2.1.2) + fastlane-sirp (1.0.0) + sysrandom (~> 1.0) gh_inspector (1.1.3) google-apis-androidpublisher_v3 (0.54.0) google-apis-core (>= 0.11.0, < 2.a) - google-apis-core (0.11.2) + google-apis-core (0.11.3) addressable (~> 2.5, >= 2.5.1) googleauth (>= 0.16.2, < 2.a) httpclient (>= 2.8.1, < 3.a) @@ -118,24 +126,23 @@ GEM representable (~> 3.0) retriable (>= 2.0, < 4.a) rexml - webrick google-apis-iamcredentials_v1 (0.17.0) google-apis-core (>= 0.11.0, < 2.a) google-apis-playcustomapp_v1 (0.13.0) google-apis-core (>= 0.11.0, < 2.a) - google-apis-storage_v1 (0.29.0) + google-apis-storage_v1 (0.31.0) google-apis-core (>= 0.11.0, < 2.a) - google-cloud-core (1.6.1) + google-cloud-core (1.7.1) google-cloud-env (>= 1.0, < 3.a) google-cloud-errors (~> 1.0) google-cloud-env (1.6.0) faraday (>= 0.17.3, < 3.0) - google-cloud-errors (1.3.1) - google-cloud-storage (1.45.0) + google-cloud-errors (1.4.0) + google-cloud-storage (1.47.0) addressable (~> 2.8) digest-crc (~> 0.4) google-apis-iamcredentials_v1 (~> 0.1) - google-apis-storage_v1 (~> 0.29.0) + google-apis-storage_v1 (~> 0.31.0) google-cloud-core (~> 1.6) googleauth (>= 0.16.2, < 2.a) mini_mime (~> 1.0) @@ -146,38 +153,42 @@ GEM os (>= 0.9, < 2.0) signet (>= 0.16, < 2.a) highline (2.0.3) - http-cookie (1.0.5) + http-cookie (1.0.7) domain_name (~> 0.5) - httparty (0.21.0) + httparty (0.22.0) + csv mini_mime (>= 1.0.0) multi_xml (>= 0.5.2) httpclient (2.8.3) jmespath (1.6.2) - json (2.7.1) - jwt (2.7.1) - mini_magick (4.12.0) + json (2.8.2) + jwt (2.9.3) + base64 + mini_magick (4.13.2) mini_mime (1.1.5) multi_json (1.15.0) - multi_xml (0.6.0) - multipart-post (2.3.0) - nanaimo (0.3.0) + multi_xml (0.7.1) + bigdecimal (~> 3.1) + multipart-post (2.4.1) + nanaimo (0.4.0) naturally (2.2.1) - optparse (0.1.1) + nkf (0.2.0) + optparse (0.6.0) os (1.1.4) plist (3.7.1) - public_suffix (5.0.4) - rake (13.1.0) + public_suffix (5.1.1) + rake (13.2.1) representable (3.2.0) declarative (< 0.1.0) trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) retriable (3.1.2) - rexml (3.2.6) + rexml (3.3.9) rouge (2.0.7) ruby2_keywords (0.0.5) rubyzip (2.3.2) - security (0.1.3) - signet (0.18.0) + security (0.1.5) + signet (0.19.0) addressable (~> 2.8) faraday (>= 0.17.5, < 3.a) jwt (>= 1.5, < 3.0) @@ -185,6 +196,7 @@ GEM simctl (1.6.10) CFPropertyList naturally + sysrandom (1.0.5) terminal-notifier (2.0.0) terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) @@ -194,16 +206,15 @@ GEM tty-spinner (0.9.3) tty-cursor (~> 0.7) uber (0.1.0) - unicode-display_width (2.5.0) - webrick (1.8.1) + unicode-display_width (2.6.0) word_wrap (1.0.0) - xcodeproj (1.23.0) + xcodeproj (1.27.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) colored2 (~> 3.1) - nanaimo (~> 0.3.0) - rexml (~> 3.2.4) + nanaimo (~> 0.4.0) + rexml (>= 3.3.6, < 4.0) xcpretty (0.3.0) rouge (~> 2.0.7) xcpretty-travis-formatter (1.0.1) @@ -214,10 +225,10 @@ PLATFORMS x86_64-linux DEPENDENCIES - fastlane (~> 2.219.0) + fastlane (~> 2.225.0) fastlane-plugin-appcenter httparty - json (~> 2.7.0) + json (~> 2.8.0) BUNDLED WITH 2.4.20 diff --git a/Package.swift b/Package.swift index e850ac9b9..4f3e754c0 100644 --- a/Package.swift +++ b/Package.swift @@ -16,9 +16,9 @@ let package = Package( ], dependencies: [ // Dependencies declare other packages that this package depends on. - .package(url: "https://github.com/marmelroy/PhoneNumberKit.git", from: "3.7.9"), + .package(url: "https://github.com/marmelroy/PhoneNumberKit.git", from: "3.8.0"), .package(url: "git@github.com:powerhome/power-fonts.git", from: "0.0.1"), - .package(url: "https://github.com/pointfreeco/swift-snapshot-testing", from: "1.15.4") + .package(url: "https://github.com/pointfreeco/swift-snapshot-testing", from: "1.17.6") ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. diff --git a/Sources/Playbook/Components/Popover/PopoverHandler.swift b/Sources/Playbook/Components/Popover/PopoverHandler.swift index 262d3f108..1fc226471 100644 --- a/Sources/Playbook/Components/Popover/PopoverHandler.swift +++ b/Sources/Playbook/Components/Popover/PopoverHandler.swift @@ -12,6 +12,7 @@ import SwiftUI struct PopoverView: View { let id: Int let blockBackgroundInteractions: Bool + @State private var opacity: CGFloat = 0.01 @EnvironmentObject var popoverManager: PopoverManager init( @@ -35,6 +36,16 @@ struct PopoverView: View { .onTapGesture { popoverManager.closeInside(id) } + .onAppear { + if popover?.position != nil { + Timer.scheduledTimer(withTimeInterval: 0.2, repeats: false) { _ in + opacity = 1 + } + } else { + opacity = 0.01 + } + } + .opacity(opacity) } } } diff --git a/Sources/Playbook/Components/Popover/PopoverManager.swift b/Sources/Playbook/Components/Popover/PopoverManager.swift index 41e65fc4f..98b086085 100644 --- a/Sources/Playbook/Components/Popover/PopoverManager.swift +++ b/Sources/Playbook/Components/Popover/PopoverManager.swift @@ -12,7 +12,8 @@ import SwiftUI public class PopoverManager: ObservableObject { @Published var isPresented: [Int : Bool] = [:] @Published var popovers: [Int : Popover] = [:] - + @Published var activePopoverID: Int? + public static let shared = PopoverManager() struct Popover { @@ -20,12 +21,26 @@ public class PopoverManager: ObservableObject { var position: CGPoint? var close: (Close, action: (() -> Void)?) = (.anywhere, nil) } - + + func showPopover(for id: Int) { + activePopoverID = id + } + + func hidePopover(for id: Int) { + if activePopoverID == id { + activePopoverID = nil + } + } + + func isPopoverActive(for id: Int) -> Bool { + return activePopoverID == id + } + func createPopover(with id: Int, view: AnyView, position: CGPoint?, close: (Close, action: (() -> Void)?)) { popovers[id] = Popover(view: view, position: position, close: close) isPresented[id] = false } - + func removeValues() { popovers.removeAll() isPresented.removeAll() @@ -45,7 +60,14 @@ public class PopoverManager: ObservableObject { popovers.updateValue(newPopover, forKey: id) } } - + + func update(with id: Int) { + if let popover = popovers.first(where: { $0.key == id })?.value { + let newPopover = Popover(view: popover.view, position: popover.position, close: popover.close) + popovers.updateValue(newPopover, forKey: id) + } + } + func closeInside(_ key: Int) -> Void { switch popovers[key]?.close.0 { case .inside, .anywhere: diff --git a/Sources/Playbook/Components/Typeahead/GridInputField.swift b/Sources/Playbook/Components/Typeahead/GridInputField.swift index dcfbdd7f2..3de49fc71 100644 --- a/Sources/Playbook/Components/Typeahead/GridInputField.swift +++ b/Sources/Playbook/Components/Typeahead/GridInputField.swift @@ -101,6 +101,7 @@ private extension GridInputField { } .textFieldStyle(.plain) .pbFont(.body, color: textColor) + .frame(height: 24) } .fixedSize() .frame(minWidth: 60, alignment: .leading) diff --git a/Sources/Playbook/Components/Typeahead/PBTypeahead.swift b/Sources/Playbook/Components/Typeahead/PBTypeahead.swift index 285f10985..1fcebc530 100644 --- a/Sources/Playbook/Components/Typeahead/PBTypeahead.swift +++ b/Sources/Playbook/Components/Typeahead/PBTypeahead.swift @@ -20,18 +20,16 @@ public struct PBTypeahead: View { 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? @State private var hoveringOption: PBTypeahead.Option? @State private var isHovering: Bool = false - @State private var contentSize: CGSize = .zero @State private var selectedIndex: Int? @State private var focused: Bool = false @Binding var selectedOptions: [PBTypeahead.Option] @Binding var searchText: String @FocusState.Binding private var isFocused: Bool + @ObservedObject private var popoverManager: PopoverManager public init( id: Int, @@ -45,8 +43,9 @@ public struct PBTypeahead: View { listOffset: (x: CGFloat, y: CGFloat) = (0, 0), isFocused: FocusState.Binding, selectedOptions: Binding<[PBTypeahead.Option]>, - clearAction: (() -> Void)? = nil, - noOptionsText: String = "No options" + popoverManager: PopoverManager, + noOptionsText: String = "No options", + clearAction: (() -> Void)? = nil ) { self.id = id self.title = title @@ -61,6 +60,7 @@ public struct PBTypeahead: View { self.clearAction = clearAction self.noOptionsText = noOptionsText self._selectedOptions = selectedOptions + self.popoverManager = popoverManager } public var body: some View { @@ -76,9 +76,19 @@ public struct PBTypeahead: View { onItemTap: { removeSelected($0) }, onViewTap: { onViewTap } ) - .sizeReader { contentSize = $0 } .pbPopover( - isPresented: $showList, + isPresented: Binding( + get: { + popoverManager.isPopoverActive(for: id) + }, + set: { isActive in + if isActive { + popoverManager.showPopover(for: id) + } else { + popoverManager.hidePopover(for: id) + } + } + ), id: id, position: .bottom(listOffset.x, listOffset.y), variant: .dropdown, @@ -86,14 +96,19 @@ public struct PBTypeahead: View { ) { listView } - .onTapGesture { - isFocused = false - } + + } + .onTapGesture { + isFocused = false } .onAppear { focused = isFocused if debounce.numberOfCharacters == 0 { - showList = isFocused + if isFocused { + popoverManager.showPopover(for: id) + } else { + popoverManager.hidePopover(for: id) + } } setKeyboardControls if !selectedOptions.isEmpty { @@ -101,14 +116,9 @@ public struct PBTypeahead: View { } } .onChange(of: isFocused) { newValue in - showList = newValue - } - .onChange(of: searchText, debounce: debounce) { _ in - _ = searchResults - reloadList - } - .onChange(of: contentSize) { _ in - reloadList + if newValue { + popoverManager.showPopover(for: id) + } } .onChange(of: selectedOptions.count) { _ in reloadList @@ -117,8 +127,10 @@ public struct PBTypeahead: View { reloadList } .onChange(of: searchText, debounce: debounce) { _ in + _ = searchResults + reloadList if !searchText.isEmpty { - showList = true + popoverManager.showPopover(for: id) } } } @@ -126,6 +138,14 @@ public struct PBTypeahead: View { @MainActor private extension PBTypeahead { + 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) { @@ -139,9 +159,7 @@ private extension PBTypeahead { .scrollDismissesKeyboard(.immediately) .frame(maxHeight: dropdownMaxHeight) .fixedSize(horizontal: false, vertical: true) - .frame(maxWidth: .infinity, alignment: .top) } - .frame(maxWidth: .infinity, alignment: .top) } func listItemView(index: Int, option: PBTypeahead.Option) -> some View { @@ -149,26 +167,26 @@ private extension PBTypeahead { if option.text == noOptionsText { emptyView } else { - if let customView = option.customView?() { - customView - } 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 + hoveringOption = option + } + .onTapGesture { + onListSelection(index: index, option: option) } - } - } - .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 = option - } - .onTapGesture { - if option.text != noOptionsText { - onListSelection(index: index, option: option) } } } @@ -180,6 +198,8 @@ private extension PBTypeahead { .pbFont(.body, color: .text(.light)) Spacer() } + .padding(.horizontal, Spacing.xSmall + 4) + .padding(.vertical, Spacing.xSmall + 4) } var searchResults: [PBTypeahead.Option] { @@ -218,22 +238,20 @@ private extension PBTypeahead { selectedOptions = [] selectedIndex = nil hoveringIndex = nil - showList = false - } + popoverManager.hidePopover(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, option: PBTypeahead.Option) { - if showList { + if option.text != noOptionsText { switch selection { case .single: onSingleSelection(index: index, option) @@ -241,8 +259,10 @@ private extension PBTypeahead { onMultipleSelection(option) } } - showList = false + popoverManager.hidePopover(for: id) searchText = "" + reloadList + } func onSingleSelection(index: Int, _ option: PBTypeahead.Option) { @@ -251,19 +271,23 @@ private extension PBTypeahead { selectedIndex = index hoveringIndex = index selectedOptions.append(option) + reloadList } func onMultipleSelection(_ option: PBTypeahead.Option) { selectedOptions.append(option) hoveringIndex = nil selectedIndex = nil + reloadList } func removeSelected(_ index: Int) { if let selectedElementIndex = selectedOptions.indices.first(where: { $0 == index }) { let _ = selectedOptions.remove(at: selectedElementIndex) selectedIndex = nil + hoveringIndex = nil } + reloadList } func listBackgroundColor(_ index: Int?) -> Color { @@ -296,16 +320,16 @@ private extension PBTypeahead { focused = true } if event.keyCode == 36 { // return bar - if let index = hoveringIndex, index <= searchResults.count-1, showList { + if 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, showList, searchText.isEmpty { + if let index = hoveringIndex, index <= searchResults.count-1, searchText.isEmpty { onListSelection(index: index, option: searchResults[index]) } else { - showList = true + popoverManager.showPopover(for: id) } } } diff --git a/Sources/Playbook/Components/Typeahead/TypeaheadCatalog.swift b/Sources/Playbook/Components/Typeahead/TypeaheadCatalog.swift index e900097c0..59ff2d9b9 100644 --- a/Sources/Playbook/Components/Typeahead/TypeaheadCatalog.swift +++ b/Sources/Playbook/Components/Typeahead/TypeaheadCatalog.swift @@ -30,7 +30,8 @@ public struct TypeaheadCatalog: View { @FocusState private var isFocusedSection @State private var presentDialog: Bool = false - var popoverManager = PopoverManager() + + @StateObject private var popoverManager = PopoverManager() public var body: some View { PBDocStack(title: "Typeahead") { @@ -43,11 +44,9 @@ public struct TypeaheadCatalog: View { // PBDoc(title: "Sections", spacing: Spacing.small) { sections } .padding(.bottom, 500) } + .scrollDismissesKeyboard(.immediately) .onTapGesture { - isFocusedColors = false - isFocusedUsers = false - isFocusedHeight = false - isFocusedSection = false + dismissFocus() } .popoverHandler(id: 1) .popoverHandler(id: 2) @@ -65,7 +64,7 @@ extension TypeaheadCatalog { options: assetsColors, selection: .single, isFocused: $isFocusedColors, - selectedOptions: $selectedColors + selectedOptions: $selectedColors, popoverManager: popoverManager ) } @@ -78,7 +77,7 @@ extension TypeaheadCatalog { options: assetsUsers, selection: .multiple(variant: .pill), isFocused: $isFocusedUsers, - selectedOptions: $selectedUsers + selectedOptions: $selectedUsers, popoverManager: popoverManager ) } @@ -92,7 +91,7 @@ extension TypeaheadCatalog { selection: .multiple(variant: .pill), dropdownMaxHeight: 150, isFocused: $isFocusedHeight, - selectedOptions: $selectedHeight + selectedOptions: $selectedHeight, popoverManager: popoverManager ) } @@ -152,7 +151,7 @@ extension TypeaheadCatalog { selection: .multiple(variant: .pill), dropdownMaxHeight: 300, isFocused: $isFocused, - selectedOptions: $selectedUsers + selectedOptions: $selectedUsers, popoverManager: PopoverManager() ) Spacer() } @@ -164,6 +163,17 @@ extension TypeaheadCatalog { } } } + + func dismissFocus() { + isFocusedColors = false + isFocusedUsers = false + isFocusedHeight = false + isFocusedSection = false + popoverManager.hidePopover(for: 1) + popoverManager.hidePopover(for: 2) + popoverManager.hidePopover(for: 3) + popoverManager.hidePopover(for: 4) + } } #Preview { diff --git a/Sources/Playbook/Resources/Extensions/OnScrollDetection.swift b/Sources/Playbook/Resources/Extensions/OnScrollDetection.swift new file mode 100644 index 000000000..13bc24760 --- /dev/null +++ b/Sources/Playbook/Resources/Extensions/OnScrollDetection.swift @@ -0,0 +1,37 @@ +// +// Playbook Swift Design System +// +// Copyright © 2024 Power Home Remodeling Group +// This software is distributed under the ISC License +// +// OnScrollDetection.swift +// + +import SwiftUI + +struct OnScrollDetection: ViewModifier { + @State private var scrollOffset: CGFloat = .zero + let action: (() -> Void) + + func body(content: Content) -> some View { + content + .background( + GeometryReader { geo -> Color in + let offset = geo.frame(in: .global).minY + DispatchQueue.main.async { + self.scrollOffset = offset + } + return Color.clear + } + ) + .onChange(of: scrollOffset) { newValue in + action() + } + } +} + +extension View { + func onScroll(action: @escaping (() -> Void)) -> some View { + self.modifier(OnScrollDetection(action: action)) + } +}