@react-native-ama/core 2.0.0-beta.1 → 2.0.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ama.config.json +17 -0
- package/android/build.gradle +43 -0
- package/android/src/debug/AndroidManifest.xml +2 -0
- package/android/src/debug/java/expo/modules/ama/ReactNativeAmaModule.kt +544 -0
- package/android/src/debug/java/expo/modules/ama/ReactNativeAmaView.kt +30 -0
- package/android/src/debug/java/expo/modules/ama/highlight.kt +189 -0
- package/android/src/debug/java/expo/modules/ama/logger.kt +25 -0
- package/android/src/debug/java/expo/modules/ama/nodesGrabber.kt +664 -0
- package/expo-module.config.json +9 -0
- package/ios/Highlight.swift +264 -0
- package/ios/Logger.swift +31 -0
- package/ios/NodesGrabber.swift +733 -0
- package/ios/ReactNativeAma.podspec +29 -0
- package/ios/ReactNativeAmaModule.swift +568 -0
- package/ios/ReactNativeAmaView.swift +38 -0
- package/ios/TapGesture.swift +107 -0
- package/package.json +8 -2
|
@@ -0,0 +1,733 @@
|
|
|
1
|
+
import ExpoModulesCore
|
|
2
|
+
import UIKit
|
|
3
|
+
|
|
4
|
+
public struct NodePayload: Equatable {
|
|
5
|
+
let type: String
|
|
6
|
+
let viewId: Int
|
|
7
|
+
let bounds: [Double]?
|
|
8
|
+
let ariaLabel: String?
|
|
9
|
+
let content: String?
|
|
10
|
+
let ariaRole: String?
|
|
11
|
+
let traits: [String]?
|
|
12
|
+
let fg: String?
|
|
13
|
+
let bg: String?
|
|
14
|
+
let fontSize: CGFloat?
|
|
15
|
+
let isBold: Bool?
|
|
16
|
+
let isEnabled: Bool
|
|
17
|
+
let returnType: Int?
|
|
18
|
+
let isAccessible: Bool?
|
|
19
|
+
|
|
20
|
+
func toDictionary() -> [String: Any?] {
|
|
21
|
+
return [
|
|
22
|
+
"type": self.type,
|
|
23
|
+
"viewId": self.viewId,
|
|
24
|
+
"bounds": self.bounds,
|
|
25
|
+
"ariaLabel": self.ariaLabel,
|
|
26
|
+
"content": self.content,
|
|
27
|
+
"ariaRole": self.ariaRole,
|
|
28
|
+
"traits": self.traits,
|
|
29
|
+
"fg": self.fg,
|
|
30
|
+
"bg": self.bg,
|
|
31
|
+
"fontSize": self.fontSize,
|
|
32
|
+
"isBold": self.isBold,
|
|
33
|
+
"isEnabled": self.isEnabled,
|
|
34
|
+
"returnType": self.returnType,
|
|
35
|
+
"isAccessible": self.isAccessible
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
public class NodesGrabber {
|
|
41
|
+
private let appContext: AppContext
|
|
42
|
+
private var nodesToCheck: [Int: NodePayload] = [:]
|
|
43
|
+
|
|
44
|
+
public init(appContext: AppContext) {
|
|
45
|
+
self.appContext = appContext
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
public func getNodesToCheck(on rootView: UIView?) -> (
|
|
49
|
+
nodes: [Int: NodePayload], send: Bool
|
|
50
|
+
) {
|
|
51
|
+
guard let root = rootView else { return (nodesToCheck, false) }
|
|
52
|
+
|
|
53
|
+
let previous = nodesToCheck
|
|
54
|
+
|
|
55
|
+
nodesToCheck.removeAll()
|
|
56
|
+
|
|
57
|
+
traverseAndCheck(view: root)
|
|
58
|
+
|
|
59
|
+
let changed = previous != nodesToCheck
|
|
60
|
+
|
|
61
|
+
return (nodesToCheck, changed)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private func traverseAndCheck(view: UIView) {
|
|
65
|
+
checkView(view)
|
|
66
|
+
|
|
67
|
+
if view.isAccessibilityElement {
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
for subview in view.subviews {
|
|
72
|
+
traverseAndCheck(view: subview)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private func checkView(_ view: UIView) {
|
|
77
|
+
let supported = view.isPressable || view.isText() || view.isTextInput() || view.isModal() || view.isImage()
|
|
78
|
+
|
|
79
|
+
if !supported {
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
var font = view.contentFont
|
|
84
|
+
var tag = view.tag
|
|
85
|
+
var accessibilityLabel = view.accessibilityLabel
|
|
86
|
+
let info = view.extractRNTextInfo()
|
|
87
|
+
|
|
88
|
+
if info?.font != nil {
|
|
89
|
+
font = info?.font
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if view.isTextInput() {
|
|
93
|
+
tag = view.superview?.tag ?? 0
|
|
94
|
+
// Use the TextInput's own accessibilityLabel first, fall back to superview's label
|
|
95
|
+
accessibilityLabel = view.accessibilityLabel ?? view.superview?.accessibilityLabel
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
addNode(
|
|
99
|
+
node: NodePayload(
|
|
100
|
+
type: view.getType(),
|
|
101
|
+
viewId: tag,
|
|
102
|
+
bounds: view.getTargetArea(),
|
|
103
|
+
ariaLabel: accessibilityLabel,
|
|
104
|
+
content: info?.text ?? view.content,
|
|
105
|
+
ariaRole: getDefaultAriaRole(view),
|
|
106
|
+
traits: view.accessibilityTraits.names,
|
|
107
|
+
fg: info?.fg?.hexString ?? view.contentColor?.hexString,
|
|
108
|
+
bg: view.getBackground(info),
|
|
109
|
+
fontSize: font?.pointSize,
|
|
110
|
+
isBold: font?.fontDescriptor.symbolicTraits.contains(
|
|
111
|
+
.traitBold
|
|
112
|
+
),
|
|
113
|
+
isEnabled: !view.isDisabled(),
|
|
114
|
+
returnType: view.getReturnKeyType(),
|
|
115
|
+
isAccessible: (view.isText() || view.isImage()) ? !view.accessibilityElementsHidden : nil
|
|
116
|
+
)
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private func getDefaultAriaRole(_ view: UIView) -> String? {
|
|
121
|
+
let defaultRole: String? = {
|
|
122
|
+
switch view {
|
|
123
|
+
case is UIButton:
|
|
124
|
+
return "button"
|
|
125
|
+
case is UISwitch:
|
|
126
|
+
return "switch"
|
|
127
|
+
case is UITextField:
|
|
128
|
+
return "text field"
|
|
129
|
+
case let iv as UIImageView where iv.isUserInteractionEnabled:
|
|
130
|
+
return "image button"
|
|
131
|
+
default:
|
|
132
|
+
return nil
|
|
133
|
+
}
|
|
134
|
+
}()
|
|
135
|
+
|
|
136
|
+
return defaultRole
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private func addNode(node: NodePayload) {
|
|
140
|
+
guard node.viewId > 0 else { return }
|
|
141
|
+
|
|
142
|
+
nodesToCheck[node.viewId] = node
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private struct RNTextInfo {
|
|
147
|
+
let text: String?
|
|
148
|
+
let fg: UIColor?
|
|
149
|
+
let bg: UIColor?
|
|
150
|
+
let font: UIFont?
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private func previousSiblingsBackground(
|
|
154
|
+
in parent: UIView,
|
|
155
|
+
before view: UIView,
|
|
156
|
+
covering rect: CGRect
|
|
157
|
+
) -> UIColor? {
|
|
158
|
+
guard let idx = parent.subviews.firstIndex(of: view) else { return nil }
|
|
159
|
+
if idx == 0 { return nil }
|
|
160
|
+
|
|
161
|
+
for i in stride(from: idx - 1, through: 0, by: -1) {
|
|
162
|
+
let sib = parent.subviews[i]
|
|
163
|
+
if sib.isHidden || sib.alpha <= 0.01 { continue }
|
|
164
|
+
|
|
165
|
+
// intersect with our area?
|
|
166
|
+
let sibRect = sib.convert(sib.bounds, to: parent)
|
|
167
|
+
if !sibRect.intersects(rect) { continue }
|
|
168
|
+
|
|
169
|
+
// direct background on the sibling?
|
|
170
|
+
if let c = ownBackground(of: sib, resolveFor: view) {
|
|
171
|
+
return c
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return nil
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/// Non-transparent background for a single view (either UIView.backgroundColor or layer.backgroundColor)
|
|
178
|
+
private func ownBackground(
|
|
179
|
+
of v: UIView,
|
|
180
|
+
resolveFor resolver: UITraitEnvironment
|
|
181
|
+
) -> UIColor? {
|
|
182
|
+
if let c = v.backgroundColor, c.cgColor.alpha > 0.01 {
|
|
183
|
+
return c.resolvedColor(with: resolver.traitCollection)
|
|
184
|
+
}
|
|
185
|
+
if let cg = v.layer.backgroundColor, cg.alpha > 0.01 {
|
|
186
|
+
return UIColor(cgColor: cg).resolvedColor(
|
|
187
|
+
with: resolver.traitCollection
|
|
188
|
+
)
|
|
189
|
+
}
|
|
190
|
+
return nil
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
extension UIView {
|
|
194
|
+
struct A11yStates {
|
|
195
|
+
let isExpanded: Bool
|
|
196
|
+
let isDisabled: Bool
|
|
197
|
+
let isSelected: Bool
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
func getType() -> String {
|
|
201
|
+
if isTextInput() {
|
|
202
|
+
return "TextInput"
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if isPressable {
|
|
206
|
+
return "Pressable"
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if isText() {
|
|
210
|
+
return "Text"
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if isImage() {
|
|
214
|
+
return "Image"
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return ""
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
func getTargetArea() -> [Double]? {
|
|
221
|
+
if !isPressable && !isImage() {
|
|
222
|
+
return nil
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
let baseSize: CGRect = self.frame
|
|
226
|
+
let insets = isPressable ? self.getHitSlopRect() : .zero
|
|
227
|
+
let width = baseSize.width - insets.left - insets.right
|
|
228
|
+
let height = baseSize.height - insets.top - insets.bottom
|
|
229
|
+
|
|
230
|
+
return [width, height]
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
private func findColorIn(view: UIView) -> UIColor? {
|
|
234
|
+
if let imageView = view as? UIImageView,
|
|
235
|
+
let bgColor = imageView.backgroundColor
|
|
236
|
+
{
|
|
237
|
+
return bgColor
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if let bgColor = view.backgroundColor, bgColor.cgColor.alpha > 0 {
|
|
241
|
+
return bgColor
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
for subview in view.subviews {
|
|
245
|
+
if let color = findColorIn(view: subview) {
|
|
246
|
+
return color
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return nil
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
var content: String? {
|
|
254
|
+
let className = String(describing: type(of: self))
|
|
255
|
+
|
|
256
|
+
if className.contains("RCTParagraphComponentView") {
|
|
257
|
+
if let attrText = self.value(forKey: "attributedText")
|
|
258
|
+
as? NSAttributedString
|
|
259
|
+
{
|
|
260
|
+
return attrText.string
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return subviews.compactMap { $0.content }.first
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
var textBackgroundColor: UIColor? {
|
|
268
|
+
var current: UIView? = self
|
|
269
|
+
|
|
270
|
+
while let superview = current?.superview {
|
|
271
|
+
for subview in superview.subviews {
|
|
272
|
+
let className = String(describing: type(of: subview))
|
|
273
|
+
if className.contains("_UIBarBackground") {
|
|
274
|
+
if let color = findColorIn(view: subview) {
|
|
275
|
+
return color
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
current = superview
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if let parent = self.superview {
|
|
283
|
+
let rectInParent = self.convert(self.bounds, to: parent)
|
|
284
|
+
if let c = previousSiblingsBackground(
|
|
285
|
+
in: parent,
|
|
286
|
+
before: self,
|
|
287
|
+
covering: rectInParent
|
|
288
|
+
) {
|
|
289
|
+
return c
|
|
290
|
+
}
|
|
291
|
+
if let c = ownBackground(of: parent, resolveFor: self) { return c }
|
|
292
|
+
|
|
293
|
+
return parent.textBackgroundColor
|
|
294
|
+
}
|
|
295
|
+
return nil
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
var contentBackgroundColor: UIColor? {
|
|
299
|
+
var current: UIView? = self
|
|
300
|
+
|
|
301
|
+
while let superview = current?.superview {
|
|
302
|
+
for subview in superview.subviews {
|
|
303
|
+
let className = String(describing: type(of: subview))
|
|
304
|
+
if className.contains("_UIBarBackground") {
|
|
305
|
+
if let color = findColorIn(view: subview) {
|
|
306
|
+
return color
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
current = superview
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if let bg = self.backgroundColor { return bg }
|
|
314
|
+
|
|
315
|
+
var parent = self.superview
|
|
316
|
+
while let p = parent {
|
|
317
|
+
if let bg = p.backgroundColor, bg.cgColor.alpha > 0 {
|
|
318
|
+
return bg
|
|
319
|
+
}
|
|
320
|
+
parent = p.superview
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return nil
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
var contentColor: UIColor? {
|
|
327
|
+
let className = String(describing: type(of: self))
|
|
328
|
+
|
|
329
|
+
if className.contains("SVG") {
|
|
330
|
+
return nil
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if className.contains("RCTParagraphComponentView"),
|
|
334
|
+
let anyObj = self as AnyObject?,
|
|
335
|
+
let attrText = anyObj.value(forKey: "attributedText")
|
|
336
|
+
as? NSAttributedString,
|
|
337
|
+
attrText.length > 0,
|
|
338
|
+
let fgColor = attrText.attribute(
|
|
339
|
+
.foregroundColor,
|
|
340
|
+
at: 0,
|
|
341
|
+
effectiveRange: nil
|
|
342
|
+
)
|
|
343
|
+
as? UIColor
|
|
344
|
+
{
|
|
345
|
+
return fgColor
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if let label = self as? UILabel {
|
|
349
|
+
return label.textColor
|
|
350
|
+
}
|
|
351
|
+
if let button = self as? UIButton {
|
|
352
|
+
if let tc = button.titleColor(for: .normal) { return tc }
|
|
353
|
+
return button.tintColor
|
|
354
|
+
}
|
|
355
|
+
if let iv = self as? UIImageView {
|
|
356
|
+
return iv.tintColor
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
for sub in subviews {
|
|
360
|
+
if let cc = sub.contentColor {
|
|
361
|
+
return cc
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return self.tintColor
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
var contentFont: UIFont? {
|
|
369
|
+
if let label = self as? UILabel {
|
|
370
|
+
return label.font
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if let button = self as? UIButton,
|
|
374
|
+
let f = button.titleLabel?.font
|
|
375
|
+
{
|
|
376
|
+
return f
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
let className = String(describing: type(of: self))
|
|
380
|
+
|
|
381
|
+
if className.contains("RCTParagraphComponentView"),
|
|
382
|
+
let anyObj = self as AnyObject?,
|
|
383
|
+
let attrText = anyObj.value(forKey: "attributedText")
|
|
384
|
+
as? NSAttributedString,
|
|
385
|
+
attrText.length > 0,
|
|
386
|
+
let font = attrText.attribute(.font, at: 0, effectiveRange: nil)
|
|
387
|
+
as? UIFont
|
|
388
|
+
{
|
|
389
|
+
return font
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
for sub in subviews {
|
|
393
|
+
if let f = sub.contentFont {
|
|
394
|
+
return f
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return nil
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
fileprivate func getHitSlopRect() -> UIEdgeInsets {
|
|
402
|
+
let selector = NSSelectorFromString("hitTestEdgeInsets")
|
|
403
|
+
|
|
404
|
+
guard self.responds(to: selector) else {
|
|
405
|
+
return .zero
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
let anyObj = self as AnyObject
|
|
409
|
+
guard
|
|
410
|
+
let edgeValue = anyObj.value(forKey: "hitTestEdgeInsets")
|
|
411
|
+
as? UIEdgeInsets
|
|
412
|
+
else {
|
|
413
|
+
return .zero
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return edgeValue
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Returns true if the user can tap on the element
|
|
421
|
+
*/
|
|
422
|
+
var isPressable: Bool {
|
|
423
|
+
return isUserInteractionEnabled && isAccessibilityElement
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
func isDisabled() -> Bool {
|
|
427
|
+
if accessibilityTraits.contains(.notEnabled) { return true }
|
|
428
|
+
|
|
429
|
+
if let control = self as? UIControl, control.isEnabled == false {
|
|
430
|
+
return true
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if hasActivationRole
|
|
434
|
+
&& (isHidden || alpha <= 0.01 || isUserInteractionEnabled == false)
|
|
435
|
+
{
|
|
436
|
+
return true
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
return false
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
private var hasActivationRole: Bool {
|
|
443
|
+
let t = accessibilityTraits
|
|
444
|
+
if t.contains(.button) || t.contains(.link) { return true }
|
|
445
|
+
if responds(to: NSSelectorFromString("accessibilityActivate")) {
|
|
446
|
+
return true
|
|
447
|
+
}
|
|
448
|
+
if let actions = accessibilityCustomActions, !actions.isEmpty {
|
|
449
|
+
return true
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return false
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
func isText() -> Bool {
|
|
456
|
+
let names = [
|
|
457
|
+
"RCTParagraphComponentView",
|
|
458
|
+
"RCTParagraphTextView",
|
|
459
|
+
"RCTTextView",
|
|
460
|
+
"RCTText",
|
|
461
|
+
]
|
|
462
|
+
for name in names {
|
|
463
|
+
if let cls = NSClassFromString(name), self.isKind(of: cls) {
|
|
464
|
+
return true
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
return self.responds(to: NSSelectorFromString("attributedText"))
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
func isTextInput() -> Bool {
|
|
472
|
+
if self is UITextField || self is UITextView {
|
|
473
|
+
return true
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
return false
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
func isImage() -> Bool {
|
|
480
|
+
return self is UIImageView
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
fileprivate func extractRNTextInfo() -> RNTextInfo? {
|
|
484
|
+
if !isText() {
|
|
485
|
+
return nil
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
let sel = NSSelectorFromString("attributedText")
|
|
489
|
+
|
|
490
|
+
guard self.responds(to: sel),
|
|
491
|
+
let obj = self as AnyObject?,
|
|
492
|
+
let attr = obj.value(forKey: "attributedText")
|
|
493
|
+
as? NSAttributedString,
|
|
494
|
+
attr.length > 0
|
|
495
|
+
else { return nil }
|
|
496
|
+
|
|
497
|
+
let fg =
|
|
498
|
+
attr.attribute(.foregroundColor, at: 0, effectiveRange: nil)
|
|
499
|
+
as? UIColor
|
|
500
|
+
let bg =
|
|
501
|
+
(attr.attribute(.backgroundColor, at: 0, effectiveRange: nil)
|
|
502
|
+
as? UIColor)
|
|
503
|
+
?? (attr.attribute(
|
|
504
|
+
NSAttributedString.Key("NSBackgroundColor"),
|
|
505
|
+
at: 0,
|
|
506
|
+
effectiveRange: nil
|
|
507
|
+
) as? UIColor)
|
|
508
|
+
|
|
509
|
+
let font = attr.attribute(.font, at: 0, effectiveRange: nil) as? UIFont
|
|
510
|
+
|
|
511
|
+
return RNTextInfo(text: attr.string, fg: fg, bg: bg, font: font)
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
func isBusy() -> Bool {
|
|
515
|
+
if matchesBusyToken(accessibilityValue) { return true }
|
|
516
|
+
if matchesBusyToken(accessibilityLabel) { return true }
|
|
517
|
+
if matchesBusyToken(accessibilityHint) { return true }
|
|
518
|
+
|
|
519
|
+
if let spinner = self as? UIActivityIndicatorView,
|
|
520
|
+
!isHidden, alpha > 0.01, spinner.isAnimating
|
|
521
|
+
{
|
|
522
|
+
return true
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
return false
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
private func matchesBusyToken(_ s: String?) -> Bool {
|
|
529
|
+
guard let s = s, !s.isEmpty else { return false }
|
|
530
|
+
|
|
531
|
+
if let rnBusy = localizedStringIfExists(key: "state_busy_description") {
|
|
532
|
+
if s.range(
|
|
533
|
+
of: rnBusy,
|
|
534
|
+
options: [.caseInsensitive, .diacriticInsensitive]
|
|
535
|
+
) != nil {
|
|
536
|
+
return true
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
let fallbacks = ["busy", "loading", "in progress"]
|
|
541
|
+
return fallbacks.contains { token in
|
|
542
|
+
s.range(
|
|
543
|
+
of: token,
|
|
544
|
+
options: [.caseInsensitive, .diacriticInsensitive]
|
|
545
|
+
) != nil
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
private func localizedStringIfExists(key: String, table: String? = nil)
|
|
550
|
+
-> String?
|
|
551
|
+
{
|
|
552
|
+
// Check main bundle first (RN often merges strings here)
|
|
553
|
+
let main = NSLocalizedString(
|
|
554
|
+
key,
|
|
555
|
+
tableName: table,
|
|
556
|
+
bundle: .main,
|
|
557
|
+
value: key,
|
|
558
|
+
comment: ""
|
|
559
|
+
)
|
|
560
|
+
if main != key { return main }
|
|
561
|
+
|
|
562
|
+
for b in Bundle.allBundles where b != .main {
|
|
563
|
+
let v = NSLocalizedString(
|
|
564
|
+
key,
|
|
565
|
+
tableName: table,
|
|
566
|
+
bundle: b,
|
|
567
|
+
value: key,
|
|
568
|
+
comment: ""
|
|
569
|
+
)
|
|
570
|
+
if v != key { return v }
|
|
571
|
+
}
|
|
572
|
+
return nil
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
func a11yStates() -> A11yStates {
|
|
576
|
+
let disabledByTrait = accessibilityTraits.contains(.notEnabled)
|
|
577
|
+
let disabledByControl = (self as? UIControl)?.isEnabled == false
|
|
578
|
+
let interactionBlocked =
|
|
579
|
+
isHidden || alpha <= 0.01 || isUserInteractionEnabled == false
|
|
580
|
+
let isDisabled =
|
|
581
|
+
disabledByTrait || disabledByControl || interactionBlocked
|
|
582
|
+
|
|
583
|
+
let selectedByTrait = accessibilityTraits.contains(.selected)
|
|
584
|
+
let selectedByControl = (self as? UIControl)?.isSelected == true
|
|
585
|
+
let isSelected = selectedByTrait || selectedByControl
|
|
586
|
+
|
|
587
|
+
let haystacks = [
|
|
588
|
+
accessibilityValue?.lowercased(),
|
|
589
|
+
accessibilityLabel?.lowercased(),
|
|
590
|
+
accessibilityHint?.lowercased(),
|
|
591
|
+
].compactMap { $0 }
|
|
592
|
+
|
|
593
|
+
let expandedTokens = rnLocalizedTokens(
|
|
594
|
+
key: "state_expanded_description",
|
|
595
|
+
fallbacks: ["expanded"]
|
|
596
|
+
)
|
|
597
|
+
let collapsedTokens = rnLocalizedTokens(
|
|
598
|
+
key: "state_collapsed_description",
|
|
599
|
+
fallbacks: ["collapsed"]
|
|
600
|
+
)
|
|
601
|
+
|
|
602
|
+
let isExpanded: Bool? = {
|
|
603
|
+
if haystacks.contains(where: { s in
|
|
604
|
+
expandedTokens.contains(where: { s.contains($0) })
|
|
605
|
+
}) {
|
|
606
|
+
return true
|
|
607
|
+
}
|
|
608
|
+
if haystacks.contains(where: { s in
|
|
609
|
+
collapsedTokens.contains(where: { s.contains($0) })
|
|
610
|
+
}) {
|
|
611
|
+
return false
|
|
612
|
+
}
|
|
613
|
+
return nil // unknown
|
|
614
|
+
}()
|
|
615
|
+
|
|
616
|
+
return A11yStates(
|
|
617
|
+
isExpanded: isExpanded ?? false,
|
|
618
|
+
isDisabled: isDisabled,
|
|
619
|
+
isSelected: isSelected
|
|
620
|
+
)
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
private func rnLocalizedTokens(key: String, fallbacks: [String]) -> [String]
|
|
624
|
+
{
|
|
625
|
+
var out = [String]()
|
|
626
|
+
let vMain = NSLocalizedString(
|
|
627
|
+
key,
|
|
628
|
+
tableName: nil,
|
|
629
|
+
bundle: .main,
|
|
630
|
+
value: key,
|
|
631
|
+
comment: ""
|
|
632
|
+
)
|
|
633
|
+
if vMain != key { out.append(vMain.lowercased()) }
|
|
634
|
+
|
|
635
|
+
for b in Bundle.allBundles where b != .main {
|
|
636
|
+
let v = NSLocalizedString(
|
|
637
|
+
key,
|
|
638
|
+
tableName: nil,
|
|
639
|
+
bundle: b,
|
|
640
|
+
value: key,
|
|
641
|
+
comment: ""
|
|
642
|
+
)
|
|
643
|
+
if v != key { out.append(v.lowercased()) }
|
|
644
|
+
}
|
|
645
|
+
return (out + fallbacks.map { $0.lowercased() }).uniqued()
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
func getReturnKeyType() -> Int? {
|
|
649
|
+
if !isTextInput() {
|
|
650
|
+
return nil
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
if let field = self as? UITextField {
|
|
654
|
+
return field.returnKeyType.rawValue
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
if let view = self as? UITextView {
|
|
658
|
+
return view.returnKeyType.rawValue
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
return nil
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
fileprivate func getBackground(_ info: RNTextInfo?) -> String? {
|
|
665
|
+
if isText() {
|
|
666
|
+
return self.textBackgroundColor?.hexString ?? info?.bg?.hexString
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
return self.contentBackgroundColor?.hexString
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
extension Array where Element: Hashable {
|
|
674
|
+
fileprivate func uniqued() -> [Element] {
|
|
675
|
+
var seen = Set<Element>()
|
|
676
|
+
return filter { seen.insert($0).inserted }
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
extension UIAccessibilityTraits {
|
|
681
|
+
fileprivate var names: [String] {
|
|
682
|
+
let all: [(UIAccessibilityTraits, String)] = [
|
|
683
|
+
(.button, "button"),
|
|
684
|
+
(.link, "link"),
|
|
685
|
+
(.header, "header"),
|
|
686
|
+
(.searchField, "searchField"),
|
|
687
|
+
(.image, "image"),
|
|
688
|
+
(.selected, "selected"),
|
|
689
|
+
(.playsSound, "playsSound"),
|
|
690
|
+
(.keyboardKey, "keyboardKey"),
|
|
691
|
+
(.staticText, "staticText"),
|
|
692
|
+
(.summaryElement, "summaryElement"),
|
|
693
|
+
(.notEnabled, "notEnabled"),
|
|
694
|
+
(.updatesFrequently, "updatesFrequently"),
|
|
695
|
+
(.startsMediaSession, "startsMediaSession"),
|
|
696
|
+
(.adjustable, "adjustable"),
|
|
697
|
+
(.allowsDirectInteraction, "allowsDirectInteraction"),
|
|
698
|
+
(.causesPageTurn, "causesPageTurn"),
|
|
699
|
+
]
|
|
700
|
+
return all.compactMap { (trait, name) in
|
|
701
|
+
(self.rawValue & trait.rawValue) != 0 ? name : nil
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
extension UIColor {
|
|
707
|
+
var rgbaComponents: (r: CGFloat, g: CGFloat, b: CGFloat, a: CGFloat)? {
|
|
708
|
+
var r: CGFloat = 0
|
|
709
|
+
var g: CGFloat = 0
|
|
710
|
+
var b: CGFloat = 0
|
|
711
|
+
var a: CGFloat = 0
|
|
712
|
+
guard self.getRed(&r, green: &g, blue: &b, alpha: &a) else {
|
|
713
|
+
return nil
|
|
714
|
+
}
|
|
715
|
+
return (r, g, b, a)
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
var rgbaString: String {
|
|
719
|
+
guard let c = rgbaComponents else { return self.description }
|
|
720
|
+
let R = Int(c.r * 255)
|
|
721
|
+
let G = Int(c.g * 255)
|
|
722
|
+
let B = Int(c.b * 255)
|
|
723
|
+
return String(format: "rgba(%d,%d,%d,%.2f)", R, G, B, c.a)
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
var hexString: String {
|
|
727
|
+
guard let c = rgbaComponents else { return self.description }
|
|
728
|
+
let R = Int(c.r * 255)
|
|
729
|
+
let G = Int(c.g * 255)
|
|
730
|
+
let B = Int(c.b * 255)
|
|
731
|
+
return String(format: "#%02X%02X%02X", R, G, B)
|
|
732
|
+
}
|
|
733
|
+
}
|