Spike: [UINC-19] Investigate Stipes challenges with notifications center designs

Spike: [UINC-19] Investigate Stipes challenges with notifications center designs

Spike Results: Stripes Challenges for Notifications Center Design

Status and Disclaimer

This document captures results of a Proof of Concept (PoC) spike. It is intentionally exploratory and should not be treated as a final architecture contract. Alternative implementation and architecture proposals are welcome.

Scope of Investigation

The spike evaluated how to support Notifications Center behavior from Main Navigation while preserving Stripes visual and accessibility consistency.

Focus areas:

  • main navigation trigger ownership and behavior

  • support for badges and unread counters

  • anchored interaction near trigger (popover, dock, panel)

  • module and core ownership boundaries

  • forward compatibility with future Tasks application expectations

POC Tracks Used for Validation

The POC was validated across two implementation tracks:

Live POC environment:

Visual Overview and Reference Snippets

┌──────────────────────────────────────────────────┐ │ module package.json │ │ stripes.links.mainNavigation.render │ └─────────────────────────┬────────────────────────┘ ┌──────────────────────────────────────────────────┐ │ Root static method │ │ renderNotificationsCenter │ └─────────────────────────┬────────────────────────┘ ┌──────────────────────────────────────────────────┐ │ stripes-core │ │ MainNavButtons │ └─────────────────────────┬────────────────────────┘ ┌──────────────────────────────────────────────────┐ │ renderFn │ │ renderTrigger + triggerProps │ └─────────────────────────┬────────────────────────┘ ┌──────────────────────────────────────────────────┐ │ module container │ │ state, badge, overlay lifecycle │ └──────────────┬───────────────────────────────────┘ │ │ ▼ ▼ ┌──────────────────────────┐ ┌─────────────────────────────┐ │ pure content component │ │ backend notifications │ │ │ │ polling / WebSocket / push │ └──────────────────────────┘ └─────────────────────────────┘

Reference snippet: declarative registration in package.json

{ "stripes": { "links": { "mainNavigation": [ { "render": "renderNotificationsCenter", "icon": "flag", "caption": "ui-notifications-center.meta.title", "check": "checkNotificationsVisibility" } ] } } }

This is the declarative entry point. The module does not register a route interaction here. Instead, it registers a renderer name that core resolves from the module root.

Reference snippet: static binding on module root

class Root extends React.Component<Props> { static readonly checkNotificationsVisibility = checkNotificationsVisibility; static readonly renderNotificationsCenter = renderNotificationsCenter; }

This is the runtime bridge between manifest-level configuration and module-level implementation.

Reference snippet: module render function

interface RenderProps { renderTrigger?: (extraProps?: Record<string, unknown>) => React.ReactNode; triggerProps?: Record<string, unknown>; } export const renderNotificationsCenter = (props: RenderProps = {}) => { return <NotificationsCenterMainNav {...props} />; };

This is the key difference versus route, href, and event: the module receives a trigger-rendering contract rather than a pre-defined navigation behavior. That makes it possible to connect live notification state, unread badges, overlay anchoring, and future subscription logic in one module-owned component boundary.

Current Main Navigation Interaction Modes (Observed)

The spike reviewed practical behavior of the existing navigation interaction modes supported by NavButton and MainNavButtons in stripes-core.

All four modes are declarative: a module registers its main navigation entry in package.json under stripes.links.mainNavigation, and MainNavButtons resolves the correct interaction from the link configuration at runtime.

Route mode (route)

The link config contains a route path. MainNavButtons resolves this into a to prop and renders NavButton as a React Router Link. Clicking navigates within the SPA without a page reload.

Strengths:

  • Covers the standard FOLIO application pattern cleanly.

  • No state management required on module side.

  • Consistent active/selected state detection based on route.

Limitations for Notifications Center:

  • Route change as the interaction model does not map naturally to an anchored control surface.

  • Badge and unread count cannot be driven reactively because the trigger is owned entirely by routing logic.

  • The panel content would need to live at a persistent route, which conflicts with the expected overlay/dock interaction pattern.

  • Receiving real-time notification events and updating UI state is not addressable by route config alone.

Href mode (href)

The link config contains an href string. MainNavButtons resolves this into an href prop and renders NavButton as a native anchor element. Used in production today for the Help button.

Strengths:

  • Simple and zero-overhead for external resources.

  • Supports target=_blank and other anchor attributes naturally.

Limitations for Notifications Center:

  • Not applicable for in-app reactive surfaces.

  • No mechanism to inject dynamic state such as badge counts.

  • No mechanism to receive or react to internal notification events.

Event mode (event)

The link config contains an event string. On click, MainNavButtons calls handleEvent(event, stripes, module, data) from handlerService. Handler modules that react to the event may return a React component, which is then mounted into the DOM alongside the nav.

Strengths:

  • Supports cross-module behavior through event interception.

  • Allows side effects and optionally renders a component into the page.

  • Historically used for similar open-something interactions.

Limitations for Notifications Center:

  • Trigger rendering is controlled by core; extra props such as badge, aria-expanded, and stable ref for anchor positioning cannot easily be injected from the event handler.

  • The handler component is mounted globally via a map in MainNavButtons state, which creates indirect ownership and lifecycle coupling.

  • There is no straightforward path for the module to connect incoming notification events to the trigger badge or to overlay open/close without additional workarounds.

  • The indirection makes it harder to reason about ownership: who reacts to the event, how the component is mounted, and how state is communicated back to the trigger.

  • Anchoring a popover or dock to a specific trigger element requires a stable ref; the event model does not naturally expose or stabilize this ref across the handler boundary.

Render mode (render), selected for this POC

The link config contains a render key that resolves to a static method on the module root class. MainNavButtons calls this renderer with a trigger contract and renders whatever the module returns.

Core Challenge Identified

Notifications Center is not only a destination route. It is a persistent interactive surface in main navigation: always visible, always reactive, and capable of showing dynamic state.

The trigger must:

  • look and behave identically to other Stripes main-nav buttons

  • show a live unread badge driven by actual notification state

  • open and close a contextual overlay anchored to the trigger element

  • maintain correct accessibility semantics (aria-expanded, aria-haspopup, focus management)

  • expose a stable DOM ref so overlay positioning logic can anchor relative to the trigger

Beyond initial render, the module must also be prepared to receive incoming notification events from a backend source (WebSocket, polling, or push) and update both the badge and the panel content reactively.

This makes the trigger not just a clickable element but the external-facing control surface of a running notification subscription. The module must be in full control of this surface to wire it correctly to live data.

None of the existing modes cover this end-to-end:

  • route treats the trigger as a pure navigation item with no dynamic state

  • href is static and has no reactive capabilities

  • event provides a click side-effect path but no mechanism to continuously push state updates back to the trigger or to anchor overlays reliably

A different contract is needed: one where the module retains full control over trigger behavior, badge state, and overlay lifecycle while still rendering a Stripes-native button through core.

Proposed Direction Validated by Spike

Use a render entry in Main Navigation registration, resolved from module root, with a trigger rendering contract provided by Stripes core.

This direction is conceptually based on the existing declarative links.userDropdown extension point implemented in stripes-core through ProfileDropdown: ProfileDropdown.js. The model also leverages a checks layer similar to userDropdownLinksService.

The important similarity is the overall pattern: modules contribute navigation metadata under stripes.links, and core resolves and renders those contributions at runtime. The main difference is that userDropdown resolves a menu item model, while the Notifications Center POC needs a trigger-rendering contract because the component must stay under module control for badge state, overlay lifecycle, and future notification-event subscription handling.

Validated contract shape:

renderFn({ renderTrigger, triggerProps })

Where:

  • renderTrigger(extraProps?) renders a canonical Stripes NavButton with baseline props already set (id, icon, aria-label, ref). The module spreads additional interaction props on top.

  • triggerProps exposes the same resolved props object for cases where the module needs direct access, for example reading the ref to pass it as overlay anchor.

Why this is the right choice for Notifications Center specifically

  • Full control over badge state. The module owns when and how the badge is rendered and with what value; notification events update internal state and thus the trigger immediately.

  • Stable trigger ref for overlay anchoring. The ref is resolved and stabilized by core and exposed via triggerProps, enabling precise anchoring.

  • Explicit open/close lifecycle owned by the module. The module manages onClick, open/close, outside clicks, Escape handling, and updates aria-expanded.

  • Direct path for real-time notification data. Backend events update module state which drives both badge and panel content within a single component tree.

  • Separation of trigger contract from overlay technology. Core provides the trigger; the module decides Popper, Dock, Drawer, etc.

  • Forward compatibility with Tasks-like patterns. Works for any main-nav trigger requiring non-route behavior.

What this contract deliberately leaves out of core

  • No overlay rendering logic inside stripes-core

  • No badge state management in core

  • No subscription or event-reception infrastructure in core

  • No assumptions about data source (polling, WebSocket, etc.)

This keeps stripes-core responsible only for canonical trigger rendering and a safe contract exposing that trigger to modules.

Architecture Boundary Outcome

What should remain in stripes-core

  • main-nav button baseline rendering and style

  • baseline accessibility semantics of trigger

  • registration and execution pipeline for custom nav renderer

What should remain in UI module

  • open and close behavior

  • contextual content rendering

  • badge and unread state lifecycle

  • outside click and escape handling

  • overlay composition strategy

This boundary proved practical for the POC and helps avoid over-specializing stripes-core around a single feature domain.

Why This Matters for Tasks Application

The same pattern is relevant for future Tasks UX where a main-nav entry may also represent an interaction surface rather than a pure route transition. The spike outcome suggests a generic extension strategy is preferable to a Notifications-only implementation.

Risks and Unresolved Questions

  • Should this renderer contract become official API in current shape?

  • Should stricter typing be introduced for renderer arguments and return behavior?

  • Should there be platform-level guidance for accessibility of nav-anchored overlays?

Recommended Next Decisions

  1. Decide whether custom main-nav renderer is accepted as strategic direction.
  2. Confirm stable contract shape and ownership boundary.
  3. Define minimal quality and accessibility checklist for modules using this pattern.
  4. Validate the same contract with at least one non-notifications scenario (Tasks-like).

Request for Stripes Force Feedback

This is a POC spike result, not a final recommendation. The Stripes Force team is invited to propose alternative or improved architecture options, especially for:

  • extension point naming and discoverability

  • long-term API stability and typing

  • consistency and accessibility governance for trigger-driven nav interactions

All architecture and implementation suggestions are welcome.

References