87 |
setIsOpen(!isOpen)}
90 | />
91 |
92 |
93 |
94 | )}
95 | >
96 | );
97 | }
98 |
99 | export default TimePicker;
100 |
--------------------------------------------------------------------------------
/src/components/TimePickerSelection.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import HourFormat from './HourFormat';
3 | import HourWheel from './HourWheel';
4 | import MinuteWheel from './MinuteWheel';
5 |
6 | function TimePickerSelection({
7 | pickerDefaultValue,
8 | initialValue,
9 | onChange,
10 | height,
11 | onSave,
12 | onCancel,
13 | cancelButtonText,
14 | saveButtonText,
15 | controllers,
16 | setInputValue,
17 | setIsOpen,
18 | seperator,
19 | use12Hours,
20 | onAmPmChange,
21 | }) {
22 | const initialTimeValue = use12Hours ? initialValue.slice(0, 5) : initialValue;
23 | const [value, setValue] = useState(
24 | initialValue === null ? pickerDefaultValue : initialTimeValue,
25 | );
26 | const [hourFormat, setHourFormat] = useState({
27 | mount: false,
28 | hourFormat: initialValue.slice(6, 8),
29 | });
30 |
31 | useEffect(() => {
32 | if (controllers === false) {
33 | const finalSelectedValue = use12Hours ? `${value} ${hourFormat.hourFormat}` : value;
34 | setInputValue(finalSelectedValue);
35 | onChange(finalSelectedValue);
36 | }
37 | }, [value]);
38 |
39 | useEffect(() => {
40 | if (hourFormat.mount) {
41 | onAmPmChange(hourFormat.hourFormat);
42 | }
43 | }, [hourFormat]);
44 |
45 | const params = {
46 | height,
47 | value,
48 | setValue,
49 | controllers,
50 | use12Hours,
51 | onAmPmChange,
52 | setHourFormat,
53 | hourFormat,
54 | };
55 |
56 | const handleSave = () => {
57 | const finalSelectedValue = use12Hours ? `${value} ${hourFormat.hourFormat}` : value;
58 | setInputValue(finalSelectedValue);
59 | onChange(finalSelectedValue);
60 | onSave(finalSelectedValue);
61 | setIsOpen(false);
62 | };
63 | const handleCancel = () => {
64 | onCancel();
65 | setIsOpen(false);
66 | };
67 |
68 | return (
69 |
70 | {controllers && (
71 |
72 |
78 |
81 |
82 | )}
83 |
87 |
94 |
95 | {seperator &&
:
}
96 |
97 | {use12Hours &&
}
98 |
99 |
100 | );
101 | }
102 |
103 | export default TimePickerSelection;
104 |
--------------------------------------------------------------------------------
/src/helpers/index.js:
--------------------------------------------------------------------------------
1 | export const initialNumbersValue = (heightValue = 54, numbersLength = 24, value = null) => {
2 | const initialValue24hourFormat = [
3 | {
4 | number: '00',
5 | translatedValue: (heightValue * 2).toString(),
6 | selected: false,
7 | },
8 | {
9 | number: '01',
10 | translatedValue: heightValue.toString(),
11 | selected: false,
12 | },
13 | ];
14 |
15 | const initialValue12hourFormat = [
16 | {
17 | number: '00',
18 | translatedValue: heightValue.toString(),
19 | selected: false,
20 | hidden: true,
21 | },
22 | {
23 | number: '01',
24 | translatedValue: heightValue.toString(),
25 | selected: false,
26 | },
27 | ];
28 | const arrayOfSelectedValue =
29 | numbersLength === 13 ? initialValue12hourFormat : initialValue24hourFormat;
30 | let count = 0;
31 | for (let index = 0; index < 3; index++) {
32 | for (let j = 0; j < numbersLength; j++) {
33 | if ((index === 0 && j < 2) || (numbersLength === 13 && j === 0)) {
34 | continue;
35 | }
36 | if (index === 1 && j === value) {
37 | if (j.toString().length === 1) {
38 | arrayOfSelectedValue.push({
39 | number: `0${j.toString()}`,
40 | translatedValue: `-${count}`,
41 | selected: true,
42 | });
43 | } else {
44 | arrayOfSelectedValue.push({
45 | number: j.toString(),
46 | translatedValue: `-${count}`,
47 | selected: true,
48 | });
49 | }
50 | count += heightValue;
51 | continue;
52 | }
53 | if (j.toString().length === 1) {
54 | arrayOfSelectedValue.push({
55 | number: `0${j.toString()}`,
56 | translatedValue: `-${count}`,
57 | selected: false,
58 | });
59 | } else {
60 | arrayOfSelectedValue.push({
61 | number: j.toString(),
62 | translatedValue: `-${count}`,
63 | selected: false,
64 | });
65 | }
66 |
67 | count += heightValue;
68 | }
69 | }
70 |
71 | return arrayOfSelectedValue;
72 | };
73 |
74 | export const returnSelectedValue = (heightValue = 54, numbersLength = 24) => {
75 | const arrayOfSelectedValue = [
76 | {
77 | number: '00',
78 | translatedValue: (heightValue * 2).toString(),
79 | arrayNumber: 0,
80 | },
81 | {
82 | number: '01',
83 | translatedValue: heightValue.toString(),
84 | arrayNumber: 1,
85 | },
86 | ];
87 | let count = 0;
88 | for (let index = 0; index < 3; index++) {
89 | for (let j = 0; j < numbersLength; j++) {
90 | if ((index === 0 && j < 2) || (numbersLength === 13 && j === 0)) {
91 | continue;
92 | }
93 | if (j.toString().length === 1) {
94 | arrayOfSelectedValue.push({
95 | number: `0${j.toString()}`,
96 | translatedValue: `-${count}`,
97 | selected: false,
98 | });
99 | } else {
100 | arrayOfSelectedValue.push({
101 | number: j.toString(),
102 | translatedValue: `-${count}`,
103 | selected: false,
104 | });
105 | }
106 |
107 | count += heightValue;
108 | }
109 | }
110 | return arrayOfSelectedValue;
111 | };
112 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://www.npmjs.com/package/react-ios-time-picker) 
2 |
3 | # React ios time picker
4 |
5 | 
6 |
7 | A modern time picker for your next React app.
8 |
9 | - No moment.js needed
10 | - Zero dependencies and lightweight
11 |
12 | ## install
13 |
14 | ```
15 | npm install react-ios-time-picker
16 | ```
17 |
18 | ## Usage
19 |
20 | ### 24 hours format
21 |
22 | 
23 |
24 | ```js
25 | import React, { useState } from 'react';
26 | import { TimePicker } from 'react-ios-time-picker';
27 |
28 | export default const MyApp = () => {
29 | const [value, setValue] = useState('10:00');
30 |
31 | const onChange = (timeValue) => {
32 | setValue(timeValue);
33 | }
34 |
35 | return (
36 |
37 |
38 |
39 | );
40 | }
41 | ```
42 |
43 | ### 12 hours format
44 |
45 | 
46 |
47 | ```js
48 | import React, { useState } from 'react';
49 | import { TimePicker } from 'react-ios-time-picker';
50 |
51 | export default const MyApp = () => {
52 | const [value, setValue] = useState('10:00 AM');
53 |
54 | const onChange = (timeValue) => {
55 | setValue(timeValue);
56 | }
57 |
58 | return (
59 |
60 |
61 |
62 | );
63 | }
64 | ```
65 |
66 | ## API
67 |
68 | | Name | Type | Default | Description |
69 | | ------------------ | --------------------------------------------- | -------------- | --------------------------------------------------------------- |
70 | | value | String | n/a | Current value. |
71 | | cellHeight | Number | 35 | The height of the cell number. |
72 | | placeHolder | String | `"Selet_time"` | Time input's placeholder. |
73 | | pickerDefaultValue | String | `"00:00"` | The initial value that the picker begin with in the first time. |
74 | | disabled | Boolean | `false` | Whether picker is disabled. |
75 | | isOpen | Boolean | `false` | Whether the time picker should be opened. |
76 | | required | Boolean | `false` | Whether time input should be required. |
77 | | cancelButtonText | String | `"Cancel"` | Cancel button text content |
78 | | saveButtonText | String | `"Save"` | Save button text content |
79 | | controllers | Boolean | `true` | Whether the buttons should be displayed |
80 | | seperator | Boolean | `true` | whether show the colon seperator |
81 | | id | String | n/a | Input time picker id |
82 | | name | String | n/a | Input time picker name |
83 | | use12Hours | Boolean | false | 12 hours display mode |
84 | | inputClassName | String | n/a | Input time picker className |
85 | | popupClassName | String | n/a | time picker popup className |
86 | | onChange | `(value) => alert ('New time is: ', value)` | n/a | Called when select a different value |
87 | | onSave | `(value) => alert ('Time saved is: ', value)` | n/a | When the user clicks on save button |
88 | | onClose | `() => alert('Clock closed')` | n/a | When the user clicks on cancel button |
89 | | onAmPmChange | `(value) => alert('Am/Pm changed : value')` | n/a | called when select an am/pm value |
90 | | onOpen | `() => alert('time picker opened')` | n/a | called when time picker is opened |
91 |
92 | ## Contributions Welcome!
93 |
94 | ```shell
95 | git clone https://github.com/MEddarhri/react-ios-time-picker
96 | cd react-ios-time-picker
97 | ```
98 |
99 | ## License
100 |
101 | The MIT License.
102 |
--------------------------------------------------------------------------------
/src/styles/react-ios-time-picker.css:
--------------------------------------------------------------------------------
1 | * {
2 | margin: 0;
3 | padding: 0;
4 | box-sizing: border-box;
5 | font-family: 'Roboto', sans-serif;
6 | }
7 |
8 | button {
9 | border: none;
10 | background: transparent;
11 | cursor: pointer;
12 | }
13 |
14 | input {
15 | border: none;
16 | background: transparent;
17 | cursor: pointer;
18 | }
19 | input:focus {
20 | outline: none;
21 | }
22 |
23 | .react-ios-time-picker {
24 | margin-bottom: 50px;
25 | border-radius: 12px;
26 | overflow: hidden;
27 | box-shadow: 0 11px 15px #0005;
28 | }
29 |
30 | .react-ios-time-picker-transition {
31 | animation: fade-in 150ms ease-out;
32 | }
33 |
34 | @keyframes fade-in {
35 | 0% {
36 | transform: translateY(150px);
37 | opacity: 0;
38 | }
39 | 100% {
40 | transform: translateY(0);
41 | opacity: 1;
42 | }
43 | }
44 |
45 | .react-ios-time-picker-container {
46 | display: flex;
47 | justify-content: center;
48 | position: relative;
49 | background-color: #1d1d1d;
50 | width: 220px;
51 | overflow: hidden;
52 | /* border-radius: 0 0 15px 17px; */
53 | padding: 20px 0;
54 | /* box-shadow: inset 0px 0px 5px 0px rgba(255, 159, 10, 0.5); */
55 | /* box-shadow: 0 11px 15px -7px rgb(0 0 0 / 20%),
56 | 0 24px 38px 3px rgb(0 0 0 / 14%), 0 9px 46px 8px rgb(0 0 0 / 12%); */
57 | }
58 |
59 | .react-ios-time-picker-hour {
60 | position: relative;
61 | width: 50px;
62 | overflow: hidden;
63 | z-index: 100;
64 | margin-right: 5px;
65 | }
66 |
67 | .react-ios-time-picker-minute {
68 | position: relative;
69 | width: 50px;
70 | overflow: hidden;
71 | z-index: 100;
72 | margin-left: 5px;
73 | }
74 |
75 | .react-ios-time-picker-hour-format {
76 | position: relative;
77 | width: 40px;
78 | overflow: hidden;
79 | z-index: 100;
80 | }
81 |
82 | .react-ios-time-picker-fast {
83 | transition: transform 700ms cubic-bezier(0.13, 0.67, 0.01, 0.94);
84 | }
85 |
86 | .react-ios-time-picker-slow {
87 | transition: transform 600ms cubic-bezier(0.13, 0.67, 0.01, 0.94);
88 | }
89 |
90 | .react-ios-time-picker-selected-overlay {
91 | position: absolute;
92 | border-radius: 6px;
93 | background-color: #2c2c2f;
94 | pointer-events: none;
95 | margin: 0 10px;
96 | left: 0;
97 | right: 0;
98 | z-index: 1;
99 | /* box-shadow: inset 0px 0px 2px 0px rgba(255, 159, 10, 0.3); */
100 | }
101 |
102 | .react-ios-time-picker-top-shadow {
103 | position: absolute;
104 | top: 0;
105 | width: 100%;
106 | background: #0009;
107 | background: linear-gradient(180deg, #0009 0%, #1c1c1c 100%);
108 | }
109 |
110 | .react-ios-time-picker-bottom-shadow {
111 | position: absolute;
112 | bottom: 0;
113 | width: 100%;
114 | background: #0009;
115 | background: linear-gradient(0deg, #0009 0%, hsla(0, 0%, 11%, 1) 100%);
116 | }
117 |
118 | .react-ios-time-picker-cell-hour {
119 | width: 100%;
120 | text-align: center;
121 | display: flex;
122 | justify-content: end;
123 | align-items: center;
124 | user-select: none;
125 | transition: all 100ms linear;
126 | }
127 | .react-ios-time-picker-cell-minute {
128 | width: 100%;
129 | text-align: center;
130 | display: flex;
131 | justify-content: start;
132 | align-items: center;
133 | user-select: none;
134 | transition: all 100ms linear;
135 | }
136 | .react-ios-time-picker-cell-hour-format {
137 | width: 100%;
138 | text-align: center;
139 | display: flex;
140 | justify-content: end;
141 | align-items: center;
142 | user-select: none;
143 | transition: all 100ms linear;
144 | }
145 |
146 | .react-ios-time-picker-cell-inner-hour {
147 | width: fit-content;
148 | height: 100%;
149 | transition: all 100ms linear;
150 | cursor: pointer;
151 | border-radius: 7px;
152 | line-height: 35px;
153 | text-align: center;
154 | display: flex;
155 | justify-content: end;
156 | align-items: center;
157 | font-size: 14px;
158 | color: #666;
159 | padding: 0 10px;
160 | }
161 |
162 | .react-ios-time-picker-cell-inner-hour-format {
163 | width: fit-content;
164 | height: 100%;
165 | transition: all 100ms linear;
166 | cursor: pointer;
167 | border-radius: 7px;
168 | line-height: 35px;
169 | text-align: center;
170 | display: flex;
171 | justify-content: end;
172 | align-items: center;
173 | font-size: 14px;
174 | color: #6a6a6b;
175 | padding: 0 10px;
176 | }
177 |
178 | .react-ios-time-picker-cell-inner-minute {
179 | width: fit-content;
180 | height: 100%;
181 | transition: all 100ms linear;
182 | cursor: pointer;
183 | border-radius: 7px;
184 | line-height: 35px;
185 | text-align: center;
186 | display: flex;
187 | justify-content: start;
188 | align-items: center;
189 | font-size: 14px;
190 | color: #6a6a6b;
191 | padding: 0 10px;
192 | }
193 |
194 | .react-ios-time-picker-cell-inner-hour:hover,
195 | .react-ios-time-picker-cell-inner-minute:hover,
196 | .react-ios-time-picker-cell-inner-hour-format:hover {
197 | background-color: #ff9d0ac9;
198 | color: white;
199 | }
200 |
201 | .react-ios-time-picker-cell-inner-selected {
202 | /* font-weight: 500; */
203 | color: #f7f7f7;
204 | font-size: 16px;
205 | }
206 |
207 | .react-ios-time-picker-cell-inner-hour-format-selected {
208 | font-weight: 400;
209 | color: #f7f7f7;
210 | }
211 |
212 | .react-ios-time-picker-btn-container {
213 | position: relative;
214 | display: flex;
215 | justify-content: space-between;
216 | background-color: #292929;
217 | border-bottom: 1px solid #333;
218 | z-index: 100;
219 | }
220 |
221 | .react-ios-time-picker-btn {
222 | padding: 10px 15px;
223 | font-size: 13px;
224 | color: #fe9f06;
225 | transition: all 150ms linear;
226 | font-weight: 500;
227 | z-index: 1;
228 | }
229 |
230 | .react-ios-time-picker-btn:hover {
231 | opacity: 0.6;
232 | }
233 |
234 | .react-ios-time-picker-btn-cancel {
235 | font-size: 12px;
236 | font-weight: 300;
237 | }
238 |
239 | .react-ios-time-picker-popup {
240 | position: fixed;
241 | top: 0;
242 | bottom: 0;
243 | left: 0;
244 | right: 0;
245 | display: flex;
246 | justify-content: center;
247 | align-items: flex-end;
248 | z-index: 99998;
249 | }
250 |
251 | .react-ios-time-picker-popup-overlay {
252 | position: fixed;
253 | top: 0;
254 | bottom: 0;
255 | left: 0;
256 | right: 0;
257 | }
258 |
259 | .react-ios-time-picker-input {
260 | cursor: text;
261 | padding: 5px 10px;
262 | border-radius: 5px;
263 | border: 1px solid #0005;
264 | }
265 |
266 | .react-ios-time-picker-colon {
267 | display: flex;
268 | justify-content: center;
269 | align-items: center;
270 | height: 100%;
271 | color: #f7f7f7;
272 | position: relative;
273 | z-index: 100;
274 | font-weight: 600;
275 | }
276 |
277 | .react-ios-time-picker-cell-inner-hidden {
278 | opacity: 0;
279 | visibility: hidden;
280 | pointer-events: none;
281 | }
282 |
283 | .react-ios-time-picker-hour-format-transition {
284 | transition: transform 100ms ease-out;
285 | }
286 |
--------------------------------------------------------------------------------
/src/components/HourFormat.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState, useRef } from 'react';
2 | import { initialNumbersValue, returnSelectedValue } from '../helpers';
3 | import PickerEffects from './PickerEffects';
4 |
5 | function HourFormat({ height, value, setValue, onAmPmChange, setHourFormat, hourFormat }) {
6 | const Hours = [
7 | {
8 | number: 'AM',
9 | translatedValue: (height * 2).toString(),
10 | selected: false,
11 | },
12 | {
13 | number: 'PM',
14 | translatedValue: height.toString(),
15 | selected: false,
16 | },
17 | ];
18 |
19 | const [hours, setHours] = useState([
20 | {
21 | number: 'AM',
22 | translatedValue: (height * 2).toString(),
23 | selected: hourFormat.hourFormat === 'AM',
24 | },
25 | {
26 | number: 'PM',
27 | translatedValue: height.toString(),
28 | selected: hourFormat.hourFormat === 'PM',
29 | },
30 | ]);
31 | const mainListRef = useRef(null);
32 | const [cursorPosition, setCursorPosition] = useState(null);
33 | const [firstCursorPosition, setFirstCursorPosition] = useState(null);
34 | const [currentTranslatedValue, setCurrentTranslatedValue] = useState(
35 | parseInt(hours.filter((item) => item.selected === true)[0].translatedValue),
36 | );
37 | const [startCapture, setStartCapture] = useState(false);
38 | const [showFinalTranslate, setShowFinalTranslate] = useState(false);
39 | // start and end times
40 | const [dragStartTime, setDragStartTime] = useState(null);
41 | const [dragEndTime, setDragEndTime] = useState(null);
42 | // drag duration
43 | const [dragDuration, setDragDuration] = useState(null);
44 | // drag type fast or slow
45 | const [dragType, setDragType] = useState(null);
46 | // drag direction
47 | const [dragDirection, setDragDirection] = useState(null);
48 | // selected number
49 | const [selectedNumber, setSelectedNumber] = useState(null);
50 |
51 | const handleMouseDown = (e) => {
52 | setShowFinalTranslate(false);
53 | setFirstCursorPosition(e.clientY);
54 | setStartCapture(true);
55 | setDragStartTime(performance.now());
56 | };
57 |
58 | const handleTouchStart = (e) => {
59 | setShowFinalTranslate(false);
60 | setFirstCursorPosition(e.targetTouches[0].clientY);
61 | setStartCapture(true);
62 | setDragStartTime(performance.now());
63 | };
64 |
65 | const handleMouseUp = (e) => {
66 | setStartCapture(false);
67 | setCurrentTranslatedValue((prev) => prev + cursorPosition);
68 | setShowFinalTranslate(true);
69 | setDragEndTime(performance.now());
70 | if (performance.now() - dragStartTime <= 100) {
71 | setDragType('fast');
72 | } else {
73 | setDragType('slow');
74 | }
75 | if (cursorPosition < 0) {
76 | setDragDirection('down');
77 | } else {
78 | setDragDirection('up');
79 | }
80 | };
81 |
82 | const handleMouseLeave = (e) => {
83 | setStartCapture(false);
84 | setCurrentTranslatedValue((prev) => prev + cursorPosition);
85 | setShowFinalTranslate(true);
86 | setDragEndTime(performance.now());
87 |
88 | if (cursorPosition < 0) {
89 | setDragDirection('down');
90 | } else {
91 | setDragDirection('up');
92 | }
93 | };
94 |
95 | const handleMouseMove = (e) => {
96 | if (startCapture) {
97 | setCursorPosition(e.clientY - firstCursorPosition);
98 | } else {
99 | setCursorPosition(0);
100 | }
101 | };
102 |
103 | const handleTouchMove = (e) => {
104 | if (startCapture) {
105 | setCursorPosition(e.targetTouches[0].clientY - firstCursorPosition);
106 | } else {
107 | setCursorPosition(0);
108 | }
109 | };
110 |
111 | // preview translation
112 | useEffect(() => {
113 | if (startCapture) {
114 | mainListRef.current.style.transform = `translateY(${
115 | currentTranslatedValue + cursorPosition
116 | }px)`;
117 | }
118 | }, [cursorPosition]);
119 |
120 | // final translation here
121 | useEffect(() => {
122 | if (showFinalTranslate) {
123 | setDragDuration(dragEndTime - dragStartTime);
124 |
125 | let finalValue = Math.round(currentTranslatedValue / height) * height;
126 | if (finalValue < height) finalValue = height;
127 | if (finalValue > height * 2) finalValue = height * 2;
128 | mainListRef.current.style.transform = `translateY(${finalValue}px)`;
129 | setCurrentTranslatedValue(finalValue);
130 | setCursorPosition(0);
131 | }
132 | }, [showFinalTranslate]);
133 |
134 | // return to default position after drag end (handleTransitionEnd)
135 | const handleTransitionEnd = (e) => {
136 | if (e.propertyName === 'transform') {
137 | const selectedValueArray = [
138 | {
139 | number: 'AM',
140 | translatedValue: (height * 2).toString(),
141 | arrayNumber: 0,
142 | },
143 | {
144 | number: 'PM',
145 | translatedValue: height.toString(),
146 | arrayNumber: 1,
147 | },
148 | ];
149 | selectedValueArray.map((item) => {
150 | if (parseInt(item.translatedValue) === currentTranslatedValue) {
151 | setSelectedNumber(item.arrayNumber);
152 | setHourFormat({ mount: true, hourFormat: item.number });
153 | setHours(() => {
154 | const newValue = Hours.map((hour) => {
155 | if (
156 | hour.number == item.number &&
157 | hour.translatedValue == currentTranslatedValue
158 | ) {
159 | return {
160 | ...hour,
161 | selected: true,
162 | };
163 | }
164 | return hour;
165 | });
166 | return newValue;
167 | });
168 | }
169 | });
170 | }
171 | };
172 |
173 | // handle click to select number
174 | const handleClickToSelect = (e) => {
175 | if (cursorPosition === 0) {
176 | setCurrentTranslatedValue(parseInt(e.target.dataset.translatedValue));
177 | }
178 | };
179 |
180 | /** *************************** handle wheel scroll ************************* */
181 |
182 | const handleWheelScroll = (e) => {
183 | if (e.deltaY > 0) {
184 | if (currentTranslatedValue <= height) {
185 | setCurrentTranslatedValue((prev) => prev + height);
186 | }
187 | } else if (currentTranslatedValue >= height * 2) {
188 | setCurrentTranslatedValue((prev) => prev - height);
189 | }
190 | };
191 |
192 | return (
193 |
205 | {/*
*/}
206 |
212 | {hours.map((hourObj, index) => (
213 |
218 |
227 | {hourObj.number}
228 |
229 |
230 | ))}
231 |
232 |
233 | );
234 | }
235 |
236 | export default HourFormat;
237 |
--------------------------------------------------------------------------------
/src/components/MinuteWheel.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState, useRef } from 'react';
2 | import { initialNumbersValue, returnSelectedValue } from '../helpers';
3 |
4 | function MinuteWheel({ height, value, setValue }) {
5 | const [hours, setHours] = useState(initialNumbersValue(height, 60, parseInt(value.slice(3, 6))));
6 | const mainListRef = useRef(null);
7 | const [cursorPosition, setCursorPosition] = useState(null);
8 | const [firstCursorPosition, setFirstCursorPosition] = useState(null);
9 | const [currentTranslatedValue, setCurrentTranslatedValue] = useState(
10 | parseInt(
11 | initialNumbersValue(height, 60, parseInt(value.slice(3, 6))).filter(
12 | (item) => item.number === value.slice(3, 6) && item.selected === true,
13 | )[0].translatedValue,
14 | ),
15 | );
16 | const [startCapture, setStartCapture] = useState(false);
17 | const [showFinalTranslate, setShowFinalTranslate] = useState(false);
18 | // start and end times
19 | const [dragStartTime, setDragStartTime] = useState(null);
20 | const [dragEndTime, setDragEndTime] = useState(null);
21 | // drag duration
22 | const [dragDuration, setDragDuration] = useState(null);
23 | // drag type fast or slow
24 | const [dragType, setDragType] = useState(null);
25 | // drag direction
26 | const [dragDirection, setDragDirection] = useState(null);
27 | // selected number
28 | const [selectedNumber, setSelectedNumber] = useState(null);
29 |
30 | const handleMouseDown = (e) => {
31 | setShowFinalTranslate(false);
32 | setFirstCursorPosition(e.clientY);
33 | setStartCapture(true);
34 | setDragStartTime(performance.now());
35 | };
36 |
37 | const handleTouchStart = (e) => {
38 | setShowFinalTranslate(false);
39 | setFirstCursorPosition(e.targetTouches[0].clientY);
40 | setStartCapture(true);
41 | setDragStartTime(performance.now());
42 | };
43 |
44 | const handleMouseUp = (e) => {
45 | setStartCapture(false);
46 | setCurrentTranslatedValue((prev) => prev + cursorPosition);
47 | setShowFinalTranslate(true);
48 | setDragEndTime(performance.now());
49 | if (performance.now() - dragStartTime <= 100) {
50 | setDragType('fast');
51 | } else {
52 | setDragType('slow');
53 | }
54 |
55 | if (cursorPosition < 0) {
56 | setDragDirection('down');
57 | } else {
58 | setDragDirection('up');
59 | }
60 | };
61 |
62 | const handleMouseLeave = (e) => {
63 | setStartCapture(false);
64 | setCurrentTranslatedValue((prev) => prev + cursorPosition);
65 | setShowFinalTranslate(true);
66 | setDragEndTime(performance.now());
67 | if (performance.now() - dragStartTime <= 100) {
68 | setDragType('fast');
69 | } else {
70 | setDragType('slow');
71 | }
72 |
73 | if (cursorPosition < 0) {
74 | setDragDirection('down');
75 | } else {
76 | setDragDirection('up');
77 | }
78 | };
79 |
80 | const handleMouseMove = (e) => {
81 | if (startCapture) {
82 | setCursorPosition(e.clientY - firstCursorPosition);
83 | } else {
84 | setCursorPosition(0);
85 | }
86 | };
87 |
88 | const handleTouchMove = (e) => {
89 | if (startCapture) {
90 | setCursorPosition(e.targetTouches[0].clientY - firstCursorPosition);
91 | } else {
92 | setCursorPosition(0);
93 | }
94 | };
95 |
96 | // preview translation
97 | useEffect(() => {
98 | if (startCapture) {
99 | mainListRef.current.style.transform = `translateY(${
100 | currentTranslatedValue + cursorPosition
101 | }px)`;
102 | }
103 | }, [cursorPosition]);
104 |
105 | // final translation here
106 | useEffect(() => {
107 | if (showFinalTranslate) {
108 | setDragDuration(dragEndTime - dragStartTime);
109 | if (dragEndTime - dragStartTime <= 100 && cursorPosition !== 0) {
110 | let currentValue;
111 | if (dragDirection === 'down') {
112 | currentValue = currentTranslatedValue - (120 / (dragEndTime - dragStartTime)) * 100;
113 | } else if (dragDirection === 'up') {
114 | currentValue = currentTranslatedValue + (120 / (dragEndTime - dragStartTime)) * 100;
115 | }
116 | let finalValue = Math.round(currentValue / height) * height;
117 | if (finalValue < height * -177) finalValue = height * -177;
118 | if (finalValue > height * 2) finalValue = height * 2;
119 |
120 | mainListRef.current.style.transform = `translateY(${finalValue}px)`;
121 | setCurrentTranslatedValue(finalValue);
122 | }
123 | if (dragEndTime - dragStartTime > 100 && cursorPosition !== 0) {
124 | let finalValue = Math.round(currentTranslatedValue / height) * height;
125 | if (finalValue < height * -177) finalValue = height * -177;
126 | if (finalValue > height * 2) finalValue = height * 2;
127 | mainListRef.current.style.transform = `translateY(${finalValue}px)`;
128 | setCurrentTranslatedValue(finalValue);
129 | }
130 | setCursorPosition(0);
131 | }
132 | }, [showFinalTranslate]);
133 |
134 | // return to default position after drag end (handleTransitionEnd)
135 | const handleTransitionEnd = (e) => {
136 | returnSelectedValue(height, 60).map((item) => {
137 | if (parseInt(item.translatedValue) === currentTranslatedValue) {
138 | setSelectedNumber(item.arrayNumber);
139 | setValue((prev) => `${prev.slice(0, 2)}:${item.number}`);
140 | setHours(() => {
141 | const newValue = initialNumbersValue(height, 60).map((hour) => {
142 | if (
143 | hour.number == item.number &&
144 | hour.translatedValue == currentTranslatedValue
145 | ) {
146 | return {
147 | ...hour,
148 | selected: true,
149 | };
150 | }
151 | return hour;
152 | });
153 | return newValue;
154 | });
155 | }
156 | });
157 | };
158 |
159 | // handle click to select number
160 | const handleClickToSelect = (e) => {
161 | if (cursorPosition === 0) {
162 | setCurrentTranslatedValue(parseInt(e.target.dataset.translatedValue));
163 | }
164 | };
165 |
166 | const isFastCondition = showFinalTranslate && dragType === 'fast';
167 | const isSlowCondition = showFinalTranslate && dragType === 'slow';
168 |
169 | /* *************************** handle wheel scroll ************************* */
170 |
171 | const handleWheelScroll = (e) => {
172 | if (e.deltaY > 0) {
173 | if (currentTranslatedValue < height * 2) {
174 | setCurrentTranslatedValue((prev) => prev + height);
175 | }
176 | } else if (currentTranslatedValue > height * -177) {
177 | setCurrentTranslatedValue((prev) => prev - height);
178 | }
179 | };
180 |
181 | return (
182 |
194 | {/*
*/}
195 |
203 | {hours.map((hourObj, index) => (
204 |
209 |
216 | {hourObj.number}
217 |
218 |
219 | ))}
220 |
221 |
222 | );
223 | }
224 |
225 | export default MinuteWheel;
226 |
--------------------------------------------------------------------------------
/src/components/HourWheel.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState, useRef } from 'react';
2 | import { initialNumbersValue, returnSelectedValue } from '../helpers';
3 | import PickerEffects from './PickerEffects';
4 |
5 | function HourWheel({ height, value, setValue, use12Hours }) {
6 | const hourLength = use12Hours ? 13 : 24;
7 | const [hours, setHours] = useState(
8 | initialNumbersValue(height, hourLength, parseInt(value.slice(0, 2))),
9 | );
10 | const mainListRef = useRef(null);
11 | const [cursorPosition, setCursorPosition] = useState(null);
12 | const [firstCursorPosition, setFirstCursorPosition] = useState(null);
13 | const [currentTranslatedValue, setCurrentTranslatedValue] = useState(
14 | parseInt(
15 | initialNumbersValue(height, hourLength, parseInt(value.slice(0, 2))).filter(
16 | (item) => item.number === value.slice(0, 2) && item.selected === true,
17 | )[0].translatedValue,
18 | ),
19 | );
20 | const [startCapture, setStartCapture] = useState(false);
21 | const [showFinalTranslate, setShowFinalTranslate] = useState(false);
22 | // start and end times
23 | const [dragStartTime, setDragStartTime] = useState(null);
24 | const [dragEndTime, setDragEndTime] = useState(null);
25 | // drag duration
26 | const [dragDuration, setDragDuration] = useState(null);
27 | // drag type fast or slow
28 | const [dragType, setDragType] = useState(null);
29 | // drag direction
30 | const [dragDirection, setDragDirection] = useState(null);
31 | // selected number
32 | const [selectedNumber, setSelectedNumber] = useState(null);
33 |
34 | const handleMouseDown = (e) => {
35 | setShowFinalTranslate(false);
36 | setFirstCursorPosition(e.clientY);
37 | setStartCapture(true);
38 | setDragStartTime(performance.now());
39 | };
40 |
41 | const handleTouchStart = (e) => {
42 | setShowFinalTranslate(false);
43 | setFirstCursorPosition(e.targetTouches[0].clientY);
44 | setStartCapture(true);
45 | setDragStartTime(performance.now());
46 | };
47 |
48 | const handleMouseUp = (e) => {
49 | setStartCapture(false);
50 | setCurrentTranslatedValue((prev) => prev + cursorPosition);
51 | setShowFinalTranslate(true);
52 | setDragEndTime(performance.now());
53 | if (performance.now() - dragStartTime <= 100) {
54 | setDragType('fast');
55 | } else {
56 | setDragType('slow');
57 | }
58 | if (cursorPosition < 0) {
59 | setDragDirection('down');
60 | } else {
61 | setDragDirection('up');
62 | }
63 | };
64 |
65 | const handleMouseLeave = (e) => {
66 | setStartCapture(false);
67 | setCurrentTranslatedValue((prev) => prev + cursorPosition);
68 | setShowFinalTranslate(true);
69 | setDragEndTime(performance.now());
70 | if (performance.now() - dragStartTime <= 100) {
71 | setDragType('fast');
72 | } else {
73 | setDragType('slow');
74 | }
75 |
76 | if (cursorPosition < 0) {
77 | setDragDirection('down');
78 | } else {
79 | setDragDirection('up');
80 | }
81 | };
82 |
83 | const handleMouseMove = (e) => {
84 | if (startCapture) {
85 | setCursorPosition(e.clientY - firstCursorPosition);
86 | } else {
87 | setCursorPosition(0);
88 | }
89 | };
90 |
91 | const handleTouchMove = (e) => {
92 | if (startCapture) {
93 | setCursorPosition(e.targetTouches[0].clientY - firstCursorPosition);
94 | } else {
95 | setCursorPosition(0);
96 | }
97 | };
98 |
99 | // preview translation
100 | useEffect(() => {
101 | if (startCapture) {
102 | mainListRef.current.style.transform = `translateY(${
103 | currentTranslatedValue + cursorPosition
104 | }px)`;
105 | }
106 | }, [cursorPosition]);
107 |
108 | // final translation here
109 | useEffect(() => {
110 | if (showFinalTranslate) {
111 | setDragDuration(dragEndTime - dragStartTime);
112 | if (dragEndTime - dragStartTime <= 100 && cursorPosition !== 0) {
113 | let currentValue;
114 | if (dragDirection === 'down') {
115 | currentValue = currentTranslatedValue - (120 / (dragEndTime - dragStartTime)) * 100;
116 | } else if (dragDirection === 'up') {
117 | currentValue = currentTranslatedValue + (120 / (dragEndTime - dragStartTime)) * 100;
118 | }
119 | let finalValue = Math.round(currentValue / height) * height;
120 | if (use12Hours) {
121 | if (finalValue < height * -34) finalValue = height * -34;
122 | if (finalValue > height) finalValue = height;
123 | } else {
124 | if (finalValue < height * -69) finalValue = height * -69;
125 | if (finalValue > height * 2) finalValue = height * 2;
126 | }
127 |
128 | mainListRef.current.style.transform = `translateY(${finalValue}px)`;
129 | setCurrentTranslatedValue(finalValue);
130 | }
131 | if (dragEndTime - dragStartTime > 100 && cursorPosition !== 0) {
132 | let finalValue = Math.round(currentTranslatedValue / height) * height;
133 | if (use12Hours) {
134 | if (finalValue < height * -34) finalValue = height * -34;
135 | if (finalValue > height) finalValue = height;
136 | } else {
137 | if (finalValue < height * -69) finalValue = height * -69;
138 | if (finalValue > height * 2) finalValue = height * 2;
139 | }
140 | mainListRef.current.style.transform = `translateY(${finalValue}px)`;
141 | setCurrentTranslatedValue(finalValue);
142 | }
143 | setCursorPosition(0);
144 | }
145 | }, [showFinalTranslate]);
146 |
147 | // return to default position after drag end (handleTransitionEnd)
148 | const handleTransitionEnd = (e) => {
149 | returnSelectedValue(height, hourLength).map((item) => {
150 | if (parseInt(item.translatedValue) === currentTranslatedValue) {
151 | setSelectedNumber(item.arrayNumber);
152 | setValue((prev) => `${item.number}:${prev.slice(3, 6)}`);
153 | setHours(() => {
154 | const newValue = initialNumbersValue(height, hourLength).map((hour) => {
155 | if (
156 | hour.number == item.number &&
157 | hour.translatedValue == currentTranslatedValue
158 | ) {
159 | return {
160 | ...hour,
161 | selected: true,
162 | };
163 | }
164 | return hour;
165 | });
166 | return newValue;
167 | });
168 | }
169 | });
170 | };
171 |
172 | // handle click to select number
173 | const handleClickToSelect = (e) => {
174 | if (cursorPosition === 0) {
175 | setCurrentTranslatedValue(parseInt(e.target.dataset.translatedValue));
176 | }
177 | };
178 |
179 | const isFastCondition = showFinalTranslate && dragType === 'fast';
180 | const isSlowCondition = showFinalTranslate && dragType === 'slow';
181 |
182 | /** *************************** handle wheel scroll ************************* */
183 |
184 | const handleWheelScroll = (e) => {
185 | if (use12Hours) {
186 | if (e.deltaY > 0) {
187 | if (currentTranslatedValue < height) {
188 | setCurrentTranslatedValue((prev) => prev + height);
189 | }
190 | } else if (currentTranslatedValue > height * -34) {
191 | setCurrentTranslatedValue((prev) => prev - height);
192 | }
193 | } else if (e.deltaY > 0) {
194 | if (currentTranslatedValue < height * 2) {
195 | setCurrentTranslatedValue((prev) => prev + height);
196 | }
197 | } else if (currentTranslatedValue > height * -69) {
198 | setCurrentTranslatedValue((prev) => prev - height);
199 | }
200 | };
201 |
202 | return (
203 |
217 | {/*
*/}
218 |
226 | {hours.map((hourObj, index) => (
227 |
232 |
239 | {hourObj.number}
240 |
241 |
242 | ))}
243 |
244 |
245 | );
246 | }
247 |
248 | export default HourWheel;
249 |
--------------------------------------------------------------------------------