/** * `x-dialog-dividers`, on ``'s body — the one part of a dialog that scrolls: marks the * `` with `data-md-overflow-top` while the body is scrolled away from its top, and * `data-md-overflow-bottom` while more of it is below. resources/css/components/modal.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 ``, which is `wire:ignore.self`, so a Livewire morph never wipes one * until the next scroll; the body is `wire:ignore.self` too, for `materialShowModal()`'s * `tabindex`, but the dividers are the dialog's frame and modal.css reads them there. * * 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. */ /** * `materialShowModal(dialog)`, how `` opens: `showModal()`, and then the first focus Chrome * gives a text-only dialog and Firefox and WebKit do not. With nothing focusable inside, * `showModal()` focuses the `` itself, the HTML fallback; Chrome's scroll containers are * keyboard-focusable, so there the scrolling body is the focus delegate instead. A focused dialog * leaves a long body unscrollable from the keyboard — the arrow keys scroll what has the focus, and * WebKit never lets Tab reach a scroll container — so when the dialog took the focus itself and its * body overflows, the body is made focusable and takes it, as in Chrome, where modal.css draws the * inset ring. The `tabindex` goes when the dialog closes, so each opening decides afresh. */ window.materialShowModal = (dialog) => { dialog.showModal() const body = dialog.querySelector(':scope > [data-md-modal-box] > [data-md-modal-body]') if (document.activeElement !== dialog || !body || body.scrollHeight - body.clientHeight < 1) { return } body.tabIndex = 0 body.focus() dialog.addEventListener('close', () => body.removeAttribute('tabindex'), { once: true }) } 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-md-overflow-top', el.scrollTop >= 1) dialog.toggleAttribute('data-md-overflow-bottom', el.scrollHeight - el.clientHeight - el.scrollTop >= 1) } const resizes = new ResizeObserver(measure) resizes.observe(el) const content = el.querySelector(':scope > [data-md-modal-content]') if (content) { resizes.observe(content) } el.addEventListener('scroll', measure, { passive: true }) cleanup(() => { resizes.disconnect() el.removeEventListener('scroll', measure) dialog.removeAttribute('data-md-overflow-top') dialog.removeAttribute('data-md-overflow-bottom') }) }) })