Skip to content

Commit

Permalink
Merge pull request #2128 from Skyscanner/donburi/DON-1048_m1b_flights…
Browse files Browse the repository at this point in the history
…dateselector

DON-1019: Fix BPKCalendar internal containers
  • Loading branch information
brunomdac authored Jan 2, 2025
2 parents aeb19d4 + 187d7e8 commit e55955b
Show file tree
Hide file tree
Showing 15 changed files with 422 additions and 388 deletions.
24 changes: 23 additions & 1 deletion Backpack-SwiftUI/Calendar/Classes/BPKCalendar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ public struct BPKCalendar<DayAccessoryView: View>: View {
private var accessoryAction: ((Date) -> CalendarMonthAccessoryAction?)?
private var initialMonthScroll: MonthScroll?
private let monthHeaderDateFormatter: DateFormatter
private let calendarAccessibilityConfiguration: CalendarAccessibilityConfiguration

private let dayAccessoryView: (Date) -> DayAccessoryView
@State private var currentlyShownMonth: Date
Expand All @@ -49,13 +50,15 @@ public struct BPKCalendar<DayAccessoryView: View>: View {
calendar: Calendar,
validRange: ClosedRange<Date>,
initialMonthScroll: MonthScroll? = nil,
calendarAccessibilityConfiguration: CalendarAccessibilityConfiguration,
dayAccessoryView: @escaping (Date) -> DayAccessoryView = { _ in EmptyView() }
) {
self.dayAccessoryView = dayAccessoryView
_currentlyShownMonth = State(initialValue: validRange.lowerBound)
self.validRange = validRange
self.calendar = calendar
self.selectionType = selectionType
self.calendarAccessibilityConfiguration = calendarAccessibilityConfiguration
self.initialMonthScroll = initialMonthScroll

monthHeaderDateFormatter = DateFormatter()
Expand Down Expand Up @@ -88,7 +91,8 @@ public struct BPKCalendar<DayAccessoryView: View>: View {
parentProxy: calendarProxy
)
},
dayAccessoryView: dayAccessoryView
dayAccessoryView: dayAccessoryView,
calendarAccessibilityConfiguration: calendarAccessibilityConfiguration
)
yearBadge
}
Expand Down Expand Up @@ -139,6 +143,24 @@ struct BPKCalendar_Previews: PreviewProvider {
),
calendar: calendar,
validRange: minValidDate...maxValidDate,
calendarAccessibilityConfiguration: CalendarAccessibilityConfiguration(
singleSelection: .init(
accessibilityConfigurations: .init(selectionHint: "hint"),
dateFormatter: DateFormatter()
),
rangeSelection: .init(
accessibilityConfigurations: .init(
startSelectionHint: "startSelectionHint",
endSelectionHint: "endSelectionHint",
startSelectionState: "startSelectionState",
endSelectionState: "endSelectionState",
betweenSelectionState: "betweenSelectionState",
startAndEndSelectionState: "startAndEndSelectionState",
returnDatePrompt: "returnDatePrompt"
),
dateFormatter: DateFormatter()
)
),
dayAccessoryView: { _ in
BPKText("20", style: .caption)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,111 @@

import SwiftUI

struct CalendarSelectableCell<Cell: View>: View {
@ViewBuilder let cell: Cell
let onSelection: () -> Void
struct CalendarSelectableCell: View {
let selectionType: CalendarSelectionType
let calendar: Calendar
let accessibilityProvider: CalendarAccessibilityConfiguration
let dayDate: Date
let onSelection: (Date) -> Void

var body: some View {
cell.onTapGesture(perform: onSelection)
Group {
switch selectionType {
case .range(let selectionState, _):
rangeView(selectionState: selectionState.wrappedValue)
case .single(let selection, _):
singleView(selectionState: selection.wrappedValue)
}
}.onTapGesture(perform: {
onSelection(dayDate)
})
}

@ViewBuilder private func rangeView(selectionState: CalendarRangeSelectionState?) -> some View {
if case .intermediate(let date) = selectionState,
initialSelection(date, matchesDate: dayDate) {
singleCell(date: date)
} else if case .range(let range) = selectionState, range.contains(dayDate) {
rangeCell(closedRange: range, highlightRangeEnds: true)
} else if case .wholeMonth(let range) = selectionState, range.contains(dayDate) {
wholeMonthRangeCell(range: range)
} else {
defaultCell
}
}

@ViewBuilder private func singleView(selectionState: CalendarSingleSelectionState?) -> some View {
switch selectionState {
case .single(let date):
if date == dayDate {
singleCell(date: date)
} else {
defaultCell
}
case .wholeMonth(let closedRange, _):
if closedRange.contains(dayDate) {
rangeCell(closedRange: closedRange, highlightRangeEnds: false)
} else {
defaultCell
}
case .none:
defaultCell
}
}

private var defaultCell: some View {
DefaultCalendarDayCell(calendar: calendar, date: dayDate)
.accessibilityLabel(Text(
accessibilityProvider.rangeSelection.accessibilityLabel(for: dayDate)
))
}

private func singleCell(date: Date) -> some View {
SingleSelectedCell(calendar: calendar, date: dayDate)
.accessibilityLabel(
Text(
accessibilityProvider.rangeSelection.accessibilityLabel(
for: dayDate,
intermediateSelectionDate: date
)
)
)
}

private func rangeCell(closedRange: ClosedRange<Date>, highlightRangeEnds: Bool) -> some View {
RangeSelectionCalendarDayCell(
date: dayDate,
selection: closedRange,
calendar: calendar,
highlightRangeEnds: highlightRangeEnds
)
.accessibilityLabel(Text(
accessibilityProvider.rangeSelection.accessibilityLabel(
for: dayDate,
selection: closedRange
)
))
.accessibility(addTraits: .isSelected)
}

private func wholeMonthRangeCell(range: ClosedRange<Date>) -> some View {
RangeSelectionCalendarDayCell(
date: dayDate,
selection: range,
calendar: calendar,
highlightRangeEnds: false
)
.accessibilityLabel(Text(
accessibilityProvider.rangeSelection.accessibilityLabel(
for: dayDate,
selection: range
)
))
.accessibility(addTraits: .isSelected)
}

private func initialSelection(_ initialDateSelection: Date, matchesDate date: Date) -> Bool {
let matchingDayComponents = calendar.dateComponents([.year, .month, .day], from: date)
return calendar.date(initialDateSelection, matchesComponents: matchingDayComponents)
}
}
31 changes: 19 additions & 12 deletions Backpack-SwiftUI/Calendar/Classes/Core/CalendarMonthGrid.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ struct CalendarMonthGrid<

@State private var dayCellHeight: CGFloat = 0
@ViewBuilder let dayCell: (Date) -> DayCell
@ViewBuilder let emptyLeadingDayCell: () -> EmptyLeadingDayCell
@ViewBuilder let emptyTrailingDayCell: () -> EmptyTrailingDayCell
@ViewBuilder let emptyLeadingDayCell: (EmptyCellInfo) -> EmptyLeadingDayCell
@ViewBuilder let emptyTrailingDayCell: (EmptyCellInfo) -> EmptyTrailingDayCell
@ViewBuilder let dayAccessoryView: (Date) -> DayAccessoryView

private let daysInAWeek = 7
Expand All @@ -42,34 +42,41 @@ struct CalendarMonthGrid<
// Calculate the offset based on the first weekday
let weekdaysOffset = (weekdayOfMonthStart - firstWeekday + daysInAWeek) % daysInAWeek
let daysFromPreviousMonth = weekdaysOffset
let emptyDaysLeading = (0..<daysFromPreviousMonth).map { index in
EmptyCellInfo(cellIndex: index, month: monthDate)
}

let numberOfDaysInMonth = calendar.range(of: .day, in: .month, for: monthDate)!.count
let totalCellsUsed = numberOfDaysInMonth + daysFromPreviousMonth
let remainingCells = daysInAWeek - (totalCellsUsed % daysInAWeek)
let emptyDaysTrailing = (0..<remainingCells).map { index in
EmptyCellInfo(cellIndex: index, month: monthDate)
}

LazyVGrid(
columns: Array(repeating: GridItem(spacing: BPKSpacing.none.value), count: daysInAWeek),
spacing: BPKSpacing.lg.value
) {
// Create cells for the days from the previous month that are shown in the first week of the current month.
ForEach(0..<daysFromPreviousMonth) { _ in
ForEach(emptyDaysLeading) { emptyDayInfo in
VStack(spacing: BPKSpacing.none) {
emptyLeadingDayCell()
emptyLeadingDayCell(emptyDayInfo)
.frame(height: dayCellHeight)
Spacer(minLength: BPKSpacing.none)
}
}

let numberOfDaysInMonth = calendar.range(of: .day, in: .month, for: monthDate)!.count
// Create cells for the days in the current month
currentMonthDayCell(numberOfDaysInMonth: numberOfDaysInMonth)

// Create cells for the days from the next month that are shown in the last week of the current month
// The total number of cells used is the sum of the number of days in the current month and the number of
// days from the previous month that are shown
let totalCellsUsed = numberOfDaysInMonth + daysFromPreviousMonth
let remainingCells = daysInAWeek - (totalCellsUsed % daysInAWeek)


if remainingCells < daysInAWeek {
ForEach(0..<remainingCells) { _ in
ForEach(emptyDaysTrailing) { emptyDayInfo in
VStack(spacing: BPKSpacing.none) {
emptyTrailingDayCell()
emptyTrailingDayCell(emptyDayInfo)
.frame(height: dayCellHeight)
Spacer(minLength: BPKSpacing.none)
}
Expand Down Expand Up @@ -116,8 +123,8 @@ struct CalendarMonthGrid_Previews: PreviewProvider {
dayCell: { day in
BPKText("\(calendar.component(.day, from: day))")
},
emptyLeadingDayCell: { Color.red },
emptyTrailingDayCell: { Color.green },
emptyLeadingDayCell: { _ in Color.red },
emptyTrailingDayCell: { _ in Color.green },
dayAccessoryView: { _ in
BPKText("$200", style: .caption)
.foregroundColor(.infoBannerSuccessColor)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,23 @@

import SwiftUI

public struct CalendarAccessibilityConfiguration {
public let singleSelection: SingleDayAccessibilityProvider
public let rangeSelection: RangeDayAccessibilityProvider
public init(singleSelection: SingleDayAccessibilityProvider, rangeSelection: RangeDayAccessibilityProvider) {
self.singleSelection = singleSelection
self.rangeSelection = rangeSelection
}
}

struct CalendarTypeContainerFactory<MonthHeader: View, DayAccessoryView: View>: View {
let selectionType: CalendarSelectionType
let calendar: Calendar
let validRange: ClosedRange<Date>
let monthScroll: MonthScroll?
@ViewBuilder let monthHeader: (_ monthDate: Date) -> MonthHeader
@ViewBuilder let dayAccessoryView: (Date) -> DayAccessoryView
var calendarAccessibilityConfiguration: CalendarAccessibilityConfiguration

private var accessibilityDateFormatter: DateFormatter {
let formatter = DateFormatter()
Expand All @@ -34,34 +44,102 @@ struct CalendarTypeContainerFactory<MonthHeader: View, DayAccessoryView: View>:
}

var body: some View {
switch selectionType {
case .range(let selection, let accessibilityConfigurations):
RangeCalendarContainer(
selectionState: selection,
CalendarContainer(
calendar: calendar,
validRange: validRange,
monthScroll: monthScroll
) { month in
monthHeader(month)
CalendarMonthGrid(
monthDate: month,
calendar: calendar,
validRange: validRange,
accessibilityProvider: RangeDayAccessibilityProvider(
accessibilityConfigurations: accessibilityConfigurations,
dateFormatter: accessibilityDateFormatter
),
monthScroll: monthScroll,
monthHeader: monthHeader,
dayCell: returnMakeCellFunction(),
emptyLeadingDayCell: emptyLeadingDayCell,
emptyTrailingDayCell: emptyTrailingDayCell,
dayAccessoryView: dayAccessoryView
)
case .single(let selection, let accessibilityConfigurations):
SingleCalendarContainer(
selection: selection,
}
}

func returnMakeCellFunction() -> ((Date) -> CalendarSelectableCell) {
return { dayDate in
CalendarSelectableCell(
selectionType: selectionType,
calendar: calendar,
validRange: validRange,
accessibilityProvider: SingleDayAccessibilityProvider(
accessibilityConfigurations: accessibilityConfigurations,
dateFormatter: accessibilityDateFormatter
),
monthScroll: monthScroll,
monthHeader: monthHeader,
dayAccessoryView: dayAccessoryView
accessibilityProvider: calendarAccessibilityConfiguration,
dayDate: dayDate,
onSelection: handleSelection
)

}

}

@ViewBuilder func emptyLeadingDayCell(for emptyDayInfo: EmptyCellInfo) -> some View {
switch selectionType {
case .range(let selection, _):
if
case .range(let selectionRange) = selection.wrappedValue,
let lastDayOfPreviousMonth = calendar.date(byAdding: .init(day: -1), to: emptyDayInfo.month),
let firstDayOfCurrentMonth = calendar.date(byAdding: .init(day: 1), to: lastDayOfPreviousMonth),
selectionRange.contains(lastDayOfPreviousMonth),
selectionRange.contains(firstDayOfCurrentMonth)
{
Color(.surfaceSubtleColor)
} else {
// otherwise we occupy the space with a clear view
DefaultEmptyCalendarDayCell()
}
case .single:
DefaultEmptyCalendarDayCell()
}
}

@ViewBuilder func emptyTrailingDayCell(for emptyDayInfo: EmptyCellInfo) -> some View {
switch selectionType {
case .range(let selection, _):
if
case .range(let selectionRange) = selection.wrappedValue,
let firstDayOfNextMonth = calendar.date(byAdding: .init(month: 1), to: emptyDayInfo.month),
let lastDayOfCurrentMonth = calendar.date(byAdding: .init(day: -1), to: firstDayOfNextMonth),
selectionRange.contains(lastDayOfCurrentMonth),
selectionRange.contains(firstDayOfNextMonth)
{
Color(.surfaceSubtleColor)
} else {
// otherwise we occupy the space with a clear view
DefaultEmptyCalendarDayCell()
}
case .single:
DefaultEmptyCalendarDayCell()
}
}

func handleSelection(dayDate: Date) {
switch selectionType {
case .range(let selection, _):
switch selection.wrappedValue {
case .intermediate(let initialDateSelection):
if dayDate < initialDateSelection {
selection.wrappedValue = .intermediate(dayDate)
UIAccessibility.post(
notification: .announcement,
argument: calendarAccessibilityConfiguration.rangeSelection
.accessibilityInstructionAfterSelectingDate()
)
} else {
selection.wrappedValue = .range(initialDateSelection...dayDate)
}
default:
selection.wrappedValue = .intermediate(dayDate)
UIAccessibility.post(
notification: .announcement,
argument: calendarAccessibilityConfiguration.rangeSelection
.accessibilityInstructionAfterSelectingDate()
)
}
case .single(let selection, _):
selection.wrappedValue = .single(dayDate)
}
}
}
Loading

0 comments on commit e55955b

Please sign in to comment.