react-native-drum-picker 0.1.4 → 0.1.5

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/CHANGELOG.md CHANGED
@@ -9,6 +9,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
9
9
 
10
10
  _No changes yet._
11
11
 
12
+ ## [0.1.5] - 2026-05-22
13
+
14
+ ### Fixed
15
+
16
+ - Center `selectedIndex` reliably using `scrollToPositionWithOffset` with a computed center offset (after items, layout, and metric changes).
17
+ - Cancel pending scroll work and `stopScroll()` on detach; broader lifecycle guards for navigation transitions.
18
+
19
+ ### Changed
20
+
21
+ - JS `DrumPicker` applies default `minWidth` / `height` (or `minHeight` with flex) so the picker is visible without manual sizing.
22
+ - Android native `minimumWidth` / `minimumHeight` aligned with `itemHeight * visibleItemCount`.
23
+ - One-time `__DEV__` warning when layout dimensions are missing.
24
+
25
+ ### Documentation
26
+
27
+ - README: tested compatibility matrix, practical examples, `onChange` / debounce guidance.
28
+ - Example app: basic, time, height/weight, date, controlled, debounced demos.
29
+ - Bug report template: Expo, react-native-screens, navigation crash, empty picker.
30
+
12
31
  ## [0.1.4] - 2026-05-22
13
32
 
14
33
  ### Fixed
@@ -63,7 +82,8 @@ First release on [npm](https://www.npmjs.com/package/react-native-drum-picker).
63
82
 
64
83
  Initial GitHub release. See [v0.0.1](https://github.com/scrollDynasty/react-native-drum-picker/releases/tag/v0.0.1).
65
84
 
66
- [Unreleased]: https://github.com/scrollDynasty/react-native-drum-picker/compare/v0.1.4...HEAD
85
+ [Unreleased]: https://github.com/scrollDynasty/react-native-drum-picker/compare/v0.1.5...HEAD
86
+ [0.1.5]: https://github.com/scrollDynasty/react-native-drum-picker/releases/tag/v0.1.5
67
87
  [0.1.4]: https://github.com/scrollDynasty/react-native-drum-picker/releases/tag/v0.1.4
68
88
  [0.1.2]: https://github.com/scrollDynasty/react-native-drum-picker/releases/tag/v0.1.2
69
89
  [0.1.1]: https://github.com/scrollDynasty/react-native-drum-picker/releases/tag/v0.1.1
package/README.md CHANGED
@@ -67,22 +67,27 @@ Requires **React Native 0.76+** with the **New Architecture** enabled.
67
67
  | React Native New Architecture | **Required** |
68
68
  | Fabric | **Required** |
69
69
  | Android | Supported |
70
- | iOS | Not supported yet |
71
- | Expo SDK 54 | Supported via **prebuild** / development build |
72
- | React Native 0.81+ | Supported |
73
- | Expo Go | **Not supported** (native module) |
70
+ | iOS | **Not supported yet** |
71
+ | Expo Go | **Not supported** (native Android library) |
72
+ | Expo SDK 54 + dev build / prebuild | Tested |
73
+ | `react-native-screens` navigation | Tested (use **0.1.4+** for detach safety) |
74
74
 
75
- This package is an **Android Fabric View** library. New Architecture must be enabled (`newArchEnabled: true` in Expo, or equivalent in bare React Native).
75
+ ### Tested with
76
76
 
77
- Recommended: **React Native >= 0.76**. Use a **development build** or `expo run:android` after `expo prebuild` — not Expo Go.
77
+ | Tool | Version |
78
+ |------|---------|
79
+ | Expo SDK | 54 |
80
+ | React Native | 0.81.5, 0.85.0 (example app) |
81
+ | New Architecture | enabled |
82
+ | Android | emulator / device |
78
83
 
79
- ### React Native 0.81+ event dispatch
84
+ **Intended range:** `react-native >= 0.76` with New Architecture. The package is **actively tested on RN 0.81.x / 0.85.x**. Older 0.76–0.80 may work but are not CI-guaranteed.
80
85
 
81
- If Android Kotlin compile fails with:
86
+ This package is an **Android Fabric View** library. Use a **development build** or `expo run:android` after `expo prebuild` — not Expo Go.
82
87
 
83
- `No value passed for parameter 'uiManagerType'`
88
+ ### React Native 0.81+ event dispatch
84
89
 
85
- upgrade to a release that dispatches Fabric events with `UIManagerType.FABRIC` (0.1.3+).
90
+ If Android Kotlin compile fails with `No value passed for parameter 'uiManagerType'`, upgrade to **0.1.3+** (Fabric `UIManagerType.FABRIC`).
86
91
 
87
92
  ## Basic usage
88
93
 
@@ -105,7 +110,73 @@ export function Example() {
105
110
  }
106
111
  ```
107
112
 
108
- Set `style.height` `itemHeight * visibleItemCount` (default `44 × 5 = 220`).
113
+ ### Layout defaults (0.1.5+)
114
+
115
+ The JS wrapper applies **`minWidth: 64`** and, unless you use flex or pass `height` / `minHeight`, **`height: itemHeight * visibleItemCount`** (default **220**). Native Android also sets matching `minimumWidth` / `minimumHeight`.
116
+
117
+ For production layouts, still pass explicit dimensions:
118
+
119
+ ```tsx
120
+ style={{ width: 120, height: itemHeight * visibleItemCount }}
121
+ ```
122
+
123
+ In `__DEV__`, a **one-time** warning is logged if neither height nor flex sizing is provided.
124
+
125
+ ## Examples
126
+
127
+ ### Time picker (hour · minute)
128
+
129
+ ```tsx
130
+ const hours = Array.from({ length: 24 }, (_, i) => String(i).padStart(2, '0'));
131
+ const minutes = Array.from({ length: 60 }, (_, i) => String(i).padStart(2, '0'));
132
+
133
+ <View style={{ flexDirection: 'row', alignItems: 'center' }}>
134
+ <DrumPicker items={hours} style={{ width: 72, height: 220 }} />
135
+ <DrumPicker items={minutes} style={{ width: 72, height: 220 }} />
136
+ </View>
137
+ ```
138
+
139
+ ### Height / weight row (onboarding style)
140
+
141
+ ```tsx
142
+ <View style={{ flexDirection: 'row', alignItems: 'center' }}>
143
+ <DrumPicker items={heightsCm} style={{ width: 90, height: 220 }} />
144
+ <DrumPicker items={weightsKg} style={{ width: 96, height: 220 }} />
145
+ </View>
146
+ ```
147
+
148
+ ### Controlled `selectedIndex`
149
+
150
+ ```tsx
151
+ const [index, setIndex] = useState(1);
152
+
153
+ <DrumPicker
154
+ items={items}
155
+ selectedIndex={index}
156
+ onChange={(e) => setIndex(e.nativeEvent.index)}
157
+ style={{ width: 120, height: 220 }}
158
+ />
159
+ ```
160
+
161
+ ### `onChange` and expensive side effects
162
+
163
+ Native emits `onChange` when the wheel **snaps to idle** and the **centered index changes** (duplicate indices are ignored). Use it for UI state. For AsyncStorage, APIs, or analytics, **debounce** in your app:
164
+
165
+ ```tsx
166
+ const save = useMemo(() => {
167
+ let t: ReturnType<typeof setTimeout> | undefined;
168
+ return (value: string) => {
169
+ if (t) clearTimeout(t);
170
+ t = setTimeout(() => {
171
+ // persist value
172
+ }, 300);
173
+ };
174
+ }, []);
175
+
176
+ <DrumPicker onChange={(e) => save(e.nativeEvent.value)} ... />
177
+ ```
178
+
179
+ See the **example** app (`example/src/App.tsx`) for basic, time, height/weight, date, controlled, and debounced demos.
109
180
 
110
181
  ## DateDrumPicker
111
182
 
@@ -246,12 +317,14 @@ Use an **odd** `visibleItemCount` (e.g. `5`) for a symmetric wheel.
246
317
  | Metro shows old code | `npx react-native start --reset-cache` |
247
318
  | Gradle / build errors | `cd android && ./gradlew clean` (Windows: `.\gradlew clean`) |
248
319
  | `adb` not found | Install Android SDK Platform-Tools; add to `PATH` |
249
- | Empty or white picker | Enable New Architecture; set explicit `width` / `height` in `style` |
320
+ | Empty or white picker | Enable New Architecture; upgrade to **0.1.5+** for layout defaults; or set `style={{ width, height: itemHeight * visibleItemCount }}` |
321
+ | Wrong initial row / off-center | Upgrade to **0.1.5+**; avoid `key` remount hacks unless needed for other reasons |
250
322
  | Props not applied | Rebuild app after native changes; run `yarn build` in the library before packing |
251
323
  | iOS | Not supported — Android only |
252
324
  | Expo Go | Use prebuild + dev build; this library is not in Expo Go |
253
325
  | RN 0.81 `uiManagerType` compile error | Upgrade to 0.1.3+ |
254
326
  | Crash when leaving a screen | Upgrade to 0.1.4+ (safe `onDetachedFromWindow` with react-native-screens) |
327
+ | npm shows “no README” | Often a registry UI lag; run `npm view react-native-drum-picker readme` — if content appears, hard-refresh the package page |
255
328
 
256
329
  ### Android build fails in Expo / React Native 0.81+
257
330
 
@@ -76,6 +76,7 @@ internal class DrumPickerAdapter(
76
76
  textView.layoutParams =
77
77
  (textView.layoutParams as RecyclerView.LayoutParams).apply { this.height = height }
78
78
  textView.text = items[position]
79
+ textView.contentDescription = items[position]
79
80
  textView.setBackgroundColor(itemBackgroundColor)
80
81
  holder.lastStyleBucket = Int.MIN_VALUE
81
82
  val distance = distanceForPosition?.invoke(position) ?: 2f
@@ -8,6 +8,7 @@ import android.graphics.Color
8
8
  internal object DrumPickerDefaults {
9
9
  const val ITEM_HEIGHT_DP = 44f
10
10
  const val VISIBLE_ITEM_COUNT = 5
11
+ const val MIN_WIDTH_DP = 64f
11
12
  const val TEXT_SIZE_SP = 20f
12
13
  const val SELECTED_TEXT_SIZE_SP = 22f
13
14
  const val SELECTION_INDICATOR_HEIGHT_DP = 1f
@@ -51,6 +51,8 @@ class DrumPickerView @JvmOverloads constructor(
51
51
  private var isAttachedToWindow = false
52
52
  private var isDisposed = false
53
53
  private var styleUpdatePosted = false
54
+ private var pendingScrollRunnable: Runnable? = null
55
+ private val minWidthPx = dpToPx(DrumPickerDefaults.MIN_WIDTH_DP)
54
56
 
55
57
  private val styleUpdateRunnable =
56
58
  Runnable {
@@ -120,11 +122,12 @@ class DrumPickerView @JvmOverloads constructor(
120
122
  applyBackgroundColors()
121
123
  applyRecyclerPadding()
122
124
  updateIndicatorAppearance()
125
+ updateMinimumDimensions()
123
126
  }
124
127
 
125
128
  fun setItemsProp(value: Any?) {
126
129
  val newItems = parseItems(value)
127
- if (newItems == items) {
130
+ if (itemsContentEquals(newItems, items)) {
128
131
  return
129
132
  }
130
133
 
@@ -139,7 +142,7 @@ class DrumPickerView @JvmOverloads constructor(
139
142
  }
140
143
 
141
144
  selectedIndex = selectedIndex.coerceIn(0, items.size - 1)
142
- runWhenAttached { scrollToSelectedIndex(animated = false, emit = false) }
145
+ runWhenAttached { scheduleScrollToSelectedIndexCentered(animated = false, emit = false) }
143
146
  }
144
147
 
145
148
  fun setSelectedIndexProp(value: Any?) {
@@ -215,13 +218,11 @@ class DrumPickerView @JvmOverloads constructor(
215
218
  }
216
219
 
217
220
  override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
218
- val width = resolveSize(suggestedMinimumWidth, widthMeasureSpec)
221
+ updateMinimumDimensions()
222
+ val width = resolveSize(minWidthPx, widthMeasureSpec)
219
223
  val safeVisibleCount = visibleItemCount.coerceAtLeast(1)
220
- val height =
221
- resolveSize(
222
- (itemHeightPx * safeVisibleCount).coerceAtLeast(suggestedMinimumHeight),
223
- heightMeasureSpec,
224
- )
224
+ val defaultHeight = itemHeightPx * safeVisibleCount
225
+ val height = resolveSize(defaultHeight, heightMeasureSpec)
225
226
 
226
227
  val childWidthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY)
227
228
  val childHeightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
@@ -237,7 +238,7 @@ class DrumPickerView @JvmOverloads constructor(
237
238
  layoutSelectionIndicators(width, height)
238
239
  if (changed) {
239
240
  applyRecyclerPadding()
240
- runWhenAttached { scrollToSelectedIndex(animated = false, emit = false) }
241
+ runWhenAttached { scheduleScrollToSelectedIndexCentered(animated = false, emit = false) }
241
242
  }
242
243
  }
243
244
 
@@ -245,6 +246,7 @@ class DrumPickerView @JvmOverloads constructor(
245
246
  super.onAttachedToWindow()
246
247
  isAttachedToWindow = true
247
248
  isDisposed = false
249
+ suppressChangeEvent = false
248
250
  recyclerView.removeOnScrollListener(scrollListener)
249
251
  recyclerView.addOnScrollListener(scrollListener)
250
252
  }
@@ -252,7 +254,10 @@ class DrumPickerView @JvmOverloads constructor(
252
254
  override fun onDetachedFromWindow() {
253
255
  isAttachedToWindow = false
254
256
  isDisposed = true
257
+ suppressChangeEvent = false
255
258
  styleUpdatePosted = false
259
+ cancelPendingScroll()
260
+ recyclerView.stopScroll()
256
261
  recyclerView.removeCallbacks(styleUpdateRunnable)
257
262
  removeCallbacks(null)
258
263
  recyclerView.removeCallbacks(null)
@@ -271,11 +276,15 @@ class DrumPickerView @JvmOverloads constructor(
271
276
  }
272
277
  val clamped = index.coerceIn(0, items.size - 1)
273
278
  val snapIndex = findSnapCenterIndex()
274
- if (clamped == selectedIndex && snapIndex == clamped) {
279
+ val isAlreadyCentered =
280
+ clamped == selectedIndex &&
281
+ snapIndex == clamped &&
282
+ recyclerView.scrollState == RecyclerView.SCROLL_STATE_IDLE
283
+ if (isAlreadyCentered) {
275
284
  return
276
285
  }
277
286
  selectedIndex = clamped
278
- scrollToSelectedIndex(animated = false, emit = false)
287
+ scheduleScrollToSelectedIndexCentered(animated = false, emit = false)
279
288
  }
280
289
 
281
290
  fun setItemHeight(height: Float) {
@@ -287,6 +296,7 @@ class DrumPickerView @JvmOverloads constructor(
287
296
  adapter.itemHeightPx = itemHeightPx
288
297
  adapter.notifyRowMetricsChanged()
289
298
  applyRecyclerPadding()
299
+ updateMinimumDimensions()
290
300
  requestLayout()
291
301
  }
292
302
 
@@ -296,9 +306,16 @@ class DrumPickerView @JvmOverloads constructor(
296
306
  }
297
307
  visibleItemCount = count
298
308
  applyRecyclerPadding()
309
+ updateMinimumDimensions()
299
310
  requestLayout()
300
311
  }
301
312
 
313
+ private fun updateMinimumDimensions() {
314
+ val safeVisibleCount = visibleItemCount.coerceAtLeast(1)
315
+ minimumHeight = itemHeightPx * safeVisibleCount
316
+ minimumWidth = minWidthPx
317
+ }
318
+
302
319
  fun setTextColor(color: Int) {
303
320
  textColor = color
304
321
  syncAdapterStyle()
@@ -391,7 +408,36 @@ class DrumPickerView @JvmOverloads constructor(
391
408
  bottomIndicator.requestLayout()
392
409
  }
393
410
 
394
- private fun scrollToSelectedIndex(animated: Boolean, emit: Boolean) {
411
+ private fun scheduleScrollToSelectedIndexCentered(animated: Boolean, emit: Boolean) {
412
+ if (!isLifecycleActive() || items.isEmpty()) {
413
+ return
414
+ }
415
+ cancelPendingScroll()
416
+ val runnable =
417
+ Runnable {
418
+ pendingScrollRunnable = null
419
+ if (!isLifecycleActive()) {
420
+ return@Runnable
421
+ }
422
+ scrollToSelectedIndexCentered(animated, emit)
423
+ }
424
+ pendingScrollRunnable = runnable
425
+ if (recyclerView.height > 0) {
426
+ recyclerView.post(runnable)
427
+ } else {
428
+ post(runnable)
429
+ }
430
+ }
431
+
432
+ private fun cancelPendingScroll() {
433
+ pendingScrollRunnable?.let { pending ->
434
+ recyclerView.removeCallbacks(pending)
435
+ removeCallbacks(pending)
436
+ }
437
+ pendingScrollRunnable = null
438
+ }
439
+
440
+ private fun scrollToSelectedIndexCentered(animated: Boolean, emit: Boolean) {
395
441
  if (!isLifecycleActive() || items.isEmpty()) {
396
442
  return
397
443
  }
@@ -402,16 +448,30 @@ class DrumPickerView @JvmOverloads constructor(
402
448
  return
403
449
  }
404
450
 
405
- layoutManager.scrollToPositionWithOffset(index, 0)
451
+ if (recyclerView.height <= 0) {
452
+ scheduleScrollToSelectedIndexCentered(animated = false, emit = emit)
453
+ return
454
+ }
455
+
456
+ val centerOffset = ((recyclerView.height - itemHeightPx) / 2).coerceAtLeast(0)
457
+ layoutManager.scrollToPositionWithOffset(index, centerOffset)
406
458
  recyclerView.post {
407
459
  if (!isLifecycleActive()) {
408
460
  return@post
409
461
  }
462
+ val snappedIndex = findSnapCenterIndex()
463
+ if (snappedIndex != RecyclerView.NO_POSITION && snappedIndex != index) {
464
+ layoutManager.scrollToPositionWithOffset(snappedIndex, centerOffset)
465
+ selectedIndex = snappedIndex
466
+ }
410
467
  updateVisibleItemStyles()
468
+ val resolvedIndex =
469
+ if (snappedIndex != RecyclerView.NO_POSITION) snappedIndex else index
411
470
  if (emit) {
412
- maybeEmitChange(index)
471
+ maybeEmitChange(resolvedIndex)
413
472
  } else {
414
- lastEmittedIndex = index
473
+ lastEmittedIndex = resolvedIndex
474
+ selectedIndex = resolvedIndex
415
475
  }
416
476
  suppressChangeEvent = false
417
477
  }
@@ -506,7 +566,8 @@ class DrumPickerView @JvmOverloads constructor(
506
566
  if (!reactContext.hasActiveReactInstance()) {
507
567
  return
508
568
  }
509
- // Fabric-only: RN 0.81+ requires uiManagerType when resolving the event dispatcher.
569
+ // Fabric-only: pass UIManagerType.FABRIC for RN 0.81–0.84; deprecated but still required there.
570
+ @Suppress("DEPRECATION")
510
571
  val dispatcher: EventDispatcher? =
511
572
  UIManagerHelper.getEventDispatcher(reactContext, UIManagerType.FABRIC)
512
573
  dispatcher?.dispatchEvent(
@@ -589,4 +650,19 @@ class DrumPickerView @JvmOverloads constructor(
589
650
 
590
651
  private fun dpToPx(dp: Float): Int =
591
652
  (dp * resources.displayMetrics.density + 0.5f).toInt()
653
+
654
+ private fun itemsContentEquals(a: List<String>, b: List<String>): Boolean {
655
+ if (a === b) {
656
+ return true
657
+ }
658
+ if (a.size != b.size) {
659
+ return false
660
+ }
661
+ for (i in a.indices) {
662
+ if (a[i] != b[i]) {
663
+ return false
664
+ }
665
+ }
666
+ return true
667
+ }
592
668
  }
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
 
3
- import { useCallback, useMemo, useState } from 'react';
3
+ import { useCallback, useEffect, useMemo, useState } from 'react';
4
4
  import { StyleSheet, View } from 'react-native';
5
5
  import { buildDayItems, buildMonthItems, buildYearItems, clampDateDrumPickerValue, clampDayForMonth, clampYear, normalizeYearRange, parseMonthFromLabel } from "./dateDrumPickerLogic.js";
6
6
  import { DrumPicker } from './DrumPicker';
@@ -75,6 +75,20 @@ export function DateDrumPicker({
75
75
  }
76
76
  onChange?.(next);
77
77
  }, [isControlled, onChange, resolvedValue, minYear, maxYear]);
78
+
79
+ // Controlled: parent may pass day 31 + April — clamp and notify once.
80
+ useEffect(() => {
81
+ if (!isControlled || !onChange || value === undefined) {
82
+ return;
83
+ }
84
+ const clamped = clampDateDrumPickerValue(value, minYear, maxYear);
85
+ const day = value.day ?? clamped.day;
86
+ const month = value.month ?? clamped.month;
87
+ const year = value.year ?? clamped.year;
88
+ if (day !== clamped.day || month !== clamped.month || year !== clamped.year) {
89
+ onChange(clamped);
90
+ }
91
+ }, [isControlled, onChange, value, minYear, maxYear]);
78
92
  const sharedPickerProps = {
79
93
  itemHeight,
80
94
  visibleItemCount,
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
 
3
- import { StyleSheet } from 'react-native';
4
3
  import DrumPickerNative from './DrumPickerViewNativeComponent';
4
+ import { resolveDrumPickerStyle } from "./drumPickerLayout.js";
5
5
  import { jsx as _jsx } from "react/jsx-runtime";
6
6
  const DEFAULTS = {
7
7
  selectedIndex: 0,
@@ -36,10 +36,7 @@ export function DrumPicker({
36
36
  onChange,
37
37
  style
38
38
  }) {
39
- const pickerHeight = itemHeight * visibleItemCount;
40
- const pickerStyle = StyleSheet.flatten([{
41
- height: pickerHeight
42
- }, style]);
39
+ const pickerStyle = resolveDrumPickerStyle(itemHeight, visibleItemCount, style);
43
40
  return /*#__PURE__*/_jsx(DrumPickerNative, {
44
41
  collapsable: false,
45
42
  items: items,
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+
3
+ import { StyleSheet } from 'react-native';
4
+ let didWarnMissingLayout = false;
5
+
6
+ /**
7
+ * Merges consumer style with sensible defaults so the native picker is visible
8
+ * without forcing dimensions when flex layout is used.
9
+ */
10
+ export function resolveDrumPickerStyle(itemHeight, visibleItemCount, style) {
11
+ const flat = StyleSheet.flatten(style) ?? {};
12
+ const pickerHeight = itemHeight * visibleItemCount;
13
+ const hasExplicitHeight = flat.height != null || flat.minHeight != null || flat.maxHeight != null;
14
+ const usesFlex = flat.flex != null || flat.flexGrow != null || flat.flexShrink != null || flat.alignSelf === 'stretch';
15
+ if (__DEV__ && !didWarnMissingLayout && !hasExplicitHeight && !usesFlex) {
16
+ didWarnMissingLayout = true;
17
+ console.warn('react-native-drum-picker: DrumPicker needs a visible height. ' + `Defaulting to itemHeight * visibleItemCount (${pickerHeight}). ` + 'Pass style={{ height: ... }} or style={{ minHeight: ... }} for precise control.');
18
+ }
19
+ const sizeDefaults = hasExplicitHeight || usesFlex ? {
20
+ minWidth: 64,
21
+ minHeight: pickerHeight
22
+ } : {
23
+ minWidth: 64,
24
+ minHeight: pickerHeight,
25
+ height: pickerHeight
26
+ };
27
+ return StyleSheet.flatten([sizeDefaults, style]);
28
+ }
29
+ //# sourceMappingURL=drumPickerLayout.js.map
@@ -0,0 +1,7 @@
1
+ import { type StyleProp, type ViewStyle } from 'react-native';
2
+ /**
3
+ * Merges consumer style with sensible defaults so the native picker is visible
4
+ * without forcing dimensions when flex layout is used.
5
+ */
6
+ export declare function resolveDrumPickerStyle(itemHeight: number, visibleItemCount: number, style?: StyleProp<ViewStyle>): ViewStyle;
7
+ //# sourceMappingURL=drumPickerLayout.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-drum-picker",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Android-native iOS-style drum/wheel picker for React Native (Fabric)",
5
5
  "main": "./lib/module/index.js",
6
6
  "module": "./lib/module/index.js",
@@ -1,4 +1,4 @@
1
- import { useCallback, useMemo, useState } from 'react';
1
+ import { useCallback, useEffect, useMemo, useState } from 'react';
2
2
  import {
3
3
  StyleSheet,
4
4
  View,
@@ -164,6 +164,24 @@ export function DateDrumPicker({
164
164
  [isControlled, onChange, resolvedValue, minYear, maxYear]
165
165
  );
166
166
 
167
+ // Controlled: parent may pass day 31 + April — clamp and notify once.
168
+ useEffect(() => {
169
+ if (!isControlled || !onChange || value === undefined) {
170
+ return;
171
+ }
172
+ const clamped = clampDateDrumPickerValue(value, minYear, maxYear);
173
+ const day = value.day ?? clamped.day;
174
+ const month = value.month ?? clamped.month;
175
+ const year = value.year ?? clamped.year;
176
+ if (
177
+ day !== clamped.day ||
178
+ month !== clamped.month ||
179
+ year !== clamped.year
180
+ ) {
181
+ onChange(clamped);
182
+ }
183
+ }, [isControlled, onChange, value, minYear, maxYear]);
184
+
167
185
  const sharedPickerProps = {
168
186
  itemHeight,
169
187
  visibleItemCount,
@@ -1,5 +1,5 @@
1
- import { StyleSheet } from 'react-native';
2
1
  import DrumPickerNative from './DrumPickerViewNativeComponent';
2
+ import { resolveDrumPickerStyle } from './drumPickerLayout';
3
3
  import type { DrumPickerProps } from './types';
4
4
 
5
5
  const DEFAULTS = {
@@ -36,13 +36,11 @@ export function DrumPicker({
36
36
  onChange,
37
37
  style,
38
38
  }: DrumPickerProps) {
39
- const pickerHeight = itemHeight * visibleItemCount;
40
- const pickerStyle = StyleSheet.flatten([
41
- {
42
- height: pickerHeight,
43
- },
44
- style,
45
- ]);
39
+ const pickerStyle = resolveDrumPickerStyle(
40
+ itemHeight,
41
+ visibleItemCount,
42
+ style
43
+ );
46
44
 
47
45
  return (
48
46
  <DrumPickerNative
@@ -0,0 +1,39 @@
1
+ import { StyleSheet, type StyleProp, type ViewStyle } from 'react-native';
2
+
3
+ let didWarnMissingLayout = false;
4
+
5
+ /**
6
+ * Merges consumer style with sensible defaults so the native picker is visible
7
+ * without forcing dimensions when flex layout is used.
8
+ */
9
+ export function resolveDrumPickerStyle(
10
+ itemHeight: number,
11
+ visibleItemCount: number,
12
+ style?: StyleProp<ViewStyle>
13
+ ): ViewStyle {
14
+ const flat = StyleSheet.flatten(style) ?? {};
15
+ const pickerHeight = itemHeight * visibleItemCount;
16
+ const hasExplicitHeight =
17
+ flat.height != null || flat.minHeight != null || flat.maxHeight != null;
18
+ const usesFlex =
19
+ flat.flex != null ||
20
+ flat.flexGrow != null ||
21
+ flat.flexShrink != null ||
22
+ flat.alignSelf === 'stretch';
23
+
24
+ if (__DEV__ && !didWarnMissingLayout && !hasExplicitHeight && !usesFlex) {
25
+ didWarnMissingLayout = true;
26
+ console.warn(
27
+ 'react-native-drum-picker: DrumPicker needs a visible height. ' +
28
+ `Defaulting to itemHeight * visibleItemCount (${pickerHeight}). ` +
29
+ 'Pass style={{ height: ... }} or style={{ minHeight: ... }} for precise control.'
30
+ );
31
+ }
32
+
33
+ const sizeDefaults: ViewStyle =
34
+ hasExplicitHeight || usesFlex
35
+ ? { minWidth: 64, minHeight: pickerHeight }
36
+ : { minWidth: 64, minHeight: pickerHeight, height: pickerHeight };
37
+
38
+ return StyleSheet.flatten([sizeDefaults, style]) as ViewStyle;
39
+ }