diff --git a/resources/css/components.css b/resources/css/components.css index c22b2871..6301bffd 100644 --- a/resources/css/components.css +++ b/resources/css/components.css @@ -28,6 +28,7 @@ @import './components/chip-set.css'; @import './components/choices.css'; @import './components/slider.css'; +@import './components/search.css'; /* Containment */ diff --git a/resources/css/components/search.css b/resources/css/components/search.css index 97eb04d5..035b8bc4 100644 --- a/resources/css/components/search.css +++ b/resources/css/components/search.css @@ -12,36 +12,46 @@ * Widths are M3's: never wider than 720px, and the bar grows to that width while it is focused — * `--search-width` is its resting width (the room it is given, by default) and * `--search-open-width` the focused one, so a caller that wants M3's 360px resting bar sets - * `--search-width: 22.5rem` on a wrapper. The leading and trailing padding is 24px unfocused and + * `--search-width: 360px` on a wrapper. The leading and trailing padding is 24px unfocused and * 16px focused, as the search specs table gives it. * - * [data-search] the root; data-open, data-full-screen, data-trigger - * [data-search-scrim] over the page while the view is docked - * [data-search-trigger] data-trigger="icon" only: the icon button that expands the view - * [data-search-bar] the pill, above the view - * [data-search-leading], [data-search-field] (the combobox around [data-search-input]), - * [data-search-clear], [data-search-trailing] - * [data-search-view] the container behind the bar - * [data-search-suggestions] before the first keystroke - * [data-search-results] once something is typed - * [data-search-status] the polite live region that counts whichever list is on screen + * Hovered and focused buttons in the bar take M3's state-layer opacities (tokens/state.css), and the + * docked scrim is M3's scrim role at 32%, which no system token names. The breakpoint that makes the + * view full screen is decided in resources/js/search.js (`upTo('medium')`, 600px). + * + * [data-md-search] the root; data-md-open, data-md-full-screen, data-md-trigger + * [data-md-search-scrim] over the page while the view is docked + * [data-md-search-trigger] data-md-trigger="icon" only: the icon button that expands the view + * [data-md-search-bar] the pill, above the view + * [data-md-search-leading], [data-md-search-field] (the combobox around + * [data-md-search-input]), [data-md-search-clear], [data-md-search-trailing] + * [data-md-search-view] the container behind the bar + * [data-md-search-suggestions] before the first keystroke + * [data-md-search-results] once something is typed; [data-md-search-empty] when nothing is + * [data-md-search-status] the polite live region that counts whichever list is on screen */ -@layer components { - [data-search] { - --search-height: 3.5rem; +@layer material.reset, material.tokens, material.base, material.layout, material.components, material.text, material.visibility; + +@import './icon.css'; + +@layer material.components { + [data-md-search] { + --search-height: 56px; /* M3: min 360px, max 720px, and wider while focused. */ --search-width: 100%; - --search-open-width: 45rem; + --search-open-width: 720px; + + position: relative; } - [data-search][data-open] { + [data-md-search][data-md-open] { z-index: 50; } /* Docked, the view opens over the page: M3 puts a scrim under it. Behind the bar and the view inside the root's own stacking context, above everything the page draws outside it. */ - [data-search-scrim] { + [data-md-search-scrim] { position: fixed; inset: 0; z-index: -1; @@ -53,20 +63,20 @@ } @starting-style { - [data-search-scrim] { + [data-md-search-scrim] { opacity: 0; } } - [data-search-bar] { + [data-md-search-bar] { position: relative; z-index: 1; display: flex; align-items: center; - gap: 0.25rem; + gap: var(--md-sys-measurement-space50); width: min(100%, var(--search-width)); height: var(--search-height); - padding-inline: 0.75rem; + padding-inline: 12px; border-radius: var(--md-sys-shape-corner-full); background-color: var(--md-sys-color-surface-container-high); color: var(--md-sys-color-on-surface); @@ -77,70 +87,70 @@ } /* Focused: wider, and the padding drops from 24px to the contained style's 16px. */ - [data-search][data-open] [data-search-bar] { + [data-md-search][data-md-open] [data-md-search-bar] { width: min(100%, var(--search-open-width)); - padding-inline: 0.25rem; + padding-inline: var(--md-sys-measurement-space50); } @media (hover: hover) { - [data-search]:not([data-open]) [data-search-bar]:hover { - background-color: color-mix(in srgb, var(--md-sys-color-on-surface) 8%, var(--md-sys-color-surface-container-high)); + [data-md-search]:not([data-md-open]) [data-md-search-bar]:hover { + background-color: color-mix(in srgb, var(--md-sys-color-on-surface) calc(var(--md-sys-state-hover-state-layer-opacity) * 100%), var(--md-sys-color-surface-container-high)); } } - [data-search][data-open] [data-search-bar] { + [data-md-search][data-md-open] [data-md-search-bar] { background-color: transparent; } - [data-search-leading], - [data-search-clear] { + [data-md-search-leading], + [data-md-search-clear] { display: grid; flex: none; place-items: center; - width: 3rem; - height: 3rem; + width: var(--md-sys-measurement-space600); + height: var(--md-sys-measurement-space600); border-radius: var(--md-sys-shape-corner-full); } - [data-search-leading] { + [data-md-search-leading] { color: var(--md-sys-color-on-surface); } - [data-search-clear] { + [data-md-search-clear] { color: var(--md-sys-color-on-surface-variant); cursor: pointer; } - [data-search-bar] button { + [data-md-search-bar] button { outline: none; transition: background-color var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast); } @media (hover: hover) { - [data-search-bar] button:hover { - background-color: color-mix(in srgb, var(--md-sys-color-on-surface-variant) 8%, transparent); + [data-md-search-bar] button:hover { + background-color: color-mix(in srgb, var(--md-sys-color-on-surface-variant) calc(var(--md-sys-state-hover-state-layer-opacity) * 100%), transparent); } } - [data-search-bar] button:focus-visible { - background-color: color-mix(in srgb, var(--md-sys-color-on-surface-variant) 10%, transparent); + [data-md-search-bar] button:focus-visible { + background-color: color-mix(in srgb, var(--md-sys-color-on-surface-variant) calc(var(--md-sys-state-focus-state-layer-opacity) * 100%), transparent); outline: 3px solid var(--md-sys-color-secondary); outline-offset: -3px; } /* The combobox wrapper is only a role holder; it has to lay out as the input used to. */ - [data-search-field] { + [data-md-search-field] { display: flex; flex: 1 1 0%; min-width: 0; height: 100%; } - [data-search-input] { + [data-md-search-input] { flex: 1 1 0%; min-width: 0; height: 100%; - padding-inline: 0.25rem; + padding-inline: var(--md-sys-measurement-space50); appearance: none; background: transparent; color: var(--md-sys-color-on-surface); @@ -150,41 +160,41 @@ letter-spacing: var(--md-sys-typescale-body-lg-tracking); } - [data-search-input]::placeholder { + [data-md-search-input]::placeholder { color: var(--md-sys-color-on-surface-variant); opacity: 1; } - [data-search-input]::-webkit-search-cancel-button, - [data-search-input]::-webkit-search-decoration { + [data-md-search-input]::-webkit-search-cancel-button, + [data-md-search-input]::-webkit-search-decoration { appearance: none; display: none; } - [data-search-bar]:has([data-search-input]:placeholder-shown) [data-search-clear] { + [data-md-search-bar]:has([data-md-search-input]:placeholder-shown) [data-md-search-clear] { display: none; } - [data-search-trailing] { + [data-md-search-trailing] { display: flex; flex: none; align-items: center; - gap: 0.25rem; - padding-inline-end: 0.25rem; + gap: var(--md-sys-measurement-space50); + padding-inline-end: var(--md-sys-measurement-space50); } - [data-search][data-open] [data-search-trailing] { + [data-md-search][data-md-open] [data-md-search-trailing] { display: none; } - [data-search-view] { + [data-md-search-view] { position: absolute; inset-inline: 0; top: 0; display: flex; flex-direction: column; max-width: var(--search-open-width); - max-height: min(40rem, 70dvh); + max-height: min(640px, 70dvh); padding-top: var(--search-height); overflow: hidden; border-radius: var(--md-sys-shape-corner-xl); @@ -198,7 +208,7 @@ } @starting-style { - [data-search-view] { + [data-md-search-view] { opacity: 0; scale: 1 0.9; } @@ -206,35 +216,35 @@ /* M3: the docked container is at least 240px tall — once there is something in it to be tall about; a search with nothing to show stays the height of its bar. */ - [data-search-view]:has([data-search-results], [data-search-suggestions]) { - min-height: 15rem; + [data-md-search-view]:has([data-md-search-results], [data-md-search-suggestions]) { + min-height: 240px; } /* No divider: that belongs to the divided style, which Expressive deprecates in favour of the contained one this file draws. The suggestions stand in the same place, before the first keystroke; only one of the two is ever on screen. */ - [data-search-results], - [data-search-suggestions] { + [data-md-search-results], + [data-md-search-suggestions] { min-height: 0; overflow-y: auto; overscroll-behavior: contain; - padding-block: 0.5rem; + padding-block: var(--md-sys-measurement-space100); } /* M3's second entry point: search as a secondary action, one icon button that expands into the full-screen view. The root keeps the button's 48px whether the view is open or not, so a toolbar does not shift under it, and the bar is the view's header rather than a resting bar. */ - [data-search][data-trigger="icon"] { + [data-md-search][data-md-trigger='icon'] { flex: none; - width: 3rem; - height: 3rem; + width: var(--md-sys-measurement-space600); + height: var(--md-sys-measurement-space600); } - [data-search-trigger] { + [data-md-search-trigger] { display: grid; place-items: center; - width: 3rem; - height: 3rem; + width: var(--md-sys-measurement-space600); + height: var(--md-sys-measurement-space600); border-radius: var(--md-sys-shape-corner-full); color: var(--md-sys-color-on-surface-variant); cursor: pointer; @@ -243,45 +253,55 @@ } @media (hover: hover) { - [data-search-trigger]:hover { - background-color: color-mix(in srgb, var(--md-sys-color-on-surface-variant) 8%, transparent); + [data-md-search-trigger]:hover { + background-color: color-mix(in srgb, var(--md-sys-color-on-surface-variant) calc(var(--md-sys-state-hover-state-layer-opacity) * 100%), transparent); } } - [data-search-trigger]:focus-visible { - background-color: color-mix(in srgb, var(--md-sys-color-on-surface-variant) 10%, transparent); + [data-md-search-trigger]:focus-visible { + background-color: color-mix(in srgb, var(--md-sys-color-on-surface-variant) calc(var(--md-sys-state-focus-state-layer-opacity) * 100%), transparent); outline: 3px solid var(--md-sys-color-secondary); outline-offset: -3px; } - [data-search-trigger]:active { - background-color: color-mix(in srgb, var(--md-sys-color-on-surface-variant) 10%, transparent); + [data-md-search-trigger]:active { + background-color: color-mix(in srgb, var(--md-sys-color-on-surface-variant) calc(var(--md-sys-state-pressed-state-layer-opacity) * 100%), transparent); } - [data-search][data-trigger="icon"]:not([data-open]) [data-search-bar] { + [data-md-search][data-md-trigger='icon']:not([data-md-open]) [data-md-search-bar] { display: none; } /* Full screen, on a compact window. */ - [data-search][data-full-screen] [data-search-bar] { + [data-md-search][data-md-full-screen] [data-md-search-bar] { position: fixed; inset: 0 0 auto; width: auto; - height: calc(4.5rem + var(--material-safe-top, env(safe-area-inset-top))); + height: calc(var(--md-sys-measurement-space900) + var(--material-safe-top, env(safe-area-inset-top))); padding-top: var(--material-safe-top, env(safe-area-inset-top)); - padding-inline: 0.25rem; + padding-inline: var(--md-sys-measurement-space50); border-radius: 0; } - [data-search][data-full-screen] [data-search-view] { + [data-md-search][data-md-full-screen] [data-md-search-view] { position: fixed; inset: 0; max-width: none; max-height: none; - padding-top: calc(4.5rem + var(--material-safe-top, env(safe-area-inset-top))); + padding-top: calc(var(--md-sys-measurement-space900) + var(--material-safe-top, env(safe-area-inset-top))); /* The full-screen layout has its own container role, one step from the docked one. */ background-color: var(--md-sys-color-surface-container-low); border-radius: 0; box-shadow: none; } + + /* What the caller says when nothing matches: body-medium in on-surface-variant, padded like a + list row (16px across, 12px down). */ + [data-md-search-empty] { + padding: 12px var(--md-sys-measurement-space200); + color: var(--md-sys-color-on-surface-variant); + font: var(--md-sys-typescale-body-md); + letter-spacing: var(--md-sys-typescale-body-md-tracking); + font-variation-settings: normal; + } } diff --git a/resources/css/tailwind.css b/resources/css/tailwind.css index 69952f2f..32b14226 100644 --- a/resources/css/tailwind.css +++ b/resources/css/tailwind.css @@ -22,7 +22,6 @@ @import './components/menu.css'; /* Inputs, selection and data — leaving in step 36 */ -@import './components/search.css'; @import './components/datepicker.css'; @import './components/timepicker.css'; @import './components/table.css'; diff --git a/resources/js/search.js b/resources/js/search.js index fb59822e..85612782 100644 --- a/resources/js/search.js +++ b/resources/js/search.js @@ -78,11 +78,11 @@ document.addEventListener('alpine:init', () => { } // Suggestions and results never show together; count whichever one is on screen. - const list = [...this.$refs.view.querySelectorAll('[data-search-results], [data-search-suggestions]')] + const list = [...this.$refs.view.querySelectorAll('[data-md-search-results], [data-md-search-suggestions]')] .find((element) => element.getClientRects().length > 0) const items = list ? list.querySelectorAll('[role="listitem"], li') : [] const total = items.length || (list ? this.results().length : 0) - const suggesting = Boolean(list?.hasAttribute('data-search-suggestions')) + const suggesting = Boolean(list?.hasAttribute('data-md-search-suggestions')) this.announcement = total === 0 ? (announce.none ?? '') diff --git a/resources/views/components/search.blade.php b/resources/views/components/search.blade.php index 152fab5f..742ba8af 100644 --- a/resources/views/components/search.blade.php +++ b/resources/views/components/search.blade.php @@ -25,7 +25,11 @@ The bar is a combobox: the wrapper around the input carries `role="combobox"` with `aria-expanded` and `aria-controls`, because ARIA gives a bare textbox neither. The results are a list, and a polite live region says how many of them there are whenever they change — M3 asks - for both (docs/reference/m3/components-navigation-selection-inputs.md § Search). --}} + for both (docs/reference/m3/components-navigation-selection-inputs.md § Search). + + The root renders `data-md-search` with `data-md-trigger`, and `data-md-open` and + `data-md-full-screen` while they hold; the parts are `data-md-search-*`, drawn by + resources/css/components/search.css. `class` and `wire:key` land on the root. --}} @props([ 'placeholder' => null, @@ -56,13 +60,13 @@ x-on:focusout="leave($event)" x-on:pointerdown.outside="close()" x-trap.noscroll="fullScreen" - x-bind:data-open="open ? '' : null" - x-bind:data-full-screen="fullScreen ? '' : null" - data-search - data-trigger="{{ $trigger }}" - {{ $attributes->only(['class', 'wire:key'])->class(['relative']) }} + x-bind:data-md-open="open ? '' : null" + x-bind:data-md-full-screen="fullScreen ? '' : null" + data-md-search + data-md-trigger="{{ $trigger }}" + {{ $attributes->only(['class', 'style', 'wire:key']) }} > -
+ @if ($trigger === 'icon') @endif -{{ $empty }}
{{ $empty }}
No shares match.
') + ->not->toContain('data-md-search-results role="list"'); }); it('offers M3\'s search-icon entry point', function () { $html = (string) $this->blade('