Skip to content

Commit

Permalink
feat: 🎸 [JIRA:HCPSDKFIORIUIKIT-2687] DateTimePicker(2)
Browse files Browse the repository at this point in the history
  • Loading branch information
angiexyang committed Oct 9, 2024
1 parent eb956f9 commit 2479701
Show file tree
Hide file tree
Showing 5 changed files with 131 additions and 67 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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])
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand All @@ -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
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand All @@ -26,7 +45,10 @@ public struct DateTimePicker {
controlState: ControlState = .normal,
errorMessage: AttributedString? = nil,
selectedDate: Binding<Date>,
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)
Expand All @@ -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
}
}

Expand All @@ -47,9 +72,12 @@ public extension DateTimePicker {
controlState: ControlState = .normal,
errorMessage: AttributedString? = nil,
selectedDate: Binding<Date>,
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)
}
}

Expand All @@ -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
}
}
Expand All @@ -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()
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 2479701

Please sign in to comment.