Skip to content

feat(announcements): open the full announcement from the banner text - #987

Merged
philmerrell merged 1 commit into
developfrom
feature/announcement-banner-opens-detail
Sep 6, 2026
Merged

feat(announcements): open the full announcement from the banner text#987
philmerrell merged 1 commit into
developfrom
feature/announcement-banner-opens-detail

Conversation

@philmerrell

Copy link
Copy Markdown
Contributor

The announcement banner pill renders no body, so its only affordances were a ✕ and an optional external CTA — which trains people to dismiss unread, with What's New (buried in the user menu) as the only other route to the content.

The pill's text is now a button that opens the announcement's own dialog, and the dialog owns the ack from there: any of its exits writes dismissed (or acknowledged) and retires the pill durably, on every device. Having read the body is a stronger signal of consumption than clicking ✕ on a one-line strip, and leaving the pill up afterwards would only ask the user to dismiss something they have already dealt with.

It opens the single-announcement dialog rather than the What's New list deliberately: the pill named one thing, and handing back a list to search through is a worse answer than the thing itself.

Supporting changes

  • AnnouncementModalData.sourceSurface (default modal) attributes every ack to where the gesture happened, so reach stats can tell a banner that earned a read from an interruption nobody asked for. No backend change is needed — AnnouncementSurface is a plain 3-value literal and the ack route never cross-checks it against the announcement's own surfaces. The funnel counters key on action alone, so attribution cannot distort them.
  • AnnouncementModalService.openFor() is now the one entry point for a user-initiated open, and dialog construction lives in a single private method so openRef cannot drift. It skips the §D8 turn-safety gate — a click is not an interruption, the user is the one asking — but still refuses to stack on an open dialog, and marks the item shown so the auto-modal cannot re-interrupt with something already read. disableClose still tracks requiresAck even when the user opened it themselves.
  • A requiresAck announcement gets no ✕ on the pill. This closes a real hole: dismissal suppression is rank-based and covers the banner and modal slots alike, so a ✕ on the strip let a user retire a compliance notice before the blocking modal ever fired — leaving no acknowledged record anywhere. On those, the only way out is to open it and press the button.

Accessibility

The text button uses an sr-only prefix span rather than an aria-label. The visible line may be the summary, so a title-derived label would leave the visible words outside the accessible name — a WCAG 2.5.3 (Label in Name) failure for voice-control users. The untruncated headline stays reachable via title.

Testing

25 new cases across the banner, modal, and modal-service specs, including the ack-attribution matrix, the requiresAck ✕ suppression, and openFor bypassing the gate while still respecting openRef. Full SPA suite: 2510 passed; tsc --noEmit clean.

Not browser-verified — SKIP_AUTH=false locally, so this needs a manual pass. Worth eyeballing: truncation and left-alignment of the text button at ~500px width, the focus ring, and that the banner stays visible behind the dialog until the ack lands.

Spec updated: docs/specs/feature-announcements.md §D1 (banner) and §D2 (the ack's surface records where the gesture happened, not which surface owns the announcement).

🤖 Generated with Claude Code

The banner pill renders no body, so its only affordances were a ✕ and an
optional external CTA — which trains people to dismiss unread, with What's
New (buried in the user menu) as the only other route to the content.

The text is now a button that opens the announcement's own dialog, and the
dialog owns the ack from there: any of its exits writes `dismissed` (or
`acknowledged`) and retires the pill durably. Having read the body is a
stronger signal of consumption than clicking ✕ on a one-line strip, and
leaving the pill up afterwards would only ask the user to dismiss something
they have already dealt with.

It opens the single-announcement dialog rather than the What's New list
deliberately: the pill named one thing, and handing back a list to search
through is a worse answer than the thing itself.

Three supporting changes:

- `AnnouncementModalData.sourceSurface` (default `modal`) attributes every
  ack to where the gesture happened, so reach stats can tell a banner that
  earned a read from an interruption nobody asked for. No backend change:
  `AnnouncementSurface` is a plain literal and the ack route never
  cross-checks it against the announcement's own `surfaces`.
- `AnnouncementModalService.openFor()` is the one entry point for a
  user-initiated open. It skips the §D8 turn-safety gate — a click is not an
  interruption — but still respects `openRef`, and marks the item `shown` so
  the auto-modal cannot re-interrupt with something already read.
- A `requiresAck` announcement gets no ✕ on the pill. Suppression is
  rank-based and covers the banner *and* modal slots alike, so a ✕ there let
  a user retire a compliance notice before the blocking modal ever fired,
  leaving no `acknowledged` record anywhere.

The text button uses an `sr-only` prefix rather than an `aria-label`: the
visible line may be the summary, so a title-derived label would leave the
visible words outside the accessible name (WCAG 2.5.3, Label in Name).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@philmerrell
philmerrell merged commit 71b02d0 into develop Sep 6, 2026
4 checks passed
@philmerrell
philmerrell deleted the feature/announcement-banner-opens-detail branch September 6, 2026 20:41
@philmerrell philmerrell mentioned this pull request Sep 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant