Plan step 23 (containment), containment.md § Missing: "no divider pinned between a scrolling body and the header/actions". Every dialog now draws a 1px outline-variant rule under the header once the body is scrolled away from its top, and over the actions while more of it is below; a body that fits shows neither. The rules are pseudo-elements in the rows' own padding (dialog.css), so showing one moves nothing, and x-dialog-dividers (dialog.js) marks the wire:ignore.self <dialog>, which a morph leaves alone, watching scroll and a ResizeObserver on the body and a wrapper around the slot. The M3 gaps are split around the rules (8/8 under the header, 8/16 over the actions). A full-screen dialog's rule sits under its phone bar, and its action bar's always-on border follows the scroll too. `separator` now means "draw both rules always" instead of rendering two <x-divider> elements. The fade uses the effects-fast token, zero under reduced motion. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
51 lines
2.1 KiB
JavaScript
51 lines
2.1 KiB
JavaScript
/**
|
|
* `x-dialog-dividers`, on `<x-modal>`'s body — the one part of a dialog that scrolls: marks the
|
|
* `<dialog>` with `data-overflow-top` while the body is scrolled away from its top, and
|
|
* `data-overflow-bottom` while more of it is below. resources/css/components/dialog.css draws
|
|
* those as the dividers under the pinned header and over the pinned actions; a body that fits
|
|
* marks neither.
|
|
*
|
|
* The marks go on the `<dialog>` rather than the body because the dialog is `wire:ignore.self`: a
|
|
* Livewire morph hands the body back the server's attributes, which would wipe a mark until the
|
|
* next scroll, but it never touches the dialog's own.
|
|
*
|
|
* Measured on scroll, and whenever the body or what is in it changes size: the body when the window
|
|
* or the dialog does (opening, too — a closed dialog has no size, and the observer reports the
|
|
* one it opens to), and the element the component wraps around the slot when a morph, an image or
|
|
* a disclosure makes the content taller or shorter while the body stays at its cap.
|
|
*/
|
|
document.addEventListener('alpine:init', () => {
|
|
window.Alpine.directive('dialog-dividers', (el, _, { cleanup }) => {
|
|
const dialog = el.closest('dialog')
|
|
|
|
if (!dialog) {
|
|
return
|
|
}
|
|
|
|
// A pixel of slack: at a fractional device pixel ratio the end of a scroll can stop a
|
|
// fraction short of `scrollHeight`, which is rounded.
|
|
const measure = () => {
|
|
dialog.toggleAttribute('data-overflow-top', el.scrollTop >= 1)
|
|
dialog.toggleAttribute('data-overflow-bottom', el.scrollHeight - el.clientHeight - el.scrollTop >= 1)
|
|
}
|
|
|
|
const resizes = new ResizeObserver(measure)
|
|
resizes.observe(el)
|
|
|
|
const content = el.querySelector(':scope > [data-dialog-content]')
|
|
|
|
if (content) {
|
|
resizes.observe(content)
|
|
}
|
|
|
|
el.addEventListener('scroll', measure, { passive: true })
|
|
|
|
cleanup(() => {
|
|
resizes.disconnect()
|
|
el.removeEventListener('scroll', measure)
|
|
dialog.removeAttribute('data-overflow-top')
|
|
dialog.removeAttribute('data-overflow-bottom')
|
|
})
|
|
})
|
|
})
|