Skip to content
Merged
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
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ node_modules
.yarn
examples/example-bare/android
examples/example-bare/ios
*.md
35 changes: 22 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Works with Expo and bare React Native apps ✅

Includes iOS-style haptic and audio feedback 🍏

- [React Native Timer Picker ⏰🕰️⏳](#react-native-timer-picker-️)
- [Demos 📱](#demos-)
- [Installation 🚀](#installation-)
- [Peer Dependencies 👶](#peer-dependencies-)
Expand Down Expand Up @@ -289,6 +290,9 @@ return (
setIsVisible={setShowPicker}
styles={{
theme: "light",
pickerColumnWidth: {
hours: 90,
},
}}
use12HourPicker
visible={showPicker}
Expand Down Expand Up @@ -426,6 +430,8 @@ return (
setIsVisible={setShowPicker}
styles={{
theme: "dark",
pickerLabelGap: 10,
text: { fontSize: 18 },
}}
visible={showPicker}
/>
Expand Down Expand Up @@ -464,14 +470,13 @@ return (
pickerItem: {
fontSize: 34,
},
pickerLabelContainer: {
marginTop: -4,
right: 0,
left: undefined,
},
pickerLabel: {
fontSize: 32,
},
pickerLabelContainer: {
marginTop: -4,
},
pickerLabelGap: 23,
pickerContainer: {
paddingHorizontal: 50,
},
Expand Down Expand Up @@ -502,7 +507,7 @@ return (
secondLabel="sec"
styles={{
theme: "light",
labelOffsetPercentage: 0,
pickerLabelGap: 8,
pickerItem: {
fontSize: 34,
},
Expand Down Expand Up @@ -582,14 +587,18 @@ return (

#### Custom Styles 👗

The component should look good straight out of the box, but you can use these styles to make it fit in with your App's theme:
The component should look good straight out of the box, but you can use these easy styles to make it fit in with your App's theme:

| Style Prop | Description | Type |
| :---------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------- | :----------------: |
| theme | Theme of the component | "light" \| "dark" |
| backgroundColor | Main background color | string |
| text | Base text style | TextStyle |
| pickerLabelGap | Pixel gap between the label and the picker number column. Can be a single number or a per-column object (e.g. `{ hours: 10, minutes: 8 }`). Default: `6` | `PerColumnValue`\* |
| pickerColumnWidth | Width of individual picker columns in pixels. Can be a single number or a per-column object. Overrides default flex-based sizing when set | `PerColumnValue`\* |
| labelOffsetPercentage **(DEPRECATED)** | Percentage offset for horizontal label positioning relative to the picker (use `pickerLabelGap` instead) | number |

| Style Prop | Description | Type |
| :-------------------: | :----------------------------------------------------------------------- | :---------------: |
| theme | Theme of the component | "light" \| "dark" |
| backgroundColor | Main background color | string |
| text | Base text style | TextStyle |
| labelOffsetPercentage | Percentage offset for horizonal label positioning relative to the picker | number |
**\*`PerColumnValue` type:** `number | { days?: number, hours?: number, minutes?: number, seconds?: number }` — pass a single number for all columns, or an object to set values per column. Omitted columns use the default.

For deeper style customization, you can supply the following custom styles to adjust the component in any way. These are applied on top of the default styling so take a look at those [styles](src/components/TimerPicker/styles.ts) if something isn't adjusting in the way you'd expect.

Expand Down
10 changes: 7 additions & 3 deletions examples/example-bare/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,9 @@ export default function App() {
pickerFeedback={pickerFeedback}
setIsVisible={setShowPickerExample2}
styles={{
pickerColumnWidth: {
hours: 90,
},
theme: "light",
}}
use12HourPicker
Expand Down Expand Up @@ -217,6 +220,8 @@ export default function App() {
pickerFeedback={pickerFeedback}
setIsVisible={setShowPickerExample3}
styles={{
pickerLabelGap: 10,
text: { fontSize: 18 },
theme: "dark",
}}
visible={showPickerExample3}
Expand Down Expand Up @@ -253,10 +258,9 @@ export default function App() {
fontSize: 32,
},
pickerLabelContainer: {
left: undefined,
marginTop: -4,
right: 0,
},
pickerLabelGap: 23,
theme: "dark",
}}
/>
Expand All @@ -275,7 +279,6 @@ export default function App() {
pickerFeedback={pickerFeedback}
secondLabel="sec"
styles={{
labelOffsetPercentage: 0,
pickerContainer: {
paddingHorizontal: 50,
},
Expand All @@ -285,6 +288,7 @@ export default function App() {
pickerLabel: {
fontSize: 26,
},
pickerLabelGap: 8,
theme: "light",
}}
/>
Expand Down
20 changes: 14 additions & 6 deletions examples/example-expo/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,6 @@ export default function App() {
</TouchableOpacity>
<TimerPickerModal
closeOnOverlayPress
hideCancelButton
LinearGradient={LinearGradient}
modalProps={{ overlayOpacity: 0.2 }}
modalTitle="Set Alarm"
Expand Down Expand Up @@ -166,7 +165,12 @@ export default function App() {
}}
pickerFeedback={pickerFeedback}
setIsVisible={setShowPickerExample2}
styles={{ theme: "light" }}
styles={{
theme: "light",
pickerColumnWidth: {
hours: 90,
},
}}
use12HourPicker
visible={showPickerExample2}
/>
Expand Down Expand Up @@ -206,7 +210,11 @@ export default function App() {
}}
pickerFeedback={pickerFeedback}
setIsVisible={setShowPickerExample3}
styles={{ theme: "dark" }}
styles={{
pickerLabelGap: 10,
text: { fontWeight: "bold" },
theme: "dark",
}}
visible={showPickerExample3}
/>
</View>
Expand Down Expand Up @@ -241,10 +249,9 @@ export default function App() {
fontSize: 32,
},
pickerLabelContainer: {
left: undefined,
marginTop: -4,
right: 0,
},
pickerLabelGap: 23,
theme: "dark",
}}
/>
Expand All @@ -263,7 +270,6 @@ export default function App() {
pickerFeedback={pickerFeedback}
secondLabel="sec"
styles={{
labelOffsetPercentage: 0,
pickerContainer: {
paddingHorizontal: 50,
},
Expand All @@ -273,6 +279,7 @@ export default function App() {
pickerLabel: {
fontSize: 26,
},
pickerLabelGap: 8,
theme: "light",
}}
/>
Expand Down Expand Up @@ -350,6 +357,7 @@ export default function App() {
horizontal
onMomentumScrollEnd={onMomentumScrollEnd}
pagingEnabled
showsHorizontalScrollIndicator={false}
>
{renderExample1}
{renderExample2}
Expand Down
18 changes: 16 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"url": "https://github.com/troberts-28"
},
"license": "MIT",
"version": "2.5.0",
"version": "2.6.0",
"main": "dist/commonjs/index.js",
"module": "dist/module/index.js",
"types": "dist/typescript/index.d.ts",
Expand All @@ -30,7 +30,7 @@
"lint:fix": "eslint src/ examples/ --fix",
"format": "prettier --check ./src ./examples",
"format:fix": "prettier --write ./src ./examples",
"prepare": "yarn build"
"prepare": "simple-git-hooks"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prepare script no longer builds package

High Severity

The prepare script was changed from yarn build to simple-git-hooks. The prepare script runs on yarn install / npm install, so the package no longer builds automatically. After cloning and running yarn install, the dist/ folder will not exist, and the examples (and any devs importing the package) will fail because main points to dist/commonjs/index.js.

Fix in Cursor Fix in Web

},
"homepage": "https://github.com/troberts-28/react-native-timer-picker",
"bugs": {
Expand Down Expand Up @@ -108,12 +108,14 @@
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0",
"jest": "^29.0.0",
"lint-staged": "^16.2.7",
"metro-react-native-babel-preset": "^0.71.1",
"prettier": "2.8.8",
"react": "18.2.0",
"react-native": "0.72.0",
"react-native-builder-bob": "^0.18.3",
"react-test-renderer": "18.2.0",
"simple-git-hooks": "^2.13.1",
"typescript": "~5.8.0",
"typescript-eslint": "^8.33.0"
},
Expand All @@ -126,6 +128,18 @@
"typescript"
]
},
"simple-git-hooks": {
"pre-commit": "npx lint-staged"
},
"lint-staged": {
"*.{ts,tsx,js,jsx}": [
"eslint --fix",
"prettier --write"
],
"*.{json}": [
"prettier --write"
]
},
"eslintIgnore": [
"node_modules/",
"dist/"
Expand Down
26 changes: 25 additions & 1 deletion src/components/DurationScroll/DurationScroll.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,10 @@ const DurationScroll = forwardRef<DurationScrollRef, DurationScrollProps>((props
onDurationChange,
padNumbersWithZero = false,
padWithNItems,
pickerColumnWidth,
pickerFeedback,
pickerGradientOverlayProps,
pickerLabelGap,
pmLabel,
repeatNumbersNTimes = 3,
repeatNumbersNTimesNotExplicitlySet,
Expand All @@ -55,6 +57,24 @@ const DurationScroll = forwardRef<DurationScrollRef, DurationScrollProps>((props
testID,
} = props;

const labelPositionStyle = useMemo(() => {
// When the style already has an explicit `left` (from legacy percentage system or
// user override), don't apply pixel-based positioning.
if (styles.pickerLabelContainer.left != null) {
return undefined;
}

const gap = pickerLabelGap ?? 6;
const fontSize = styles.pickerItem.fontSize ?? 25;
const maxDigitCount = Math.max(2, String(maximumValue).length);
const halfNumberWidth = (maxDigitCount * fontSize * 0.55) / 2;

return {
left: "50%" as const,
marginLeft: halfNumberWidth + gap,
};
}, [maximumValue, pickerLabelGap, styles.pickerItem.fontSize, styles.pickerLabelContainer.left]);

const numberOfItems = useMemo(() => {
// guard against negative maximum values
if (maximumValue < 0) {
Expand Down Expand Up @@ -210,6 +230,7 @@ const DurationScroll = forwardRef<DurationScrollRef, DurationScrollProps>((props
amLabel={amLabel}
is12HourPicker={is12HourPicker}
item={item}
pickerAmPmPositionStyle={labelPositionStyle}
pmLabel={pmLabel}
selectedValue={selectedValue}
styles={styles}
Expand All @@ -221,6 +242,7 @@ const DurationScroll = forwardRef<DurationScrollRef, DurationScrollProps>((props
allowFontScaling,
amLabel,
is12HourPicker,
labelPositionStyle,
pmLabel,
selectedValue,
styles,
Expand Down Expand Up @@ -488,7 +510,7 @@ const DurationScroll = forwardRef<DurationScrollRef, DurationScrollProps>((props
viewabilityConfigCallbackPairs={viewabilityConfigCallbackPairs}
windowSize={numberOfItemsToShow}
/>
<View pointerEvents="none" style={styles.pickerLabelContainer}>
<View pointerEvents="none" style={[styles.pickerLabelContainer, labelPositionStyle]}>
{typeof label === "string" ? (
<Text allowFontScaling={allowFontScaling} style={styles.pickerLabel}>
{label}
Expand All @@ -508,6 +530,7 @@ const DurationScroll = forwardRef<DurationScrollRef, DurationScrollProps>((props
initialScrollIndex,
isDisabled,
label,
labelPositionStyle,
numberOfItemsToShow,
numbersForFlatList,
onMomentumScrollEnd,
Expand Down Expand Up @@ -571,6 +594,7 @@ const DurationScroll = forwardRef<DurationScrollRef, DurationScrollProps>((props
pointerEvents={isDisabled ? "none" : undefined}
style={[
styles.durationScrollFlatListContainer,
pickerColumnWidth != null && { flex: 0, width: pickerColumnWidth },
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pickerColumnWidth zero creates zero-width column

Low Severity

When pickerColumnWidth is 0 (or a per-column object with 0 for a column), the condition pickerColumnWidth != null is true, so { flex: 0, width: 0 } is applied and the column gets zero width, making it effectively invisible.

Fix in Cursor Fix in Web

{
height: styles.pickerItemContainer.height * numberOfItemsToShow,
},
Expand Down
2 changes: 2 additions & 0 deletions src/components/DurationScroll/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,10 @@ export interface DurationScrollProps {
onDurationChange: (duration: number) => void;
padNumbersWithZero?: boolean;
padWithNItems: number;
pickerColumnWidth?: number;
pickerFeedback?: () => void | Promise<void>;
pickerGradientOverlayProps?: Partial<LinearGradientProps>;
pickerLabelGap?: number;
pmLabel?: string;
repeatNumbersNTimes?: number;
repeatNumbersNTimesNotExplicitlySet: boolean;
Expand Down
4 changes: 3 additions & 1 deletion src/components/PickerItem/PickerItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ interface PickerItemProps {
amLabel?: string;
is12HourPicker?: boolean;
item: string;
pickerAmPmPositionStyle?: { left: "50%"; marginLeft: number };
pmLabel?: string;
selectedValue?: number;
styles: ReturnType<typeof generateStyles>;
Expand All @@ -24,6 +25,7 @@ const PickerItem = React.memo<PickerItemProps>(
amLabel,
is12HourPicker,
item,
pickerAmPmPositionStyle,
pmLabel,
selectedValue,
styles,
Expand Down Expand Up @@ -57,7 +59,7 @@ const PickerItem = React.memo<PickerItemProps>(
{stringItem}
</Text>
{is12HourPicker && (
<View style={styles.pickerAmPmContainer}>
<View style={[styles.pickerAmPmContainer, pickerAmPmPositionStyle]}>
<Text allowFontScaling={allowFontScaling} style={styles.pickerAmPmLabel}>
{isAm ? amLabel : pmLabel}
</Text>
Expand Down
Loading