# Dropdown

A dropdown menu displays a list of actions and more with JavaScript dropdown plugin.

[![npm](https://img.shields.io/badge/npm-v4.2.0-blue)](https://www.npmjs.com/package/@preline/dropdown) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) [![Demo](https://img.shields.io/badge/demo-online-brightgreen)](https://preline.co/plugins/dropdown.html)

## Contents

- [Overview](#overview)
- [Installation](#installation)
- [Basic usage](#basic-usage)
- [Accessibility notes](#accessibility-notes)
- [Configuration Options](#configuration-options)
- [JavaScript API](#javascript-api)
- [Events](#events)
- [Common Patterns](#common-patterns)
- [License](#license)

## Overview

The Dropdown component provides a menu that appears when triggered, displaying a list of actions or options. It supports multiple placement options, customizable triggers, keyboard navigation, and integrates with Floating UI for advanced positioning.

**Key Features:**
- Multiple placement options (top, bottom, left, right, auto)
- Customizable trigger events (click, hover, contextmenu)
- Floating UI integration for smart positioning
- Keyboard navigation support (Arrow keys, Home, End, Esc, A-Z)
- Programmatic control via JavaScript API
- Event system for lifecycle hooks
- Accessibility attributes (ARIA) built-in

## Installation

To get started, install Dropdown plugin via npm, else you can skip this step if you are already using Preline UI as a package.

```bash
npm i @preline/dropdown
```

### CSS

Use [`@source`](https://tailwindcss.com/docs/functions-and-directives#source-directive) to register the plugin's JavaScript path for Tailwind CSS scanning, then [`@import`](https://tailwindcss.com/docs/functions-and-directives#import-directive) the plugin's CSS files into your Tailwind CSS file.

```css
@import "tailwindcss";

/* @preline/dropdown */
@source "../node_modules/@preline/dropdown/*.js";
@import "./node_modules/@preline/dropdown/variants.css";
@import "./node_modules/@preline/dropdown/theme.css";
```

### JavaScript

Include the JavaScript that powers the interactive elements near the end of your `</body>` tag:

```html
<script src="./node_modules/@preline/dropdown/index.js"></script>
```

### Manual Initialization

Use the `non-auto` entry if you need manual initialization. In this mode, automatic initialization on page load is not included, so the component should be initialized explicitly.

```html
<script type="module">
  import HSDropdown from "@preline/dropdown/non-auto.mjs";

  new HSDropdown(document.querySelector("#dropdown"));
</script>
```

### Via Bundler

When using a bundler (Vite, webpack, etc.), import the plugin directly as an ES module.

`@preline/dropdown` is the auto-init entry: it scans the DOM and initializes matching elements automatically.

```js
import "@preline/dropdown";
```

`@preline/dropdown/non-auto` is the manual entry: use it when you want explicit control over when initialization happens, either via `autoInit()` or by creating a specific instance yourself.

```js
import HSDropdown from "@preline/dropdown/non-auto";

HSDropdown.autoInit();

// Or initialize a specific element manually
const el = document.querySelector("#dropdown");
if (el) new HSDropdown(el);
```

### TypeScript

This package ships with TypeScript type definitions. No additional `@types/` package is needed.

## Basic usage

The following example demonstrates the minimal HTML structure required for a dropdown component. This is a base template without custom styling - you can apply your own CSS classes and styles as needed. The dropdown menu appears when clicking the button.

```html
<div class="hs-dropdown relative inline-flex">
  <button id="hs-dropdown-unstyled" type="button" class="hs-dropdown-toggle inline-flex justify-center items-center gap-x-2" aria-expanded="false" aria-label="Menu">
    Actions
  </button>

  <div class="hs-dropdown-menu transition-[opacity,margin] duration hs-dropdown-open:opacity-100 opacity-0 w-56 hidden z-10 mt-2 min-w-60 bg-white" role="menu" aria-labelledby="hs-dropdown-unstyled">
    <a class="block" href="#">Newsletter</a>
    <a class="block" href="#">Purchases</a>
    <a class="block" href="#">Downloads</a>
    <a class="block" href="#">Team Account</a>
  </div>
</div>
```

**Structure Requirements:**
- `hs-dropdown`: Required container wrapper
- `hs-dropdown-toggle`: Required button element that triggers the dropdown
- `hs-dropdown-menu`: Required container for dropdown menu items
- Proper ARIA attributes (`aria-expanded`, `aria-label`, `role="menu"`, `aria-labelledby`)

**Initial State:**
- Set `aria-expanded="false"` on the toggle button initially
- Add `hidden` class to the dropdown menu initially
- Menu visibility is controlled by `hs-dropdown-open:*` modifier classes

## Accessibility notes

### Keyboard interactions

| Command | Description |
| --- | --- |
| Enter | Activates the selected menu item. |
| ArrowUp / ArrowDown | Focuses previous/next non-disabled item. |
| Home / End | Focuses first/last non-disabled item. |
| Esc | Closes any open dropdown menus. |
| A-Z / a-z | Focuses first item that matches keyboard input. |

## Configuration Options

### Data Attributes

Data attributes are added directly to HTML elements to configure dropdown behavior.

| Attribute | Target Element | Type | Default | Description |
| --- | --- | --- | --- | --- |
| `data-hs-dropdown-transition` | `hs-dropdown` (container) | - | - | Data attribute to designate the container to be animated. When present, enables transition animations for the dropdown menu. |

### CSS Classes (Modifiers)

CSS class modifiers use Tailwind-style syntax with `--` prefix to control dropdown behavior.

| Class Modifier | Target Element | Values | Default | Description |
| --- | --- | --- | --- | --- |
| `[--placement:*]` | `hs-dropdown` (container) | `"auto"` \| `"auto-start"` \| `"auto-end"` \| `"top"` \| `"top-left"` \| `"top-right"` \| `"bottom"` \| `"bottom-left"` \| `"bottom-right"` \| `"right"` \| `"right-start"` \| `"right-end"` \| `"left"` \| `"left-start"` \| `"left-end"` | `"bottom"` | Specifies the position of the menu when opened relative to the toggle button. |
| `[--scope:*]` | `hs-dropdown` (container) | `"window"` \| `"parent"` | `"parent"` | Determines whether the dropdown will be moved outside the parent, for correct display in elements with hidden overflow. Requires the `Floating UI` plugin. |
| `[--auto-close:*]` | `hs-dropdown` (container) | `"inside"` \| `"outside"` \| `"false"` \| `"true"` | `"true"` | Specifies the zone, when clicked, which will close the menu. `inside` closes only when clicking inside, `outside` closes only when clicking outside, `true` closes on any click, `false` prevents auto-close. |
| `[--has-autofocus:*]` | `hs-dropdown` (container) | `"true"` \| `"false"` | `"true"` | Disables autofocus on the first focusable element when opening a dropdown. Set to `"false"` to disable autofocus. |
| `[--strategy:*]` | `hs-dropdown` (container) | `"fixed"` \| `"absolute"` | `"fixed"` | Sets the position strategy. `fixed` positions relative to viewport, `absolute` positions relative to the nearest positioned ancestor. |
| `[--adaptive:*]` | `hs-dropdown` (container) | `"none"` \| `"adaptive"` | `"adaptive"` | Used to disable horizontal position calculations (useful for dropdown menus that span the full width of the screen) while still calculating the vertical position. |
| `[--gpu-acceleration:*]` | `hs-dropdown` (container) | `"true"` \| `"false"` | `"false"` | Disable/enable position calculation using the transform property. Set to `"true"` to enable GPU acceleration. |
| `[--offset:*]` | `hs-dropdown` (container) | number | `10` | Sets the dropdown's bottom or top offset in pixels. |
| `[--flip:*]` | `hs-dropdown` (container) | `"true"` \| `"false"` | `"true"` | Flips the menu's placement when it starts to overlap its reference element. Set to `"false"` to disable flipping. |
| `[--trigger:*]` | `hs-dropdown` (container) | `"hover"` \| `"click"` \| `"contextmenu"` | `"click"` | Event to trigger a dropdown. `hover` shows on mouse enter, `click` shows on click, `contextmenu` shows on right-click. |

**Example:**
```html
<div class="hs-dropdown --trigger:hover --placement:top --auto-close:false">
  <button class="hs-dropdown-toggle">Hover me</button>
  <div class="hs-dropdown-menu">Menu content</div>
</div>
```

### Required CSS Classes

These classes define the structure and must be present for the dropdown to function.

| Class | Required On | Purpose |
| --- | --- | --- |
| `hs-dropdown` | Container wrapper | Groups the toggle and menu elements together |
| `hs-dropdown-toggle` | Button element | The clickable element that activates the dropdown |
| `hs-dropdown-menu` | Menu container | The dropdown menu that appears on trigger |

### Optional CSS Classes

| Class | Required On | Purpose |
| --- | --- | --- |
| `hs-dropdown-toggle-wrapper` | Wrapper element | A wrapper for a Dropdown toggle, useful when some other element is placed in the Dropdown toggle. For example, if you want to place a "+" button inside an existing Dropdown toggle button that opens a modal. |
| `hs-dropdown-close` | Close element | Dropdown close element (can be multiple). Elements with this class will close the dropdown when clicked. |

### Tailwind Modifiers

| Name | Description |
| --- | --- |
| `hs-dropdown-open:*` | The modifier that allows you to set Tailwind classes when the dropdown menu is open. |

## JavaScript API

The `HSDropdown` object is available in the global `window` object after the plugin is loaded.

### Instance Methods

These methods are called on a dropdown instance.

| Method | Parameters | Return Type | Description |
| --- | --- | --- | --- |
| `open(target, openedViaKeyboard)` | `target`: `VirtualElement \| HTMLElement` (optional)<br>`openedViaKeyboard`: `boolean` (optional) | `boolean` | Forces the dropdown menu to open programmatically. Returns `true` if opened successfully, `false` otherwise. |
| `close(isAnimated)` | `isAnimated`: `boolean` (optional) | `boolean` | Forces the dropdown menu to close programmatically. `isAnimated` indicates whether the dropdown menu is closed with animation. Returns `true` if closed successfully, `false` otherwise. |
| `forceClearState()` | None | `void` | Destroys dropdown state without removing the instance. Use to reset dropdown to initial state. |
| `destroy()` | None | `void` | Destroys the dropdown instance, removes all generated markup, classes, and event listeners. Use when removing dropdown from DOM. |

### Static Methods

These methods are called directly on the `HSDropdown` class.

| Method | Parameters | Return Type | Description |
| --- | --- | --- | --- |
| `HSDropdown.getInstance(target, isInstance)` | `target`: `HTMLElement \| string` (CSS selector)<br>`isInstance`: `boolean` (optional) | `HSDropdown \| { id: string \| number, element: HSDropdown } \| null` | Returns the dropdown instance associated with the target. If `isInstance` is `true`, returns collection item object `{ id, element }` where `element` is the `HSDropdown` instance. If `isInstance` is `false` or omitted, returns the `HSDropdown` instance directly. Returns `null` if dropdown instance is not found. |
| `HSDropdown.open(target, openedViaKeyboard)` | `target`: `HSDropdown \| HTMLElement \| string` (CSS selector)<br>`openedViaKeyboard`: `boolean` (optional) | `void` | Opens the dropdown identified by target. Accepts a dropdown instance, DOM element, or CSS selector string. Static alternative to instance `open()` method. |
| `HSDropdown.close(target)` | `target`: `HSDropdown \| HTMLElement \| string` (CSS selector) | `void` | Closes the dropdown identified by target. Accepts a dropdown instance, DOM element, or CSS selector string. Static alternative to instance `close()` method. |

### Usage Examples

**Example 1: Using instance methods (public API)**
```javascript
// Create a new dropdown instance
const dropdown = new HSDropdown(document.querySelector('#hs-dropdown'));
const openBtn = document.querySelector('#hs-open-btn');

// Open dropdown programmatically
openBtn.addEventListener('click', () => {
  dropdown.open();
});
```

**Example 2: Using static methods**
```javascript
const openBtn = document.querySelector('#hs-open-btn');

// Open dropdown using static method (no instance needed)
openBtn.addEventListener('click', () => {
  HSDropdown.open('#hs-dropdown');
});
```

**Example 3: Getting instance and using methods (recommended pattern)**
```javascript
// Get the dropdown instance
const instance = HSDropdown.getInstance('#hs-dropdown', true);

if (instance) {
  const { element } = instance; // element is the HSDropdown instance
  const openBtn = document.querySelector('#hs-open-btn');

  // Use instance methods
  openBtn.addEventListener('click', () => {
    element.open();
  });

  // Clean up when removing from DOM
  function removeDropdown() {
    element.destroy();
  }
}
```

**Example 4: Destroying dropdown instance**
```javascript
const instance = HSDropdown.getInstance('#hs-dropdown', true);

if (instance) {
  const { element } = instance;
  const removeBtn = document.querySelector('#hs-remove-btn');

  removeBtn.addEventListener('click', () => {
    // Clean up before removing from DOM
    element.destroy();
    // Now safe to remove the element
    document.querySelector('#hs-dropdown').remove();
  });
}
```

## Events

Dropdown instances emit events that can be listened to for lifecycle hooks and custom behavior.

| Event Name | When Fired | Callback Parameter | Description |
| --- | --- | --- | --- |
| `on:open` | After dropdown menu is opened | `HSDropdown` (dropdown instance) | Fires after the dropdown menu has been displayed. Use for post-open actions like loading content or tracking analytics. |
| `on:close` | After dropdown menu is closed | `HSDropdown` (dropdown instance) | Fires after the dropdown menu has been hidden. Use for cleanup or state updates. |

### Event Usage Example

```javascript
// Get dropdown instance
const instance = HSDropdown.getInstance('#hs-dropdown', true);

if (instance) {
  const { element } = instance;

  // Listen to open event
  element.on('open', (dropdownInstance) => {
    console.log('Dropdown opened:', dropdownInstance);
    // Perform actions after dropdown opens
    // e.g., load content, update UI, track analytics
  });

  // Listen to close event
  element.on('close', (dropdownInstance) => {
    console.log('Dropdown closed:', dropdownInstance);
    // Perform cleanup or state updates
  });
}
```

## Common Patterns

### Pattern 1: Hover Trigger

Show dropdown on hover instead of click.

```html
<div class="hs-dropdown --trigger:hover">
  <button class="hs-dropdown-toggle">Hover me</button>
  <div class="hs-dropdown-menu">Menu content</div>
</div>
```

### Pattern 2: Programmatic Control

Control dropdown from external buttons.

```html
<div class="hs-dropdown" id="hs-dropdown-first">
  <button class="hs-dropdown-toggle">Actions</button>
  <div class="hs-dropdown-menu">Menu content</div>
</div>

<button id="hs-open-dropdown">Open Dropdown</button>
<button id="hs-close-dropdown">Close Dropdown</button>

<script>
  const instance = HSDropdown.getInstance('#hs-dropdown-first', true);
  
  if (instance) {
    const { element } = instance;

    document.querySelector('#hs-open-dropdown').addEventListener('click', () => {
      element.open();
    });

    document.querySelector('#hs-close-dropdown').addEventListener('click', () => {
      element.close();
    });
  }
</script>
```

## License

Copyright (c) 2024 Htmlstream.

Licensed under the [MIT License](https://opensource.org/licenses/MIT).
