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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
This commit is contained in:
co-authored by
Claude Opus 5
parent
e3fcbf39ed
commit
22c141d380
@@ -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 <x-button>, 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 <x-button>, 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 @@
|
||||
</div>
|
||||
|
||||
<div x-show="! typing" x-on:scroll="extendMonths($event)" data-md-datepicker-calendar>
|
||||
<span id="{{ $id }}-month" aria-live="polite" x-text="monthYear" data-md-datepicker-month-live></span>
|
||||
<span id="{{ $id }}-month" class="md-visually-hidden" aria-live="polite" x-text="monthYear"></span>
|
||||
|
||||
<div data-md-datepicker-nav x-show="presentation === 'modal'">
|
||||
<button
|
||||
type="button"
|
||||
class="md-state-layer md-focus-ring"
|
||||
data-md-datepicker-menu-button="years"
|
||||
x-on:click="toggleView('years')"
|
||||
x-bind:aria-expanded="(view === 'years').toString()"
|
||||
@@ -341,6 +345,7 @@
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
class="md-state-layer md-focus-ring"
|
||||
data-md-datepicker-menu-button="months"
|
||||
x-on:click="toggleView('months')"
|
||||
x-bind:aria-expanded="(view === 'months').toString()"
|
||||
@@ -360,6 +365,7 @@
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
class="md-state-layer md-focus-ring"
|
||||
data-md-datepicker-menu-button="years"
|
||||
x-on:click="toggleView('years')"
|
||||
x-bind:aria-expanded="(view === 'years').toString()"
|
||||
@@ -425,6 +431,7 @@
|
||||
x-bind:data-md-current="year.current ? '' : null"
|
||||
x-on:click="showMonthOf(year.value, +shown.slice(5, 7))"
|
||||
x-text="year.label"
|
||||
class="md-state-layer md-focus-ring"
|
||||
data-md-datepicker-year
|
||||
></button>
|
||||
</template>
|
||||
@@ -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
|
||||
>
|
||||
<x-livewire-material::icon name="check" data-md-datepicker-check />
|
||||
@@ -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
|
||||
>
|
||||
<x-livewire-material::icon name="check" data-md-datepicker-check />
|
||||
|
||||
@@ -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]')
|
||||
|
||||
@@ -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('<x-datepicker id="expires" label="Expires on" />');
|
||||
$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('<span id="expires-month" class="md-visually-hidden" aria-live="polite" x-text="monthYear"></span>')
|
||||
// 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('<x-datepicker label="Date" mode="wheel" />'))['mode'])->toBe('docked');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user