Skip to content

Commit

Permalink
[DON-715] Add capabilities to BPKcalendar required for wholeMonth s…
Browse files Browse the repository at this point in the history
…election (#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
  • Loading branch information
novinfard authored Nov 15, 2024
1 parent fb8af08 commit 97e3d7f
Show file tree
Hide file tree
Showing 16 changed files with 275 additions and 37 deletions.
11 changes: 7 additions & 4 deletions Backpack-SwiftUI/Calendar/Classes/BPKCalendar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ public struct BPKCalendar<DayAccessoryView: View>: View {
let calendar: Calendar
let selectionType: CalendarSelectionType
let validRange: ClosedRange<Date>
private var accessoryAction: ((Date) -> CalendarMonthAccessoryAction?)?
private var initialMonthScroll: MonthScroll?
private var accessoryAction: CalendarMonthAccessoryAction?
private let monthHeaderDateFormatter: DateFormatter

private let dayAccessoryView: (Date) -> DayAccessoryView
Expand Down Expand Up @@ -82,6 +82,7 @@ public struct BPKCalendar<DayAccessoryView: View>: View {
monthDate: monthDate,
dateFormatter: monthHeaderDateFormatter,
calendar: calendar,
validRange: validRange,
accessoryAction: accessoryAction,
currentlyShownMonth: $currentlyShownMonth,
parentProxy: calendarProxy
Expand All @@ -106,8 +107,8 @@ public struct BPKCalendar<DayAccessoryView: View>: 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
Expand Down Expand Up @@ -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 }))
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<Date>` for the start and end of the selected month (calendar range considered).
case wholeMonthSelection((ClosedRange<Date>) -> Void)
}
}
38 changes: 31 additions & 7 deletions Backpack-SwiftUI/Calendar/Classes/CalendarMonthHeader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ struct CalendarMonthHeader: View {
let monthDate: Date
let dateFormatter: DateFormatter
let calendar: Calendar
let accessoryAction: CalendarMonthAccessoryAction?
let validRange: ClosedRange<Date>
let accessoryAction: ((Date) -> CalendarMonthAccessoryAction?)?
@Binding var currentlyShownMonth: Date
let parentProxy: GeometryProxy

Expand All @@ -41,17 +42,34 @@ 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)
}
}
.padding(.horizontal, .base)
.padding(.vertical, .lg)
}


private func monthRangeFor(date: Date) -> ClosedRange<Date>? {
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
Expand All @@ -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
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,7 @@ public enum CalendarRangeSelectionState {

/// The final state, where the user has selected both dates of the range.
case range(ClosedRange<Date>)

/// The state the user has selected the whole month range.
case wholeMonth(ClosedRange<Date>)
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@ struct CalendarTypeContainerFactory<MonthHeader: View, DayAccessoryView: View>:
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(
Expand All @@ -61,6 +61,7 @@ struct CalendarTypeContainerFactory<MonthHeader: View, DayAccessoryView: View>:
monthHeader: monthHeader,
dayAccessoryView: dayAccessoryView
)

}
}
}
56 changes: 56 additions & 0 deletions Backpack-SwiftUI/Calendar/Classes/Date+GetMonthDateRange.swift
Original file line number Diff line number Diff line change
@@ -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<Date>? {
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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)

}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -30,21 +31,29 @@ 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 {
static var previews: some View {
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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,15 @@ struct RangeSelectionCalendarDayCell: View {
let date: Date
let selection: ClosedRange<Date>
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)
}
Expand All @@ -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)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,29 +21,38 @@ import SwiftUI
struct UpperBoundSelectedCell: View {
let calendar: Calendar
let date: Date

let highlighted: Bool

var body: some View {
ZStack {
GeometryReader { proxy in
Color(.surfaceSubtleColor)
.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 {
static var previews: some View {
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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,15 +65,30 @@ struct RangeCalendarContainer<MonthHeader: View, DayAccessoryView: View>: 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(
for: dayDate,
selection: closedRange
)
))
.accessibility(addTraits: closedRange.contains(dayDate) ? .isSelected : [])
.accessibility(addTraits: .isSelected)
} else {
DefaultCalendarDayCell(calendar: calendar, date: dayDate)
.accessibilityLabel(Text(
Expand Down
Loading

0 comments on commit 97e3d7f

Please sign in to comment.