Draw the search bar and its view as the contained style
Expressive deprecates the divided style, so the results lose their divider; the full-screen layout takes its own surface-container-low, one step from the docked one; the docked view opens over a scrim, as M3's variants table says it does; and the bar is bounded at M3's 720px, grows to that width while focused, and rests with the 24px leading and trailing padding the specs table gives an unfocused bar. --search-width sets the resting width for M3's 360px bar. Plan step 20, findings IN-07, IN-08, IN-09, IN-20 and IN-21. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
66f9d36a37
commit
f30fd575cf
@@ -662,7 +662,7 @@ Bind with `wire:model` (entangled) or, without Livewire, `x-model`. The options
|
|||||||
|
|
||||||
### `<x-search>`
|
### `<x-search>`
|
||||||
|
|
||||||
M3 search bar that opens into a search view: docked under the bar from `medium` (600px), full screen with a back arrow on a compact window (`docked` keeps it docked). Bind the input like any other and render the results in the slot; `empty` is shown when the slot renders nothing. Choosing a result (a link or button) closes the view; ArrowDown walks the results, Escape closes. Props: `placeholder` ("Search"), `label`, `icon`; `trailing` slot (avatar, icon buttons).
|
M3 search bar that opens into a search view: docked under the bar from `medium` (600px) over a scrim, full screen with a back arrow on a compact window (`docked` keeps it docked). Bind the input like any other and render the results in the slot; `empty` is shown when the slot renders nothing. The results are a list and a live region says how many there are. Choosing a result (a link or button) closes the view; ArrowDown walks the results, Escape closes. Props: `placeholder` ("Search"), `label`, `icon`; `trailing` slot (avatar, icon buttons).
|
||||||
|
|
||||||
```blade
|
```blade
|
||||||
<x-search wire:model.live.debounce.300ms="query" placeholder="Search shares">
|
<x-search wire:model.live.debounce.300ms="query" placeholder="Search shares">
|
||||||
@@ -673,7 +673,7 @@ M3 search bar that opens into a search view: docked under the bar from `medium`
|
|||||||
</x-search>
|
</x-search>
|
||||||
```
|
```
|
||||||
|
|
||||||
The docked view overlaps what is under it; never place a search inside an element with `overflow-hidden` (a card), which clips it.
|
The docked view overlaps what is under it; never place a search inside an element with `overflow-hidden` (a card), which clips it. The bar is never wider than M3's 720px and grows to that width while it is focused; for M3's 360px resting bar, wrap it in an element carrying `style="--search-width: 22.5rem"`.
|
||||||
|
|
||||||
### `<x-app-shell>`
|
### `<x-app-shell>`
|
||||||
|
|
||||||
|
|||||||
@@ -4,12 +4,19 @@
|
|||||||
*
|
*
|
||||||
* The bar is a 56px pill in surface-container-high: a leading search icon, the input in
|
* The bar is a 56px pill in surface-container-high: a leading search icon, the input in
|
||||||
* body-large, a clear button once something is typed, and whatever the caller trails it with (an
|
* body-large, a clear button once something is typed, and whatever the caller trails it with (an
|
||||||
* avatar, an icon button). Focus opens the view. On a medium or wider window the view is docked:
|
* avatar, an icon button). Focus opens the view. On a medium or wider window the view is docked
|
||||||
* the bar grows down into an extra-large-cornered container at elevation 3 that holds the
|
* over a scrim: the bar grows down into an extra-large-cornered container at elevation 3 that
|
||||||
* results. On a compact window it takes the whole screen, and the search icon turns into a back
|
* holds the results. On a compact window it takes the whole screen, and the search icon turns into
|
||||||
* arrow. `docked` keeps it docked at every width.
|
* a back arrow. `docked` keeps it docked at every width.
|
||||||
|
*
|
||||||
|
* 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
|
||||||
|
* 16px focused, as the search specs table gives it.
|
||||||
*
|
*
|
||||||
* [data-search] the root; data-open, data-full-screen
|
* [data-search] the root; data-open, data-full-screen
|
||||||
|
* [data-search-scrim] over the page while the view is docked
|
||||||
* [data-search-bar] the pill, above the view
|
* [data-search-bar] the pill, above the view
|
||||||
* [data-search-leading], [data-search-field] (the combobox around [data-search-input]),
|
* [data-search-leading], [data-search-field] (the combobox around [data-search-input]),
|
||||||
* [data-search-clear], [data-search-trailing]
|
* [data-search-clear], [data-search-trailing]
|
||||||
@@ -21,25 +28,56 @@
|
|||||||
@layer components {
|
@layer components {
|
||||||
[data-search] {
|
[data-search] {
|
||||||
--search-height: 3.5rem;
|
--search-height: 3.5rem;
|
||||||
|
/* M3: min 360px, max 720px, and wider while focused. */
|
||||||
|
--search-width: 100%;
|
||||||
|
--search-open-width: 45rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-search][data-open] {
|
[data-search][data-open] {
|
||||||
z-index: 50;
|
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] {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: -1;
|
||||||
|
background-color: color-mix(in srgb, var(--md-sys-color-scrim) 32%, transparent);
|
||||||
|
transition-property: opacity, display;
|
||||||
|
transition-duration: var(--md-sys-motion-effects-fast-duration);
|
||||||
|
transition-timing-function: var(--md-sys-motion-effects-fast);
|
||||||
|
transition-behavior: allow-discrete;
|
||||||
|
}
|
||||||
|
|
||||||
|
@starting-style {
|
||||||
|
[data-search-scrim] {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[data-search-bar] {
|
[data-search-bar] {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.25rem;
|
gap: 0.25rem;
|
||||||
|
width: min(100%, var(--search-width));
|
||||||
height: var(--search-height);
|
height: var(--search-height);
|
||||||
padding-inline: 0.25rem;
|
padding-inline: 0.75rem;
|
||||||
border-radius: var(--md-sys-shape-corner-full);
|
border-radius: var(--md-sys-shape-corner-full);
|
||||||
background-color: var(--md-sys-color-surface-container-high);
|
background-color: var(--md-sys-color-surface-container-high);
|
||||||
color: var(--md-sys-color-on-surface);
|
color: var(--md-sys-color-on-surface);
|
||||||
cursor: text;
|
cursor: text;
|
||||||
transition: background-color var(--md-sys-motion-effects-fast-duration) var(--md-sys-motion-effects-fast);
|
transition-property: background-color, width, padding-inline;
|
||||||
|
transition-duration: var(--md-sys-motion-effects-fast-duration);
|
||||||
|
transition-timing-function: var(--md-sys-motion-effects-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Focused: wider, and the padding drops from 24px to the contained style's 16px. */
|
||||||
|
[data-search][data-open] [data-search-bar] {
|
||||||
|
width: min(100%, var(--search-open-width));
|
||||||
|
padding-inline: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (hover: hover) {
|
@media (hover: hover) {
|
||||||
@@ -143,6 +181,9 @@
|
|||||||
top: 0;
|
top: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
max-width: var(--search-open-width);
|
||||||
|
/* M3: the docked container is at least 240px tall and at most two thirds of the window. */
|
||||||
|
min-height: 15rem;
|
||||||
max-height: min(40rem, 70dvh);
|
max-height: min(40rem, 70dvh);
|
||||||
padding-top: var(--search-height);
|
padding-top: var(--search-height);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -163,11 +204,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* No divider: that belongs to the divided style, which Expressive deprecates in favour of the
|
||||||
|
contained one this file draws. */
|
||||||
[data-search-results] {
|
[data-search-results] {
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overscroll-behavior: contain;
|
overscroll-behavior: contain;
|
||||||
border-top: 1px solid var(--md-sys-color-outline);
|
|
||||||
padding-block: 0.5rem;
|
padding-block: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,6 +217,7 @@
|
|||||||
[data-search][data-full-screen] [data-search-bar] {
|
[data-search][data-full-screen] [data-search-bar] {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0 0 auto;
|
inset: 0 0 auto;
|
||||||
|
width: auto;
|
||||||
height: calc(4.5rem + var(--material-safe-top, env(safe-area-inset-top)));
|
height: calc(4.5rem + var(--material-safe-top, env(safe-area-inset-top)));
|
||||||
padding-top: 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: 0.25rem;
|
||||||
@@ -184,8 +227,11 @@
|
|||||||
[data-search][data-full-screen] [data-search-view] {
|
[data-search][data-full-screen] [data-search-view] {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
|
max-width: none;
|
||||||
max-height: none;
|
max-height: none;
|
||||||
padding-top: calc(4.5rem + var(--material-safe-top, env(safe-area-inset-top)));
|
padding-top: calc(4.5rem + 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;
|
border-radius: 0;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,10 @@
|
|||||||
`empty` is shown instead when the slot renders nothing (say, "No shares match"). Results are
|
`empty` is shown instead when the slot renders nothing (say, "No shares match"). Results are
|
||||||
usually `<x-list-item>`s with a `link`, or buttons: choosing one closes the view.
|
usually `<x-list-item>`s with a `link`, or buttons: choosing one closes the view.
|
||||||
|
|
||||||
Docked under the bar from `medium`, full screen below it with a back arrow (resources/css/
|
Docked under the bar from `medium` over a scrim, full screen below it with a back arrow
|
||||||
components/search.css); `docked` keeps it docked at every width. `placeholder` ("Search"),
|
(resources/css/components/search.css); `docked` keeps it docked at every width. The bar is
|
||||||
|
never wider than M3's 720px and grows to that width while it is focused: set `--search-width`
|
||||||
|
on a wrapper for a narrower resting bar (M3's own is 360px). `placeholder` ("Search"),
|
||||||
`label` (the input's name when it differs from the placeholder), leading `icon` (`search`), and
|
`label` (the input's name when it differs from the placeholder), leading `icon` (`search`), and
|
||||||
a `trailing` slot for an avatar or icon buttons in the bar. Every other attribute reaches the
|
a `trailing` slot for an avatar or icon buttons in the bar. Every other attribute reaches the
|
||||||
`<input type="search">`.
|
`<input type="search">`.
|
||||||
@@ -46,6 +48,8 @@
|
|||||||
data-search
|
data-search
|
||||||
{{ $attributes->only(['class', 'wire:key'])->class(['relative']) }}
|
{{ $attributes->only(['class', 'wire:key'])->class(['relative']) }}
|
||||||
>
|
>
|
||||||
|
<div data-search-scrim x-cloak x-show="open && ! fullScreen" x-on:pointerdown="close()" aria-hidden="true"></div>
|
||||||
|
|
||||||
<div data-search-bar role="search" x-on:click="if ($event.target === $el) $refs.input.focus()">
|
<div data-search-bar role="search" x-on:click="if ($event.target === $el) $refs.input.focus()">
|
||||||
<span data-search-leading x-show="! fullScreen">
|
<span data-search-leading x-show="! fullScreen">
|
||||||
<x-livewire-material::icon :name="$icon" />
|
<x-livewire-material::icon :name="$icon" />
|
||||||
|
|||||||
@@ -126,9 +126,12 @@
|
|||||||
</x-search>
|
</x-search>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<x-search placeholder="Search in settings" docked>
|
{{-- M3's resting bar is 360px and grows to 720 while focused; --search-width sets the resting one. --}}
|
||||||
<x-slot:empty>Type to search your settings.</x-slot:empty>
|
<div style="--search-width: 22.5rem">
|
||||||
</x-search>
|
<x-search placeholder="Search in settings" docked>
|
||||||
|
<x-slot:empty>Type to search your settings.</x-slot:empty>
|
||||||
|
</x-search>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
BLADE,
|
BLADE,
|
||||||
'A form' => <<<'BLADE'
|
'A form' => <<<'BLADE'
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ it('draws a search bar whose combobox controls the view', function () {
|
|||||||
->toContain('wire:model.live.debounce.300ms="query"')
|
->toContain('wire:model.live.debounce.300ms="query"')
|
||||||
->toContain('id="find-view"')
|
->toContain('id="find-view"')
|
||||||
->toContain('data-search-clear')
|
->toContain('data-search-clear')
|
||||||
|
->toContain('data-search-scrim')
|
||||||
->toContain('aria-label="Back"')
|
->toContain('aria-label="Back"')
|
||||||
->toContain('materialSearch(false,')
|
->toContain('materialSearch(false,')
|
||||||
->not->toContain('data-search-results');
|
->not->toContain('data-search-results');
|
||||||
|
|||||||
Reference in New Issue
Block a user