Skip to content

Commit cbfaca3

Browse files
authored
feat: Improving the implementation of DatePicker (#23)
1 parent 28623cf commit cbfaca3

File tree

5 files changed

+141
-68
lines changed

5 files changed

+141
-68
lines changed

Sources/Authenticator/Models/SignUpField.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -308,21 +308,23 @@ public extension SignUpField where Self == BaseSignUpField {
308308
/// A date-based field associated with the given attribute key
309309
/// - Parameter key: The `AuthUserAttributeKey`
310310
/// - Parameter label: The label that is displayed along the field
311+
/// - Parameter placeholder: The placeholder that is displayed in the field
311312
/// - Parameter isRequired: Whether the view will require a date to be entered before proceeding. Defaults to false.
312313
/// - Parameter minDate: The minimum date this field's value can be set to. Defaults to nil
313314
/// - Parameter maxDate: The maximum date this field's value can be set to. Defaults to nil
314315
/// - Parameter validator: An additional validator that will be invoked before proceeding. Defaults to nil
315316
static func date(
316317
key: AuthUserAttributeKey,
317318
label: String,
319+
placeholder: String,
318320
isRequired: Bool = false,
319321
minDate: Date? = nil,
320322
maxDate: Date? = nil,
321323
validator: FieldValidator? = nil
322324
) -> SignUpField {
323325
return signUpField(
324326
label: label,
325-
placeholder: "",
327+
placeholder: placeholder,
326328
isRequired: isRequired,
327329
attributeType: .custom(attributeKey: key),
328330
inputType: .date,

Sources/Authenticator/Resources/en.lproj/Localizable.strings

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"authenticator.imageButton.open" = "Open";
1616
"authenticator.imageButton.showPassword" = "Show password";
1717
"authenticator.imageButton.hidePassword" = "Hide password";
18+
"authenticator.imageButton.calendar" = "Calendar";
1819

1920
"authenticator.field.username.label" = "Username";
2021
"authenticator.field.username.placeholder" = "Enter your username";

Sources/Authenticator/Views/Internal/SignUpInputField.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ struct SignUpInputField: View {
5252
DatePicker(
5353
field.label,
5454
text: $field.value,
55+
placeholder: field.placeholder,
5556
validator: validator
5657
)
5758
case .phoneNumber:

Sources/Authenticator/Views/Primitives/DatePicker.swift

Lines changed: 133 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -7,104 +7,157 @@
77

88
import SwiftUI
99

10-
/// This field allows the user to select a Date and parse it to a ISO-8601 format.
11-
/// It displays a label and a "Select date" button, which when tapped shows a native DatePicker
12-
/// This is done in order to allow not selecting anything, which the native component doesn't
10+
/// This field allows the user to select a date and parse it to a ISO-8601 format.
11+
/// It allows to select a date by revealing a native `SwiftUI.DatePicker` when tapped.
12+
/// It also applies Amplify UI theming
1313
struct DatePicker: View {
1414
@Environment(\.isEnabled) private var isEnabled: Bool
1515
@Environment(\.authenticatorOptions) private var options
1616
@Environment(\.authenticatorTheme) var theme
1717
@ObservedObject private var validator: Validator
1818
@State private var selectedDate: Date = .now
1919
@State private var actualDate: Date? = nil
20+
@State private var isShowingDatePicker = false
2021
@FocusState private var isFocused: Bool
2122
@Binding private var text: String
2223
private let label: String
24+
private let placeholder: String
2325
private let formatter = ISO8601DateFormatter()
2426

2527
init(_ label: String,
2628
text: Binding<String>,
29+
placeholder: String,
2730
validator: Validator? = nil) {
2831
self.label = label
2932
self._text = text
30-
33+
self.placeholder = placeholder
3134
self.validator = validator ?? .init(
3235
using: FieldValidators.none
3336
)
3437
self.validator.value = text
3538
}
3639

40+
#if os(iOS)
3741
var body: some View {
3842
VStack(alignment: .trailing, spacing: theme.components.field.spacing.vertical) {
39-
HStack(alignment: .center, spacing: theme.components.field.spacing.horizontal) {
40-
SwiftUI.Text(label)
41-
.foregroundColor(foregroundColor)
42-
.font(theme.fonts.body)
43-
.accessibilityHidden(true)
44-
45-
Spacer()
46-
47-
if actualDate == nil {
48-
HStack(spacing: 0) {
49-
Button("authenticator.field.date.label".localized()) {
50-
updateDate(selectedDate)
43+
AuthenticatorField(
44+
label,
45+
placeholder: placeholder,
46+
validator: validator,
47+
isFocused: isFocused
48+
) {
49+
HStack(spacing: 0) {
50+
createDisplayedDateText()
51+
52+
Spacer()
53+
54+
if !text.isEmpty {
55+
ImageButton(.clear) {
56+
actualDate = nil
57+
text = ""
58+
validator.validate()
5159
}
52-
.buttonStyle(.link)
53-
.frame(maxWidth: nil)
60+
.tintColor(clearButtonColor)
61+
.padding([.top, .bottom, .trailing], theme.components.field.padding)
62+
}
5463

55-
ImageButton(.open) {
56-
updateDate(selectedDate)
64+
Divider()
65+
.frame(width: 1)
66+
.overlay(theme.colors.border.primary)
67+
68+
ImageButton(.calendar) {
69+
withAnimation {
70+
isShowingDatePicker.toggle()
5771
}
58-
.tintColor(tintColor)
59-
.padding([.top, .bottom], theme.components.field.padding)
60-
.accessibilityHidden(true)
6172
}
62-
.accessibilityAddTraits(.isButton)
63-
.accessibilityElement(children: .combine)
64-
} else {
65-
SwiftUI.DatePicker(
66-
"",
67-
selection: $selectedDate,
68-
displayedComponents: .date
69-
)
70-
.fixedSize()
71-
.tint(theme.colors.background.interactive)
72-
.focused($isFocused)
73-
.onChange(of: selectedDate) { date in
74-
updateDate(date)
73+
.tintColor(tintColor)
74+
.padding(theme.components.field.padding)
75+
.frame(maxHeight: .infinity)
76+
}
77+
}
78+
.contentShape(Rectangle())
79+
.onTapGesture {
80+
withAnimation {
81+
isShowingDatePicker.toggle()
82+
}
83+
}
84+
85+
createDatePicker(style: .graphical)
86+
.frame(height: isShowingDatePicker ? nil : 0, alignment: .top)
87+
.clipped()
88+
}
89+
}
90+
#elseif os(macOS)
91+
var body: some View {
92+
VStack(alignment: .trailing, spacing: theme.components.field.spacing.vertical) {
93+
AuthenticatorField(
94+
label,
95+
placeholder: placeholder,
96+
validator: validator,
97+
isFocused: isFocused
98+
) {
99+
HStack(spacing: 0) {
100+
if isShowingDatePicker {
101+
createDatePicker(height: 20, style: .stepperField)
102+
} else {
103+
createDisplayedDateText()
75104
}
76-
.onChange(of: isFocused) { isFocused in
77-
if !isFocused {
105+
106+
Spacer()
107+
108+
if isShowingDatePicker {
109+
ImageButton(.clear) {
110+
actualDate = nil
111+
text = ""
78112
validator.validate()
113+
isShowingDatePicker = false
79114
}
115+
.tintColor(clearButtonColor)
116+
.padding([.top, .bottom, .trailing], theme.components.field.padding)
80117
}
81-
82-
ImageButton(.clear) {
83-
actualDate = nil
84-
text = ""
85-
validator.validate()
86-
}
87-
.tintColor(tintColor)
88-
.padding([.top, .bottom], theme.components.field.padding)
89118
}
90119
}
91-
92-
if let errorMessage = errorMessage {
93-
SwiftUI.Text(errorMessage)
94-
.font(theme.fonts.subheadline)
95-
.foregroundColor(borderColor)
96-
.transition(options.contentTransition)
120+
.contentShape(Rectangle())
121+
.onTapGesture {
122+
guard !Platform.isMacOS || text.isEmpty else {
123+
return
124+
}
125+
withAnimation {
126+
isShowingDatePicker.toggle()
127+
}
97128
}
98129
}
99-
.accessibilityElement(children: .contain)
100-
.accessibilityLabel(accessibilityLabel)
101-
.background(backgroundColor)
102-
.animation(options.contentAnimation, value: validator.state)
130+
}
131+
#endif
132+
133+
@ViewBuilder private func createDatePicker<S: DatePickerStyle>(
134+
height: CGFloat? = nil,
135+
style: S
136+
) -> some View {
137+
SwiftUI.DatePicker(
138+
"",
139+
selection: $selectedDate,
140+
displayedComponents: .date
141+
)
142+
.frame(height: height)
143+
.datePickerStyle(style)
144+
.tint(theme.colors.background.interactive)
145+
.onChange(of: selectedDate) { date in
146+
updateDate(date)
147+
}
148+
.padding([.top, .bottom], theme.components.field.padding)
149+
}
103150

151+
@ViewBuilder private func createDisplayedDateText() -> some View {
152+
Text(displayedDate)
153+
.frame(height: Platform.isMacOS ? 20 : 25)
154+
.padding([.top, .bottom, .leading], theme.components.field.padding)
155+
.foregroundColor(text.isEmpty ? placeholderColor : theme.colors.foreground.primary)
156+
.accessibilityAddTraits(.isButton)
104157
}
105158

106159
private var tintColor: Color {
107-
if actualDate == nil {
160+
if isShowingDatePicker {
108161
return theme.colors.background.interactive
109162
}
110163

@@ -142,19 +195,32 @@ struct DatePicker: View {
142195
validator.validate()
143196
}
144197

145-
private var errorMessage: String? {
146-
if case .error(let message) = validator.state,
147-
let message = message {
148-
return String(format: message, label)
198+
private var displayedDate: String {
199+
guard let date = actualDate else {
200+
return placeholder
149201
}
150-
return nil
202+
203+
return date.formatted(
204+
.dateTime
205+
.day()
206+
.month()
207+
.year()
208+
)
151209
}
152210

153-
private var accessibilityLabel: Text {
154-
if let errorMessage = errorMessage {
155-
return Text(errorMessage)
156-
}
211+
private var placeholderColor: Color {
212+
Platform.isMacOS
213+
? Color(red: 178/255, green: 178/255, blue: 178/255)
214+
: Color(red: 184/255, green: 184/255, blue: 187/255)
215+
}
157216

158-
return Text(label)
217+
private var clearButtonColor: Color {
218+
switch validator.state {
219+
case .normal:
220+
return isFocused ?
221+
theme.colors.border.interactive : theme.colors.border.primary
222+
case .error:
223+
return theme.colors.border.error
224+
}
159225
}
160226
}

Sources/Authenticator/Views/Primitives/ImageButton.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ struct ImageButton: View {
5555
return "authenticator.imageButton.showPassword".localized()
5656
case .hidePassword:
5757
return "authenticator.imageButton.hidePassword".localized()
58+
case .calendar:
59+
return "authenticator.imageButton.calendar".localized()
5860
}
5961
}
6062
}
@@ -66,5 +68,6 @@ extension ImageButton {
6668
case open = "chevron.down.circle"
6769
case showPassword = "eye.fill"
6870
case hidePassword = "eye.slash.fill"
71+
case calendar = "calendar"
6972
}
7073
}

0 commit comments

Comments
 (0)