dev #5

Merged
Astrian merged 4 commits from dev into main 2025-02-21 06:55:14 +00:00
3 changed files with 123 additions and 23 deletions
Showing only changes of commit 4b0b821da5 - Show all commits

View File

@ -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>)
} }

View File

@ -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>}

View File

@ -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;
};
}
} }
} }