PhoenixKitWeb.Components.Core.ChangeCue (phoenix_kit v2.6.0)

Copy Markdown View Source

Tells the reader that a choice they just made changed something they can't currently see.

Long forms hide detail behind collapsed sections, and a choice in one place often rewrites another: picking a preset rewrites a checklist, enabling an extension adds a permission row. The change is real, correct, and invisible — it lands inside something nobody is looking at.

Deliberately NOT called "flash": Phoenix already has flash messages, and the capability is "something over here changed", not an animation.

Using it

Mark the regions that can be cued — <.accordion cue> does this, or add data-change-region to any container yourself — give the things inside stable DOM ids, and push the ids that changed:

# in the LiveView, after working out what actually changed
{:noreply, ChangeCue.push(socket, ["ext-row-files", "authz-row-upload_files"],
  announce: gettext("Permissions updated"))}

The server says WHAT changed. The client decides how to show it, because only the client knows what is on screen:

  • the region is open → the changed rows highlight;
  • the region is closed → the region highlights AND keeps a quiet "changed" marker. Opening it replays the row highlights and clears the marker.

The marker is what makes this survive reality: a highlight that plays while the reader is scrolled elsewhere is simply lost, and a timer that forgets after N seconds turns "what changed here?" into a race. The marker persists until the region is opened.

What it will not do

Scroll the page, open a section, move focus, or raise a toast. A cue that moves the page under someone mid-form is worse than no cue.

Accessibility

A pulse tells a screen-reader user nothing, so a closed-region cue also writes to a polite live region — the region's own words (:announce), once per push, coalesced. Never assertive, never per-row, and never repeated when the deferred highlights replay.

prefers-reduced-motion keeps the outline and the marker, drops the pulse.

Summary

Functions

The marker shown on a closed region that changed while it was closed.

Tells the client a region has nothing to show any more — its state went back to what the reader last saw. Clears the marker and any rows queued for replay.

The client event name, exposed so consumers can assert on it.

Cues the elements whose ids are given.

The event a cued region pushes when the reader OPENS it, carrying %{"region" => id}.

Functions

change_marker(assigns)

The marker shown on a closed region that changed while it was closed.

Rendered inside the region's own summary by <.accordion cue>; it stays hidden until the client sets data-changed on the region, and the CSS lives in the same stylesheet as the highlight so a consumer adds nothing.

Attributes

  • label (:string) - Defaults to nil.

clear(socket, regions)

Tells the client a region has nothing to show any more — its state went back to what the reader last saw. Clears the marker and any rows queued for replay.

event()

@spec event() :: String.t()

The client event name, exposed so consumers can assert on it.

push(socket, ids, opts \\ [])

@spec push(
  Phoenix.LiveView.Socket.t(),
  [String.t()] | %{required(String.t()) => [String.t()]},
  keyword()
) :: Phoenix.LiveView.Socket.t()

Cues the elements whose ids are given.

Two forms:

# ids alone — the client finds each one's region itself
ChangeCue.push(socket, ["ext-row-files"])

# grouped by the region that CONTAINS them
ChangeCue.push(socket, %{"create-people" => ["authz-row-comment"]})

Group them when a change can make the element disappear. A row that was removed cannot be found, and neither can the region around it, so a plain id yields no cue at all — precisely when something visibly vanished and the reader most needs telling. With a region named, the client falls back to cueing that.

This is not the server deciding how to show the change: the client still chooses rows-versus-region from what is actually on screen. The region is provenance the DOM can no longer answer for itself.

Options:

  • :announce — what a screen reader should hear when the change lands in a CLOSED region. Say the result ("Permissions updated"), not the mechanics. Omit it and nothing is announced.

A push with nothing in it is a no-op, so callers can pipe an empty diff through without a conditional.

seen_event()

@spec seen_event() :: String.t()

The event a cued region pushes when the reader OPENS it, carrying %{"region" => id}.

Handle it and reset that region's baseline:

def handle_event(ChangeCue.seen_event(), %{"region" => region}, socket) do
  {:noreply, mark_region_seen(socket, region)}
end

Without it a marker means "something happened", which accumulates into a lie: flip a preset back and forth and every section ends up marked even though nothing differs from where you started. Diff against what the reader last SAW, and returning to the original state clears the marks by itself.