react-native-platform-components 1.6.0 → 1.7.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.
Files changed (139) hide show
  1. package/README.md +19 -18
  2. package/android/src/main/java/com/platformcomponents/PCActionSheetView.kt +309 -0
  3. package/android/src/main/java/com/platformcomponents/PCActionSheetViewManager.kt +131 -0
  4. package/android/src/main/java/com/platformcomponents/PCButtonGroupView.kt +5 -1
  5. package/android/src/main/java/com/platformcomponents/PCButtonView.kt +5 -1
  6. package/android/src/main/java/com/platformcomponents/PCContextMenuView.kt +82 -27
  7. package/android/src/main/java/com/platformcomponents/PCContextMenuViewManager.kt +12 -13
  8. package/android/src/main/java/com/platformcomponents/PCMenuSupport.kt +66 -2
  9. package/android/src/main/java/com/platformcomponents/PlatformComponentsPackage.kt +1 -0
  10. package/android/src/main/res/values/ids.xml +2 -0
  11. package/ios/PCActionSheet.h +12 -0
  12. package/ios/PCActionSheet.mm +160 -0
  13. package/ios/PCActionSheetView.swift +280 -0
  14. package/ios/PCContextMenu.mm +22 -1
  15. package/ios/PCContextMenu.swift +218 -15
  16. package/ios/PCMenuItems.h +2 -1
  17. package/ios/PCMenuSupport.swift +22 -3
  18. package/lib/commonjs/ActionSheet.js +85 -0
  19. package/lib/commonjs/ActionSheet.js.map +1 -0
  20. package/lib/commonjs/ActionSheetNativeComponent.ts +94 -0
  21. package/lib/commonjs/ButtonGroupNativeComponent.ts +2 -0
  22. package/lib/commonjs/ButtonNativeComponent.ts +2 -0
  23. package/lib/commonjs/ContextMenu.js +50 -4
  24. package/lib/commonjs/ContextMenu.js.map +1 -1
  25. package/lib/commonjs/ContextMenuNativeComponent.ts +29 -8
  26. package/lib/commonjs/index.js +11 -0
  27. package/lib/commonjs/index.js.map +1 -1
  28. package/lib/commonjs/index.web.js +7 -0
  29. package/lib/commonjs/index.web.js.map +1 -1
  30. package/lib/commonjs/jest.js +92 -5
  31. package/lib/commonjs/jest.js.map +1 -1
  32. package/lib/commonjs/menuItems.js +2 -1
  33. package/lib/commonjs/menuItems.js.map +1 -1
  34. package/lib/commonjs/selectionIdentifiers.js.map +1 -1
  35. package/lib/commonjs/web/ActionSheet.js +131 -0
  36. package/lib/commonjs/web/ActionSheet.js.map +1 -0
  37. package/lib/commonjs/web/components.js +3 -1
  38. package/lib/commonjs/web/components.js.map +1 -1
  39. package/lib/commonjs/web/containers.js +1 -0
  40. package/lib/commonjs/web/containers.js.map +1 -1
  41. package/lib/module/ActionSheet.js +78 -0
  42. package/lib/module/ActionSheet.js.map +1 -0
  43. package/lib/module/ActionSheetNativeComponent.ts +94 -0
  44. package/lib/module/ButtonGroupNativeComponent.ts +2 -0
  45. package/lib/module/ButtonNativeComponent.ts +2 -0
  46. package/lib/module/ContextMenu.js +51 -4
  47. package/lib/module/ContextMenu.js.map +1 -1
  48. package/lib/module/ContextMenuNativeComponent.ts +29 -8
  49. package/lib/module/index.js +1 -0
  50. package/lib/module/index.js.map +1 -1
  51. package/lib/module/index.web.js +1 -1
  52. package/lib/module/index.web.js.map +1 -1
  53. package/lib/module/jest.js +90 -5
  54. package/lib/module/jest.js.map +1 -1
  55. package/lib/module/menuItems.js +2 -1
  56. package/lib/module/menuItems.js.map +1 -1
  57. package/lib/module/selectionIdentifiers.js.map +1 -1
  58. package/lib/module/web/ActionSheet.js +126 -0
  59. package/lib/module/web/ActionSheet.js.map +1 -0
  60. package/lib/module/web/components.js +2 -0
  61. package/lib/module/web/components.js.map +1 -1
  62. package/lib/module/web/containers.js +1 -0
  63. package/lib/module/web/containers.js.map +1 -1
  64. package/lib/typescript/commonjs/docs/sidebars.d.ts.map +1 -1
  65. package/lib/typescript/commonjs/src/ActionSheet.d.ts +98 -0
  66. package/lib/typescript/commonjs/src/ActionSheet.d.ts.map +1 -0
  67. package/lib/typescript/commonjs/src/ActionSheetNativeComponent.d.ts +78 -0
  68. package/lib/typescript/commonjs/src/ActionSheetNativeComponent.d.ts.map +1 -0
  69. package/lib/typescript/commonjs/src/ButtonGroupNativeComponent.d.ts +2 -0
  70. package/lib/typescript/commonjs/src/ButtonGroupNativeComponent.d.ts.map +1 -1
  71. package/lib/typescript/commonjs/src/ButtonNativeComponent.d.ts +2 -0
  72. package/lib/typescript/commonjs/src/ButtonNativeComponent.d.ts.map +1 -1
  73. package/lib/typescript/commonjs/src/ContextMenu.d.ts +22 -6
  74. package/lib/typescript/commonjs/src/ContextMenu.d.ts.map +1 -1
  75. package/lib/typescript/commonjs/src/ContextMenuNativeComponent.d.ts +20 -7
  76. package/lib/typescript/commonjs/src/ContextMenuNativeComponent.d.ts.map +1 -1
  77. package/lib/typescript/commonjs/src/index.d.ts +1 -0
  78. package/lib/typescript/commonjs/src/index.d.ts.map +1 -1
  79. package/lib/typescript/commonjs/src/index.web.d.ts +2 -1
  80. package/lib/typescript/commonjs/src/index.web.d.ts.map +1 -1
  81. package/lib/typescript/commonjs/src/jest.d.ts +17 -3
  82. package/lib/typescript/commonjs/src/jest.d.ts.map +1 -1
  83. package/lib/typescript/commonjs/src/menuItems.d.ts +12 -0
  84. package/lib/typescript/commonjs/src/menuItems.d.ts.map +1 -1
  85. package/lib/typescript/commonjs/src/selectionIdentifiers.d.ts +1 -1
  86. package/lib/typescript/commonjs/src/selectionIdentifiers.d.ts.map +1 -1
  87. package/lib/typescript/commonjs/src/web/ActionSheet.d.ts +11 -0
  88. package/lib/typescript/commonjs/src/web/ActionSheet.d.ts.map +1 -0
  89. package/lib/typescript/commonjs/src/web/components.d.ts +1 -0
  90. package/lib/typescript/commonjs/src/web/components.d.ts.map +1 -1
  91. package/lib/typescript/commonjs/src/web/containers.d.ts.map +1 -1
  92. package/lib/typescript/commonjs/src/webComponents.d.ts +2 -0
  93. package/lib/typescript/commonjs/src/webComponents.d.ts.map +1 -1
  94. package/lib/typescript/module/docs/sidebars.d.ts.map +1 -1
  95. package/lib/typescript/module/src/ActionSheet.d.ts +98 -0
  96. package/lib/typescript/module/src/ActionSheet.d.ts.map +1 -0
  97. package/lib/typescript/module/src/ActionSheetNativeComponent.d.ts +78 -0
  98. package/lib/typescript/module/src/ActionSheetNativeComponent.d.ts.map +1 -0
  99. package/lib/typescript/module/src/ButtonGroupNativeComponent.d.ts +2 -0
  100. package/lib/typescript/module/src/ButtonGroupNativeComponent.d.ts.map +1 -1
  101. package/lib/typescript/module/src/ButtonNativeComponent.d.ts +2 -0
  102. package/lib/typescript/module/src/ButtonNativeComponent.d.ts.map +1 -1
  103. package/lib/typescript/module/src/ContextMenu.d.ts +22 -6
  104. package/lib/typescript/module/src/ContextMenu.d.ts.map +1 -1
  105. package/lib/typescript/module/src/ContextMenuNativeComponent.d.ts +20 -7
  106. package/lib/typescript/module/src/ContextMenuNativeComponent.d.ts.map +1 -1
  107. package/lib/typescript/module/src/index.d.ts +1 -0
  108. package/lib/typescript/module/src/index.d.ts.map +1 -1
  109. package/lib/typescript/module/src/index.web.d.ts +2 -1
  110. package/lib/typescript/module/src/index.web.d.ts.map +1 -1
  111. package/lib/typescript/module/src/jest.d.ts +17 -3
  112. package/lib/typescript/module/src/jest.d.ts.map +1 -1
  113. package/lib/typescript/module/src/menuItems.d.ts +12 -0
  114. package/lib/typescript/module/src/menuItems.d.ts.map +1 -1
  115. package/lib/typescript/module/src/selectionIdentifiers.d.ts +1 -1
  116. package/lib/typescript/module/src/selectionIdentifiers.d.ts.map +1 -1
  117. package/lib/typescript/module/src/web/ActionSheet.d.ts +11 -0
  118. package/lib/typescript/module/src/web/ActionSheet.d.ts.map +1 -0
  119. package/lib/typescript/module/src/web/components.d.ts +1 -0
  120. package/lib/typescript/module/src/web/components.d.ts.map +1 -1
  121. package/lib/typescript/module/src/web/containers.d.ts.map +1 -1
  122. package/lib/typescript/module/src/webComponents.d.ts +2 -0
  123. package/lib/typescript/module/src/webComponents.d.ts.map +1 -1
  124. package/package.json +9 -2
  125. package/src/ActionSheet.tsx +193 -0
  126. package/src/ActionSheetNativeComponent.ts +94 -0
  127. package/src/ButtonGroupNativeComponent.ts +2 -0
  128. package/src/ButtonNativeComponent.ts +2 -0
  129. package/src/ContextMenu.tsx +71 -12
  130. package/src/ContextMenuNativeComponent.ts +29 -8
  131. package/src/index.tsx +1 -0
  132. package/src/index.web.tsx +2 -0
  133. package/src/jest.tsx +108 -4
  134. package/src/menuItems.ts +13 -0
  135. package/src/selectionIdentifiers.ts +1 -1
  136. package/src/web/ActionSheet.tsx +130 -0
  137. package/src/web/components.tsx +5 -0
  138. package/src/web/containers.tsx +1 -0
  139. package/src/webComponents.ts +2 -0
package/README.md CHANGED
@@ -5,20 +5,20 @@
5
5
  [![CI](https://github.com/JarX-Concepts/react-native-platform-components/actions/workflows/ci.yml/badge.svg)](https://github.com/JarX-Concepts/react-native-platform-components/actions/workflows/ci.yml)
6
6
  [![license](https://img.shields.io/npm/l/react-native-platform-components.svg)](./LICENSE)
7
7
 
8
- Native **TextField**, **DatePicker**, **ContextMenu**, **SelectionMenu**, **SegmentedControl**, **TabBar**, **NavigationRail**, **Button**, **ButtonGroup**, **FloatingActionButton**, **FloatingToolbar** and **LiquidGlass** for React Native. Every component is the real platform widget on both iOS and Android (UIKit on iOS, Material 3 Expressive on Android), behind one typed, declarative API. No JavaScript re-implementations.
8
+ Native **TextField**, **DatePicker**, **ContextMenu**, **ActionSheet**, **SelectionMenu**, **SegmentedControl**, **TabBar**, **NavigationRail**, **Button**, **ButtonGroup**, **FloatingActionButton**, **FloatingToolbar** and **LiquidGlass** for React Native. Every component is the real platform widget on both iOS and Android (UIKit on iOS, Material 3 Expressive on Android), behind one typed, declarative API. No JavaScript re-implementations.
9
9
 
10
10
  <p align="center">
11
11
  <a href="https://jarx-concepts.github.io/react-native-platform-components">
12
12
  <picture>
13
13
  <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/JarX-Concepts/react-native-platform-components/main/assets/hero-dark.webp" />
14
- <img src="https://raw.githubusercontent.com/JarX-Concepts/react-native-platform-components/main/assets/hero-light.webp" alt="DatePicker, TextField, SegmentedControl, Button, TabBar, NavigationRail, FloatingActionButton, ContextMenu, LiquidGlass, FloatingToolbar and SelectionMenu on iOS and Android, and the dark mode from useNativeTheme" />
14
+ <img src="https://raw.githubusercontent.com/JarX-Concepts/react-native-platform-components/main/assets/hero-light.webp" alt="DatePicker, TextField, SegmentedControl, Button, TabBar, NavigationRail, FloatingActionButton, ContextMenu, ActionSheet, FloatingToolbar, LiquidGlass and SelectionMenu on iOS and Android, and the dark mode from useNativeTheme" />
15
15
  </picture>
16
16
  </a>
17
17
  </p>
18
18
  <p align="center"><sub>Every tile is the platform's own widget, captured from the example app. The <a href="https://jarx-concepts.github.io/react-native-platform-components">docs site</a> has full recordings of each component on both platforms; <a href="https://jarx-concepts.github.io/react-native-platform-components/components/liquidglass">LiquidGlass</a> is iOS 26 only.</sub></p>
19
19
 
20
20
  <p align="center">
21
- <a href="https://jarx-concepts.github.io/react-native-platform-components"><img src="https://raw.githubusercontent.com/JarX-Concepts/react-native-platform-components/main/assets/showreel.gif" width="640" alt="Showreel: the native theme changing brand color and switching to dark mode, then DatePicker, TextField, SegmentedControl, TabBar, NavigationRail, Button, FloatingActionButton, ContextMenu, SelectionMenu, FloatingToolbar and LiquidGlass in use, iOS on the left and Android on the right" /></a>
21
+ <a href="https://jarx-concepts.github.io/react-native-platform-components"><img src="https://raw.githubusercontent.com/JarX-Concepts/react-native-platform-components/main/assets/showreel.gif" width="640" alt="Showreel: the native theme changing brand color and switching to dark mode, then DatePicker, TextField, SegmentedControl, TabBar, NavigationRail, Button, FloatingActionButton, ContextMenu, ActionSheet, SelectionMenu, FloatingToolbar and LiquidGlass in use, iOS on the left and Android on the right" /></a>
22
22
  </p>
23
23
  <p align="center"><sub>iOS on the left, Android on the right. Recorded by the Detox suite in <code>example/</code>.</sub></p>
24
24
 
@@ -43,20 +43,21 @@ That renders `UISegmentedControl` on iOS and Material 3 segmented buttons (`Mate
43
43
 
44
44
  ## Components
45
45
 
46
- | Component | iOS | Android | Instead of |
47
- | ---------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
48
- | [**TextField**](https://jarx-concepts.github.io/react-native-platform-components/components/textfield) | `UITextField` / `UITextView`, with Writing Tools and the iOS 17–18 text traits | Material 3 `TextInputLayout`: outlined or filled, floating label, supporting text, counter | `TextInput` (a bare `EditText` on Android); `react-native-paper` (drawn in JavaScript) |
49
- | [**DatePicker**](https://jarx-concepts.github.io/react-native-platform-components/components/datepicker) | `UIDatePicker`, modal or embedded | `MaterialDatePicker` / `MaterialTimePicker`, or the AppCompat dialogs | `@react-native-community/datetimepicker` (no Material 3 pickers) |
50
- | [**SelectionMenu**](https://jarx-concepts.github.io/react-native-platform-components/components/selectionmenu) | System menus | Material exposed dropdown or `Spinner` | `@react-native-picker/picker` (wheel or dialog, no menu) |
51
- | [**ContextMenu**](https://jarx-concepts.github.io/react-native-platform-components/components/contextmenu) | `UIContextMenuInteraction` | `PopupMenu` | `zeego` (through two more native packages) |
52
- | [**SegmentedControl**](https://jarx-concepts.github.io/react-native-platform-components/components/segmentedcontrol) | `UISegmentedControl` | Material 3 Expressive connected buttons, or classic segmented buttons | `@react-native-segmented-control/segmented-control` (JavaScript on Android) |
53
- | [**TabBar**](https://jarx-concepts.github.io/react-native-platform-components/components/tabbar) | `UITabBar`, the floating Liquid Glass bar on iOS 26 | Material 3 navigation bar | React Navigation's bottom tabs (JavaScript); `react-native-bottom-tabs` (tied to its navigator) |
54
- | [**NavigationRail**](https://jarx-concepts.github.io/react-native-platform-components/components/navigationrail) | A column of tab-style `UIButton`s (iOS has no rail; use TabBar on iPhone) | Material 3 Expressive `NavigationRailView`: header, badges, menu gravity, expanded rail | React Navigation's drawer or a custom view (JavaScript) |
55
- | [**Button**](https://jarx-concepts.github.io/react-native-platform-components/components/button) | `UIButton` configurations, Liquid Glass variants on iOS 26 | Material 3 Expressive `MaterialButton`: five sizes, shape morph | `react-native-paper` (Material 3, not Expressive, in JavaScript) |
56
- | [**ButtonGroup**](https://jarx-concepts.github.io/react-native-platform-components/components/buttongroup) | A row of `UIButton`s with a "…" overflow menu; SplitButton: a button and a chevron `UIMenu` button | Material 3 Expressive button groups, single or multiple selection, overflow menu; `MaterialSplitButton` | `react-native-paper` segmented buttons (JavaScript) |
57
- | [**FloatingActionButton**](https://jarx-concepts.github.io/react-native-platform-components/components/floatingactionbutton) | A round prominent `UIButton`, Liquid Glass on iOS 26 (iOS has no FAB) | Material `FloatingActionButton` / `ExtendedFloatingActionButton`: four sizes, shrink on scroll | `react-native-paper` FAB (drawn in JavaScript) |
58
- | [**FloatingToolbar**](https://jarx-concepts.github.io/react-native-platform-components/components/floatingtoolbar) | Liquid Glass capsule on iOS 26, blur before | Material 3 Expressive `FloatingToolbarLayout` | A custom view |
59
- | [**LiquidGlass**](https://jarx-concepts.github.io/react-native-platform-components/components/liquidglass) | `UIGlassEffect` on iOS 26+; `LiquidGlassContainer` merges and morphs it (`UIGlassContainerEffect`) | A fallback `View` | `@callstack/liquid-glass`, `expo-glass-effect` |
46
+ | Component | iOS | Android | Instead of |
47
+ | ---------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
48
+ | [**TextField**](https://jarx-concepts.github.io/react-native-platform-components/components/textfield) | `UITextField` / `UITextView`, with Writing Tools and the iOS 17–18 text traits | Material 3 `TextInputLayout`: outlined or filled, floating label, supporting text, counter | `TextInput` (a bare `EditText` on Android); `react-native-paper` (drawn in JavaScript) |
49
+ | [**DatePicker**](https://jarx-concepts.github.io/react-native-platform-components/components/datepicker) | `UIDatePicker`, modal or embedded | `MaterialDatePicker` / `MaterialTimePicker`, or the AppCompat dialogs | `@react-native-community/datetimepicker` (no Material 3 pickers) |
50
+ | [**SelectionMenu**](https://jarx-concepts.github.io/react-native-platform-components/components/selectionmenu) | System menus | Material exposed dropdown or `Spinner` | `@react-native-picker/picker` (wheel or dialog, no menu) |
51
+ | [**ContextMenu**](https://jarx-concepts.github.io/react-native-platform-components/components/contextmenu) | `UIContextMenuInteraction` | `PopupMenu` | `zeego` (through two more native packages) |
52
+ | [**ActionSheet**](https://jarx-concepts.github.io/react-native-platform-components/components/actionsheet) | `UIAlertController` (`.actionSheet`), a popover pointing at its trigger on iPad and iOS 26 | Material 3 modal bottom sheet (`BottomSheetDialog`) with icons | `react-native-actions-sheet` (drawn in JavaScript); `ActionSheetIOS` (iOS only, no testIDs or anchor) |
53
+ | [**SegmentedControl**](https://jarx-concepts.github.io/react-native-platform-components/components/segmentedcontrol) | `UISegmentedControl` | Material 3 Expressive connected buttons, or classic segmented buttons | `@react-native-segmented-control/segmented-control` (JavaScript on Android) |
54
+ | [**TabBar**](https://jarx-concepts.github.io/react-native-platform-components/components/tabbar) | `UITabBar`, the floating Liquid Glass bar on iOS 26 | Material 3 navigation bar | React Navigation's bottom tabs (JavaScript); `react-native-bottom-tabs` (tied to its navigator) |
55
+ | [**NavigationRail**](https://jarx-concepts.github.io/react-native-platform-components/components/navigationrail) | A column of tab-style `UIButton`s (iOS has no rail; use TabBar on iPhone) | Material 3 Expressive `NavigationRailView`: header, badges, menu gravity, expanded rail | React Navigation's drawer or a custom view (JavaScript) |
56
+ | [**Button**](https://jarx-concepts.github.io/react-native-platform-components/components/button) | `UIButton` configurations, Liquid Glass variants on iOS 26 | Material 3 Expressive `MaterialButton`: five sizes, shape morph | `react-native-paper` (Material 3, not Expressive, in JavaScript) |
57
+ | [**ButtonGroup**](https://jarx-concepts.github.io/react-native-platform-components/components/buttongroup) | A row of `UIButton`s with a "…" overflow menu; SplitButton: a button and a chevron `UIMenu` button | Material 3 Expressive button groups, single or multiple selection, overflow menu; `MaterialSplitButton` | `react-native-paper` segmented buttons (JavaScript) |
58
+ | [**FloatingActionButton**](https://jarx-concepts.github.io/react-native-platform-components/components/floatingactionbutton) | A round prominent `UIButton`, Liquid Glass on iOS 26 (iOS has no FAB) | Material `FloatingActionButton` / `ExtendedFloatingActionButton`: four sizes, shrink on scroll | `react-native-paper` FAB (drawn in JavaScript) |
59
+ | [**FloatingToolbar**](https://jarx-concepts.github.io/react-native-platform-components/components/floatingtoolbar) | Liquid Glass capsule on iOS 26, blur before | Material 3 Expressive `FloatingToolbarLayout` | A custom view |
60
+ | [**LiquidGlass**](https://jarx-concepts.github.io/react-native-platform-components/components/liquidglass) | `UIGlassEffect` on iOS 26+; `LiquidGlassContainer` merges and morphs it (`UIGlassContainerEffect`) | A fallback `View` | `@callstack/liquid-glass`, `expo-glass-effect` |
60
61
 
61
62
  One package and one API shape: platform-only props go under `ios={{ }}` and `android={{ }}`, with Fabric + Codegen typed bindings, an Expo config plugin and TypeScript, instead of four or five packages with their own conventions, install steps and upgrade cadences.
62
63
 
@@ -136,7 +137,7 @@ Full details: [Installation](https://jarx-concepts.github.io/react-native-platfo
136
137
  Everything else lives on the documentation site:
137
138
 
138
139
  - [Quick Start](https://jarx-concepts.github.io/react-native-platform-components/quick-start): copy-paste examples for every component
139
- - [DatePicker](https://jarx-concepts.github.io/react-native-platform-components/components/datepicker), [ContextMenu](https://jarx-concepts.github.io/react-native-platform-components/components/contextmenu), [SelectionMenu](https://jarx-concepts.github.io/react-native-platform-components/components/selectionmenu), [SegmentedControl](https://jarx-concepts.github.io/react-native-platform-components/components/segmentedcontrol), [Button](https://jarx-concepts.github.io/react-native-platform-components/components/button), [ButtonGroup](https://jarx-concepts.github.io/react-native-platform-components/components/buttongroup), [FloatingToolbar](https://jarx-concepts.github.io/react-native-platform-components/components/floatingtoolbar), [LiquidGlass](https://jarx-concepts.github.io/react-native-platform-components/components/liquidglass): props, platform options and behavior
140
+ - [DatePicker](https://jarx-concepts.github.io/react-native-platform-components/components/datepicker), [ContextMenu](https://jarx-concepts.github.io/react-native-platform-components/components/contextmenu), [ActionSheet](https://jarx-concepts.github.io/react-native-platform-components/components/actionsheet), [SelectionMenu](https://jarx-concepts.github.io/react-native-platform-components/components/selectionmenu), [SegmentedControl](https://jarx-concepts.github.io/react-native-platform-components/components/segmentedcontrol), [Button](https://jarx-concepts.github.io/react-native-platform-components/components/button), [ButtonGroup](https://jarx-concepts.github.io/react-native-platform-components/components/buttongroup), [FloatingToolbar](https://jarx-concepts.github.io/react-native-platform-components/components/floatingtoolbar), [LiquidGlass](https://jarx-concepts.github.io/react-native-platform-components/components/liquidglass): props, platform options and behavior
140
141
  - [Android Theme Configuration](https://jarx-concepts.github.io/react-native-platform-components/guides/android-theme): Material 3 setup and what happens without it
141
142
  - [Icons](https://jarx-concepts.github.io/react-native-platform-components/guides/icons): SF Symbols, drawables and images
142
143
  - [Theming and Colors](https://jarx-concepts.github.io/react-native-platform-components/guides/theming): light/dark mode and your brand color from JavaScript (`useNativeTheme`), accepted color formats
@@ -0,0 +1,309 @@
1
+ package com.platformcomponents
2
+
3
+ import android.content.Context
4
+ import android.content.res.ColorStateList
5
+ import android.graphics.drawable.Drawable
6
+ import android.util.Log
7
+ import android.util.TypedValue
8
+ import android.view.Gravity
9
+ import android.view.View
10
+ import android.view.ViewGroup
11
+ import android.widget.ImageView
12
+ import android.widget.LinearLayout
13
+ import android.widget.TextView
14
+ import androidx.core.view.ViewCompat
15
+ import androidx.core.widget.ImageViewCompat
16
+ import androidx.core.widget.NestedScrollView
17
+ import androidx.core.widget.TextViewCompat
18
+ import com.facebook.react.views.view.ReactViewGroup
19
+ import com.google.android.material.R as MaterialR
20
+ import com.google.android.material.bottomsheet.BottomSheetBehavior
21
+ import com.google.android.material.bottomsheet.BottomSheetDialog
22
+ import com.google.android.material.bottomsheet.BottomSheetDragHandleView
23
+ import kotlin.math.roundToInt
24
+
25
+ /**
26
+ * The ActionSheet: a Material 3 modal bottom sheet (`BottomSheetDialog`)
27
+ * with an optional title and message and a list of actions, shown while
28
+ * `visible` is "open". Following Material, there is no cancel row: a swipe
29
+ * down, a tap on the scrim and the back button dismiss it and report a
30
+ * cancel. The view itself only hosts the React children.
31
+ */
32
+ class PCActionSheetView(context: Context) : ReactViewGroup(context) {
33
+
34
+ companion object {
35
+ private const val TAG = "PCActionSheet"
36
+
37
+ // Material 3 list item metrics
38
+ private const val ROW_MIN_HEIGHT_DP = 56
39
+ private const val ROW_START_PADDING_DP = 16
40
+ private const val ROW_END_PADDING_DP = 24
41
+ private const val ICON_SIZE_DP = 24
42
+ private const val ICON_GAP_DP = 16
43
+ private const val HEADER_BOTTOM_PADDING_DP = 8
44
+ private const val LIST_BOTTOM_PADDING_DP = 8
45
+
46
+ // Material's disabled content alpha
47
+ private const val DISABLED_ALPHA = 0.38f
48
+ }
49
+
50
+ /** One action from the `actions` prop. */
51
+ data class Action(
52
+ val id: String,
53
+ val title: String,
54
+ val icon: PCButtonSupport.Icon,
55
+ val imageColor: String,
56
+ val destructive: Boolean,
57
+ val disabled: Boolean,
58
+ /** The action's own `haptics`; "" = the component's */
59
+ val haptics: String,
60
+ /** The row's view tag, which Detox's `by.id` matches */
61
+ val testID: String
62
+ )
63
+
64
+ // --- Props ---
65
+ var sheetTitle: String = ""
66
+ var message: String = ""
67
+ var actions: List<Action> = emptyList()
68
+ var visible: String = "closed" // "open" | "closed"
69
+ var maxFontSizeMultiplier: Float = 0f // < 1 = no cap
70
+
71
+ /** The `haptics` prop, played when an action is picked (an action's own `haptics` wins). */
72
+ var haptics: String = ""
73
+
74
+ // --- Events ---
75
+ var onSelect: ((id: String) -> Unit)? = null
76
+ var onCancel: (() -> Unit)? = null
77
+
78
+ // --- Internal ---
79
+ /** The sheet on screen, until a pick, a cancel or `visible` ends it */
80
+ private var dialog: BottomSheetDialog? = null
81
+ private var openToken = 0
82
+
83
+ // ---- Public apply* (called by manager) ----
84
+
85
+ fun applyVisible(value: String?) {
86
+ val next = if (value == "open") "open" else "closed"
87
+ if (next == visible) return
88
+ visible = next
89
+ openToken += 1
90
+ if (visible == "open") {
91
+ presentIfNeeded(openToken)
92
+ } else {
93
+ dismissIfNeeded()
94
+ }
95
+ }
96
+
97
+ // ---- Presentation ----
98
+
99
+ /** Presents after the current layout pass, so the props of this update are all in. */
100
+ private fun presentIfNeeded(token: Int) {
101
+ post {
102
+ if (token != openToken || visible != "open") return@post
103
+ if (dialog != null) return@post
104
+ if (!isAttachedToWindow) {
105
+ Log.d(TAG, "presentIfNeeded: not attached -> skip")
106
+ return@post
107
+ }
108
+ present()
109
+ }
110
+ }
111
+
112
+ /** Dismisses without a callback: neither onSelect nor onCancel fires. */
113
+ private fun dismissIfNeeded() {
114
+ val sheet = dialog ?: return
115
+ dialog = null
116
+ sheet.dismiss()
117
+ }
118
+
119
+ private fun present() {
120
+ val activity = PCNativeTheme.findActivity(context)
121
+ if (activity == null || activity.isFinishing) {
122
+ Log.w(TAG, "present: no activity")
123
+ return
124
+ }
125
+
126
+ // The app's Material theme, or Material 3 defaults when it has none;
127
+ // BottomSheetDialog adds the theme's bottomSheetDialogTheme overlay.
128
+ val sheet = BottomSheetDialog(PCThemeSupport.materialContext(activity, "ActionSheet"))
129
+ // Current before the rows are built: drawable icons resolve right away
130
+ // and check it.
131
+ dialog = sheet
132
+ sheet.setContentView(buildContent(sheet, sheet.context))
133
+
134
+ // Open fully, and a swipe down closes it instead of stopping half way.
135
+ sheet.behavior.skipCollapsed = true
136
+ sheet.behavior.state = BottomSheetBehavior.STATE_EXPANDED
137
+
138
+ // Swipe down, scrim tap and back: BottomSheetDialog cancels.
139
+ sheet.setOnCancelListener {
140
+ if (dialog !== sheet) return@setOnCancelListener
141
+ dialog = null
142
+ onCancel?.invoke()
143
+ }
144
+ sheet.setOnDismissListener {
145
+ if (dialog === sheet) dialog = null
146
+ }
147
+
148
+ sheet.show()
149
+ }
150
+
151
+ private fun buildContent(sheet: BottomSheetDialog, ctx: Context): View {
152
+ val density = ctx.resources.displayMetrics.density
153
+ fun dp(value: Int) = (value * density).roundToInt()
154
+
155
+ val root = LinearLayout(ctx).apply {
156
+ orientation = LinearLayout.VERTICAL
157
+ }
158
+
159
+ // Material 3 modal sheets carry a drag handle; it also gives TalkBack a
160
+ // way to dismiss the sheet.
161
+ root.addView(
162
+ BottomSheetDragHandleView(ctx),
163
+ LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
164
+ )
165
+
166
+ if (sheetTitle.isNotEmpty() || message.isNotEmpty()) {
167
+ val header = LinearLayout(ctx).apply {
168
+ orientation = LinearLayout.VERTICAL
169
+ setPadding(dp(ROW_START_PADDING_DP), 0, dp(ROW_END_PADDING_DP), dp(HEADER_BOTTOM_PADDING_DP))
170
+ }
171
+ if (sheetTitle.isNotEmpty()) {
172
+ header.addView(
173
+ TextView(ctx).apply {
174
+ applyTextAppearance(this, MaterialR.attr.textAppearanceTitleMedium)
175
+ PCMenuSupport.themeColor(ctx, MaterialR.attr.colorOnSurface)?.let { setTextColor(it) }
176
+ text = sheetTitle
177
+ ViewCompat.setAccessibilityHeading(this, true)
178
+ }
179
+ )
180
+ }
181
+ if (message.isNotEmpty()) {
182
+ header.addView(
183
+ TextView(ctx).apply {
184
+ applyTextAppearance(this, MaterialR.attr.textAppearanceBodyMedium)
185
+ PCMenuSupport.themeColor(ctx, MaterialR.attr.colorOnSurfaceVariant)?.let { setTextColor(it) }
186
+ text = message
187
+ if (sheetTitle.isNotEmpty()) setPadding(0, dp(4), 0, 0)
188
+ }
189
+ )
190
+ }
191
+ root.addView(header)
192
+ }
193
+
194
+ val list = LinearLayout(ctx).apply {
195
+ orientation = LinearLayout.VERTICAL
196
+ setPadding(0, 0, 0, dp(LIST_BOTTOM_PADDING_DP))
197
+ }
198
+ // Rows line up: with any icon, every row keeps the icon column.
199
+ val iconColumn = actions.any { it.icon.isPresent }
200
+ for (action in actions) {
201
+ list.addView(buildRow(sheet, ctx, action, iconColumn))
202
+ }
203
+
204
+ // Long lists scroll inside the sheet; the header and handle stay.
205
+ val scroll = NestedScrollView(ctx).apply {
206
+ isFillViewport = false
207
+ addView(list)
208
+ }
209
+ root.addView(
210
+ scroll,
211
+ LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
212
+ )
213
+
214
+ PCThemeSupport.capTextSizes(root, maxFontSizeMultiplier)
215
+ return root
216
+ }
217
+
218
+ private fun buildRow(sheet: BottomSheetDialog, ctx: Context, action: Action, iconColumn: Boolean): View {
219
+ val density = ctx.resources.displayMetrics.density
220
+ fun dp(value: Int) = (value * density).roundToInt()
221
+
222
+ val contentColor = if (action.destructive) {
223
+ PCMenuSupport.errorColor(ctx)
224
+ } else {
225
+ PCMenuSupport.themeColor(ctx, MaterialR.attr.colorOnSurface)
226
+ }
227
+
228
+ val row = LinearLayout(ctx).apply {
229
+ orientation = LinearLayout.HORIZONTAL
230
+ gravity = Gravity.CENTER_VERTICAL
231
+ minimumHeight = dp(ROW_MIN_HEIGHT_DP)
232
+ setPadding(dp(ROW_START_PADDING_DP), dp(8), dp(ROW_END_PADDING_DP), dp(8))
233
+ setBackgroundResource(resolveStyle(ctx, android.R.attr.selectableItemBackground))
234
+ isClickable = true
235
+ isFocusable = true
236
+ isEnabled = !action.disabled
237
+ tag = action.testID.ifEmpty { null }
238
+ }
239
+
240
+ if (iconColumn) {
241
+ val icon = ImageView(ctx).apply {
242
+ scaleType = ImageView.ScaleType.CENTER_INSIDE
243
+ importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
244
+ }
245
+ row.addView(
246
+ icon,
247
+ LinearLayout.LayoutParams(dp(ICON_SIZE_DP), dp(ICON_SIZE_DP)).apply {
248
+ marginEnd = dp(ICON_GAP_DP)
249
+ }
250
+ )
251
+ if (action.icon.isPresent) {
252
+ PCMenuSupport.loadIcon(ctx, action.icon) { drawable: Drawable? ->
253
+ if (drawable == null || dialog !== sheet) return@loadIcon
254
+ icon.setImageDrawable(drawable)
255
+ // An explicit imageColor wins; destructive icons take the error color.
256
+ val tint = when {
257
+ action.imageColor.isNotEmpty() -> ColorParser.parse(action.imageColor)
258
+ action.destructive -> PCMenuSupport.errorColor(ctx)
259
+ else -> null
260
+ }
261
+ if (tint != null) ImageViewCompat.setImageTintList(icon, ColorStateList.valueOf(tint))
262
+ }
263
+ }
264
+ if (action.disabled) icon.alpha = DISABLED_ALPHA
265
+ }
266
+
267
+ val label = TextView(ctx).apply {
268
+ applyTextAppearance(this, MaterialR.attr.textAppearanceBodyLarge)
269
+ contentColor?.let { setTextColor(it) }
270
+ text = action.title
271
+ if (action.disabled) alpha = DISABLED_ALPHA
272
+ }
273
+ row.addView(label, LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f))
274
+
275
+ row.setOnClickListener {
276
+ if (dialog !== sheet || action.disabled) return@setOnClickListener
277
+ dialog = null
278
+ PCHaptics.perform(this, action.haptics.ifEmpty { haptics })
279
+ sheet.dismiss()
280
+ onSelect?.invoke(action.id)
281
+ }
282
+ return row
283
+ }
284
+
285
+ /** Applies the theme's text appearance [attr], when the theme has one. */
286
+ private fun applyTextAppearance(view: TextView, attr: Int) {
287
+ val style = resolveStyle(view.context, attr)
288
+ if (style != 0) TextViewCompat.setTextAppearance(view, style)
289
+ }
290
+
291
+ /** The resource id an attribute points at (a style or a drawable), or 0. */
292
+ private fun resolveStyle(ctx: Context, attr: Int): Int {
293
+ val value = TypedValue()
294
+ return if (ctx.theme.resolveAttribute(attr, value, true)) value.resourceId else 0
295
+ }
296
+
297
+ override fun onAttachedToWindow() {
298
+ super.onAttachedToWindow()
299
+ // A sheet asked for before the view was on screen opens now.
300
+ if (visible == "open" && dialog == null) presentIfNeeded(++openToken)
301
+ }
302
+
303
+ override fun onDetachedFromWindow() {
304
+ // Unmounted, or its screen went away: the sheet goes too, without a callback.
305
+ openToken += 1
306
+ dismissIfNeeded()
307
+ super.onDetachedFromWindow()
308
+ }
309
+ }
@@ -0,0 +1,131 @@
1
+ package com.platformcomponents
2
+
3
+ import android.view.View
4
+ import com.facebook.react.bridge.Arguments
5
+ import com.facebook.react.bridge.ReadableArray
6
+ import com.facebook.react.bridge.WritableMap
7
+ import com.facebook.react.uimanager.ThemedReactContext
8
+ import com.facebook.react.uimanager.UIManagerHelper
9
+ import com.facebook.react.uimanager.ViewGroupManager
10
+ import com.facebook.react.uimanager.ViewManagerDelegate
11
+ import com.facebook.react.uimanager.events.Event
12
+ import com.facebook.react.viewmanagers.PCActionSheetManagerDelegate
13
+ import com.facebook.react.viewmanagers.PCActionSheetManagerInterface
14
+ import com.platformcomponents.PCButtonSupport.stringOr
15
+
16
+ class PCActionSheetViewManager :
17
+ ViewGroupManager<PCActionSheetView>(),
18
+ PCActionSheetManagerInterface<PCActionSheetView> {
19
+
20
+ private val delegate: ViewManagerDelegate<PCActionSheetView> =
21
+ PCActionSheetManagerDelegate(this)
22
+
23
+ override fun getName(): String = "PCActionSheet"
24
+
25
+ override fun getDelegate(): ViewManagerDelegate<PCActionSheetView> = delegate
26
+
27
+ override fun createViewInstance(reactContext: ThemedReactContext): PCActionSheetView =
28
+ PCActionSheetView(reactContext)
29
+
30
+ override fun addEventEmitters(reactContext: ThemedReactContext, view: PCActionSheetView) {
31
+ view.onSelect = { id ->
32
+ UIManagerHelper.getEventDispatcherForReactTag(reactContext, view.id)?.dispatchEvent(
33
+ SelectEvent(UIManagerHelper.getSurfaceId(view), view.id, id)
34
+ )
35
+ }
36
+ view.onCancel = {
37
+ UIManagerHelper.getEventDispatcherForReactTag(reactContext, view.id)?.dispatchEvent(
38
+ CancelEvent(UIManagerHelper.getSurfaceId(view), view.id)
39
+ )
40
+ }
41
+ }
42
+
43
+ // "open" | "closed"
44
+ override fun setVisible(view: PCActionSheetView, value: String?) {
45
+ view.applyVisible(value)
46
+ }
47
+
48
+ override fun setTitle(view: PCActionSheetView, value: String?) {
49
+ view.sheetTitle = value ?: ""
50
+ }
51
+
52
+ override fun setMessage(view: PCActionSheetView, value: String?) {
53
+ view.message = value ?: ""
54
+ }
55
+
56
+ // actions: [{id, title, icon fields, imageColor, destructive, disabled, haptics, testID}]
57
+ override fun setActions(view: PCActionSheetView, value: ReadableArray?) {
58
+ val out = ArrayList<PCActionSheetView.Action>()
59
+ if (value != null) {
60
+ for (i in 0 until value.size()) {
61
+ val m = value.getMap(i) ?: continue
62
+ out.add(
63
+ PCActionSheetView.Action(
64
+ id = m.stringOr("id", ""),
65
+ title = m.stringOr("title", ""),
66
+ icon = PCButtonSupport.parseIcon(m),
67
+ imageColor = m.stringOr("imageColor", ""),
68
+ destructive = m.stringOr("destructive", "false") == "true",
69
+ disabled = m.stringOr("disabled", "false") == "true",
70
+ haptics = m.stringOr("haptics", ""),
71
+ testID = m.stringOr("testID", "")
72
+ )
73
+ )
74
+ }
75
+ }
76
+ view.actions = out
77
+ }
78
+
79
+ // iOS only: Material bottom sheets have no cancel row
80
+ override fun setCancelTitle(view: PCActionSheetView, value: String?) {}
81
+
82
+ override fun setCancelTestID(view: PCActionSheetView, value: String?) {}
83
+
84
+ // "" (none added) | "none" | "selection" | "light" | "medium" | "heavy" | "success" | "warning" | "error"
85
+ override fun setHaptics(view: PCActionSheetView, value: String?) {
86
+ view.haptics = PCHaptics.configure(view, value)
87
+ }
88
+
89
+ override fun setMaxFontSizeMultiplier(view: PCActionSheetView, value: Double) {
90
+ view.maxFontSizeMultiplier = value.toFloat()
91
+ }
92
+
93
+ // --- ViewGroupManager child management ---
94
+ override fun addView(parent: PCActionSheetView, child: View, index: Int) {
95
+ parent.addView(child, index)
96
+ }
97
+
98
+ override fun removeViewAt(parent: PCActionSheetView, index: Int) {
99
+ parent.removeViewAt(index)
100
+ }
101
+
102
+ override fun getChildCount(parent: PCActionSheetView): Int = parent.childCount
103
+
104
+ override fun getChildAt(parent: PCActionSheetView, index: Int): View? = parent.getChildAt(index)
105
+
106
+ // --- Events ---
107
+
108
+ private class SelectEvent(
109
+ surfaceId: Int,
110
+ viewTag: Int,
111
+ private val actionId: String
112
+ ) : Event<SelectEvent>(surfaceId, viewTag) {
113
+ // Every pick counts
114
+ override fun canCoalesce(): Boolean = false
115
+
116
+ override fun getEventName(): String = "topSelect"
117
+
118
+ // Fabric reads the payload from here
119
+ override fun getEventData(): WritableMap = Arguments.createMap().apply {
120
+ putString("actionId", actionId)
121
+ }
122
+ }
123
+
124
+ private class CancelEvent(surfaceId: Int, viewTag: Int) : Event<CancelEvent>(surfaceId, viewTag) {
125
+ override fun canCoalesce(): Boolean = false
126
+
127
+ override fun getEventName(): String = "topCancel"
128
+
129
+ override fun getEventData(): WritableMap = Arguments.createMap()
130
+ }
131
+ }
@@ -465,7 +465,10 @@ class PCButtonGroupView(context: Context) :
465
465
  popup.setOnMenuItemClickListener { menuItem ->
466
466
  // Submenu headers open their submenu; they aren't picks
467
467
  val item = PCMenuSupport.itemFor(menuItem, items)
468
- if (item == null || !item.isAction) return@setOnMenuItemClickListener false
468
+ if (item == null || !item.isAction) {
469
+ post { PCMenuSupport.tagPopupRows(popup.menu, items) }
470
+ return@setOnMenuItemClickListener false
471
+ }
469
472
  PCHaptics.perform(this, item.haptics.ifEmpty { haptics })
470
473
  onMenuSelect?.invoke(item.id, item.title)
471
474
  true
@@ -479,6 +482,7 @@ class PCButtonGroupView(context: Context) :
479
482
 
480
483
  onMenuOpen?.invoke()
481
484
  popup.show()
485
+ PCMenuSupport.tagPopupRows(popup.menu, items)
482
486
  }
483
487
 
484
488
  private fun scheduleSelectionEmit() {
@@ -402,7 +402,10 @@ class PCButtonView(context: Context) :
402
402
  popup.setOnMenuItemClickListener { menuItem ->
403
403
  // Submenu headers open their submenu; they aren't picks
404
404
  val item = PCMenuSupport.itemFor(menuItem, items)
405
- if (item == null || !item.isAction) return@setOnMenuItemClickListener false
405
+ if (item == null || !item.isAction) {
406
+ post { PCMenuSupport.tagPopupRows(popup.menu, items) }
407
+ return@setOnMenuItemClickListener false
408
+ }
406
409
  PCHaptics.perform(this, item.haptics.ifEmpty { haptics })
407
410
  onMenuSelect?.invoke(item.id, item.title)
408
411
  true
@@ -414,6 +417,7 @@ class PCButtonView(context: Context) :
414
417
 
415
418
  onMenuOpen?.invoke()
416
419
  popup.show()
420
+ PCMenuSupport.tagPopupRows(popup.menu, items)
417
421
  }
418
422
 
419
423
  // ---- Layout ----