Skip to content

Commit 5ddb035

Browse files
committed
refactor(Calendar): improve handling of disabled dates, ranges, and selection for months and years
1 parent 205d7f8 commit 5ddb035

File tree

2 files changed

+548
-205
lines changed

2 files changed

+548
-205
lines changed

js/src/calendar.js

Lines changed: 141 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,13 @@ import {
2323
isDateInRange,
2424
isDateSelected,
2525
isDisableDateInRange,
26-
isToday
26+
isMonthDisabled,
27+
isMonthInRange,
28+
isMonthSelected,
29+
isToday,
30+
isYearDisabled,
31+
isYearInRange,
32+
isYearSelected
2733
} from './util/calendar.js'
2834

2935
/**
@@ -185,6 +191,13 @@ class Calendar extends BaseComponent {
185191
this._view = viewMap[this._config.selectionType] || 'days'
186192
}
187193

194+
_classNames(classNames) {
195+
return Object.entries(classNames)
196+
.filter(([_, value]) => Boolean(value))
197+
.map(([key]) => key)
198+
.join(' ')
199+
}
200+
188201
_getDate(target) {
189202
if (this._config.selectionType === 'week') {
190203
const firstCell = SelectorEngine.findOne(SELECTOR_CALENDAR_CELL, target.closest(SELECTOR_CALENDAR_ROW))
@@ -200,10 +213,6 @@ class Calendar extends BaseComponent {
200213
const cloneDate = new Date(date)
201214
const index = Manipulator.getDataAttribute(target.closest(SELECTOR_CALENDAR), 'calendar-index')
202215

203-
if (isDateDisabled(date, this._config.minDate, this._config.maxDate, this._config.disabledDates)) {
204-
return
205-
}
206-
207216
if (this._view === 'days') {
208217
this._setCalendarDate(index ? new Date(cloneDate.setMonth(cloneDate.getMonth() - index)) : date)
209218
}
@@ -222,6 +231,11 @@ class Calendar extends BaseComponent {
222231
return
223232
}
224233

234+
// Allow to change the calendarDate but not startDate or endDate
235+
if (isDateDisabled(date, this._config.minDate, this._config.maxDate, this._config.disabledDates)) {
236+
return
237+
}
238+
225239
this._hoverDate = null
226240
this._selectDate(date)
227241
this._updateClassNamesAndAriaLabels()
@@ -630,49 +644,45 @@ class Calendar extends BaseComponent {
630644
`${calendarDate.getFullYear()}W${week.weekNumber}`,
631645
this._config.selectionType
632646
)
647+
const rowAttributes = this._rowWeekAttributes(date)
633648
return (
634649
`<tr
635-
class="${CLASS_NAME_CALENDAR_ROW} ${this._config.selectionType === 'week' && this._sharedClassNames(date)}"
636-
tabindex="${
637-
this._config.selectionType === 'week' &&
638-
week.days.some(day => day.month === 'current') &&
639-
!isDateDisabled(date, this._config.minDate, this._config.maxDate, this._config.disabledDates) ? 0 : -1
640-
}"
641-
${isDateSelected(date, this._startDate, this._endDate) ? 'aria-selected="true"' : ''}
650+
class="${rowAttributes.className}"
651+
tabindex="${rowAttributes.tabIndex}"
652+
${rowAttributes.ariaSelected ? 'aria-selected="true"' : ''}
642653
>
643654
${this._config.showWeekNumber ?
644655
`<th class="calendar-cell-week-number">${week.weekNumber === 0 ? 53 : week.weekNumber}</td>` : ''
645656
}
646-
${week.days.map(({ date, month }) => (
647-
month === 'current' || this._config.showAdjacementDays ?
648-
`<td
649-
class="${CLASS_NAME_CALENDAR_CELL} ${this._dayClassNames(date, month)}"
650-
tabindex="${
651-
this._config.selectionType === 'day' &&
652-
(month === 'current' || this._config.selectAdjacementDays) &&
653-
!isDateDisabled(date, this._config.minDate, this._config.maxDate, this._config.disabledDates) ? 0 : -1
654-
}"
655-
${isDateSelected(date, this._startDate, this._endDate) ? 'aria-selected="true"' : ''}
656-
data-coreui-date="${date}"
657-
>
658-
<div class="calendar-cell-inner day">
659-
${date.toLocaleDateString(this._config.locale, { day: 'numeric' })}
660-
</div>
661-
</td>` :
662-
'<td></td>'
663-
)).join('')}</tr>`
657+
${week.days.map(({ date, month }) => {
658+
const cellAttributes = this._cellDayAttributes(date, month)
659+
return month === 'current' || this._config.showAdjacementDays ?
660+
`<td
661+
class="${cellAttributes.className}"
662+
tabindex="${cellAttributes.tabIndex}"
663+
${cellAttributes.ariaSelected ? 'aria-selected="true"' : ''}
664+
data-coreui-date="${date}"
665+
>
666+
<div class="calendar-cell-inner day">
667+
${date.toLocaleDateString(this._config.locale, { day: 'numeric' })}
668+
</div>
669+
</td>` :
670+
'<td></td>'
671+
}
672+
).join('')}</tr>`
664673
)
665674
}).join('') : ''}
666675
${this._view === 'months' ? listOfMonths.map((row, index) => (
667676
`<tr>
668677
${row.map((month, idx) => {
669678
const date = new Date(calendarDate.getFullYear(), (index * 3) + idx, 1)
679+
const cellAttributes = this._cellMonthAttributes(date)
670680
return (
671681
`<td
672-
class="${CLASS_NAME_CALENDAR_CELL} ${this._sharedClassNames(date)}"
682+
class="${cellAttributes.className}"
683+
tabindex="${cellAttributes.tabIndex}"
684+
${cellAttributes.ariaSelected ? 'aria-selected="true"' : ''}
673685
data-coreui-date="${date.toDateString()}"
674-
tabindex="${isDateDisabled(date, this._config.minDate, this._config.maxDate, this._config.disabledDates) ? -1 : 0}"
675-
${isDateSelected(date, this._startDate, this._endDate) ? 'aria-selected="true"' : ''}
676686
>
677687
<div class="calendar-cell-inner month">
678688
${month}
@@ -686,12 +696,13 @@ class Calendar extends BaseComponent {
686696
`<tr>
687697
${row.map(year => {
688698
const date = new Date(year, 0, 1)
699+
const cellAttributes = this._cellYearAttributes(date)
689700
return (
690701
`<td
691-
class="${CLASS_NAME_CALENDAR_CELL} ${this._sharedClassNames(date)}"
702+
class="${cellAttributes.className}"
703+
tabindex="${cellAttributes.tabIndex}"
704+
${cellAttributes.ariaSelected ? 'aria-selected="true"' : ''}
692705
data-coreui-date="${date.toDateString()}"
693-
tabindex="${isDateDisabled(date, this._config.minDate, this._config.maxDate, this._config.disabledDates) ? -1 : 0}"
694-
${isDateSelected(date, this._startDate, this._endDate) ? 'aria-selected="true"' : ''}
695706
>
696707
<div class="calendar-cell-inner year">
697708
${year}
@@ -740,11 +751,12 @@ class Calendar extends BaseComponent {
740751
for (const row of rows) {
741752
const firstCell = SelectorEngine.findOne(SELECTOR_CALENDAR_CELL, row)
742753
const date = new Date(Manipulator.getDataAttribute(firstCell, 'date'))
743-
const classNames = this._sharedClassNames(date)
754+
const rowAttributes = this._rowWeekAttributes(date)
744755

745-
row.className = `${CLASS_NAME_CALENDAR_ROW} ${classNames}`
756+
row.className = rowAttributes.className
757+
row.tabIndex = rowAttributes.tabIndex
746758

747-
if (isDateSelected(date, this._startDate, this._endDate)) {
759+
if (rowAttributes.ariaSelected) {
748760
row.setAttribute('aria-selected', true)
749761
} else {
750762
row.removeAttribute('aria-selected')
@@ -758,68 +770,121 @@ class Calendar extends BaseComponent {
758770

759771
for (const cell of cells) {
760772
const date = new Date(Manipulator.getDataAttribute(cell, 'date'))
761-
const classNames = this._config.selectionType === 'day' ? this._dayClassNames(date, 'current') : this._sharedClassNames(date)
773+
let cellAttributes
762774

763-
cell.className = `${CLASS_NAME_CALENDAR_CELL} ${classNames}`
775+
if (this._view === 'days') {
776+
cellAttributes = this._cellDayAttributes(date, 'current')
777+
} else if (this._view === 'months') {
778+
cellAttributes = this._cellMonthAttributes(date)
779+
} else {
780+
cellAttributes = this._cellYearAttributes(date)
781+
}
764782

765-
if (isDateSelected(date, this._startDate, this._endDate)) {
783+
cell.className = cellAttributes.className
784+
cell.tabIndex = cellAttributes.tabIndex
785+
786+
if (cellAttributes.ariaSelected) {
766787
cell.setAttribute('aria-selected', true)
767788
} else {
768789
cell.removeAttribute('aria-selected')
769790
}
770791
}
771792
}
772793

773-
_dayClassNames(date, month) {
774-
const classNames = {
794+
_cellDayAttributes(date, month) {
795+
const isCurrentMonth = month === 'current'
796+
const isDisabled = isDateDisabled(date, this._config.minDate, this._config.maxDate, this._config.disabledDates)
797+
const isSelected = isDateSelected(date, this._startDate, this._endDate)
798+
799+
const classNames = this._classNames({
800+
[CLASS_NAME_CALENDAR_CELL]: true,
775801
...(this._config.selectionType === 'day' && this._view === 'days' && {
776-
clickable: month !== 'current' && this._config.selectAdjacementDays,
777-
disabled: isDateDisabled(date, this._config.minDate, this._config.maxDate, this._config.disabledDates),
778-
range: month === 'current' && isDateInRange(date, this._startDate, this._endDate),
779-
'range-hover': month === 'current' &&
802+
clickable: !isCurrentMonth && this._config.selectAdjacementDays,
803+
disabled: isDisabled,
804+
range: isCurrentMonth && isDateInRange(date, this._startDate, this._endDate),
805+
'range-hover': isCurrentMonth &&
780806
(this._hoverDate && this._selectEndDate ?
781807
isDateInRange(date, this._startDate, this._hoverDate) :
782808
isDateInRange(date, this._hoverDate, this._endDate)),
783-
selected: isDateSelected(date, this._startDate, this._endDate)
809+
selected: isSelected
784810
}),
785811
today: isToday(date),
786812
[month]: true
813+
})
814+
815+
return {
816+
className: classNames,
817+
tabIndex: this._config.selectionType === 'day' &&
818+
(isCurrentMonth || this._config.selectAdjacementDays) &&
819+
!isDisabled ? 0 : -1,
820+
ariaSelected: isSelected
787821
}
822+
}
788823

789-
const result = {}
824+
_cellMonthAttributes(date) {
825+
const isDisabled = isMonthDisabled(date, this._config.minDate, this._config.maxDate, this._config.disabledDates)
826+
const isSelected = isMonthSelected(date, this._startDate, this._endDate)
827+
828+
const classNames = this._classNames({
829+
[CLASS_NAME_CALENDAR_CELL]: true,
830+
disabled: isDisabled,
831+
'range-hover': this._config.selectionType === 'month' &&
832+
(this._hoverDate && this._selectEndDate ?
833+
isMonthInRange(date, this._startDate, this._hoverDate) :
834+
isMonthInRange(date, this._hoverDate, this._endDate)),
835+
range: isMonthInRange(date, this._startDate, this._endDate),
836+
selected: isSelected
837+
})
790838

791-
for (const key in classNames) {
792-
if (classNames[key] === true) {
793-
result[key] = true
794-
}
839+
return {
840+
className: classNames,
841+
tabIndex: isDisabled ? -1 : 0,
842+
ariaSelected: isSelected
795843
}
844+
}
845+
846+
_cellYearAttributes(date) {
847+
const isDisabled = isYearDisabled(date, this._config.minDate, this._config.maxDate, this._config.disabledDates)
848+
const isSelected = isYearSelected(date, this._startDate, this._endDate)
849+
850+
const classNames = this._classNames({
851+
[CLASS_NAME_CALENDAR_CELL]: true,
852+
disabled: isDisabled,
853+
'range-hover': this._config.selectionType === 'year' &&
854+
(this._hoverDate && this._selectEndDate ?
855+
isYearInRange(date, this._startDate, this._hoverDate) :
856+
isYearInRange(date, this._hoverDate, this._endDate)),
857+
range: isYearInRange(date, this._startDate, this._endDate),
858+
selected: isSelected
859+
})
796860

797-
return Object.keys(result).join(' ')
798-
}
799-
800-
_sharedClassNames(date) {
801-
const classNames = {
802-
disabled: isDateDisabled(date, this._config.minDate, this._config.maxDate, this._config.disabledDates),
803-
range: isDateInRange(date, this._startDate, this._endDate),
804-
'range-hover': (
805-
(this._config.selectionType === 'week' && this._view === 'days') ||
806-
(this._config.selectionType === 'month' && this._view === 'months') ||
807-
(this._config.selectionType === 'year' && this._view === 'years')
808-
) && (this._hoverDate && this._selectEndDate ?
809-
isDateInRange(date, this._startDate, this._hoverDate) :
810-
isDateInRange(date, this._hoverDate, this._endDate)),
811-
selected: isDateSelected(date, this._startDate, this._endDate)
861+
return {
862+
className: classNames,
863+
tabIndex: isDisabled ? -1 : 0,
864+
ariaSelected: isSelected
812865
}
866+
}
813867

814-
const result = {}
868+
_rowWeekAttributes(date) {
869+
const isDisabled = isDateDisabled(date, this._config.minDate, this._config.maxDate, this._config.disabledDates)
870+
const isSelected = isDateSelected(date, this._startDate, this._endDate)
871+
872+
const classNames = this._classNames({
873+
[CLASS_NAME_CALENDAR_ROW]: true,
874+
disabled: isDisabled,
875+
range: this._config.selectionType === 'week' && isDateInRange(date, this._startDate, this._endDate),
876+
'range-hover': this._config.selectionType === 'week' &&
877+
(this._hoverDate && this._selectEndDate ?
878+
isYearInRange(date, this._startDate, this._hoverDate) :
879+
isYearInRange(date, this._hoverDate, this._endDate)),
880+
selected: isSelected
881+
})
815882

816-
for (const key in classNames) {
817-
if (classNames[key] === true) {
818-
result[key] = true
819-
}
883+
return {
884+
className: classNames,
885+
tabIndex: this._config.selectionType === 'week' && !isDisabled ? 0 : -1,
886+
ariaSelected: isSelected
820887
}
821-
822-
return Object.keys(result).join(' ')
823888
}
824889

825890
// Static

0 commit comments

Comments
 (0)