Skip to content
Draft
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
18 changes: 9 additions & 9 deletions packages/date-picker/src/vaadin-date-picker-mixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
* Copyright (c) 2016 - 2025 Vaadin Ltd.
* This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
*/
import { hideOthers } from '@vaadin/a11y-base/src/aria-hidden.js';
import { DelegateFocusMixin } from '@vaadin/a11y-base/src/delegate-focus-mixin.js';
import { isKeyboardActive } from '@vaadin/a11y-base/src/focus-utils.js';
import { KeyboardMixin } from '@vaadin/a11y-base/src/keyboard-mixin.js';
Expand Down Expand Up @@ -461,6 +460,15 @@ export const DatePickerMixin = (subclass) =>
// Currently only supported for locales that start the week on Monday.
this.toggleAttribute('week-numbers', this.showWeekNumbers && this.__effectiveI18n.firstDayOfWeek === 1);
}

if ((props.has('opened') || props.has('_fullscreen')) && this._overlayContent) {
if (this.opened && this._fullscreen) {
// Mark as modal on mobile if the input can not be accessed
this._overlayContent.setAttribute('aria-modal', 'true');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not really able to verify if this is effective. We want other elements on the page to be removed from the accessibility tree.

In VoiceOver, bringing up the web rotor still lists heading and form controls in the background of the overlay. This does not happen with the previous aria-hidden logic. So aria-modal doesn't seem to do what it should here.

In NVDA, when the focus is in the overlay it is at least not possible to navigate to other parts of the page using shortcuts like h (next / previous heading) or f (next / previous form control). But that is also the case when not using aria-modal, so it seems that is probably how navigation works in general within dialogs in NVDA. There is a NVDA+F7 shortcut that is supposed to list other available elements on the page, but I have not figured out how to trigger that in my VM.

I do not have JAWS, so I wasn't able to test with that, maybe you could give it a try?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tested with JAWS and it seems to be possible to escape the content using hotkeys like F / Shift + F for next / previous form control (although for some reason it only worked after I toggle virtual cursor mode off and back on).

Screenshot 2025-08-13 at 15 08 06 Screenshot 2025-08-13 at 15 09 53

} else {
this._overlayContent.removeAttribute('aria-modal');
}
}
}

/** @protected */
Expand Down Expand Up @@ -910,9 +918,6 @@ export const DatePickerMixin = (subclass) =>
input.blur();
this._overlayContent.focusDateElement();
}

const focusables = this._noInput ? content : [input, content];
this.__showOthers = hideOthers(focusables);
}

/** @private */
Expand Down Expand Up @@ -959,11 +964,6 @@ export const DatePickerMixin = (subclass) =>

/** @protected */
_onOverlayClosed() {
// Reset `aria-hidden` state.
if (this.__showOthers) {
this.__showOthers();
this.__showOthers = null;
}
window.removeEventListener('scroll', this._boundOnScroll, true);

if (this._closedByEscape) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,11 @@ export const DatePickerOverlayContentMixin = (superClass) =>
}

/** @protected */
_initControllers() {
firstUpdated() {
super.firstUpdated();

this.setAttribute('role', 'dialog');

this.addController(
new MediaQueryController(this._desktopMediaQuery, (matches) => {
this._desktopMode = matches;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,6 @@ class DatePickerOverlayContent extends DatePickerOverlayContentMixin(
</div>
`;
}

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

this.setAttribute('role', 'dialog');
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved this to the mixin - extra method feels unnecessary now when we removed Polymer version.


this._initControllers();
}
}

defineCustomElement(DatePickerOverlayContent);
Original file line number Diff line number Diff line change
Expand Up @@ -319,16 +319,12 @@ snapshots["vaadin-date-picker host opened default"] =
top-aligned=""
>
<label
aria-hidden="true"
data-aria-hidden="true"
for="search-input-vaadin-date-picker-3"
id="label-vaadin-date-picker-0"
slot="label"
>
</label>
<div
aria-hidden="true"
data-aria-hidden="true"
hidden=""
id="error-message-vaadin-date-picker-2"
slot="error-message"
Expand Down
53 changes: 18 additions & 35 deletions packages/date-picker/test/wai-aria.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,24 @@ describe('WAI-ARIA', () => {
expect(calendar.getAttribute('aria-hidden')).to.equal(focusable ? null : 'true');
});
});

it('should not set aria-modal attribute on the overlay content on open by default', async () => {
await open(datePicker);
const content = datePicker._overlayContent;

expect(content.hasAttribute('aria-modal')).to.be.false;
});

it('should toggle aria-modal attribute on the overlay content on open if fullscreen', async () => {
datePicker._fullscreen = true;

await open(datePicker);
const content = datePicker._overlayContent;
expect(content.getAttribute('aria-modal')).to.equal('true');

datePicker.close();
expect(content.hasAttribute('aria-modal')).to.be.false;
});
});

describe('overlay contents', () => {
Expand Down Expand Up @@ -84,39 +102,4 @@ describe('WAI-ARIA', () => {
expect(todayElement.getAttribute('aria-label')).to.match(/, Today$/u);
});
});

describe('aria-hidden', () => {
let wrapper, datePicker, input, button;

beforeEach(async () => {
wrapper = fixtureSync(`
<div>
<button>Button</button>
<vaadin-date-picker></vaadin-date-picker>
<input placeholder="input" />
</div>
`);
await nextRender();
[button, datePicker, input] = wrapper.children;
});

it('should set aria-hidden on other elements when overlay is opened', async () => {
await open(datePicker);
expect(button.getAttribute('aria-hidden')).to.equal('true');
expect(input.getAttribute('aria-hidden')).to.equal('true');
});

it('should not set aria-hidden on slotted input and overlay element', async () => {
await open(datePicker);
expect(datePicker.inputElement.hasAttribute('aria-hidden')).to.be.false;
expect(datePicker.$.overlay.hasAttribute('aria-hidden')).to.be.false;
});

it('should remove aria-hidden from other elements when overlay is closed', async () => {
await open(datePicker);
datePicker.close();
expect(button.hasAttribute('aria-hidden')).to.be.false;
expect(input.hasAttribute('aria-hidden')).to.be.false;
});
});
});