|
7 | 7 |
|
8 | 8 | import SwiftUI |
9 | 9 |
|
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 |
13 | 13 | struct DatePicker: View { |
14 | 14 | @Environment(\.isEnabled) private var isEnabled: Bool |
15 | 15 | @Environment(\.authenticatorOptions) private var options |
16 | 16 | @Environment(\.authenticatorTheme) var theme |
17 | 17 | @ObservedObject private var validator: Validator |
18 | 18 | @State private var selectedDate: Date = .now |
19 | 19 | @State private var actualDate: Date? = nil |
| 20 | + @State private var isShowingDatePicker = false |
20 | 21 | @FocusState private var isFocused: Bool |
21 | 22 | @Binding private var text: String |
22 | 23 | private let label: String |
| 24 | + private let placeholder: String |
23 | 25 | private let formatter = ISO8601DateFormatter() |
24 | 26 |
|
25 | 27 | init(_ label: String, |
26 | 28 | text: Binding<String>, |
| 29 | + placeholder: String, |
27 | 30 | validator: Validator? = nil) { |
28 | 31 | self.label = label |
29 | 32 | self._text = text |
30 | | - |
| 33 | + self.placeholder = placeholder |
31 | 34 | self.validator = validator ?? .init( |
32 | 35 | using: FieldValidators.none |
33 | 36 | ) |
34 | 37 | self.validator.value = text |
35 | 38 | } |
36 | 39 |
|
| 40 | +#if os(iOS) |
37 | 41 | var body: some View { |
38 | 42 | 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() |
51 | 59 | } |
52 | | - .buttonStyle(.link) |
53 | | - .frame(maxWidth: nil) |
| 60 | + .tintColor(clearButtonColor) |
| 61 | + .padding([.top, .bottom, .trailing], theme.components.field.padding) |
| 62 | + } |
54 | 63 |
|
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() |
57 | 71 | } |
58 | | - .tintColor(tintColor) |
59 | | - .padding([.top, .bottom], theme.components.field.padding) |
60 | | - .accessibilityHidden(true) |
61 | 72 | } |
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() |
75 | 104 | } |
76 | | - .onChange(of: isFocused) { isFocused in |
77 | | - if !isFocused { |
| 105 | + |
| 106 | + Spacer() |
| 107 | + |
| 108 | + if isShowingDatePicker { |
| 109 | + ImageButton(.clear) { |
| 110 | + actualDate = nil |
| 111 | + text = "" |
78 | 112 | validator.validate() |
| 113 | + isShowingDatePicker = false |
79 | 114 | } |
| 115 | + .tintColor(clearButtonColor) |
| 116 | + .padding([.top, .bottom, .trailing], theme.components.field.padding) |
80 | 117 | } |
81 | | - |
82 | | - ImageButton(.clear) { |
83 | | - actualDate = nil |
84 | | - text = "" |
85 | | - validator.validate() |
86 | | - } |
87 | | - .tintColor(tintColor) |
88 | | - .padding([.top, .bottom], theme.components.field.padding) |
89 | 118 | } |
90 | 119 | } |
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 | + } |
97 | 128 | } |
98 | 129 | } |
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 | + } |
103 | 150 |
|
| 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) |
104 | 157 | } |
105 | 158 |
|
106 | 159 | private var tintColor: Color { |
107 | | - if actualDate == nil { |
| 160 | + if isShowingDatePicker { |
108 | 161 | return theme.colors.background.interactive |
109 | 162 | } |
110 | 163 |
|
@@ -142,19 +195,32 @@ struct DatePicker: View { |
142 | 195 | validator.validate() |
143 | 196 | } |
144 | 197 |
|
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 |
149 | 201 | } |
150 | | - return nil |
| 202 | + |
| 203 | + return date.formatted( |
| 204 | + .dateTime |
| 205 | + .day() |
| 206 | + .month() |
| 207 | + .year() |
| 208 | + ) |
151 | 209 | } |
152 | 210 |
|
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 | + } |
157 | 216 |
|
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 | + } |
159 | 225 | } |
160 | 226 | } |
0 commit comments