diff --git a/resources/boost/skills/livewire-material-development/SKILL.md b/resources/boost/skills/livewire-material-development/SKILL.md
index e449690f..30784477 100644
--- a/resources/boost/skills/livewire-material-development/SKILL.md
+++ b/resources/boost/skills/livewire-material-development/SKILL.md
@@ -564,7 +564,7 @@ M3 text fields. `variant`: `outlined` or `filled`; without it, `config('livewire
M3 selection controls on native inputs; the whole row is the label.
- `` — `indeterminate` for a "select all" whose items are partly ticked (bind it to a server expression; it follows every render).
-- `` — options `['id', 'name', 'hint', 'disabled']` (`option-value`, `option-label`, `option-hint`); `value` checks an option without `wire:model`; `name` names an unbound group.
+- `` — options `['id', 'name', 'hint', 'disabled']` (`option-value`, `option-label`, `option-hint`); `value` checks an option without `wire:model`; `name` names an unbound group. M3 stacks radios and cautions against a row at any width, so reach for `inline` only for two or three short labels; it also wants five options or fewer and one of them chosen when the page loads.
- `` — M3 switch (`role="switch"`); `icons` puts a check and a cross on the handle, `icons="selected"` only the check. Without `label`, pass `aria-label`.
```blade
diff --git a/resources/js/datepicker.js b/resources/js/datepicker.js
index 84c57f86..cc439c99 100644
--- a/resources/js/datepicker.js
+++ b/resources/js/datepicker.js
@@ -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),
diff --git a/resources/views/components/choices.blade.php b/resources/views/components/choices.blade.php
index b485af4f..3e7087a3 100644
--- a/resources/views/components/choices.blade.php
+++ b/resources/views/components/choices.blade.php
@@ -7,8 +7,10 @@
you are free" is seven filter chips, not a dropdown opened seven times. `single` makes them
choice chips, one on at a time.
- **`searchable`**, for a list too long to lay out — four hundred time zones. A text field
- that filters as you type, with the matches in M3's menu under it: the arrow keys move,
- Enter chooses, Escape puts the field back. One value only. The list is a popover in the top
+ that filters as you type, with the matches in M3's menu under it: the arrow keys move, Home
+ and End go to the ends of the open list (with it closed they are the field's own and move the
+ caret), Enter chooses, Escape puts the field back — WAI-ARIA's combobox keyboard, since M3
+ has no combobox. One value only. The list is a popover in the top
layer, placed by CSS anchor positioning, so a card's clipping never cuts it off. `icon`,
`variant` and `placeholder` are the field's.
@@ -104,6 +106,14 @@
this.active = (this.active + step + this.filtered.length) % this.filtered.length;
this.$nextTick(() => this.reveal());
},
+ // The APG combobox puts Home and End at the ends of the open popup. With the popup
+ // closed they are the text field's own, and move the caret.
+ jump(event, last) {
+ if (! this.open || this.filtered.length === 0) return;
+ event.preventDefault();
+ this.active = last ? this.filtered.length - 1 : 0;
+ this.$nextTick(() => this.reveal());
+ },
choose(option) {
if (! option || option.disabled) return;
this.value = option.value;
@@ -139,6 +149,8 @@
x-on:input="open = true; active = 0"
x-on:keydown.arrow-down.prevent="move(1)"
x-on:keydown.arrow-up.prevent="move(-1)"
+ x-on:keydown.home="jump($event, false)"
+ x-on:keydown.end="jump($event, true)"
x-on:keydown.enter.prevent="choose(filtered[active])"
x-on:keydown.escape="close()"
x-on:keydown.tab="close()"
diff --git a/resources/views/components/datepicker.blade.php b/resources/views/components/datepicker.blade.php
index 9d9bc1d9..033fec61 100644
--- a/resources/views/components/datepicker.blade.php
+++ b/resources/views/components/datepicker.blade.php
@@ -286,7 +286,7 @@