diff --git a/resources/css/components.css b/resources/css/components.css index d6309bee..41f99dd7 100644 --- a/resources/css/components.css +++ b/resources/css/components.css @@ -53,6 +53,7 @@ @import './components/sort-header.css'; @import './components/pagination.css'; @import './components/datepicker.css'; +@import './components/timepicker.css'; /* Containment */ diff --git a/resources/css/components/field.css b/resources/css/components/field.css index 2de826a3..374c1991 100644 --- a/resources/css/components/field.css +++ b/resources/css/components/field.css @@ -165,11 +165,12 @@ pointer-events: none; } - /* A trailing button — clear, copy, reveal — is an icon button: the icon in a state layer that - scales with the field's own icon size (40px at `md`, smaller at `sm`/`xs`), which the - foundation's fixed-size classes cannot draw; the layer doubles as the touch target, since it - already reaches 40px+ without a second pseudo. The hook stays `data-md-field-button` rather - than a class because datepicker and timepicker, not yet rewritten, still select it. */ + /* A trailing button — clear, copy, reveal, the datepicker's calendar and the timepicker's clock — + is an icon button: the icon in a state layer that scales with the field's own icon size (40px + at `md`, smaller at `sm`/`xs`), which the foundation's fixed-size classes cannot draw; the + layer doubles as the touch target, since it already reaches 40px+ without a second pseudo. The + hook stays `data-md-field-button` rather than a class for that reason alone now: every other + component that draws one — input, password, datepicker, timepicker — selects it from here. */ [data-md-field-button] { position: relative; display: grid; diff --git a/resources/css/components/timepicker.css b/resources/css/components/timepicker.css index f3c67292..c74b6676 100644 --- a/resources/css/components/timepicker.css +++ b/resources/css/components/timepicker.css @@ -5,22 +5,22 @@ * Sizes and roles are androidx Compose Material 3's (TimePickerTokens, TimeInputTokens, TimePicker.kt * and TimePickerDialog.kt at commit 27cf9a7d5788aa0f5f2d8b6699ce279560daf326, Apache-2.0): * - * [data-timepicker-surface] surface-container-high, extra-large corner, elevation 3; 24px in - * [data-timepicker-title] label-medium, on-surface-variant, 20px above the content - * [data-timepicker-picker] the dial variant - * [data-timepicker-display] the time selector: hour and minute boxes 96×80 (display-large, + * [data-md-timepicker-surface] surface-container-high, extra-large corner, elevation 3; 24px in + * [data-md-timepicker-title] label-medium, on-surface-variant, 20px above the content + * [data-md-timepicker-picker] the dial variant + * [data-md-timepicker-display] the time selector: hour and minute boxes 96×80 (display-large, * small corner; primary-container when selected, otherwise * surface-container-highest), a 24px separator, and the period * selector 52×80 beside them, 4px away. On a 24-hour clock - * (data-cycle="24") there is no period selector and the boxes are + * (data-md-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-md-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, * an 8px centre and a 48px handle — with the number under the handle * in on-primary; 36px below the display, 24px above the actions - * [data-timepicker-inputs] the input variant: fields 96×72 in display-medium, "Hour" and + * [data-md-timepicker-inputs] the input variant: fields 96×72 in display-medium, "Hour" and * "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-md-timepicker-actions] a 48px row: the mode toggle, then Cancel and OK * * The period selector keeps Compose's current shape (ComposeMaterial3Flags.isUpdatedTimepickerToggleEnabled, * on by default): two buttons 4px apart, round and surface-container-lowest when off, a 12px corner @@ -36,34 +36,71 @@ * The switch is orientation and viewport *height*, never a width breakpoint: M3 says the picker * "swaps orientation/variant based on device orientation and viewport height" * (docs/reference/m3/components-navigation-selection-inputs.md § Time pickers/Behaviour), and no - * window size class applies. 35rem is what the upright dial needs — 33rem of surface (3 padding, - * 2.25 title, 7.25 display, 17.5 dial, 3 actions) inside the dialog's `100dvh - 2rem` — so a - * landscape phone lies the dial down and a landscape tablet or desktop, which has the height, - * leaves it standing. + * window size class applies — so the two landscape media queries below are the one place in this + * file a length is not one of M3's four breakpoints. 560px is what the upright dial needs — 528px of + * surface (3 padding, 2.25 title, 7.25 display, 17.5 dial, 3 actions in rem, ×16) inside the + * dialog's `100dvh - 32px` — so a landscape phone lies the dial down and a landscape tablet or + * desktop, which has the height, leaves it standing. + * + * The hour and minute boxes and the period buttons render the foundation's `md-state-layer` and + * `md-focus-ring` (foundation/interaction.css) — each is its own interactive box with nothing drawn + * smaller inside it, so the classes need no refinement. The dial renders `md-focus-ring` alone (it + * draws no state layer of its own); nothing here copies its outline rule. Cancel, OK and the mode + * toggle are , which draws from the classes already. No element here reaches a 48px target + * of its own accord, so `md-touch-target` is never rendered. * * The selector's angle is a registered property, so a single transition turns the line, carries the * handle round and moves the clip that inks the number under it, on the default spatial spring. The * numbers cross-fade between hours and minutes on the default effects spring. */ -@property --timepicker-angle { - syntax: ''; - inherits: true; - initial-value: 0deg; -} +@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility; -@layer components { - [data-timepicker-field] [data-md-field-control] { +@import './field.css'; +@import './icon.css'; +@import './button.css'; + +@layer material.components { + @property --timepicker-angle { + syntax: ''; + inherits: true; + initial-value: 0deg; + } + + [data-md-timepicker-field] [data-md-field-control] { cursor: pointer; caret-color: transparent; } - [data-timepicker-dialog] { + /* The dialog: M3's spatial-fast pop, a 32%-scrim backdrop, no default border or padding. */ + [data-md-timepicker-dialog] { max-width: calc(100vw - 2rem); max-height: calc(100dvh - 2rem); + margin: auto; + padding: 0; + overflow: visible; + border: 0; + background: transparent; + color: var(--md-sys-color-on-surface); + opacity: 1; + scale: 1; + transition-property: opacity, scale; + transition-duration: var(--md-sys-motion-spatial-fast-duration); + transition-timing-function: var(--md-sys-motion-spatial-fast); } - [data-timepicker-surface] { + @starting-style { + [data-md-timepicker-dialog] { + opacity: 0; + scale: 0.95; + } + } + + [data-md-timepicker-dialog]::backdrop { + background-color: color-mix(in srgb, var(--md-sys-color-scrim) 32%, transparent); + } + + [data-md-timepicker-surface] { display: grid; grid-template-columns: max-content; justify-content: center; @@ -74,14 +111,14 @@ color: var(--md-sys-color-on-surface); } - [data-timepicker-title] { + [data-md-timepicker-title] { padding-bottom: 1.25rem; color: var(--md-sys-color-on-surface-variant); font: var(--md-sys-typescale-label-md); letter-spacing: var(--md-sys-typescale-label-md-tracking); } - [data-timepicker-picker] { + [data-md-timepicker-picker] { display: flex; flex-direction: column; align-items: center; @@ -89,30 +126,30 @@ /* The mode is an attribute rather than x-show, which shows only on the next frame: focus moves into the variant that was just switched to within the same tick. */ - [data-timepicker-surface]:not([data-mode="input"]) :is([data-timepicker-typing], [data-timepicker-when="input"]), - [data-timepicker-surface][data-mode="input"] :is([data-timepicker-picker], [data-timepicker-when="dial"]) { + [data-md-timepicker-surface]:not([data-md-mode='input']) :is([data-md-timepicker-typing], [data-md-timepicker-when='input']), + [data-md-timepicker-surface][data-md-mode='input'] :is([data-md-timepicker-picker], [data-md-timepicker-when='dial']) { display: none; } - [data-timepicker-when] { + [data-md-timepicker-when] { display: contents; } /* ---- The time selector and the period selector ------------------------------------------ */ - [data-timepicker-display] { + [data-md-timepicker-display] { display: flex; margin-bottom: 2.25rem; } - [data-timepicker-numbers], - [data-timepicker-inputs] { + [data-md-timepicker-numbers], + [data-md-timepicker-inputs] { display: flex; align-items: flex-start; direction: ltr; } - [data-timepicker-box] { + [data-md-timepicker-box] { display: grid; place-items: center; width: 6rem; @@ -129,7 +166,7 @@ transition-timing-function: var(--md-sys-motion-effects-fast); } - [data-timepicker-box][aria-pressed="true"] { + [data-md-timepicker-box][aria-pressed='true'] { background-color: var(--md-sys-color-primary-container); color: var(--md-sys-color-on-primary-container); } @@ -137,11 +174,11 @@ /* 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] { + [data-md-timepicker-display][data-md-cycle='24'] [data-md-timepicker-box] { width: 7.125rem; } - [data-timepicker-separator] { + [data-md-timepicker-separator] { display: grid; place-items: center; width: 1.5rem; @@ -152,7 +189,7 @@ user-select: none; } - [data-timepicker-period] { + [data-md-timepicker-period] { display: flex; flex-direction: column; gap: 0.25rem; @@ -161,7 +198,7 @@ margin-inline-start: 0.25rem; } - [data-timepicker-period] > button { + [data-md-timepicker-period] > button { flex: 1 1 0%; min-width: 0; border-radius: var(--md-sys-shape-corner-full); @@ -175,20 +212,20 @@ transition-timing-function: var(--md-sys-motion-spatial-fast); } - [data-timepicker-period] > button:active, - [data-timepicker-period] > button[aria-checked="true"] { + [data-md-timepicker-period] > button:active, + [data-md-timepicker-period] > button[aria-checked='true'] { border-radius: 0.75rem; } /* Tertiary, not the primary the hour and minute boxes take: M3 gives the two selectors different emphases (PeriodSelectorSelectedContainerColor = TertiaryContainer). */ - [data-timepicker-period] > button[aria-checked="true"] { + [data-md-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); } - [data-timepicker-period] > button:disabled { + [data-md-timepicker-period] > button:disabled { cursor: default; 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) 12%, transparent); @@ -196,7 +233,7 @@ /* ---- The dial ----------------------------------------------------------------------------- */ - [data-timepicker-dial] { + [data-md-timepicker-dial] { --dial: 16rem; --unit: calc(var(--dial) / 256); --outer: calc(101 * var(--unit)); @@ -219,50 +256,44 @@ user-select: none; -webkit-user-select: none; -webkit-tap-highlight-color: transparent; - outline: none; direction: ltr; transition: --timepicker-angle var(--md-sys-motion-spatial-default-duration) var(--md-sys-motion-spatial-default); } - [data-timepicker-dial]:focus-visible { - outline: 3px solid var(--md-sys-color-secondary); - outline-offset: 2px; - } - /* A drag keeps the handle under the pointer; the spring only settles it on release. */ - [data-timepicker-dial][data-dragging] { + [data-md-timepicker-dial][data-md-dragging] { transition: none; } - [data-timepicker-dial][data-inner] { + [data-md-timepicker-dial][data-md-inner] { --reach: var(--inner); } - [data-timepicker-labels], - [data-timepicker-ink], - [data-timepicker-set] { + [data-md-timepicker-labels], + [data-md-timepicker-ink], + [data-md-timepicker-set] { position: absolute; inset: 0; } - [data-timepicker-set] { + [data-md-timepicker-set] { transition-property: opacity, visibility; transition-duration: var(--md-sys-motion-effects-default-duration); transition-timing-function: var(--md-sys-motion-effects-default); } - [data-timepicker-dial]:not([data-cycle="24"]) [data-timepicker-set="hour24"], - [data-timepicker-dial][data-cycle="24"] [data-timepicker-set="hour12"] { + [data-md-timepicker-dial]:not([data-md-cycle='24']) [data-md-timepicker-set='hour24'], + [data-md-timepicker-dial][data-md-cycle='24'] [data-md-timepicker-set='hour12'] { display: none; } - [data-timepicker-dial][data-view="minute"] :is([data-timepicker-set="hour12"], [data-timepicker-set="hour24"]), - [data-timepicker-dial]:not([data-view="minute"]) [data-timepicker-set="minute"] { + [data-md-timepicker-dial][data-md-view='minute'] :is([data-md-timepicker-set='hour12'], [data-md-timepicker-set='hour24']), + [data-md-timepicker-dial]:not([data-md-view='minute']) [data-md-timepicker-set='minute'] { opacity: 0; visibility: hidden; } - [data-timepicker-set] > span { + [data-md-timepicker-set] > span { --ring: var(--outer); position: absolute; @@ -277,22 +308,22 @@ font-variant-numeric: tabular-nums; } - [data-timepicker-set] > span[data-inner] { + [data-md-timepicker-set] > span[data-md-inner] { --ring: var(--inner); } - [data-timepicker-set] > span[data-disabled] { + [data-md-timepicker-set] > span[data-md-disabled] { color: color-mix(in srgb, var(--md-sys-color-on-surface) 38%, transparent); } /* The selector: the line from the centre to the handle's edge, the centre dot, the handle. */ - [data-timepicker-selector] { + [data-md-timepicker-selector] { position: absolute; inset: 0; pointer-events: none; } - [data-timepicker-track] { + [data-md-timepicker-track] { position: absolute; left: calc(50% - 1px); bottom: 50%; @@ -303,22 +334,22 @@ rotate: var(--timepicker-angle); } - [data-timepicker-centre], - [data-timepicker-handle] { + [data-md-timepicker-centre], + [data-md-timepicker-handle] { position: absolute; translate: -50% -50%; border-radius: var(--md-sys-shape-corner-full); background-color: var(--md-sys-color-primary); } - [data-timepicker-centre] { + [data-md-timepicker-centre] { left: 50%; top: 50%; width: 0.5rem; height: 0.5rem; } - [data-timepicker-handle] { + [data-md-timepicker-handle] { left: calc(50% + sin(var(--timepicker-angle)) * var(--reach)); top: calc(50% - cos(var(--timepicker-angle)) * var(--reach)); width: var(--handle); @@ -326,31 +357,31 @@ } /* The numbers again, in on-primary, cut to the handle: what Compose draws with BlendMode.DstOver. */ - [data-timepicker-ink] { + [data-md-timepicker-ink] { color: var(--md-sys-color-on-primary); pointer-events: none; clip-path: circle(calc(var(--handle) / 2) at calc(50% + sin(var(--timepicker-angle)) * var(--reach)) calc(50% - cos(var(--timepicker-angle)) * var(--reach))); } - [data-timepicker-ink] [data-timepicker-set] > span[data-disabled] { + [data-md-timepicker-ink] [data-md-timepicker-set] > span[data-md-disabled] { color: inherit; } /* ---- The input variant -------------------------------------------------------------------- */ - [data-timepicker-inputs] [data-timepicker-separator] { + [data-md-timepicker-inputs] [data-md-timepicker-separator] { height: 4.5rem; } - [data-timepicker-inputs] [data-timepicker-period] { + [data-md-timepicker-inputs] [data-md-timepicker-period] { height: 4.5rem; } - [data-timepicker-column] { + [data-md-timepicker-column] { width: 6rem; } - [data-timepicker-input] { + [data-md-timepicker-input] { display: block; width: 6rem; height: 4.5rem; @@ -370,25 +401,25 @@ transition-timing-function: var(--md-sys-motion-effects-fast); } - [data-timepicker-input]:focus { + [data-md-timepicker-input]:focus { background-color: var(--md-sys-color-primary-container); color: var(--md-sys-color-on-primary-container); box-shadow: inset 0 0 0 2px var(--md-sys-color-primary); } - [data-timepicker-input][aria-invalid="true"] { + [data-md-timepicker-input][aria-invalid='true'] { background-color: var(--md-sys-color-error-container); color: var(--md-sys-color-error); caret-color: var(--md-sys-color-error); box-shadow: inset 0 0 0 1px var(--md-sys-color-error); } - [data-timepicker-input][aria-invalid="true"]:focus { + [data-md-timepicker-input][aria-invalid='true']:focus { box-shadow: inset 0 0 0 2px var(--md-sys-color-error); } /* SupportingText: two lines' room, 7px under the field; the error in its place. */ - [data-timepicker-support] { + [data-md-timepicker-support] { min-height: 2rem; padding-top: 0.4375rem; color: var(--md-sys-color-on-surface-variant); @@ -396,89 +427,89 @@ letter-spacing: var(--md-sys-typescale-body-sm-tracking); } - [data-timepicker-support][data-error], - [data-timepicker-range-error] { + [data-md-timepicker-support][data-md-error], + [data-md-timepicker-range-error] { color: var(--md-sys-color-error); } - [data-timepicker-range-error] { + [data-md-timepicker-range-error] { max-width: 17rem; padding-bottom: 0.5rem; font: var(--md-sys-typescale-body-sm); letter-spacing: var(--md-sys-typescale-body-sm-tracking); } - [data-timepicker-actions] { + [data-md-timepicker-actions] { display: flex; align-items: center; gap: 0.5rem; min-height: 3rem; } - [data-timepicker-actions] > [data-timepicker-spacer] { + [data-md-timepicker-actions] > [data-md-timepicker-spacer] { flex: 1 1 0%; } /* ---- Landscape ---------------------------------------------------------------------------- */ - @media (orientation: landscape) and (height < 35rem) { - [data-timepicker-surface][data-mode="dial"] { + @media (orientation: landscape) and (height < 560px) { + [data-md-timepicker-surface][data-md-mode='dial'] { grid-template-columns: 13.5rem auto; grid-template-areas: - "display dial" - "actions actions"; + 'display dial' + 'actions actions'; column-gap: 2.25rem; padding: 1rem 1.5rem 0.5rem; } - [data-timepicker-surface][data-mode="dial"] [data-timepicker-title] { + [data-md-timepicker-surface][data-md-mode='dial'] [data-md-timepicker-title] { grid-area: display; align-self: start; margin-top: 0.5rem; padding: 0; } - [data-timepicker-surface][data-mode="dial"] [data-timepicker-picker] { + [data-md-timepicker-surface][data-md-mode='dial'] [data-md-timepicker-picker] { display: contents; } - [data-timepicker-surface][data-mode="dial"] [data-timepicker-display] { + [data-md-timepicker-surface][data-md-mode='dial'] [data-md-timepicker-display] { grid-area: display; flex-direction: column; align-self: center; margin: 0; } - [data-timepicker-surface][data-mode="dial"] [data-timepicker-display][data-cycle="24"] [data-timepicker-box] { + [data-md-timepicker-surface][data-md-mode='dial'] [data-md-timepicker-display][data-md-cycle='24'] [data-md-timepicker-box] { width: 6rem; } - [data-timepicker-surface][data-mode="dial"] [data-timepicker-display] [data-timepicker-period] { + [data-md-timepicker-surface][data-md-mode='dial'] [data-md-timepicker-display] [data-md-timepicker-period] { flex-direction: row; width: 13.5rem; height: 2.375rem; margin: 1rem 0 0; } - [data-timepicker-surface][data-mode="dial"] [data-timepicker-dial] { + [data-md-timepicker-surface][data-md-mode='dial'] [data-md-timepicker-dial] { grid-area: dial; margin: 0; } - [data-timepicker-surface][data-mode="dial"] [data-timepicker-actions] { + [data-md-timepicker-surface][data-md-mode='dial'] [data-md-timepicker-actions] { grid-area: actions; margin-top: 0.25rem; } } - @media (orientation: landscape) and (max-height: 22.75rem) { - [data-timepicker-surface][data-mode="dial"] [data-timepicker-dial] { + @media (orientation: landscape) and (height <= 364px) { + [data-md-timepicker-surface][data-md-mode='dial'] [data-md-timepicker-dial] { --dial: 14.875rem; } } - @media (orientation: landscape) and (max-height: 21.625rem) { - [data-timepicker-surface][data-mode="dial"] [data-timepicker-dial] { + @media (orientation: landscape) and (height <= 346px) { + [data-md-timepicker-surface][data-md-mode='dial'] [data-md-timepicker-dial] { --dial: 12.5rem; } } diff --git a/resources/css/tailwind.css b/resources/css/tailwind.css index a8755fa0..d030938a 100644 --- a/resources/css/tailwind.css +++ b/resources/css/tailwind.css @@ -17,9 +17,6 @@ @import './tokens/theme.css'; @import './tokens/utilities.css'; -/* Inputs, selection and data — leaving in step 36 */ -@import './components/timepicker.css'; - /* Containment — leaving in step 36 */ @import './components/list.css'; @import './components/collapse.css'; diff --git a/resources/js/timepicker.js b/resources/js/timepicker.js index 85ff7558..524ca8e8 100644 --- a/resources/js/timepicker.js +++ b/resources/js/timepicker.js @@ -392,7 +392,7 @@ document.addEventListener('alpine:init', () => { } this.setPeriod(pm) - event.currentTarget.querySelector(`[data-timepicker-period-option="${pm ? 'pm' : 'am'}"]`)?.focus() + event.currentTarget.querySelector(`[data-md-timepicker-period-option="${pm ? 'pm' : 'am'}"]`)?.focus() }, /** The allowed time in a period nearest to `time` (periodAllowed has said there is one). */ diff --git a/resources/views/components/timepicker.blade.php b/resources/views/components/timepicker.blade.php index 0098650e..5bc7441c 100644 --- a/resources/views/components/timepicker.blade.php +++ b/resources/views/components/timepicker.blade.php @@ -143,7 +143,7 @@ x-data="materialTimepicker(@if ($model !== null) @entangle($attributes->wire('model')) @else @js($current) @endif, @js($config))" @if ($model === null) x-modelable="value" @endif > - + @@ -206,33 +206,32 @@ wire:ignore x-ref="dialog" aria-labelledby="{{ $id }}-title" - data-timepicker-dialog + data-md-timepicker-dialog x-on:close="closed()" x-on:pointerdown="scrimPressed = $event.target === $el" x-on:click.self="if (scrimPressed) cancel()" - class="m-auto overflow-visible bg-transparent p-0 text-on-surface backdrop:bg-scrim/32 opacity-100 scale-100 starting:opacity-0 starting:scale-95 transition-[opacity,scale] duration-(--md-sys-motion-spatial-fast-duration) ease-spatial-fast" > -
-

{{ __('Select time') }}

+
+

{{ __('Select time') }}

-
-
-
+
+
+
- +
-

+

-
- - +
+ + - - + + - - - + + +
diff --git a/tests/Browser/TimepickerTest.php b/tests/Browser/TimepickerTest.php index 72461185..56f6dfa4 100644 --- a/tests/Browser/TimepickerTest.php +++ b/tests/Browser/TimepickerTest.php @@ -79,37 +79,37 @@ it('opens the dial on the hour, as the bound time says', function () { ->assertScript("document.querySelector('#meeting-dialog').open") ->assertAttribute('#meeting', 'aria-expanded', 'true') ->assertSeeIn('#meeting-title', 'Select time') - ->assertAttribute('#meeting-dialog [data-timepicker-box="hour"]', 'aria-pressed', 'true') - ->assertSeeIn('#meeting-dialog [data-timepicker-box="hour"]', '09') - ->assertSeeIn('#meeting-dialog [data-timepicker-box="minute"]', '30') - ->assertAttribute('#meeting-dialog [data-timepicker-display] [data-timepicker-period-option="am"]', 'aria-checked', 'true') - ->assertAttribute('#meeting-dialog [data-timepicker-dial]', 'aria-valuenow', '9') - ->assertScript('document.activeElement === '.inPicker('meeting', '[data-timepicker-dial]')); + ->assertAttribute('#meeting-dialog [data-md-timepicker-box="hour"]', 'aria-pressed', 'true') + ->assertSeeIn('#meeting-dialog [data-md-timepicker-box="hour"]', '09') + ->assertSeeIn('#meeting-dialog [data-md-timepicker-box="minute"]', '30') + ->assertAttribute('#meeting-dialog [data-md-timepicker-display] [data-md-timepicker-period-option="am"]', 'aria-checked', 'true') + ->assertAttribute('#meeting-dialog [data-md-timepicker-dial]', 'aria-valuenow', '9') + ->assertScript('document.activeElement === '.inPicker('meeting', '[data-md-timepicker-dial]')); }); it('writes the hour and the minute pressed on the dial to Livewire on OK', function () { - $dial = inPicker('meeting', '[data-timepicker-dial]'); + $dial = inPicker('meeting', '[data-md-timepicker-dial]'); $page = timeProbe() ->click('#meeting') - ->click('#meeting-dialog [data-timepicker-label="hour12"][data-value="3"]') - ->assertSeeIn('#meeting-dialog [data-timepicker-box="hour"]', '03') - ->assertAttribute('#meeting-dialog [data-timepicker-dial]', 'data-view', 'minute') - ->assertAttribute('#meeting-dialog [data-timepicker-box="minute"]', 'aria-pressed', 'true') + ->click('#meeting-dialog [data-md-timepicker-label="hour12"][data-md-value="3"]') + ->assertSeeIn('#meeting-dialog [data-md-timepicker-box="hour"]', '03') + ->assertAttribute('#meeting-dialog [data-md-timepicker-dial]', 'data-md-view', 'minute') + ->assertAttribute('#meeting-dialog [data-md-timepicker-box="minute"]', 'aria-pressed', 'true') // The selector turns to the value it chose: the handle ends over the number. ->assertScript(<< { - const handle = {$dial}.querySelector('[data-timepicker-handle]').getBoundingClientRect(); - const label = {$dial}.querySelector('[data-timepicker-label="minute"][data-value="30"]').getBoundingClientRect(); + const handle = {$dial}.querySelector('[data-md-timepicker-handle]').getBoundingClientRect(); + const label = {$dial}.querySelector('[data-md-timepicker-label="minute"][data-md-value="30"]').getBoundingClientRect(); return Math.abs(handle.x - label.x) < 1 && Math.abs(handle.y - label.y) < 1; })() JS); - $page->click('#meeting-dialog [data-timepicker-label="minute"][data-value="45"]') - ->assertSeeIn('#meeting-dialog [data-timepicker-box="minute"]', '45') + $page->click('#meeting-dialog [data-md-timepicker-label="minute"][data-md-value="45"]') + ->assertSeeIn('#meeting-dialog [data-md-timepicker-box="minute"]', '45') ->assertSeeIn('#meeting-value', '09:30') - ->click('#meeting-dialog [data-timepicker-confirm]') + ->click('#meeting-dialog [data-md-timepicker-confirm]') ->assertSeeIn('#meeting-value', '03:45') ->assertValue('#meeting', '3:45 AM') ->assertScript("! document.querySelector('#meeting-dialog').open") @@ -119,9 +119,9 @@ it('writes the hour and the minute pressed on the dial to Livewire on OK', funct it('leaves the time alone when the picker is cancelled', function () { timeProbe() ->click('#meeting') - ->click('#meeting-dialog [data-timepicker-label="hour12"][data-value="7"]') - ->assertSeeIn('#meeting-dialog [data-timepicker-box="hour"]', '07') - ->click('#meeting-dialog [data-timepicker-cancel]') + ->click('#meeting-dialog [data-md-timepicker-label="hour12"][data-md-value="7"]') + ->assertSeeIn('#meeting-dialog [data-md-timepicker-box="hour"]', '07') + ->click('#meeting-dialog [data-md-timepicker-cancel]') ->assertScript("! document.querySelector('#meeting-dialog').open") ->assertSeeIn('#meeting-value', '09:30') ->assertValue('#meeting', '9:30 AM'); @@ -130,21 +130,21 @@ it('leaves the time alone when the picker is cancelled', function () { it('turns three o\'clock into 15 with PM', function () { timeProbe() ->click('#meeting') - ->click('#meeting-dialog [data-timepicker-display] [data-timepicker-period-option="pm"]') - ->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="minute"][data-value="15"]') - ->click('#meeting-dialog [data-timepicker-confirm]') + ->click('#meeting-dialog [data-md-timepicker-display] [data-md-timepicker-period-option="pm"]') + ->assertAttribute('#meeting-dialog [data-md-timepicker-display] [data-md-timepicker-period-option="pm"]', 'aria-checked', 'true') + ->click('#meeting-dialog [data-md-timepicker-label="hour12"][data-md-value="3"]') + ->click('#meeting-dialog [data-md-timepicker-label="minute"][data-md-value="15"]') + ->click('#meeting-dialog [data-md-timepicker-confirm]') ->assertSeeIn('#meeting-value', '15:15') ->assertValue('#meeting', '3:15 PM'); }); it('draws a German dial with 24 hours, 12 to 23 on the inner ring', function () { - $dial = inPicker('alarm', '[data-timepicker-dial]'); + $dial = inPicker('alarm', '[data-md-timepicker-dial]'); $radius = fn (string $value): string => << { const dial = {$dial}.getBoundingClientRect(); - const label = {$dial}.querySelector('[data-timepicker-label="hour24"][data-value="{$value}"]').getBoundingClientRect(); + const label = {$dial}.querySelector('[data-md-timepicker-label="hour24"][data-md-value="{$value}"]').getBoundingClientRect(); return Math.round(Math.hypot(label.x + label.width / 2 - dial.x - dial.width / 2, label.y + label.height / 2 - dial.y - dial.height / 2)); })() @@ -152,41 +152,41 @@ it('draws a German dial with 24 hours, 12 to 23 on the inner ring', function () $page = timeProbe() ->click('#alarm') - ->assertAttribute('#alarm-dialog [data-timepicker-dial]', 'data-cycle', '24') - ->assertScript("getComputedStyle(document.querySelector('#alarm-dialog [data-timepicker-period]')).display === 'none'") - ->assertScript("{$dial}.querySelector('[data-timepicker-label=\"hour24\"][data-value=\"0\"]').textContent.trim() === '00'") + ->assertAttribute('#alarm-dialog [data-md-timepicker-dial]', 'data-md-cycle', '24') + ->assertScript("getComputedStyle(document.querySelector('#alarm-dialog [data-md-timepicker-period]')).display === 'none'") + ->assertScript("{$dial}.querySelector('[data-md-timepicker-label=\"hour24\"][data-md-value=\"0\"]').textContent.trim() === '00'") ->assertScript($radius('0').' === 101') ->assertScript($radius('11').' === 101') ->assertScript($radius('12').' === 69') ->assertScript($radius('13').' === 69') ->assertScript($radius('23').' === 69'); - $page->click('#alarm-dialog [data-timepicker-label="hour24"][data-value="15"]') - ->assertSeeIn('#alarm-dialog [data-timepicker-box="hour"]', '15') - ->assertAttribute('#alarm-dialog [data-timepicker-dial]', 'data-view', 'minute') - ->click('#alarm-dialog [data-timepicker-label="minute"][data-value="0"]') - ->click('#alarm-dialog [data-timepicker-confirm]') + $page->click('#alarm-dialog [data-md-timepicker-label="hour24"][data-md-value="15"]') + ->assertSeeIn('#alarm-dialog [data-md-timepicker-box="hour"]', '15') + ->assertAttribute('#alarm-dialog [data-md-timepicker-dial]', 'data-md-view', 'minute') + ->click('#alarm-dialog [data-md-timepicker-label="minute"][data-md-value="0"]') + ->click('#alarm-dialog [data-md-timepicker-confirm]') ->assertSeeIn('#alarm-value', '15:00') ->assertValue('#alarm', '15:00'); // The outer ring is the morning. $page->click('#alarm') - ->click('#alarm-dialog [data-timepicker-label="hour24"][data-value="3"]') - ->assertSeeIn('#alarm-dialog [data-timepicker-box="hour"]', '03') - ->click('#alarm-dialog [data-timepicker-label="minute"][data-value="0"]') - ->click('#alarm-dialog [data-timepicker-confirm]') + ->click('#alarm-dialog [data-md-timepicker-label="hour24"][data-md-value="3"]') + ->assertSeeIn('#alarm-dialog [data-md-timepicker-box="hour"]', '03') + ->click('#alarm-dialog [data-md-timepicker-label="minute"][data-md-value="0"]') + ->click('#alarm-dialog [data-md-timepicker-confirm]') ->assertSeeIn('#alarm-value', '03:00'); }); it('follows a drag round the dial and settles on the nearest hour', function () { - $dial = inPicker('meeting', '[data-timepicker-dial]'); + $dial = inPicker('meeting', '[data-md-timepicker-dial]'); $page = timeProbe()->click('#meeting'); // Pointer events built in the page's own realm, from twelve o'clock round to just past four. $send = fn (string $type, int $degrees): string => << { - const dial = document.querySelector('#meeting-dialog [data-timepicker-dial]'); + const dial = document.querySelector('#meeting-dialog [data-md-timepicker-dial]'); const box = dial.getBoundingClientRect(); const radians = {$degrees} * Math.PI / 180; @@ -201,14 +201,14 @@ it('follows a drag round the dial and settles on the nearest hour', function () $page->script($send('pointerdown', 0).';'.$send('pointermove', 20).';'.$send('pointermove', 60).';'.$send('pointermove', 125)); // While dragging, the handle stays under the pointer rather than on a number. - $page->assertScript("{$dial}.hasAttribute('data-dragging') && parseFloat(getComputedStyle({$dial}).getPropertyValue('--timepicker-angle')) % 360 === 125") - ->assertSeeIn('#meeting-dialog [data-timepicker-box="hour"]', '04'); + $page->assertScript("{$dial}.hasAttribute('data-md-dragging') && parseFloat(getComputedStyle({$dial}).getPropertyValue('--timepicker-angle')) % 360 === 125") + ->assertSeeIn('#meeting-dialog [data-md-timepicker-box="hour"]', '04'); $page->script($send('pointerup', 125)); - $page->assertScript("! {$dial}.hasAttribute('data-dragging')") - ->assertAttribute('#meeting-dialog [data-timepicker-dial]', 'data-view', 'minute') - ->assertSeeIn('#meeting-dialog [data-timepicker-box="hour"]', '04'); + $page->assertScript("! {$dial}.hasAttribute('data-md-dragging')") + ->assertAttribute('#meeting-dialog [data-md-timepicker-dial]', 'data-md-view', 'minute') + ->assertSeeIn('#meeting-dialog [data-md-timepicker-box="hour"]', '04'); }); it('changes the focused hour and minute with the arrow keys and confirms with Enter', function () { @@ -217,23 +217,23 @@ it('changes the focused hour and minute with the arrow keys and confirms with En $page->script("document.querySelector('#meeting').focus()"); $page->keys('#meeting', 'Enter') - ->assertScript('document.activeElement === '.inPicker('meeting', '[data-timepicker-dial]')) + ->assertScript('document.activeElement === '.inPicker('meeting', '[data-md-timepicker-dial]')) ->keys(':focus', 'ArrowUp') - ->assertAttribute('#meeting-dialog [data-timepicker-dial]', 'aria-valuenow', '10') - ->assertSeeIn('#meeting-dialog [data-timepicker-box="hour"]', '10') + ->assertAttribute('#meeting-dialog [data-md-timepicker-dial]', 'aria-valuenow', '10') + ->assertSeeIn('#meeting-dialog [data-md-timepicker-box="hour"]', '10') // The keyboard never moves on to the minutes by itself. - ->assertAttribute('#meeting-dialog [data-timepicker-dial]', 'data-view', 'hour'); + ->assertAttribute('#meeting-dialog [data-md-timepicker-dial]', 'data-md-view', 'hour'); - $page->script(inPicker('meeting', '[data-timepicker-box="minute"]').'.focus()'); + $page->script(inPicker('meeting', '[data-md-timepicker-box="minute"]').'.focus()'); $page->keys(':focus', 'Enter') - ->assertAttribute('#meeting-dialog [data-timepicker-dial]', 'data-view', 'minute'); + ->assertAttribute('#meeting-dialog [data-md-timepicker-dial]', 'data-md-view', 'minute'); - $page->script(inPicker('meeting', '[data-timepicker-dial]').'.focus()'); + $page->script(inPicker('meeting', '[data-md-timepicker-dial]').'.focus()'); $page->keys(':focus', ['ArrowDown', 'ArrowLeft']) - ->assertAttribute('#meeting-dialog [data-timepicker-dial]', 'aria-valuenow', '28') - ->assertAttribute('#meeting-dialog [data-timepicker-dial]', 'aria-valuetext', '28 minutes') + ->assertAttribute('#meeting-dialog [data-md-timepicker-dial]', 'aria-valuenow', '28') + ->assertAttribute('#meeting-dialog [data-md-timepicker-dial]', 'aria-valuetext', '28 minutes') ->keys(':focus', 'Enter') ->assertScript("! document.querySelector('#meeting-dialog').open") ->assertSeeIn('#meeting-value', '10:28'); @@ -242,7 +242,7 @@ it('changes the focused hour and minute with the arrow keys and confirms with En it('takes a typed time in the input variant and says what is wrong with it', function () { $page = timeProbe() ->click('#meeting') - ->click('#meeting-dialog [data-timepicker-mode="input"]') + ->click('#meeting-dialog [data-md-timepicker-mode="input"]') ->assertSeeIn('#meeting-title', 'Enter time') ->assertScript("document.activeElement.id === 'meeting-hour'") ->assertValue('#meeting-hour', '09') @@ -252,7 +252,7 @@ it('takes a typed time in the input variant and says what is wrong with it', fun ->assertAttribute('#meeting-hour', 'aria-invalid', 'true') // Word joiners keep the range on one line in the narrow column. ->assertScript(supportText('meeting-hour')." === 'Hour must be 1–12'") - ->click('#meeting-dialog [data-timepicker-confirm]') + ->click('#meeting-dialog [data-md-timepicker-confirm]') ->assertScript("document.querySelector('#meeting-dialog').open") ->assertSeeIn('#meeting-value', '09:30'); @@ -273,37 +273,37 @@ it('takes a typed time in the input variant and says what is wrong with it', fun }); it('keeps to the step and the limits', function () { - $dial = inPicker('slot', '[data-timepicker-dial]'); + $dial = inPicker('slot', '[data-md-timepicker-dial]'); $page = timeProbe() ->click('#slot') - ->assertScript("{$dial}.querySelector('[data-timepicker-label=\"hour24\"][data-value=\"8\"]').hasAttribute('data-disabled')") - ->assertScript("! {$dial}.querySelector('[data-timepicker-label=\"hour24\"][data-value=\"9\"]').hasAttribute('data-disabled')") - ->assertScript("{$dial}.querySelector('[data-timepicker-label=\"hour24\"][data-value=\"18\"]').hasAttribute('data-disabled')") + ->assertScript("{$dial}.querySelector('[data-md-timepicker-label=\"hour24\"][data-md-value=\"8\"]').hasAttribute('data-md-disabled')") + ->assertScript("! {$dial}.querySelector('[data-md-timepicker-label=\"hour24\"][data-md-value=\"9\"]').hasAttribute('data-md-disabled')") + ->assertScript("{$dial}.querySelector('[data-md-timepicker-label=\"hour24\"][data-md-value=\"18\"]').hasAttribute('data-md-disabled')") // An hour outside the limits is not taken. - ->click('#slot-dialog [data-timepicker-label="hour24"][data-value="20"]') - ->assertSeeIn('#slot-dialog [data-timepicker-box="hour"]', '10') - ->click('#slot-dialog [data-timepicker-label="hour24"][data-value="16"]') - ->assertAttribute('#slot-dialog [data-timepicker-dial]', 'data-view', 'minute') - ->assertScript("{$dial}.querySelector('[data-timepicker-label=\"minute\"][data-value=\"20\"]').hasAttribute('data-disabled')") + ->click('#slot-dialog [data-md-timepicker-label="hour24"][data-md-value="20"]') + ->assertSeeIn('#slot-dialog [data-md-timepicker-box="hour"]', '10') + ->click('#slot-dialog [data-md-timepicker-label="hour24"][data-md-value="16"]') + ->assertAttribute('#slot-dialog [data-md-timepicker-dial]', 'data-md-view', 'minute') + ->assertScript("{$dial}.querySelector('[data-md-timepicker-label=\"minute\"][data-md-value=\"20\"]').hasAttribute('data-md-disabled')") // A press between the steps lands on the nearest one. - ->click('#slot-dialog [data-timepicker-label="minute"][data-value="20"]') - ->assertSeeIn('#slot-dialog [data-timepicker-box="minute"]', '15'); + ->click('#slot-dialog [data-md-timepicker-label="minute"][data-md-value="20"]') + ->assertSeeIn('#slot-dialog [data-md-timepicker-box="minute"]', '15'); $page->script("{$dial}.focus()"); $page->keys(':focus', 'ArrowUp') - ->assertAttribute('#slot-dialog [data-timepicker-dial]', 'aria-valuenow', '30') - ->click('#slot-dialog [data-timepicker-mode="input"]') + ->assertAttribute('#slot-dialog [data-md-timepicker-dial]', 'aria-valuenow', '30') + ->click('#slot-dialog [data-md-timepicker-mode="input"]') ->type('#slot-hour', '18') - ->assertSeeIn('#slot-dialog [data-timepicker-range-error]', 'Choose a time from 09:00 to 17:00') + ->assertSeeIn('#slot-dialog [data-md-timepicker-range-error]', 'Choose a time from 09:00 to 17:00') ->type('#slot-hour', '16') ->type('#slot-minute', '20') ->assertSeeIn('#slot-minute-support', 'Minute must be a multiple of 15') - ->click('#slot-dialog [data-timepicker-confirm]') + ->click('#slot-dialog [data-md-timepicker-confirm]') ->assertScript("document.activeElement.id === 'slot-minute'") ->type('#slot-minute', '45') - ->click('#slot-dialog [data-timepicker-confirm]') + ->click('#slot-dialog [data-md-timepicker-confirm]') ->assertSeeIn('#slot-value', '16:45'); }); @@ -324,16 +324,71 @@ it('gives focus back to the field on Escape', function () { it('stays open with its draft through a Livewire render', function () { $page = timeProbe() ->click('#meeting') - ->click('#meeting-dialog [data-timepicker-label="hour12"][data-value="5"]') - ->assertAttribute('#meeting-dialog [data-timepicker-dial]', 'data-view', 'minute'); + ->click('#meeting-dialog [data-md-timepicker-label="hour12"][data-md-value="5"]') + ->assertAttribute('#meeting-dialog [data-md-timepicker-dial]', 'data-md-view', 'minute'); $page->script('window.eval("Livewire.first().touch()")'); $page->assertSeeIn('#renders', '1') ->assertScript("document.querySelector('#meeting-dialog').open") - ->assertSeeIn('#meeting-dialog [data-timepicker-box="hour"]', '05') - ->assertAttribute('#meeting-dialog [data-timepicker-dial]', 'data-view', 'minute') - ->click('#meeting-dialog [data-timepicker-label="minute"][data-value="10"]') - ->click('#meeting-dialog [data-timepicker-confirm]') + ->assertSeeIn('#meeting-dialog [data-md-timepicker-box="hour"]', '05') + ->assertAttribute('#meeting-dialog [data-md-timepicker-dial]', 'data-md-view', 'minute') + ->click('#meeting-dialog [data-md-timepicker-label="minute"][data-md-value="10"]') + ->click('#meeting-dialog [data-md-timepicker-confirm]') ->assertSeeIn('#meeting-value', '05:10'); }); + +it('moves the period selector between AM and PM with arrow keys, as a radio group', function () { + $am = '#meeting-dialog [data-md-timepicker-display] [data-md-timepicker-period-option="am"]'; + $pm = '#meeting-dialog [data-md-timepicker-display] [data-md-timepicker-period-option="pm"]'; + + $page = timeProbe() + ->click('#meeting') + ->assertAttribute($am, 'role', 'radio') + ->assertAttribute($am, 'aria-checked', 'true') + ->assertAttribute($pm, 'aria-checked', 'false'); + + $page->script("document.querySelector('{$am}').focus()"); + + $page->keys(':focus', 'ArrowRight') + ->assertAttribute($pm, 'aria-checked', 'true') + ->assertAttribute($am, 'aria-checked', 'false') + ->assertScript("document.activeElement.dataset.mdTimepickerPeriodOption === 'pm'") + // M3's radio-in-a-list: an arrow key moves to the other option and selects it, so PM + // reaches the dial straight away — the hour turns 9 into 21 without a confirm. + ->assertSeeIn('#meeting-dialog [data-md-timepicker-box="hour"]', '09'); + + $page->keys(':focus', 'ArrowLeft') + ->assertAttribute($am, 'aria-checked', 'true') + ->assertAttribute($pm, 'aria-checked', 'false') + ->assertScript("document.activeElement.dataset.mdTimepickerPeriodOption === 'am'"); +}); + +it('lies the dial on its side in a short landscape window', function () { + $page = timeProbe() + ->resize(700, 400) + ->click('#meeting') + ->assertScript("document.querySelector('#meeting-dialog').open"); + + // Side by side rather than stacked: the display and the dial share a top edge, the dial to + // the display's end, and the picker's own layout (flex column) gives way to the grid areas. + $page->assertScript(<<<'JS' + (() => { + const display = document.querySelector('#meeting-dialog [data-md-timepicker-display]').getBoundingClientRect(); + const dial = document.querySelector('#meeting-dialog [data-md-timepicker-dial]').getBoundingClientRect(); + + return Math.abs(display.top - dial.top) < 4 && dial.left >= display.right; + })() + JS) + ->assertScript("getComputedStyle(document.querySelector('#meeting-dialog [data-md-timepicker-picker]')).display === 'contents'"); + + $page->resize(390, 800) + ->assertScript(<<<'JS' + (() => { + const display = document.querySelector('#meeting-dialog [data-md-timepicker-display]').getBoundingClientRect(); + const dial = document.querySelector('#meeting-dialog [data-md-timepicker-dial]').getBoundingClientRect(); + + return dial.top >= display.bottom; + })() + JS); +}); diff --git a/tests/Feature/Components/InputStylesheetsTest.php b/tests/Feature/Components/InputStylesheetsTest.php index 48614741..a6085dac 100644 --- a/tests/Feature/Components/InputStylesheetsTest.php +++ b/tests/Feature/Components/InputStylesheetsTest.php @@ -34,6 +34,7 @@ dataset('input components', [ 'table', 'sort-header', 'datepicker', + 'timepicker', ]); /** diff --git a/tests/Feature/Components/TimepickerTest.php b/tests/Feature/Components/TimepickerTest.php index c76c0da9..4468dff4 100644 --- a/tests/Feature/Components/TimepickerTest.php +++ b/tests/Feature/Components/TimepickerTest.php @@ -28,13 +28,13 @@ it('draws a read-only field that opens the picker in a labelled dialog', functio ->toContain('aria-describedby="starts-support"') ->toMatch('/

In your time zone<\/p>/') ->toContain('x-on:keydown.enter.prevent="show()"') - ->toContain('data-timepicker-open') + ->toContain('data-md-timepicker-open') ->toContain('aria-label="Choose time"') ->toContain('toContain('id="starts-dialog"') ->toContain('wire:ignore') ->toContain('aria-labelledby="starts-title"') - ->toContain('

toContain('

toContain('x-modelable="value"') ->toContain("materialTimepicker( '14:30' ,") ->not->toContain('aria-invalid="true"') @@ -47,18 +47,18 @@ it('draws the dial as a slider, with twelve numbers per ring and M3\'s 24-hour i expect($html) ->toContain('role="slider"') ->toContain('x-bind:aria-valuetext="valueText"') - ->and(substr_count($html, 'data-timepicker-label="hour12"'))->toBe(12) - ->and(substr_count($html, 'data-timepicker-label="hour24"'))->toBe(24) - ->and(substr_count($html, 'data-timepicker-label="minute"'))->toBe(12) + ->and(substr_count($html, 'data-md-timepicker-label="hour12"'))->toBe(12) + ->and(substr_count($html, 'data-md-timepicker-label="hour24"'))->toBe(24) + ->and(substr_count($html, 'data-md-timepicker-label="minute"'))->toBe(12) ->and($html) - ->toMatch('/data-timepicker-label="hour24" data-value="0"\s+style="--x: 0\.0000; --y: -1\.0000"\s*>00toMatch('/data-timepicker-label="hour24" data-value="12"\s+data-inner\s+style="--x: 0\.0000; --y: -1\.0000"\s*>12toMatch('/data-timepicker-label="hour24" data-value="15"\s+data-inner\s+style="--x: 1\.0000; --y: -0\.0000"\s*>15toMatch('/data-timepicker-label="minute" data-value="0"\s+style="[^"]*"\s*>00toMatch('/data-timepicker-label="hour12" data-value="12"\s+style="--x: 0\.0000; --y: -1\.0000"\s*>12toContain('data-timepicker-handle') - ->toContain('data-timepicker-ink') - ->not->toContain('x-bind:data-disabled'); + ->toMatch('/data-md-timepicker-label="hour24" data-md-value="0"\s+style="--x: 0\.0000; --y: -1\.0000"\s*>00toMatch('/data-md-timepicker-label="hour24" data-md-value="12"\s+data-md-inner\s+style="--x: 0\.0000; --y: -1\.0000"\s*>12toMatch('/data-md-timepicker-label="hour24" data-md-value="15"\s+data-md-inner\s+style="--x: 1\.0000; --y: -0\.0000"\s*>15toMatch('/data-md-timepicker-label="minute" data-md-value="0"\s+style="[^"]*"\s*>00toMatch('/data-md-timepicker-label="hour12" data-md-value="12"\s+style="--x: 0\.0000; --y: -1\.0000"\s*>12toContain('data-md-timepicker-handle') + ->toContain('data-md-timepicker-ink') + ->not->toContain('x-bind:data-md-disabled'); }); it('offers the input variant, the period selector and the actions', function () { @@ -70,19 +70,19 @@ it('offers the input variant, the period selector and the actions', function () ->toContain('inputmode="numeric"') ->toContain('aria-describedby="pickup-hour-support"') ->toContain('aria-describedby="pickup-minute-support"') - ->toContain('data-timepicker-period-option="am"') - ->toContain('data-timepicker-period-option="pm"') + ->toContain('data-md-timepicker-period-option="am"') + ->toContain('data-md-timepicker-period-option="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('data-md-timepicker-display x-bind:data-md-cycle="is24 ? \'24\' : \'12\'"') ->toContain('aria-label="Switch to text input mode"') ->toContain('aria-label="Switch to clock mode"') - ->toContain('data-timepicker-cancel') - ->toContain('data-timepicker-confirm') + ->toContain('data-md-timepicker-cancel') + ->toContain('data-md-timepicker-confirm') ->toContain('>Cancel') ->toContain('>OK'); }); @@ -104,8 +104,8 @@ it('marks what lies outside the limits only when there are limits', function () $html = (string) $this->blade(''); expect($html) - ->toContain('x-bind:data-disabled="hourAllowed(3 + (isPm ? 12 : 0)) ? null : \'\'"') - ->toContain('x-bind:data-disabled="minuteAllowed(45) ? null : \'\'"'); + ->toContain('x-bind:data-md-disabled="hourAllowed(3 + (isPm ? 12 : 0)) ? null : \'\'"') + ->toContain('x-bind:data-md-disabled="minuteAllowed(45) ? null : \'\'"'); }); it('writes the first frame as the locale writes the time', function () { @@ -167,11 +167,11 @@ it('puts the field props on the field, its attributes on the input, and posts a ->toMatch('/^toContain('data-md-variant="filled"') ->toContain('data-md-size="sm"') - ->toContain('data-timepicker-field') + ->toContain('data-md-timepicker-field') ->toContain('required') ->toContain('placeholder="hh:mm"') ->toContain('') ->toContain('data-md-field-clear') - ->toMatch('/]*data-timepicker-open[^>]*disabled/s') + ->toMatch('/]*data-md-timepicker-open[^>]*disabled/s') ->and(substr_count($html, 'class="field w-60"'))->toBe(0); });