Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,7 @@ android/generated

# React Native Nitro Modules
nitrogen/

# CodeQL local databases (generated by the CodeQL CLI; not source)
codeql-db-js/
codeql-db-*/
160 changes: 158 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ iOS preview coming soon — see the `ios-build` CI job for validation status.
- Custom text colors and sizes
- TypeScript API
- Flexible `DateDrumPicker` wrapper (day / month / year columns)
- Flexible `TimeDrumPicker` wrapper (hour / minute / second / AM·PM columns, 12h or 24h, minute & second intervals)
- Fabric View / New Architecture

## Installation
Expand All @@ -54,7 +55,7 @@ npx react-native run-android
|----------|--------|
| Android | Supported |
| iOS | Supported |
| Web | Not supported |
| Web | Fallback (`<select>` element, accessible, no drum animation) |

Requires **React Native 0.76+** with the **New Architecture** enabled.

Expand Down Expand Up @@ -84,6 +85,23 @@ Requires **React Native 0.76+** with the **New Architecture** enabled.

This package is an **Android Fabric View** library. Use a **development build** or `expo run:android` after `expo prebuild` — not Expo Go.

### Web fallback

On web (Expo Web, `react-native-web`, or SSR contexts), `DrumPicker`
renders a native HTML `<select>` element instead of throwing at module
load. This means:

- The library is **SSR-safe** — `import { DrumPicker } from 'react-native-drum-picker'`
in a server-rendered React app no longer crashes the bundle.
- The web rendering is **keyboard-navigable and screen-reader-friendly by
default** (browser-provided semantics).
- The `onChange` contract matches native — callers read
`event.nativeEvent.index` and `event.nativeEvent.value` the same way
cross-platform, so app code doesn't need a `Platform.OS` branch.

A full drum-style scroll wheel on web is a future enhancement; the
current fallback prioritizes correctness and accessibility.

### React Native 0.81+ event dispatch

If Android Kotlin compile fails with `No value passed for parameter 'uiManagerType'`, upgrade to **0.1.3+** (Fabric `UIManagerType.FABRIC`).
Expand Down Expand Up @@ -157,6 +175,46 @@ const [index, setIndex] = useState(1);
/>
```

### Labeled items: display one thing, receive another

When you want to render human-readable text but receive a typed identifier
(an enum value, database id, country code, etc.) on selection, pass
`{ label, value }` items instead of strings:

```tsx
type CountryCode = 'us' | 'de' | 'jp';

const COUNTRIES: Array<{ label: string; value: CountryCode }> = [
{ label: 'United States', value: 'us' },
{ label: 'Germany', value: 'de' },
{ label: 'Japan', value: 'jp' },
];

<DrumPicker<CountryCode>
items={COUNTRIES}
onChange={(event) => {
// event.nativeEvent.value === 'United States' (the label that was shown)
// event.nativeEvent.item === 'us' (the typed value, fully inferred)
setCountry(event.nativeEvent.item);
}}
/>
```

Plain string items keep working exactly as before — for them, `item` simply
equals `value`, so `event.nativeEvent.item` is always safe to read.

`value` can be any type — primitives, ids, or full objects:

```tsx
<DrumPicker
items={[
{ label: 'United States', value: { id: 1, iso: 'us' } },
{ label: 'Germany', value: { id: 2, iso: 'de' } },
]}
onChange={(e) => console.log(e.nativeEvent.item.iso)}
/>
```

### `onChange` and expensive side effects

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:
Expand Down Expand Up @@ -220,7 +278,7 @@ Day count follows month/year (e.g. February has 28/29 days).

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `items` | `string[]` | required | Wheel labels |
| `items` | `Array<string \| { label: string; value: T }>` | required | Wheel rows. Strings are used as both label and value; labeled items render `label` and report `value` back on `onChange` |
| `selectedIndex` | `number` | `0` | Selected row index |
| `itemHeight` | `number` | `44` | Row height (dp) |
| `visibleItemCount` | `number` | `5` | Visible rows (odd recommended) |
Expand Down Expand Up @@ -293,6 +351,104 @@ type DateDrumPickerMode =
| 'year-month-day';
```

## TimeDrumPicker

A composed wrapper for picking a time of day. It is built on top of `DrumPicker`, so it
inherits the same native rendering, theming, and haptics — no extra native dependencies.

```tsx
import { TimeDrumPicker } from 'react-native-drum-picker';

function Example() {
const [time, setTime] = useState({ hour: 9, minute: 30 });

return (
<TimeDrumPicker
mode="hour-minute-period"
value={time}
minuteInterval={15}
onChange={setTime}
/>
);
}
```

Hour values in `value` / `onChange` are **always 24-hour** (`0..23`), regardless of
display mode. The component handles the 12h ↔ 24h conversion internally so callers
do not have to track AM/PM state separately.

### `TimeDrumPicker` props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `mode` | `TimeDrumPickerMode` | `'hour-minute'` | Which columns to show |
| `value` | `{ hour?: 0..23; minute?: 0..59; second?: 0..59 }` | now | Controlled value (24h) |
| `onChange` | `(value) => void` | – | Called with the full clamped `{ hour, minute, second }` |
| `hourFormat` | `'12' \| '24'` | inferred from `mode` | Force a specific hour column format |
| `minuteInterval` | `1 \| 2 \| 3 \| 4 \| 5 \| 6 \| 10 \| 12 \| 15 \| 20 \| 30` | `1` | Step between minute items (mirrors `UIDatePicker.minuteInterval`) |
| `secondInterval` | same as `minuteInterval` | `1` | Step between second items |
| `padWithZero` | `boolean` | `true` | Pad single-digit values with `0` |
| `amLabel` / `pmLabel` | `string` | `'AM'` / `'PM'` | Localized period labels |
| `columnTestIDs` | `Partial<Record<'hour' \| 'minute' \| 'second' \| 'period', string>>` | – | Per-column `testID`s |

All shared visual props from `DrumPicker` (`itemHeight`, `visibleItemCount`,
`textColor`, `selectedTextColor`, `textSize`, `selectedTextSize`,
`showSelectionIndicator`, `selectionIndicatorColor`, `selectionIndicatorHeight`,
`backgroundColor`, `itemBackgroundColor`, `containerBackgroundColor`,
`hapticFeedback`) are forwarded to every column.

### `TimeDrumPicker` modes

| Mode | Columns |
|------|---------|
| `hour` | hour |
| `minute` | minute |
| `hour-minute` | hour, minute |
| `hour-minute-second` | hour, minute, second |
| `hour-minute-period` | hour (12h), minute, AM/PM |
| `hour-minute-second-period` | hour (12h), minute, second, AM/PM |

```ts
type TimeDrumPickerMode =
| 'hour'
| 'minute'
| 'hour-minute'
| 'hour-minute-second'
| 'hour-minute-period'
| 'hour-minute-second-period';
```

### Behavior notes

- **Controlled value clamping.** If you pass a value outside the supported range
(e.g. `minute: 53` with `minuteInterval={15}`), the component clamps to the
nearest valid value and calls `onChange` once so your state stays in sync with
what the picker actually shows. This mirrors `DateDrumPicker`'s clamp-and-notify
contract for invalid February dates.
- **Uncontrolled mode.** Omit `value` and the component manages its own state; it
initializes from `new Date()`.
- **12h ↔ 24h.** `value` and `onChange` are always 24-hour. Flipping AM/PM keeps
the displayed 12h hour and shifts the 24h hour by ±12.

## Accessibility

`DrumPicker` accepts an `accessibilityLabel` prop. It is forwarded to the
native view's `accessibilityLabel` and, on web, to the `<select>` element's
`aria-label` (defaults to `Picker`).

`DateDrumPicker` and `TimeDrumPicker` render multiple columns and give each a
distinct default label (`Day` / `Month` / `Year`,
`Hour` / `Minute` / `Second` / `AM/PM`) so assistive technologies can tell the
wheels apart instead of announcing every column as "Picker". Override per
column with `columnAccessibilityLabels`:

```tsx
<TimeDrumPicker
mode="hour-minute"
columnAccessibilityLabels={{ hour: 'Hours', minute: 'Minutes' }}
/>
```

## Styling

Backgrounds are **transparent by default**. Only text and optional indicator lines are visible.
Expand Down
1 change: 0 additions & 1 deletion codeql-db-js/baseline-info.json

This file was deleted.

46 changes: 0 additions & 46 deletions codeql-db-js/codeql-database.yml

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

Loading