dev #5
| 
						 | 
					@ -6,7 +6,15 @@ export default () => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return (<div className='app'>
 | 
						return (<div className='app'>
 | 
				
			||||||
		<div className="border">
 | 
							<div className="border">
 | 
				
			||||||
			<SingleWeekPicker value={new Date(2025, 0, 1)} onSelect={(date) => console.log(date)} />
 | 
								<SingleDatePicker availableRange={[{
 | 
				
			||||||
 | 
									year: 2023,
 | 
				
			||||||
 | 
									month: 1,
 | 
				
			||||||
 | 
									day: 1
 | 
				
			||||||
 | 
								}, {
 | 
				
			||||||
 | 
									year: 2023,
 | 
				
			||||||
 | 
									month: 12,
 | 
				
			||||||
 | 
									day: 31
 | 
				
			||||||
 | 
								}]} />
 | 
				
			||||||
		</div>
 | 
							</div>
 | 
				
			||||||
	</div>)
 | 
						</div>)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -70,6 +70,24 @@ export interface SingleDatePickerProps {
 | 
				
			||||||
	 *@default '#e0e0e0'
 | 
						 *@default '#e0e0e0'
 | 
				
			||||||
	 */
 | 
						 */
 | 
				
			||||||
	borderColor?: string
 | 
						borderColor?: string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * Limit a range of dates that can be selected. It should be an array of two dates, which the first
 | 
				
			||||||
 | 
						 * one is the available range start date, and the second one is the available range end date. 
 | 
				
			||||||
 | 
						 * 
 | 
				
			||||||
 | 
						 * If the first one is null, it means that the all dates after the second one is not available. If the second
 | 
				
			||||||
 | 
						 * one is null, it means that the all dates before the first one is not available. 
 | 
				
			||||||
 | 
						 * 
 | 
				
			||||||
 | 
						 * If the first one is behind the second one, Datenel will exchange them automatically.
 | 
				
			||||||
 | 
						 * 
 | 
				
			||||||
 | 
						 * The parameter will be ignored if the array length is not 2.
 | 
				
			||||||
 | 
						 * @example [new Date(2025, 0, 1), new Date(2025, 11, 31)]
 | 
				
			||||||
 | 
						 * @example [new Date(2025, 0, 1), null]
 | 
				
			||||||
 | 
						 * @example [null, new Date(2025, 11, 31)]
 | 
				
			||||||
 | 
						 * @example [new Date(2025, 11, 31), new Date(2025, 0, 1)]
 | 
				
			||||||
 | 
						 * @default undefined
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
 | 
						availableRange?: [(Date | { year: number, month: number, day: number } | null), (Date | { year: number, month: number, day: number } | null)]
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
| 
						 | 
					@ -80,7 +98,7 @@ export interface SingleDatePickerProps {
 | 
				
			||||||
 * 
 | 
					 * 
 | 
				
			||||||
 * @param {SingleDatePickerProps} props
 | 
					 * @param {SingleDatePickerProps} props
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
const SingleDatePicker: React.FC<SingleDatePickerProps> = ({ value, onSelect, localization, onClose, mainColor = '#000000', accentColor = '#000000', reversedColor = '#ffffff', hoverColor = '#00000017', borderColor = '#e0e0e0' }: SingleDatePickerProps) => {
 | 
					const SingleDatePicker: React.FC<SingleDatePickerProps> = ({ value, onSelect, localization, onClose, mainColor = '#000000', accentColor = '#000000', reversedColor = '#ffffff', hoverColor = '#00000017', borderColor = '#e0e0e0', availableRange: inputAvailableRange }: SingleDatePickerProps) => {
 | 
				
			||||||
	const [currentMonth, setCurrentMonth] = useState(new Date().getMonth())
 | 
						const [currentMonth, setCurrentMonth] = useState(new Date().getMonth())
 | 
				
			||||||
	const [currentYear, setCurrentYear] = useState(new Date().getFullYear())
 | 
						const [currentYear, setCurrentYear] = useState(new Date().getFullYear())
 | 
				
			||||||
	const [selectedDate, setSelectedDate] = useState(new Date())
 | 
						const [selectedDate, setSelectedDate] = useState(new Date())
 | 
				
			||||||
| 
						 | 
					@ -88,6 +106,8 @@ const SingleDatePicker: React.FC<SingleDatePickerProps> = ({ value, onSelect, lo
 | 
				
			||||||
	const [l10nDays, setL10nDays] = useState<string[]>([])
 | 
						const [l10nDays, setL10nDays] = useState<string[]>([])
 | 
				
			||||||
	const [selectMonth, setSelectMonth] = useState(false)
 | 
						const [selectMonth, setSelectMonth] = useState(false)
 | 
				
			||||||
	const uniqueId = generateUniqueId()
 | 
						const uniqueId = generateUniqueId()
 | 
				
			||||||
 | 
						const [availableRangeStart, setAvailableRangeStart] = useState<Date | null>(null)
 | 
				
			||||||
 | 
						const [availableRangeEnd, setAvailableRangeEnd] = useState<Date | null>(null)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	useEffect(() => {
 | 
						useEffect(() => {
 | 
				
			||||||
		setDates(getCalendarDates(currentMonth, currentYear))
 | 
							setDates(getCalendarDates(currentMonth, currentYear))
 | 
				
			||||||
| 
						 | 
					@ -123,6 +143,46 @@ const SingleDatePicker: React.FC<SingleDatePickerProps> = ({ value, onSelect, lo
 | 
				
			||||||
		})
 | 
							})
 | 
				
			||||||
	}, [mainColor, accentColor, reversedColor, hoverColor, borderColor])
 | 
						}, [mainColor, accentColor, reversedColor, hoverColor, borderColor])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						useEffect(() => {
 | 
				
			||||||
 | 
							if (!inputAvailableRange) {
 | 
				
			||||||
 | 
								setAvailableRangeEnd(null)
 | 
				
			||||||
 | 
								setAvailableRangeStart(null)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if (inputAvailableRange.length !== 2) {
 | 
				
			||||||
 | 
								console.warn('Invalid availableRange: The length of the array should be 2. The parameter will be ignored.')
 | 
				
			||||||
 | 
								setAvailableRangeEnd(null)
 | 
				
			||||||
 | 
								setAvailableRangeStart(null)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							const [start, end] = inputAvailableRange
 | 
				
			||||||
 | 
							if (start && end) {
 | 
				
			||||||
 | 
								const inputStart = !(start instanceof Date) ? new Date(start.year, start.month - 1, start.day) : start
 | 
				
			||||||
 | 
								const inputEnd = !(end instanceof Date) ? new Date(end.year, end.month - 1, end.day) : end
 | 
				
			||||||
 | 
								if (inputStart > inputEnd) {
 | 
				
			||||||
 | 
									setAvailableRangeStart(inputEnd)
 | 
				
			||||||
 | 
									setAvailableRangeEnd(inputStart)
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									setAvailableRangeStart(inputStart)
 | 
				
			||||||
 | 
									setAvailableRangeEnd(inputEnd)
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							else if (start && !end) {
 | 
				
			||||||
 | 
								if (!(start instanceof Date)) setAvailableRangeStart(new Date(start.year, start.month - 1, start.day))
 | 
				
			||||||
 | 
								else setAvailableRangeStart(start)
 | 
				
			||||||
 | 
								setAvailableRangeEnd(null)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							else if (!start && end) {
 | 
				
			||||||
 | 
								if (!(end instanceof Date)) setAvailableRangeEnd(new Date(end.year, end.month - 1, end.day))
 | 
				
			||||||
 | 
								else setAvailableRangeEnd(end)
 | 
				
			||||||
 | 
								setAvailableRangeStart(null)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							else {
 | 
				
			||||||
 | 
								setAvailableRangeStart(null)
 | 
				
			||||||
 | 
								setAvailableRangeEnd(null)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}, [inputAvailableRange])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	function selectDate(date: Date) {
 | 
						function selectDate(date: Date) {
 | 
				
			||||||
		setSelectedDate(date)
 | 
							setSelectedDate(date)
 | 
				
			||||||
		onSelect?.({
 | 
							onSelect?.({
 | 
				
			||||||
| 
						 | 
					@ -177,12 +237,30 @@ const SingleDatePicker: React.FC<SingleDatePickerProps> = ({ value, onSelect, lo
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
			<div className='body'>
 | 
								<div className='body'>
 | 
				
			||||||
				<div className='month-selector-body'>
 | 
									<div className='month-selector-body'>
 | 
				
			||||||
					{Array.from({ length: 12 }).map((_, index) => <button className={`item`} key={index} onClick={() => {
 | 
										{Array.from({ length: 12 }).map((_, index) => {
 | 
				
			||||||
						setCurrentMonth(index)
 | 
											function calculateNotAvailable() {
 | 
				
			||||||
						setSelectMonth(false)
 | 
												// When the last day of a month not inside the range of available dates
 | 
				
			||||||
					}} aria-label={`Go to ${new Date(currentYear, index).toLocaleString(localization || navigator.language, { month: 'long' })} of the year ${currentYear}`}>
 | 
												const lastDayOfMonth = new Date(currentYear, index + 1, 0)
 | 
				
			||||||
						{new Date(currentYear, index).toLocaleString(localization || navigator.language, { month: 'long' })}
 | 
												if (availableRangeStart && lastDayOfMonth < availableRangeStart) return true
 | 
				
			||||||
					</button>)}
 | 
												// When the first day of a month not inside the range of available dates
 | 
				
			||||||
 | 
												const firstDayOfMonth = new Date(currentYear, index, 1)
 | 
				
			||||||
 | 
												if (availableRangeEnd && firstDayOfMonth > availableRangeEnd) return true
 | 
				
			||||||
 | 
												return false
 | 
				
			||||||
 | 
											}
 | 
				
			||||||
 | 
											return <button
 | 
				
			||||||
 | 
												className={`item ${calculateNotAvailable() && 'not-available'}`}
 | 
				
			||||||
 | 
												key={index}
 | 
				
			||||||
 | 
												onClick={() => {
 | 
				
			||||||
 | 
													setCurrentMonth(index)
 | 
				
			||||||
 | 
													setSelectMonth(false)
 | 
				
			||||||
 | 
												}}
 | 
				
			||||||
 | 
												aria-label={`Go to ${new Date(currentYear, index).toLocaleString(localization || navigator.language, { month: 'long' })} of the year ${currentYear}`}
 | 
				
			||||||
 | 
												disabled={calculateNotAvailable()}
 | 
				
			||||||
 | 
												aria-hidden={calculateNotAvailable()}
 | 
				
			||||||
 | 
											>
 | 
				
			||||||
 | 
												{new Date(currentYear, index).toLocaleString(localization || navigator.language, { month: 'long' })}
 | 
				
			||||||
 | 
											</button>
 | 
				
			||||||
 | 
										})}
 | 
				
			||||||
				</div>
 | 
									</div>
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
			{!!onClose && <button className='sr-only' onClick={onClose}>Close the panel</button>}
 | 
								{!!onClose && <button className='sr-only' onClick={onClose}>Close the panel</button>}
 | 
				
			||||||
| 
						 | 
					@ -201,18 +279,21 @@ const SingleDatePicker: React.FC<SingleDatePickerProps> = ({ value, onSelect, lo
 | 
				
			||||||
				<div className='calendar-view-body grid' aria-live="polite">
 | 
									<div className='calendar-view-body grid' aria-live="polite">
 | 
				
			||||||
					{l10nDays.map((day, index) => <div className='item day-indicator' key={index}>{day}</div>)}
 | 
										{l10nDays.map((day, index) => <div className='item day-indicator' key={index}>{day}</div>)}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
					{dates.map(date => <button
 | 
										{dates.map(date => {
 | 
				
			||||||
						className={`item date ${currentMonth !== date.getMonth() && 'extra-month'} ${selectedDate.toDateString() === date.toDateString() && 'active'}`}
 | 
											const notAvailable = (availableRangeStart && date < availableRangeStart) || (availableRangeEnd && date > availableRangeEnd) || currentMonth !== date.getMonth()
 | 
				
			||||||
						key={date.toISOString()}
 | 
											return <button
 | 
				
			||||||
						onClick={() => selectDate(date)}
 | 
												className={`item date ${notAvailable && 'not-available'} ${selectedDate.toDateString() === date.toDateString() && 'active'}`}
 | 
				
			||||||
						aria-label={`${date.toLocaleString(localization || navigator.language, { dateStyle: 'full' })}${date.toDateString() === new Date().toDateString() ? ", this is today" : ""}, click to select this date`}
 | 
												key={date.toISOString()}
 | 
				
			||||||
						tabIndex={currentMonth !== date.getMonth() ? -1 : 0}
 | 
												onClick={() => selectDate(date)}
 | 
				
			||||||
						aria-hidden={currentMonth !== date.getMonth()}
 | 
												aria-label={`${date.toLocaleString(localization || navigator.language, { dateStyle: 'full' })}${date.toDateString() === new Date().toDateString() ? ", this is today" : ""}, click to select this date`}
 | 
				
			||||||
						disabled={currentMonth !== date.getMonth()}
 | 
												tabIndex={currentMonth !== date.getMonth() ? -1 : 0}
 | 
				
			||||||
					>
 | 
												aria-hidden={notAvailable}
 | 
				
			||||||
						{date.getDate()}
 | 
												disabled={notAvailable}
 | 
				
			||||||
						{date.toDateString() === new Date().toDateString() && <svg xmlns="http://www.w3.org/2000/svg" className='today-indicator' viewBox="0 0 24 24" fill="currentColor"><path d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z"></path></svg>}
 | 
											>
 | 
				
			||||||
					</button>)}
 | 
												{date.getDate()}
 | 
				
			||||||
 | 
												{date.toDateString() === new Date().toDateString() && <svg xmlns="http://www.w3.org/2000/svg" className='today-indicator' viewBox="0 0 24 24" fill="currentColor"><path d="M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z"></path></svg>}
 | 
				
			||||||
 | 
											</button>
 | 
				
			||||||
 | 
										})}
 | 
				
			||||||
				</div>
 | 
									</div>
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
			{!!onClose && <button className='sr-only' onClick={onClose}>Close the panel</button>}
 | 
								{!!onClose && <button className='sr-only' onClick={onClose}>Close the panel</button>}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -83,7 +83,7 @@
 | 
				
			||||||
				.item.date {
 | 
									.item.date {
 | 
				
			||||||
					border-radius: 50%;
 | 
										border-radius: 50%;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
					&.extra-month {
 | 
										&.extra-month, &.not-available {
 | 
				
			||||||
						cursor: default;
 | 
											cursor: default;
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -112,7 +112,7 @@
 | 
				
			||||||
						background: var(--datenel-accent-color);
 | 
											background: var(--datenel-accent-color);
 | 
				
			||||||
						color: var(--datenel-reversed-color);
 | 
											color: var(--datenel-reversed-color);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
						.extra-month {
 | 
											.extra-month, .not-available {
 | 
				
			||||||
							opacity: 0.5;
 | 
												opacity: 0.5;
 | 
				
			||||||
						}
 | 
											}
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
| 
						 | 
					@ -129,8 +129,11 @@
 | 
				
			||||||
				font-size: 0.75rem;
 | 
									font-size: 0.75rem;
 | 
				
			||||||
				position: relative;
 | 
									position: relative;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				&.extra-month {
 | 
									&.extra-month, &.not-available {
 | 
				
			||||||
					opacity: 0.3;
 | 
										opacity: 0.3;
 | 
				
			||||||
 | 
										&:hover {
 | 
				
			||||||
 | 
											background: none;
 | 
				
			||||||
 | 
										};
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				&.day-indicator {
 | 
									&.day-indicator {
 | 
				
			||||||
| 
						 | 
					@ -156,6 +159,14 @@
 | 
				
			||||||
				border-radius: 0.25rem;
 | 
									border-radius: 0.25rem;
 | 
				
			||||||
				height: 2rem;
 | 
									height: 2rem;
 | 
				
			||||||
				padding: 0 0.5rem;
 | 
									padding: 0 0.5rem;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									&.not-available {
 | 
				
			||||||
 | 
										opacity: 0.3;
 | 
				
			||||||
 | 
										cursor: default;
 | 
				
			||||||
 | 
										&:hover {
 | 
				
			||||||
 | 
											background: none;
 | 
				
			||||||
 | 
										};
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue
	
	Block a user