Hold the time picker's two selectors apart
The 24-hour clock has no period selector beside the hour and minute boxes, so M3 widens them to 114px; AM/PM takes its own tertiary-container rather than borrowing the boxes' primary-container; a disabled period button uses M3's 12% disabled container, not 10%; and the pair is a radio group, which is what M3's labelling table asks for, so the arrows move between AM and PM. Plan step 20, findings IN-10, IN-11, IN-26 and IN-27. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
f30fd575cf
commit
9624d4bfd4
@@ -11,7 +11,9 @@
|
|||||||
* [data-timepicker-display] the time selector: hour and minute boxes 96×80 (display-large,
|
* [data-timepicker-display] the time selector: hour and minute boxes 96×80 (display-large,
|
||||||
* small corner; primary-container when selected, otherwise
|
* small corner; primary-container when selected, otherwise
|
||||||
* surface-container-highest), a 24px separator, and the period
|
* surface-container-highest), a 24px separator, and the period
|
||||||
* selector 52×80 beside them, 4px away
|
* selector 52×80 beside them, 4px away. On a 24-hour clock
|
||||||
|
* (data-cycle="24") there is no period selector and the boxes are
|
||||||
|
* 114px, TimeSelector24HVerticalContainerWidth.
|
||||||
* [data-timepicker-dial] 256px, surface-container-highest; numbers in body-large at radius
|
* [data-timepicker-dial] 256px, surface-container-highest; numbers in body-large at radius
|
||||||
* 101 (the 24-hour inner ring at 69), a primary selector — a 2px line,
|
* 101 (the 24-hour inner ring at 69), a primary selector — a 2px line,
|
||||||
* an 8px centre and a 48px handle — with the number under the handle
|
* an 8px centre and a 48px handle — with the number under the handle
|
||||||
@@ -20,9 +22,11 @@
|
|||||||
* "Minute" (or the error) below in body-small, the period 52×72
|
* "Minute" (or the error) below in body-small, the period 52×72
|
||||||
* [data-timepicker-actions] a 48px row: the mode toggle, then Cancel and OK
|
* [data-timepicker-actions] a 48px row: the mode toggle, then Cancel and OK
|
||||||
*
|
*
|
||||||
* The period selector is Compose's current one (ComposeMaterial3Flags.isUpdatedTimepickerToggleEnabled,
|
* The period selector keeps Compose's current shape (ComposeMaterial3Flags.isUpdatedTimepickerToggleEnabled,
|
||||||
* on by default): two toggle buttons 4px apart, round and surface-container-lowest when off, a 12px
|
* on by default): two buttons 4px apart, round and surface-container-lowest when off, a 12px corner
|
||||||
* corner and primary-container with a bold label when on, not the outlined pair of the tokens.
|
* when on, not the outlined pair of the tokens. The selected colour is M3's own tertiary-container
|
||||||
|
* with a bold label (PeriodSelectorSelectedContainerColor), which keeps AM/PM apart from the
|
||||||
|
* primary-container hour and minute boxes. They are radios, so the state is `aria-checked`.
|
||||||
*
|
*
|
||||||
* In a landscape window with no room for the upright dial the dial variant lies on its side, as
|
* In a landscape window with no room for the upright dial the dial variant lies on its side, as
|
||||||
* Compose's HorizontalTimePicker does: the display with the period selector (216×38, 16px under it)
|
* Compose's HorizontalTimePicker does: the display with the period selector (216×38, 16px under it)
|
||||||
@@ -130,6 +134,13 @@
|
|||||||
color: var(--md-sys-color-on-primary-container);
|
color: var(--md-sys-color-on-primary-container);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* A 24-hour clock has no period selector beside the boxes, and M3 widens them into the room
|
||||||
|
(TimePickerTokens.TimeSelector24HVerticalContainerWidth = 114dp). Landscape keeps 96px: the
|
||||||
|
display is a 216px column there. */
|
||||||
|
[data-timepicker-display][data-cycle="24"] [data-timepicker-box] {
|
||||||
|
width: 7.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
[data-timepicker-separator] {
|
[data-timepicker-separator] {
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
@@ -165,20 +176,22 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
[data-timepicker-period] > button:active,
|
[data-timepicker-period] > button:active,
|
||||||
[data-timepicker-period] > button[aria-pressed="true"] {
|
[data-timepicker-period] > button[aria-checked="true"] {
|
||||||
border-radius: 0.75rem;
|
border-radius: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-timepicker-period] > button[aria-pressed="true"] {
|
/* Tertiary, not the primary the hour and minute boxes take: M3 gives the two selectors different
|
||||||
background-color: var(--md-sys-color-primary-container);
|
emphases (PeriodSelectorSelectedContainerColor = TertiaryContainer). */
|
||||||
color: var(--md-sys-color-on-primary-container);
|
[data-timepicker-period] > button[aria-checked="true"] {
|
||||||
|
background-color: var(--md-sys-color-tertiary-container);
|
||||||
|
color: var(--md-sys-color-on-tertiary-container);
|
||||||
font-weight: var(--md-ref-typeface-weight-bold);
|
font-weight: var(--md-ref-typeface-weight-bold);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-timepicker-period] > button:disabled {
|
[data-timepicker-period] > button:disabled {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
color: color-mix(in srgb, var(--md-sys-color-on-surface) 38%, transparent);
|
color: color-mix(in srgb, var(--md-sys-color-on-surface) 38%, transparent);
|
||||||
background-color: color-mix(in srgb, var(--md-sys-color-on-surface) 10%, transparent);
|
background-color: color-mix(in srgb, var(--md-sys-color-on-surface) 12%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---- The dial ----------------------------------------------------------------------------- */
|
/* ---- The dial ----------------------------------------------------------------------------- */
|
||||||
@@ -436,6 +449,10 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-timepicker-surface][data-mode="dial"] [data-timepicker-display][data-cycle="24"] [data-timepicker-box] {
|
||||||
|
width: 6rem;
|
||||||
|
}
|
||||||
|
|
||||||
[data-timepicker-surface][data-mode="dial"] [data-timepicker-display] [data-timepicker-period] {
|
[data-timepicker-surface][data-mode="dial"] [data-timepicker-display] [data-timepicker-period] {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
width: 13.5rem;
|
width: 13.5rem;
|
||||||
|
|||||||
@@ -380,6 +380,21 @@ document.addEventListener('alpine:init', () => {
|
|||||||
this.aim()
|
this.aim()
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AM and PM are a radio group, which M3's labelling table asks for: an arrow key moves to
|
||||||
|
* the other option and selects it, and focus follows, as WAI-ARIA's radio group does.
|
||||||
|
*/
|
||||||
|
movePeriod(event) {
|
||||||
|
const pm = !this.isPm
|
||||||
|
|
||||||
|
if (!this.periodAllowed(pm)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setPeriod(pm)
|
||||||
|
event.currentTarget.querySelector(`[data-timepicker-period-option="${pm ? 'pm' : 'am'}"]`)?.focus()
|
||||||
|
},
|
||||||
|
|
||||||
/** The allowed time in a period nearest to `time` (periodAllowed has said there is one). */
|
/** The allowed time in a period nearest to `time` (periodAllowed has said there is one). */
|
||||||
nearestInPeriod(pm, time) {
|
nearestInPeriod(pm, time) {
|
||||||
let best = null
|
let best = null
|
||||||
|
|||||||
@@ -31,14 +31,16 @@
|
|||||||
TimePickerTokens and TimeInputTokens — a surface-container-high dialog with an extra-large
|
TimePickerTokens and TimeInputTokens — a surface-container-high dialog with an extra-large
|
||||||
corner and elevation 3, a 256dp surface-container-highest dial with body-large numbers, a primary
|
corner and elevation 3, a 256dp surface-container-highest dial with body-large numbers, a primary
|
||||||
selector with a 48dp handle, a 2dp line and an 8dp centre, 96×80 time selector boxes in
|
selector with a 48dp handle, a 2dp line and an 8dp centre, 96×80 time selector boxes in
|
||||||
display-large (primary-container when selected), 96×72 time fields in display-medium — and
|
display-large (primary-container when selected; 114dp wide on a 24-hour clock, where there is no
|
||||||
TimePicker.kt and TimePickerDialog.kt for the layout, the 24-hour inner ring (12–23, at 69dp;
|
period selector beside them, per TimeSelector24HVerticalContainerWidth), 96×72 time fields in
|
||||||
00–11 outside at 101dp, as Material Components for Android labels them too), the gestures, the
|
display-medium — and TimePicker.kt and TimePickerDialog.kt for the layout, the 24-hour inner
|
||||||
move on to minutes and the error texts. The period selector is Compose's current default
|
ring (12–23, at 69dp; 00–11 outside at 101dp, as Material Components for Android labels them
|
||||||
(`isUpdatedTimepickerToggleEnabled`): two separate shape-morphing toggle buttons in
|
too), the gestures, the move on to minutes and the error texts. The period selector keeps
|
||||||
primary-container, not the outlined pair its tokens still describe. The dial's numbers are
|
Compose's current default shape morph (`isUpdatedTimepickerToggleEnabled`) but M3's own
|
||||||
aria-hidden; the dial is a `slider` whose value text names the hour or minute. The dialog is
|
tertiary-container, which is what holds hour/minute (primary-container) and AM/PM apart; it is a
|
||||||
`wire:ignore`, so a Livewire render never closes an open picker or resets its draft. --}}
|
radio group, as M3's labelling table says, so the arrows move between AM and PM. The dial's
|
||||||
|
numbers are aria-hidden; the dial is a `slider` whose value text names the hour or minute. The
|
||||||
|
dialog is `wire:ignore`, so a Livewire render never closes an open picker or resets its draft. --}}
|
||||||
|
|
||||||
@props([
|
@props([
|
||||||
'label' => null,
|
'label' => null,
|
||||||
@@ -211,7 +213,7 @@
|
|||||||
<h2 id="{{ $id }}-title" data-timepicker-title x-text="mode === 'dial' ? @js(__('Select time')) : @js(__('Enter time'))">{{ __('Select time') }}</h2>
|
<h2 id="{{ $id }}-title" data-timepicker-title x-text="mode === 'dial' ? @js(__('Select time')) : @js(__('Enter time'))">{{ __('Select time') }}</h2>
|
||||||
|
|
||||||
<div data-timepicker-picker>
|
<div data-timepicker-picker>
|
||||||
<div data-timepicker-display>
|
<div data-timepicker-display x-bind:data-cycle="is24 ? '24' : '12'">
|
||||||
<div data-timepicker-numbers>
|
<div data-timepicker-numbers>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -236,13 +238,25 @@
|
|||||||
></button>
|
></button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div data-timepicker-period role="group" aria-label="{{ __('Select AM or PM') }}" x-show="! is24">
|
<div
|
||||||
|
data-timepicker-period
|
||||||
|
role="radiogroup"
|
||||||
|
aria-label="{{ __('Select AM or PM') }}"
|
||||||
|
x-show="! is24"
|
||||||
|
x-on:keydown.arrow-right.prevent="movePeriod($event)"
|
||||||
|
x-on:keydown.arrow-left.prevent="movePeriod($event)"
|
||||||
|
x-on:keydown.arrow-down.prevent="movePeriod($event)"
|
||||||
|
x-on:keydown.arrow-up.prevent="movePeriod($event)"
|
||||||
|
>
|
||||||
@foreach ([false => 'am', true => 'pm'] as $pm => $period)
|
@foreach ([false => 'am', true => 'pm'] as $pm => $period)
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
data-timepicker-period-option="{{ $period }}"
|
data-timepicker-period-option="{{ $period }}"
|
||||||
class="state-layer focus-ring"
|
class="state-layer focus-ring"
|
||||||
x-bind:aria-pressed="({{ $pm ? '' : '! ' }}isPm).toString()"
|
role="radio"
|
||||||
|
aria-checked="false"
|
||||||
|
x-bind:aria-checked="({{ $pm ? '' : '! ' }}isPm).toString()"
|
||||||
|
x-bind:tabindex="({{ $pm ? '' : '! ' }}isPm) ? 0 : -1"
|
||||||
x-bind:disabled="! periodAllowed({{ $pm ? 'true' : 'false' }})"
|
x-bind:disabled="! periodAllowed({{ $pm ? 'true' : 'false' }})"
|
||||||
x-on:click="setPeriod({{ $pm ? 'true' : 'false' }})"
|
x-on:click="setPeriod({{ $pm ? 'true' : 'false' }})"
|
||||||
x-text="periods[{{ (int) $pm }}]"
|
x-text="periods[{{ (int) $pm }}]"
|
||||||
@@ -330,13 +344,25 @@
|
|||||||
</div>
|
</div>
|
||||||
@endforeach
|
@endforeach
|
||||||
|
|
||||||
<div data-timepicker-period role="group" aria-label="{{ __('Select AM or PM') }}" x-show="! is24">
|
<div
|
||||||
|
data-timepicker-period
|
||||||
|
role="radiogroup"
|
||||||
|
aria-label="{{ __('Select AM or PM') }}"
|
||||||
|
x-show="! is24"
|
||||||
|
x-on:keydown.arrow-right.prevent="movePeriod($event)"
|
||||||
|
x-on:keydown.arrow-left.prevent="movePeriod($event)"
|
||||||
|
x-on:keydown.arrow-down.prevent="movePeriod($event)"
|
||||||
|
x-on:keydown.arrow-up.prevent="movePeriod($event)"
|
||||||
|
>
|
||||||
@foreach ([false => 'am', true => 'pm'] as $pm => $period)
|
@foreach ([false => 'am', true => 'pm'] as $pm => $period)
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
data-timepicker-period-option="{{ $period }}"
|
data-timepicker-period-option="{{ $period }}"
|
||||||
class="state-layer focus-ring"
|
class="state-layer focus-ring"
|
||||||
x-bind:aria-pressed="({{ $pm ? '' : '! ' }}isPm).toString()"
|
role="radio"
|
||||||
|
aria-checked="false"
|
||||||
|
x-bind:aria-checked="({{ $pm ? '' : '! ' }}isPm).toString()"
|
||||||
|
x-bind:tabindex="({{ $pm ? '' : '! ' }}isPm) ? 0 : -1"
|
||||||
x-bind:disabled="! periodAllowed({{ $pm ? 'true' : 'false' }})"
|
x-bind:disabled="! periodAllowed({{ $pm ? 'true' : 'false' }})"
|
||||||
x-on:click="setPeriod({{ $pm ? 'true' : 'false' }})"
|
x-on:click="setPeriod({{ $pm ? 'true' : 'false' }})"
|
||||||
x-text="periods[{{ (int) $pm }}]"
|
x-text="periods[{{ (int) $pm }}]"
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ it('opens the dial on the hour, as the bound time says', function () {
|
|||||||
->assertAttribute('#meeting-dialog [data-timepicker-box="hour"]', 'aria-pressed', 'true')
|
->assertAttribute('#meeting-dialog [data-timepicker-box="hour"]', 'aria-pressed', 'true')
|
||||||
->assertSeeIn('#meeting-dialog [data-timepicker-box="hour"]', '09')
|
->assertSeeIn('#meeting-dialog [data-timepicker-box="hour"]', '09')
|
||||||
->assertSeeIn('#meeting-dialog [data-timepicker-box="minute"]', '30')
|
->assertSeeIn('#meeting-dialog [data-timepicker-box="minute"]', '30')
|
||||||
->assertAttribute('#meeting-dialog [data-timepicker-display] [data-timepicker-period-option="am"]', 'aria-pressed', 'true')
|
->assertAttribute('#meeting-dialog [data-timepicker-display] [data-timepicker-period-option="am"]', 'aria-checked', 'true')
|
||||||
->assertAttribute('#meeting-dialog [data-timepicker-dial]', 'aria-valuenow', '9')
|
->assertAttribute('#meeting-dialog [data-timepicker-dial]', 'aria-valuenow', '9')
|
||||||
->assertScript('document.activeElement === '.inPicker('meeting', '[data-timepicker-dial]'));
|
->assertScript('document.activeElement === '.inPicker('meeting', '[data-timepicker-dial]'));
|
||||||
});
|
});
|
||||||
@@ -131,7 +131,7 @@ it('turns three o\'clock into 15 with PM', function () {
|
|||||||
timeProbe()
|
timeProbe()
|
||||||
->click('#meeting')
|
->click('#meeting')
|
||||||
->click('#meeting-dialog [data-timepicker-display] [data-timepicker-period-option="pm"]')
|
->click('#meeting-dialog [data-timepicker-display] [data-timepicker-period-option="pm"]')
|
||||||
->assertAttribute('#meeting-dialog [data-timepicker-display] [data-timepicker-period-option="pm"]', 'aria-pressed', 'true')
|
->assertAttribute('#meeting-dialog [data-timepicker-display] [data-timepicker-period-option="pm"]', 'aria-checked', 'true')
|
||||||
->click('#meeting-dialog [data-timepicker-label="hour12"][data-value="3"]')
|
->click('#meeting-dialog [data-timepicker-label="hour12"][data-value="3"]')
|
||||||
->click('#meeting-dialog [data-timepicker-label="minute"][data-value="15"]')
|
->click('#meeting-dialog [data-timepicker-label="minute"][data-value="15"]')
|
||||||
->click('#meeting-dialog [data-timepicker-confirm]')
|
->click('#meeting-dialog [data-timepicker-confirm]')
|
||||||
|
|||||||
@@ -73,6 +73,12 @@ it('offers the input variant, the period selector and the actions', function ()
|
|||||||
->toContain('data-timepicker-period-option="am"')
|
->toContain('data-timepicker-period-option="am"')
|
||||||
->toContain('data-timepicker-period-option="pm"')
|
->toContain('data-timepicker-period-option="pm"')
|
||||||
->toContain('aria-label="Select AM or PM"')
|
->toContain('aria-label="Select AM or PM"')
|
||||||
|
// M3's labelling table: AM and PM are radios in a list, not toggle buttons.
|
||||||
|
->toContain('role="radiogroup"')
|
||||||
|
->toContain('role="radio"')
|
||||||
|
->toContain('x-on:keydown.arrow-right.prevent="movePeriod($event)"')
|
||||||
|
// The 24-hour selector widens into the room the period selector leaves.
|
||||||
|
->toContain('data-timepicker-display x-bind:data-cycle="is24 ? \'24\' : \'12\'"')
|
||||||
->toContain('aria-label="Switch to text input mode"')
|
->toContain('aria-label="Switch to text input mode"')
|
||||||
->toContain('aria-label="Switch to clock mode"')
|
->toContain('aria-label="Switch to clock mode"')
|
||||||
->toContain('data-timepicker-cancel')
|
->toContain('data-timepicker-cancel')
|
||||||
|
|||||||
Reference in New Issue
Block a user