react-native-platform-components 0.8.9 → 0.9.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 +126 -30
- package/android/src/main/java/com/platformcomponents/PCImageLoader.kt +88 -0
- package/android/src/main/java/com/platformcomponents/PCSegmentedControlView.kt +288 -29
- package/android/src/main/java/com/platformcomponents/PCSegmentedControlViewManager.kt +74 -8
- package/ios/PCConstants.swift +39 -3
- package/ios/PCImageLoader.swift +58 -0
- package/ios/PCSegmentedControl.mm +85 -25
- package/ios/PCSegmentedControl.swift +231 -308
- package/ios/PCSelectionMenu.swift +60 -14
- package/lib/commonjs/DatePickerNativeComponent.ts +4 -2
- package/lib/commonjs/SegmentedControl.js +138 -6
- package/lib/commonjs/SegmentedControl.js.map +1 -1
- package/lib/commonjs/SegmentedControlNativeComponent.ts +62 -8
- package/lib/commonjs/SelectionMenuNativeComponent.ts +4 -2
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/module/DatePickerNativeComponent.ts +4 -2
- package/lib/module/SegmentedControl.js +138 -7
- package/lib/module/SegmentedControl.js.map +1 -1
- package/lib/module/SegmentedControlNativeComponent.ts +62 -8
- package/lib/module/SelectionMenuNativeComponent.ts +4 -2
- package/lib/module/index.web.js.map +1 -1
- package/lib/typescript/commonjs/src/DatePickerNativeComponent.d.ts +2 -2
- package/lib/typescript/commonjs/src/DatePickerNativeComponent.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/SegmentedControl.d.ts +111 -6
- package/lib/typescript/commonjs/src/SegmentedControl.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/SegmentedControlNativeComponent.d.ts +46 -7
- package/lib/typescript/commonjs/src/SegmentedControlNativeComponent.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/SelectionMenuNativeComponent.d.ts +2 -2
- package/lib/typescript/commonjs/src/SelectionMenuNativeComponent.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/index.web.d.ts +36 -1
- package/lib/typescript/commonjs/src/index.web.d.ts.map +1 -1
- package/lib/typescript/module/src/DatePicker.d.ts +1 -1
- package/lib/typescript/module/src/DatePicker.d.ts.map +1 -1
- package/lib/typescript/module/src/DatePickerNativeComponent.d.ts +2 -2
- package/lib/typescript/module/src/DatePickerNativeComponent.d.ts.map +1 -1
- package/lib/typescript/module/src/SegmentedControl.d.ts +111 -6
- package/lib/typescript/module/src/SegmentedControl.d.ts.map +1 -1
- package/lib/typescript/module/src/SegmentedControlNativeComponent.d.ts +46 -7
- package/lib/typescript/module/src/SegmentedControlNativeComponent.d.ts.map +1 -1
- package/lib/typescript/module/src/SelectionMenu.d.ts +1 -1
- package/lib/typescript/module/src/SelectionMenu.d.ts.map +1 -1
- package/lib/typescript/module/src/SelectionMenuNativeComponent.d.ts +2 -2
- package/lib/typescript/module/src/SelectionMenuNativeComponent.d.ts.map +1 -1
- package/lib/typescript/module/src/index.d.ts +6 -6
- package/lib/typescript/module/src/index.d.ts.map +1 -1
- package/lib/typescript/module/src/index.web.d.ts +36 -1
- package/lib/typescript/module/src/index.web.d.ts.map +1 -1
- package/package.json +30 -25
- package/plugin/tsconfig.json +2 -2
- package/src/DatePickerNativeComponent.ts +4 -2
- package/src/SegmentedControl.tsx +238 -11
- package/src/SegmentedControlNativeComponent.ts +62 -8
- package/src/SelectionMenuNativeComponent.ts +4 -2
- package/src/index.web.tsx +34 -1
package/README.md
CHANGED
|
@@ -371,18 +371,22 @@ export function Example() {
|
|
|
371
371
|
|
|
372
372
|
```tsx
|
|
373
373
|
import { SegmentedControl } from 'react-native-platform-components';
|
|
374
|
-
import { Platform } from 'react-native';
|
|
375
374
|
|
|
376
375
|
const segments = [
|
|
377
376
|
{
|
|
378
377
|
label: 'List',
|
|
379
378
|
value: 'list',
|
|
380
|
-
|
|
379
|
+
// Native symbol on each platform, no Platform.OS branching
|
|
380
|
+
icon: {
|
|
381
|
+
ios: { type: 'sfSymbol', name: 'list.bullet' },
|
|
382
|
+
android: { type: 'drawable', name: 'list_bullet' },
|
|
383
|
+
},
|
|
381
384
|
},
|
|
382
385
|
{
|
|
383
|
-
label: '
|
|
384
|
-
value: '
|
|
385
|
-
|
|
386
|
+
label: 'Alerts',
|
|
387
|
+
value: 'alerts',
|
|
388
|
+
// One image asset shared by both platforms, tinted like a template
|
|
389
|
+
icon: { type: 'image', source: require('./bell.png') },
|
|
386
390
|
},
|
|
387
391
|
];
|
|
388
392
|
|
|
@@ -394,6 +398,8 @@ export function Example() {
|
|
|
394
398
|
segments={segments}
|
|
395
399
|
selectedValue={selected}
|
|
396
400
|
onSelect={(value) => setSelected(value)}
|
|
401
|
+
// 'auto' | 'labeled' | 'unlabeled'
|
|
402
|
+
labelVisibility="auto"
|
|
397
403
|
ios={{ apportionsSegmentWidthsByContent: true }}
|
|
398
404
|
/>
|
|
399
405
|
);
|
|
@@ -611,42 +617,132 @@ Native segmented control using **UISegmentedControl** on iOS and **MaterialButto
|
|
|
611
617
|
|
|
612
618
|
### Props
|
|
613
619
|
|
|
614
|
-
| Prop
|
|
615
|
-
|
|
|
616
|
-
| `segments`
|
|
617
|
-
| `selectedValue`
|
|
618
|
-
| `disabled`
|
|
619
|
-
| `
|
|
620
|
+
| Prop | Type | Description |
|
|
621
|
+
| ----------------- | ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
622
|
+
| `segments` | `SegmentedControlSegment[]` | Array of segments to display |
|
|
623
|
+
| `selectedValue` | `string \| null` | Currently selected segment's `value` |
|
|
624
|
+
| `disabled` | `boolean` | Disables the entire control |
|
|
625
|
+
| `labelVisibility` | `'auto' \| 'labeled' \| 'unlabeled'` | How labels and icons combine. See [Label visibility](#label-visibility). Default: `'auto'` |
|
|
626
|
+
| `selectedSegmentColor` | `ColorValue` | Background of the selected segment |
|
|
627
|
+
| `activeTintColor` | `ColorValue` | Text and icon color of the selected segment |
|
|
628
|
+
| `inactiveTintColor` | `ColorValue` | Text and icon color of unselected segments |
|
|
629
|
+
| `labelStyle` | `{ fontFamily?, fontSize?, fontWeight?, fontStyle? }` | Font for segment labels. See [Styling](#styling) |
|
|
630
|
+
| `badgeStyle` | `{ backgroundColor?, color? }` | Colors for segment badges. See [Badges](#badges) |
|
|
631
|
+
| `onSelect` | `(value: string, index: number) => void` | Called when user selects a segment |
|
|
632
|
+
| `onDeselect` | `() => void` | Called when the user clears the selection by tapping the selected segment. Android only; requires `android.selectionRequired: false` |
|
|
620
633
|
|
|
621
634
|
### SegmentedControlSegment
|
|
622
635
|
|
|
623
|
-
| Property
|
|
624
|
-
|
|
|
625
|
-
| `label`
|
|
626
|
-
| `value`
|
|
627
|
-
| `disabled`
|
|
628
|
-
| `icon`
|
|
636
|
+
| Property | Type | Description |
|
|
637
|
+
| -------------------- | ---------------------- | ----------------------------------------------------------------------------------------------------------------- |
|
|
638
|
+
| `label` | `string` | Display text for the segment |
|
|
639
|
+
| `value` | `string` | Unique value returned in callbacks |
|
|
640
|
+
| `disabled` | `boolean` | Disables this specific segment |
|
|
641
|
+
| `icon` | `SegmentedControlIcon` | Optional icon. See [Icon Support](#icon-support-1) |
|
|
642
|
+
| `badge` | `string \| number` | Badge at the segment's top-right corner, e.g. an unread count. See [Badges](#badges) |
|
|
643
|
+
| `accessibilityLabel` | `string` | Screen-reader label. Defaults to `label`. On iOS it applies to icon segments; text segments announce their title |
|
|
629
644
|
|
|
630
645
|
### iOS Props (`ios`)
|
|
631
646
|
|
|
632
|
-
| Prop | Type | Description
|
|
633
|
-
| ---------------------------------- | --------- |
|
|
634
|
-
| `momentary` | `boolean` | If true, segments don't show selected state
|
|
635
|
-
| `apportionsSegmentWidthsByContent` | `boolean` | If true, segment widths are proportional to content
|
|
636
|
-
| `selectedSegmentTintColor` | `string` |
|
|
647
|
+
| Prop | Type | Description |
|
|
648
|
+
| ---------------------------------- | --------- | --------------------------------------------------------------------------- |
|
|
649
|
+
| `momentary` | `boolean` | If true, segments don't show selected state |
|
|
650
|
+
| `apportionsSegmentWidthsByContent` | `boolean` | If true, segment widths are proportional to content |
|
|
651
|
+
| `selectedSegmentTintColor` | `string` | **Deprecated.** Use `selectedSegmentColor`, which works on both platforms. |
|
|
637
652
|
|
|
638
653
|
### Android Props (`android`)
|
|
639
654
|
|
|
640
|
-
| Prop | Type
|
|
641
|
-
| ------------------- |
|
|
642
|
-
| `selectionRequired` | `boolean`
|
|
655
|
+
| Prop | Type | Description |
|
|
656
|
+
| ------------------- | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
657
|
+
| `selectionRequired` | `boolean` | If true (default), one segment must always be selected. Set to `false` to let a tap on the selected segment clear it and fire `onDeselect`. |
|
|
658
|
+
| `rippleColor` | `ColorValue` | Ripple shown while pressing a segment |
|
|
659
|
+
| `strokeColor` | `ColorValue` | Outline color of the segments |
|
|
643
660
|
|
|
644
661
|
### Icon Support
|
|
645
662
|
|
|
646
|
-
|
|
663
|
+
`icon` accepts a single source, or an `{ ios, android }` pair so you never branch on `Platform.OS`:
|
|
664
|
+
|
|
665
|
+
| Shape | iOS | Android |
|
|
666
|
+
| ------------------------------------------- | ----------------------------------- | --------------------------------------------- |
|
|
667
|
+
| `'name'` (string) | SF Symbol name | Drawable resource name |
|
|
668
|
+
| `{ type: 'sfSymbol', name }` | SF Symbol | Ignored (segment shows its label) |
|
|
669
|
+
| `{ type: 'drawable', name }` | Ignored (segment shows its label) | Drawable from `res/drawable` |
|
|
670
|
+
| `{ type: 'image', source, tinted? }` | Image asset or `{ uri }` | Image asset or `{ uri }` |
|
|
671
|
+
| `{ ios: <source>, android: <source> }` | Uses `ios` | Uses `android` |
|
|
672
|
+
|
|
673
|
+
```tsx
|
|
674
|
+
// A bundled asset works everywhere and is tinted like a template.
|
|
675
|
+
icon: { type: 'image', source: require('./icons/bell.png') }
|
|
676
|
+
|
|
677
|
+
// Keep the original colors of a full-color image.
|
|
678
|
+
icon: { type: 'image', source: require('./icons/logo.png'), tinted: false }
|
|
679
|
+
|
|
680
|
+
// Remote images load asynchronously; pass `scale` for @2x/@3x artwork.
|
|
681
|
+
icon: { type: 'image', source: { uri: 'https://example.com/icon@2x.png', scale: 2 } }
|
|
682
|
+
```
|
|
683
|
+
|
|
684
|
+
Image icons render at their point size, so ship `@2x` / `@3x` variants sized around 18–22 points. Local assets load synchronously in release builds; in development they stream from Metro and the segment shows its label until the image arrives.
|
|
685
|
+
|
|
686
|
+
### Label visibility
|
|
687
|
+
|
|
688
|
+
`UISegmentedControl` shows either a title or an image per segment, while Material buttons can show both. `labelVisibility` makes the outcome predictable:
|
|
689
|
+
|
|
690
|
+
| Value | iOS | Android |
|
|
691
|
+
| --------------------- | ------------------------------------------ | -------------------------- |
|
|
692
|
+
| `'auto'` (default) | Icon when the segment has one, else label | Icon and label |
|
|
693
|
+
| `'labeled'` | Label only (icon is not shown) | Icon and label |
|
|
694
|
+
| `'unlabeled'` | Icon when the segment has one, else label | Icon only, else label |
|
|
695
|
+
|
|
696
|
+
Screen readers announce the label (or `accessibilityLabel`) in every mode on both platforms.
|
|
697
|
+
|
|
698
|
+
### Styling
|
|
699
|
+
|
|
700
|
+
Colors take any React Native `ColorValue` (hex, `rgba()`, named colors, `PlatformColor`, `DynamicColorIOS`). Fonts follow the `Text` style conventions, and each field falls back to the platform default.
|
|
701
|
+
|
|
702
|
+
```tsx
|
|
703
|
+
<SegmentedControl
|
|
704
|
+
segments={segments}
|
|
705
|
+
selectedValue={selected}
|
|
706
|
+
onSelect={setSelected}
|
|
707
|
+
selectedSegmentColor="#FF6B35"
|
|
708
|
+
activeTintColor="white"
|
|
709
|
+
inactiveTintColor="#8E8E93"
|
|
710
|
+
labelStyle={{ fontWeight: '700', fontSize: 14 }}
|
|
711
|
+
android={{ rippleColor: 'rgba(255, 107, 53, 0.25)', strokeColor: '#FF6B35' }}
|
|
712
|
+
/>
|
|
713
|
+
```
|
|
714
|
+
|
|
715
|
+
| Prop | iOS | Android |
|
|
716
|
+
| ---------------------- | -------------------------------------------- | -------------------------------------------- |
|
|
717
|
+
| `selectedSegmentColor` | `selectedSegmentTintColor` (the pill) | Checked button background |
|
|
718
|
+
| `activeTintColor` | Selected title / template image color | Checked button text and icon tint |
|
|
719
|
+
| `inactiveTintColor` | Normal title / template image color | Unchecked button text and icon tint |
|
|
720
|
+
| `labelStyle` | Title font (default: 13pt system) | Button typeface and size (default: theme) |
|
|
721
|
+
| `android.rippleColor` | — | Press ripple |
|
|
722
|
+
| `android.strokeColor` | — | Button outline |
|
|
723
|
+
|
|
724
|
+
Images with `tinted: false` keep their own colors and ignore the tint props.
|
|
725
|
+
|
|
726
|
+
### Badges
|
|
727
|
+
|
|
728
|
+
Give a segment a `badge` to show a count or short status at its top-right corner. Numbers render as-is, so format them yourself (`'99+'`); `undefined` hides the badge.
|
|
729
|
+
|
|
730
|
+
```tsx
|
|
731
|
+
<SegmentedControl
|
|
732
|
+
segments={[
|
|
733
|
+
{ label: 'Inbox', value: 'inbox', badge: unreadCount || undefined },
|
|
734
|
+
{ label: 'Sent', value: 'sent' },
|
|
735
|
+
{ label: 'Drafts', value: 'drafts', badge: 'new' },
|
|
736
|
+
]}
|
|
737
|
+
selectedValue={mailbox}
|
|
738
|
+
onSelect={setMailbox}
|
|
739
|
+
badgeStyle={{ backgroundColor: '#5856D6', color: 'white' }}
|
|
740
|
+
/>
|
|
741
|
+
```
|
|
647
742
|
|
|
648
|
-
- **iOS**:
|
|
649
|
-
- **Android**:
|
|
743
|
+
- **iOS**: a capsule label drawn over the segment (UISegmentedControl has no badge API). Defaults to system red with white text.
|
|
744
|
+
- **Android**: a Material `BadgeDrawable` attached to the button. Defaults to the theme's error color.
|
|
745
|
+
- Screen readers announce the badge with the segment label ("Inbox, 3").
|
|
650
746
|
|
|
651
747
|
---
|
|
652
748
|
|
|
@@ -746,7 +842,7 @@ All color props in this library support the same formats as React Native's `back
|
|
|
746
842
|
**Props that accept colors:**
|
|
747
843
|
|
|
748
844
|
- `ContextMenu`: `imageColor` (icon tint)
|
|
749
|
-
- `SegmentedControl`: `
|
|
845
|
+
- `SegmentedControl`: `selectedSegmentColor`, `activeTintColor`, `inactiveTintColor`, `badgeStyle.backgroundColor`, `badgeStyle.color`, `android.rippleColor`, `android.strokeColor` (these also accept `PlatformColor` / `DynamicColorIOS`)
|
|
750
846
|
- `LiquidGlass`: `ios.tintColor`, `android.fallbackBackgroundColor`
|
|
751
847
|
|
|
752
848
|
```tsx
|
|
@@ -897,7 +993,7 @@ Then run `npx expo prebuild` to apply the configuration.
|
|
|
897
993
|
|
|
898
994
|
## Icons
|
|
899
995
|
|
|
900
|
-
ContextMenu supports icons on menu items. Icons are specified by name and resolved differently on each platform.
|
|
996
|
+
ContextMenu supports icons on menu items. Icons are specified by name and resolved differently on each platform. SegmentedControl accepts the same names, plus image assets and per-platform pairs; see [SegmentedControl Icon Support](#icon-support-1).
|
|
901
997
|
|
|
902
998
|
### iOS
|
|
903
999
|
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
package com.platformcomponents
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.graphics.Bitmap
|
|
5
|
+
import android.graphics.BitmapFactory
|
|
6
|
+
import android.net.Uri
|
|
7
|
+
import android.os.Handler
|
|
8
|
+
import android.os.Looper
|
|
9
|
+
import android.util.Base64
|
|
10
|
+
import android.util.DisplayMetrics
|
|
11
|
+
import android.util.Log
|
|
12
|
+
import android.util.LruCache
|
|
13
|
+
import java.net.HttpURLConnection
|
|
14
|
+
import java.net.URL
|
|
15
|
+
import java.util.concurrent.Executors
|
|
16
|
+
import kotlin.math.roundToInt
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Loads images referenced by React Native asset URIs for native controls that
|
|
20
|
+
* take a Drawable (segmented control icons).
|
|
21
|
+
*
|
|
22
|
+
* Release builds ship local assets as drawable resources, which callers resolve
|
|
23
|
+
* directly. Debug builds (Metro), `file://`, `content://`, `data:` and remote
|
|
24
|
+
* sources go through here on a background thread and are cached in memory.
|
|
25
|
+
*/
|
|
26
|
+
object PCImageLoader {
|
|
27
|
+
private const val TAG = "PCImageLoader"
|
|
28
|
+
private const val TIMEOUT_MS = 10_000
|
|
29
|
+
|
|
30
|
+
private val executor = Executors.newFixedThreadPool(2)
|
|
31
|
+
private val mainHandler = Handler(Looper.getMainLooper())
|
|
32
|
+
private val cache = LruCache<String, Bitmap>(32)
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Loads [uri] and delivers the bitmap on the main thread. [scale] is the
|
|
36
|
+
* asset scale React Native resolved (2 for an `@2x` file), used to set the
|
|
37
|
+
* bitmap density so the drawable measures in dp like an `Image` would.
|
|
38
|
+
*
|
|
39
|
+
* Cached images are delivered synchronously.
|
|
40
|
+
*/
|
|
41
|
+
fun load(context: Context, uri: String, scale: Float, callback: (Bitmap?) -> Unit) {
|
|
42
|
+
val key = "$uri@${scale}x"
|
|
43
|
+
cache.get(key)?.let {
|
|
44
|
+
callback(it)
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
val appContext = context.applicationContext
|
|
49
|
+
executor.execute {
|
|
50
|
+
val bitmap = try {
|
|
51
|
+
decode(appContext, uri)
|
|
52
|
+
} catch (e: Exception) {
|
|
53
|
+
Log.w(TAG, "Failed to load $uri: ${e.message}")
|
|
54
|
+
null
|
|
55
|
+
}
|
|
56
|
+
if (bitmap != null) {
|
|
57
|
+
bitmap.density = (scale.coerceAtLeast(0.01f) * DisplayMetrics.DENSITY_DEFAULT).roundToInt()
|
|
58
|
+
cache.put(key, bitmap)
|
|
59
|
+
}
|
|
60
|
+
mainHandler.post { callback(bitmap) }
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private fun decode(context: Context, uri: String): Bitmap? {
|
|
65
|
+
val parsed = Uri.parse(uri)
|
|
66
|
+
return when (parsed.scheme?.lowercase()) {
|
|
67
|
+
"http", "https" -> {
|
|
68
|
+
val connection = URL(uri).openConnection() as HttpURLConnection
|
|
69
|
+
connection.connectTimeout = TIMEOUT_MS
|
|
70
|
+
connection.readTimeout = TIMEOUT_MS
|
|
71
|
+
try {
|
|
72
|
+
connection.inputStream.use { BitmapFactory.decodeStream(it) }
|
|
73
|
+
} finally {
|
|
74
|
+
connection.disconnect()
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
"file" -> parsed.path?.let { BitmapFactory.decodeFile(it) }
|
|
78
|
+
"content" -> context.contentResolver.openInputStream(parsed)?.use { BitmapFactory.decodeStream(it) }
|
|
79
|
+
"data" -> {
|
|
80
|
+
val comma = uri.indexOf(',')
|
|
81
|
+
if (comma < 0) return null
|
|
82
|
+
val bytes = Base64.decode(uri.substring(comma + 1), Base64.DEFAULT)
|
|
83
|
+
BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
|
|
84
|
+
}
|
|
85
|
+
else -> null
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|