feat: enable navigation to, selecting & give accessible message for calendar disabled dates (#1978)

Co-authored-by: Konstantinos Norgias <Konstantinos.Norgias@ing.com>
This commit is contained in:
gerjanvangeest 2023-07-04 10:36:18 +02:00 committed by GitHub
parent 137a1b6cf5
commit b44bfc5d1f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 619 additions and 341 deletions

View file

@ -0,0 +1,9 @@
---
'@lion/ui': patch
---
[calendar] updates:
- Enables focus to disabled dates to make it more reasonable for screen readers
- Do not automatically force selection of a valid date
- Add helper functions to find next/previous/nearest enabled date

View file

@ -190,3 +190,42 @@ export const combinedDisabledDates = () => {
`; `;
}; };
``` ```
### Finding enabled dates
The next available date may be multiple days/month in the future/past.
For that we offer convenient helpers as
- `findNextEnabledDate()`
- `findPreviousEnabledDate()`
- `findNearestEnabledDate()`
```js preview-story
export const findingEnabledDates = () => {
function getCalendar(ev) {
return ev.target.parentElement.querySelector('.js-calendar');
}
return html`
<style>
.demo-calendar {
border: 1px solid #adadad;
box-shadow: 0 0 16px #ccc;
max-width: 500px;
}
</style>
<lion-calendar
class="demo-calendar js-calendar"
.disableDates=${day => day.getDay() === 6 || day.getDay() === 0}
></lion-calendar>
<button @click="${ev => getCalendar(ev).focusDate(getCalendar(ev).findNextEnabledDate())}">
focus findNextEnabledDate
</button>
<button @click="${ev => getCalendar(ev).focusDate(getCalendar(ev).findPreviousEnabledDate())}">
focus findPreviousEnabledDate
</button>
<button @click="${ev => getCalendar(ev).focusDate(getCalendar(ev).findNearestEnabledDate())}">
focus findNearestEnabledDate
</button>
`;
};
```

View file

@ -6,6 +6,7 @@ import { MinMaxDate, IsDateDisabled } from '@lion/ui/form-core.js';
import { loadDefaultFeedbackMessages } from '@lion/ui/validate-messages.js'; import { loadDefaultFeedbackMessages } from '@lion/ui/validate-messages.js';
import { formatDate } from '@lion/ui/localize.js'; import { formatDate } from '@lion/ui/localize.js';
import '@lion/ui/define/lion-input-datepicker.js'; import '@lion/ui/define/lion-input-datepicker.js';
loadDefaultFeedbackMessages();
``` ```
## Minimum and maximum date ## Minimum and maximum date
@ -35,6 +36,7 @@ export const disableSpecificDates = () => html`
<lion-input-datepicker <lion-input-datepicker
label="IsDateDisabled" label="IsDateDisabled"
help-text="You're not allowed to choose any 15th." help-text="You're not allowed to choose any 15th."
.modelValue=${new Date('2023/06/15')}
.validators=${[new IsDateDisabled(d => d.getDate() === 15)]} .validators=${[new IsDateDisabled(d => d.getDate() === 15)]}
></lion-input-datepicker> ></lion-input-datepicker>
`; `;

View file

@ -15,6 +15,7 @@ import { dayTemplate } from './utils/dayTemplate.js';
import { getFirstDayNextMonth } from './utils/getFirstDayNextMonth.js'; import { getFirstDayNextMonth } from './utils/getFirstDayNextMonth.js';
import { getLastDayPreviousMonth } from './utils/getLastDayPreviousMonth.js'; import { getLastDayPreviousMonth } from './utils/getLastDayPreviousMonth.js';
import { isSameDate } from './utils/isSameDate.js'; import { isSameDate } from './utils/isSameDate.js';
import { getDayMonthYear } from './utils/getDayMonthYear.js';
/** /**
* @typedef {import('../types/day.js').Day} Day * @typedef {import('../types/day.js').Day} Day
@ -22,6 +23,17 @@ import { isSameDate } from './utils/isSameDate.js';
* @typedef {import('../types/day.js').Month} Month * @typedef {import('../types/day.js').Month} Month
*/ */
const isDayButton = /** @param {HTMLElement} el */ el =>
el.classList.contains('calendar__day-button');
/**
* @param {HTMLElement} el
* @returns {boolean}
*/
function isDisabledDayButton(el) {
return el.getAttribute('aria-disabled') === 'true';
}
/** /**
* @customElement lion-calendar * @customElement lion-calendar
*/ */
@ -213,19 +225,19 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
} }
goToNextMonth() { goToNextMonth() {
this.__modifyDate(1, { dateType: 'centralDate', type: 'Month', mode: 'both' }); this.__modifyDate(1, { dateType: 'centralDate', type: 'Month' });
} }
goToPreviousMonth() { goToPreviousMonth() {
this.__modifyDate(-1, { dateType: 'centralDate', type: 'Month', mode: 'both' }); this.__modifyDate(-1, { dateType: 'centralDate', type: 'Month' });
} }
goToNextYear() { goToNextYear() {
this.__modifyDate(1, { dateType: 'centralDate', type: 'FullYear', mode: 'both' }); this.__modifyDate(1, { dateType: 'centralDate', type: 'FullYear' });
} }
goToPreviousYear() { goToPreviousYear() {
this.__modifyDate(-1, { dateType: 'centralDate', type: 'FullYear', mode: 'both' }); this.__modifyDate(-1, { dateType: 'centralDate', type: 'FullYear' });
} }
/** /**
@ -238,9 +250,7 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
} }
focusCentralDate() { focusCentralDate() {
const button = /** @type {HTMLElement} */ ( const button = /** @type {HTMLElement} */ (this.shadowRoot?.querySelector('[tabindex="0"]'));
this.shadowRoot?.querySelector('button[tabindex="0"]')
);
button.focus(); button.focus();
this.__focusedDate = this.centralDate; this.__focusedDate = this.centralDate;
} }
@ -328,13 +338,8 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
return; return;
} }
const map = { if (name === '__focusedDate') {
disableDates: () => this.__disableDatesChanged(), this.__focusedDateChanged();
centralDate: () => this.__centralDateChanged(),
__focusedDate: () => this.__focusedDateChanged(),
};
if (map[name]) {
map[name]();
} }
const updateDataOn = ['centralDate', 'minDate', 'maxDate', 'selectedDate', 'disableDates']; const updateDataOn = ['centralDate', 'minDate', 'maxDate', 'selectedDate', 'disableDates'];
@ -362,10 +367,8 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
*/ */
__calculateInitialCentralDate() { __calculateInitialCentralDate() {
if (this.centralDate === this.__today && this.selectedDate) { if (this.centralDate === this.__today && this.selectedDate) {
// initialised with selectedDate only if user didn't provide another one // initialized with selectedDate only if user didn't provide another one
this.centralDate = this.selectedDate; this.centralDate = this.selectedDate;
} else {
this.__ensureValidCentralDate();
} }
/** @type {Date} */ /** @type {Date} */
this.__initialCentralDate = this.centralDate; this.__initialCentralDate = this.centralDate;
@ -581,6 +584,35 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
return `${this.msgLit(`lion-calendar:${mode}${type}`)}, ${month} ${year}`; return `${this.msgLit(`lion-calendar:${mode}${type}`)}, ${month} ${year}`;
} }
/**
*
* @private
*/
__getSelectableDateRange() {
const newMinDate = createDay(new Date(this.minDate));
const newMaxDate = createDay(new Date(this.maxDate));
const getSelectableDate = (/** @type {import("../types/day.js").Day} */ date) => {
const { dayNumber, monthName, year } = getDayMonthYear(
date,
getWeekdayNames({
locale: this.__getLocale(),
style: 'long',
firstDayOfWeek: this.firstDayOfWeek,
}),
);
return `${dayNumber} ${monthName} ${year}`;
};
const earliestSelectableDate = getSelectableDate(newMinDate);
const latestSelectableDate = getSelectableDate(newMaxDate);
return {
earliestSelectableDate,
latestSelectableDate,
};
}
/** /**
* *
* @param {Day} _day * @param {Day} _day
@ -605,13 +637,27 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
day.tabindex = day.central ? '0' : '-1'; day.tabindex = day.central ? '0' : '-1';
day.ariaPressed = day.selected ? 'true' : 'false'; day.ariaPressed = day.selected ? 'true' : 'false';
day.ariaCurrent = day.today ? 'date' : undefined; day.ariaCurrent = day.today ? 'date' : undefined;
day.disabledInfo = '';
if (this.minDate && normalizeDateTime(day.date) < normalizeDateTime(this.minDate)) { if (this.minDate && normalizeDateTime(day.date) < normalizeDateTime(this.minDate)) {
day.disabled = true; day.disabled = true;
// TODO: turn this into a translated string
day.disabledInfo = `This date is unavailable. Earliest date to select is ${
this.__getSelectableDateRange().earliestSelectableDate
}. Please select another date.`;
} }
if (this.maxDate && normalizeDateTime(day.date) > normalizeDateTime(this.maxDate)) { if (this.maxDate && normalizeDateTime(day.date) > normalizeDateTime(this.maxDate)) {
day.disabled = true; day.disabled = true;
// TODO: turn this into a translated string
day.disabledInfo = `This date is unavailable. Latest date to select is ${
this.__getSelectableDateRange().latestSelectableDate
}. Please select another date.`;
}
if (day.disabled) {
// TODO: turn this into a translated string
day.disabledInfo = `This date is unavailable. Please select another date`;
} }
return this.dayPreprocessor(day); return this.dayPreprocessor(day);
@ -641,15 +687,6 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
return data; return data;
} }
/**
* @private
*/
__disableDatesChanged() {
if (this.__connectedCallbackDone) {
this.__ensureValidCentralDate();
}
}
/** /**
* @param {Date} selectedDate * @param {Date} selectedDate
* @private * @private
@ -666,15 +703,6 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
); );
} }
/**
* @private
*/
__centralDateChanged() {
if (this.__connectedCallbackDone) {
this.__ensureValidCentralDate();
}
}
/** /**
* @private * @private
*/ */
@ -685,12 +713,30 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
} }
/** /**
* @private * @param {Date} [date]
* @returns
*/ */
__ensureValidCentralDate() { findNextEnabledDate(date) {
if (!this.__isEnabledDate(this.centralDate)) { const _date = date || this.centralDate;
this.centralDate = this.__findBestEnabledDateFor(this.centralDate); return this.__findBestEnabledDateFor(_date, { mode: 'future' });
} }
/**
* @param {Date} [date]
* @returns
*/
findPreviousEnabledDate(date) {
const _date = date || this.centralDate;
return this.__findBestEnabledDateFor(_date, { mode: 'past' });
}
/**
* @param {Date} [date]
* @returns
*/
findNearestEnabledDate(date) {
const _date = date || this.centralDate;
return this.__findBestEnabledDateFor(_date, { mode: 'both' });
} }
/** /**
@ -750,11 +796,8 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
* @private * @private
*/ */
__clickDateDelegation(ev) { __clickDateDelegation(ev) {
const isDayButton = /** @param {HTMLElement} el */ el => const el = /** @type {HTMLElement & { date: Date }} */ (ev.target);
el.classList.contains('calendar__day-button'); if (isDayButton(el) && !isDisabledDayButton(el)) {
const el = /** @type {HTMLElement & { date: Date }} */ (ev.composedPath()[0]);
if (isDayButton(el)) {
this.__dateSelectedByUser(el.date); this.__dateSelectedByUser(el.date);
} }
} }
@ -763,9 +806,6 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
* @private * @private
*/ */
__focusDateDelegation() { __focusDateDelegation() {
const isDayButton = /** @param {HTMLElement} el */ el =>
el.classList.contains('calendar__day-button');
if ( if (
!this.__focusedDate && !this.__focusedDate &&
isDayButton(/** @type {HTMLElement} el */ (this.shadowRoot?.activeElement)) isDayButton(/** @type {HTMLElement} el */ (this.shadowRoot?.activeElement))
@ -780,9 +820,6 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
* @private * @private
*/ */
__blurDateDelegation() { __blurDateDelegation() {
const isDayButton = /** @param {HTMLElement} el */ el =>
el.classList.contains('calendar__day-button');
setTimeout(() => { setTimeout(() => {
if ( if (
this.shadowRoot?.activeElement && this.shadowRoot?.activeElement &&
@ -793,42 +830,65 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
}, 1); }, 1);
} }
/**
* @param {HTMLElement & { date: Date }} el
* @private
*/
__dayButtonSelection(el) {
if (isDayButton(el)) {
this.__dateSelectedByUser(el.date);
}
}
/** /**
* @param {KeyboardEvent} ev * @param {KeyboardEvent} ev
* @private * @private
*/ */
__keyboardNavigationEvent(ev) { __keyboardNavigationEvent(ev) {
const preventedKeys = ['ArrowUp', 'ArrowDown', 'PageDown', 'PageUp']; const preventedKeys = [
'ArrowLeft',
'ArrowUp',
'ArrowRight',
'ArrowDown',
'PageDown',
'PageUp',
' ',
'Enter',
];
if (preventedKeys.includes(ev.key)) { if (preventedKeys.includes(ev.key)) {
ev.preventDefault(); ev.preventDefault();
} }
switch (ev.key) { switch (ev.key) {
case ' ':
case 'Enter':
this.__dayButtonSelection(/** @type {HTMLElement & { date: Date }} */ (ev.target));
break;
case 'ArrowUp': case 'ArrowUp':
this.__modifyDate(-7, { dateType: '__focusedDate', type: 'Date', mode: 'past' }); this.__modifyDate(-7, { dateType: '__focusedDate', type: 'Date' });
break; break;
case 'ArrowDown': case 'ArrowDown':
this.__modifyDate(7, { dateType: '__focusedDate', type: 'Date', mode: 'future' }); this.__modifyDate(7, { dateType: '__focusedDate', type: 'Date' });
break; break;
case 'ArrowLeft': case 'ArrowLeft':
this.__modifyDate(-1, { dateType: '__focusedDate', type: 'Date', mode: 'past' }); this.__modifyDate(-1, { dateType: '__focusedDate', type: 'Date' });
break; break;
case 'ArrowRight': case 'ArrowRight':
this.__modifyDate(1, { dateType: '__focusedDate', type: 'Date', mode: 'future' }); this.__modifyDate(1, { dateType: '__focusedDate', type: 'Date' });
break; break;
case 'PageDown': case 'PageDown':
if (ev.altKey === true) { if (ev.altKey === true) {
this.__modifyDate(1, { dateType: '__focusedDate', type: 'FullYear', mode: 'future' }); this.__modifyDate(1, { dateType: '__focusedDate', type: 'FullYear' });
} else { } else {
this.__modifyDate(1, { dateType: '__focusedDate', type: 'Month', mode: 'future' }); this.__modifyDate(1, { dateType: '__focusedDate', type: 'Month' });
} }
break; break;
case 'PageUp': case 'PageUp':
if (ev.altKey === true) { if (ev.altKey === true) {
this.__modifyDate(-1, { dateType: '__focusedDate', type: 'FullYear', mode: 'past' }); this.__modifyDate(-1, { dateType: '__focusedDate', type: 'FullYear' });
} else { } else {
this.__modifyDate(-1, { dateType: '__focusedDate', type: 'Month', mode: 'past' }); this.__modifyDate(-1, { dateType: '__focusedDate', type: 'Month' });
} }
break; break;
case 'Tab': case 'Tab':
@ -844,11 +904,10 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
* @param {Object} opts * @param {Object} opts
* @param {string} opts.dateType * @param {string} opts.dateType
* @param {string} opts.type * @param {string} opts.type
* @param {string} opts.mode
* @private * @private
*/ */
__modifyDate(modify, { dateType, type, mode }) { __modifyDate(modify, { dateType, type }) {
let tmpDate = new Date(this.centralDate); const tmpDate = new Date(this.centralDate);
// if we're not working with days, reset // if we're not working with days, reset
// day count to first day of the month // day count to first day of the month
if (type !== 'Date') { if (type !== 'Date') {
@ -861,9 +920,6 @@ export class LionCalendar extends LocalizeMixin(LitElement) {
const maxDays = new Date(tmpDate.getFullYear(), tmpDate.getMonth() + 1, 0).getDate(); const maxDays = new Date(tmpDate.getFullYear(), tmpDate.getMonth() + 1, 0).getDate();
tmpDate.setDate(Math.min(this.centralDate.getDate(), maxDays)); tmpDate.setDate(Math.min(this.centralDate.getDate(), maxDays));
} }
if (!this.__isEnabledDate(tmpDate)) {
tmpDate = this.__findBestEnabledDateFor(tmpDate, { mode });
}
this[dateType] = tmpDate; this[dateType] = tmpDate;
} }

View file

@ -54,6 +54,16 @@ export const calendarStyle = css`
padding: 0; padding: 0;
min-width: 40px; min-width: 40px;
min-height: 40px; min-height: 40px;
/** give div[role=button][aria-disabled] same display type as native btn */
display: inline-flex;
justify-content: center;
align-items: center;
box-sizing: border-box;
}
.calendar__day-button:focus {
border: 1px solid blue;
outline: none;
} }
.calendar__day-button__text { .calendar__day-button__text {
@ -77,9 +87,23 @@ export const calendarStyle = css`
border: 1px solid green; border: 1px solid green;
} }
.calendar__day-button[disabled] { .calendar__day-button[aria-disabled='true'] {
background-color: #fff; background-color: #fff;
color: #eee; color: #eee;
outline: none; outline: none;
} }
.u-sr-only {
position: absolute;
top: 0;
width: 1px;
height: 1px;
overflow: hidden;
clip-path: inset(100%);
clip: rect(1px, 1px, 1px, 1px);
white-space: nowrap;
border: 0;
margin: 0;
padding: 0;
}
`; `;

View file

@ -16,6 +16,7 @@ export function createDay(
today = false, today = false,
future = false, future = false,
disabled = false, disabled = false,
disabledInfo = '',
} = {}, } = {},
) { ) {
return { return {
@ -34,5 +35,6 @@ export function createDay(
tabindex: '-1', tabindex: '-1',
ariaPressed: 'false', ariaPressed: 'false',
ariaCurrent: undefined, ariaCurrent: undefined,
disabledInfo,
}; };
} }

View file

@ -1,20 +1,7 @@
import { html } from 'lit'; import { html } from 'lit';
import { ifDefined } from 'lit/directives/if-defined.js'; import { ifDefined } from 'lit/directives/if-defined.js';
import { defaultMonthLabels, getDayMonthYear } from './getDayMonthYear.js';
const defaultMonthLabels = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December',
];
const firstWeekDays = [1, 2, 3, 4, 5, 6, 7]; const firstWeekDays = [1, 2, 3, 4, 5, 6, 7];
const lastDaysOfYear = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; const lastDaysOfYear = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
@ -24,10 +11,15 @@ const lastDaysOfYear = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
* @param {{ weekdays: string[], monthsLabels?: string[] }} opts * @param {{ weekdays: string[], monthsLabels?: string[] }} opts
*/ */
export function dayTemplate(day, { weekdays, monthsLabels = defaultMonthLabels }) { export function dayTemplate(day, { weekdays, monthsLabels = defaultMonthLabels }) {
const dayNumber = day.date.getDate(); const { dayNumber, monthName, year, weekdayName } = getDayMonthYear(day, weekdays, monthsLabels);
const monthName = monthsLabels[day.date.getMonth()];
const year = day.date.getFullYear(); function __getFullDate() {
const weekdayName = day.weekOrder ? weekdays[day.weekOrder] : weekdays[0]; return `${monthName} ${year} ${weekdayName}`;
}
function __getAccessibleMessage() {
return `${day.disabledInfo}`;
}
const firstDay = dayNumber === 1; const firstDay = dayNumber === 1;
const endOfFirstWeek = day.weekOrder === 6 && firstWeekDays.includes(dayNumber); const endOfFirstWeek = day.weekOrder === 6 && firstWeekDays.includes(dayNumber);
@ -57,20 +49,14 @@ export function dayTemplate(day, { weekdays, monthsLabels = defaultMonthLabels }
?start-of-last-week=${startOfLastWeek} ?start-of-last-week=${startOfLastWeek}
?last-day=${lastDay} ?last-day=${lastDay}
> >
<button <div
role="button"
.date=${day.date} .date=${day.date}
class="calendar__day-button" class="calendar__day-button"
tabindex=${ifDefined(Number(day.tabindex))} tabindex=${ifDefined(day.tabindex)}
aria-label=${`${dayNumber} ${monthName} ${year} ${weekdayName}`} aria-pressed=${ifDefined(day.ariaPressed)}
aria-pressed=${ aria-current=${ifDefined(day.ariaCurrent)}
/** @type {'true'|'false'|'mixed'|'undefined'} */ (ifDefined(day.ariaPressed)) aria-disabled=${day.disabled ? 'true' : 'false'}
}
aria-current=${
/** @type {'page'|'step'|'location'|'date'|'time'|'true'|'false'} */ (
ifDefined(day.ariaCurrent)
)
}
?disabled=${day.disabled}
?selected=${day.selected} ?selected=${day.selected}
?past=${day.past} ?past=${day.past}
?today=${day.today} ?today=${day.today}
@ -79,8 +65,9 @@ export function dayTemplate(day, { weekdays, monthsLabels = defaultMonthLabels }
?current-month=${day.currentMonth} ?current-month=${day.currentMonth}
?next-month=${day.nextMonth} ?next-month=${day.nextMonth}
> >
<span class="calendar__day-button__text"> ${day.date.getDate()} </span> <span class="calendar__day-button__text">${dayNumber}</span>
</button> <span class="u-sr-only">${__getFullDate()} ${__getAccessibleMessage()}</span>
</div>
</td> </td>
`; `;
} }

View file

@ -0,0 +1,28 @@
export const defaultMonthLabels = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December',
];
/**
* @param {import('../../types/day.js').Day} day
* @param {string[]} weekdays
* @param {string[] } monthsLabels
*/
export function getDayMonthYear(day, weekdays, monthsLabels = defaultMonthLabels) {
const dayNumber = day.date.getDate();
const monthName = monthsLabels[day.date.getMonth()];
const year = day.date.getFullYear();
const weekdayName = day.weekOrder ? weekdays[day.weekOrder] : weekdays[0];
return { dayNumber, monthName, year, weekdayName };
}

View file

@ -34,7 +34,7 @@ export class DayObject {
*/ */
get isDisabled() { get isDisabled() {
return this.buttonEl.hasAttribute('disabled'); return this.buttonEl.getAttribute('aria-disabled') === 'true';
} }
get isSelected() { get isSelected() {
@ -54,7 +54,7 @@ export class DayObject {
} }
get monthday() { get monthday() {
return Number(this.buttonEl.textContent); return Number(this.buttonEl.children[0].textContent);
} }
/** /**

View file

@ -396,7 +396,7 @@ describe('<lion-calendar>', () => {
clock.restore(); clock.restore();
}); });
it('should set centralDate to the unique valid value when minDate and maxDate are equal', async () => { it('requires the user to set an appropriate centralDate even when minDate and maxDate are equal', async () => {
const clock = sinon.useFakeTimers({ now: new Date('2019/06/03').getTime() }); const clock = sinon.useFakeTimers({ now: new Date('2019/06/03').getTime() });
const el = await fixture(html` const el = await fixture(html`
@ -405,9 +405,18 @@ describe('<lion-calendar>', () => {
.maxDate="${new Date('2019/07/03')}" .maxDate="${new Date('2019/07/03')}"
></lion-calendar> ></lion-calendar>
`); `);
expect(isSameDate(el.centralDate, new Date('2019/07/03')), 'central date').to.be.true; const elSetting = await fixture(html`
<lion-calendar
.centralDate="${new Date('2019/07/03')}"
.minDate="${new Date('2019/07/03')}"
.maxDate="${new Date('2019/07/03')}"
></lion-calendar>
`);
clock.restore(); clock.restore();
expect(isSameDate(el.centralDate, new Date('2019/06/03')), 'central date').to.be.true;
expect(isSameDate(elSetting.centralDate, new Date('2019/07/03')), 'central date').to.be
.true;
}); });
describe('Normalization', () => { describe('Normalization', () => {
@ -482,6 +491,98 @@ describe('<lion-calendar>', () => {
}); });
describe('Navigation', () => { describe('Navigation', () => {
describe('finding enabled dates', () => {
it('has helper for `findNextEnabledDate()`, `findPreviousEnabledDate()`, `findNearestEnabledDate()`', async () => {
const el = await fixture(html`
<lion-calendar
.selectedDate="${new Date('2001/01/02')}"
.disableDates=${
/** @param {Date} date */ date => date.getDate() === 3 || date.getDate() === 4
}
></lion-calendar>
`);
const elObj = new CalendarObject(el);
el.focusDate(el.findNextEnabledDate());
await el.updateComplete;
expect(elObj.focusedDayObj?.monthday).to.equal(5);
el.focusDate(el.findPreviousEnabledDate());
await el.updateComplete;
expect(elObj.focusedDayObj?.monthday).to.equal(2);
el.focusDate(el.findNearestEnabledDate());
await el.updateComplete;
expect(elObj.focusedDayObj?.monthday).to.equal(1);
});
it('future dates take precedence over past dates when "distance" between dates is equal', async () => {
const clock = sinon.useFakeTimers({ now: new Date('2000/12/15').getTime() });
const el = await fixture(html`
<lion-calendar
.disableDates="${/** @param {Date} d */ d => d.getDate() === 15}"
></lion-calendar>
`);
el.focusDate(el.findNearestEnabledDate());
await el.updateComplete;
const elObj = new CalendarObject(el);
expect(elObj.centralDayObj?.monthday).to.equal(16);
clock.restore();
});
it('will search 750 days in the past', async () => {
const clock = sinon.useFakeTimers({ now: new Date('2000/12/15').getTime() });
const el = await fixture(html`
<lion-calendar
.disableDates="${/** @param {Date} d */ d => d.getFullYear() > 1998}"
></lion-calendar>
`);
el.focusDate(el.findNearestEnabledDate());
await el.updateComplete;
expect(el.centralDate.getFullYear()).to.equal(1998);
expect(el.centralDate.getMonth()).to.equal(11);
expect(el.centralDate.getDate()).to.equal(31);
clock.restore();
});
it('will search 750 days in the future', async () => {
const clock = sinon.useFakeTimers({ now: new Date('2000/12/15').getTime() });
const el = await fixture(html`
<lion-calendar
.disableDates="${/** @param {Date} d */ d => d.getFullYear() < 2002}"
></lion-calendar>
`);
el.focusDate(el.findNearestEnabledDate());
await el.updateComplete;
expect(el.centralDate.getFullYear()).to.equal(2002);
expect(el.centralDate.getMonth()).to.equal(0);
expect(el.centralDate.getDate()).to.equal(1);
clock.restore();
});
it('throws if no available date can be found within +/- 750 days', async () => {
const el = await fixture(html`
<lion-calendar
.disableDates="${/** @param {Date} d */ d => d.getFullYear() < 2002}"
></lion-calendar>
`);
expect(() => {
el.findNextEnabledDate(new Date('1900/01/01'));
}).to.throw(Error, 'Could not find a selectable date within +/- 750 day for 1900/1/1');
});
});
describe('Year', () => { describe('Year', () => {
it('has a button for navigation to previous year', async () => { it('has a button for navigation to previous year', async () => {
const el = await fixture( const el = await fixture(
@ -655,7 +756,7 @@ describe('<lion-calendar>', () => {
await el.updateComplete; await el.updateComplete;
expect(elObj.activeMonth).to.equal('November'); expect(elObj.activeMonth).to.equal('November');
expect(elObj.activeYear).to.equal('2000'); expect(elObj.activeYear).to.equal('2000');
expect(isSameDate(el.centralDate, new Date('2000/11/20'))).to.be.true; expect(isSameDate(el.centralDate, new Date('2000/11/15'))).to.be.true;
clock.restore(); clock.restore();
}); });
@ -677,7 +778,7 @@ describe('<lion-calendar>', () => {
await el.updateComplete; await el.updateComplete;
expect(elObj.activeMonth).to.equal('January'); expect(elObj.activeMonth).to.equal('January');
expect(elObj.activeYear).to.equal('2001'); expect(elObj.activeYear).to.equal('2001');
expect(isSameDate(el.centralDate, new Date('2001/01/10'))).to.be.true; expect(isSameDate(el.centralDate, new Date('2001/01/15'))).to.be.true;
clock.restore(); clock.restore();
}); });
@ -696,17 +797,21 @@ describe('<lion-calendar>', () => {
expect(remote.activeMonth).to.equal('September'); expect(remote.activeMonth).to.equal('September');
expect(remote.activeYear).to.equal('2019'); expect(remote.activeYear).to.equal('2019');
expect(remote.centralDayObj?.el).dom.to.equal(` expect(remote.centralDayObj?.el).dom.to.equal(`
<button <div
class="calendar__day-button" class="calendar__day-button"
aria-disabled="false"
role="button"
tabindex="0" tabindex="0"
aria-label="30 September 2019 Monday"
aria-pressed="false" aria-pressed="false"
past="" past=""
current-month=""> current-month="">
<span class="calendar__day-button__text"> <span class="calendar__day-button__text">
30 30
</span> </span>
</button> <span class="u-sr-only">
September 2019 Monday
</span>
</div>
`); `);
}); });
}); });
@ -801,7 +906,7 @@ describe('<lion-calendar>', () => {
).to.equal(true); ).to.equal(true);
}); });
it('adds "disabled" attribute to disabled dates', async () => { it('adds aria-disabled="true" attribute to disabled dates', async () => {
const clock = sinon.useFakeTimers({ now: new Date('2000/12/15').getTime() }); const clock = sinon.useFakeTimers({ now: new Date('2000/12/15').getTime() });
const el = await fixture(html` const el = await fixture(html`
@ -815,7 +920,7 @@ describe('<lion-calendar>', () => {
const elObj = new CalendarObject(el); const elObj = new CalendarObject(el);
expect( expect(
elObj.checkForAllDayObjs( elObj.checkForAllDayObjs(
/** @param {DayObject} d */ d => d.el.hasAttribute('disabled'), /** @param {DayObject} d */ d => d.el.getAttribute('aria-disabled') === 'true',
[1, 2, 30, 31], [1, 2, 30, 31],
), ),
).to.equal(true); ).to.equal(true);
@ -972,7 +1077,7 @@ describe('<lion-calendar>', () => {
expect(elObj.focusedDayObj?.monthday).to.equal(12 + 1); expect(elObj.focusedDayObj?.monthday).to.equal(12 + 1);
}); });
it('navigates (sets focus) to next selectable column item via [arrow right] key', async () => { it('navigates (sets focus) to next column item via [arrow right] key', async () => {
const el = await fixture(html` const el = await fixture(html`
<lion-calendar <lion-calendar
.selectedDate="${new Date('2001/01/02')}" .selectedDate="${new Date('2001/01/02')}"
@ -987,7 +1092,7 @@ describe('<lion-calendar>', () => {
new KeyboardEvent('keydown', { key: 'ArrowRight' }), new KeyboardEvent('keydown', { key: 'ArrowRight' }),
); );
await el.updateComplete; await el.updateComplete;
expect(elObj.focusedDayObj?.monthday).to.equal(5); expect(elObj.focusedDayObj?.monthday).to.equal(3);
}); });
it('navigates (sets focus) to next row via [arrow right] key if last item in row', async () => { it('navigates (sets focus) to next row via [arrow right] key if last item in row', async () => {
@ -1108,77 +1213,6 @@ describe('<lion-calendar>', () => {
clock.restore(); clock.restore();
}); });
it('is on day closest to today, if today (and surrounding dates) is/are disabled', async () => {
const el = await fixture(html`
<lion-calendar
.centralDate="${new Date('2000/12/15')}"
.disableDates="${/** @param {Date} d */ d => d.getDate() <= 16}"
></lion-calendar>
`);
const elObj = new CalendarObject(el);
expect(elObj.centralDayObj?.monthday).to.equal(17);
el.disableDates = d => d.getDate() >= 12;
await el.updateComplete;
expect(elObj.centralDayObj?.monthday).to.equal(11);
});
it('future dates take precedence over past dates when "distance" between dates is equal', async () => {
const clock = sinon.useFakeTimers({ now: new Date('2000/12/15').getTime() });
const el = await fixture(html`
<lion-calendar
.disableDates="${/** @param {Date} d */ d => d.getDate() === 15}"
></lion-calendar>
`);
const elObj = new CalendarObject(el);
expect(elObj.centralDayObj?.monthday).to.equal(16);
clock.restore();
});
it('will search 750 days in the past', async () => {
const clock = sinon.useFakeTimers({ now: new Date('2000/12/15').getTime() });
const el = await fixture(html`
<lion-calendar
.disableDates="${/** @param {Date} d */ d => d.getFullYear() > 1998}"
></lion-calendar>
`);
expect(el.centralDate.getFullYear()).to.equal(1998);
expect(el.centralDate.getMonth()).to.equal(11);
expect(el.centralDate.getDate()).to.equal(31);
clock.restore();
});
it('will search 750 days in the future', async () => {
const clock = sinon.useFakeTimers({ now: new Date('2000/12/15').getTime() });
const el = await fixture(html`
<lion-calendar
.disableDates="${/** @param {Date} d */ d => d.getFullYear() < 2002}"
></lion-calendar>
`);
expect(el.centralDate.getFullYear()).to.equal(2002);
expect(el.centralDate.getMonth()).to.equal(0);
expect(el.centralDate.getDate()).to.equal(1);
clock.restore();
});
it('throws if no available date can be found within +/- 750 days', async () => {
const el = await fixture(html`
<lion-calendar
.disableDates="${/** @param {Date} d */ d => d.getFullYear() < 2002}"
></lion-calendar>
`);
expect(() => {
el.centralDate = new Date('1900/01/01');
}).to.throw(Error, 'Could not find a selectable date within +/- 750 day for 1900/1/1');
});
}); });
/** /**
@ -1226,7 +1260,8 @@ describe('<lion-calendar>', () => {
it('renders each day as a button inside a table cell', async () => { it('renders each day as a button inside a table cell', async () => {
const elObj = new CalendarObject(await fixture(html`<lion-calendar></lion-calendar>`)); const elObj = new CalendarObject(await fixture(html`<lion-calendar></lion-calendar>`));
const hasBtn = /** @param {DayObject} d */ d => d.el.tagName === 'BUTTON'; const hasBtn = /** @param {DayObject} d */ d =>
d.el.tagName === 'DIV' && d.el.getAttribute('role') === 'button';
expect(elObj.checkForAllDayObjs(hasBtn)).to.equal(true); expect(elObj.checkForAllDayObjs(hasBtn)).to.equal(true);
}); });
@ -1301,31 +1336,6 @@ describe('<lion-calendar>', () => {
expect(elObj.checkForAllDayObjs(hasAriaPressed, [12])).to.equal(true); expect(elObj.checkForAllDayObjs(hasAriaPressed, [12])).to.equal(true);
}); });
// This implementation mentions "button" inbetween and doesn't mention table
// column and row. As an alternative, see Deque implementation below.
// it(`on focus on a day, the screen reader pronounces "day of the week", "day number"
// and "month" (in this order)', async () => {
// // implemented by labelelledby referencing row and column names
// const el = await fixture('<lion-calendar></lion-calendar>');
// });
// Alternative: Deque implementation
it(`sets aria-label on button, that consists of
"{day number} {month name} {year} {weekday name}"`, async () => {
const elObj = new CalendarObject(
await fixture(html`
<lion-calendar .centralDate="${new Date('2000/11/12')}"></lion-calendar>
`),
);
expect(
elObj.checkForAllDayObjs(
/** @param {DayObject} d */ d =>
d.buttonEl.getAttribute('aria-label') ===
`${d.monthday} November 2000 ${d.weekdayNameLong}`,
),
).to.equal(true);
});
/** /**
* Not in scope: * Not in scope:
* - reads the new focused day on month navigation" * - reads the new focused day on month navigation"

View file

@ -11,14 +11,18 @@ describe('dayTemplate', () => {
const el = await fixture(dayTemplate(day, { weekdays })); const el = await fixture(dayTemplate(day, { weekdays }));
expect(el).dom.to.equal(` expect(el).dom.to.equal(`
<td role="gridcell" class="calendar__day-cell"> <td role="gridcell" class="calendar__day-cell">
<button <div
class="calendar__day-button" aria-disabled="false"
aria-label="19 April 2019 Friday"
aria-pressed="false" aria-pressed="false"
role="button"
class="calendar__day-button"
tabindex="-1" tabindex="-1"
> >
<span class="calendar__day-button__text">19</span> <span class="calendar__day-button__text">19</span>
</button> <span class="u-sr-only">
April 2019 Friday
</span>
</div>
</td> </td>
`); `);
}); });

View file

@ -52,434 +52,518 @@ export default html`
<tbody> <tbody>
<tr role="row"> <tr role="row">
<td class="calendar__day-cell" role="gridcell" start-of-last-week> <td class="calendar__day-cell" role="gridcell" start-of-last-week>
<button <div
aria-label="25 November 2018 Sunday" role="button"
aria-disabled="false"
aria-pressed="false" aria-pressed="false"
class="calendar__day-button" class="calendar__day-button"
tabindex="-1" tabindex="-1"
> >
<span class="calendar__day-button__text">25</span> <span class="calendar__day-button__text">25</span>
</button> <span class="u-sr-only"> November 2018 Sunday</span>
</div>
</td> </td>
<td class="calendar__day-cell" role="gridcell"> <td class="calendar__day-cell" role="gridcell">
<button <div
aria-label="26 November 2018 Monday" role="button"
aria-disabled="false"
aria-pressed="false" aria-pressed="false"
class="calendar__day-button" class="calendar__day-button"
tabindex="-1" tabindex="-1"
> >
<span class="calendar__day-button__text">26</span> <span class="calendar__day-button__text">26</span>
</button> <span class="u-sr-only"> November 2018 Monday </span>
</div>
</td> </td>
<td class="calendar__day-cell" role="gridcell"> <td class="calendar__day-cell" role="gridcell">
<button <div
aria-label="27 November 2018 Tuesday" role="button"
aria-disabled="false"
aria-pressed="false" aria-pressed="false"
class="calendar__day-button" class="calendar__day-button"
tabindex="-1" tabindex="-1"
> >
<span class="calendar__day-button__text">27</span> <span class="calendar__day-button__text">27</span>
</button> <span class="u-sr-only"> November 2018 Tuesday </span>
</div>
</td> </td>
<td class="calendar__day-cell" role="gridcell"> <td class="calendar__day-cell" role="gridcell">
<button <div
aria-label="28 November 2018 Wednesday" role="button"
aria-disabled="false"
aria-pressed="false" aria-pressed="false"
class="calendar__day-button" class="calendar__day-button"
tabindex="-1" tabindex="-1"
> >
<span class="calendar__day-button__text">28</span> <span class="calendar__day-button__text">28</span>
</button> <span class="u-sr-only"> November 2018 Wednesday </span>
</div>
</td> </td>
<td class="calendar__day-cell" role="gridcell"> <td class="calendar__day-cell" role="gridcell">
<button <div
aria-label="29 November 2018 Thursday" role="button"
aria-disabled="false"
aria-pressed="false" aria-pressed="false"
class="calendar__day-button" class="calendar__day-button"
tabindex="-1" tabindex="-1"
> >
<span class="calendar__day-button__text">29</span> <span class="calendar__day-button__text">29</span>
</button> <span class="u-sr-only"> November 2018 Thursday </span>
</div>
</td> </td>
<td class="calendar__day-cell" role="gridcell" last-day> <td class="calendar__day-cell" role="gridcell" last-day>
<button <div
aria-label="30 November 2018 Friday" role="button"
aria-disabled="false"
aria-pressed="false" aria-pressed="false"
class="calendar__day-button" class="calendar__day-button"
tabindex="-1" tabindex="-1"
> >
<span class="calendar__day-button__text">30</span> <span class="calendar__day-button__text">30</span>
</button> <span class="u-sr-only"> November 2018 Friday </span>
</div>
</td> </td>
<td class="calendar__day-cell" role="gridcell" end-of-first-week first-day> <td class="calendar__day-cell" role="gridcell" end-of-first-week first-day>
<button <div
aria-label="1 December 2018 Saturday" role="button"
aria-disabled="false"
aria-pressed="false" aria-pressed="false"
class="calendar__day-button" class="calendar__day-button"
tabindex="-1" tabindex="-1"
> >
<span class="calendar__day-button__text">1</span> <span class="calendar__day-button__text">1</span>
</button> <span class="u-sr-only"> December 2018 Saturday </span>
</div>
</td> </td>
</tr> </tr>
<tr role="row"> <tr role="row">
<td class="calendar__day-cell" role="gridcell" start-of-first-full-week> <td class="calendar__day-cell" role="gridcell" start-of-first-full-week>
<button <div
aria-label="2 December 2018 Sunday" role="button"
aria-disabled="false"
aria-pressed="false" aria-pressed="false"
class="calendar__day-button" class="calendar__day-button"
tabindex="-1" tabindex="-1"
> >
<span class="calendar__day-button__text">2</span> <span class="calendar__day-button__text">2</span>
</button> <span class="u-sr-only"> December 2018 Sunday </span>
</div>
</td> </td>
<td class="calendar__day-cell" role="gridcell"> <td class="calendar__day-cell" role="gridcell">
<button <div
aria-label="3 December 2018 Monday" role="button"
aria-disabled="false"
aria-pressed="false" aria-pressed="false"
class="calendar__day-button" class="calendar__day-button"
tabindex="-1" tabindex="-1"
> >
<span class="calendar__day-button__text">3</span> <span class="calendar__day-button__text">3</span>
</button> <span class="u-sr-only"> December 2018 Monday </span>
</div>
</td> </td>
<td class="calendar__day-cell" role="gridcell"> <td class="calendar__day-cell" role="gridcell">
<button <div
aria-label="4 December 2018 Tuesday" role="button"
aria-disabled="false"
aria-pressed="false" aria-pressed="false"
class="calendar__day-button" class="calendar__day-button"
tabindex="-1" tabindex="-1"
> >
<span class="calendar__day-button__text">4</span> <span class="calendar__day-button__text">4</span>
</button> <span class="u-sr-only"> December 2018 Tuesday </span>
</div>
</td> </td>
<td class="calendar__day-cell" role="gridcell"> <td class="calendar__day-cell" role="gridcell">
<button <div
aria-label="5 December 2018 Wednesday" role="button"
aria-disabled="false"
aria-pressed="false" aria-pressed="false"
class="calendar__day-button" class="calendar__day-button"
tabindex="-1" tabindex="-1"
> >
<span class="calendar__day-button__text">5</span> <span class="calendar__day-button__text">5</span>
</button> <span class="u-sr-only"> December 2018 Wednesday </span>
</div>
</td> </td>
<td class="calendar__day-cell" role="gridcell"> <td class="calendar__day-cell" role="gridcell">
<button <div
aria-label="6 December 2018 Thursday" role="button"
aria-disabled="false"
aria-pressed="false" aria-pressed="false"
class="calendar__day-button" class="calendar__day-button"
tabindex="-1" tabindex="-1"
> >
<span class="calendar__day-button__text">6</span> <span class="calendar__day-button__text">6</span>
</button> <span class="u-sr-only"> December 2018 Thursday </span>
</div>
</td> </td>
<td class="calendar__day-cell" role="gridcell"> <td class="calendar__day-cell" role="gridcell">
<button <div
aria-label="7 December 2018 Friday" role="button"
aria-disabled="false"
aria-pressed="false" aria-pressed="false"
class="calendar__day-button" class="calendar__day-button"
tabindex="-1" tabindex="-1"
> >
<span class="calendar__day-button__text">7</span> <span class="calendar__day-button__text">7</span>
</button> <span class="u-sr-only"> December 2018 Friday </span>
</div>
</td> </td>
<td class="calendar__day-cell" role="gridcell"> <td class="calendar__day-cell" role="gridcell">
<button <div
aria-label="8 December 2018 Saturday" role="button"
aria-disabled="false"
aria-pressed="false" aria-pressed="false"
class="calendar__day-button" class="calendar__day-button"
tabindex="-1" tabindex="-1"
> >
<span class="calendar__day-button__text">8</span> <span class="calendar__day-button__text">8</span>
</button> <span class="u-sr-only"> December 2018 Saturday </span>
</div>
</td> </td>
</tr> </tr>
<tr role="row"> <tr role="row">
<td class="calendar__day-cell" role="gridcell"> <td class="calendar__day-cell" role="gridcell">
<button <div
aria-label="9 December 2018 Sunday" role="button"
aria-disabled="false"
aria-pressed="false" aria-pressed="false"
class="calendar__day-button" class="calendar__day-button"
tabindex="-1" tabindex="-1"
> >
<span class="calendar__day-button__text">9</span> <span class="calendar__day-button__text">9</span>
</button> <span class="u-sr-only"> December 2018 Sunday </span>
</div>
</td> </td>
<td class="calendar__day-cell" role="gridcell"> <td class="calendar__day-cell" role="gridcell">
<button <div
aria-label="10 December 2018 Monday" role="button"
aria-disabled="false"
aria-pressed="false" aria-pressed="false"
class="calendar__day-button" class="calendar__day-button"
tabindex="-1" tabindex="-1"
> >
<span class="calendar__day-button__text">10</span> <span class="calendar__day-button__text">10</span>
</button> <span class="u-sr-only"> December 2018 Monday </span>
</div>
</td> </td>
<td class="calendar__day-cell" role="gridcell"> <td class="calendar__day-cell" role="gridcell">
<button <div
aria-label="11 December 2018 Tuesday" role="button"
aria-disabled="false"
aria-pressed="false" aria-pressed="false"
class="calendar__day-button" class="calendar__day-button"
tabindex="-1" tabindex="-1"
> >
<span class="calendar__day-button__text">11</span> <span class="calendar__day-button__text">11</span>
</button> <span class="u-sr-only"> December 2018 Tuesday </span>
</div>
</td> </td>
<td class="calendar__day-cell" role="gridcell"> <td class="calendar__day-cell" role="gridcell">
<button <div
aria-label="12 December 2018 Wednesday" role="button"
aria-disabled="false"
aria-pressed="false" aria-pressed="false"
class="calendar__day-button" class="calendar__day-button"
tabindex="-1" tabindex="-1"
> >
<span class="calendar__day-button__text">12</span> <span class="calendar__day-button__text">12</span>
</button> <span class="u-sr-only"> December 2018 Wednesday </span>
</div>
</td> </td>
<td class="calendar__day-cell" role="gridcell"> <td class="calendar__day-cell" role="gridcell">
<button <div
aria-label="13 December 2018 Thursday" role="button"
aria-disabled="false"
aria-pressed="false" aria-pressed="false"
class="calendar__day-button" class="calendar__day-button"
tabindex="-1" tabindex="-1"
> >
<span class="calendar__day-button__text">13</span> <span class="calendar__day-button__text">13</span>
</button> <span class="u-sr-only"> December 2018 Thursday </span>
</div>
</td> </td>
<td class="calendar__day-cell" role="gridcell"> <td class="calendar__day-cell" role="gridcell">
<button <div
aria-label="14 December 2018 Friday" role="button"
aria-disabled="false"
aria-pressed="false" aria-pressed="false"
class="calendar__day-button" class="calendar__day-button"
tabindex="-1" tabindex="-1"
> >
<span class="calendar__day-button__text">14</span> <span class="calendar__day-button__text">14</span>
</button> <span class="u-sr-only"> December 2018 Friday </span>
</div>
</td> </td>
<td class="calendar__day-cell" role="gridcell"> <td class="calendar__day-cell" role="gridcell">
<button <div
aria-label="15 December 2018 Saturday" role="button"
aria-disabled="false"
aria-pressed="false" aria-pressed="false"
class="calendar__day-button" class="calendar__day-button"
tabindex="-1" tabindex="-1"
> >
<span class="calendar__day-button__text">15</span> <span class="calendar__day-button__text">15</span>
</button> <span class="u-sr-only"> December 2018 Saturday </span>
</div>
</td> </td>
</tr> </tr>
<tr role="row"> <tr role="row">
<td class="calendar__day-cell" role="gridcell"> <td class="calendar__day-cell" role="gridcell">
<button <div
aria-label="16 December 2018 Sunday" role="button"
aria-disabled="false"
aria-pressed="false" aria-pressed="false"
class="calendar__day-button" class="calendar__day-button"
tabindex="-1" tabindex="-1"
> >
<span class="calendar__day-button__text">16</span> <span class="calendar__day-button__text">16</span>
</button> <span class="u-sr-only"> December 2018 Sunday </span>
</div>
</td> </td>
<td class="calendar__day-cell" role="gridcell"> <td class="calendar__day-cell" role="gridcell">
<button <div
aria-label="17 December 2018 Monday" role="button"
aria-disabled="false"
aria-pressed="false" aria-pressed="false"
class="calendar__day-button" class="calendar__day-button"
tabindex="-1" tabindex="-1"
> >
<span class="calendar__day-button__text">17</span> <span class="calendar__day-button__text">17</span>
</button> <span class="u-sr-only"> December 2018 Monday </span>
</div>
</td> </td>
<td class="calendar__day-cell" role="gridcell"> <td class="calendar__day-cell" role="gridcell">
<button <div
aria-label="18 December 2018 Tuesday" role="button"
aria-disabled="false"
aria-pressed="false" aria-pressed="false"
class="calendar__day-button" class="calendar__day-button"
tabindex="-1" tabindex="-1"
> >
<span class="calendar__day-button__text">18</span> <span class="calendar__day-button__text">18</span>
</button> <span class="u-sr-only"> December 2018 Tuesday </span>
</div>
</td> </td>
<td class="calendar__day-cell" role="gridcell"> <td class="calendar__day-cell" role="gridcell">
<button <div
aria-label="19 December 2018 Wednesday" role="button"
aria-disabled="false"
aria-pressed="false" aria-pressed="false"
class="calendar__day-button" class="calendar__day-button"
tabindex="-1" tabindex="-1"
> >
<span class="calendar__day-button__text">19</span> <span class="calendar__day-button__text">19</span>
</button> <span class="u-sr-only"> December 2018 Wednesday </span>
</div>
</td> </td>
<td class="calendar__day-cell" role="gridcell"> <td class="calendar__day-cell" role="gridcell">
<button <div
aria-label="20 December 2018 Thursday" role="button"
aria-disabled="false"
aria-pressed="false" aria-pressed="false"
class="calendar__day-button" class="calendar__day-button"
tabindex="-1" tabindex="-1"
> >
<span class="calendar__day-button__text">20</span> <span class="calendar__day-button__text">20</span>
</button> <span class="u-sr-only"> December 2018 Thursday </span>
</div>
</td> </td>
<td class="calendar__day-cell" role="gridcell"> <td class="calendar__day-cell" role="gridcell">
<button <div
aria-label="21 December 2018 Friday" role="button"
aria-disabled="false"
aria-pressed="false" aria-pressed="false"
class="calendar__day-button" class="calendar__day-button"
tabindex="-1" tabindex="-1"
> >
<span class="calendar__day-button__text">21</span> <span class="calendar__day-button__text">21</span>
</button> <span class="u-sr-only"> December 2018 Friday </span>
</div>
</td> </td>
<td class="calendar__day-cell" role="gridcell"> <td class="calendar__day-cell" role="gridcell">
<button <div
aria-label="22 December 2018 Saturday" role="button"
aria-disabled="false"
aria-pressed="false" aria-pressed="false"
class="calendar__day-button" class="calendar__day-button"
tabindex="-1" tabindex="-1"
> >
<span class="calendar__day-button__text">22</span> <span class="calendar__day-button__text">22</span>
</button> <span class="u-sr-only"> December 2018 Saturday </span>
</div>
</td> </td>
</tr> </tr>
<tr role="row"> <tr role="row">
<td class="calendar__day-cell" role="gridcell"> <td class="calendar__day-cell" role="gridcell">
<button <div
aria-label="23 December 2018 Sunday" role="button"
aria-disabled="false"
aria-pressed="false" aria-pressed="false"
class="calendar__day-button" class="calendar__day-button"
tabindex="-1" tabindex="-1"
> >
<span class="calendar__day-button__text">23</span> <span class="calendar__day-button__text">23</span>
</button> <span class="u-sr-only"> December 2018 Sunday </span>
</div>
</td> </td>
<td class="calendar__day-cell" role="gridcell"> <td class="calendar__day-cell" role="gridcell">
<button <div
aria-label="24 December 2018 Monday" role="button"
aria-disabled="false"
aria-pressed="false" aria-pressed="false"
class="calendar__day-button" class="calendar__day-button"
tabindex="-1" tabindex="-1"
> >
<span class="calendar__day-button__text">24</span> <span class="calendar__day-button__text">24</span>
</button> <span class="u-sr-only"> December 2018 Monday </span>
</div>
</td> </td>
<td class="calendar__day-cell" role="gridcell"> <td class="calendar__day-cell" role="gridcell">
<button <div
aria-label="25 December 2018 Tuesday" role="button"
aria-disabled="false"
aria-pressed="false" aria-pressed="false"
class="calendar__day-button" class="calendar__day-button"
tabindex="-1" tabindex="-1"
> >
<span class="calendar__day-button__text">25</span> <span class="calendar__day-button__text">25</span>
</button> <span class="u-sr-only"> December 2018 Tuesday </span>
</div>
</td> </td>
<td class="calendar__day-cell" role="gridcell"> <td class="calendar__day-cell" role="gridcell">
<button <div
aria-label="26 December 2018 Wednesday" role="button"
aria-disabled="false"
aria-pressed="false" aria-pressed="false"
class="calendar__day-button" class="calendar__day-button"
tabindex="-1" tabindex="-1"
> >
<span class="calendar__day-button__text">26</span> <span class="calendar__day-button__text">26</span>
</button> <span class="u-sr-only"> December 2018 Wednesday </span>
</div>
</td> </td>
<td class="calendar__day-cell" role="gridcell"> <td class="calendar__day-cell" role="gridcell">
<button <div
aria-label="27 December 2018 Thursday" role="button"
aria-disabled="false"
aria-pressed="false" aria-pressed="false"
class="calendar__day-button" class="calendar__day-button"
tabindex="-1" tabindex="-1"
> >
<span class="calendar__day-button__text">27</span> <span class="calendar__day-button__text">27</span>
</button> <span class="u-sr-only"> December 2018 Thursday </span>
</div>
</td> </td>
<td class="calendar__day-cell" role="gridcell"> <td class="calendar__day-cell" role="gridcell">
<button <div
aria-label="28 December 2018 Friday" role="button"
aria-disabled="false"
aria-pressed="false" aria-pressed="false"
class="calendar__day-button" class="calendar__day-button"
tabindex="-1" tabindex="-1"
> >
<span class="calendar__day-button__text">28</span> <span class="calendar__day-button__text">28</span>
</button> <span class="u-sr-only"> December 2018 Friday </span>
</div>
</td> </td>
<td class="calendar__day-cell" role="gridcell" end-of-last-full-week> <td class="calendar__day-cell" role="gridcell" end-of-last-full-week>
<button <div
aria-label="29 December 2018 Saturday" role="button"
aria-disabled="false"
aria-pressed="false" aria-pressed="false"
class="calendar__day-button" class="calendar__day-button"
tabindex="-1" tabindex="-1"
> >
<span class="calendar__day-button__text">29</span> <span class="calendar__day-button__text">29</span>
</button> <span class="u-sr-only"> December 2018 Saturday </span>
</div>
</td> </td>
</tr> </tr>
<tr role="row"> <tr role="row">
<td class="calendar__day-cell" role="gridcell" start-of-last-week> <td class="calendar__day-cell" role="gridcell" start-of-last-week>
<button <div
aria-label="30 December 2018 Sunday" role="button"
aria-disabled="false"
aria-pressed="false" aria-pressed="false"
class="calendar__day-button" class="calendar__day-button"
tabindex="-1" tabindex="-1"
> >
<span class="calendar__day-button__text">30</span> <span class="calendar__day-button__text">30</span>
</button> <span class="u-sr-only"> December 2018 Sunday </span>
</div>
</td> </td>
<td class="calendar__day-cell" last-day role="gridcell"> <td class="calendar__day-cell" last-day role="gridcell">
<button <div
aria-label="31 December 2018 Monday" role="button"
aria-disabled="false"
aria-pressed="false" aria-pressed="false"
class="calendar__day-button" class="calendar__day-button"
tabindex="-1" tabindex="-1"
> >
<span class="calendar__day-button__text">31</span> <span class="calendar__day-button__text">31</span>
</button> <span class="u-sr-only"> December 2018 Monday </span>
</div>
</td> </td>
<td class="calendar__day-cell" role="gridcell" first-day> <td class="calendar__day-cell" role="gridcell" first-day>
<button <div
aria-label="1 January 2019 Tuesday" role="button"
aria-disabled="false"
aria-pressed="false" aria-pressed="false"
class="calendar__day-button" class="calendar__day-button"
tabindex="-1" tabindex="-1"
> >
<span class="calendar__day-button__text">1</span> <span class="calendar__day-button__text">1</span>
</button> <span class="u-sr-only"> January 2019 Tuesday </span>
</div>
</td> </td>
<td class="calendar__day-cell" role="gridcell"> <td class="calendar__day-cell" role="gridcell">
<button <div
aria-label="2 January 2019 Wednesday" role="button"
aria-disabled="false"
aria-pressed="false" aria-pressed="false"
class="calendar__day-button" class="calendar__day-button"
tabindex="-1" tabindex="-1"
> >
<span class="calendar__day-button__text">2</span> <span class="calendar__day-button__text">2</span>
</button> <span class="u-sr-only"> January 2019 Wednesday </span>
</div>
</td> </td>
<td class="calendar__day-cell" role="gridcell"> <td class="calendar__day-cell" role="gridcell">
<button <div
aria-label="3 January 2019 Thursday" role="button"
aria-disabled="false"
aria-pressed="false" aria-pressed="false"
class="calendar__day-button" class="calendar__day-button"
tabindex="-1" tabindex="-1"
> >
<span class="calendar__day-button__text">3</span> <span class="calendar__day-button__text">3</span>
</button> <span class="u-sr-only"> January 2019 Thursday </span>
</div>
</td> </td>
<td class="calendar__day-cell" role="gridcell"> <td class="calendar__day-cell" role="gridcell">
<button <div
aria-label="4 January 2019 Friday" role="button"
aria-disabled="false"
aria-pressed="false" aria-pressed="false"
class="calendar__day-button" class="calendar__day-button"
tabindex="-1" tabindex="-1"
> >
<span class="calendar__day-button__text">4</span> <span class="calendar__day-button__text">4</span>
</button> <span class="u-sr-only"> January 2019 Friday </span>
</div>
</td> </td>
<td class="calendar__day-cell" role="gridcell" end-of-first-week> <td class="calendar__day-cell" role="gridcell" end-of-first-week>
<button <div
aria-label="5 January 2019 Saturday" role="button"
aria-disabled="false"
aria-pressed="false" aria-pressed="false"
class="calendar__day-button" class="calendar__day-button"
tabindex="-1" tabindex="-1"
> >
<span class="calendar__day-button__text">5</span> <span class="calendar__day-button__text">5</span>
</button> <span class="u-sr-only"> January 2019 Saturday </span>
</div>
</td> </td>
</tr> </tr>
</tbody> </tbody>

View file

@ -14,6 +14,7 @@ export declare interface Day {
tabindex?: string; tabindex?: string;
ariaPressed?: string; ariaPressed?: string;
ariaCurrent?: string | undefined; ariaCurrent?: string | undefined;
disabledInfo?: string | undefined;
} }
export declare interface Week { export declare interface Week {

View file

@ -96,10 +96,28 @@ export class LionInputDatepicker extends ScopedElementsMixin(
__calendarDisableDates: { __calendarDisableDates: {
attribute: false, attribute: false,
type: Array,
}, },
}; };
} }
_inputFormatter = new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
}).formatToParts;
/**
* @param {Date} date
*/
// eslint-disable-next-line class-methods-use-this
_formatDate(date) {
const day = String(date.getDate()).padStart(2, '0');
const month = String(date.getMonth() + 1).padStart(2, '0');
const year = String(date.getFullYear());
return `${day}/${month}/${year}`;
}
get slots() { get slots() {
return { return {
...super.slots, ...super.slots,
@ -346,6 +364,7 @@ export class LionInputDatepicker extends ScopedElementsMixin(
} }
/** /**
* Triggered when a user selects a date from the calendar overlay
* @param {{ target: { selectedDate: Date }}} opts * @param {{ target: { selectedDate: Date }}} opts
*/ */
_onCalendarUserSelectedChanged({ target: { selectedDate } }) { _onCalendarUserSelectedChanged({ target: { selectedDate } }) {
@ -355,8 +374,21 @@ export class LionInputDatepicker extends ScopedElementsMixin(
if (this._syncOnUserSelect) { if (this._syncOnUserSelect) {
// Synchronize new selectedDate value to input // Synchronize new selectedDate value to input
this._isHandlingUserInput = true; this._isHandlingUserInput = true;
this._isHandlingCalendarUserInput = true;
if (
Array.isArray(this.__calendarDisableDates) &&
this.__calendarDisableDates.includes(selectedDate)
) {
// If the selected date is disabled, reset the values
this.value = '';
this.formattedValue = '';
this.modelValue = undefined;
} else {
this.formattedValue = this._formatDate(selectedDate);
this.value = this.formattedValue;
this.modelValue = selectedDate; this.modelValue = selectedDate;
}
this._isHandlingUserInput = false; this._isHandlingUserInput = false;
this._isHandlingCalendarUserInput = false; this._isHandlingCalendarUserInput = false;
} }