fix: Replace platform-wide broadcast_refresh with sync toast (#1964)

* fix: Replace platform-wide broadcast_refresh with sync toast

Instead of calling family.broadcast_refresh on every sync completion
(which reloads the page for all connected family members), broadcast
a lightweight static toast to the existing notification-tray.

A new sync-toast Stimulus controller handles two cases:
- User is idle (no focused form): auto-reloads after 500ms
- User is mid-form: toast stays visible with a manual Refresh button

This prevents in-progress form state from being wiped when a background
sync fires (e.g. adding a transaction, filling an import form).

The toast partial contains no user-scoped data, so the Current.user nil
constraint in background jobs is no longer a concern.

* fix(a11y): add explicit button types and aria-label to sync toast controls

* fix(sync-toast): improve interaction detection and replace broadcast strategy

- Increase auto-refresh delay from 500ms to 2000ms
- Expand interaction detection to include contentEditable, dialogs, and role="dialog" elements
- Switch from broadcast_append_to to broadcast_replace_to with dedicated #sync-toast target
- Add explicit id="sync-toast" to partial for targeted replacement
- Move sync_toast i18n keys from defaults/en.yml to views/shared/en.yml

* fix(sync-toast): replace hardcoded white icon color with inverse token
This commit is contained in:
Abhinav Dhiman
2026-05-31 19:35:54 +05:30
committed by GitHub
parent 9ccc2719c1
commit cc070853b7
5 changed files with 73 additions and 5 deletions

View File

@@ -0,0 +1,33 @@
import { Controller } from "@hotwired/stimulus";
// Connects to data-controller="sync-toast"
//
// Shown when a background sync completes and the family's data changes.
// - If the user is not interacting with a form, auto-reloads after a short delay.
// - If the user is mid-form, the toast stays visible so they can choose when to refresh.
export default class extends Controller {
static values = {
autoRefreshDelay: { type: Number, default: 2000 },
};
connect() {
if (!this.#userIsInteracting()) {
this._timer = setTimeout(() => this.refresh(), this.autoRefreshDelayValue);
}
}
disconnect() {
clearTimeout(this._timer);
}
refresh() {
clearTimeout(this._timer);
window.location.reload();
}
#userIsInteracting() {
const el = document.activeElement;
if (!el || el === document.body || el === document.documentElement) return false;
return el.isContentEditable || el.closest("form, dialog, [role='dialog']") !== null;
}
}