diff --git a/README.md b/README.md
index 9faffb5..0941ba3 100644
--- a/README.md
+++ b/README.md
@@ -177,6 +177,18 @@ const [index, setIndex] = useState(1);
/>;
```
+### Locking the picker while a form submits
+
+```tsx
+const [saving, setSaving] = useState(false);
+
+
+
+;
+```
+
+`disabled` blocks drags and taps, and swallows touches so nothing behind the picker reacts either. Programmatic movement is deliberately left working — `selectedIndex` and the `scrollToIndex` / `scrollToValue` ref methods still scroll the wheel, so you can reset or correct a locked form. Appearance is up to you: the prop changes behaviour only, so wrap it as above if it should also look inert.
+
### `onChange` and expensive side effects

@@ -624,6 +636,7 @@ In addition to all `DrumPicker` props (on the wrapped instance):
| `selectionIndicatorColor` | `string` | `#D1D1D6` | Line color |
| `selectionIndicatorHeight` | `number` | `1` | Line thickness (dp) |
| `hapticFeedback` | `boolean` | `false` | Light haptic on snap (Android + iOS) |
+| `disabled` | `boolean` | `false` | Block user drags and taps. Programmatic scrolling still works. |
| `circular` | `boolean` | `false` | Enable infinite loop scroll. Wraps last->first and first->last. |
| `enableScrollByTapOnItem` | `boolean` | `false` | Tap a visible row to scroll it to center (Android + iOS) |
| `onChange` | `function` | — | `nativeEvent: { index, value }` |
@@ -658,6 +671,7 @@ In addition to all `DrumPicker` props (on the wrapped instance):
| `itemBackgroundColor` | `string` | `transparent` | Passed to each column |
| `containerBackgroundColor` | `string` | `transparent` | Passed to each column |
| `hapticFeedback` | `boolean` | `false` | Passed to each column |
+| `disabled` | `boolean` | `false` | Passed to each column |
| `enableScrollByTapOnItem` | `boolean` | `false` | Passed to each column |
| `style` | `ViewStyle` | — | Row container |
| `columnStyle` | `ViewStyle` | — | All columns |
diff --git a/android/src/androidTest/java/com/drumpicker/DrumPickerTapTest.kt b/android/src/androidTest/java/com/drumpicker/DrumPickerTapTest.kt
index b199b60..72a9e95 100644
--- a/android/src/androidTest/java/com/drumpicker/DrumPickerTapTest.kt
+++ b/android/src/androidTest/java/com/drumpicker/DrumPickerTapTest.kt
@@ -93,4 +93,46 @@ class DrumPickerTapTest {
assertEquals(2, readSelectedIndex(scenario))
}
}
+
+ @Test
+ fun tapIgnoredWhileInteractionDisabled() {
+ val instrumentation = InstrumentationRegistry.getInstrumentation()
+ ActivityScenario.launch(TestActivity::class.java).use { scenario ->
+ scenario.onActivity { activity ->
+ // Tap-to-scroll is on: only `disabled` should be keeping the wheel still.
+ activity.picker.setEnableScrollByTapOnItemProp(true)
+ activity.picker.setDisabledProp(true)
+ activity.picker.setSelectedIndexProp(2)
+ activity.picker.requestLayout()
+ }
+ instrumentation.waitForIdleSync()
+ waitForSelectedIndex(scenario, 2)
+
+ scenario.onActivity { activity ->
+ activity.picker.testingPerformItemTap(0)
+ }
+ instrumentation.waitForIdleSync()
+ assertEquals(2, readSelectedIndex(scenario))
+ }
+ }
+
+ @Test
+ fun programmaticSelectionStillWorksWhileInteractionDisabled() {
+ val instrumentation = InstrumentationRegistry.getInstrumentation()
+ ActivityScenario.launch(TestActivity::class.java).use { scenario ->
+ scenario.onActivity { activity ->
+ activity.picker.setDisabledProp(true)
+ activity.picker.setSelectedIndexProp(1)
+ activity.picker.requestLayout()
+ }
+ instrumentation.waitForIdleSync()
+ waitForSelectedIndex(scenario, 1)
+
+ scenario.onActivity { activity ->
+ activity.picker.setSelectedIndexProp(3)
+ }
+ instrumentation.waitForIdleSync()
+ waitForSelectedIndex(scenario, 3)
+ }
+ }
}
diff --git a/android/src/main/java/com/drumpicker/DrumPickerView.kt b/android/src/main/java/com/drumpicker/DrumPickerView.kt
index 09d7e60..4050d81 100644
--- a/android/src/main/java/com/drumpicker/DrumPickerView.kt
+++ b/android/src/main/java/com/drumpicker/DrumPickerView.kt
@@ -97,6 +97,12 @@ class DrumPickerView @JvmOverloads constructor(
private var nativeGestureNotified = false
private var lastDownEvent: MotionEvent? = null
+ /**
+ * Blocks touch-driven scrolling. Programmatic centering stays available, so a controlled
+ * parent can still move the wheel while the user cannot.
+ */
+ private var isInteractionDisabled = false
+
private val styleUpdateRunnable =
Runnable {
styleUpdatePosted = false
@@ -285,6 +291,19 @@ class DrumPickerView @JvmOverloads constructor(
hapticFeedback = value as? Boolean ?: false
}
+ fun setDisabledProp(value: Any?) {
+ val disabled = toBoolean(value, false)
+ if (isInteractionDisabled == disabled) {
+ return
+ }
+ isInteractionDisabled = disabled
+ if (disabled) {
+ // A drag may be in flight when the prop flips; drop it rather than letting it settle
+ // somewhere the user can no longer correct.
+ recyclerView.stopScroll()
+ }
+ }
+
fun setEnableScrollByTapOnItemProp(value: Any?) {
enableScrollByTapOnItem = toBoolean(value, false)
adapter.enableScrollByTapOnItem = enableScrollByTapOnItem
@@ -304,7 +323,12 @@ class DrumPickerView @JvmOverloads constructor(
}
private fun scrollToPositionFromTap(position: Int) {
- if (!enableScrollByTapOnItem || !isLifecycleActive() || items.isEmpty()) {
+ if (
+ isInteractionDisabled ||
+ !enableScrollByTapOnItem ||
+ !isLifecycleActive() ||
+ items.isEmpty()
+ ) {
return
}
val clamped = position.coerceIn(0, items.size - 1)
@@ -414,6 +438,11 @@ class DrumPickerView @JvmOverloads constructor(
}
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
+ // Intercept before the RecyclerView sees anything, so a disabled wheel neither scrolls nor
+ // leaks the gesture to whatever sits behind it.
+ if (isInteractionDisabled) {
+ return true
+ }
if (ev.actionMasked == MotionEvent.ACTION_DOWN) {
nativeGestureNotified = false
lastDownEvent?.recycle()
@@ -422,6 +451,9 @@ class DrumPickerView @JvmOverloads constructor(
return super.onInterceptTouchEvent(ev)
}
+ override fun onTouchEvent(ev: MotionEvent): Boolean =
+ if (isInteractionDisabled) true else super.onTouchEvent(ev)
+
/**
* Tells React Native that a native scroll has taken over the touch sequence.
*
diff --git a/android/src/main/java/com/drumpicker/DrumPickerViewManager.kt b/android/src/main/java/com/drumpicker/DrumPickerViewManager.kt
index 19b0dd9..1e6433e 100644
--- a/android/src/main/java/com/drumpicker/DrumPickerViewManager.kt
+++ b/android/src/main/java/com/drumpicker/DrumPickerViewManager.kt
@@ -61,6 +61,7 @@ class DrumPickerViewManager :
"containerBackgroundColor" -> view.setContainerBackgroundColorProp(value)
"itemBackgroundColor" -> view.setItemBackgroundColorProp(value)
"hapticFeedback" -> view.setHapticFeedbackProp(value)
+ "disabled" -> view.setDisabledProp(value)
"circular" -> view.setCircularProp(value)
"enableScrollByTapOnItem" -> view.setEnableScrollByTapOnItemProp(value)
"onValueChangingEnabled" -> view.setOnValueChangingEnabledProp(value)
@@ -132,6 +133,10 @@ class DrumPickerViewManager :
view?.setHapticFeedbackProp(value)
}
+ override fun setDisabled(view: DrumPickerView?, value: Boolean) {
+ view?.setDisabledProp(value)
+ }
+
override fun setCircular(view: DrumPickerView?, value: Boolean) {
view?.setCircularProp(value)
}
diff --git a/ios/DrumPickerTests/DrumPickerWheelViewTests.swift b/ios/DrumPickerTests/DrumPickerWheelViewTests.swift
index 0dc6469..ed4246b 100644
--- a/ios/DrumPickerTests/DrumPickerWheelViewTests.swift
+++ b/ios/DrumPickerTests/DrumPickerWheelViewTests.swift
@@ -216,6 +216,22 @@ final class DrumPickerWheelViewTests: XCTestCase {
XCTAssertEqual(view.selectedIndexForTesting(), 2)
}
+
+ func testSetDisabledTogglesUserInteraction() {
+ view.setDisabled(true)
+ XCTAssertFalse(picker.isUserInteractionEnabled)
+
+ view.setDisabled(false)
+ XCTAssertTrue(picker.isUserInteractionEnabled)
+ }
+
+ func testProgrammaticSelectionStillWorksWhileDisabled() {
+ view.setDisabled(true)
+ view.setSelectedIndex(3, animated: false)
+
+ XCTAssertEqual(view.selectedIndexForTesting(), 3)
+ XCTAssertEqual(picker.selectedRow(inComponent: 0), 3)
+ }
}
private final class MockWheelDelegate: NSObject, DrumPickerWheelViewDelegate {
diff --git a/ios/DrumPickerView.mm b/ios/DrumPickerView.mm
index 2e64258..4764723 100644
--- a/ios/DrumPickerView.mm
+++ b/ios/DrumPickerView.mm
@@ -124,6 +124,10 @@ - (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &
[_wheelView setEnableScrollByTapOnItem:newViewProps.enableScrollByTapOnItem];
}
+ if (oldViewProps.disabled != newViewProps.disabled) {
+ [_wheelView setDisabled:newViewProps.disabled];
+ }
+
if (oldViewProps.circular != newViewProps.circular) {
_wheelView.isCircular = newViewProps.circular;
}
diff --git a/ios/DrumPickerView.swift b/ios/DrumPickerView.swift
index 50c14ee..c412c16 100644
--- a/ios/DrumPickerView.swift
+++ b/ios/DrumPickerView.swift
@@ -386,6 +386,12 @@ public final class DrumPickerWheelView: UIView, UIPickerViewDataSource, UIPicker
enableScrollByTapOnItem = value
}
+ /// Blocks touch-driven scrolling. `setSelectedIndex(_:animated:)` keeps working, so a
+ /// controlled parent can still move the wheel while the user cannot.
+ @objc public func setDisabled(_ value: Bool) {
+ picker.isUserInteractionEnabled = !value
+ }
+
@objc public func setOnValueChangingEnabled(_ value: Bool) {
onValueChangingEnabled = value
if value {
diff --git a/src/DateDrumPicker.tsx b/src/DateDrumPicker.tsx
index 4b60b7d..9d42a29 100644
--- a/src/DateDrumPicker.tsx
+++ b/src/DateDrumPicker.tsx
@@ -103,6 +103,8 @@ export type DateDrumPickerProps = {
itemBackgroundColor?: string;
containerBackgroundColor?: string;
hapticFeedback?: boolean;
+ /** Block user interaction on every column. Programmatic scrolling still works. */
+ disabled?: boolean;
enableScrollByTapOnItem?: boolean;
style?: StyleProp;
columnStyle?: StyleProp;
@@ -160,6 +162,7 @@ export const DateDrumPicker = forwardRef<
itemBackgroundColor = 'transparent',
containerBackgroundColor = 'transparent',
hapticFeedback = false,
+ disabled = false,
enableScrollByTapOnItem = false,
style,
columnStyle,
@@ -429,6 +432,7 @@ export const DateDrumPicker = forwardRef<
itemBackgroundColor,
containerBackgroundColor,
hapticFeedback,
+ disabled,
enableScrollByTapOnItem,
};
diff --git a/src/DrumPicker.native.tsx b/src/DrumPicker.native.tsx
index 4706e5c..86cf909 100644
--- a/src/DrumPicker.native.tsx
+++ b/src/DrumPicker.native.tsx
@@ -33,6 +33,7 @@ const DEFAULTS = {
itemBackgroundColor: 'transparent',
containerBackgroundColor: 'transparent',
hapticFeedback: false,
+ disabled: false,
enableScrollByTapOnItem: false,
} as const;
@@ -73,6 +74,7 @@ export const DrumPickerNativeBase = forwardRef<
itemBackgroundColor = DEFAULTS.itemBackgroundColor,
containerBackgroundColor = DEFAULTS.containerBackgroundColor,
hapticFeedback = DEFAULTS.hapticFeedback,
+ disabled = DEFAULTS.disabled,
enableScrollByTapOnItem = DEFAULTS.enableScrollByTapOnItem,
onValueChanging,
onChange,
@@ -375,6 +377,7 @@ export const DrumPickerNativeBase = forwardRef<
itemBackgroundColor={itemBackgroundColor}
containerBackgroundColor={containerBackgroundColor}
hapticFeedback={hapticFeedback}
+ disabled={disabled}
enableScrollByTapOnItem={enableScrollByTapOnItem}
onValueChangingEnabled={shouldEmitValueChanging}
onValueChanging={
@@ -408,6 +411,7 @@ const DrumPickerImpl = forwardRef>(
itemBackgroundColor = DEFAULTS.itemBackgroundColor,
containerBackgroundColor = DEFAULTS.containerBackgroundColor,
hapticFeedback = DEFAULTS.hapticFeedback,
+ disabled = DEFAULTS.disabled,
enableScrollByTapOnItem = DEFAULTS.enableScrollByTapOnItem,
onValueChanging,
onChange,
@@ -439,6 +443,7 @@ const DrumPickerImpl = forwardRef>(
itemBackgroundColor={itemBackgroundColor}
containerBackgroundColor={containerBackgroundColor}
hapticFeedback={hapticFeedback}
+ disabled={disabled}
enableScrollByTapOnItem={enableScrollByTapOnItem}
onValueChanging={onValueChanging}
onChange={onChange}
@@ -470,6 +475,7 @@ const DrumPickerImpl = forwardRef>(
itemBackgroundColor={itemBackgroundColor}
containerBackgroundColor={containerBackgroundColor}
hapticFeedback={hapticFeedback}
+ disabled={disabled}
enableScrollByTapOnItem={enableScrollByTapOnItem}
onValueChanging={onValueChanging}
onChange={onChange}
diff --git a/src/DrumPickerViewNativeComponent.ts b/src/DrumPickerViewNativeComponent.ts
index bd14c20..5ad1e63 100644
--- a/src/DrumPickerViewNativeComponent.ts
+++ b/src/DrumPickerViewNativeComponent.ts
@@ -28,6 +28,7 @@ interface NativeProps extends ViewProps {
containerBackgroundColor?: ColorValue;
itemBackgroundColor?: ColorValue;
hapticFeedback?: CodegenTypes.WithDefault;
+ disabled?: CodegenTypes.WithDefault;
enableScrollByTapOnItem?: CodegenTypes.WithDefault;
onValueChangingEnabled?: CodegenTypes.WithDefault;
onValueChange?: CodegenTypes.DirectEventHandler;
diff --git a/src/__mocks__/DrumPickerViewNativeComponent.tsx b/src/__mocks__/DrumPickerViewNativeComponent.tsx
index d92fec1..9997e28 100644
--- a/src/__mocks__/DrumPickerViewNativeComponent.tsx
+++ b/src/__mocks__/DrumPickerViewNativeComponent.tsx
@@ -13,6 +13,7 @@ type MockProps = ViewProps & {
circular?: boolean;
scrollAnimated?: boolean;
hapticFeedback?: boolean;
+ disabled?: boolean;
enableScrollByTapOnItem?: boolean;
onValueChangingEnabled?: boolean;
onValueChange?: (event: {
diff --git a/src/__tests__/DrumPicker.test.tsx b/src/__tests__/DrumPicker.test.tsx
index 5fed97d..564ff3f 100644
--- a/src/__tests__/DrumPicker.test.tsx
+++ b/src/__tests__/DrumPicker.test.tsx
@@ -1,5 +1,6 @@
import { render } from '@testing-library/react-native';
import React from 'react';
+import { Text } from 'react-native';
import {
fireNativeDrumPickerChange,
getLatestNativeDrumPickerProps,
@@ -100,3 +101,39 @@ describe('enableScrollByTapOnItem', () => {
);
});
});
+
+describe('disabled', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ resetNativeDrumPickerMocks();
+ });
+
+ it('passes disabled=false by default', () => {
+ render();
+ expect(getLatestNativeDrumPickerProps()?.disabled).toBe(false);
+ });
+
+ it('passes disabled=true when set', () => {
+ render();
+ expect(getLatestNativeDrumPickerProps()?.disabled).toBe(true);
+ });
+
+ it('forwards disabled through the renderItem path', () => {
+ render(
+ {label}}
+ />
+ );
+ expect(getLatestNativeDrumPickerProps()?.disabled).toBe(true);
+ });
+
+ it('still follows a controlled selectedIndex while disabled', () => {
+ const { rerender } = render(
+
+ );
+ rerender();
+ expect(getLatestNativeDrumPickerProps()?.selectedIndex).toBe(2);
+ });
+});
diff --git a/src/__tests__/snapshots/__snapshots__/DateDrumPicker.snapshot.test.tsx.snap b/src/__tests__/snapshots/__snapshots__/DateDrumPicker.snapshot.test.tsx.snap
index 6580469..fb75787 100644
--- a/src/__tests__/snapshots/__snapshots__/DateDrumPicker.snapshot.test.tsx.snap
+++ b/src/__tests__/snapshots/__snapshots__/DateDrumPicker.snapshot.test.tsx.snap
@@ -18,6 +18,7 @@ exports[`DateDrumPicker snapshots matches day-month-year mode 1`] = `
circular={false}
collapsable={false}
containerBackgroundColor="transparent"
+ disabled={false}
enableScrollByTapOnItem={false}
hapticFeedback={false}
itemBackgroundColor="transparent"
@@ -86,6 +87,7 @@ exports[`DateDrumPicker snapshots matches day-month-year mode 1`] = `
circular={false}
collapsable={false}
containerBackgroundColor="transparent"
+ disabled={false}
enableScrollByTapOnItem={false}
hapticFeedback={false}
itemBackgroundColor="transparent"
@@ -135,6 +137,7 @@ exports[`DateDrumPicker snapshots matches day-month-year mode 1`] = `
circular={false}
collapsable={false}
containerBackgroundColor="transparent"
+ disabled={false}
enableScrollByTapOnItem={false}
hapticFeedback={false}
itemBackgroundColor="transparent"
diff --git a/src/__tests__/snapshots/__snapshots__/DrumPicker.snapshot.test.tsx.snap b/src/__tests__/snapshots/__snapshots__/DrumPicker.snapshot.test.tsx.snap
index ead6454..2c3fc03 100644
--- a/src/__tests__/snapshots/__snapshots__/DrumPicker.snapshot.test.tsx.snap
+++ b/src/__tests__/snapshots/__snapshots__/DrumPicker.snapshot.test.tsx.snap
@@ -6,6 +6,7 @@ exports[`DrumPicker snapshots matches default render 1`] = `
circular={false}
collapsable={false}
containerBackgroundColor="transparent"
+ disabled={false}
enableScrollByTapOnItem={false}
hapticFeedback={false}
itemBackgroundColor="transparent"
diff --git a/src/types.ts b/src/types.ts
index d979812..45f8674 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -142,6 +142,20 @@ export type DrumPickerProps = {
itemBackgroundColor?: string;
containerBackgroundColor?: string;
hapticFeedback?: boolean;
+ /**
+ * Block user interaction. The wheel stops responding to drags and taps,
+ * and swallows touches instead of passing them to views underneath.
+ *
+ * Scrolling driven from your own code is unaffected: `selectedIndex`
+ * updates and the `scrollToIndex` / `scrollToValue` ref methods still work,
+ * which is what you usually want while a form is submitting.
+ *
+ * Appearance is left to you — combine with `textColor` or a wrapping
+ * `` if the picker should also look inert.
+ *
+ * @default false
+ */
+ disabled?: boolean;
/**
* When true, tapping an item above or below the center
* scrolls the picker to that item.