Skip to content

Commit d8ae01b

Browse files
authored
Merge pull request #77 from troberts-28/feat/improve-styling-options
Feat/improve styling options
2 parents dff150e + d02544f commit d8ae01b

13 files changed

Lines changed: 439 additions & 32 deletions

File tree

.prettierignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ node_modules
44
.yarn
55
examples/example-bare/android
66
examples/example-bare/ios
7+
*.md

README.md

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -464,14 +464,13 @@ return (
464464
pickerItem: {
465465
fontSize: 34,
466466
},
467-
pickerLabelContainer: {
468-
marginTop: -4,
469-
right: 0,
470-
left: undefined,
471-
},
472467
pickerLabel: {
473468
fontSize: 32,
474469
},
470+
pickerLabelContainer: {
471+
marginTop: -4,
472+
},
473+
pickerLabelGap: 23,
475474
pickerContainer: {
476475
paddingHorizontal: 50,
477476
},
@@ -502,7 +501,7 @@ return (
502501
secondLabel="sec"
503502
styles={{
504503
theme: "light",
505-
labelOffsetPercentage: 0,
504+
pickerLabelGap: 8,
506505
pickerItem: {
507506
fontSize: 34,
508507
},
@@ -582,14 +581,18 @@ return (
582581

583582
#### Custom Styles 👗
584583

585-
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:
584+
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:
585+
586+
| Style Prop | Description | Type |
587+
| :---------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------- | :----------------: |
588+
| theme | Theme of the component | "light" \| "dark" |
589+
| backgroundColor | Main background color | string |
590+
| text | Base text style | TextStyle |
591+
| 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`\* |
592+
| 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`\* |
593+
| labelOffsetPercentage **(DEPRECATED)** | Percentage offset for horizontal label positioning relative to the picker (use `pickerLabelGap` instead) | number |
586594

587-
| Style Prop | Description | Type |
588-
| :-------------------: | :----------------------------------------------------------------------- | :---------------: |
589-
| theme | Theme of the component | "light" \| "dark" |
590-
| backgroundColor | Main background color | string |
591-
| text | Base text style | TextStyle |
592-
| labelOffsetPercentage | Percentage offset for horizonal label positioning relative to the picker | number |
595+
**\*`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.
593596

594597
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.
595598

examples/example-bare/App.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -253,10 +253,9 @@ export default function App() {
253253
fontSize: 32,
254254
},
255255
pickerLabelContainer: {
256-
left: undefined,
257256
marginTop: -4,
258-
right: 0,
259257
},
258+
pickerLabelGap: 23,
260259
theme: "dark",
261260
}}
262261
/>
@@ -275,7 +274,6 @@ export default function App() {
275274
pickerFeedback={pickerFeedback}
276275
secondLabel="sec"
277276
styles={{
278-
labelOffsetPercentage: 0,
279277
pickerContainer: {
280278
paddingHorizontal: 50,
281279
},
@@ -285,6 +283,7 @@ export default function App() {
285283
pickerLabel: {
286284
fontSize: 26,
287285
},
286+
pickerLabelGap: 8,
288287
theme: "light",
289288
}}
290289
/>

examples/example-expo/App.tsx

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,6 @@ export default function App() {
119119
</TouchableOpacity>
120120
<TimerPickerModal
121121
closeOnOverlayPress
122-
hideCancelButton
123122
LinearGradient={LinearGradient}
124123
modalProps={{ overlayOpacity: 0.2 }}
125124
modalTitle="Set Alarm"
@@ -166,7 +165,12 @@ export default function App() {
166165
}}
167166
pickerFeedback={pickerFeedback}
168167
setIsVisible={setShowPickerExample2}
169-
styles={{ theme: "light" }}
168+
styles={{
169+
theme: "light",
170+
pickerColumnWidth: {
171+
hours: 90,
172+
},
173+
}}
170174
use12HourPicker
171175
visible={showPickerExample2}
172176
/>
@@ -241,10 +245,9 @@ export default function App() {
241245
fontSize: 32,
242246
},
243247
pickerLabelContainer: {
244-
left: undefined,
245248
marginTop: -4,
246-
right: 0,
247249
},
250+
pickerLabelGap: 23,
248251
theme: "dark",
249252
}}
250253
/>
@@ -263,7 +266,6 @@ export default function App() {
263266
pickerFeedback={pickerFeedback}
264267
secondLabel="sec"
265268
styles={{
266-
labelOffsetPercentage: 0,
267269
pickerContainer: {
268270
paddingHorizontal: 50,
269271
},
@@ -273,6 +275,7 @@ export default function App() {
273275
pickerLabel: {
274276
fontSize: 26,
275277
},
278+
pickerLabelGap: 8,
276279
theme: "light",
277280
}}
278281
/>
@@ -350,6 +353,7 @@ export default function App() {
350353
horizontal
351354
onMomentumScrollEnd={onMomentumScrollEnd}
352355
pagingEnabled
356+
showsHorizontalScrollIndicator={false}
353357
>
354358
{renderExample1}
355359
{renderExample2}

package.json

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
"lint:fix": "eslint src/ examples/ --fix",
3131
"format": "prettier --check ./src ./examples",
3232
"format:fix": "prettier --write ./src ./examples",
33-
"prepare": "yarn build"
33+
"prepare": "simple-git-hooks"
3434
},
3535
"homepage": "https://github.com/troberts-28/react-native-timer-picker",
3636
"bugs": {
@@ -108,12 +108,14 @@
108108
"eslint-plugin-react": "^7.37.5",
109109
"eslint-plugin-react-hooks": "^5.2.0",
110110
"jest": "^29.0.0",
111+
"lint-staged": "^16.2.7",
111112
"metro-react-native-babel-preset": "^0.71.1",
112113
"prettier": "2.8.8",
113114
"react": "18.2.0",
114115
"react-native": "0.72.0",
115116
"react-native-builder-bob": "^0.18.3",
116117
"react-test-renderer": "18.2.0",
118+
"simple-git-hooks": "^2.13.1",
117119
"typescript": "~5.8.0",
118120
"typescript-eslint": "^8.33.0"
119121
},
@@ -126,6 +128,18 @@
126128
"typescript"
127129
]
128130
},
131+
"simple-git-hooks": {
132+
"pre-commit": "npx lint-staged"
133+
},
134+
"lint-staged": {
135+
"*.{ts,tsx,js,jsx}": [
136+
"eslint --fix",
137+
"prettier --write"
138+
],
139+
"*.{json}": [
140+
"prettier --write"
141+
]
142+
},
129143
"eslintIgnore": [
130144
"node_modules/",
131145
"dist/"

src/components/DurationScroll/DurationScroll.tsx

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,10 @@ const DurationScroll = forwardRef<DurationScrollRef, DurationScrollProps>((props
4545
onDurationChange,
4646
padNumbersWithZero = false,
4747
padWithNItems,
48+
pickerColumnWidth,
4849
pickerFeedback,
4950
pickerGradientOverlayProps,
51+
pickerLabelGap,
5052
pmLabel,
5153
repeatNumbersNTimes = 3,
5254
repeatNumbersNTimesNotExplicitlySet,
@@ -55,6 +57,24 @@ const DurationScroll = forwardRef<DurationScrollRef, DurationScrollProps>((props
5557
testID,
5658
} = props;
5759

60+
const labelPositionStyle = useMemo(() => {
61+
// When the style already has an explicit `left` (from legacy percentage system or
62+
// user override), don't apply pixel-based positioning.
63+
if (styles.pickerLabelContainer.left != null) {
64+
return undefined;
65+
}
66+
67+
const gap = pickerLabelGap ?? 6;
68+
const fontSize = styles.pickerItem.fontSize ?? 25;
69+
const maxDigitCount = Math.max(2, String(maximumValue).length);
70+
const halfNumberWidth = (maxDigitCount * fontSize * 0.55) / 2;
71+
72+
return {
73+
left: "50%" as const,
74+
marginLeft: halfNumberWidth + gap,
75+
};
76+
}, [maximumValue, pickerLabelGap, styles.pickerItem.fontSize, styles.pickerLabelContainer.left]);
77+
5878
const numberOfItems = useMemo(() => {
5979
// guard against negative maximum values
6080
if (maximumValue < 0) {
@@ -210,6 +230,7 @@ const DurationScroll = forwardRef<DurationScrollRef, DurationScrollProps>((props
210230
amLabel={amLabel}
211231
is12HourPicker={is12HourPicker}
212232
item={item}
233+
pickerAmPmPositionStyle={labelPositionStyle}
213234
pmLabel={pmLabel}
214235
selectedValue={selectedValue}
215236
styles={styles}
@@ -221,6 +242,7 @@ const DurationScroll = forwardRef<DurationScrollRef, DurationScrollProps>((props
221242
allowFontScaling,
222243
amLabel,
223244
is12HourPicker,
245+
labelPositionStyle,
224246
pmLabel,
225247
selectedValue,
226248
styles,
@@ -488,7 +510,7 @@ const DurationScroll = forwardRef<DurationScrollRef, DurationScrollProps>((props
488510
viewabilityConfigCallbackPairs={viewabilityConfigCallbackPairs}
489511
windowSize={numberOfItemsToShow}
490512
/>
491-
<View pointerEvents="none" style={styles.pickerLabelContainer}>
513+
<View pointerEvents="none" style={[styles.pickerLabelContainer, labelPositionStyle]}>
492514
{typeof label === "string" ? (
493515
<Text allowFontScaling={allowFontScaling} style={styles.pickerLabel}>
494516
{label}
@@ -508,6 +530,7 @@ const DurationScroll = forwardRef<DurationScrollRef, DurationScrollProps>((props
508530
initialScrollIndex,
509531
isDisabled,
510532
label,
533+
labelPositionStyle,
511534
numberOfItemsToShow,
512535
numbersForFlatList,
513536
onMomentumScrollEnd,
@@ -571,6 +594,7 @@ const DurationScroll = forwardRef<DurationScrollRef, DurationScrollProps>((props
571594
pointerEvents={isDisabled ? "none" : undefined}
572595
style={[
573596
styles.durationScrollFlatListContainer,
597+
pickerColumnWidth != null && { flex: 0, width: pickerColumnWidth },
574598
{
575599
height: styles.pickerItemContainer.height * numberOfItemsToShow,
576600
},

src/components/DurationScroll/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,10 @@ export interface DurationScrollProps {
2828
onDurationChange: (duration: number) => void;
2929
padNumbersWithZero?: boolean;
3030
padWithNItems: number;
31+
pickerColumnWidth?: number;
3132
pickerFeedback?: () => void | Promise<void>;
3233
pickerGradientOverlayProps?: Partial<LinearGradientProps>;
34+
pickerLabelGap?: number;
3335
pmLabel?: string;
3436
repeatNumbersNTimes?: number;
3537
repeatNumbersNTimesNotExplicitlySet: boolean;

src/components/PickerItem/PickerItem.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ interface PickerItemProps {
1111
amLabel?: string;
1212
is12HourPicker?: boolean;
1313
item: string;
14+
pickerAmPmPositionStyle?: { left: "50%"; marginLeft: number };
1415
pmLabel?: string;
1516
selectedValue?: number;
1617
styles: ReturnType<typeof generateStyles>;
@@ -24,6 +25,7 @@ const PickerItem = React.memo<PickerItemProps>(
2425
amLabel,
2526
is12HourPicker,
2627
item,
28+
pickerAmPmPositionStyle,
2729
pmLabel,
2830
selectedValue,
2931
styles,
@@ -57,7 +59,7 @@ const PickerItem = React.memo<PickerItemProps>(
5759
{stringItem}
5860
</Text>
5961
{is12HourPicker && (
60-
<View style={styles.pickerAmPmContainer}>
62+
<View style={[styles.pickerAmPmContainer, pickerAmPmPositionStyle]}>
6163
<Text allowFontScaling={allowFontScaling} style={styles.pickerAmPmLabel}>
6264
{isAm ? amLabel : pmLabel}
6365
</Text>

src/components/TimerPicker/TimerPicker.tsx

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,24 @@ import { getSafeInitialValue } from "../../utils/getSafeInitialValue";
1313
import DurationScroll from "../DurationScroll";
1414
import type { DurationScrollRef } from "../DurationScroll";
1515
import { generateStyles } from "./styles";
16+
import type { PerColumnValue, PickerColumn } from "./styles";
1617
import type { TimerPickerProps, TimerPickerRef } from "./types";
1718

19+
const resolvePerColumn = (
20+
value: PerColumnValue | undefined,
21+
column: PickerColumn
22+
): number | undefined => {
23+
if (value == null) {
24+
return undefined;
25+
}
26+
27+
if (typeof value === "number") {
28+
return value;
29+
}
30+
31+
return value[column];
32+
};
33+
1834
const TimerPicker = forwardRef<TimerPickerRef, TimerPickerProps>((props, ref) => {
1935
const {
2036
aggressivelyGetLatestDuration = false,
@@ -80,7 +96,24 @@ const TimerPicker = forwardRef<TimerPickerRef, TimerPickerProps>((props, ref) =>
8096
'The "clickSoundAsset" prop is deprecated and will be removed in a future version. Please use the "pickerFeedback" prop instead.'
8197
);
8298
}
83-
}, [otherProps.Audio, otherProps.Haptics, otherProps.clickSoundAsset]);
99+
if (customStyles?.labelOffsetPercentage != null) {
100+
if (customStyles?.pickerLabelGap != null) {
101+
console.warn(
102+
"labelOffsetPercentage is ignored when pickerLabelGap is set. Please remove labelOffsetPercentage."
103+
);
104+
} else {
105+
console.warn(
106+
'The "labelOffsetPercentage" style prop is deprecated and will be removed in a future version. Please use the "pickerLabelGap" style prop instead.'
107+
);
108+
}
109+
}
110+
}, [
111+
otherProps.Audio,
112+
otherProps.Haptics,
113+
otherProps.clickSoundAsset,
114+
customStyles?.labelOffsetPercentage,
115+
customStyles?.pickerLabelGap,
116+
]);
84117

85118
const safePadWithNItems = useMemo(() => {
86119
if (padWithNItems < 0 || isNaN(padWithNItems)) {
@@ -107,6 +140,9 @@ const TimerPicker = forwardRef<TimerPickerRef, TimerPickerProps>((props, ref) =>
107140
[initialValue?.days, initialValue?.hours, initialValue?.minutes, initialValue?.seconds]
108141
);
109142

143+
const pickerLabelGap = customStyles?.pickerLabelGap;
144+
const pickerColumnWidth = customStyles?.pickerColumnWidth;
145+
110146
const styles = useMemo(
111147
() => generateStyles(customStyles),
112148

@@ -187,6 +223,8 @@ const TimerPicker = forwardRef<TimerPickerRef, TimerPickerProps>((props, ref) =>
187223
onDurationChange={setSelectedDays}
188224
padNumbersWithZero={padDaysWithZero}
189225
padWithNItems={safePadWithNItems}
226+
pickerColumnWidth={resolvePerColumn(pickerColumnWidth, "days")}
227+
pickerLabelGap={resolvePerColumn(pickerLabelGap, "days")}
190228
repeatNumbersNTimes={repeatDayNumbersNTimes}
191229
repeatNumbersNTimesNotExplicitlySet={props?.repeatDayNumbersNTimes === undefined}
192230
selectedValue={selectedDays}
@@ -213,6 +251,8 @@ const TimerPicker = forwardRef<TimerPickerRef, TimerPickerProps>((props, ref) =>
213251
onDurationChange={setSelectedHours}
214252
padNumbersWithZero={padHoursWithZero}
215253
padWithNItems={safePadWithNItems}
254+
pickerColumnWidth={resolvePerColumn(pickerColumnWidth, "hours")}
255+
pickerLabelGap={resolvePerColumn(pickerLabelGap, "hours")}
216256
pmLabel={pmLabel}
217257
repeatNumbersNTimes={repeatHourNumbersNTimes}
218258
repeatNumbersNTimesNotExplicitlySet={props?.repeatHourNumbersNTimes === undefined}
@@ -238,6 +278,8 @@ const TimerPicker = forwardRef<TimerPickerRef, TimerPickerProps>((props, ref) =>
238278
onDurationChange={setSelectedMinutes}
239279
padNumbersWithZero={padMinutesWithZero}
240280
padWithNItems={safePadWithNItems}
281+
pickerColumnWidth={resolvePerColumn(pickerColumnWidth, "minutes")}
282+
pickerLabelGap={resolvePerColumn(pickerLabelGap, "minutes")}
241283
repeatNumbersNTimes={repeatMinuteNumbersNTimes}
242284
repeatNumbersNTimesNotExplicitlySet={props?.repeatMinuteNumbersNTimes === undefined}
243285
selectedValue={selectedMinutes}
@@ -262,6 +304,8 @@ const TimerPicker = forwardRef<TimerPickerRef, TimerPickerProps>((props, ref) =>
262304
onDurationChange={setSelectedSeconds}
263305
padNumbersWithZero={padSecondsWithZero}
264306
padWithNItems={safePadWithNItems}
307+
pickerColumnWidth={resolvePerColumn(pickerColumnWidth, "seconds")}
308+
pickerLabelGap={resolvePerColumn(pickerLabelGap, "seconds")}
265309
repeatNumbersNTimes={repeatSecondNumbersNTimes}
266310
repeatNumbersNTimesNotExplicitlySet={props?.repeatSecondNumbersNTimes === undefined}
267311
selectedValue={selectedSeconds}

0 commit comments

Comments
 (0)