Finish the keyboards, and say a state once

Shift+M and Shift+Y reach the month and year dropdowns, which is M3's
date picker keyboard table; Home and End go to the ends of an open
combobox list, and stay the field's own caret keys when it is closed. The
password reveal drops aria-pressed and keeps only its flipping label, so
a screen reader hears the state once rather than twice and inverted. The
radio's `inline` now says that M3 cautions against a row and wants an
option chosen on load.

Plan step 20, findings IN-22, IN-23, IN-28 and IN-29.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
This commit is contained in:
Andreas Reinhold / reini
2026-09-14 06:13:00 +02:00
co-authored by Claude Fable 5.1
parent 856b6f476b
commit 0074f9bd5c
10 changed files with 69 additions and 14 deletions
+27 -2
View File
@@ -18,8 +18,11 @@
*
* The keyboard is WAI-ARIA's date picker dialog: arrows move a day or a week, Home and End go to
* the start and end of the week, PageUp and PageDown a month (with Shift a year), Space selects,
* Enter selects and confirms, Escape closes and hands focus back to the field. Focus never leaves
* the `min``max` span, so a disabled day cannot be chosen from the keyboard either.
* Enter selects and confirms, Escape closes and hands focus back to the field. M3's own table says
* Home and End "move to the first day of the month"; the APG behaviour is the stronger web
* convention and is what this keeps. Shift+M and Shift+Y are M3's, and reach the month and year
* dropdowns. Focus never leaves the `min``max` span, so a disabled day cannot be chosen from the
* keyboard either.
*/
import { upTo } from './breakpoints.js'
@@ -576,11 +579,33 @@ document.addEventListener('alpine:init', () => {
return config.range ? value.start : value
},
/**
* The month or the year dropdown, whichever of them this presentation draws: the docked
* picker has one of each, the modal picker a single button that opens the years.
*/
menuButton(view) {
const buttons = [...this.$refs.dialog.querySelectorAll('[data-datepicker-menu-button]')].filter((button) => button.getClientRects().length > 0)
return buttons.find((button) => button.dataset.datepickerMenuButton === view) ?? buttons[0] ?? null
},
gridKey(event) {
const rtl = getComputedStyle(this.$refs.dialog).direction === 'rtl'
const focused = this.focused
const column = (utc(...parts(focused)).getUTCDay() - this.firstDay + 7) % 7
// M3's keyboard table: Shift+M moves to the month dropdown, Shift+Y to the year one.
if (event.shiftKey && !event.ctrlKey && !event.metaKey && !event.altKey && ['M', 'Y'].includes(event.key.toUpperCase())) {
const button = this.menuButton(event.key.toUpperCase() === 'M' ? 'months' : 'years')
if (button) {
event.preventDefault()
button.focus()
}
return
}
const target = {
ArrowRight: () => addDays(focused, rtl ? -1 : 1),
ArrowLeft: () => addDays(focused, rtl ? 1 : -1),