# WPopover

A flexible popover component for creating dropdown menus, notification panels, user menus, tooltips, and similar overlay patterns.

- [Basic Usage](#basic-usage)
- [Constructor](#constructor)
- [Props](#props)
- [Layout Modes](#layout-modes)
- [Event Handling](#event-handling)
- [State Variants](#state-variants)
- [Styling Examples](#styling-examples)
- [All Supported Classes](#all-supported-classes)
- [Customizing Theme](#customizing-theme)
- [Related Documentation](#related-documentation)

```dart
WPopover(
  alignment: PopoverAlignment.bottomRight,
  className: 'w-64 bg-white dark:bg-gray-800 rounded-xl shadow-xl p-2',
  triggerBuilder: (context, isOpen, isHovering) => WButton(
    className: 'bg-blue-600 text-white',
    child: Text('Open Menu'),
  ),
  contentBuilder: (context, close) => Column(
    mainAxisSize: MainAxisSize.min,
    children: [
      ListTile(title: Text('Profile'), onTap: close),
      ListTile(title: Text('Settings'), onTap: close),
    ],
  ),
)
```

## Basic Usage

The `WPopover` widget uses two builders: `triggerBuilder` for the element that activates the popover, and `contentBuilder` for the overlay content. It manages the overlay state and positioning automatically.

```dart
WPopover(
  triggerBuilder: (context, isOpen, isHovering) => WText(
    'Click me',
    className: 'text-blue-500 cursor-pointer',
  ),
  contentBuilder: (context, close) => WDiv(
    className: 'p-4',
    child: WText('Hello from Popover!'),
  ),
)
```

## Constructor

```dart
const WPopover({
  Key? key,
  required PopoverTriggerBuilder triggerBuilder,
  required PopoverContentBuilder contentBuilder,
  PopoverController? controller,
  bool enableTriggerOnTap = true,
  PopoverAlignment alignment = PopoverAlignment.bottomLeft,
  String? className,
  Offset offset = const Offset(0, 4),
  double maxHeight = 400,
  double? width,
  double? maxWidth,
  bool disabled = false,
  bool closeOnContentTap = false,
  VoidCallback? onOpen,
  VoidCallback? onClose,
  bool autoFlip = true,
})
```

## Props

| Prop | Type | Default | Description |
|:-----|:-----|:--------|:------------|
| `triggerBuilder` | `PopoverTriggerBuilder` | **Required** | Builder for the trigger widget. Receives `isOpen` and `isHovering` states; `hover:`, `focus:` and `disabled:` classes on the trigger also follow the popover. |
| `contentBuilder` | `PopoverContentBuilder` | **Required** | Builder for the popover content. Receives a `close` callback. |
| `className` | `String?` | `null` | Wind utility classes for the popover container styling. |
| `controller` | `PopoverController?` | `null` | Optional controller for programmatic show/hide/toggle. |
| `alignment` | `PopoverAlignment` | `bottomLeft` | Where to position the popover relative to the trigger. |
| `offset` | `Offset` | `Offset(0, 4)` | Gap between the trigger and the popover. |
| `maxHeight` | `double` | `400` | Maximum height for the content. It will scroll if exceeded. With `autoFlip` on, the overlay is also capped at the room left on its side of the trigger, so it never runs off the window. |
| `width` | `double?` | `null` | Pins the overlay to a fixed width (like `WSelect`'s `menuWidth`). Overrides a `w-*` token in `className`. |
| `maxWidth` | `double?` | `null` | Upper bound for the overlay width. Falls back to a `max-w-*` token, then to the screen width, so the overlay never stretches off-screen. Ignored when a fixed `width` (or `w-*`) is set. |
| `enableTriggerOnTap` | `bool` | `true` | Whether tapping the trigger toggles the popover. |
| `closeOnContentTap` | `bool` | `false` | Whether tapping inside the content closes the popover. |
| `disabled` | `bool` | `false` | When true, the trigger will not respond to interactions and is not a Tab stop. |
| `onOpen` | `VoidCallback?` | `null` | Callback fired when the popover is opened. |
| `onClose` | `VoidCallback?` | `null` | Callback fired when the popover is closed. |
| `autoFlip` | `bool` | `true` | When true, the popover flips its alignment to the opposite side if the natural position would render off-screen, and is held inside the viewport: pulled in horizontally, and capped vertically at the room on its side (the content scrolls). |

## Layout Modes

While `WPopover` itself isn't a layout container, it supports various **Alignment Modes** to determine how the overlay is positioned relative to the trigger.

### Alignment

`WPopover` intelligently "flips" the alignment if the requested position would cause the overlay to overflow the screen edges. When neither side has room for `maxHeight`, it keeps the roomier side and shrinks to the space there, 8 pixels short of the edge, so a tall menu scrolls instead of running off a small window. The content has to be able to scroll for that: give the popover `overflow-y-auto` (or build the content on a scroll view), or a plain column overflows once capped.

| Alignment | Description |
|:----------|:------------|
| `bottomLeft` | Below trigger, aligned to left edge (Default). |
| `bottomRight` | Below trigger, aligned to right edge. |
| `bottomCenter` | Below trigger, centered horizontally. |
| `topLeft` | Above trigger, aligned to left edge. |
| `topRight` | Above trigger, aligned to right edge. |
| `topCenter` | Above trigger, centered horizontally. |

## Event Handling

The `triggerBuilder` provides the `isOpen` and `isHovering` states, allowing you to reactively style the trigger. The `contentBuilder` provides a `close` callback to dismiss the popover from within the content (e.g., when a menu item is clicked).

When `enableTriggerOnTap` is `true` (the default), tapping the trigger toggles the popover, and this works even when the trigger is an interactive widget that owns its own `onTap` (a `WButton` or `WAnchor`): the trigger's own callback and the popover toggle both fire on the same tap. Set `enableTriggerOnTap: false` for manual control via a `PopoverController`.

```dart
WPopover(
  triggerBuilder: (context, isOpen, isHovering) => WDiv(
    className: 'p-2 rounded ${isOpen ? "bg-blue-100" : "bg-gray-100"}',
    child: WText(isOpen ? 'Active' : 'Idle'),
  ),
  contentBuilder: (context, close) => WButton(
    onTap: close,
    child: Text('Click to Close'),
  ),
)
```

### Keyboard

The trigger is one Tab stop. Enter, Space, and the Android TV D-pad centre toggle the popover, through the same `ActivateIntent` a `WAnchor` answers, so the keys follow whatever the platform binds to activation. Escape closes an open popover from the trigger or from anything focused inside the panel, returns focus to the trigger, and stops there: one press closes the popover and not also the dialog or sheet holding it, and a page pushed over an open popover keeps its own Escape. While the popover is closed, Escape is left alone and reaches the app's own shortcuts, so a dialog holding a popover still closes on it. After a keyboard open, focus stays on the trigger even on the web, where the browser drops it as the panel mounts. The trigger and the panel form one traversal group in widget order, so the next Tab moves into the panel, top to bottom, whether it opened below the trigger or flipped above it, and Tab past its last control leaves the popover.

A disabled popover is not a Tab stop. With `enableTriggerOnTap: false`, Enter and Space do nothing, as a tap does nothing; Escape still closes a popover the controller opened.

## State Variants

Since the popover trigger is built via `triggerBuilder`, you can apply state-based styling manually or use Wind's state prefixes if the trigger is a Wind widget like `WButton` or `WDiv`.

The popover publishes its trigger's interaction state the way a `WAnchor` does, so a plain `WDiv` trigger answers `hover:`, `focus:` and `disabled:` without an anchor of its own. Give the trigger a `focus:` class, or a keyboard user cannot see where focus is:

```dart
WPopover(
  triggerBuilder: (context, isOpen, isHovering) => WDiv(
    className: '''
      p-2 rounded-lg bg-white dark:bg-gray-800
      focus:ring-2 focus:ring-blue-500
    ''',
    child: WIcon(Icons.notifications_outlined),
  ),
  contentBuilder: (context, close) => MyContent(),
)
```

```dart
WPopover(
  triggerBuilder: (context, isOpen, isHovering) => WButton(
    // Use the isOpen boolean for manual state toggling
    className: 'p-2 rounded-lg ${isOpen ? "bg-blue-600 text-white" : "bg-white text-gray-800"}',
    child: Text('Toggle Menu'),
  ),
  contentBuilder: (context, close) => MyContent(),
)
```

## Styling Examples

### Custom Size and Shadow

If no width is specified in `className`, the popover uses the trigger's width as a minimum. Use sizing utilities to define a specific width.

```dart
WPopover(
  className: 'w-80 bg-white rounded-2xl shadow-2xl border border-gray-100',
  triggerBuilder: (context, isOpen, isHovering) => Icon(Icons.more_horiz),
  contentBuilder: (context, close) => MyLargeMenu(),
)
```

### Dark Mode

`WPopover` fully supports dark mode styling using the `dark:` prefix.

```dart
WPopover(
  className: 'bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-800 shadow-xl',
  triggerBuilder: (context, isOpen, isHovering) => MyTrigger(),
  contentBuilder: (context, close) => MyContent(),
)
```

## All Supported Classes

The popover overlay container is a `WDiv` and supports all standard Wind utilities.

| Category | Classes |
|:---------|:--------|
| Sizing | `w-{size}`, `max-w-{size}`, `h-{size}`, `max-h-{size}` |
| Background | `bg-{color}`, `bg-opacity-{n}` |
| Borders | `border`, `border-{color}`, `rounded-{size}` |
| Shadow | `shadow-{size}`, `shadow-{color}/{opacity}` |
| Padding | `p-{n}`, `px-{n}`, `py-{n}` |

## Customizing Theme

Popovers inherit from global theme scales. You can customize the default appearance by modifying the `WindThemeData`.

```dart
WindThemeData(
  colors: {
    'popover-bg': Colors.white,
  },
  baseSpacingUnit: 4.0,
)
```

## Related Documentation

- [WSelect - A high-level dropdown built using WPopover](https://anilcancakir.com/wind_kit/docs/v1.10/widgets/w-select.md)
- [WDiv - The base container widget](https://anilcancakir.com/wind_kit/docs/v1.10/widgets/w-div.md)
- [WAnchor - Useful for interactive elements inside the popover](https://anilcancakir.com/wind_kit/docs/v1.10/widgets/w-anchor.md)
