Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@ export const monthCalendarStyles = css`

[part~='weekday'],
[part~='week-number'],
[part~='date'] {
[part~='date'],
[part~='date-button'] {
align-items: center;
display: flex;
justify-content: center;
Expand All @@ -81,7 +82,6 @@ export const monthCalendarStyles = css`
position: relative;
height: var(--vaadin-date-picker-date-height, 2rem);
cursor: var(--vaadin-clickable-cursor);
outline: none;
}

[part~='date']:empty {
Expand All @@ -97,7 +97,13 @@ export const monthCalendarStyles = css`
aspect-ratio: 1;
}

:where([part~='date']:focus-visible)::after {
[part~='date-button'] {
flex: 1;
align-self: stretch;
outline: none;
}

:where([part~='date']:has(:focus-visible))::after {
outline: var(--vaadin-focus-ring-width) solid var(--vaadin-focus-ring-color);
outline-offset: calc(var(--vaadin-focus-ring-width) * -1);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,10 @@ export const DatePickerOverlayContentMixin = (superClass) =>
return this.calendars.map((calendar) => calendar.focusableDateElement).find(Boolean);
}

get focusableDateButton() {
return this.calendars.map((calendar) => calendar.focusableDateButton).find(Boolean);
}

/** @protected */
_initControllers() {
this.addController(
Expand Down Expand Up @@ -897,12 +901,12 @@ export const DatePickerOverlayContentMixin = (superClass) =>
__tryFocusDate() {
const dateToFocus = this.__pendingDateFocus;
if (dateToFocus) {
// Check the date element with tabindex="0"
// Check the cell of the date that has the focusable button
const dateElement = this.focusableDateElement;

if (dateElement && dateEquals(dateElement.date, this.__pendingDateFocus)) {
delete this.__pendingDateFocus;
dateElement.focus();
this.focusableDateButton.focus();
}
}
}
Expand Down
27 changes: 22 additions & 5 deletions packages/date-picker/src/vaadin-month-calendar-mixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -146,15 +146,32 @@ export const MonthCalendarMixin = (superClass) =>
return ['__focusedDateChanged(focusedDate, _days)', '_showWeekNumbersChanged(showWeekNumbers, i18n)'];
}

/**
* The date cell of the focused date. It carries the `date` property and the date part names,
* while the button inside it is the element that takes DOM focus.
*/
get focusableDateElement() {
return [...this.shadowRoot.querySelectorAll('[part~=date]')].find((datePart) => {
return dateEquals(datePart.date, this.focusedDate);
});
}

/**
* The button inside the cell of the focused date. Screen readers get the date's name and state
* from this element, so it is also the element that takes DOM focus.
*/
get focusableDateButton() {
const cell = this.focusableDateElement;
return cell ? cell.querySelector('[part~=date-button]') : undefined;
}

/** @protected */
ready() {
super.ready();

// Use application to enforce focus mode for date cells in NVDA
this.setAttribute('role', 'application');
Comment thread
web-padawan marked this conversation as resolved.

addListener(this.$.monthGrid, 'tap', this._handleTap.bind(this));
}

Expand Down Expand Up @@ -294,11 +311,11 @@ export const MonthCalendarMixin = (superClass) =>

/** @protected */
_handleTap(e) {
if (!this.ignoreTaps && !this._notTapping && e.target.date && !e.target.hasAttribute('disabled')) {
this.selectedDate = e.target.date;
this.dispatchEvent(
new CustomEvent('date-tap', { detail: { date: e.target.date }, bubbles: true, composed: true }),
);
// A tap can land on the button or on the cell padding around it.
const cell = e.target.closest('[part~=date]');
if (!this.ignoreTaps && !this._notTapping && cell?.date && !cell.hasAttribute('disabled')) {
this.selectedDate = cell.date;
this.dispatchEvent(new CustomEvent('date-tap', { detail: { date: cell.date }, bubbles: true, composed: true }));
}
}

Expand Down
28 changes: 18 additions & 10 deletions packages/date-picker/src/vaadin-month-calendar.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Copyright (c) 2016 - 2026 Vaadin Ltd.
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
*/
import { html, LitElement } from 'lit';
import { html, LitElement, nothing } from 'lit';
import { defineCustomElement } from '@vaadin/component-base/src/define.js';
import { PolylitMixin } from '@vaadin/component-base/src/polylit-mixin.js';
import { LumoInjectionMixin } from '@vaadin/vaadin-themable-mixin/lumo-injection-mixin.js';
Expand Down Expand Up @@ -80,16 +80,24 @@ class MonthCalendar extends MonthCalendarMixin(ThemableMixin(PolylitMixin(LumoIn
)}"
.date="${date}"
?disabled="${this.__isDayDisabled(date, this.minDate, this.maxDate, this.isDateDisabled)}"
tabindex="${this.__computeDayTabIndex(date, this.focusedDate)}"
aria-selected="${this.__computeDayAriaSelected(date, this.selectedDate)}"
aria-disabled="${this.__computeDayAriaDisabled(
date,
this.minDate,
this.maxDate,
this.isDateDisabled,
)}"
aria-label="${this.__computeDayAriaLabel(date)}"
>${this._getDate(date)}</td
>${
date
? html`<div
part="date-button"
role="button"
tabindex="${this.__computeDayTabIndex(date, this.focusedDate)}"
aria-label="${this.__computeDayAriaLabel(date)}"
aria-disabled="${this.__computeDayAriaDisabled(
date,
this.minDate,
this.maxDate,
this.isDateDisabled,
)}"
>${this._getDate(date)}</div
>`
: nothing
}</td
>
`;
})}
Expand Down
21 changes: 15 additions & 6 deletions packages/date-picker/test/date-metadata-provider.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,16 @@ import sinon from 'sinon';
import '../src/vaadin-date-picker.js';
import { DateMetadataController } from '../src/vaadin-date-metadata-controller.js';
import { formatISODate, monthIndex, parseDate } from '../src/vaadin-date-picker-helper.js';
import { getCalendars, getDateCell, getDateCells, getMonthCalendar, isoDate, isoDateInMonth, open } from './helpers.js';
import {
getCalendars,
getDateButton,
getDateCell,
getDateCells,
getMonthCalendar,
isoDate,
isoDateInMonth,
open,
} from './helpers.js';

describe('dateMetadataProvider integration', () => {
let datePicker, overlayContent, today, year, month;
Expand Down Expand Up @@ -84,8 +93,8 @@ describe('dateMetadataProvider integration', () => {
});

it('should set aria-disabled on a provider-disabled date', () => {
expect(cell15.getAttribute('aria-disabled')).to.equal('true');
expect(cell16.getAttribute('aria-disabled')).to.equal('false');
expect(getDateButton(cell15).getAttribute('aria-disabled')).to.equal('true');
expect(getDateButton(cell16).getAttribute('aria-disabled')).to.equal('false');
});

it('should not report loading for an answer that needs no waiting', () => {
Expand Down Expand Up @@ -121,7 +130,7 @@ describe('dateMetadataProvider integration', () => {

it('should keep the dates of a pending month selectable', () => {
expect(isDisabled(cell15)).to.be.false;
expect(cell15.getAttribute('aria-disabled')).to.equal('false');
expect(getDateButton(cell15).getAttribute('aria-disabled')).to.equal('false');
});

it('should commit a date picked from a month being fetched', async () => {
Expand All @@ -144,10 +153,10 @@ describe('dateMetadataProvider integration', () => {
expect(overlayContent.hasAttribute('aria-busy')).to.be.false;

expect(isDisabled(cell15)).to.be.true;
expect(cell15.getAttribute('aria-disabled')).to.equal('true');
expect(getDateButton(cell15).getAttribute('aria-disabled')).to.equal('true');

expect(isDisabled(cell16)).to.be.false;
expect(cell16.getAttribute('aria-disabled')).to.equal('false');
expect(getDateButton(cell16).getAttribute('aria-disabled')).to.equal('false');

expect(cell15.part.contains('loading')).to.be.false;
expect(cell16.part.contains('loading')).to.be.false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -389,27 +389,42 @@ snapshots["vaadin-date-picker host opened default"] =
</vaadin-button>
<vaadin-date-picker-month-scroller slot="months">
<div slot="vaadin-infinite-scroller-item-content-4">
<vaadin-month-calendar aria-hidden="true">
<vaadin-month-calendar
aria-hidden="true"
role="application"
>
</vaadin-month-calendar>
</div>
<div slot="vaadin-infinite-scroller-item-content-5">
<vaadin-month-calendar aria-hidden="true">
<vaadin-month-calendar
aria-hidden="true"
role="application"
>
</vaadin-month-calendar>
</div>
<div slot="vaadin-infinite-scroller-item-content-6">
<vaadin-month-calendar aria-hidden="true">
<vaadin-month-calendar
aria-hidden="true"
role="application"
>
</vaadin-month-calendar>
</div>
<div slot="vaadin-infinite-scroller-item-content-7">
<vaadin-month-calendar>
<vaadin-month-calendar role="application">
</vaadin-month-calendar>
</div>
<div slot="vaadin-infinite-scroller-item-content-8">
<vaadin-month-calendar aria-hidden="true">
<vaadin-month-calendar
aria-hidden="true"
role="application"
>
</vaadin-month-calendar>
</div>
<div slot="vaadin-infinite-scroller-item-content-9">
<vaadin-month-calendar aria-hidden="true">
<vaadin-month-calendar
aria-hidden="true"
role="application"
>
</vaadin-month-calendar>
</div>
</vaadin-date-picker-month-scroller>
Expand Down
Loading
Loading