Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.

Note that the versions prior to `0.6.0` are very unstable.

## [Unreleased]

### Fixed

- The `#client-error` and `#server-error` connection notices no longer appear, and stay on screen, when the connection is restored before `client_error_delay` runs out.

## [v0.11.0] (2026-09-09)

### Changed
Expand Down
18 changes: 13 additions & 5 deletions assets/js/live_toast/live_toast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -418,9 +418,13 @@ export function createLiveToastHook(duration = 6000, maxItems = 3) {
return
}

this.el.addEventListener('show-error', async _event => {
// Pending reveal of the connection notice. It must be cancelled by
// 'hide-error', otherwise a connection that comes back within the delay
// still reveals the notice afterwards and nothing ever hides it again.
let pendingShow: ReturnType<typeof setTimeout> | undefined

this.el.addEventListener('show-error', _event => {
const delayTime = Number.parseInt(this.el.dataset.delay || '0')
await new Promise(resolve => setTimeout(resolve, delayTime))

// todo: in the future use this to execute the data-disconnected command
// https://elixirforum.com/t/can-we-use-liveview-js-commands-inside-a-hook/67324/8
Expand All @@ -431,11 +435,15 @@ export function createLiveToastHook(duration = 6000, maxItems = 3) {
// (don't want to do this quite yet because 1.0 is pretty new)
// also repeat this on hide.

this.el.removeAttribute('hidden')
this.el.style.display = 'flex'
clearTimeout(pendingShow)
pendingShow = setTimeout(() => {
this.el.removeAttribute('hidden')
this.el.style.display = 'flex'
}, delayTime)
})

this.el.addEventListener('hide-error', async _event => {
this.el.addEventListener('hide-error', _event => {
clearTimeout(pendingShow)
this.el.style.display = 'none'
})

Expand Down
17 changes: 15 additions & 2 deletions assets/test/live_toast.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,13 +117,13 @@ function mountToast(duration: number | 'Infinity' = 1000, countdown = false) {
}
}

function mountConnectionToast() {
function mountConnectionToast(delay = 0) {
document.body.innerHTML = `
<div
id="client-error"
phx-hook="LiveToast"
data-corner="bottom_right"
data-delay="0"
data-delay="${delay}"
data-duration="0"
hidden
></div>
Expand Down Expand Up @@ -261,6 +261,19 @@ describe('LiveToast timed dismissal', () => {
expect(toast.el.style.display).toBe('flex')
})

test('does not show a connection error when the connection is restored within the delay', async () => {
const toast = mountConnectionToast(3000)

toast.el.dispatchEvent(new Event('show-error'))
advance(1000)
toast.el.dispatchEvent(new Event('hide-error'))
advance(3000)
await settle()

expect(toast.el.hidden).toBe(true)
expect(toast.el.style.display).toBe('none')
})

test('cancels dismissal when the hook is destroyed', () => {
const toast = mountToast()

Expand Down
14 changes: 9 additions & 5 deletions priv/static/live_toast.cjs.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions priv/static/live_toast.cjs.js.map

Large diffs are not rendered by default.

14 changes: 9 additions & 5 deletions priv/static/live_toast.esm.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions priv/static/live_toast.esm.js.map

Large diffs are not rendered by default.

18 changes: 11 additions & 7 deletions priv/static/live_toast.js
Original file line number Diff line number Diff line change
Expand Up @@ -1037,15 +1037,19 @@ var LiveMotion = (() => {
this.el.addEventListener(clientToastEvent, clientToastListener);
return;
}
this.el.addEventListener("show-error", (_event) => __async(this, null, function* () {
let pendingShow;
this.el.addEventListener("show-error", (_event) => {
const delayTime = Number.parseInt(this.el.dataset.delay || "0");
yield new Promise((resolve) => setTimeout(resolve, delayTime));
this.el.removeAttribute("hidden");
this.el.style.display = "flex";
}));
this.el.addEventListener("hide-error", (_event) => __async(this, null, function* () {
clearTimeout(pendingShow);
pendingShow = setTimeout(() => {
this.el.removeAttribute("hidden");
this.el.style.display = "flex";
}, delayTime);
});
this.el.addEventListener("hide-error", (_event) => {
clearTimeout(pendingShow);
this.el.style.display = "none";
}));
});
if (["server-error", "client-error"].includes(this.el.id)) {
if (isHidden(document.getElementById(this.el.id))) {
return;
Expand Down
Loading