From 22c141d3806c0af1eb9b1d9369446596396450e7 Mon Sep 17 00:00:00 2001 From: Andreas Reinhold / reini Date: Mon, 14 Sep 2026 19:34:40 +0200 Subject: [PATCH] Draw the datepicker's controls from the shared classes, in px Plan step 36 review of 72d9dbfc. The year, the three menu buttons and the docked lists' options hand-rolled md-state-layer and md-focus-ring on themselves, in their own ink, on the claim that each was a bigger box than an indicator inside it; only the day is. They now render the classes, with the option's inset ring as the one refinement. The day keeps its own layer on the 40px indicator, now at M3's state-opacity tokens, gone from a disabled or blank day (a focused disabled day still showed it), and in its own stacking context so it lies over a chosen day's primary fill instead of under it. The month announcement is md-visually-hidden again, not a hook with a copy of its clip. The stylesheet kept every length in rem although the commit said px: sizes are now px, spacing the measurement tokens, and disabled ink the 38% state token, as search and slider were drawn. The view header's "below sm" is 600px, medium. Six browser assertions still read the old unprefixed hooks (dataset.presentation, data-selected, data-between, data-start, data-end); one of them would have passed vacuously. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9 --- resources/css/components/datepicker.css | 255 ++++++++---------- .../views/components/datepicker.blade.php | 15 +- tests/Browser/DatepickerTest.php | 12 +- tests/Feature/Components/DatepickerTest.php | 30 +++ 4 files changed, 157 insertions(+), 155 deletions(-) diff --git a/resources/css/components/datepicker.css b/resources/css/components/datepicker.css index 1a077731..93f65fb2 100644 --- a/resources/css/components/datepicker.css +++ b/resources/css/components/datepicker.css @@ -33,16 +33,17 @@ * [data-md-datepicker-entry] the text field(s) of the modal input * [data-md-datepicker-actions] Cancel and OK * - * Cancel, OK and the icon buttons in the app bar and the nav are , which draws its own - * state layer and focus ring from the foundation classes. The day, the year, the menu button and - * the list option keep their own hand-rolled state layer and focus ring, as `data-md-field-button` - * does (field.css): each interactive box is bigger than the round indicator drawn inside it (a - * day's cell is 48px, its indicator 40px; a menu button and an option are full rows), which the - * foundation's classes, fixed to the element they are on, cannot draw — and `:focus-visible` only - * ever matches the element actually focused, never a smaller child inside it. + * The year, the menu buttons and the docked lists' options render the foundation's `md-state-layer` + * and `md-focus-ring` (foundation/interaction.css): each draws its layer in its own ink and its + * ring round itself, so the classes need one refinement only — an option's ring sits inside its + * full-width row. Cancel, OK and the icon buttons are , which renders the classes too. + * The day alone keeps a state layer and ring of its own: the element focused and pressed is the + * 48px cell, while both are drawn on the 40px round indicator inside it, which a class drawn on the + * element it sits on cannot do, and `:focus-visible` never matches that child. Its layer takes + * M3's state opacities (tokens/state.css) and is gone from a disabled or blank day. * - * Every state layer here is the content colour at 8% on hover (only where a pointer can hover) and - * 10% on focus and press; keyboard focus adds the package's 3px secondary ring. + * Lengths are px, and spacing is the measurement tokens: M3 gives every size here in dp. A + * disabled day or option is on-surface at M3's 38% content opacity. */ @layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility; @@ -56,7 +57,7 @@ /* The padding is on the lines, not the box, so a box with nothing to say takes no room. */ [data-md-datepicker-support] { - --datepicker-pad: 1rem; + --datepicker-pad: var(--md-sys-measurement-space200); color: var(--md-sys-color-on-surface-variant); font: var(--md-sys-typescale-body-sm); @@ -64,15 +65,15 @@ } [data-md-datepicker-support] > * { - padding: 0.25rem var(--datepicker-pad) 0; + padding: var(--md-sys-measurement-space50) var(--datepicker-pad) 0; } [data-md-datepicker-support][data-md-size='sm'] { - --datepicker-pad: 0.75rem; + --datepicker-pad: 12px; } [data-md-datepicker-support][data-md-size='xs'] { - --datepicker-pad: 0.625rem; + --datepicker-pad: var(--md-sys-measurement-space125); } [data-md-datepicker-error] { @@ -83,9 +84,9 @@ [data-md-datepicker-picker] { inset: auto; - width: 22.5rem; - max-width: calc(100vw - 1rem); - max-height: calc(100dvh - 1rem); + width: 360px; + max-width: calc(100vw - var(--md-sys-measurement-space200)); + max-height: calc(100dvh - var(--md-sys-measurement-space200)); margin: 0; padding: 0; overflow: visible; @@ -97,7 +98,7 @@ [data-md-datepicker-picker]:popover-open { top: anchor(bottom); left: anchor(left); - margin-block: 0.25rem; + margin-block: var(--md-sys-measurement-space50); position-try-fallbacks: flip-block, flip-inline, flip-block flip-inline; opacity: 1; transition-property: opacity; @@ -184,22 +185,22 @@ display: flex; align-items: center; justify-content: space-between; - gap: 0.5rem; - height: 4rem; - padding-inline: 0.25rem 0.75rem; + gap: var(--md-sys-measurement-space100); + height: 64px; + padding-inline: var(--md-sys-measurement-space50) 12px; } [data-md-datepicker-app-bar-actions] { display: flex; align-items: center; - gap: 0.25rem; + gap: var(--md-sys-measurement-space50); } [data-md-datepicker-full-headline] { display: flex; flex-direction: column; - gap: 0.25rem; - padding: 0 1.5rem 1rem; + gap: var(--md-sys-measurement-space50); + padding: 0 var(--md-sys-measurement-space300) var(--md-sys-measurement-space200); } /* The calendar is the only part that scrolls, with the weekday header held at its top. */ @@ -218,11 +219,11 @@ } [data-md-datepicker-picker][data-md-presentation='full'] [data-md-datepicker-day] { - scroll-margin-block: 3rem; + scroll-margin-block: 48px; } [data-md-datepicker-month-label] { - padding: 1.25rem 0 0.5rem; + padding: 20px 0 var(--md-sys-measurement-space100); color: var(--md-sys-color-on-surface-variant); font: var(--md-sys-typescale-title-sm); letter-spacing: var(--md-sys-typescale-title-sm-tracking); @@ -236,17 +237,17 @@ flex-direction: column; justify-content: space-between; flex: none; - min-height: 7.5rem; + min-height: 120px; border-bottom: 1px solid var(--md-sys-color-outline-variant); color: var(--md-sys-color-on-surface-variant); } [data-md-datepicker-header][data-md-range] { - min-height: 8rem; + min-height: 128px; } [data-md-datepicker-title] { - padding: 1rem 0.75rem 0 1.5rem; + padding: var(--md-sys-measurement-space200) 12px 0 var(--md-sys-measurement-space300); font: var(--md-sys-typescale-label-lg); letter-spacing: var(--md-sys-typescale-label-lg-tracking); } @@ -255,8 +256,8 @@ display: flex; align-items: center; justify-content: space-between; - gap: 0.5rem; - padding: 0 0.75rem 0.75rem 1.5rem; + gap: var(--md-sys-measurement-space100); + padding: 0 12px 12px var(--md-sys-measurement-space300); } [data-md-datepicker-headline] { @@ -280,14 +281,14 @@ flex: none; align-items: center; justify-content: space-between; - height: 3.5rem; - padding-inline: 0.75rem; + height: 56px; + padding-inline: 12px; color: var(--md-sys-color-on-surface-variant); } [data-md-datepicker-nav][data-md-docked] { - height: 4rem; - padding-inline: 0.25rem; + height: 64px; + padding-inline: var(--md-sys-measurement-space50); } [data-md-datepicker-stepper], @@ -302,25 +303,22 @@ } [data-md-datepicker-menu-button] { - position: relative; display: inline-flex; align-items: center; - gap: 0.5rem; - height: 2.5rem; - padding-inline: 0.75rem 0.5rem; + gap: var(--md-sys-measurement-space100); + height: 40px; + padding-inline: 12px var(--md-sys-measurement-space100); border-radius: var(--md-sys-shape-corner-full); color: var(--md-sys-color-on-surface-variant); font: var(--md-sys-typescale-label-lg); letter-spacing: var(--md-sys-typescale-label-lg-tracking); white-space: nowrap; cursor: pointer; - outline: none; - isolation: isolate; } [data-md-datepicker-nav][data-md-docked] [data-md-datepicker-menu-button] { - padding-inline: 0.5rem 0.25rem; - gap: 0.25rem; + padding-inline: var(--md-sys-measurement-space100) var(--md-sys-measurement-space50); + gap: var(--md-sys-measurement-space50); } [data-md-datepicker-menu-arrow] { @@ -331,66 +329,17 @@ rotate: 180deg; } - /* ---- State layers shared by days, years, menu buttons and list rows ------------------------ */ - - [data-md-datepicker-menu-button]::before, - [data-md-datepicker-day] > span::before, - [data-md-datepicker-year]::before, - [data-md-datepicker-option]::before { - content: ''; - position: absolute; - inset: 0; - z-index: -1; - border-radius: inherit; - background-color: var(--datepicker-layer, currentColor); - opacity: 0; - pointer-events: none; - transition: opacity var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast); - } - - @media (hover: hover) { - [data-md-datepicker-menu-button]:hover::before, - [data-md-datepicker-day]:not([aria-disabled='true'], [data-md-blank]):hover > span::before, - [data-md-datepicker-year]:hover::before, - [data-md-datepicker-option]:not([aria-disabled='true']):hover::before { - opacity: 0.08; - } - } - - [data-md-datepicker-menu-button]:focus-visible::before, - [data-md-datepicker-menu-button]:active::before, - [data-md-datepicker-day]:focus-visible > span::before, - [data-md-datepicker-day]:not([aria-disabled='true'], [data-md-blank]):active > span::before, - [data-md-datepicker-year]:focus-visible::before, - [data-md-datepicker-year]:active::before, - [data-md-datepicker-option]:focus-visible::before, - [data-md-datepicker-option]:not([aria-disabled='true']):active::before { - opacity: 0.1; - } - - [data-md-datepicker-menu-button]:focus-visible, - [data-md-datepicker-day]:focus-visible > span, - [data-md-datepicker-year]:focus-visible { - outline: 3px solid var(--md-sys-color-secondary); - outline-offset: 2px; - } - - [data-md-datepicker-option]:focus-visible { - outline: 3px solid var(--md-sys-color-secondary); - outline-offset: -3px; - } - /* ---- The days ----------------------------------------------------------------------------- */ [data-md-datepicker-grid] { - width: calc(100% - 1.5rem); - margin-inline: 0.75rem; + width: calc(100% - 24px); + margin-inline: 12px; border-collapse: collapse; table-layout: fixed; } [data-md-datepicker-grid] th { - height: 3rem; + height: 48px; padding: 0; color: var(--md-sys-color-on-surface); font: var(--md-sys-typescale-body-lg); @@ -402,7 +351,7 @@ --datepicker-layer: var(--md-sys-color-on-surface-variant); position: relative; - height: 3rem; + height: 48px; padding: 0; color: var(--md-sys-color-on-surface); font: var(--md-sys-typescale-body-lg); @@ -414,17 +363,56 @@ -webkit-tap-highlight-color: transparent; } + /* The 40px indicator, its own stacking context so the state layer lies over a chosen day's + primary fill rather than under it. */ [data-md-datepicker-day] > span { position: relative; display: grid; place-items: center; - width: 2.5rem; - height: 2.5rem; + width: 40px; + height: 40px; margin: auto; border-radius: var(--md-sys-shape-corner-full); + isolation: isolate; transition: background-color var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast); } + /* The day's state layer and focus ring, on the indicator (see the header). */ + [data-md-datepicker-day] > span::before { + content: ''; + position: absolute; + inset: 0; + z-index: -1; + border-radius: inherit; + background-color: var(--datepicker-layer); + opacity: 0; + pointer-events: none; + transition: opacity var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast); + } + + @media (hover: hover) { + [data-md-datepicker-day]:hover > span::before { + opacity: var(--md-sys-state-hover-state-layer-opacity); + } + } + + [data-md-datepicker-day]:focus-visible > span::before { + opacity: var(--md-sys-state-focus-state-layer-opacity); + } + + [data-md-datepicker-day]:active > span::before { + opacity: var(--md-sys-state-pressed-state-layer-opacity); + } + + [data-md-datepicker-day]:is([aria-disabled='true'], [data-md-blank]) > span::before { + display: none; + } + + [data-md-datepicker-day]:focus-visible > span { + outline: 3px solid var(--md-sys-color-secondary); + outline-offset: 2px; + } + [data-md-datepicker-day][data-md-blank] { cursor: default; } @@ -450,7 +438,7 @@ [data-md-datepicker-day][data-md-end]::before { content: ''; position: absolute; - inset-block: 0.25rem; + inset-block: var(--md-sys-measurement-space50); inset-inline: 0; z-index: -1; background-color: var(--md-sys-color-secondary-container); @@ -482,12 +470,12 @@ } [data-md-datepicker-day][aria-disabled='true'] { - color: color-mix(in srgb, var(--md-sys-color-on-surface) 38%, transparent); + color: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-content-opacity) * 100%), transparent); cursor: default; } [data-md-datepicker-day][aria-disabled='true'][data-md-today] > span { - box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--md-sys-color-on-surface) 38%, transparent); + box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-content-opacity) * 100%), transparent); } /* ---- The modal year grid ----------------------------------------------------------------- */ @@ -497,40 +485,31 @@ grid-template-columns: repeat(3, 1fr); align-content: start; justify-items: center; - row-gap: 1rem; - height: 20.9375rem; - padding: 0.5rem 0.75rem; + row-gap: var(--md-sys-measurement-space200); + height: 335px; + padding: var(--md-sys-measurement-space100) 12px; overflow-y: auto; overscroll-behavior: contain; border-bottom: 1px solid var(--md-sys-color-outline-variant); } [data-md-datepicker-year] { - --datepicker-layer: var(--md-sys-color-on-surface-variant); - - position: relative; - width: 4.5rem; - height: 2.25rem; + width: 72px; + height: 36px; border-radius: var(--md-sys-shape-corner-full); color: var(--md-sys-color-on-surface-variant); font: var(--md-sys-typescale-body-lg); letter-spacing: var(--md-sys-typescale-body-lg-tracking); cursor: pointer; - outline: none; - isolation: isolate; - scroll-margin-block: 0.5rem; + scroll-margin-block: var(--md-sys-measurement-space100); } [data-md-datepicker-year][data-md-current] { - --datepicker-layer: var(--md-sys-color-primary); - color: var(--md-sys-color-primary); box-shadow: inset 0 0 0 1px var(--md-sys-color-primary); } [data-md-datepicker-year][aria-selected='true'] { - --datepicker-layer: var(--md-sys-color-on-primary); - background-color: var(--md-sys-color-primary); color: var(--md-sys-color-on-primary); box-shadow: none; @@ -539,30 +518,30 @@ /* ---- The docked month and year lists ------------------------------------------------------ */ [data-md-datepicker-menu] { - height: 21.5rem; - padding-block: 0.5rem; + height: 344px; + padding-block: var(--md-sys-measurement-space100); overflow-y: auto; overscroll-behavior: contain; border-top: 1px solid var(--md-sys-color-outline-variant); } [data-md-datepicker-option] { - --datepicker-layer: var(--md-sys-color-on-surface); - - position: relative; display: flex; align-items: center; - gap: 1rem; + gap: var(--md-sys-measurement-space200); width: 100%; - height: 3rem; - padding-inline: 1rem; + height: 48px; + padding-inline: var(--md-sys-measurement-space200); color: var(--md-sys-color-on-surface); font: var(--md-sys-typescale-body-lg); letter-spacing: var(--md-sys-typescale-body-lg-tracking); text-align: start; cursor: pointer; - outline: none; - isolation: isolate; + } + + /* The foundation's ring, drawn inside the full-width row rather than round it. */ + [data-md-datepicker-option]:focus-visible { + outline-offset: -3px; } [data-md-datepicker-option][aria-selected='true'] { @@ -570,7 +549,7 @@ } [data-md-datepicker-option][aria-disabled='true'] { - color: color-mix(in srgb, var(--md-sys-color-on-surface) 38%, transparent); + color: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-disabled-content-opacity) * 100%), transparent); cursor: default; } @@ -585,29 +564,13 @@ /* ---- The modal input ------------------------------------------------------------------------ */ [data-md-datepicker-entry] { - padding: 0.625rem 1.5rem 1rem; + padding: var(--md-sys-measurement-space125) var(--md-sys-measurement-space300) var(--md-sys-measurement-space200); } [data-md-datepicker-entry-fields][data-md-range] { display: grid; grid-template-columns: 1fr 1fr; - gap: 0.5rem; - } - - /* ---- The month announcement, spoken but not drawn -------------------------------------------- */ - - /* Same clip as the text classes' `md-visually-hidden`, kept as a hook because a package view - writes no class but the interaction classes (plan step 36). */ - [data-md-datepicker-month-live] { - position: absolute; - width: 1px; - height: 1px; - padding: 0; - margin: -1px; - overflow: hidden; - clip-path: inset(50%); - white-space: nowrap; - border-width: 0; + gap: var(--md-sys-measurement-space100); } /* ---- Cancel and OK ---------------------------------------------------------------------------- */ @@ -618,7 +581,7 @@ flex-wrap: wrap; align-items: center; justify-content: flex-end; - gap: 0.5rem; - padding: 0.25rem 0.375rem 0.5rem 0.75rem; + gap: var(--md-sys-measurement-space100); + padding: var(--md-sys-measurement-space50) var(--md-sys-measurement-space75) var(--md-sys-measurement-space100) 12px; } } diff --git a/resources/views/components/datepicker.blade.php b/resources/views/components/datepicker.blade.php index b7dfb116..d265953f 100644 --- a/resources/views/components/datepicker.blade.php +++ b/resources/views/components/datepicker.blade.php @@ -5,7 +5,8 @@ - `docked` (the default): a text field that takes a typed date in the locale's numeric format (`13.09.2026` in `de`, `09/13/2026` in `en-US`), with the calendar dropping open under it — on a press of the field, its calendar button, or ArrowDown. On a compact window (below - `sm`) the calendar opens as the modal picker instead, where a docked one would not fit. + 600px, `medium`) the calendar opens as the modal picker instead, where a docked one would not + fit. - `modal`: the field only shows the date; pressing it (or Enter, Space, ArrowDown) opens the calendar in a dialog, with a pencil to switch to typing. - `input`: the same dialog, opened on its text field, with a calendar icon to switch back. @@ -47,7 +48,9 @@ material-web's md-comp-date-picker-docked tokens, v0_192, as Compose has none; all Apache-2.0): surface-container-high at elevation 3, 360px wide, the extra-large corner (modal) or large (docked), 40px days in 48px cells, today outlined in primary, the chosen day in primary and a - range's middle in secondary-container. The styles are resources/css/components/datepicker.css. --}} + range's middle in secondary-container. The styles are resources/css/components/datepicker.css; + the year, the menu buttons and the list options render `md-state-layer md-focus-ring`, the + month's announcement `md-visually-hidden`, and nothing else here writes a class. --}} @props([ 'label' => null, @@ -314,11 +317,12 @@
- +
@@ -439,6 +446,7 @@ x-bind:aria-disabled="month.disabled ? 'true' : null" x-bind:tabindex="month.selected ? 0 : -1" x-on:click="month.disabled || showMonthOf(+shown.slice(0, 4), month.value)" + class="md-state-layer md-focus-ring" data-md-datepicker-option > @@ -455,6 +463,7 @@ x-bind:aria-selected="year.selected.toString()" x-bind:tabindex="year.selected ? 0 : -1" x-on:click="showMonthOf(year.value, +shown.slice(5, 7))" + class="md-state-layer md-focus-ring" data-md-datepicker-option > diff --git a/tests/Browser/DatepickerTest.php b/tests/Browser/DatepickerTest.php index 034f07a6..da1fd93f 100644 --- a/tests/Browser/DatepickerTest.php +++ b/tests/Browser/DatepickerTest.php @@ -181,7 +181,7 @@ it('opens the docked picker under its field and walks the grid from the keyboard $page = dateProbe() ->click('[aria-controls="expires-field-picker"][data-md-datepicker-toggle]') - ->assertScript("{$picker}.matches(':popover-open') && {$picker}.dataset.presentation === 'docked'") + ->assertScript("{$picker}.matches(':popover-open') && {$picker}.dataset.mdPresentation === 'docked'") ->assertAttribute('#expires-field', 'aria-expanded', 'true') ->assertScript("(() => { const field = document.querySelector('#expires-field').closest('[data-md-field-box]').getBoundingClientRect(); const box = {$picker}.getBoundingClientRect(); return Math.abs(box.top - field.bottom - 4) < 2 && Math.abs(box.left - field.left) < 2; })()") ->assertScript(focusedDay('2026-09-13')); @@ -212,8 +212,8 @@ it('keeps focus and choice inside min and max', function () { $page->assertAttribute(day('expires-field', '2026-09-09'), 'aria-disabled', 'true') ->script("document.querySelector('".day('expires-field', '2026-09-09')."').click()"); - $page->assertScript("! document.querySelector('".day('expires-field', '2026-09-09')."').hasAttribute('data-selected')") - ->assertScript("document.querySelector('".day('expires-field', '2026-09-13')."').hasAttribute('data-selected')"); + $page->assertScript("! document.querySelector('".day('expires-field', '2026-09-09')."').hasAttribute('data-md-selected')") + ->assertScript("document.querySelector('".day('expires-field', '2026-09-13')."').hasAttribute('data-md-selected')"); $page->keys(':focus', 'PageUp')->assertScript(focusedDay('2026-09-10')); $page->keys(':focus', 'ArrowLeft')->assertScript(focusedDay('2026-09-10')); @@ -327,12 +327,12 @@ it('picks a range: a start, an end, and the days between', function () { ->assertAttribute(day('trip-field', '2026-09-14'), 'aria-selected', 'true'); $page->click(day('trip-field', '2026-09-20')) - ->assertScript("document.querySelector('".day('trip-field', '2026-09-20')."').hasAttribute('data-selected')") - ->assertScript("! document.querySelector('".day('trip-field', '2026-09-14')."').hasAttribute('data-between')"); + ->assertScript("document.querySelector('".day('trip-field', '2026-09-20')."').hasAttribute('data-md-selected')") + ->assertScript("! document.querySelector('".day('trip-field', '2026-09-14')."').hasAttribute('data-md-between')"); $page->click(day('trip-field', '2026-09-24')) ->assertScript("[...document.querySelectorAll('#trip-field-picker [data-md-between]')].map((cell) => cell.dataset.mdValue).join() === '2026-09-21,2026-09-22,2026-09-23'") - ->assertScript("document.querySelector('".day('trip-field', '2026-09-20')."').hasAttribute('data-start') && document.querySelector('".day('trip-field', '2026-09-24')."').hasAttribute('data-end')") + ->assertScript("document.querySelector('".day('trip-field', '2026-09-20')."').hasAttribute('data-md-start') && document.querySelector('".day('trip-field', '2026-09-24')."').hasAttribute('data-md-end')") ->assertSeeIn('#trip', '2026-09-15'); $page->click('#trip-field-picker [data-md-datepicker-confirm]') diff --git a/tests/Feature/Components/DatepickerTest.php b/tests/Feature/Components/DatepickerTest.php index 226e14f7..ce39c4a0 100644 --- a/tests/Feature/Components/DatepickerTest.php +++ b/tests/Feature/Components/DatepickerTest.php @@ -3,6 +3,7 @@ use Carbon\CarbonImmutable; use Livewire\Component; use Livewire\Livewire; +use NoNameWeb\LivewireMaterial\Tests\Support\ComponentStylesheet; /** * The JSON config handed to `materialDatepicker(…)`, decoded. @@ -97,6 +98,35 @@ it('names each dropdown so Shift+M and Shift+Y can reach it', function () { ->toContain('data-md-datepicker-menu-button="years"'); }); +it('draws the year, the menu buttons and the list options from the interaction classes', function () { + $html = (string) $this->blade(''); + $css = ComponentStylesheet::read('datepicker'); + + expect(preg_match_all('/class="md-state-layer md-focus-ring"\s+data-md-datepicker-menu-button=/', $html))->toBe(3) + ->and(preg_match_all('/class="md-state-layer md-focus-ring"\s+data-md-datepicker-year\b/', $html))->toBe(1) + ->and(preg_match_all('/class="md-state-layer md-focus-ring"\s+data-md-datepicker-option\b/', $html))->toBe(2) + ->and($html)->toContain('') + // Nothing in the stylesheet takes back what the classes draw… + ->and($css->declarations('[data-md-datepicker-menu-button]'))->not->toHaveKeys(['position', 'isolation', 'outline']) + ->and($css->declarations('[data-md-datepicker-year]'))->not->toHaveKeys(['position', 'isolation', 'outline']) + ->and($css->declarations('[data-md-datepicker-option]'))->not->toHaveKeys(['position', 'isolation', 'outline']) + // …but the option's ring, which sits inside its full-width row. + ->and($css->declarations('[data-md-datepicker-option]:focus-visible'))->toBe(['outline-offset' => '-3px']); +}); + +it('draws the day\'s state layer on its indicator at M3\'s state opacities, and none on a disabled day', function () { + $css = ComponentStylesheet::read('datepicker'); + + expect($css->declarations('[data-md-datepicker-day] > span::before'))->toMatchArray(['background-color' => 'var(--datepicker-layer)', 'opacity' => '0', 'z-index' => '-1']) + ->and($css->declarations('[data-md-datepicker-day] > span'))->toMatchArray(['width' => '40px', 'height' => '40px', 'isolation' => 'isolate']) + ->and($css->declarations('[data-md-datepicker-day]:hover > span::before', ['@media (hover: hover)']))->toBe(['opacity' => 'var(--md-sys-state-hover-state-layer-opacity)']) + ->and($css->declarations('[data-md-datepicker-day]:focus-visible > span::before'))->toBe(['opacity' => 'var(--md-sys-state-focus-state-layer-opacity)']) + ->and($css->declarations('[data-md-datepicker-day]:active > span::before'))->toBe(['opacity' => 'var(--md-sys-state-pressed-state-layer-opacity)']) + ->and($css->declarations("[data-md-datepicker-day]:is([aria-disabled='true'], [data-md-blank]) > span::before"))->toBe(['display' => 'none']) + ->and($css->declarations('[data-md-datepicker-day]:focus-visible > span'))->toBe(['outline' => '3px solid var(--md-sys-color-secondary)', 'outline-offset' => '2px']) + ->and($css->declarations('[data-md-datepicker-day]'))->toMatchArray(['height' => '48px']); +}); + it('falls back to the docked picker for an unknown mode', function () { expect(datepickerConfig((string) $this->blade(''))['mode'])->toBe('docked'); });