<div id="root"></div>
const { useState, useRef, useCallback, useEffect } = React;
const { createGlobalStyle, css } = styled;

const GlobalStyle = createGlobalStyle`
	@import url('https://fonts.googleapis.com/css2?family=Orelega+One&display=swap');

:root {
  --accent-color: rgb(62, 93, 228);
  --text-color: #333;
  --bg-color: #CFDBEA;
	--button-color: #333;
	--button-text-color: #fff;
	--button-disabled-color: #ccc;
}

  body {
  	background-color: var(--bg-color);
	color: var(--text-color);
	display: flex;
	align-items: center;
	justify-content: center;
    font-family: 'Orelega One', cursive;
	font-size: 4vmin;
	margin: 0;
	height: 100vh;
	width: 100%;
  }
`;

const Spacer = ({ size, horizontal }) => {
  return (
    <div
      style={
        horizontal
          ? { width: size, height: 'auto', display: 'inline-block', flexShrink: 0 }
          : { width: 'auto', height: size, flexShrink: 0  }
      }
    />
  )
}

const StyledTimeDisplay = styled.div`
	color: ${({ textColor }) => textColor ? textColor : `#333`};
	font-weight: bold;
	font-size: ${({ fontSize }) => fontSize ? fontSize : '1em'};
`;

const TimeDisplay = ({ className, time, delimiter, fontSize, textColor }) => {
	const newTime = Array.isArray(time) ? time : Object.values(time)
	
	return (
		<StyledTimeDisplay
			className={className}
			fontSize={fontSize}
			textColor={textColor}
		>
			{
				newTime.map((n, i, array) => (
					<>
						<span>{String(n).padStart(2, "0")}</span>
						{(i !== array.length - 1) && delimiter}
					</>
				))
			}
		</StyledTimeDisplay>
	);
};

const StyledWheelPicker = styled.div`
	display: grid;
	grid-template-rows: repeat(2, auto);
	${({ itemLength, delimiterLength }) => `
		grid-template-columns: repeat(${itemLength + delimiterLength}, auto);
	`}// = auto-fill
	
	place-items: center; 
	align-items: center;
	justify-content: center;
	gap: .5em;
	
	& .delimiter {
		font-size: .75em;
		
		&-1 {
			grid-column: 2 / 3;
		}
		&-2 {
			grid-column: 4 / 5;
		}
		&-3 {
			grid-column: 6 / 7;
		}
	}
	
	& .dial {
		&-display {
			border-top: .1em solid #333;
			border-bottom: .1em solid #333;
			height: 1em;
			overflow-y: hidden;
			padding: .4em;
			
			&-1 {
				grid-column: 1 / 2;
			}
			&-2 {
				grid-column: 3 / 4;
			}
			&-3 {
				grid-column: 5 / 6;
			}
		}
		&-title {
			font-size: .6em;
			
			&-1 {
				grid-column: 1 / 2;
			}
			&-2 {
				grid-column: 3 / 4;
			}
			&-3 {
				grid-column: 5 / 6;
			}
		}
		&-nums {
			cursor: pointer;
			position: relative;
			top: 0em;
			transition: .2s linear;
			user-select: none;
		}
		
		&-num {
			height: 1em;
		}
	}
`

const WheelPicker = ({ className, items, delimiters, handleWheel, handleDown }) => {
	const itemsArray = Object.keys(items)//Object.entries(items).map((t) => t.shift())
	/*useEffect(() => {
		console.log('items', Object.entries(items).map((t) => t.shift()))
	}, [items])*/
	return (
		<StyledWheelPicker
			className={className}
			itemLength={items.length}
			delimiterLength={delimiters.length}
		>
			{
				itemsArray.map((item, i, arr) => (
					<div className={`dial-title dial-title-${i + 1}`}>
								{item}
							</div>
				))
			}
			{
				itemsArray.map((item, i, arr) => (
					<>
						<div className={`dial-display dial-display-${i + 1}`}>
							<div
								className={`dial-nums dial-nums-${i}
								draggable`}
								key={i}
								id={item}
								onMouseDown={handleDown}
								onWheel={handleWheel}
							>
								{
									items[item].map((elem, i, array) => (
										<div className="dial-num" key={i}>{String(elem).padStart(2, "0")}</div>
									))
								}
							</div>
						</div>
						{delimiters[i] && <div className={`delimiter delimiter-${i + 1}`}>{delimiters[i]}</div>}
					</>
				))
			}	
			<Spacer size=".5em" horizontal={true} />
		</StyledWheelPicker>
	)
}

/*const useDragElement = () => {
	const [dragAmount, setDragAmount] = useState({ x: 0, y: 0 });
	const [elementPosition, setElementPosition] = useState({ top: 0, left: 0 });
	const startPoint = useRef({ x: 0, y: 0 });
	const draggingElement = useRef(null);

	const verticalDirection = useRef(null);
	const horizontalDirection = useRef(null);
	const prevPosition = useRef({ x: 0, y: 0 });

	const handleDown = useCallback((e) => {
		draggingElement.current = e.currentTarget;

		//const rect = draggingElement.current.getBoundingClientRect();

		//const top = rect.top;
		//const left = rect.left;

		const x = e.clientX - draggingElement.current.offsetLeft;//left;
		const y = e.clientY - draggingElement.current.offsetTop;//top;

		startPoint.current = { x, y };
	});

	const handleMove = useCallback((e) => {
		if (!draggingElement.current) return;

		const x = e.clientX - startPoint.current.x;
		const y = e.clientY - startPoint.current.y;

		if (x > prevPosition.current.x) {
			horizontalDirection.current = "right";
		} 
		if(x < prevPosition.current.x) {
			horizontalDirection.current = "left";
		}

		if (y > prevPosition.current.y) {
			verticalDirection.current = "bottom";
		}
		if (y < prevPosition.current.y) {
			verticalDirection.current = "top";
		}

		setDragAmount({ x, y });
		
		//draggingElement.current.style.transform = `translate3d(${x}px, ${y}px, 0)`

		prevPosition.current = {
			x,
			y
		};
	});

	
	const handleUp = useCallback((e) => {
		if (!draggingElement.current) return;

		setElementPosition((p) => ({
			top: dragAmount.y,
			left: dragAmount.x
		}));
		
		//draggingElement.current.style.top = `${dragAmount.y}px`
		//draggingElement.current.style.left = `${dragAmount.x}px`

		draggingElement.current = null;
		console.log('up')
	});

	useEffect(() => {
		document.body.addEventListener("mousemove", handleMove);
		document.body.addEventListener("mouseup", handleUp);
		document.body.addEventListener("mouseleave", handleUp);

		return () => {
			document.body.removeEventListener("mousemove", handleMove);
			document.body.removeEventListener("mouseup", handleUp);
			document.body.removeEventListener("mouseleave", handleUp);
		};
	}, []);
	
	return [
		draggingElement.current,
		dragAmount,
		elementPosition,
		verticalDirection.current,
		horizontalDirection.current,
		handleDown
	]
}*/

const useDraggableElements = () => {
	const [draggableElements, setDraggableElements] = useState([])
	const startPoint = useRef({ x: 0, y: 0 });
	const draggingElement = useRef(null);
	const draggingDirection = useRef({
		horizontal: null,
		vertical: null,
	})
	
	const handleDown = useCallback((e) => {
		draggingElement.current = e.currentTarget;
		
		const draggableElements = document.getElementsByClassName("draggable");

		for(let i = 0; i < draggableElements.length; i++) {
			draggableElements[i].style.zIndex = `1000`
		}
		
		draggingElement.current.style.zIndex = `1001`

		setDraggableElements(currentDraggableElements => {
			currentDraggableElements.forEach(elm => {
				if(draggingElement.current.className.includes(elm.name)) {
					const x = e.pageX - elm.x
					const y = e.pageY - elm.y
					
					startPoint.current = { x, y };
				}
			})
			
			return currentDraggableElements
		})
	
		console.log(startPoint.current)
	});
	
	const handleMove = useCallback((e) => {
		e.preventDefault();

		if (!draggingElement.current) return;
		
		const x = e.pageX - startPoint.current.x;
		const y = e.pageY - startPoint.current.y;
		
		setDraggableElements(currentDraggableElements => {		
			currentDraggableElements.forEach(elm => {
				if(draggingElement.current.className.includes(elm.name)){
					if (x > elm.x) {
						draggingDirection.current.horizontal = "right";
					}
					if (x < elm.x) {
						draggingDirection.current.horizontal = "left";
					}

					if (y > elm.y) {
						draggingDirection.current.vertical = "bottom";
					}
					if (y < elm.y) {
						draggingDirection.current.vertical = "top";
					}
				}			
			})
			
			return currentDraggableElements
		})
		
		draggingElement.current.style.transform = `translate3d(${x}px, ${y}px, 0)`;
		
		setDraggableElements(currentDraggableElements => 
			currentDraggableElements.map(elm => 
				draggingElement.current.className.includes(elm.name) ? ({
					...elm,
					x,
					y
				}) : elm))
		});

		const handleUp = useCallback((e) => {
			if (!draggingElement.current) return;

			draggingElement.current = null;// nullにしないと、先頭でif (!draggingElement.current) return;としているため、要素がカーソルから離れない
			console.log("up");
		});
	
	// 初回のレンダー後に一度だけ行う処理
	useEffect(() => {
		const draggableElements = document.getElementsByClassName("draggable");
		
		for(let i = 0; i < draggableElements.length; i++) {
			draggableElements[i].addEventListener("mousedown", handleDown);
		}
		
		document.body.addEventListener("mousemove", handleMove);
		document.body.addEventListener("mouseup", handleUp);
		document.body.addEventListener("mouseleave", handleUp);
		
		setDraggableElements(currentDraggableElements => {
			const newDraggableElements = [...currentDraggableElements]
			
			for(let i = 0; i < draggableElements.length; i++) {
				newDraggableElements[i] = {
					name: draggableElements[i].className,
					x: 0,
					y: 0
				}
			}
			
			return newDraggableElements
		})
		
		return () => {
			document.body.removeEventListener("mousemove", handleMove);
			document.body.removeEventListener("mouseup", handleUp);
			document.body.removeEventListener("mouseleave", handleUp);
		};
	}, [])

	// ドラッグしている方向
	useEffect(() => {
		console.log(draggingDirection.current);
	}, [draggableElements]);

	return [
		draggingElement.current,
		draggableElements,
		draggingDirection.current
	];
};

const useWheelPicker = (nums) => {
	const [selectNum, setSelectNum] = useState(() => {
		const newNums = {}
		
		Object.keys(nums).forEach((key) => {
			newNums[key] = nums[key][0]
		})
		
		return newNums
	})
	
	/*const [
		draggingElement,
		dragAmount,
		elementPosition,
		verticalDirection,
		horizontalDirection,
		handleDown
	] = useDragElement()*/
	
	const [
		draggingElement,
		draggableElements,
		draggingDirection
	] = useDraggableElements();
	
	const handleWheel = useCallback((e) => {
		const deltaY = e.deltaY;
		const elm = e.currentTarget;
		
		const targetNums = nums[elm.id]
		
		setSelectNum(p => {
			const currentSelectNum = Math.abs(p[elm.id])
			const currentSelectNumIndex = targetNums.indexOf(currentSelectNum)
			
			const firstIndex = 0
			const lastIndex = targetNums.length - 1
			
			console.log('currentSelectNum', currentSelectNum)
			console.log('currentSelectNumIndex', currentSelectNumIndex)
			
			const firstNum = targetNums[firstIndex]
			const lastNum = targetNums[lastIndex]
			
			if (e.deltaY > firstIndex) {// 下にホイール
				const nextIndex = currentSelectNumIndex + 1
				const nextNum = targetNums[currentSelectNumIndex + 1];
	
				console.log('nextNum', nextNum)
				
				if(nextIndex > lastIndex){
					elm.style.transform = `translate3d(0, ${firstIndex}em, 0)`
		
					return {
						...p,
						[elm.id]: firstNum
					}
				}else{
					elm.style.transform = `translate3d(0, -${nextIndex}em, 0)`
	
					return {
						...p,
						[elm.id]: nextNum
					}
				}
			}else{// 上にホイール
				const prevIndex = currentSelectNumIndex - 1
				const prevNum = targetNums[currentSelectNumIndex - 1];
				
				if(prevIndex < firstIndex) {
					elm.style.transform = `translate3d(0, -${lastIndex}em, 0)`
					
					return {
						...p,
						[elm.id]: lastNum
					}
				}else{
					elm.style.transform = `translate3d(0, -${prevIndex}em, 0)`
					
					return {
						...p,
						[elm.id]: prevNum
					}
				}
			}
		})
	});

	useEffect(() => {
		if(!draggingElement) return;
		
		//if(Math.abs(dragAmount.y) % 8 !== 0) return
		
		//console.log('dragAmount.y', dragAmount.y)
		console.log('draggingElement', draggingElement)
		
		const targetNums = nums[draggingElement.id]
		
		setSelectNum(p => {
			const currentSelectNum = Math.abs(p[draggingElement.id])
			const currentSelectNumIndex = targetNums.indexOf(currentSelectNum)
			
			const firstIndex = 0
			const lastIndex = targetNums.length - 1
			
			console.log('currentSelectNum', currentSelectNum)
			console.log('currentSelectNumIndex', currentSelectNumIndex)
			
			const firstNum = targetNums[firstIndex]
			const lastNum = targetNums[lastIndex]
					
			if(draggingDirection.vertical === 'top') {
				// 上へドラッグ

				const prevIndex = currentSelectNumIndex - 1
				const prevNum = targetNums[currentSelectNumIndex - 1];
				
				if(prevIndex < firstIndex) {
					draggingElement.style.transform = `translate3d(0, -${lastIndex}em, 0)`

					return {
						...p,
						[draggingElement.id]: lastNum
					}
				}else{
					draggingElement.style.transform = `translate3d(0, -${prevIndex}em, 0)`
				
					return {
						...p,
						[draggingElement.id]: prevNum
					}
				}
			}
			if(draggingDirection.vertical === 'bottom') {
				// 下へドラッグ
				
				const nextIndex = currentSelectNumIndex + 1
				const nextNum = targetNums[currentSelectNumIndex + 1];
				
				if(nextIndex > lastIndex) {
					draggingElement.style.transform = `translate3d(0, -${firstIndex}em, 0)`//`0em`

					return {
						...p,
						[draggingElement.id]: firstNum
					}
				}else{
					draggingElement.style.transform = `translate3d(0, -${nextIndex}em, 0)`
				
					return {
						...p,
						[draggingElement.id]: nextNum
					}
				}
			}				
		})	

		console.log('isVerticalDirection', draggingDirection.vertical)
	}, [draggableElements])
	
	return [
		selectNum,
		handleWheel
	]
}

const date = new Date()
const year = date.getFullYear()

const DATE = {
	Days: [...Array(31)].map((u, i) => i + 1),
	Months: [...Array(12)].map((u, i) => i + 1),
	Years: [...Array(120)].map((u, i) => year - i),
}
// 6th Janauary 2008
const DELIMITERS = ['/', '/']

const StyledApp = styled.div`
	width: 16em;
`

const App = () => {
	const [
		selectNum,
		//handleDown,
		handleWheel
	] = useWheelPicker(DATE)
	
	return (
		<>
			<GlobalStyle />
			<StyledApp>
				<WheelPicker
					className="wheel-picker"
					items={DATE}
					delimiters={DELIMITERS}
					handleWheel={handleWheel}
					//handleDown={handleDown}
				/>
				<Spacer size="1.4em" />
				<TimeDisplay
					className="date-of-birth"
					time={selectNum}
					delimiter="/"
					fontSize="2.8em"
				/>
			</StyledApp>
		</>
	);
};

ReactDOM.render(<App />, document.getElementById("root"));
View Compiled

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js
  2. https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js
  3. https://unpkg.com/[email protected]/dist/styled-components.min.js