poppo

repository·main·Indexed 41 days ago

https://github.com/alantoa/universal-tooltip

A unified library for Tooltips, Popovers, and Toasts for Expo and React Native, with web support via Base UI. It provides native popup windows on iOS and Android to ensure content is never clipped by parent views and remains interactive. Version 3.0.0.

Tokens
9.9K
Snippets
25
Records
42
Agent score
52%

What's inside poppo

  1. Compare Tooltip vs. Popover

    main

    While Tooltip and Popover share a similar structure, they serve different purposes and behave differently regarding user interaction and accessibility:

    FeatureTooltipPopover
    Content takes touchesNoYes
    Pressing contentDismisses (unless disableTapToDismiss is set)Handled by the content itself
    Assistive techAnnounced as a hintTreated as a modal surface

    Best Practice: Always put buttons inside a Popover, never a Tooltip.

  2. How Tooltip and Popover components are structured

    main

    Both Tooltip and Popover share the same component anatomy. They use a compound component pattern where the Root component manages the state and reads configuration from its children.

    Important: Do not wrap the sub-components (like Positioner or Popup) in your own custom components. Root uses React context/composition to find these elements, and custom wrappers can prevent Root from detecting them.

    Anatomy:

    • <Root>: Manages the open state.
    • <Trigger />: The element that initiates the popup (via hover or press).
    • <Portal>: Renders the content outside the parent hierarchy (to avoid clipping).
    • <Positioner>: Controls positioning via side and sideOffset.
    • <Popup>: The actual content bubble.
    • <Arrow />: The directional pointer for the bubble.
  3. Platform-specific notes for Native and Web

    main

    Native

    • Interactivity: Popover content remains interactive (gestures, text inputs, onPress work) using mechanisms similar to React Native's Modal.
    • Positioning: The popup follows its trigger during scrolling and closes when the trigger is no longer visible. It automatically flips to the opposite side if the preferred side lacks space, maintaining an 8pt clearance from the display edge.
    • Limitations:
      • No automatic focus management (focus is not moved into the popover or restored on close).
      • No Android back-button handling.
      • No ref forwarding or asChild support on native. Trigger renders its own Pressable around children.

    Web

    • Tooltip and Popover are powered by Base UI. The required stylesheet is automatically imported by the web entry.
    • Important: If using Metro, ensure inlineRequires is disabled for web builds to avoid recoverable React errors during the first render.
  4. How Toast management works

    main

    Toasts follow a provider-based architecture designed for mobile notifications.

    The Hierarchy:

    1. <Toast.Provider>: The central manager. It accepts configuration for timeout, limit (max visible toasts), and overflow (how to handle new toasts when the limit is reached, e.g., replace or queue).
    2. <Toast.Viewport>: The visual container where toasts are stacked. It accepts position and insets (for safe areas).
    3. <Toast.Root>: Represents a single toast instance. It provides built-in support for enter/exit animations, swipe-to-dismiss gestures, and pausing the auto-dismiss timer when the user is touching the toast.
    4. Toast Content: Inside Toast.Root, you compose the UI using <Toast.Title />, <Toast.Description />, <Toast.Action />, and <Toast.Close />.
  5. Run the example playground and tests

    main

    The example/ directory contains a playground app used for testing. Use the following commands to run the test suite or the app:

    yarn test      # Run toast manager schedule tests (Jest)
    cd example
    yarn ios       # Run on iOS
    yarn android    # Run on Android
    yarn test:ios  # Run XCUITest suite (real touches against real popups)
    yarn test
    cd example
    yarn ios
    yarn test:ios
  6. Style Tooltip and Popover popups

    main

    You can style popups using standard React Native ViewStyle and TextStyle properties.

    Native Platform Behavior

    On native, the rendering path depends on the children of Tooltip.Popup:

    1. String children: These are drawn by the native bubble. This path allows the bubble to measure itself against the screen rather than the trigger, preventing text squeezing. Supported properties are: backgroundColor, borderRadius, padding*, width/maxWidth, fontSize, color, fontWeight, and fontFamily. Other properties are ignored.
    2. Other children (Components/Views): These are rendered by React Native inside the popup window. In this mode, every style and component works. Note: You must give the outermost view its own background and radius, as that view acts as the bubble.

    Web Platform Behavior

    On web, all ViewStyle and TextStyle properties work as expected.

    Toasts

    Toasts are ordinary React Native views on all platforms and can be styled freely.

    <Tooltip.Popup
      style={{
        backgroundColor: "rgba(54,58,62,0.85)",
        borderRadius: 12,
        paddingHorizontal: 20,
        paddingVertical: 12,
        maxWidth: 240,
        fontSize: 14,
        lineHeight: 21,
        color: "#fff",
      }}
    >
      Drag to reschedule
      <Tooltip.Arrow width={14} height={8} />
    </Tooltip.Popup>
  7. Install poppo for Expo and React Native

    main

    Install the core package using Expo:

    npx expo install poppo

    Native Dependencies

    Toasts require react-native-reanimated and react-native-gesture-handler. You must wrap your application root with GestureHandlerRootView:

    import { GestureHandlerRootView } from "react-native-gesture-handler";
    
    <GestureHandlerRootView style={{ flex: 1 }}>
      <App />
    </GestureHandlerRootView>;

    Web Dependency

    For web support, you must also install @base-ui/react:

    yarn add @base-ui/react

    Note: Expo SDK 53+ handles native requirements (iOS 16.4+, Swift 5.9) automatically via autolinking.

    npx expo install poppo
    npx expo install react-native-reanimated react-native-gesture-handler
    yarn add @base-ui/react
  8. Migrate from universal-tooltip to poppo

    main

    As of version 2.0, universal-tooltip has been renamed to poppo. The universal-tooltip package now only serves as a re-export of poppo to maintain backward compatibility. For new projects or to access the latest features and fixes, you should install and use poppo directly. The native module is included in poppo, and Expo autolinking will handle it as a dependency.

    # Install the new package
    yarn add poppo
    - import { Tooltip } from "universal-tooltip";
    + import { Tooltip } from "poppo";
  9. Configure Anchored Popup components (Tooltip and Popover)

    main

    Anchored popups (either tooltip or popover) are composed of several specialized components. You can use the AnchoredPopupComponents interface to access the following parts:

    • Root: The main container that manages the open/closed state. Supports controlled (open) and uncontrolled (defaultOpen) modes.
    • Trigger: The element that activates the popup. On web, it supports delay and closeDelay for hover interactions.
    • Portal: Handles rendering the popup into a different part of the DOM tree (Web only).
    • Positioner: Controls where the popup appears relative to the trigger using side, sideOffset, and align.
    • Popup: The actual content bubble. Supports animations (presetAnimation) and custom styling.
    • Arrow: An optional visual pointer attached to the popup.
    // Conceptual structure of an anchored popup
    <Root open={isOpen} onOpenChange={setIsOpen}>
      <Trigger>
        <Button />
      </Trigger>
      <Portal>
        <Positioner side="top" sideOffset={10}>
          <Popup presetAnimation="fadeIn">
            <Arrow />
            <Text>Hello World</Text>
          </Popup>
        </Positioner>
      </Portal>
    </Root>
  10. How toast overflow and demotion work

    main

    The ToastManager handles high volumes of toasts using two main mechanisms:

    Overflow Strategies

    When the limit of visible toasts is reached, the overflow setting determines what happens to new arrivals:

    • queue (default): New toasts are placed in a queue and will only become visible once an existing toast is removed.
    • replace: The oldest visible toast is immediately closed to make room for the new toast (similar to Android Snackbars).

    Demotion

    To prevent a long stack of toasts from staying visible for too long, the manager uses demotedTimeout. When a new toast arrives and becomes the front-most item, all other visible toasts are 'demoted'. Their remaining auto-dismiss time is capped at the demotedTimeout value. This ensures the stack clears more quickly when many toasts are triggered in rapid succession.

  11. Use the Popover component

    main

    The Popover component is an anchored popup designed for interactive content that opens on click or press. It is composed of several sub-components that work together to manage positioning and rendering:

    • Root: The main controller that manages the open/closed state.
    • Trigger: The element that initiates the popover (e.g., a button).
    • Portal: Handles rendering the popover content outside the local DOM hierarchy (often at the document body level).
    • Positioner: Manages the physical placement of the popup relative to the trigger.
    • Popup: The actual container for the content being displayed.
    • Arrow: An optional visual indicator pointing from the popup to the trigger.
    import { Popover } from './popover';
    
    // The Popover component is a set of coordinated components:
    // <Popover.Root>
    //   <Popover.Trigger>Click me</Popover.Trigger>
    //   <Popover.Portal>
    //     <Popover.Positioner>
    //       <Popover.Popup>Content here</Popover.Popup>
    //       <Popover.Arrow />
    //     </Popover.Positioner>
    //   </Popover.Portal>
    // </Popover.Root>
  12. Configure the Toast Provider

    main

    Wrap your application (or a specific part of it) in <Toast.Provider> to enable toast functionality. You can provide an external toastManager created via createToastManager(), or let the provider create its own internal manager with custom settings.

    Key configuration options for the internal manager:

    • timeout: Default auto-dismiss timeout in ms (default: 5000).
    • limit: Maximum number of toasts visible at once (default: Infinity).
    • overflow: Behavior when limit is reached. Options: 'queue' (shows when a slot frees up) or 'replace' (closes oldest visible toast to show new one).
    • demotedTimeout: Ceiling in ms on how long a toast has left once pushed back in the stack (default: 2000).
    import { Provider } from './toast';
    
    <Provider 
      timeout={3000} 
      limit={3} 
      overflow="queue"
    >
      <App />
    </Provider>