Spike: [UINC-19] Investigate Stipes challenges with notifications center designs
Spike Results: Stripes Challenges for Notifications Center Design
- 1 Spike Results: Stripes Challenges for Notifications Center Design
- 1.1 Status and Disclaimer
- 1.2 Scope of Investigation
- 1.3 POC Tracks Used for Validation
- 1.4 Visual Overview and Reference Snippets
- 1.5 Current Main Navigation Interaction Modes (Observed)
- 1.5.1 Route mode (route)
- 1.5.2 Href mode (href)
- 1.5.3 Event mode (event)
- 1.5.4 Render mode (render), selected for this POC
- 1.6 Core Challenge Identified
- 1.7 Proposed Direction Validated by Spike
- 1.8 Architecture Boundary Outcome
- 1.9 Why This Matters for Tasks Application
- 1.10 Risks and Unresolved Questions
- 1.11 Recommended Next Decisions
- 1.12 Request for Stripes Force Feedback
- 1.13 References
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:
Stripes core extension track: https://github.com/folio-org/stripes-core/pull/1709
Host application integration track: https://github.com/folio-org/ui-claims/pull/69
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=_blankand 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
MainNavButtonsstate, 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.triggerPropsexposes 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
- Decide whether custom main-nav renderer is accepted as strategic direction.
- Confirm stable contract shape and ownership boundary.
- Define minimal quality and accessibility checklist for modules using this pattern.
- 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.