react-native-tinyui 0.5.0 → 1.0.0
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/README.md +351 -22
- package/ios/ConcentricView/TinyuiConcentricView.h +12 -0
- package/ios/ConcentricView/TinyuiConcentricView.mm +60 -0
- package/ios/Core/Tinyui.mm +54 -0
- package/ios/Core/TinyuiComponentClasses.mm +44 -0
- package/ios/Menu/TinyuiMenuBuilder.swift +238 -0
- package/ios/Menu/TinyuiMenuProvider.swift +51 -85
- package/ios/Menu/TinyuiMenuView.mm +12 -35
- package/ios/Popover/TinyuiPopoverProvider.swift +198 -44
- package/ios/Popover/TinyuiPopoverView.mm +26 -1
- package/ios/SFSymbol/TinyuiSFSymbolEffects.h +9 -0
- package/ios/SFSymbol/TinyuiSFSymbolEffects.mm +180 -0
- package/ios/SFSymbol/TinyuiSFSymbolImage.h +8 -0
- package/ios/SFSymbol/TinyuiSFSymbolImage.mm +141 -0
- package/ios/SFSymbol/TinyuiSFSymbolShadowNode.h +31 -0
- package/ios/SFSymbol/TinyuiSFSymbolShadowNode.mm +30 -0
- package/ios/SFSymbol/TinyuiSFSymbolView.h +4 -0
- package/ios/SFSymbol/TinyuiSFSymbolView.mm +173 -0
- package/ios/Stepper/TinyuiStepperShadowNode.h +32 -0
- package/ios/Stepper/TinyuiStepperShadowNode.mm +28 -0
- package/ios/Stepper/TinyuiStepperView.h +12 -0
- package/ios/Stepper/TinyuiStepperView.mm +128 -0
- package/ios/TipKit/TinyuiTip.swift +31 -0
- package/ios/TipKit/TinyuiTipPopoverProvider.swift +276 -0
- package/ios/TipKit/TinyuiTipPopoverView.h +8 -0
- package/ios/TipKit/TinyuiTipPopoverView.mm +114 -0
- package/ios/TipKit/TinyuiTipsRuntime.swift +148 -0
- package/lib/module/ConcentricView/ConcentricViewNativeComponent.ts +14 -0
- package/lib/module/ConcentricView/index.js +29 -0
- package/lib/module/ConcentricView/index.js.map +1 -0
- package/lib/module/ConcentricView/types.js +4 -0
- package/lib/module/ConcentricView/types.js.map +1 -0
- package/lib/module/Menu/MenuNativeComponent.ts +1 -3
- package/lib/module/Menu/index.js +4 -9
- package/lib/module/Menu/index.js.map +1 -1
- package/lib/module/Menu/options.js +4 -12
- package/lib/module/Menu/options.js.map +1 -1
- package/lib/module/NativeTinyui.js.map +1 -1
- package/lib/module/Popover/index.js +4 -2
- package/lib/module/Popover/index.js.map +1 -1
- package/lib/module/SFSymbol/SFSymbolNativeComponent.ts +20 -0
- package/lib/module/SFSymbol/index.js +73 -0
- package/lib/module/SFSymbol/index.js.map +1 -0
- package/lib/module/SFSymbol/options.js +79 -0
- package/lib/module/SFSymbol/options.js.map +1 -0
- package/lib/module/SFSymbol/types.js +4 -0
- package/lib/module/SFSymbol/types.js.map +1 -0
- package/lib/module/Stepper/StepperNativeComponent.ts +29 -0
- package/lib/module/Stepper/index.js +75 -0
- package/lib/module/Stepper/index.js.map +1 -0
- package/lib/module/Stepper/types.js +4 -0
- package/lib/module/Stepper/types.js.map +1 -0
- package/lib/module/TipKit/Popover.js +89 -0
- package/lib/module/TipKit/Popover.js.map +1 -0
- package/lib/module/TipKit/TipPopoverNativeComponent.ts +37 -0
- package/lib/module/TipKit/index.js +28 -0
- package/lib/module/TipKit/index.js.map +1 -0
- package/lib/module/TipKit/types.js +4 -0
- package/lib/module/TipKit/types.js.map +1 -0
- package/lib/module/TipKit/validation.js +24 -0
- package/lib/module/TipKit/validation.js.map +1 -0
- package/lib/module/index.js +4 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/utils/getNativeTinyui.js.map +1 -1
- package/lib/typescript/src/ConcentricView/ConcentricViewNativeComponent.d.ts +7 -0
- package/lib/typescript/src/ConcentricView/ConcentricViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/ConcentricView/index.d.ts +4 -0
- package/lib/typescript/src/ConcentricView/index.d.ts.map +1 -0
- package/lib/typescript/src/ConcentricView/types.d.ts +6 -0
- package/lib/typescript/src/ConcentricView/types.d.ts.map +1 -0
- package/lib/typescript/src/Menu/MenuNativeComponent.d.ts +2 -3
- package/lib/typescript/src/Menu/MenuNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/Menu/index.d.ts +1 -1
- package/lib/typescript/src/Menu/index.d.ts.map +1 -1
- package/lib/typescript/src/Menu/options.d.ts +1 -5
- package/lib/typescript/src/Menu/options.d.ts.map +1 -1
- package/lib/typescript/src/Menu/types.d.ts +16 -4
- package/lib/typescript/src/Menu/types.d.ts.map +1 -1
- package/lib/typescript/src/NativeTinyui.d.ts +2 -0
- package/lib/typescript/src/NativeTinyui.d.ts.map +1 -1
- package/lib/typescript/src/Popover/index.d.ts.map +1 -1
- package/lib/typescript/src/Popover/types.d.ts +2 -0
- package/lib/typescript/src/Popover/types.d.ts.map +1 -1
- package/lib/typescript/src/SFSymbol/SFSymbolNativeComponent.d.ts +11 -0
- package/lib/typescript/src/SFSymbol/SFSymbolNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/SFSymbol/index.d.ts +4 -0
- package/lib/typescript/src/SFSymbol/index.d.ts.map +1 -0
- package/lib/typescript/src/SFSymbol/options.d.ts +86 -0
- package/lib/typescript/src/SFSymbol/options.d.ts.map +1 -0
- package/lib/typescript/src/SFSymbol/types.d.ts +113 -0
- package/lib/typescript/src/SFSymbol/types.d.ts.map +1 -0
- package/lib/typescript/src/Stepper/StepperNativeComponent.d.ts +21 -0
- package/lib/typescript/src/Stepper/StepperNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/Stepper/index.d.ts +4 -0
- package/lib/typescript/src/Stepper/index.d.ts.map +1 -0
- package/lib/typescript/src/Stepper/types.d.ts +24 -0
- package/lib/typescript/src/Stepper/types.d.ts.map +1 -0
- package/lib/typescript/src/TipKit/Popover.d.ts +3 -0
- package/lib/typescript/src/TipKit/Popover.d.ts.map +1 -0
- package/lib/typescript/src/TipKit/TipPopoverNativeComponent.d.ts +38 -0
- package/lib/typescript/src/TipKit/TipPopoverNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/TipKit/index.d.ts +10 -0
- package/lib/typescript/src/TipKit/index.d.ts.map +1 -0
- package/lib/typescript/src/TipKit/types.d.ts +46 -0
- package/lib/typescript/src/TipKit/types.d.ts.map +1 -0
- package/lib/typescript/src/TipKit/validation.d.ts +4 -0
- package/lib/typescript/src/TipKit/validation.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +9 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/utils/getNativeTinyui.d.ts +1 -1
- package/lib/typescript/src/utils/getNativeTinyui.d.ts.map +1 -1
- package/package.json +37 -2
- package/{Tinyui.podspec → react-native-tinyui.podspec} +23 -2
- package/src/ConcentricView/ConcentricViewNativeComponent.ts +14 -0
- package/src/ConcentricView/index.tsx +34 -0
- package/src/ConcentricView/types.ts +6 -0
- package/src/Menu/MenuNativeComponent.ts +1 -3
- package/src/Menu/index.tsx +6 -6
- package/src/Menu/options.ts +2 -20
- package/src/Menu/types.ts +18 -4
- package/src/NativeTinyui.ts +2 -0
- package/src/Popover/index.tsx +8 -3
- package/src/Popover/types.ts +2 -0
- package/src/SFSymbol/SFSymbolNativeComponent.ts +20 -0
- package/src/SFSymbol/index.tsx +89 -0
- package/src/SFSymbol/options.ts +116 -0
- package/src/SFSymbol/types.ts +158 -0
- package/src/Stepper/StepperNativeComponent.ts +29 -0
- package/src/Stepper/index.tsx +91 -0
- package/src/Stepper/types.ts +24 -0
- package/src/TipKit/Popover.tsx +87 -0
- package/src/TipKit/TipPopoverNativeComponent.ts +37 -0
- package/src/TipKit/index.ts +47 -0
- package/src/TipKit/types.ts +57 -0
- package/src/TipKit/validation.ts +39 -0
- package/src/index.tsx +30 -0
- package/src/utils/getNativeTinyui.ts +8 -1
- package/ios/Core/TinyuiHostingHelpers.swift +0 -74
- package/ios/Menu/TinyuiMenuViewSwift.swift +0 -326
- package/ios/Popover/TinyuiPopoverViewSwift.swift +0 -88
|
@@ -1,111 +1,265 @@
|
|
|
1
|
-
import SwiftUI
|
|
2
1
|
import UIKit
|
|
3
2
|
|
|
3
|
+
@objc public protocol TinyuiPopoverViewDelegate: AnyObject {
|
|
4
|
+
func onIsPresentedChange(isPresented: Bool)
|
|
5
|
+
}
|
|
6
|
+
|
|
4
7
|
/**
|
|
5
|
-
* UIKit
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
+
* UIKit implementation used by the Fabric `TinyuiPopoverView` component.
|
|
9
|
+
* Presents the React Native content view with `UIPopoverPresentationController`
|
|
10
|
+
* and keeps the trigger view in the normal React Native view hierarchy.
|
|
8
11
|
*/
|
|
9
|
-
@objc public class TinyuiPopoverProvider: UIView
|
|
12
|
+
@objc public final class TinyuiPopoverProvider: UIView,
|
|
13
|
+
UIPopoverPresentationControllerDelegate
|
|
14
|
+
{
|
|
10
15
|
private weak var delegate: TinyuiPopoverViewDelegate?
|
|
11
|
-
private var
|
|
12
|
-
private var
|
|
16
|
+
private var presentedViewController: UIViewController?
|
|
17
|
+
private var isDismissing = false
|
|
18
|
+
private var isPresentationUpdateScheduled = false
|
|
13
19
|
|
|
14
20
|
/// Presenting view controller, resolved by the component view from its
|
|
15
21
|
/// React view controller hierarchy.
|
|
16
22
|
@objc public weak var parentViewController: UIViewController? {
|
|
17
23
|
didSet {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
24
|
+
guard oldValue !== parentViewController else { return }
|
|
25
|
+
schedulePresentationUpdate()
|
|
21
26
|
}
|
|
22
27
|
}
|
|
23
28
|
|
|
24
29
|
@objc public var triggerView: UIView? {
|
|
25
30
|
didSet {
|
|
26
|
-
|
|
31
|
+
guard oldValue !== triggerView else { return }
|
|
32
|
+
schedulePresentationUpdate()
|
|
27
33
|
}
|
|
28
34
|
}
|
|
29
35
|
|
|
30
36
|
@objc public var contentView: UIView? {
|
|
31
37
|
didSet {
|
|
32
|
-
|
|
38
|
+
guard oldValue !== contentView else { return }
|
|
39
|
+
oldValue?.removeFromSuperview()
|
|
40
|
+
contentSize = .zero
|
|
41
|
+
schedulePresentationUpdate()
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/// The content wrapper's Yoga size, synchronized after each Fabric commit.
|
|
46
|
+
/// Keep it separate from the presentation controller's UIKit layout.
|
|
47
|
+
@objc public var contentSize: CGSize = .zero {
|
|
48
|
+
didSet {
|
|
49
|
+
guard oldValue != contentSize else { return }
|
|
50
|
+
schedulePresentationUpdate()
|
|
33
51
|
}
|
|
34
52
|
}
|
|
35
53
|
|
|
36
54
|
@objc public var isPresented: Bool = false {
|
|
37
55
|
didSet {
|
|
38
|
-
|
|
56
|
+
guard oldValue != isPresented else { return }
|
|
57
|
+
schedulePresentationUpdate()
|
|
39
58
|
}
|
|
40
59
|
}
|
|
41
60
|
|
|
42
61
|
@objc public var attachmentAnchor: String = "center" {
|
|
43
62
|
didSet {
|
|
44
|
-
|
|
63
|
+
guard oldValue != attachmentAnchor else { return }
|
|
64
|
+
schedulePresentationUpdate()
|
|
45
65
|
}
|
|
46
66
|
}
|
|
47
67
|
|
|
48
68
|
@objc public var arrowEdge: String = "none" {
|
|
49
69
|
didSet {
|
|
50
|
-
|
|
70
|
+
guard oldValue != arrowEdge else { return }
|
|
71
|
+
schedulePresentationUpdate()
|
|
51
72
|
}
|
|
52
73
|
}
|
|
53
74
|
|
|
54
75
|
@objc public convenience init(delegate: TinyuiPopoverViewDelegate) {
|
|
55
|
-
self.init()
|
|
76
|
+
self.init(frame: .zero)
|
|
56
77
|
self.delegate = delegate
|
|
57
78
|
}
|
|
58
79
|
|
|
59
80
|
override public func didMoveToWindow() {
|
|
60
81
|
super.didMoveToWindow()
|
|
61
|
-
|
|
62
|
-
setupView()
|
|
63
|
-
}
|
|
82
|
+
schedulePresentationUpdate()
|
|
64
83
|
}
|
|
65
84
|
|
|
66
85
|
override public func layoutSubviews() {
|
|
67
86
|
super.layoutSubviews()
|
|
68
|
-
|
|
69
|
-
|
|
87
|
+
updatePresentedPopoverConfiguration()
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private func schedulePresentationUpdate() {
|
|
91
|
+
guard !isPresentationUpdateScheduled else { return }
|
|
92
|
+
isPresentationUpdateScheduled = true
|
|
93
|
+
|
|
94
|
+
DispatchQueue.main.async { [weak self] in
|
|
95
|
+
guard let self else { return }
|
|
96
|
+
self.isPresentationUpdateScheduled = false
|
|
97
|
+
self.updatePresentation()
|
|
70
98
|
}
|
|
71
99
|
}
|
|
72
100
|
|
|
73
|
-
private func
|
|
74
|
-
|
|
75
|
-
|
|
101
|
+
private func updatePresentation() {
|
|
102
|
+
guard isPresented, window != nil else {
|
|
103
|
+
dismissPopoverIfNeeded()
|
|
76
104
|
return
|
|
77
105
|
}
|
|
78
106
|
|
|
79
|
-
guard
|
|
107
|
+
guard
|
|
108
|
+
presentedViewController == nil,
|
|
109
|
+
let parentViewController,
|
|
110
|
+
let triggerView,
|
|
111
|
+
triggerView.window != nil,
|
|
112
|
+
contentView != nil,
|
|
113
|
+
let preferredContentSize
|
|
114
|
+
else {
|
|
115
|
+
updatePresentedPopoverConfiguration()
|
|
80
116
|
return
|
|
81
117
|
}
|
|
82
118
|
|
|
83
|
-
let
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
119
|
+
let contentViewController = UIViewController()
|
|
120
|
+
// The system popover supplies its own material (Liquid Glass on iOS 26+).
|
|
121
|
+
// An opaque content background would cover it, including its adaptive tint.
|
|
122
|
+
contentViewController.view.backgroundColor = .clear
|
|
123
|
+
contentViewController.view.clipsToBounds = true
|
|
124
|
+
contentViewController.preferredContentSize = preferredContentSize
|
|
125
|
+
contentViewController.modalPresentationStyle = .popover
|
|
126
|
+
attachContent(to: contentViewController)
|
|
87
127
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
128
|
+
guard let popoverPresentationController = contentViewController.popoverPresentationController
|
|
129
|
+
else {
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
configure(popoverPresentationController, from: triggerView)
|
|
134
|
+
popoverPresentationController.delegate = self
|
|
135
|
+
presentedViewController = contentViewController
|
|
136
|
+
|
|
137
|
+
parentViewController.present(contentViewController, animated: true) { [weak self] in
|
|
138
|
+
guard let self else { return }
|
|
139
|
+
if !self.isPresented || self.window == nil {
|
|
140
|
+
self.dismissPopoverIfNeeded()
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private func dismissPopoverIfNeeded() {
|
|
146
|
+
guard let presentedViewController, !isDismissing else { return }
|
|
147
|
+
|
|
148
|
+
isDismissing = true
|
|
149
|
+
presentedViewController.dismiss(animated: true) { [weak self] in
|
|
150
|
+
guard let self else { return }
|
|
151
|
+
self.presentedViewController = nil
|
|
152
|
+
self.isDismissing = false
|
|
153
|
+
self.delegate?.onIsPresentedChange(isPresented: false)
|
|
154
|
+
if self.isPresented, self.window != nil {
|
|
155
|
+
self.schedulePresentationUpdate()
|
|
156
|
+
}
|
|
157
|
+
}
|
|
94
158
|
}
|
|
95
159
|
|
|
96
|
-
|
|
97
|
-
private func syncParentViewController(to parentViewController: UIViewController? = nil) {
|
|
160
|
+
private func updatePresentedPopoverConfiguration() {
|
|
98
161
|
guard
|
|
99
|
-
let
|
|
100
|
-
let
|
|
101
|
-
|
|
162
|
+
let presentedViewController,
|
|
163
|
+
let triggerView,
|
|
164
|
+
let preferredContentSize,
|
|
165
|
+
let popoverPresentationController = presentedViewController.popoverPresentationController
|
|
102
166
|
else {
|
|
103
167
|
return
|
|
104
168
|
}
|
|
105
169
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
170
|
+
attachContent(to: presentedViewController)
|
|
171
|
+
if presentedViewController.preferredContentSize != preferredContentSize {
|
|
172
|
+
presentedViewController.preferredContentSize = preferredContentSize
|
|
173
|
+
}
|
|
174
|
+
configure(popoverPresentationController, from: triggerView)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private func attachContent(to viewController: UIViewController) {
|
|
178
|
+
guard let contentView, contentView.superview !== viewController.view else { return }
|
|
179
|
+
contentView.removeFromSuperview()
|
|
180
|
+
// Fabric owns the content frame. Stretching it to the popover's bounds
|
|
181
|
+
// would overwrite the measured size and feed it back into presentation.
|
|
182
|
+
contentView.autoresizingMask = []
|
|
183
|
+
viewController.view.addSubview(contentView)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private func configure(
|
|
187
|
+
_ popoverPresentationController: UIPopoverPresentationController,
|
|
188
|
+
from triggerView: UIView
|
|
189
|
+
) {
|
|
190
|
+
popoverPresentationController.sourceView = triggerView
|
|
191
|
+
popoverPresentationController.sourceRect = sourceRect(in: triggerView.bounds)
|
|
192
|
+
popoverPresentationController.permittedArrowDirections = permittedArrowDirections
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private func sourceRect(in bounds: CGRect) -> CGRect {
|
|
196
|
+
let point: CGPoint
|
|
197
|
+
switch attachmentAnchor {
|
|
198
|
+
case "top":
|
|
199
|
+
point = CGPoint(x: bounds.midX, y: bounds.minY)
|
|
200
|
+
case "bottom":
|
|
201
|
+
point = CGPoint(x: bounds.midX, y: bounds.maxY)
|
|
202
|
+
case "leading":
|
|
203
|
+
point = CGPoint(x: bounds.minX, y: bounds.midY)
|
|
204
|
+
case "trailing":
|
|
205
|
+
point = CGPoint(x: bounds.maxX, y: bounds.midY)
|
|
206
|
+
default:
|
|
207
|
+
point = CGPoint(x: bounds.midX, y: bounds.midY)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return CGRect(x: point.x, y: point.y, width: 1, height: 1)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private var permittedArrowDirections: UIPopoverArrowDirection {
|
|
214
|
+
switch arrowEdge {
|
|
215
|
+
case "top":
|
|
216
|
+
return .up
|
|
217
|
+
case "bottom":
|
|
218
|
+
return .down
|
|
219
|
+
case "leading":
|
|
220
|
+
return .left
|
|
221
|
+
case "trailing":
|
|
222
|
+
return .right
|
|
223
|
+
default:
|
|
224
|
+
return .any
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private var preferredContentSize: CGSize? {
|
|
229
|
+
guard contentSize.width.isFinite, contentSize.height.isFinite,
|
|
230
|
+
contentSize.width > 0, contentSize.height > 0
|
|
231
|
+
else {
|
|
232
|
+
// Wait for Fabric's first layout instead of presenting at an arbitrary
|
|
233
|
+
// fallback size that can become the content's next measured size.
|
|
234
|
+
return nil
|
|
235
|
+
}
|
|
236
|
+
return CGSize(width: ceil(contentSize.width), height: ceil(contentSize.height))
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Returning `.none` is UIKit's equivalent of keeping a SwiftUI popover as a
|
|
240
|
+
// popover in compact size classes (`presentationCompactAdaptation(.popover)`).
|
|
241
|
+
public func adaptivePresentationStyle(
|
|
242
|
+
for controller: UIPresentationController
|
|
243
|
+
) -> UIModalPresentationStyle {
|
|
244
|
+
.none
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
public func adaptivePresentationStyle(
|
|
248
|
+
for controller: UIPresentationController,
|
|
249
|
+
traitCollection: UITraitCollection
|
|
250
|
+
) -> UIModalPresentationStyle {
|
|
251
|
+
.none
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
public func presentationControllerDidDismiss(
|
|
255
|
+
_ presentationController: UIPresentationController
|
|
256
|
+
) {
|
|
257
|
+
guard presentedViewController === presentationController.presentedViewController else {
|
|
258
|
+
return
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
presentedViewController = nil
|
|
262
|
+
isDismissing = false
|
|
263
|
+
delegate?.onIsPresentedChange(isPresented: false)
|
|
110
264
|
}
|
|
111
265
|
}
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
#import "TinyuiPopoverView.h"
|
|
4
4
|
|
|
5
5
|
#import <React/RCTConversions.h>
|
|
6
|
+
#import <React/RCTMountingTransactionObserving.h>
|
|
7
|
+
#import <React/RCTSurfaceTouchHandler.h>
|
|
6
8
|
#import <React/UIView+React.h>
|
|
7
9
|
|
|
8
10
|
#import <react/renderer/components/TinyuiSpec/ComponentDescriptors.h>
|
|
@@ -18,13 +20,16 @@
|
|
|
18
20
|
|
|
19
21
|
using namespace facebook::react;
|
|
20
22
|
|
|
21
|
-
@interface TinyuiPopoverView () <RCTTinyuiPopoverViewViewProtocol,
|
|
23
|
+
@interface TinyuiPopoverView () <RCTTinyuiPopoverViewViewProtocol,
|
|
24
|
+
TinyuiPopoverViewDelegate,
|
|
25
|
+
RCTMountingTransactionObserving>
|
|
22
26
|
@end
|
|
23
27
|
|
|
24
28
|
@implementation TinyuiPopoverView {
|
|
25
29
|
TinyuiPopoverProvider *_popoverProvider;
|
|
26
30
|
UIView *_triggerView;
|
|
27
31
|
UIView<RCTComponentViewProtocol> *_contentComponentView;
|
|
32
|
+
RCTSurfaceTouchHandler *_contentTouchHandler;
|
|
28
33
|
}
|
|
29
34
|
|
|
30
35
|
+ (ComponentDescriptorProvider)componentDescriptorProvider
|
|
@@ -39,6 +44,7 @@ using namespace facebook::react;
|
|
|
39
44
|
_props = defaultProps;
|
|
40
45
|
|
|
41
46
|
_popoverProvider = [[TinyuiPopoverProvider alloc] initWithDelegate:self];
|
|
47
|
+
_contentTouchHandler = [RCTSurfaceTouchHandler new];
|
|
42
48
|
self.contentView = _popoverProvider;
|
|
43
49
|
}
|
|
44
50
|
return self;
|
|
@@ -68,6 +74,8 @@ using namespace facebook::react;
|
|
|
68
74
|
// Second child is the popover content, presented inside the popover.
|
|
69
75
|
NSAssert(_contentComponentView == nil, @"TinyuiPopoverView accepts one content child.");
|
|
70
76
|
_contentComponentView = childComponentView;
|
|
77
|
+
// The popover is outside the React surface's native view hierarchy.
|
|
78
|
+
[_contentTouchHandler attachToView:childComponentView];
|
|
71
79
|
_popoverProvider.contentView = childComponentView;
|
|
72
80
|
}
|
|
73
81
|
|
|
@@ -79,11 +87,23 @@ using namespace facebook::react;
|
|
|
79
87
|
_triggerView = nil;
|
|
80
88
|
_popoverProvider.triggerView = nil;
|
|
81
89
|
} else if (_contentComponentView == childComponentView) {
|
|
90
|
+
[_contentTouchHandler detachFromView:childComponentView];
|
|
82
91
|
_contentComponentView = nil;
|
|
83
92
|
_popoverProvider.contentView = nil;
|
|
84
93
|
}
|
|
85
94
|
}
|
|
86
95
|
|
|
96
|
+
- (void)mountingTransactionDidMount:(const MountingTransaction &)transaction
|
|
97
|
+
withSurfaceTelemetry:(const SurfaceTelemetry &)surfaceTelemetry
|
|
98
|
+
{
|
|
99
|
+
// Content can change size without changing the trigger's layout or props.
|
|
100
|
+
// Read only after Fabric has applied the entire transaction. UIKit must not
|
|
101
|
+
// resize this child, so its bounds remain the size computed by Yoga.
|
|
102
|
+
_popoverProvider.contentSize = _contentComponentView != nil
|
|
103
|
+
? _contentComponentView.bounds.size
|
|
104
|
+
: CGSizeZero;
|
|
105
|
+
}
|
|
106
|
+
|
|
87
107
|
#pragma mark - Props
|
|
88
108
|
|
|
89
109
|
- (void)updateProps:(Props::Shared const &)props
|
|
@@ -112,10 +132,15 @@ using namespace facebook::react;
|
|
|
112
132
|
|
|
113
133
|
- (void)prepareForRecycle
|
|
114
134
|
{
|
|
135
|
+
_popoverProvider.isPresented = NO;
|
|
136
|
+
if (_contentComponentView != nil) {
|
|
137
|
+
[_contentTouchHandler detachFromView:_contentComponentView];
|
|
138
|
+
}
|
|
115
139
|
_triggerView = nil;
|
|
116
140
|
_contentComponentView = nil;
|
|
117
141
|
_popoverProvider.triggerView = nil;
|
|
118
142
|
_popoverProvider.contentView = nil;
|
|
143
|
+
_popoverProvider.parentViewController = nil;
|
|
119
144
|
[super prepareForRecycle];
|
|
120
145
|
}
|
|
121
146
|
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#import <Symbols/Symbols.h>
|
|
4
|
+
|
|
5
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
6
|
+
NSSymbolEffect * _Nullable TinyuiSFSymbolEffect(NSDictionary *configuration);
|
|
7
|
+
NSSymbolEffectOptions *TinyuiSFSymbolOptions(NSDictionary * _Nullable configuration);
|
|
8
|
+
NSSymbolContentTransition * _Nullable TinyuiSFSymbolTransition(NSDictionary *configuration);
|
|
9
|
+
NS_ASSUME_NONNULL_END
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
#if TINYUI_FEATURE_SF_SYMBOL
|
|
2
|
+
|
|
3
|
+
#import "TinyuiSFSymbolEffects.h"
|
|
4
|
+
#include <cmath>
|
|
5
|
+
|
|
6
|
+
// Keep the concrete receiver types so the compiler checks every Symbols API.
|
|
7
|
+
#define TINYUI_LAYER_SCOPE(effect) \
|
|
8
|
+
if ([scope isEqual:@"byLayer"]) effect = [effect effectWithByLayer]; \
|
|
9
|
+
if ([scope isEqual:@"wholeSymbol"]) effect = [effect effectWithWholeSymbol];
|
|
10
|
+
|
|
11
|
+
NSSymbolEffect *TinyuiSFSymbolEffect(NSDictionary *configuration)
|
|
12
|
+
{
|
|
13
|
+
NSString *type = configuration[@"type"];
|
|
14
|
+
NSString *scope = configuration[@"scope"];
|
|
15
|
+
NSString *direction = configuration[@"direction"];
|
|
16
|
+
if ([type isEqual:@"pulse"]) {
|
|
17
|
+
NSSymbolPulseEffect *effect = NSSymbolPulseEffect.effect;
|
|
18
|
+
TINYUI_LAYER_SCOPE(effect);
|
|
19
|
+
return effect;
|
|
20
|
+
}
|
|
21
|
+
if ([type isEqual:@"bounce"]) {
|
|
22
|
+
NSSymbolBounceEffect *effect = NSSymbolBounceEffect.effect;
|
|
23
|
+
if ([direction isEqual:@"up"]) effect = NSSymbolBounceEffect.bounceUpEffect;
|
|
24
|
+
if ([direction isEqual:@"down"]) effect = NSSymbolBounceEffect.bounceDownEffect;
|
|
25
|
+
TINYUI_LAYER_SCOPE(effect);
|
|
26
|
+
return effect;
|
|
27
|
+
}
|
|
28
|
+
if ([type isEqual:@"scale"]) {
|
|
29
|
+
NSSymbolScaleEffect *effect = NSSymbolScaleEffect.effect;
|
|
30
|
+
if ([direction isEqual:@"up"]) effect = NSSymbolScaleEffect.scaleUpEffect;
|
|
31
|
+
if ([direction isEqual:@"down"]) effect = NSSymbolScaleEffect.scaleDownEffect;
|
|
32
|
+
TINYUI_LAYER_SCOPE(effect);
|
|
33
|
+
return effect;
|
|
34
|
+
}
|
|
35
|
+
if ([type isEqual:@"appear"]) {
|
|
36
|
+
NSSymbolAppearEffect *effect = NSSymbolAppearEffect.effect;
|
|
37
|
+
if ([direction isEqual:@"up"]) effect = NSSymbolAppearEffect.appearUpEffect;
|
|
38
|
+
if ([direction isEqual:@"down"]) effect = NSSymbolAppearEffect.appearDownEffect;
|
|
39
|
+
TINYUI_LAYER_SCOPE(effect);
|
|
40
|
+
return effect;
|
|
41
|
+
}
|
|
42
|
+
if ([type isEqual:@"disappear"]) {
|
|
43
|
+
NSSymbolDisappearEffect *effect = NSSymbolDisappearEffect.effect;
|
|
44
|
+
if ([direction isEqual:@"up"]) effect = NSSymbolDisappearEffect.disappearUpEffect;
|
|
45
|
+
if ([direction isEqual:@"down"]) effect = NSSymbolDisappearEffect.disappearDownEffect;
|
|
46
|
+
TINYUI_LAYER_SCOPE(effect);
|
|
47
|
+
return effect;
|
|
48
|
+
}
|
|
49
|
+
if ([type isEqual:@"variableColor"]) {
|
|
50
|
+
NSSymbolVariableColorEffect *effect = NSSymbolVariableColorEffect.effect;
|
|
51
|
+
if ([configuration[@"iteration"] isEqual:@"iterative"]) effect = [effect effectWithIterative];
|
|
52
|
+
if ([configuration[@"iteration"] isEqual:@"cumulative"]) effect = [effect effectWithCumulative];
|
|
53
|
+
if ([configuration[@"reversing"] isKindOfClass:NSNumber.class]) {
|
|
54
|
+
effect = [configuration[@"reversing"] boolValue]
|
|
55
|
+
? [effect effectWithReversing] : [effect effectWithNonReversing];
|
|
56
|
+
}
|
|
57
|
+
if ([configuration[@"inactiveLayers"] isEqual:@"hide"]) effect = [effect effectWithHideInactiveLayers];
|
|
58
|
+
if ([configuration[@"inactiveLayers"] isEqual:@"dim"]) effect = [effect effectWithDimInactiveLayers];
|
|
59
|
+
return effect;
|
|
60
|
+
}
|
|
61
|
+
if (@available(iOS 18.0, *)) {
|
|
62
|
+
if ([type isEqual:@"wiggle"]) {
|
|
63
|
+
NSSymbolWiggleEffect *effect = NSSymbolWiggleEffect.effect;
|
|
64
|
+
if ([direction isEqual:@"up"]) effect = NSSymbolWiggleEffect.wiggleUpEffect;
|
|
65
|
+
if ([direction isEqual:@"down"]) effect = NSSymbolWiggleEffect.wiggleDownEffect;
|
|
66
|
+
if ([direction isEqual:@"left"]) effect = NSSymbolWiggleEffect.wiggleLeftEffect;
|
|
67
|
+
if ([direction isEqual:@"right"]) effect = NSSymbolWiggleEffect.wiggleRightEffect;
|
|
68
|
+
if ([direction isEqual:@"forward"]) effect = NSSymbolWiggleEffect.wiggleForwardEffect;
|
|
69
|
+
if ([direction isEqual:@"backward"]) effect = NSSymbolWiggleEffect.wiggleBackwardEffect;
|
|
70
|
+
if ([direction isEqual:@"clockwise"]) effect = NSSymbolWiggleEffect.wiggleClockwiseEffect;
|
|
71
|
+
if ([direction isEqual:@"counterClockwise"]) effect = NSSymbolWiggleEffect.wiggleCounterClockwiseEffect;
|
|
72
|
+
id angle = configuration[@"angle"];
|
|
73
|
+
if ([angle isKindOfClass:NSNumber.class] && std::isfinite([angle doubleValue])) {
|
|
74
|
+
effect = [NSSymbolWiggleEffect wiggleCustomAngleEffect:[angle doubleValue]];
|
|
75
|
+
}
|
|
76
|
+
TINYUI_LAYER_SCOPE(effect);
|
|
77
|
+
return effect;
|
|
78
|
+
}
|
|
79
|
+
if ([type isEqual:@"rotate"]) {
|
|
80
|
+
NSSymbolRotateEffect *effect = NSSymbolRotateEffect.effect;
|
|
81
|
+
if ([direction isEqual:@"clockwise"]) effect = NSSymbolRotateEffect.rotateClockwiseEffect;
|
|
82
|
+
if ([direction isEqual:@"counterClockwise"]) effect = NSSymbolRotateEffect.rotateCounterClockwiseEffect;
|
|
83
|
+
TINYUI_LAYER_SCOPE(effect);
|
|
84
|
+
return effect;
|
|
85
|
+
}
|
|
86
|
+
if ([type isEqual:@"breathe"]) {
|
|
87
|
+
NSSymbolBreatheEffect *effect = NSSymbolBreatheEffect.effect;
|
|
88
|
+
if ([configuration[@"style"] isEqual:@"plain"]) effect = NSSymbolBreatheEffect.breathePlainEffect;
|
|
89
|
+
if ([configuration[@"style"] isEqual:@"pulse"]) effect = NSSymbolBreatheEffect.breathePulseEffect;
|
|
90
|
+
TINYUI_LAYER_SCOPE(effect);
|
|
91
|
+
return effect;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (@available(iOS 26.0, *)) {
|
|
95
|
+
if ([type isEqual:@"drawOn"]) {
|
|
96
|
+
NSSymbolDrawOnEffect *effect = NSSymbolDrawOnEffect.effect;
|
|
97
|
+
TINYUI_LAYER_SCOPE(effect);
|
|
98
|
+
if ([scope isEqual:@"individually"]) effect = [effect effectWithIndividually];
|
|
99
|
+
return effect;
|
|
100
|
+
}
|
|
101
|
+
if ([type isEqual:@"drawOff"]) {
|
|
102
|
+
NSSymbolDrawOffEffect *effect = NSSymbolDrawOffEffect.effect;
|
|
103
|
+
TINYUI_LAYER_SCOPE(effect);
|
|
104
|
+
if ([scope isEqual:@"individually"]) effect = [effect effectWithIndividually];
|
|
105
|
+
if ([configuration[@"reversed"] isKindOfClass:NSNumber.class]) {
|
|
106
|
+
effect = [configuration[@"reversed"] boolValue]
|
|
107
|
+
? [effect effectWithReversed] : [effect effectWithNonReversed];
|
|
108
|
+
}
|
|
109
|
+
return effect;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return nil;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
#undef TINYUI_LAYER_SCOPE
|
|
116
|
+
|
|
117
|
+
NSSymbolEffectOptions *TinyuiSFSymbolOptions(NSDictionary *configuration)
|
|
118
|
+
{
|
|
119
|
+
NSSymbolEffectOptions *options = NSSymbolEffectOptions.options;
|
|
120
|
+
if (![configuration isKindOfClass:NSDictionary.class]) return options;
|
|
121
|
+
id speedValue = configuration[@"speed"];
|
|
122
|
+
double speed = [speedValue isKindOfClass:NSNumber.class] ? [speedValue doubleValue] : 1;
|
|
123
|
+
if (std::isfinite(speed) && speed > 0) options = [options optionsWithSpeed:speed];
|
|
124
|
+
id repeat = configuration[@"repeat"];
|
|
125
|
+
if ([repeat isEqual:@NO]) return [options optionsWithNonRepeating];
|
|
126
|
+
BOOL countProvided = [repeat isKindOfClass:NSNumber.class];
|
|
127
|
+
// JS validates safe integers; cap the conversion at the native integer range.
|
|
128
|
+
NSInteger count = countProvided ? (NSInteger)fmin([repeat doubleValue], (double)NSIntegerMax / 2) : 1;
|
|
129
|
+
count = MAX(1, count);
|
|
130
|
+
id behaviorValue = configuration[@"repeatBehavior"];
|
|
131
|
+
NSString *behavior = [behaviorValue isKindOfClass:NSString.class] ? behaviorValue : nil;
|
|
132
|
+
id delayValue = configuration[@"repeatDelay"];
|
|
133
|
+
BOOL hasDelay = [delayValue isKindOfClass:NSNumber.class];
|
|
134
|
+
double delay = hasDelay ? [delayValue doubleValue] : 0;
|
|
135
|
+
if (!std::isfinite(delay) || delay < 0) delay = 0;
|
|
136
|
+
BOOL forever = [repeat isEqual:@"forever"] || behavior != nil || hasDelay;
|
|
137
|
+
|
|
138
|
+
if (@available(iOS 18.0, *)) {
|
|
139
|
+
NSSymbolEffectOptionsRepeatBehavior *repetition = nil;
|
|
140
|
+
if ([behavior isEqual:@"continuous"]) {
|
|
141
|
+
repetition = NSSymbolEffectOptionsRepeatBehavior.behaviorContinuous;
|
|
142
|
+
} else if (countProvided) {
|
|
143
|
+
repetition = hasDelay
|
|
144
|
+
? [NSSymbolEffectOptionsRepeatBehavior behaviorPeriodicWithCount:count delay:delay]
|
|
145
|
+
: [NSSymbolEffectOptionsRepeatBehavior behaviorPeriodicWithCount:count];
|
|
146
|
+
} else if (forever) {
|
|
147
|
+
repetition = hasDelay
|
|
148
|
+
? [NSSymbolEffectOptionsRepeatBehavior behaviorPeriodicWithDelay:delay]
|
|
149
|
+
: NSSymbolEffectOptionsRepeatBehavior.behaviorPeriodic;
|
|
150
|
+
}
|
|
151
|
+
if (repetition != nil) options = [options optionsWithRepeatBehavior:repetition];
|
|
152
|
+
} else {
|
|
153
|
+
if (countProvided) options = [options optionsWithRepeatCount:count];
|
|
154
|
+
else if (forever) options = [options optionsWithRepeating];
|
|
155
|
+
}
|
|
156
|
+
return options;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
NSSymbolContentTransition *TinyuiSFSymbolTransition(NSDictionary *configuration)
|
|
160
|
+
{
|
|
161
|
+
NSString *type = configuration[@"type"];
|
|
162
|
+
if ([type isEqual:@"automatic"]) return NSSymbolAutomaticContentTransition.transition;
|
|
163
|
+
if (![type isEqual:@"replace"] && ![type isEqual:@"magicReplace"]) return nil;
|
|
164
|
+
|
|
165
|
+
NSSymbolReplaceContentTransition *transition = NSSymbolReplaceContentTransition.transition;
|
|
166
|
+
NSString *direction = configuration[@"direction"];
|
|
167
|
+
if ([direction isEqual:@"downUp"]) transition = NSSymbolReplaceContentTransition.replaceDownUpTransition;
|
|
168
|
+
if ([direction isEqual:@"upUp"]) transition = NSSymbolReplaceContentTransition.replaceUpUpTransition;
|
|
169
|
+
if ([direction isEqual:@"offUp"]) transition = NSSymbolReplaceContentTransition.replaceOffUpTransition;
|
|
170
|
+
if ([configuration[@"scope"] isEqual:@"byLayer"]) transition = [transition transitionWithByLayer];
|
|
171
|
+
if ([configuration[@"scope"] isEqual:@"wholeSymbol"]) transition = [transition transitionWithWholeSymbol];
|
|
172
|
+
if (@available(iOS 18.0, *)) {
|
|
173
|
+
if ([type isEqual:@"magicReplace"]) {
|
|
174
|
+
return [NSSymbolReplaceContentTransition magicTransitionWithFallback:transition];
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return transition;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
#endif
|