From 08097e80c6a8170d74968df3ed28c150021edfda Mon Sep 17 00:00:00 2001 From: scroll Date: Mon, 27 Jul 2026 09:53:09 +0500 Subject: [PATCH 1/3] fix: centre without a viewport and keep the selected value MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit React Native never runs an Android layout pass for native children: ReactViewGroup.requestLayout() and ReactViewGroup.onLayout() are both empty, so scrollToPositionWithOffset only parked an anchor nobody consumed. Combined with centering scheduled through nested post() calls off a height that was still 0, a picker mounted in a collapsed container stayed on index 0 with only its centre row drawn. Centering is now layout-driven and drives the RecyclerView's measure/layout itself. selectedIndex was also clamped against the items list it was replacing. Fabric hands props over as an unordered map, so a range change permanently moved the selection and emitted onChange for a value the user never picked. The raw requested index is kept and re-resolved against the new list, prop application order is fixed, and change events are suppressed while props are applied. Inside a Modal the same layout fix applies — DialogRootViewGroup only reports its size asynchronously — and gestures are handed over via NativeGestureUtil, since that root discards requestDisallowInterceptTouchEvent. iOS gets the same re-resolution plus a layoutSubviews reassert. Android instrumented tests and the iOS test additions were not executed; see CHANGELOG for what was and was not verified. BREAKING CHANGE: DateDrumPickerValue now requires day, month and year; use DateDrumPickerPartialValue where an incomplete date is intentional. --- CHANGELOG.md | 66 ++++ README.md | 111 +++++- android/src/androidTest/AndroidManifest.xml | 4 + .../com/drumpicker/CollapsedTestActivity.kt | 59 ++++ .../drumpicker/DrumPickerRegressionTest.kt | 203 +++++++++++ .../java/com/drumpicker/DrumPickerView.kt | 320 +++++++++++++----- .../com/drumpicker/DrumPickerViewManager.kt | 72 ++-- example/src/App.tsx | 9 +- .../DrumPickerWheelViewTests.swift | 74 ++++ ios/DrumPickerView.swift | 31 +- package.json | 2 +- src/DateDrumPicker.tsx | 23 +- src/DrumPicker.native.tsx | 68 +++- .../DateDrumPickerControlled.test.tsx | 197 +++++++++++ .../DateDrumPicker.snapshot.test.tsx.snap | 3 + .../DrumPicker.snapshot.test.tsx.snap | 1 + src/dateDrumPickerLogic.ts | 18 +- src/index.tsx | 1 + 18 files changed, 1136 insertions(+), 126 deletions(-) create mode 100644 android/src/androidTest/java/com/drumpicker/CollapsedTestActivity.kt create mode 100644 android/src/androidTest/java/com/drumpicker/DrumPickerRegressionTest.kt create mode 100644 src/__tests__/DateDrumPickerControlled.test.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a668c1..7085b85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,72 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.3.0](https://github.com/scrollDynasty/react-native-drum-picker/compare/v0.2.4...v0.3.0) (2026-07-27) + +Fixes the integration defects that forced consumers to work around the picker with delayed +mounting, two-phase range changes and `onChange` suppression flags. + +### ⚠ BREAKING CHANGES + +* **types:** `DateDrumPickerValue` now requires `day`, `month` and `year`. A partial value passed to + `DateDrumPicker.value` silently fell back to today's date for the missing fields. Use the new + `DateDrumPickerPartialValue` where an incomplete date is intentional — `scrollToDate` and + `clampDateDrumPickerValue` both accept it. Migration: `useState({})` becomes + an explicit `{ day, month, year }`. +* **android:** changing `items` now restores the selected **value** instead of the scroll position, + and no longer emits `onChange`. Code that relied on the old drift (or on the spurious event) will + see different behaviour. + +### Bug Fixes + +* **android:** center on `selectedIndex` when the picker mounts without a size. Centering was + scheduled through nested `post` calls and computed its offset from a height that was still `0`, + so a picker mounted in a collapsed container stayed on index 0. The request is now parked and + flushed from `onLayout`, where the real height is known. +* **android:** draw the rows around the selection after a programmatic scroll. + `scrollToPositionWithOffset` only parks an anchor and calls `requestLayout()`; React Native's + root view does not lay out native children, so the anchor went unused and the wheel was left + with just the centre row until the user dragged it. +* **android:** keep the selected value when `items` or the date range changes. + `selectedIndex` was clamped against the list it was replacing — Fabric applies props in + arbitrary order — which permanently moved the selection. The raw requested index is now kept and + re-resolved against the new list, and `DrumPickerViewManager` applies size, list and position + props in a fixed order. +* **android:** never emit `onChange` while props are being applied, so a controlled parent no + longer needs a ref flag to mute events during reconfiguration. +* **android:** hand the touch sequence to React Native via `NativeGestureUtil` when the wheel + starts scrolling and back when it settles, so the JS responder system stops competing for it. + This is what makes the wheel spin inside a `Modal`, where + `DialogRootViewGroup.requestDisallowInterceptTouchEvent` is an empty method and the usual signal + is discarded. +* **android:** re-centre when `itemHeight` or `visibleItemCount` changes. Both feed the centering + offset while often leaving the picker's own bounds untouched, so `onLayout` reported no change + and nothing re-centred. +* **ios:** re-resolve the selection from the requested index on `setItems`, and reassert it in + `layoutSubviews`, matching the Android behaviour for zero-size mounts and list swaps. +* **DateDrumPicker:** clamp the year column's `selectedIndex` like the day and month columns. + +### Features + +* **dev:** warn in `__DEV__` about empty `items`, an out-of-range `selectedIndex` that outlives a + render, and a picker still measuring zero after ~1.5s. +* **docs:** new "Common problems" section covering hidden containers, `Modal`, dependent columns + and controlled usage, with an "open on the current date" example. + +### Verification + +Fixes were derived from React Native 0.86's own layout and touch plumbing — +`ReactViewGroup.requestLayout()` and `ReactViewGroup.onLayout()` are both empty, and +`DialogRootViewGroup.requestDisallowInterceptTouchEvent` likewise — which is why the picker now +drives the RecyclerView's measure/layout itself and hands gestures over explicitly. + +The JS suite and a standalone compile against RN 0.86 both pass. The Android instrumented tests +(`DrumPickerRegressionTest`) and the iOS `DrumPickerWheelViewTests` additions ship with this +release but were **not executed** in the environment the fix was written in, and the reporter's +`Modal` scenario was not re-run on a device. Run +`./gradlew :react-native-drum-picker:connectedDebugAndroidTest` before trusting the Android +behaviour blind. + ## [0.2.4](https://github.com/scrollDynasty/react-native-drum-picker/compare/v0.2.3...v0.2.4) (2026-05-28) diff --git a/README.md b/README.md index aaebb9e..5269d69 100644 --- a/README.md +++ b/README.md @@ -695,6 +695,112 @@ Backgrounds are **transparent by default**. Only text and optional indicator lin Use an **odd** `visibleItemCount` (e.g. `5`) for a symmetric wheel. +## Common problems + +### Opening the picker on a given date + +The most frequent case: a section that renders the picker only while it is open, and wants it to +open already showing a date. A controlled `value` is all you need — no ref calls, no delayed +mounting, no `key` remount. + +```tsx +const [open, setOpen] = useState(false); +const [date, setDate] = useState({ day: 26, month: 7, year: 2026 }); + +return ( + <> +