|
|
|
@ -4,7 +4,7 @@ export default class extends Controller { |
|
|
|
static targets = ["dialog"] |
|
|
|
|
|
|
|
connect() { |
|
|
|
this.dialogTarget.showModal() |
|
|
|
this.#openWhenSettled() |
|
|
|
} |
|
|
|
|
|
|
|
close() { |
|
|
|
@ -18,6 +18,75 @@ export default class extends Controller { |
|
|
|
} |
|
|
|
|
|
|
|
remove() { |
|
|
|
this.dispatch("closed", { target: window }) |
|
|
|
this.element.remove() |
|
|
|
} |
|
|
|
|
|
|
|
// The screen underneath may still be animating out. The wave is the last
|
|
|
|
// thing to leave, so waiting on it holds the modal until the exit is done.
|
|
|
|
async #openWhenSettled() { |
|
|
|
const wave = document.querySelector(".is-exiting .hero-container svg.ico-wave") |
|
|
|
if (wave) await this.#settle(wave, "animationend") |
|
|
|
|
|
|
|
if (!this.element.isConnected) return |
|
|
|
|
|
|
|
this.dialogTarget.showModal() |
|
|
|
this.dispatch("opened", { target: window }) |
|
|
|
this.#playEntrance() |
|
|
|
} |
|
|
|
|
|
|
|
// The hero opens full-bleed, pushing the copy and the markers off the bottom.
|
|
|
|
// Once the wave has landed, the carousel and the hero shrink back to their
|
|
|
|
// measured resting heights, which carries everything below up into view.
|
|
|
|
// Sizing both is what hides the slide's own copy: the li clips whatever the
|
|
|
|
// hero pushes past it.
|
|
|
|
async #playEntrance() { |
|
|
|
const panel = this.element.querySelector(".modal-panel") |
|
|
|
const carousel = panel?.querySelector(".carousel") |
|
|
|
if (!carousel) return |
|
|
|
|
|
|
|
const heroes = Array.from(carousel.querySelectorAll(":scope > li > .hero-container")) |
|
|
|
const boxes = [carousel, ...heroes] |
|
|
|
const rest = new Map(boxes.map((box) => [box, box.offsetHeight])) |
|
|
|
const full = this.dialogTarget.clientHeight |
|
|
|
|
|
|
|
panel.classList.add("is-entering") |
|
|
|
boxes.forEach((box) => { |
|
|
|
box.style.transition = "none" |
|
|
|
box.style.height = `${full}px` |
|
|
|
}) |
|
|
|
getComputedStyle(carousel).height |
|
|
|
boxes.forEach((box) => (box.style.transition = "")) |
|
|
|
|
|
|
|
const wave = carousel.querySelector(".hero-container svg.ico-wave") |
|
|
|
if (wave) await this.#settle(wave, "animationend") |
|
|
|
|
|
|
|
boxes.forEach((box) => (box.style.height = `${rest.get(box)}px`)) |
|
|
|
await this.#settle(carousel, "transitionend") |
|
|
|
|
|
|
|
panel.classList.remove("is-entering") |
|
|
|
boxes.forEach((box) => (box.style.height = "")) |
|
|
|
} |
|
|
|
|
|
|
|
// Resolves when the element's animation or transition ends. Safari doesn't
|
|
|
|
// reliably have one registered at the moment we ask for it, so the declared
|
|
|
|
// timing is used as a fallback rather than getAnimations().
|
|
|
|
#settle(element, eventName) { |
|
|
|
return new Promise((resolve) => { |
|
|
|
const style = getComputedStyle(element) |
|
|
|
const seconds = |
|
|
|
eventName === "animationend" |
|
|
|
? parseFloat(style.animationDuration) + parseFloat(style.animationDelay) |
|
|
|
: parseFloat(style.transitionDuration) + parseFloat(style.transitionDelay) |
|
|
|
|
|
|
|
const finish = () => { |
|
|
|
element.removeEventListener(eventName, finish) |
|
|
|
clearTimeout(timer) |
|
|
|
resolve() |
|
|
|
} |
|
|
|
|
|
|
|
const timer = setTimeout(finish, (seconds || 0) * 1000 + 50) |
|
|
|
element.addEventListener(eventName, finish) |
|
|
|
}) |
|
|
|
} |
|
|
|
} |