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.
@@ -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
- /// Dynamic row height that respects user's preferred content size
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
- // Row height = font line height + vertical padding (16pt total)
26
- return max(popoverRowHeightMin, ceil(bodyFont.lineHeight) + 16)
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 rowHeight = PCConstants.popoverRowHeight
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
- let menuY: CGFloat
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 - rowHeight
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 - PCConstants.popoverWidth / 2
301
- menuX = max(16, min(menuX, screenBounds.maxX - PCConstants.popoverWidth - 16))
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: PCConstants.popoverWidth,
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
- textLabel?.font = .preferredFont(forTextStyle: .body)
383
- textLabel?.adjustsFontForContentSizeCategory = true
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
- tableView.rowHeight = PCConstants.popoverRowHeight
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.textLabel?.text = options[indexPath.row].label
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.9",
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": "^54.0.4",
114
- "@react-native-community/cli": "20.1.0",
115
- "@react-native/babel-preset": "0.83.1",
116
- "@react-native/eslint-config": "^0.83.1",
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": "^54.0.10",
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.83.1",
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/"