react-native-platform-components 0.8.9 → 0.8.10
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/ios/PCConstants.swift +39 -3
- package/ios/PCSelectionMenu.swift +60 -14
- package/package.json +9 -8
package/ios/PCConstants.swift
CHANGED
|
@@ -19,11 +19,47 @@ enum PCConstants {
|
|
|
19
19
|
/// Minimum row height in selection menu popover (used as baseline)
|
|
20
20
|
static let popoverRowHeightMin: CGFloat = 44
|
|
21
21
|
|
|
22
|
-
///
|
|
22
|
+
/// Horizontal inset applied to each side of a menu row's label
|
|
23
|
+
static let popoverRowHorizontalInset: CGFloat = 16
|
|
24
|
+
|
|
25
|
+
/// Vertical padding added to each menu row's text (top + bottom combined)
|
|
26
|
+
static let popoverRowVerticalPadding: CGFloat = 16
|
|
27
|
+
|
|
28
|
+
/// Estimated single-line row height that respects the user's preferred
|
|
29
|
+
/// content size. Used only as a table-view estimate; actual rows self-size.
|
|
23
30
|
static var popoverRowHeight: CGFloat {
|
|
24
31
|
let bodyFont = UIFont.preferredFont(forTextStyle: .body)
|
|
25
|
-
|
|
26
|
-
|
|
32
|
+
return max(popoverRowHeightMin, ceil(bodyFont.lineHeight) + popoverRowVerticalPadding)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/// Measured height of a single menu row for the given label and content
|
|
36
|
+
/// width, accounting for multi-line wrapping at the current Dynamic Type
|
|
37
|
+
/// size. Mirrors the Auto Layout sizing of `PCGlassMenuCell` so the popover
|
|
38
|
+
/// container can be sized to fit its content.
|
|
39
|
+
static func popoverRowHeight(forLabel label: String, width: CGFloat) -> CGFloat {
|
|
40
|
+
let bodyFont = UIFont.preferredFont(forTextStyle: .body)
|
|
41
|
+
let textWidth = max(1, width - popoverRowHorizontalInset * 2)
|
|
42
|
+
let bounding = (label as NSString).boundingRect(
|
|
43
|
+
with: CGSize(width: textWidth, height: .greatestFiniteMagnitude),
|
|
44
|
+
options: [.usesLineFragmentOrigin, .usesFontLeading],
|
|
45
|
+
attributes: [.font: bodyFont],
|
|
46
|
+
context: nil
|
|
47
|
+
)
|
|
48
|
+
return max(popoverRowHeightMin, ceil(bounding.height) + popoverRowVerticalPadding)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/// Adaptive popover width. Widens for accessibility content size categories
|
|
52
|
+
/// so long labels wrap less aggressively (mirrors system menu behavior).
|
|
53
|
+
/// The caller is responsible for clamping to the available screen width.
|
|
54
|
+
static func popoverWidth(forCategory category: UIContentSizeCategory) -> CGFloat {
|
|
55
|
+
guard category.isAccessibilityCategory else { return popoverWidth }
|
|
56
|
+
switch category {
|
|
57
|
+
case .accessibilityMedium: return 300
|
|
58
|
+
case .accessibilityLarge: return 320
|
|
59
|
+
case .accessibilityExtraLarge: return 340
|
|
60
|
+
case .accessibilityExtraExtraLarge: return 360
|
|
61
|
+
default: return 380 // accessibilityExtraExtraExtraLarge and beyond
|
|
62
|
+
}
|
|
27
63
|
}
|
|
28
64
|
|
|
29
65
|
/// Vertical padding in selection menu popover (top + bottom)
|
|
@@ -277,33 +277,53 @@ public final class PCSelectionMenuView: UIControl {
|
|
|
277
277
|
// Calculate menu position relative to source view
|
|
278
278
|
let sourceFrame = self.convert(self.bounds, to: vc.view)
|
|
279
279
|
let screenBounds = vc.view.bounds
|
|
280
|
-
let popoverHeight = min(
|
|
281
|
-
CGFloat(opts.count) * PCConstants.popoverRowHeight + PCConstants.popoverVerticalPadding,
|
|
282
|
-
PCConstants.popoverMaxHeight
|
|
283
|
-
)
|
|
284
280
|
let spacing: CGFloat = 8
|
|
285
281
|
|
|
282
|
+
// Adaptive width (wider at accessibility sizes), clamped to the screen.
|
|
283
|
+
let category = self.traitCollection.preferredContentSizeCategory
|
|
284
|
+
let menuWidth = min(
|
|
285
|
+
PCConstants.popoverWidth(forCategory: category),
|
|
286
|
+
screenBounds.width - 32
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
// Measure the real (possibly multi-line) content height so the
|
|
290
|
+
// container fits its rows instead of clipping or leaving gaps.
|
|
291
|
+
let contentHeight = opts.reduce(PCConstants.popoverVerticalPadding) { partial, opt in
|
|
292
|
+
partial + PCConstants.popoverRowHeight(forLabel: opt.label, width: menuWidth)
|
|
293
|
+
}
|
|
294
|
+
// Allow taller menus at accessibility sizes; otherwise keep them compact.
|
|
295
|
+
let maxHeight = category.isAccessibilityCategory
|
|
296
|
+
? max(PCConstants.popoverMaxHeight, screenBounds.height - 120)
|
|
297
|
+
: PCConstants.popoverMaxHeight
|
|
298
|
+
let popoverHeight = min(contentHeight, maxHeight)
|
|
299
|
+
|
|
286
300
|
// Check if menu fits below the source view
|
|
287
|
-
let
|
|
301
|
+
let firstRowHeight = PCConstants.popoverRowHeight(forLabel: opts[0].label, width: menuWidth)
|
|
288
302
|
let wouldExtendBeyondBottom = sourceFrame.maxY + spacing + popoverHeight > screenBounds.maxY - 20
|
|
289
303
|
|
|
290
|
-
|
|
304
|
+
var menuY: CGFloat
|
|
291
305
|
if wouldExtendBeyondBottom {
|
|
292
306
|
// Position above the source view (no overlap offset)
|
|
293
307
|
menuY = sourceFrame.minY - spacing - popoverHeight
|
|
294
308
|
} else {
|
|
295
309
|
// Position below, but shift up by one row to overlap trigger (like system menu)
|
|
296
|
-
menuY = sourceFrame.maxY + spacing -
|
|
310
|
+
menuY = sourceFrame.maxY + spacing - firstRowHeight
|
|
297
311
|
}
|
|
298
312
|
|
|
313
|
+
// Keep the menu fully on screen vertically (tall menus at large
|
|
314
|
+
// Dynamic Type sizes can otherwise run off the top or bottom).
|
|
315
|
+
let topMargin = vc.view.safeAreaInsets.top + spacing
|
|
316
|
+
let bottomLimit = screenBounds.maxY - vc.view.safeAreaInsets.bottom - spacing - popoverHeight
|
|
317
|
+
menuY = min(max(menuY, topMargin), max(topMargin, bottomLimit))
|
|
318
|
+
|
|
299
319
|
// Center horizontally, but keep within screen bounds
|
|
300
|
-
var menuX = sourceFrame.midX -
|
|
301
|
-
menuX = max(16, min(menuX, screenBounds.maxX -
|
|
320
|
+
var menuX = sourceFrame.midX - menuWidth / 2
|
|
321
|
+
menuX = max(16, min(menuX, screenBounds.maxX - menuWidth - 16))
|
|
302
322
|
|
|
303
323
|
let menuFrame = CGRect(
|
|
304
324
|
x: menuX,
|
|
305
325
|
y: menuY,
|
|
306
|
-
width:
|
|
326
|
+
width: menuWidth,
|
|
307
327
|
height: popoverHeight
|
|
308
328
|
)
|
|
309
329
|
|
|
@@ -365,6 +385,11 @@ public final class PCSelectionMenuView: UIControl {
|
|
|
365
385
|
private class PCGlassMenuCell: UITableViewCell {
|
|
366
386
|
static let reuseIdentifier = "PCGlassMenuCell"
|
|
367
387
|
|
|
388
|
+
/// Custom multi-line label. We avoid the legacy `textLabel` because it
|
|
389
|
+
/// defaults to a single line and does not drive self-sizing reliably, which
|
|
390
|
+
/// caused rows to overlap at large Dynamic Type sizes.
|
|
391
|
+
let menuLabel = UILabel()
|
|
392
|
+
|
|
368
393
|
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
|
369
394
|
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
|
370
395
|
setupCell()
|
|
@@ -379,8 +404,26 @@ private class PCGlassMenuCell: UITableViewCell {
|
|
|
379
404
|
backgroundColor = .clear
|
|
380
405
|
contentView.backgroundColor = .clear
|
|
381
406
|
selectionStyle = .none
|
|
382
|
-
|
|
383
|
-
|
|
407
|
+
|
|
408
|
+
menuLabel.font = .preferredFont(forTextStyle: .body)
|
|
409
|
+
menuLabel.adjustsFontForContentSizeCategory = true
|
|
410
|
+
menuLabel.numberOfLines = 0
|
|
411
|
+
menuLabel.translatesAutoresizingMaskIntoConstraints = false
|
|
412
|
+
contentView.addSubview(menuLabel)
|
|
413
|
+
|
|
414
|
+
let v = PCConstants.popoverRowVerticalPadding / 2
|
|
415
|
+
let h = PCConstants.popoverRowHorizontalInset
|
|
416
|
+
// Center the label and let it grow the row vertically. The min-height
|
|
417
|
+
// constraint guarantees the 44pt touch target for short labels, while
|
|
418
|
+
// the >= top / <= bottom pair lets tall (wrapped) labels expand the row.
|
|
419
|
+
NSLayoutConstraint.activate([
|
|
420
|
+
menuLabel.topAnchor.constraint(greaterThanOrEqualTo: contentView.topAnchor, constant: v),
|
|
421
|
+
menuLabel.bottomAnchor.constraint(lessThanOrEqualTo: contentView.bottomAnchor, constant: -v),
|
|
422
|
+
menuLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
|
|
423
|
+
menuLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: h),
|
|
424
|
+
menuLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -h),
|
|
425
|
+
contentView.heightAnchor.constraint(greaterThanOrEqualToConstant: PCConstants.popoverRowHeightMin),
|
|
426
|
+
])
|
|
384
427
|
}
|
|
385
428
|
}
|
|
386
429
|
|
|
@@ -451,7 +494,10 @@ private class PCMenuViewController: UIViewController, UITableViewDelegate, UITab
|
|
|
451
494
|
tableView.backgroundColor = .clear
|
|
452
495
|
tableView.separatorStyle = .none
|
|
453
496
|
tableView.isScrollEnabled = true
|
|
454
|
-
|
|
497
|
+
// Self-sizing rows so labels that wrap at large Dynamic Type sizes get
|
|
498
|
+
// the height they need instead of overlapping a fixed-height row.
|
|
499
|
+
tableView.rowHeight = UITableView.automaticDimension
|
|
500
|
+
tableView.estimatedRowHeight = PCConstants.popoverRowHeight
|
|
455
501
|
tableView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
456
502
|
let verticalPad = PCConstants.popoverVerticalPadding / 2
|
|
457
503
|
tableView.contentInset = UIEdgeInsets(top: verticalPad, left: 0, bottom: verticalPad, right: 0)
|
|
@@ -479,7 +525,7 @@ private class PCMenuViewController: UIViewController, UITableViewDelegate, UITab
|
|
|
479
525
|
|
|
480
526
|
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
|
481
527
|
let cell = tableView.dequeueReusableCell(withIdentifier: PCGlassMenuCell.reuseIdentifier, for: indexPath)
|
|
482
|
-
cell.
|
|
528
|
+
(cell as? PCGlassMenuCell)?.menuLabel.text = options[indexPath.row].label
|
|
483
529
|
return cell
|
|
484
530
|
}
|
|
485
531
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-platform-components",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.10",
|
|
4
4
|
"description": "Native UI components for React Native: DatePicker, ContextMenu, SelectionMenu, SegmentedControl, LiquidGlass.",
|
|
5
5
|
"main": "./lib/commonjs/index.js",
|
|
6
6
|
"module": "./lib/module/index.js",
|
|
@@ -110,15 +110,16 @@
|
|
|
110
110
|
"@eslint/compat": "^2.0.1",
|
|
111
111
|
"@eslint/eslintrc": "^3.3.3",
|
|
112
112
|
"@eslint/js": "^9.39.2",
|
|
113
|
-
"@expo/config-plugins": "^
|
|
114
|
-
"@react-native-community/cli": "20.1.
|
|
115
|
-
"@react-native/babel-preset": "0.
|
|
116
|
-
"@react-native/eslint-config": "^0.
|
|
113
|
+
"@expo/config-plugins": "^56.0.8",
|
|
114
|
+
"@react-native-community/cli": "20.1.3",
|
|
115
|
+
"@react-native/babel-preset": "0.85.3",
|
|
116
|
+
"@react-native/eslint-config": "^0.85.3",
|
|
117
|
+
"@react-native/jest-preset": "0.85.3",
|
|
117
118
|
"@release-it/conventional-changelog": "^10.0.4",
|
|
118
119
|
"@types/jest": "^30.0.0",
|
|
119
120
|
"@types/react": "^19.2.9",
|
|
120
121
|
"@types/react-test-renderer": "^19.1.0",
|
|
121
|
-
"babel-preset-expo": "^
|
|
122
|
+
"babel-preset-expo": "^56.0.14",
|
|
122
123
|
"commitlint": "^20.3.1",
|
|
123
124
|
"del-cli": "^7.0.0",
|
|
124
125
|
"eslint": "^9.39.2",
|
|
@@ -128,7 +129,7 @@
|
|
|
128
129
|
"lefthook": "^2.0.15",
|
|
129
130
|
"prettier": "^3.8.1",
|
|
130
131
|
"react": "19.2.3",
|
|
131
|
-
"react-native": "0.
|
|
132
|
+
"react-native": "0.85.3",
|
|
132
133
|
"react-native-builder-bob": "^0.40.17",
|
|
133
134
|
"react-test-renderer": "19.2.3",
|
|
134
135
|
"release-it": "^19.2.4",
|
|
@@ -218,7 +219,7 @@
|
|
|
218
219
|
}
|
|
219
220
|
},
|
|
220
221
|
"jest": {
|
|
221
|
-
"preset": "react-native",
|
|
222
|
+
"preset": "@react-native/jest-preset",
|
|
222
223
|
"modulePathIgnorePatterns": [
|
|
223
224
|
"<rootDir>/example/node_modules",
|
|
224
225
|
"<rootDir>/lib/"
|