-
Notifications
You must be signed in to change notification settings - Fork 36
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[LMN-20] SwiftUI Calendar component (#1818)
* calendar first pass working * some fixes * range extracted approach 1 * range extracted approach 2 * range extracted approach 2.1 * single selection * simplification * Refactor CalendarMonthGrid to support empty leading and trailing day cells * Refactor calendar view to improve selection logic * Extracting new calendar range and single selection cells. * Add calendar screenshots and update calendar UI * Add accessibility configurations and hide unnecessary elements in Calendar components * Add date formatter to SingleCalendarContainer_Previews * Add accessibility configurations for calendar selection types in tests. * Update CalendarMonthGrid.swift * Refactor calendar selection type to use a binding for selection state * Add CalendarRangeSelectionState enum and update RangeCalendarContainer * Update SwiftUIScreenshots.swift * Update Podfile.lock * Update Gemfile.lock
- Loading branch information
Showing
54 changed files
with
1,987 additions
and
3 deletions.
There are no files selected for viewing
61 changes: 61 additions & 0 deletions
61
...iftUI/Calendar/Classes/AccessibilityConfigurations/RangeAccessibilityConfigurations.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
/* | ||
* 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. | ||
*/ | ||
|
||
/// Create a multi-selection configuration with given accessibility strings. | ||
/// - Parameters: | ||
/// - startSelectionHint: The hint provided to assistive technologies informing a user how to select the first | ||
/// date in the range. | ||
/// - endSelectionHint: The hint provided to assistive technologies informing a user how to select the second | ||
/// date in the range. | ||
/// - startSelectionState: The label provided to assistive technologies informing a user that a date is selected | ||
/// as the first date in the range. | ||
/// - endSelectionState: The label provided to assistive technologies informing a user that a date is selected | ||
/// as the second date in the range. | ||
/// - betweenSelectionState: The label provided to assistive technologies informing a user that a date lies | ||
/// between the first and second selected dates. | ||
/// - startAndEndSelectionState: The label provided to assistive technologies informing a user that a date | ||
/// is selected as both the first and second date in the range. | ||
/// - returnDatePrompt: The prompt provided to assistive technologies informing a user that they should now | ||
/// select a second date. | ||
public struct RangeAccessibilityConfigurations { | ||
let startSelectionHint: String | ||
let endSelectionHint: String | ||
let startSelectionState: String | ||
let endSelectionState: String | ||
let betweenSelectionState: String | ||
let startAndEndSelectionState: String | ||
let returnDatePrompt: String | ||
|
||
public init( | ||
startSelectionHint: String, | ||
endSelectionHint: String, | ||
startSelectionState: String, | ||
endSelectionState: String, | ||
betweenSelectionState: String, | ||
startAndEndSelectionState: String, | ||
returnDatePrompt: String | ||
) { | ||
self.startSelectionHint = startSelectionHint | ||
self.endSelectionHint = endSelectionHint | ||
self.startSelectionState = startSelectionState | ||
self.endSelectionState = endSelectionState | ||
self.betweenSelectionState = betweenSelectionState | ||
self.startAndEndSelectionState = startAndEndSelectionState | ||
self.returnDatePrompt = returnDatePrompt | ||
} | ||
} |
28 changes: 28 additions & 0 deletions
28
...ftUI/Calendar/Classes/AccessibilityConfigurations/SingleAccessibilityConfigurations.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
/* | ||
* 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. | ||
*/ | ||
|
||
/// Create a single-selection configuration with given accessibility strings. | ||
/// - Parameters: | ||
/// - selectionHint: The hint provided to assistive technologies informing a user how to select a date. | ||
public struct SingleAccessibilityConfigurations { | ||
let selectionHint: String | ||
|
||
public init(selectionHint: String) { | ||
self.selectionHint = selectionHint | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
/* | ||
* 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 SwiftUI | ||
|
||
/// `BPKCalendar` is a SwiftUI view that represents a calendar. | ||
/// | ||
/// This view is designed to be customizable and flexible. It allows you to specify the type of selection, | ||
/// the calendar system, and the valid range of dates. | ||
/// | ||
/// - Parameters: | ||
/// - selectionType: The type of selection that the calendar should support. This can be single, range, or multiple. | ||
/// - calendar: The calendar system that the calendar should use. This can be any calendar system supported by | ||
/// the `Calendar` struct in Swift. | ||
/// - validRange: The range of dates that the calendar should allow the user to select. | ||
/// This is specified as a`ClosedRange<Date>`. | ||
/// | ||
/// The `BPKCalendar` view also allows you to specify an accessory action. This is a closure that takes a string and | ||
/// a date, and is called when the user interacts with an accessory in the calendar. | ||
public struct BPKCalendar: View { | ||
let calendar: Calendar | ||
let selectionType: CalendarSelectionType | ||
let validRange: ClosedRange<Date> | ||
private var accessoryAction: CalendarMonthAccessoryAction? | ||
private let monthHeaderDateFormatter: DateFormatter | ||
|
||
@State private var currentlyShownMonth: Date | ||
|
||
public init( | ||
selectionType: CalendarSelectionType, | ||
calendar: Calendar, | ||
validRange: ClosedRange<Date> | ||
) { | ||
_currentlyShownMonth = State(initialValue: validRange.lowerBound) | ||
self.validRange = validRange | ||
self.calendar = calendar | ||
self.selectionType = selectionType | ||
|
||
monthHeaderDateFormatter = DateFormatter() | ||
monthHeaderDateFormatter.dateFormat = "MMMM yyyy" | ||
} | ||
|
||
public var body: some View { | ||
GeometryReader { calendarProxy in | ||
VStack(spacing: BPKSpacing.none) { | ||
CalendarHeader(calendar: calendar) | ||
ZStack { | ||
CalendarTypeContainerFactory( | ||
selectionType: selectionType, | ||
calendar: calendar, | ||
validRange: validRange | ||
) { monthDate in | ||
CalendarMonthHeader( | ||
monthDate: monthDate, | ||
dateFormatter: monthHeaderDateFormatter, | ||
calendar: calendar, | ||
accessoryAction: accessoryAction, | ||
currentlyShownMonth: $currentlyShownMonth, | ||
parentProxy: calendarProxy | ||
) | ||
} | ||
yearBadge | ||
} | ||
} | ||
} | ||
} | ||
|
||
private var yearBadge: some View { | ||
VStack { | ||
CalendarBadge( | ||
currentlyShownMonth: currentlyShownMonth, | ||
calendar: calendar | ||
) | ||
.padding(.top, .base) | ||
Spacer() | ||
} | ||
} | ||
|
||
/// Sets the accessory action for the calendar to be applied to each month. | ||
public func monthAccessoryAction(_ action: CalendarMonthAccessoryAction) -> BPKCalendar { | ||
var result = self | ||
result.accessoryAction = action | ||
return result | ||
} | ||
} | ||
|
||
struct BPKCalendar_Previews: PreviewProvider { | ||
static var previews: some View { | ||
let calendar = Calendar.current | ||
let minValidDate = calendar.date(from: .init(year: 2023, month: 10, day: 12))! | ||
let maxValidDate = calendar.date(from: .init(year: 2025, month: 12, day: 2))! | ||
|
||
let minSelectedDate = calendar.date(from: .init(year: 2023, month: 11, day: 19))! | ||
let maxSelectedDate = calendar.date(from: .init(year: 2023, month: 11, day: 30))! | ||
|
||
return BPKCalendar( | ||
selectionType: .range( | ||
selection: .constant(.range(minSelectedDate...maxSelectedDate)), | ||
accessibilityConfigurations: .init( | ||
startSelectionHint: "", | ||
endSelectionHint: "", | ||
startSelectionState: "", | ||
endSelectionState: "", | ||
betweenSelectionState: "", | ||
startAndEndSelectionState: "", | ||
returnDatePrompt: "" | ||
) | ||
), | ||
calendar: calendar, | ||
validRange: minValidDate...maxValidDate | ||
) | ||
.monthAccessoryAction(CalendarMonthAccessoryAction(title: "Select whole month", action: { _ in })) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
/* | ||
* 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 SwiftUI | ||
|
||
struct CalendarBadge: View { | ||
let currentlyShownMonth: Date | ||
let calendar: Calendar | ||
|
||
var body: some View { | ||
let shownYear = calendar.component(.year, from: currentlyShownMonth) | ||
if shownYear != calendar.component(.year, from: Date()) { | ||
BPKBadge("\(shownYear)") | ||
.badgeStyle(.brand) | ||
.accessibilityHidden(true) | ||
} | ||
} | ||
} |
28 changes: 28 additions & 0 deletions
28
Backpack-SwiftUI/Calendar/Classes/CalendarDayCells/CalendarSelectableCell.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
/* | ||
* 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 SwiftUI | ||
|
||
struct CalendarSelectableCell<Cell: View>: View { | ||
@ViewBuilder let cell: Cell | ||
let onSelection: () -> Void | ||
|
||
var body: some View { | ||
cell.onTapGesture(perform: onSelection) | ||
} | ||
} |
30 changes: 30 additions & 0 deletions
30
Backpack-SwiftUI/Calendar/Classes/CalendarDayCells/DefaultCalendarDayCell.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
/* | ||
* 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 SwiftUI | ||
|
||
struct DefaultCalendarDayCell: View { | ||
let calendar: Calendar | ||
let date: Date | ||
|
||
var body: some View { | ||
BPKText("\(calendar.component(.day, from: date))", style: .label1) | ||
.lineLimit(1) | ||
.padding(.vertical, .md) | ||
} | ||
} |
25 changes: 25 additions & 0 deletions
25
Backpack-SwiftUI/Calendar/Classes/CalendarDayCells/DefaultEmptyCalendarDayCell.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
/* | ||
* 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 SwiftUI | ||
|
||
struct DefaultEmptyCalendarDayCell: View { | ||
var body: some View { | ||
Color.clear | ||
} | ||
} |
33 changes: 33 additions & 0 deletions
33
Backpack-SwiftUI/Calendar/Classes/CalendarDayCells/DisabledCalendarDayCell.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
/* | ||
* 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 SwiftUI | ||
|
||
struct DisabledCalendarDayCell: View { | ||
let calendar: Calendar | ||
let date: Date | ||
|
||
var body: some View { | ||
BPKText("\(calendar.component(.day, from: date))", style: .label1) | ||
.lineLimit(1) | ||
.foregroundColor(.textDisabledColor) | ||
.frame(maxWidth: .infinity) | ||
.padding(.vertical, .md) | ||
.accessibilityHidden(true) | ||
} | ||
} |
Oops, something went wrong.