Skip to content

Commit 5d19c0c

Browse files
feat: 🎸 [HCPSDKFIORIUIKIT-2962] Date Range Picker (SAP#1152)
* feat: 🎸 [HCPSDKFIORIUIKIT-2962] Date Range Picker Add date range picker support * feat: 🎸 [HCPSDKFIORIUIKIT-2962] Date Range Picker Add accessibility support * feat: 🎸 [HCPSDKFIORIUIKIT-2962] Date Range Picker Add unit tests * feat: 🎸 [HCPSDKFIORIUIKIT-2962] Date Range Picker Remove debug entrance * feat: 🎸 [HCPSDKFIORIUIKIT-2962] Date Range Picker Add more unit tests * feat: 🎸 [HCPSDKFIORIUIKIT-2962] Date Range Picker Bot review issue fix * feat: 🎸 [HCPSDKFIORIUIKIT-2962] Date Range Picker Bot review issue fix * feat: 🎸 [HCPSDKFIORIUIKIT-2962] Date Range Picker Add more examples * feat: 🎸 [HCPSDKFIORIUIKIT-2962] Date Range Picker Add more API doc --------- Co-authored-by: dyongxu <61523257+dyongxu@users.noreply.github.com>
1 parent cf8147c commit 5d19c0c

File tree

16 files changed

+988
-0
lines changed

16 files changed

+988
-0
lines changed

Apps/Examples/Examples.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
6D10F8A02C7DB3F50071DD3E /* BannerMultiMessageExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D10F89F2C7DB3F50071DD3E /* BannerMultiMessageExample.swift */; };
5151
6D14F05E2C9290F20053BA98 /* BannerMultiMessageCustomInitExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D14F05D2C9290F20053BA98 /* BannerMultiMessageCustomInitExample.swift */; };
5252
6D3A3DE92CDB5F1E004D4597 /* ObjectCellEnhancementExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D3A3DE82CDB5F1E004D4597 /* ObjectCellEnhancementExample.swift */; };
53+
6D5937D02E13D10D007955DB /* DateRangePickerExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D5937CF2E13D10D007955DB /* DateRangePickerExample.swift */; };
5354
6D66D7F12D02FC7B00F7A97D /* ActivityItemExample.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D66D7EF2D02FC7B00F7A97D /* ActivityItemExample.swift */; };
5455
6D6E25672D364F78009A62CA /* OnBoardingWelcomeScreenExamples.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D6E25662D364F78009A62CA /* OnBoardingWelcomeScreenExamples.swift */; };
5556
6D6E256D2D378025009A62CA /* FilterFormViewExamples.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D6E256C2D378025009A62CA /* FilterFormViewExamples.swift */; };
@@ -320,6 +321,7 @@
320321
6D10F89F2C7DB3F50071DD3E /* BannerMultiMessageExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BannerMultiMessageExample.swift; sourceTree = "<group>"; };
321322
6D14F05D2C9290F20053BA98 /* BannerMultiMessageCustomInitExample.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BannerMultiMessageCustomInitExample.swift; sourceTree = "<group>"; };
322323
6D3A3DE82CDB5F1E004D4597 /* ObjectCellEnhancementExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObjectCellEnhancementExample.swift; sourceTree = "<group>"; };
324+
6D5937CF2E13D10D007955DB /* DateRangePickerExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateRangePickerExample.swift; sourceTree = "<group>"; };
323325
6D66D7EF2D02FC7B00F7A97D /* ActivityItemExample.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityItemExample.swift; sourceTree = "<group>"; };
324326
6D6E25662D364F78009A62CA /* OnBoardingWelcomeScreenExamples.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnBoardingWelcomeScreenExamples.swift; sourceTree = "<group>"; };
325327
6D6E256C2D378025009A62CA /* FilterFormViewExamples.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterFormViewExamples.swift; sourceTree = "<group>"; };
@@ -1076,6 +1078,7 @@
10761078
5A91FABD2D923BF70024F316 /* _DurationPickerExample.swift */,
10771079
6432FF9F2C5164F8008ECE89 /* SegmentedControlExample.swift */,
10781080
64905D082C7693970062AAD4 /* DateTimePickerExample.swift */,
1081+
6D5937CF2E13D10D007955DB /* DateRangePickerExample.swift */,
10791082
);
10801083
path = Picker;
10811084
sourceTree = "<group>";
@@ -1480,6 +1483,7 @@
14801483
B80DA9C72612A54E00C0B2E9 /* WelcomeScreenSample.swift in Sources */,
14811484
8A5579D724C1293C0098003A /* SettingsCategoryAxis.swift in Sources */,
14821485
9D0086692BA8F6820004BE15 /* TitleFormViewExample.swift in Sources */,
1486+
6D5937D02E13D10D007955DB /* DateRangePickerExample.swift in Sources */,
14831487
6DEC31F82C47B7850084DD20 /* FioriButtonStyleToggleExample.swift in Sources */,
14841488
B141D6BB29261F9E008A8BD6 /* SearchableListViewExample.swift in Sources */,
14851489
6D6E256D2D378025009A62CA /* FilterFormViewExamples.swift in Sources */,

Apps/Examples/Examples/FioriSwiftUICore/CoreContentView.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ struct CoreContentView: View {
8282
MenuItem(title: "DateTimePicker", section: "Pickers", destination: DateTimePickerExample()),
8383
MenuItem(title: "ValuePicker", section: "Pickers", destination: ValuePickerExample()),
8484
MenuItem(title: "OrderPicker", section: "Pickers", destination: OrderPickerExample()),
85+
MenuItem(title: "DateRangePicker", section: "Pickers", destination: DateRangePickerExample()),
8586

8687
// Onboarding
8788
MenuItem(title: "Onboarding", section: "Onboarding", destination: OnboardingExamples(_isNewObjectItem: true)),
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import FioriSwiftUICore
2+
import Foundation
3+
import SwiftUI
4+
5+
struct DateRangePickerExample: View {
6+
@State private var isRequired = false
7+
@State private var customizedMandatoryIndicator = false
8+
@State private var showsErrorMessage = false
9+
@State private var showAINotice: Bool = false
10+
@State private var pickerVisible0 = false
11+
@State private var pickerVisible1 = false
12+
@State private var pickerVisible2 = false
13+
@State private var pickerVisible3 = false
14+
@State private var pickerVisible4 = false
15+
@State private var pickerVisible5 = false
16+
17+
// Limit the selectable dates from last seven days to next seven days
18+
private var limitDateRange: Range<Date> = Date(timeIntervalSinceNow: -60 * 60 * 24 * 7) ..< Date(timeIntervalSinceNow: 60 * 60 * 24 * 7)
19+
20+
struct CustomTitleStyle: TitleStyle {
21+
func makeBody(_ configuration: TitleConfiguration) -> some View {
22+
Title(configuration)
23+
.font(.fiori(forTextStyle: .title3))
24+
.foregroundStyle(Color.preferredColor(.indigo7))
25+
}
26+
}
27+
28+
struct CustomValueLabelStyle: ValueLabelStyle {
29+
func makeBody(_ configuration: ValueLabelConfiguration) -> some View {
30+
ValueLabel(configuration)
31+
.font(.fiori(forTextStyle: .callout))
32+
.foregroundStyle(Color.preferredColor(.green7))
33+
}
34+
}
35+
36+
var managePickerVisibleBinding: Binding<Bool> {
37+
Binding {
38+
[self.pickerVisible0, self.pickerVisible1, self.pickerVisible2, self.pickerVisible3, self.pickerVisible4, self.pickerVisible5].allSatisfy { $0 }
39+
} set: { newValue in
40+
self.pickerVisible0 = newValue
41+
self.pickerVisible1 = newValue
42+
self.pickerVisible2 = newValue
43+
self.pickerVisible3 = newValue
44+
self.pickerVisible4 = newValue
45+
self.pickerVisible5 = newValue
46+
}
47+
}
48+
49+
func mandatoryFieldIndicator(_ disabled: Bool = false) -> TextOrIcon {
50+
var indicator = AttributedString(self.customizedMandatoryIndicator ? "#" : "*")
51+
if self.customizedMandatoryIndicator, !disabled {
52+
indicator.font = .fiori(forTextStyle: .title3)
53+
indicator.foregroundColor = Color.preferredColor(.indigo7)
54+
}
55+
return .text(indicator)
56+
}
57+
58+
@State var selectedRange0: ClosedRange<Date>?
59+
@State var selectedRange1: ClosedRange<Date>? = Date.now ... Date(timeIntervalSinceNow: 24 * 60 * 60 * 2)
60+
@State var selectedRange2: ClosedRange<Date>?
61+
@State var selectedRange3: ClosedRange<Date>? = Date.now ... Date(timeIntervalSinceNow: 24 * 60 * 60 * 80)
62+
@State var selectedRange4: ClosedRange<Date>? = Date.now ... Date(timeIntervalSinceNow: 24 * 60 * 60 * 10)
63+
@State var selectedRange5: ClosedRange<Date>? = Date.now ... Date.now
64+
65+
let customizedDateFormatter: DateFormatter = {
66+
let formatter = DateFormatter()
67+
formatter.dateFormat = "dd-MM-yyyy"
68+
return formatter
69+
}()
70+
71+
var body: some View {
72+
List {
73+
Toggle("Mandatory Field", isOn: self.$isRequired)
74+
.tint(Color.preferredColor(.tintColor))
75+
if self.isRequired {
76+
Toggle("Customized Mandatory Indicator", isOn: self.$customizedMandatoryIndicator)
77+
.tint(Color.preferredColor(.tintColor))
78+
}
79+
Toggle("Show Error/Hint message", isOn: self.$showsErrorMessage)
80+
.tint(Color.preferredColor(.tintColor))
81+
Toggle("AI Notice", isOn: self.$showAINotice)
82+
.tint(Color.preferredColor(.tintColor))
83+
Toggle("Picker Visible", isOn: self.managePickerVisibleBinding)
84+
.tint(Color.preferredColor(.tintColor))
85+
Section(header: Text("")) {
86+
DateRangePicker(title: "Range Selection Long Title Long Title Long Title Long Title Long Title Long Title0", mandatoryFieldIndicator: self.mandatoryFieldIndicator(), isRequired: self.isRequired, selectedRange: self.$selectedRange0, pickerVisible: self.$pickerVisible0)
87+
.informationView(isPresented: self.$showsErrorMessage, description: AttributedString("This is information error message."))
88+
.informationViewStyle(.error)
89+
.aiNoticeView(isPresented: self.$showAINotice, description: "AI Notice")
90+
91+
DateRangePicker(title: "Range Selection1", mandatoryFieldIndicator: self.mandatoryFieldIndicator(), isRequired: self.isRequired, selectedRange: self.$selectedRange1, pickerVisible: self.$pickerVisible1)
92+
.informationView(isPresented: self.$showsErrorMessage, description: AttributedString("This is information hint message."))
93+
.informationViewStyle(.informational)
94+
.aiNoticeView(isPresented: self.$showAINotice, description: "AI Notice")
95+
.titleStyle(CustomTitleStyle())
96+
.valueLabelStyle(CustomValueLabelStyle())
97+
98+
DateRangePicker(title: "Limit inclusive range of selectable dates", mandatoryFieldIndicator: self.mandatoryFieldIndicator(), isRequired: self.isRequired, range: self.limitDateRange, selectedRange: self.$selectedRange2, noRangeSelectedString: "Please select range", pickerVisible: self.$pickerVisible2)
99+
.informationView(isPresented: self.$showsErrorMessage, description: AttributedString("This is information warning message."))
100+
.informationViewStyle(.warning)
101+
.aiNoticeView(isPresented: self.$showAINotice, description: "AI Notice")
102+
103+
DateRangePicker(title: "Customized Date Formatter", mandatoryFieldIndicator: self.mandatoryFieldIndicator(), isRequired: self.isRequired, selectedRange: self.$selectedRange3, rangeFormatter: self.customizedDateFormatter, pickerVisible: self.$pickerVisible3)
104+
.informationView(isPresented: self.$showsErrorMessage, description: AttributedString("This is information success message."))
105+
.informationViewStyle(.success)
106+
.aiNoticeView(isPresented: self.$showAINotice, description: "AI Notice")
107+
108+
DateRangePicker(title: "Custom Locale & Calendar", mandatoryFieldIndicator: self.mandatoryFieldIndicator(), isRequired: self.isRequired, selectedRange: self.$selectedRange4, pickerVisible: self.$pickerVisible4)
109+
.informationView(isPresented: self.$showsErrorMessage, description: AttributedString("This is information hint message."))
110+
.informationViewStyle(.informational)
111+
.aiNoticeView(isPresented: self.$showAINotice, description: "AI Notice")
112+
.environment(\.locale, Locale(identifier: "zh-Hans"))
113+
.environment(\.calendar, Calendar(identifier: .gregorian))
114+
115+
DateRangePicker(title: "Range Selection in Disabled Control State", mandatoryFieldIndicator: self.mandatoryFieldIndicator(true), isRequired: self.isRequired, controlState: .disabled, selectedRange: self.$selectedRange5, pickerVisible: self.$pickerVisible5)
116+
.informationView(isPresented: self.$showsErrorMessage, description: AttributedString("This is information success message."))
117+
.informationViewStyle(.success)
118+
.aiNoticeView(isPresented: self.$showAINotice, description: "AI Notice")
119+
}
120+
}
121+
.onChange(of: self.selectedRange0) { _, newValue in
122+
print("selectedRange0 new Value:\(self.getValueLabel(newValue))")
123+
}
124+
.onChange(of: self.selectedRange1) { _, newValue in
125+
print("selectedRange1 new Value:\(self.getValueLabel(newValue))")
126+
}
127+
.onChange(of: self.selectedRange2) { _, newValue in
128+
print("selectedRange2 new Value:\(self.getValueLabel(newValue))")
129+
}
130+
.onChange(of: self.selectedRange3) { _, newValue in
131+
print("selectedRange3 new Value:\(self.getValueLabel(newValue))")
132+
}
133+
.onChange(of: self.selectedRange4) { _, newValue in
134+
print("selectedRange4 new Value:\(self.getValueLabel(newValue))")
135+
}
136+
.onChange(of: self.selectedRange5) { _, newValue in
137+
print("selectedRange5 new Value:\(self.getValueLabel(newValue))")
138+
}
139+
.navigationTitle("Date Range Picker")
140+
}
141+
142+
private func getValueLabel(_ selectedRange: ClosedRange<Date>?) -> String {
143+
if let startDate = selectedRange?.lowerBound,
144+
let endDate = selectedRange?.upperBound
145+
{
146+
let valueDescDateFormatter = DateFormatter()
147+
valueDescDateFormatter.timeZone = Calendar.current.timeZone
148+
valueDescDateFormatter.locale = Calendar.current.locale
149+
valueDescDateFormatter.dateStyle = .short
150+
valueDescDateFormatter.timeStyle = .none
151+
let startDateStr = valueDescDateFormatter.string(from: startDate)
152+
let endDateStr = valueDescDateFormatter.string(from: endDate)
153+
154+
return "\(startDateStr)\(endDateStr)"
155+
} else {
156+
return "No range selected"
157+
}
158+
}
159+
}
160+
161+
struct DateRangePickerExample_Previews: PreviewProvider {
162+
static var previews: some View {
163+
DateRangePickerExample()
164+
}
165+
}

Sources/FioriSwiftUICore/_ComponentProtocols/CompositeComponentProtocols.swift

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -630,6 +630,60 @@ protocol _DateTimePickerComponent: _TitleComponent, _ValueLabelComponent, _Manda
630630
var pickerVisible: Bool { get set }
631631
}
632632

633+
/// `DateRangePicker` provides a title and value label with Fiori styling and a `MultiDatePicker`.
634+
/// ## Usage
635+
/// ```swift
636+
/// @State var isRequired = false
637+
/// @State var selectedRange1: ClosedRange<Date>? = Date.now...Date.init(timeIntervalSinceNow: 24 * 60 * 60 * 2)
638+
/// @State var selectedRange2: ClosedRange<Date>? = Date.now...Date.init(timeIntervalSinceNow: 24 * 60 * 60 * 2)
639+
/// @State var selectedRange3: ClosedRange<Date>? = Date.now...Date.init(timeIntervalSinceNow: 24 * 60 * 60 * 2)
640+
/// @State var pickerVisible1 = false
641+
/// @State var pickerVisible2 = false
642+
/// @State var pickerVisible3 = false
643+
/// @State var showsErrorMessage = false
644+
/// @State var showAINotice: Bool = false
645+
/// let customizedDateFormatter: DateFormatter = {
646+
/// let formatter = DateFormatter()
647+
/// formatter.dateFormat = "dd-MM-yyyy"
648+
/// return formatter
649+
/// }()
650+
/// DateRangePicker(title: "Range Selection1", isRequired: isRequired, selectedRange: $selectedRange1, pickerVisible: $pickerVisible1)
651+
/// .informationView(isPresented: self.$showsErrorMessage, description: AttributedString("This is information hint message."))
652+
/// .informationViewStyle(.informational)
653+
/// .aiNoticeView(isPresented: self.$showAINotice, description: "AI Notice")
654+
///
655+
/// DateRangePicker(title: "Customized Date Formatter", mandatoryFieldIndicator: self.mandatoryFieldIndicator(), isRequired: self.isRequired, selectedRange: self.$selectedRange2, rangeFormatter: self.customizedDateFormatter, pickerVisible: self.$pickerVisible2)
656+
/// .informationView(isPresented: self.$showsErrorMessage, description: AttributedString("This is information success message."))
657+
/// .informationViewStyle(.success)
658+
/// .aiNoticeView(isPresented: self.$showAINotice, description: "AI Notice")
659+
///
660+
/// DateRangePicker(title: "Custom Locale & Calendar", mandatoryFieldIndicator: self.mandatoryFieldIndicator(), isRequired: self.isRequired, selectedRange: self.$selectedRange3, pickerVisible: self.$pickerVisible3)
661+
/// .informationView(isPresented: self.$showsErrorMessage, description: AttributedString("This is information hint message."))
662+
/// .informationViewStyle(.informational)
663+
/// .aiNoticeView(isPresented: self.$showAINotice, description: "AI Notice")
664+
/// .environment(\.locale, Locale(identifier: "zh-Hans"))
665+
/// .environment(\.calendar, Calendar(identifier: .gregorian))
666+
/// ```
667+
// sourcery: CompositeComponent
668+
protocol _DateRangePickerComponent: _TitleComponent, _ValueLabelComponent, _MandatoryField, _FormViewComponent {
669+
/// The inclusive range of selectable dates.
670+
var range: Range<Date>? { get }
671+
// sourcery: @Binding
672+
// sourcery: defaultValue = ".constant(nil)"
673+
/// The range of selected dates. Default is nil. It's continuous in ascending order.
674+
var selectedRange: ClosedRange<Date>? { get }
675+
676+
/// Range date formatter. The default date formatter conforms system setting, it uses short date type in compact screen and uses long date type in regular screen.
677+
var rangeFormatter: DateFormatter? { get }
678+
679+
/// The text to be displayed when no range is selected. If this property is `nil`, the localized string “No range selected” will be used.
680+
var noRangeSelectedString: String? { get }
681+
682+
// sourcery: @Binding
683+
/// This property indicates whether the picker is to be displayed or not.
684+
var pickerVisible: Bool { get set }
685+
}
686+
633687
// sourcery: CompositeComponent
634688
protocol _AvatarStackComponent: _AvatarsComponent, _AvatarsTitleComponent {}
635689

0 commit comments

Comments
 (0)