From 97e3d7f1fd070f5e501406e94fb34d18873a5e07 Mon Sep 17 00:00:00 2001 From: Soheil Novinfard Date: Fri, 15 Nov 2024 17:03:48 +0000 Subject: [PATCH] [DON-715] Add capabilities to `BPKcalendar` required for wholeMonth selection (#2104) * DON-628 / Whole month selection as a separate category in Backpack * DON-628 / Make whole month selection as part of range stage in Backpack calendar * DON-628 / Move the calendar month selection logic to Backpack calendar * Remove CalendarMonth data entity to simplifies * DON-628 / Remove redundant file * Remove extra condition on accessibility traits * Fix lint issues * Remove extra file * Add example in Calendar SwiftUI about whole month selection * Make accessory action based on given date * Fix the example app * Change the order of examples * Improve the comments and file names * Make highlight parameter explicit based on PR feedbacks * Make calendar accessory action as enum. * Fix the linting issue * Fix the tests --- .../Calendar/Classes/BPKCalendar.swift | 11 +- .../CalendarMonthAccessoryAction.swift | 16 ++- .../Classes/CalendarMonthHeader.swift | 38 +++++-- .../Classes/CalendarRangeSelectionState.swift | 3 + .../Core/CalendarTypeContainerFactory.swift | 5 +- .../Classes/Date+GetMonthDateRange.swift | 56 ++++++++++ .../EmptyRangeSelectionCalendarDayCell.swift | 6 +- .../Range/Cells/LowerBoundSelectedCell.swift | 17 ++- .../Cells/RangeSelectionCalendarDayCell.swift | 11 +- .../Range/Cells/UpperBoundSelectedCell.swift | 17 ++- .../Range/RangeCalendarContainer.swift | 19 +++- .../Range/RangeDayAccessibilityProvider.swift | 2 +- .../Tests/Calendar/BPKCalendarTests.swift | 4 +- Example/Backpack.xcodeproj/project.pbxproj | 4 + .../CalendarExampleWholeMonthView.swift | 102 ++++++++++++++++++ .../Groups/CalendarGroups.swift | 1 + 16 files changed, 275 insertions(+), 37 deletions(-) create mode 100644 Backpack-SwiftUI/Calendar/Classes/Date+GetMonthDateRange.swift create mode 100644 Example/Backpack/SwiftUI/Components/Calendar/CalendarExampleWholeMonthView.swift diff --git a/Backpack-SwiftUI/Calendar/Classes/BPKCalendar.swift b/Backpack-SwiftUI/Calendar/Classes/BPKCalendar.swift index 9133d338e..e09f254c6 100644 --- a/Backpack-SwiftUI/Calendar/Classes/BPKCalendar.swift +++ b/Backpack-SwiftUI/Calendar/Classes/BPKCalendar.swift @@ -37,8 +37,8 @@ public struct BPKCalendar: View { let calendar: Calendar let selectionType: CalendarSelectionType let validRange: ClosedRange + private var accessoryAction: ((Date) -> CalendarMonthAccessoryAction?)? private var initialMonthScroll: MonthScroll? - private var accessoryAction: CalendarMonthAccessoryAction? private let monthHeaderDateFormatter: DateFormatter private let dayAccessoryView: (Date) -> DayAccessoryView @@ -82,6 +82,7 @@ public struct BPKCalendar: View { monthDate: monthDate, dateFormatter: monthHeaderDateFormatter, calendar: calendar, + validRange: validRange, accessoryAction: accessoryAction, currentlyShownMonth: $currentlyShownMonth, parentProxy: calendarProxy @@ -106,8 +107,8 @@ public struct BPKCalendar: View { } } - /// Sets the accessory action for the calendar to be applied to each month. - public func monthAccessoryAction(_ action: CalendarMonthAccessoryAction) -> BPKCalendar { + /// Sets the accessory action for the calendar to be applied to each month, based on the give month `Date`. + public func monthAccessoryAction(_ action: ((Date) -> CalendarMonthAccessoryAction?)?) -> BPKCalendar { var result = self result.accessoryAction = action return result @@ -142,6 +143,8 @@ struct BPKCalendar_Previews: PreviewProvider { BPKText("20", style: .caption) } ) - .monthAccessoryAction(CalendarMonthAccessoryAction(title: "Select whole month", action: { _ in })) + .monthAccessoryAction { _ in + return CalendarMonthAccessoryAction(title: "Select whole month", action: .custom({ _ in })) + } } } diff --git a/Backpack-SwiftUI/Calendar/Classes/CalendarMonthAccessoryAction.swift b/Backpack-SwiftUI/Calendar/Classes/CalendarMonthAccessoryAction.swift index 56a33051b..d76365bb7 100644 --- a/Backpack-SwiftUI/Calendar/Classes/CalendarMonthAccessoryAction.swift +++ b/Backpack-SwiftUI/Calendar/Classes/CalendarMonthAccessoryAction.swift @@ -22,14 +22,24 @@ import SwiftUI /// be performed when a user interacts with an accessory in the calendar. public struct CalendarMonthAccessoryAction { let title: String - let action: (Date) -> Void + let action: Action /// - Parameters: /// - title: The title of the accessory. /// - action: The action to be performed when the user interacts with the accessory. - /// This closure takes a `Date` as a parameter, which is the date that the user interacted with. - public init(title: String, action: @escaping (Date) -> Void) { + public init(title: String, action: Action) { self.title = title self.action = action } + + /// Defines the types of actions that can be triggered. + public enum Action { + /// A custom action triggered for a specific date month. + /// - Parameter: The `Date` representing the month interacted with by the user. + case custom((Date) -> Void) + + /// An action triggered for a whole month selection. + /// - Parameter: A `ClosedRange` for the start and end of the selected month (calendar range considered). + case wholeMonthSelection((ClosedRange) -> Void) + } } diff --git a/Backpack-SwiftUI/Calendar/Classes/CalendarMonthHeader.swift b/Backpack-SwiftUI/Calendar/Classes/CalendarMonthHeader.swift index a6a6e0275..6473a5812 100644 --- a/Backpack-SwiftUI/Calendar/Classes/CalendarMonthHeader.swift +++ b/Backpack-SwiftUI/Calendar/Classes/CalendarMonthHeader.swift @@ -24,7 +24,8 @@ struct CalendarMonthHeader: View { let monthDate: Date let dateFormatter: DateFormatter let calendar: Calendar - let accessoryAction: CalendarMonthAccessoryAction? + let validRange: ClosedRange + let accessoryAction: ((Date) -> CalendarMonthAccessoryAction?)? @Binding var currentlyShownMonth: Date let parentProxy: GeometryProxy @@ -41,9 +42,15 @@ struct CalendarMonthHeader: View { } } .frame(width: 1) - if let accessoryAction { - BPKButton(accessoryAction.title) { - accessoryAction.action(monthDate) + if let accessory = accessoryAction?(monthDate) { + BPKButton(accessory.title) { + switch accessory.action { + case .custom(let action): + action(monthDate) + case .wholeMonthSelection(let action): + guard let range = monthRangeFor(date: monthDate) else { return } + action(range) + } } .buttonStyle(.link) } @@ -51,7 +58,18 @@ struct CalendarMonthHeader: View { .padding(.horizontal, .base) .padding(.vertical, .lg) } - + + private func monthRangeFor(date: Date) -> ClosedRange? { + guard let monthRange = date.getMonthDateRange(calendar: calendar) else { + return nil + } + + let lowerBound = max(validRange.lowerBound, monthRange.lowerBound) + let upperBound = min(validRange.upperBound, monthRange.upperBound) + + return lowerBound...upperBound + } + private func isCurrentlyShowingMonth(proxy: GeometryProxy) -> Bool { let parentGlobalFrame = parentProxy.frame(in: .global) let yParentOrigin = parentGlobalFrame.origin.y @@ -69,14 +87,20 @@ struct CalendarMonthHeader_Previews: PreviewProvider { dateFormatter.dateFormat = "MMMM yyyy" return dateFormatter }() - + static var previews: some View { + let calendar = Calendar.current + + let start = calendar.date(from: .init(year: 2023, month: 10, day: 1))! + let end = calendar.date(from: .init(year: 2025, month: 12, day: 25))! + GeometryReader { proxy in CalendarMonthHeader( monthDate: Date(), dateFormatter: Self.dateFormatter, calendar: Calendar.current, - accessoryAction: .init(title: "Action", action: { _ in }), + validRange: start...end, + accessoryAction: { _ in return .init(title: "Action", action: .custom({ _ in })) }, currentlyShownMonth: .constant(Date()), parentProxy: proxy ) diff --git a/Backpack-SwiftUI/Calendar/Classes/CalendarRangeSelectionState.swift b/Backpack-SwiftUI/Calendar/Classes/CalendarRangeSelectionState.swift index bafcecb1b..27c9ade81 100644 --- a/Backpack-SwiftUI/Calendar/Classes/CalendarRangeSelectionState.swift +++ b/Backpack-SwiftUI/Calendar/Classes/CalendarRangeSelectionState.swift @@ -25,4 +25,7 @@ public enum CalendarRangeSelectionState { /// The final state, where the user has selected both dates of the range. case range(ClosedRange) + + /// The state the user has selected the whole month range. + case wholeMonth(ClosedRange) } diff --git a/Backpack-SwiftUI/Calendar/Classes/Core/CalendarTypeContainerFactory.swift b/Backpack-SwiftUI/Calendar/Classes/Core/CalendarTypeContainerFactory.swift index 767389ba8..fe780e985 100644 --- a/Backpack-SwiftUI/Calendar/Classes/Core/CalendarTypeContainerFactory.swift +++ b/Backpack-SwiftUI/Calendar/Classes/Core/CalendarTypeContainerFactory.swift @@ -48,9 +48,9 @@ struct CalendarTypeContainerFactory: monthHeader: monthHeader, dayAccessoryView: dayAccessoryView ) - case .single(let selected, let accessibilityConfigurations): + case .single(let selection, let accessibilityConfigurations): SingleCalendarContainer( - selection: selected, + selection: selection, calendar: calendar, validRange: validRange, accessibilityProvider: SingleDayAccessibilityProvider( @@ -61,6 +61,7 @@ struct CalendarTypeContainerFactory: monthHeader: monthHeader, dayAccessoryView: dayAccessoryView ) + } } } diff --git a/Backpack-SwiftUI/Calendar/Classes/Date+GetMonthDateRange.swift b/Backpack-SwiftUI/Calendar/Classes/Date+GetMonthDateRange.swift new file mode 100644 index 000000000..b0db3df11 --- /dev/null +++ b/Backpack-SwiftUI/Calendar/Classes/Date+GetMonthDateRange.swift @@ -0,0 +1,56 @@ +/* + * Backpack - Skyscanner's Design System + * + * Copyright 2018 Skyscanner Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Foundation + +extension Date { + public func getMonthDateRange(calendar: Calendar) -> ClosedRange? { + let components = calendar.dateComponents([.year, .month], from: self) + + guard let year = components.year, let month = components.month else { + return nil + } + + // Create DateComponents for the start of the month + var startComponents = DateComponents() + startComponents.year = year + startComponents.month = month + startComponents.day = 1 + + // Get the start date of the month + guard let startDate = calendar.date(from: startComponents) else { + return nil + } + + // Get the range of days in the month + guard let range = calendar.range(of: .day, in: .month, for: startDate) else { + return nil + } + + // Create the end date of the month + let endDay = range.upperBound - 1 + let endComponents = DateComponents(year: year, month: month, day: endDay) + + guard let endDate = calendar.date(from: endComponents) else { + return nil + } + + // Return the range of dates + return startDate...endDate + } +} diff --git a/Backpack-SwiftUI/Calendar/Classes/Range/Cells/EmptyRangeSelectionCalendarDayCell.swift b/Backpack-SwiftUI/Calendar/Classes/Range/Cells/EmptyRangeSelectionCalendarDayCell.swift index 2c30e2144..8083f1c22 100644 --- a/Backpack-SwiftUI/Calendar/Classes/Range/Cells/EmptyRangeSelectionCalendarDayCell.swift +++ b/Backpack-SwiftUI/Calendar/Classes/Range/Cells/EmptyRangeSelectionCalendarDayCell.swift @@ -44,14 +44,14 @@ struct EmptyRangeSelectionCalendarDayCell_Previews: PreviewProvider { let endSelection = calendar.date(from: .init(year: 2023, month: 11, day: 10))! LazyVGrid(columns: Array(repeating: GridItem(spacing: 0), count: 3), spacing: 0) { - LowerBoundSelectedCell(calendar: calendar, date: date) + LowerBoundSelectedCell(calendar: calendar, date: date, highlighted: true) EmptyRangeSelectionCalendarDayCell( cellIndex: 0, correspondingDate: date, selection: startSelection...endSelection ) - UpperBoundSelectedCell(calendar: calendar, date: date) - + UpperBoundSelectedCell(calendar: calendar, date: date, highlighted: true) + } } } diff --git a/Backpack-SwiftUI/Calendar/Classes/Range/Cells/LowerBoundSelectedCell.swift b/Backpack-SwiftUI/Calendar/Classes/Range/Cells/LowerBoundSelectedCell.swift index 46ab6349d..bcd987aa5 100644 --- a/Backpack-SwiftUI/Calendar/Classes/Range/Cells/LowerBoundSelectedCell.swift +++ b/Backpack-SwiftUI/Calendar/Classes/Range/Cells/LowerBoundSelectedCell.swift @@ -21,7 +21,8 @@ import SwiftUI struct LowerBoundSelectedCell: View { let calendar: Calendar let date: Date - + let highlighted: Bool + var body: some View { ZStack { GeometryReader { proxy in @@ -30,14 +31,22 @@ struct LowerBoundSelectedCell: View { .offset(x: proxy.size.width / 2) } BPKText("\(calendar.component(.day, from: date))", style: .label1) - .foregroundColor(.textPrimaryInverseColor) + .foregroundColor(textColor) .lineLimit(1) .frame(maxWidth: .infinity) .padding(.vertical, .md) - .background(.coreAccentColor) + .background(circleColor) .clipShape(Circle()) } } + + private var circleColor: BPKColor { + highlighted ? .coreAccentColor : .surfaceSubtleColor + } + + private var textColor: BPKColor { + highlighted ? .textPrimaryInverseColor : .black + } } struct LowerBoundSelectedCell_Previews: PreviewProvider { @@ -45,6 +54,6 @@ struct LowerBoundSelectedCell_Previews: PreviewProvider { let calendar = Calendar.current let date = calendar.date(from: .init(year: 2023, month: 11, day: 8))! - LowerBoundSelectedCell(calendar: calendar, date: date) + LowerBoundSelectedCell(calendar: calendar, date: date, highlighted: true) } } diff --git a/Backpack-SwiftUI/Calendar/Classes/Range/Cells/RangeSelectionCalendarDayCell.swift b/Backpack-SwiftUI/Calendar/Classes/Range/Cells/RangeSelectionCalendarDayCell.swift index 3bb480741..adb059a20 100644 --- a/Backpack-SwiftUI/Calendar/Classes/Range/Cells/RangeSelectionCalendarDayCell.swift +++ b/Backpack-SwiftUI/Calendar/Classes/Range/Cells/RangeSelectionCalendarDayCell.swift @@ -22,14 +22,15 @@ struct RangeSelectionCalendarDayCell: View { let date: Date let selection: ClosedRange let calendar: Calendar - + let highlightRangeEnds: Bool + var body: some View { if selection.lowerBound == selection.upperBound { LowerAndUpperBoundSelectedCell(calendar: calendar, date: date) } else if date == selection.lowerBound { - LowerBoundSelectedCell(calendar: calendar, date: date) + LowerBoundSelectedCell(calendar: calendar, date: date, highlighted: highlightRangeEnds) } else if date == selection.upperBound { - UpperBoundSelectedCell(calendar: calendar, date: date) + UpperBoundSelectedCell(calendar: calendar, date: date, highlighted: highlightRangeEnds) } else { InbetweenSelectionCell(calendar: calendar, date: date) } @@ -44,9 +45,9 @@ struct SelectedCalendarDayCell_Previews: PreviewProvider { LazyVGrid(columns: Array(repeating: GridItem(spacing: 0), count: 6), spacing: 0) { DefaultCalendarDayCell(calendar: calendar, date: date) DisabledCalendarDayCell(calendar: calendar, date: date) - LowerBoundSelectedCell(calendar: calendar, date: date) + LowerBoundSelectedCell(calendar: calendar, date: date, highlighted: true) InbetweenSelectionCell(calendar: calendar, date: date) - UpperBoundSelectedCell(calendar: calendar, date: date) + UpperBoundSelectedCell(calendar: calendar, date: date, highlighted: true) LowerAndUpperBoundSelectedCell(calendar: calendar, date: date) } } diff --git a/Backpack-SwiftUI/Calendar/Classes/Range/Cells/UpperBoundSelectedCell.swift b/Backpack-SwiftUI/Calendar/Classes/Range/Cells/UpperBoundSelectedCell.swift index 2e8070ea9..effb39d84 100644 --- a/Backpack-SwiftUI/Calendar/Classes/Range/Cells/UpperBoundSelectedCell.swift +++ b/Backpack-SwiftUI/Calendar/Classes/Range/Cells/UpperBoundSelectedCell.swift @@ -21,7 +21,8 @@ import SwiftUI struct UpperBoundSelectedCell: View { let calendar: Calendar let date: Date - + let highlighted: Bool + var body: some View { ZStack { GeometryReader { proxy in @@ -29,14 +30,22 @@ struct UpperBoundSelectedCell: View { .frame(width: proxy.size.width / 2) } BPKText("\(calendar.component(.day, from: date))", style: .label1) - .foregroundColor(.textPrimaryInverseColor) + .foregroundColor(textColor) .lineLimit(1) .frame(maxWidth: .infinity) .padding(.vertical, .md) - .background(.coreAccentColor) + .background(circleColor) .clipShape(Circle()) } } + + private var circleColor: BPKColor { + highlighted ? .coreAccentColor : .surfaceSubtleColor + } + + private var textColor: BPKColor { + highlighted ? .textPrimaryInverseColor : .black + } } struct UpperBoundSelectedCell_Previews: PreviewProvider { @@ -44,6 +53,6 @@ struct UpperBoundSelectedCell_Previews: PreviewProvider { let calendar = Calendar.current let date = calendar.date(from: .init(year: 2023, month: 11, day: 8))! - UpperBoundSelectedCell(calendar: calendar, date: date) + UpperBoundSelectedCell(calendar: calendar, date: date, highlighted: true) } } diff --git a/Backpack-SwiftUI/Calendar/Classes/Range/RangeCalendarContainer.swift b/Backpack-SwiftUI/Calendar/Classes/Range/RangeCalendarContainer.swift index 64ca75193..fca7e04f5 100644 --- a/Backpack-SwiftUI/Calendar/Classes/Range/RangeCalendarContainer.swift +++ b/Backpack-SwiftUI/Calendar/Classes/Range/RangeCalendarContainer.swift @@ -65,7 +65,22 @@ struct RangeCalendarContainer: View { RangeSelectionCalendarDayCell( date: dayDate, selection: closedRange, - calendar: calendar + calendar: calendar, + highlightRangeEnds: true + ) + .accessibilityLabel(Text( + accessibilityProvider.accessibilityLabel( + for: dayDate, + selection: closedRange + ) + )) + .accessibility(addTraits: .isSelected) + } else if case .wholeMonth(let closedRange) = selectionState, closedRange.contains(dayDate) { + RangeSelectionCalendarDayCell( + date: dayDate, + selection: closedRange, + calendar: calendar, + highlightRangeEnds: false ) .accessibilityLabel(Text( accessibilityProvider.accessibilityLabel( @@ -73,7 +88,7 @@ struct RangeCalendarContainer: View { selection: closedRange ) )) - .accessibility(addTraits: closedRange.contains(dayDate) ? .isSelected : []) + .accessibility(addTraits: .isSelected) } else { DefaultCalendarDayCell(calendar: calendar, date: dayDate) .accessibilityLabel(Text( diff --git a/Backpack-SwiftUI/Calendar/Classes/Range/RangeDayAccessibilityProvider.swift b/Backpack-SwiftUI/Calendar/Classes/Range/RangeDayAccessibilityProvider.swift index 8b74a6f4b..8fd783490 100644 --- a/Backpack-SwiftUI/Calendar/Classes/Range/RangeDayAccessibilityProvider.swift +++ b/Backpack-SwiftUI/Calendar/Classes/Range/RangeDayAccessibilityProvider.swift @@ -66,7 +66,7 @@ struct RangeDayAccessibilityProvider { switch rangeSelectionState { case .intermediate(let initialDateSelection): return date < initialDateSelection - case .range: + case .range, .wholeMonth: return true case nil: return false diff --git a/Backpack-SwiftUI/Tests/Calendar/BPKCalendarTests.swift b/Backpack-SwiftUI/Tests/Calendar/BPKCalendarTests.swift index 2f19e9312..218a56792 100644 --- a/Backpack-SwiftUI/Tests/Calendar/BPKCalendarTests.swift +++ b/Backpack-SwiftUI/Tests/Calendar/BPKCalendarTests.swift @@ -132,9 +132,9 @@ class BPKCalendarTests: XCTestCase { LazyVGrid(columns: Array(repeating: GridItem(spacing: 0), count: 6), spacing: 0) { DefaultCalendarDayCell(calendar: calendar, date: date) DisabledCalendarDayCell(calendar: calendar, date: date) - LowerBoundSelectedCell(calendar: calendar, date: date) + LowerBoundSelectedCell(calendar: calendar, date: date, highlighted: true) InbetweenSelectionCell(calendar: calendar, date: date) - UpperBoundSelectedCell(calendar: calendar, date: date) + UpperBoundSelectedCell(calendar: calendar, date: date, highlighted: true) LowerAndUpperBoundSelectedCell(calendar: calendar, date: date) } .frame(width: 400) diff --git a/Example/Backpack.xcodeproj/project.pbxproj b/Example/Backpack.xcodeproj/project.pbxproj index 69d5b744c..67c502d78 100644 --- a/Example/Backpack.xcodeproj/project.pbxproj +++ b/Example/Backpack.xcodeproj/project.pbxproj @@ -24,6 +24,7 @@ 25006AB02C60B1740038D7CA /* SearchControlInputExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25006AAF2C60B1740038D7CA /* SearchControlInputExampleView.swift */; }; 256F76152BB479850047AD1C /* SearchInputSummaryExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 256F76142BB479850047AD1C /* SearchInputSummaryExampleView.swift */; }; 25D1337D2C73EFD7002C9562 /* SearchControlInputUITest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25D1337C2C73EFD7002C9562 /* SearchControlInputUITest.swift */; }; + 2832E45B2CDE1561000B6DF4 /* CalendarExampleWholeMonthView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2832E45A2CDE1561000B6DF4 /* CalendarExampleWholeMonthView.swift */; }; 2A62FDDD2AB89F4500D545E5 /* TextAreaExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A62FDDC2AB89F4500D545E5 /* TextAreaExampleView.swift */; }; 3A7D2D47214AB9F400ECBD5B /* BPKButtonsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 3A7D2D46214AB9F400ECBD5B /* BPKButtonsViewController.m */; }; 3AA018EF215BE26600838FBB /* SpinnersViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA018EE215BE26500838FBB /* SpinnersViewController.swift */; }; @@ -258,6 +259,7 @@ 25006AAF2C60B1740038D7CA /* SearchControlInputExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchControlInputExampleView.swift; sourceTree = ""; }; 256F76142BB479850047AD1C /* SearchInputSummaryExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchInputSummaryExampleView.swift; sourceTree = ""; }; 25D1337C2C73EFD7002C9562 /* SearchControlInputUITest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchControlInputUITest.swift; sourceTree = ""; }; + 2832E45A2CDE1561000B6DF4 /* CalendarExampleWholeMonthView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarExampleWholeMonthView.swift; sourceTree = ""; }; 2A62FDDC2AB89F4500D545E5 /* TextAreaExampleView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextAreaExampleView.swift; sourceTree = ""; }; 3A7D2D45214AB9F400ECBD5B /* BPKButtonsViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BPKButtonsViewController.h; sourceTree = ""; }; 3A7D2D46214AB9F400ECBD5B /* BPKButtonsViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BPKButtonsViewController.m; sourceTree = ""; }; @@ -606,6 +608,7 @@ children = ( 537AA67E2B050D1000D97B42 /* CalendarExampleRangeView.swift */, 5318E3332AF506FA00C66D18 /* CalendarExampleSingleView.swift */, + 2832E45A2CDE1561000B6DF4 /* CalendarExampleWholeMonthView.swift */, ); path = Calendar; sourceTree = ""; @@ -1864,6 +1867,7 @@ 3AA018EF215BE26600838FBB /* SpinnersViewController.swift in Sources */, 53E075A927FC97C60033147C /* ToastGroups.swift in Sources */, 53B6DB5927FB6F930042B7C0 /* ComponentCells.swift in Sources */, + 2832E45B2CDE1561000B6DF4 /* CalendarExampleWholeMonthView.swift in Sources */, 793B9748283B6F57009A5164 /* ComponentCellDataSource.swift in Sources */, A12269752A68C09B00C7C0CD /* BottomSheetExampleView.swift in Sources */, 6055821721523A1300BF9F3E /* IconsViewController.swift in Sources */, diff --git a/Example/Backpack/SwiftUI/Components/Calendar/CalendarExampleWholeMonthView.swift b/Example/Backpack/SwiftUI/Components/Calendar/CalendarExampleWholeMonthView.swift new file mode 100644 index 000000000..5d0d24b8e --- /dev/null +++ b/Example/Backpack/SwiftUI/Components/Calendar/CalendarExampleWholeMonthView.swift @@ -0,0 +1,102 @@ +// +/* + * Backpack - Skyscanner's Design System + * + * Copyright © 2023 Skyscanner Ltd. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import SwiftUI +import Backpack_SwiftUI + +struct CalendarExampleWholeMonthView: View { + @State var selection: CalendarRangeSelectionState? + + let validRange: ClosedRange + let calendar: Calendar + let formatter: DateFormatter + + init() { + let calendar = Calendar.current + let start = calendar.date(from: .init(year: 2023, month: 11, day: 5))! + let end = calendar.date(from: .init(year: 2024, month: 11, day: 28))! + + self.validRange = start...end + self.calendar = calendar + + let formatter = DateFormatter() + formatter.dateStyle = .short + formatter.locale = calendar.locale + formatter.timeZone = calendar.timeZone + self.formatter = formatter + let selectionStart = calendar.date(from: .init(year: 2023, month: 11, day: 10))! + let selectionEnd = calendar.date(from: .init(year: 2023, month: 11, day: 30))! + _selection = State(initialValue: .range(selectionStart...selectionEnd)) + } + + var body: some View { + VStack { + HStack { + BPKText("Selected inbound:", style: .caption) + if case .wholeMonth(let selectedRange) = selection { + BPKText("\(formatter.string(from: selectedRange.lowerBound))", style: .caption) + } else if case .intermediate(let selectedDate) = selection { + BPKText("\(formatter.string(from: selectedDate))", style: .caption) + } + } + HStack { + BPKText("Selected outbound:", style: .caption) + if case .wholeMonth(let selectedRange) = selection { + BPKText("\(formatter.string(from: selectedRange.upperBound))", style: .caption) + } + } + calendarView + } + } + + @ViewBuilder + var calendarView: some View { + let accessibilityConfigurations = RangeAccessibilityConfigurations( + startSelectionHint: "Double tap to select departure date", + endSelectionHint: "Double tap to select return date", + startSelectionState: "Selected as departure date", + endSelectionState: "Selected as return date", + betweenSelectionState: "Between departure and return date", + startAndEndSelectionState: "Selected as both departure and return date", + returnDatePrompt: "Now please select a return date" + ) + BPKCalendar( + selectionType: .range( + selection: $selection, + accessibilityConfigurations: accessibilityConfigurations + ), + calendar: calendar, + validRange: validRange + ) + .monthAccessoryAction { _ in + return CalendarMonthAccessoryAction( + title: "Select whole month", + action: .wholeMonthSelection({ monthRange in + selection = .wholeMonth(monthRange) + }) + ) + } + } +} + +struct CalendarExampleMonthSelectionView_Previews: PreviewProvider { + static var previews: some View { + CalendarExampleWholeMonthView() + } +} diff --git a/Example/Backpack/Utils/FeatureStories/Groups/CalendarGroups.swift b/Example/Backpack/Utils/FeatureStories/Groups/CalendarGroups.swift index d1c1ccfd8..f5cd7272e 100644 --- a/Example/Backpack/Utils/FeatureStories/Groups/CalendarGroups.swift +++ b/Example/Backpack/Utils/FeatureStories/Groups/CalendarGroups.swift @@ -80,6 +80,7 @@ struct CalendarGroupsProvider { presentableCalendar("Range Selection", view: CalendarExampleRangeView(showAccessoryViews: false)), presentableCalendar("Single Selection", view: CalendarExampleSingleView()), presentableCalendar("With Accessory Views", view: CalendarExampleRangeView(showAccessoryViews: true)), + presentableCalendar("With Whole Month Selection", view: CalendarExampleWholeMonthView()), presentableCalendar( "With Initial Month Scrolling", view: CalendarExampleRangeView(showAccessoryViews: false, makeInitialMonthScroll: true)