From 2479701aa1f094a3e7b16d89df5fdcfa45a2d5af Mon Sep 17 00:00:00 2001 From: angiexyang Date: Wed, 9 Oct 2024 14:07:35 -0700 Subject: [PATCH] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20[JIRA:HCPSDKFIORIUIKIT-2?= =?UTF-8?q?687]=20DateTimePicker(2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Picker/DateTimePickerExample.swift | 42 +++++---- .../CompositeComponentProtocols.swift | 24 +++++ .../DateTimePickerStyle.fiori.swift | 88 +++++++++---------- .../DateTimePicker.generated.swift | 41 +++++++-- .../DateTimePickerStyle.generated.swift | 3 + 5 files changed, 131 insertions(+), 67 deletions(-) diff --git a/Apps/Examples/Examples/FioriSwiftUICore/Picker/DateTimePickerExample.swift b/Apps/Examples/Examples/FioriSwiftUICore/Picker/DateTimePickerExample.swift index 54e31af0d..207c284e3 100644 --- a/Apps/Examples/Examples/FioriSwiftUICore/Picker/DateTimePickerExample.swift +++ b/Apps/Examples/Examples/FioriSwiftUICore/Picker/DateTimePickerExample.swift @@ -8,6 +8,8 @@ struct DateTimePickerExample: View { @State var s3: Date = .init() @State var s4: Date = .init() @State var s5: Date = .now + @State var s6: Date = .now + @State var s7: Date = .now @State var isRequired = false @State var showsErrorMessage = false @@ -39,24 +41,28 @@ struct DateTimePickerExample: View { List { Toggle("Mandatory Field", isOn: self.$isRequired) Toggle("Show Error/Hint message", isOn: self.$showsErrorMessage) - - DateTimePicker(title: "Default", isRequired: self.isRequired, selectedDate: self.$s1) - .informationView(isPresented: self.$showsErrorMessage, description: AttributedString("The Date should before December.")) - .informationViewStyle(.informational) - DateTimePicker(title: "Date only", isRequired: self.isRequired, selectedDate: self.$s2, pickerComponents: [.date]) - .informationView(isPresented: self.$showsErrorMessage, description: AttributedString("The Date should before December.")) - .informationViewStyle(.error) - DateTimePicker(title: "Time only", isRequired: self.isRequired, selectedDate: self.$s3, pickerComponents: [.hourAndMinute]) - - DateTimePicker(title: "Custom Style", isRequired: self.isRequired, selectedDate: self.$s4) - .titleStyle(CustomTitleStyle()) - .mandatoryFieldIndicatorStyle(CustomIndicatorStyle()) - .valueLabelStyle(CustomValueLabelStyle()) - Text("Disabled") - DateTimePicker(title: "In Disabled Mode", controlState: .disabled, selectedDate: self.$s5) - .disabled(true) - Text("Read-Only") - DateTimePicker(title: "In Read-Only Mode", controlState: .readOnly, selectedDate: self.$s5, pickerComponents: [.date]) + Section(header: Text("")) { + DateTimePicker(title: "Default", isRequired: self.isRequired, selectedDate: self.$s1) + .informationView(isPresented: self.$showsErrorMessage, description: AttributedString("The Date should be before December.")) + .informationViewStyle(.informational) + DateTimePicker(title: "Date only", isRequired: self.isRequired, selectedDate: self.$s2, pickerComponents: [.date]) + .informationView(isPresented: self.$showsErrorMessage, description: AttributedString("The Date should be before December.")) + .informationViewStyle(.error) + DateTimePicker(title: "Time only", isRequired: self.isRequired, selectedDate: self.$s3, pickerComponents: [.hourAndMinute]) + DateTimePicker(title: "Numeric Date Style", isRequired: self.isRequired, selectedDate: self.$s4, pickerComponents: [.date], dateStyle: .numeric) + DateTimePicker(title: "Long long long long long long label", isRequired: self.isRequired, selectedDate: self.$s5) + DateTimePicker(title: "Custom Style", isRequired: self.isRequired, selectedDate: self.$s6) + .titleStyle(CustomTitleStyle()) + .mandatoryFieldIndicatorStyle(CustomIndicatorStyle()) + .valueLabelStyle(CustomValueLabelStyle()) + } + Section(header: Text("Disabled")) { + DateTimePicker(title: "In Disabled Mode", controlState: .disabled, selectedDate: self.$s7) + .disabled(true) + } + Section(header: Text("Read Only")) { + DateTimePicker(title: "In Read-Only Mode", controlState: .readOnly, selectedDate: self.$s7, pickerComponents: [.date]) + } } } } diff --git a/Sources/FioriSwiftUICore/_ComponentProtocols/CompositeComponentProtocols.swift b/Sources/FioriSwiftUICore/_ComponentProtocols/CompositeComponentProtocols.swift index 4b5edbc71..3a96c4e38 100755 --- a/Sources/FioriSwiftUICore/_ComponentProtocols/CompositeComponentProtocols.swift +++ b/Sources/FioriSwiftUICore/_ComponentProtocols/CompositeComponentProtocols.swift @@ -534,13 +534,37 @@ protocol _TimelinePreviewComponent: _OptionalTitleComponent, _ActionComponent { // sourcery: CompositeComponent protocol _SwitchViewComponent: _TitleComponent, _SwitchComponent {} +/// `DateTimePicker` provides a title and value label with Fiori styling and a `DatePicker`. +/// +/// ## Usage +/// ```swift +/// @State var selection: Date = .init(timeIntervalSince1970: 0.0) +/// @State var isRequired = false +/// @State var showsErrorMessage = false +/// +/// DateTimePicker(title: "Default", isRequired: self.isRequired, selectedDate: self.$selection) +/// .informationView(isPresented: self.$showsErrorMessage, description: AttributedString("The Date should be before December.")) +/// .informationViewStyle(.informational) +/// ``` // sourcery: CompositeComponent protocol _DateTimePickerComponent: _TitleComponent, _ValueLabelComponent, _MandatoryField, _FormViewComponent { // sourcery: @Binding var selectedDate: Date { get } // sourcery: defaultValue = [.date, .hourAndMinute] + /// The components shown in the date picker, default value shows date and time. var pickerComponents: DatePicker.Components { get } + + // sourcery: defaultValue = .abbreviated + /// The custom style for displaying the date. The default value is `.abbreviated`, showing for example, "Oct 21, 2015". + var dateStyle: Date.FormatStyle.DateStyle { get } + + // sourcery: defaultValue = .shortened + /// The custom style for displaying the time. The default value is `.shortened`, showing for example, "4:29 PM" or "16:29". + var timeStyle: Date.FormatStyle.TimeStyle { get } + + /// The text to be displayed when no date is selected. If this property is `nil`, the localized string “No date selected” will be used. + var noDateSelectedString: String? { get } } // sourcery: CompositeComponent diff --git a/Sources/FioriSwiftUICore/_FioriStyles/DateTimePickerStyle.fiori.swift b/Sources/FioriSwiftUICore/_FioriStyles/DateTimePickerStyle.fiori.swift index 0902d58e4..e27cdf59e 100644 --- a/Sources/FioriSwiftUICore/_FioriStyles/DateTimePickerStyle.fiori.swift +++ b/Sources/FioriSwiftUICore/_FioriStyles/DateTimePickerStyle.fiori.swift @@ -4,49 +4,63 @@ import SwiftUI // Base Layout style public struct DateTimePickerBaseStyle: DateTimePickerStyle { - @State var dateString: String = NSLocalizedString("No date selected", tableName: "FioriSwiftUICore", bundle: Bundle.accessor, comment: "") @State var pickerVisible: Bool = false - + @Environment(\.dynamicTypeSize) var dynamicTypeSize + public func makeBody(_ configuration: DateTimePickerConfiguration) -> some View { VStack { - HStack { - HStack(spacing: 0) { - configuration.title - if configuration.isRequired { - configuration.mandatoryFieldIndicator - } - Spacer() + if self.dynamicTypeSize >= .accessibility3 { + self.configureMainStack(configuration, isVertical: true) + } else { + ViewThatFits { + self.configureMainStack(configuration, isVertical: false) + self.configureMainStack(configuration, isVertical: true) } - Spacer() - ValueLabel(valueLabel: AttributedString(self.getValueLabel(configuration))) - .foregroundStyle(self.getFontColor(configuration)) - .font(.fiori(forTextStyle: .body)) - } - .padding(.vertical, 12) - .contentShape(Rectangle()) - .ifApply(configuration.controlState != .disabled && configuration.controlState != .readOnly) { - $0.onTapGesture(perform: { - if configuration.selectedDate == Date(timeIntervalSince1970: 0.0) { - configuration.selectedDate = Date() - } - self.pickerVisible.toggle() - }) } if self.pickerVisible { Divider() .frame(height: 0.33) .foregroundStyle(Color.preferredColor(.separatorOpaque)) - .padding(.leading, 16) self.showPicker(configuration) } } - .accessibilityElement() } + func configureMainStack(_ configuration: DateTimePickerConfiguration, isVertical: Bool) -> some View { + let mainStack = isVertical ? AnyLayout(VStackLayout(alignment: .leading, spacing: 3)) : AnyLayout(HStackLayout()) + return mainStack { + HStack(spacing: 0) { + configuration.title + if configuration.isRequired { + configuration.mandatoryFieldIndicator + } + } + if !isVertical { + Spacer() + } else { + Divider().hidden() + } + ValueLabel(valueLabel: AttributedString(self.getValueLabel(configuration))) + .foregroundStyle(self.getFontColor(configuration)) + .font(.fiori(forTextStyle: .body)) + .accessibilityLabel(self.getValueLabel(configuration)) + } + .accessibilityElement(children: .combine) + .contentShape(Rectangle()) + .ifApply(configuration.controlState != .disabled && configuration.controlState != .readOnly) { + $0.onTapGesture(perform: { + if configuration.selectedDate == Date(timeIntervalSince1970: 0.0) { + configuration.selectedDate = Date() + } + self.pickerVisible.toggle() + }) + } + } + func getValueLabel(_ configuration: DateTimePickerConfiguration) -> String { if configuration.selectedDate != Date(timeIntervalSince1970: 0.0) { - let formattedDate = configuration.selectedDate.formatted(date: .abbreviated, time: .omitted) - let formattedTime = configuration.selectedDate.formatted(date: .omitted, time: .shortened) + let formattedDate = configuration.selectedDate.formatted(date: configuration.dateStyle, time: .omitted) + let formattedTime = configuration.selectedDate.formatted(date: .omitted, time: configuration.timeStyle) if configuration.pickerComponents == .date { return formattedDate } else if configuration.pickerComponents == .hourAndMinute { @@ -55,7 +69,7 @@ public struct DateTimePickerBaseStyle: DateTimePickerStyle { return formattedDate + " " + formattedTime } } - return self.dateString + return configuration.noDateSelectedString ?? NSLocalizedString("No date selected", tableName: "FioriSwiftUICore", bundle: Bundle.accessor, comment: "") } func getFontColor(_ configuration: DateTimePickerConfiguration) -> Color { @@ -69,25 +83,11 @@ public struct DateTimePickerBaseStyle: DateTimePickerStyle { } func showPicker(_ configuration: DateTimePickerConfiguration) -> some View { - let picker = DatePicker("", selection: configuration.$selectedDate, displayedComponents: configuration.pickerComponents) + DatePicker("", selection: configuration.$selectedDate, displayedComponents: configuration.pickerComponents) .datePickerStyle(.graphical) - .padding(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16)) .onChange(of: configuration.selectedDate, perform: { _ in - self.formatDate(configuration) + _ = self.getValueLabel(configuration) }) - return picker - } - - func formatDate(_ configuration: DateTimePickerConfiguration) { - let formattedDate = configuration.selectedDate.formatted(date: .abbreviated, time: .omitted) - let formattedTime = configuration.selectedDate.formatted(date: .omitted, time: .shortened) - if configuration.pickerComponents == .date { - self.dateString = formattedDate - } else if configuration.pickerComponents == .hourAndMinute { - self.dateString = formattedTime - } else { - self.dateString = formattedDate + " " + formattedTime - } } } diff --git a/Sources/FioriSwiftUICore/_generated/StyleableComponents/DateTimePicker/DateTimePicker.generated.swift b/Sources/FioriSwiftUICore/_generated/StyleableComponents/DateTimePicker/DateTimePicker.generated.swift index 6704565cf..8847cd059 100644 --- a/Sources/FioriSwiftUICore/_generated/StyleableComponents/DateTimePicker/DateTimePicker.generated.swift +++ b/Sources/FioriSwiftUICore/_generated/StyleableComponents/DateTimePicker/DateTimePicker.generated.swift @@ -3,6 +3,18 @@ import Foundation import SwiftUI +/// `DateTimePicker` provides a title and value label with Fiori styling and a `DatePicker`. +/// +/// ## Usage +/// ```swift +/// @State var selection: Date = .init(timeIntervalSince1970: 0.0) +/// @State var isRequired = false +/// @State var showsErrorMessage = false +/// +/// DateTimePicker(title: "Default", isRequired: self.isRequired, selectedDate: self.$selection) +/// .informationView(isPresented: self.$showsErrorMessage, description: AttributedString("The Date should be before December.")) +/// .informationViewStyle(.informational) +/// ``` public struct DateTimePicker { let title: any View let valueLabel: any View @@ -13,7 +25,14 @@ public struct DateTimePicker { /// The error message of the form view. let errorMessage: AttributedString? @Binding var selectedDate: Date + /// The components shown in the date picker, default value shows date and time. let pickerComponents: DatePicker.Components + /// The custom style for displaying the date. The default value is `.abbreviated`, showing for example, "Oct 21, 2015". + let dateStyle: Date.FormatStyle.DateStyle + /// The custom style for displaying the time. The default value is `.shortened`, showing for example, "4:29 PM" or "16:29". + let timeStyle: Date.FormatStyle.TimeStyle + /// The text to be displayed when no date is selected. If this property is `nil`, the localized string “No date selected” will be used. + let noDateSelectedString: String? @Environment(\.dateTimePickerStyle) var style @@ -26,7 +45,10 @@ public struct DateTimePicker { controlState: ControlState = .normal, errorMessage: AttributedString? = nil, selectedDate: Binding, - pickerComponents: DatePicker.Components = [.date, .hourAndMinute]) + pickerComponents: DatePicker.Components = [.date, .hourAndMinute], + dateStyle: Date.FormatStyle.DateStyle = .abbreviated, + timeStyle: Date.FormatStyle.TimeStyle = .shortened, + noDateSelectedString: String? = nil) { self.title = Title(title: title) self.valueLabel = ValueLabel(valueLabel: valueLabel) @@ -36,6 +58,9 @@ public struct DateTimePicker { self.errorMessage = errorMessage self._selectedDate = selectedDate self.pickerComponents = pickerComponents + self.dateStyle = dateStyle + self.timeStyle = timeStyle + self.noDateSelectedString = noDateSelectedString } } @@ -47,9 +72,12 @@ public extension DateTimePicker { controlState: ControlState = .normal, errorMessage: AttributedString? = nil, selectedDate: Binding, - pickerComponents: DatePicker.Components = [.date, .hourAndMinute]) + pickerComponents: DatePicker.Components = [.date, .hourAndMinute], + dateStyle: Date.FormatStyle.DateStyle = .abbreviated, + timeStyle: Date.FormatStyle.TimeStyle = .shortened, + noDateSelectedString: String? = nil) { - self.init(title: { Text(title) }, valueLabel: { OptionalText(valueLabel) }, mandatoryFieldIndicator: { TextOrIconView(mandatoryFieldIndicator) }, isRequired: isRequired, controlState: controlState, errorMessage: errorMessage, selectedDate: selectedDate, pickerComponents: pickerComponents) + self.init(title: { Text(title) }, valueLabel: { OptionalText(valueLabel) }, mandatoryFieldIndicator: { TextOrIconView(mandatoryFieldIndicator) }, isRequired: isRequired, controlState: controlState, errorMessage: errorMessage, selectedDate: selectedDate, pickerComponents: pickerComponents, dateStyle: dateStyle, timeStyle: timeStyle, noDateSelectedString: noDateSelectedString) } } @@ -67,6 +95,9 @@ public extension DateTimePicker { self.errorMessage = configuration.errorMessage self._selectedDate = configuration.$selectedDate self.pickerComponents = configuration.pickerComponents + self.dateStyle = configuration.dateStyle + self.timeStyle = configuration.timeStyle + self.noDateSelectedString = configuration.noDateSelectedString self._shouldApplyDefaultStyle = shouldApplyDefaultStyle } } @@ -76,7 +107,7 @@ extension DateTimePicker: View { if self._shouldApplyDefaultStyle { self.defaultStyle() } else { - self.style.resolve(configuration: .init(title: .init(self.title), valueLabel: .init(self.valueLabel), mandatoryFieldIndicator: .init(self.mandatoryFieldIndicator), isRequired: self.isRequired, controlState: self.controlState, errorMessage: self.errorMessage, selectedDate: self.$selectedDate, pickerComponents: self.pickerComponents)).typeErased + self.style.resolve(configuration: .init(title: .init(self.title), valueLabel: .init(self.valueLabel), mandatoryFieldIndicator: .init(self.mandatoryFieldIndicator), isRequired: self.isRequired, controlState: self.controlState, errorMessage: self.errorMessage, selectedDate: self.$selectedDate, pickerComponents: self.pickerComponents, dateStyle: self.dateStyle, timeStyle: self.timeStyle, noDateSelectedString: self.noDateSelectedString)).typeErased .transformEnvironment(\.dateTimePickerStyleStack) { stack in if !stack.isEmpty { stack.removeLast() @@ -94,7 +125,7 @@ private extension DateTimePicker { } func defaultStyle() -> some View { - DateTimePicker(.init(title: .init(self.title), valueLabel: .init(self.valueLabel), mandatoryFieldIndicator: .init(self.mandatoryFieldIndicator), isRequired: self.isRequired, controlState: self.controlState, errorMessage: self.errorMessage, selectedDate: self.$selectedDate, pickerComponents: self.pickerComponents)) + DateTimePicker(.init(title: .init(self.title), valueLabel: .init(self.valueLabel), mandatoryFieldIndicator: .init(self.mandatoryFieldIndicator), isRequired: self.isRequired, controlState: self.controlState, errorMessage: self.errorMessage, selectedDate: self.$selectedDate, pickerComponents: self.pickerComponents, dateStyle: self.dateStyle, timeStyle: self.timeStyle, noDateSelectedString: self.noDateSelectedString)) .shouldApplyDefaultStyle(false) .dateTimePickerStyle(DateTimePickerFioriStyle.ContentFioriStyle()) .typeErased diff --git a/Sources/FioriSwiftUICore/_generated/StyleableComponents/DateTimePicker/DateTimePickerStyle.generated.swift b/Sources/FioriSwiftUICore/_generated/StyleableComponents/DateTimePicker/DateTimePickerStyle.generated.swift index aa6700e47..e666ced98 100644 --- a/Sources/FioriSwiftUICore/_generated/StyleableComponents/DateTimePicker/DateTimePickerStyle.generated.swift +++ b/Sources/FioriSwiftUICore/_generated/StyleableComponents/DateTimePicker/DateTimePickerStyle.generated.swift @@ -30,6 +30,9 @@ public struct DateTimePickerConfiguration { public let errorMessage: AttributedString? @Binding public var selectedDate: Date public let pickerComponents: DatePicker.Components + public let dateStyle: Date.FormatStyle.DateStyle + public let timeStyle: Date.FormatStyle.TimeStyle + public let noDateSelectedString: String? public typealias Title = ConfigurationViewWrapper public typealias ValueLabel = ConfigurationViewWrapper