-
Notifications
You must be signed in to change notification settings - Fork 9
Expand file tree
/
Copy pathOUDSCheckboxItem.swift
More file actions
401 lines (376 loc) · 19.6 KB
/
OUDSCheckboxItem.swift
File metadata and controls
401 lines (376 loc) · 19.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
//
// Software Name: OUDS iOS
// SPDX-FileCopyrightText: Copyright (c) Orange SA
// SPDX-License-Identifier: MIT
//
// This software is distributed under the MIT license,
// the text of which is available at https://opensource.org/license/MIT/
// or see the "LICENSE" file for more details.
//
// Authors: See CONTRIBUTORS.txt
// Software description: A SwiftUI components library with code examples for Orange Unified Design System
//
import OUDSFoundations
import SwiftUI
// MARK: - OUDS Checkbox Item
/// Checkbox is a UI element that allows to select multiple options from a set of mutually non exclusive choices.
/// Checkbox item covers a wider range of contexts by allowing to toggle the visibility of additional text labels and icon assets.
///
/// ## Layouts
///
/// The component can be rendered as two different layouts:
///
/// - **default**: the component has a leading indicator, a label and optional helper texts, and an optional trailing decorative icon
/// - **reversed**: like the *default* layout but with a trailing checkbox indicator and a leading optional image
///
/// ## Indicator states
///
/// The checkbox indicator has three available states:
/// - **selected**: the checkbox is filled with a tick, the user has made the action to select the checkbox
/// - **unselected**: the checkbox is empty, does not contain a tick, the user has made the action to unselect or did not select yet the checkbox
///
/// If you are looking for a checkbox component with three states, use instead ``OUDSCheckboxItemIndeterminate``.
///
/// ## Particular cases
///
/// An ``OUDSCheckboxItem`` can be related to an error situation, for example troubles for a formular.
/// A dedicated look and feel is implemented for that if the `isError` flag is risen.
/// In that case if the component displayed an icon, this icon will be replaced automatically by an error icon.
///
/// In addition, the ``OUDSCheckboxItem`` can be in read only mode, i.e. the user cannot interact with the component yet but this component must not be considered
/// as disabled.
///
/// The component does not follow the right-to-left (RTL) / left-to-right (LTR) mode returned by the system as it could have some meaning
/// to have for example the indicator in trailing position for LTR mode and vice versa.
/// However, if the component has an icon in leading position (RTL mode) or in trailing position (LTR), the content of the icon is never changed.
/// It could lead to a loss of meaning or semantics in the icon. Thus a specific flag can be used to flip the icon content whatever the layout direction is.
/// It prevents the user do implement its own rules to flip or not image.
///
/// ## Accessibility considerations
///
/// *Voice Over* will use several elements to describe the component: if component disabled / read only, if error context, the label and helper texts and a custom checkbox trait.
/// No accessibility identifier is defined in OUDS side as this value remains in the users hands.
///
/// ## Forbidden by design
///
/// **The design system does not allow to have both an error situation and a read only component.**
/// **The design system does not allow to have both an error situation and a disabled component.**
/// **The design system does not allow to have both a read only and a disabled component.**
///
/// ## Code samples
///
/// ```swift
/// // Supposing we have an unselected checkbox
/// @Published var isOn: Bool = false
///
/// // A leading checkbox with a label.
/// // The default layout will be used here.
/// OUDSCheckboxItem("Hello world", isOn: $isOn)
///
/// // Localizable from bundle can also be used
/// OUDSCheckboxItem(LocalizedStringKey("agree_terms"), bundle: Bundle.module, isOn: $isOn)
///
/// // A leading checkbox with a label, but in read only mode (user cannot interact yet, but not disabled).
/// // The default layout will be used here.
/// OUDSCheckboxItem("Hello world", isOn: $isOn, isReadOnly: true)
///
/// // A leading checkbox with a label and a description as helper text.
/// // The default layout will be used here.
/// OUDSCheckboxItem("Bazinga!", isOn: $isOn, description: "Doll-Dagga Buzz-Buzz Ziggety-Zag")
///
/// // A trailing checkbox with a label, a description and an icon.
/// // The reversed layout will be used here.
/// OUDSCheckboxItem("We live in a fabled world",
/// isOn: $isOn,
/// description: "Of dreaming boys and wide-eyed girls",
/// isReversed: true,
/// icon: Image(decorative: "ic_heart"))
///
/// // If on error, add an error message can help user to understand error context
/// OUDSCheckboxItem("We live in a fabled world",
/// isOn: $isOn,
/// isError: true,
/// errorText: "Something wrong",
/// hasDivider: true)
///
/// // A leading checkbox with a label, but disabled.
/// // The default layout will be used here.
/// OUDSCheckboxItem("Hello world", isOn: $isOn)
/// .disabled(true)
///
/// // Never disable a read only or an error-related checkbox as it will crash
/// // This is forbidden by design!
/// OUDSCheckboxItem("Hello world", isOn: $isOn, isError: true).disabled(true) // fatal error
/// OUDSCheckboxItem("Hello world", isOn: $isOn, isReadOnly: true).disabled(true) // fatal error
/// ```
///
/// If you need to flip your icon depending to the layout direction or not (e.g. if RTL mode lose semantics / meanings):
/// ```swift
/// @Environment(\.layoutDirection) var layoutDirection
///
/// OUDSCheckboxItem("Cocorico !",
/// isOn: $selection,
/// icon: Image(systemName: "figure.handball"),
/// flipIcon: layoutDirection == .rightToLeft,
/// isInversed: layoutDirection == .rightToLeft)
/// ```
///
/// ## Suggestions
///
/// According to the [documentation](https://r.orange.fr/r/S-ouds-doc-checkbox),
/// the checkbox by default must be used in unselected state.
///
/// ## Design documentation
///
/// [unified-design-system.orange.com](https://r.orange.fr/r/S-ouds-doc-checkbox)
///
/// ## Themes rendering
///
/// ### Orange
///
/// 
///
/// ### Orange Compact
///
/// 
///
/// ### Sosh
///
/// 
///
/// ### Wireframe
///
/// 
///
/// - Version: 2.4.0 (Figma component design version)
/// - Since: 0.12.0
@available(iOS 15, macOS 13, visionOS 1, watchOS 11, tvOS 16, *)
public struct OUDSCheckboxItem: View {
// MARK: - Properties
// NOTE: Do not forget to keep updated OUDSCheckboxPickerData
@Binding private var isOn: Bool
private let layoutData: ControlItemLabel.LayoutData
private let action: (() -> Void)?
@Environment(\.isEnabled) private var isEnabled
// MARK: - Initializers
/// Creates a checkbox with label and optional helper text, icon, divider.
///
/// ```swift
/// OUDSCheckboxItem(isOn: $isOn,
/// label: "Virgin Holy Lava",
/// description: "Very spicy",
/// icon: Image(systemName: "flame")
/// ```
///
/// **The design system does not allow to have both an error situation and a read only mode for the component.**
///
/// **Remark: If `label` and `description` strings are wording keys from strings catalog stored in `Bundle.main`, they are automatically localized. Else, prefer to
/// provide the localized string if key is stored in another bundle.**
///
/// - Parameters:
/// - isOn: A binding to a property that determines whether the indicator is ticked (selected) or not (unselected)
/// - label: The main label text of the checkbox, must not be empty
/// - description: An additional helper text, a description, which should not be empty, default set to `nil`. Will be replaced by `errorText` in case of error.
/// - icon: An optional icon, default set to `nil`
/// - flipIcon: Default set to `false`, set to `true` to reverse the image (i.e. flip vertically)
/// - isReversed: `true` if the checkbox indicator must be in trailing position, `false` otherwise. Default to `false`
/// - isError: `true` if the look and feel of the component must reflect an error state, default set to `false`
/// - errorText: An optional error message to display at the bottom. This message is ignored if `isError` is `false`.
/// The `errorText`can be different if switch is selected or not.
/// - isReadOnly: True if component is in read only, i.e. not really disabled but user cannot interact with it yet, default set to `false`
/// - hasDivider: If `true` a divider is added at the bottom of the view, by default set to `false`
/// - constrainedMaxWidth: When `true`, the item width is constrained to a maximum value defined by the design system.
/// When `false`, no specific width constraint is applied, allowing the component to size itself or follow external
/// modifier. Defaults to `false`.
/// - action: An additional action to trigger when the checkbox has been pressed
@available(*, deprecated, message: "Use instead OUDSCheckboxItem(:isOn:)")
public init(isOn: Binding<Bool>,
label: String,
description: String? = nil,
icon: Image? = nil,
flipIcon: Bool = false,
isReversed: Bool = false,
isError: Bool = false,
errorText: String? = nil,
isReadOnly: Bool = false,
hasDivider: Bool = false,
constrainedMaxWidth: Bool = false,
action: (() -> Void)? = nil)
{
self.init(label,
isOn: isOn,
description: description,
icon: icon,
flipIcon: flipIcon,
isReversed: isReversed,
isError: isError,
errorText: errorText,
isReadOnly: isReadOnly,
hasDivider: hasDivider,
constrainedMaxWidth: constrainedMaxWidth,
action: action)
}
/// Creates a checkbox with label and optional helper text, icon, divider.
///
/// ```swift
/// OUDSCheckboxItem("Virgin Holy Lava",
/// isOn: $isOn,
/// description: "Very spicy",
/// icon: Image(systemName: "flame")
/// ```
///
/// **The design system does not allow to have both an error situation and a read only mode for the component.**
///
/// **Remark: If `label` and `description` strings are wording keys from strings catalog stored in `Bundle.main`, they are automatically localized. Else, prefer to
/// provide the localized string if key is stored in another bundle.**
///
/// - Parameters:
/// - label: The main label text of the checkbox, must not be empty
/// - isOn: A binding to a property that determines whether the indicator is ticked (selected) or not (unselected)
/// - description: An additional helper text, a description, which should not be empty, default set to `nil`. Will be replaced by `errorText` in case of error.
/// - icon: An optional icon, default set to `nil`
/// - flipIcon: Default set to `false`, set to `true` to reverse the image (i.e. flip vertically)
/// - isReversed: `true` if the checkbox indicator must be in trailing position, `false` otherwise. Default to `false`
/// - isError: `true` if the look and feel of the component must reflect an error state, default set to `false`
/// - errorText: An optional error message to display at the bottom. This message is ignored if `isError` is `false`.
/// The `errorText`can be different if switch is selected or not.
/// - isReadOnly: True if component is in read only, i.e. not really disabled but user cannot interact with it yet, default set to `false`
/// - hasDivider: If `true` a divider is added at the bottom of the view, by default set to `false`
/// - constrainedMaxWidth: When `true`, the item width is constrained to a maximum value defined by the design system.
/// When `false`, no specific width constraint is applied, allowing the component to size itself or follow external
/// modifier. Defaults to `false`.
/// - action: An additional action to trigger when the checkbox has been pressed
public init(_ label: String,
isOn: Binding<Bool>,
description: String? = nil,
icon: Image? = nil,
flipIcon: Bool = false,
isReversed: Bool = false,
isError: Bool = false,
errorText: String? = nil,
isReadOnly: Bool = false,
hasDivider: Bool = false,
constrainedMaxWidth: Bool = false,
action: (() -> Void)? = nil)
{
if isError, isReadOnly {
OL.fatal("It is forbidden by design to have an OUDSCheckboxItem in an error context and in read only mode")
}
if label.isEmpty {
OL.warning("Label given to an OUDSCheckboxItem is empty, prefer OUDSCheckbox(isOn:accessibilityLabel:) instead")
}
if let description, description.isEmpty {
OL.warning("Description given to an OUDSCheckboxItem is defined but empty, is it expected? Prefer use of `nil` value instead")
}
// swiftlint:disable force_unwrapping
if isError, errorText == nil || errorText!.isEmpty {
OL.warning("Error text given to an OUDSCheckboxItem must be defined in case of error")
}
// swiftlint:enable force_unwrapping
_isOn = isOn
layoutData = .init(
label: label.localized(),
extraLabel: nil,
description: description?.localized(),
icon: icon,
flipIcon: flipIcon,
isOutlined: false,
isError: isError,
errorText: errorText,
isReadOnly: isReadOnly,
hasDivider: hasDivider,
constrainedMaxWidth: constrainedMaxWidth,
orientation: isReversed ? .reversed : .default)
self.action = action
}
// swiftlint:disable function_default_parameter_at_end
/// Creates a checkbox with a localized label, looking up the key in the given bundle.
///
/// ```swift
/// OUDSCheckboxItem(LocalizedStringKey("agree_terms"), bundle: Bundle.module, isOn: $isOn)
/// ```
///
/// **The design system does not allow to have both an error situation and a read only mode for the component.**
///
/// - Parameters:
/// - key: A `LocalizedStringKey` used to look up the label in the given bundle
/// - tableName: The name of the `.strings` file, or `nil` for the default
/// - bundle: The bundle in which to look up the localized string. Defaults to `Bundle.main`.
/// - isOn: A binding to a property that determines whether the indicator is ticked (selected) or not (unselected)
/// - description: An additional helper text, a description, which should not be empty, default set to `nil`
/// - icon: An optional icon, default set to `nil`
/// - flipIcon: Default set to `false`, set to `true` to reverse the image (i.e. flip vertically)
/// - isReversed: `true` if the checkbox indicator must be in trailing position, `false` otherwise. Default to `false`
/// - isError: `true` if the look and feel of the component must reflect an error state, default set to `false`
/// - errorText: An optional error message to display at the bottom. This message is ignored if `isError` is `false`.
/// - isReadOnly: True if component is in read only, default set to `false`
/// - hasDivider: If `true` a divider is added at the bottom of the view, by default set to `false`
/// - constrainedMaxWidth: When `true`, the item width is constrained to a maximum value defined by the design system.
/// - action: An additional action to trigger when the checkbox has been pressed
public init(_ key: LocalizedStringKey,
tableName: String? = nil,
bundle: Bundle = .main,
isOn: Binding<Bool>,
description: String? = nil,
icon: Image? = nil,
flipIcon: Bool = false,
isReversed: Bool = false,
isError: Bool = false,
errorText: String? = nil,
isReadOnly: Bool = false,
hasDivider: Bool = false,
constrainedMaxWidth: Bool = false,
action: (() -> Void)? = nil)
{
self.init(key.resolved(tableName: tableName, bundle: bundle),
isOn: isOn,
description: description,
icon: icon,
flipIcon: flipIcon,
isReversed: isReversed,
isError: isError,
errorText: errorText,
isReadOnly: isReadOnly,
hasDivider: hasDivider,
constrainedMaxWidth: constrainedMaxWidth,
action: action)
}
// swiftlint:enable function_default_parameter_at_end
// MARK: Body
public var body: some View {
ControlItem(indicatorType: .checkBox(convertedState), layoutData: layoutData, action: action)
.accessibilityRemoveTraits([.isButton]) // .isToggle trait for iOS 17+
.accessibilityLabel(accessibilityLabel)
.accessibilityValue(accessibilityValue)
.accessibilityHint(accessibilityHint)
}
// MARK: - Computed value
private var convertedState: Binding<OUDSCheckboxIndicatorState> {
Binding(get: { isOn ? .selected : .unselected }, set: { isOn = ($0 == .selected ? true : false) })
}
// MARK: - A11Y helpers
/// Forge a string to vocalize the component label based on label, extraLabel and description
private var accessibilityLabel: String {
let extraLabel = layoutData.extraLabel?.isEmpty != false ? "" : ", \(layoutData.extraLabel ?? "")"
let description = layoutData.description?.isEmpty != false ? "" : ", \(layoutData.description ?? "")"
return "\(layoutData.label)\(extraLabel)\(description)"
}
/// Forges a string to vocalize with *Voice Over* describing the component trait, value, state and error
private var accessibilityValue: String {
let traitDescription = "core_checkbox_trait_a11y".localized() // Fake trait for Voice Over vocalization
let valueDescription = isOn ? "core_checkbox_checked_a11y".localized() : "core_checkbox_unchecked_a11y".localized()
let stateDescription = !isEnabled || layoutData.isReadOnly ? "core_common_disabled_a11y".localized() : ""
let errorPrefix = "core_common_onError_a11y".localized()
let errorText = layoutData.errorText?.localized() ?? ""
let errorDescription = layoutData.isError ? "\(errorPrefix), \(errorText)" : ""
return "\(traitDescription). \(valueDescription). \(stateDescription). \(errorDescription)"
}
/// Forges a string to vocalize with *Voice Over* describing the component hint
private var accessibilityHint: String {
if layoutData.isReadOnly || !isEnabled {
""
} else {
convertedState.wrappedValue.a11yHint
}
}
}