# Expand Toggle

[![npm](https://badge.fury.io/js/%40threespot%2Fexpand-toggle.svg)](https://www.npmjs.com/package/@threespot/expand-toggle)

Simple and accessible expandable functionality, similar to jQuery’s `slideToggle()` method.

Inspired by:

- https://inclusive-components.design/menus-menu-buttons#truemenus
- https://www.stefanjudis.com/snippets/how-to-animate-height-with-css-grid/

## Install

```bash
yarn add @threespot/expand-toggle
```

Requires Node 20+. The package ships as an ES module (`"type": "module"`); consumers using CommonJS will need to load it via dynamic `import()`.

## Usage

**JavaScript**

```js
import ExpandToggle from "@threespot/expand-toggle";

document.querySelectorAll("[data-expands]").forEach(el => new ExpandToggle(el));
```

**Markup**

```html
<button type="button" data-expands="demo" data-expands-class="is-expanded">
  <span data-expands-text="Close">Open</span>
</button>

<div class="expandable" id="demo">
  <div class="expandable-wrap">
    <p>This content will be hidden to start.</p>
  </div>
</div>
```

**Styles**

The package ships a Sass mixin that produces the required styles. The expandable element needs a single child wrapper for `overflow: hidden`; the mixin targets `& > *`.

```scss
@use "@threespot/expand-toggle/expandable.scss" as et;

// The class name is just an example
.expandable {
  @include et.expandable;
}
```

The `.scss` extension is required — the package's `exports` map declares `./expandable.scss` explicitly, so Sass cannot fall back to the partial-style resolution it normally uses for extensionless paths.

Override the animation with two CSS custom properties (defaults: `400ms` and `ease-out`):

```css
:root {
  --expand-speed: 500ms;
  --expand-easing: cubic-bezier(0.4, 0, 0.2, 1);
}
```

The mixin honors `prefers-reduced-motion` and applies the expanded state under `.no-js` so navigation menus remain usable without JavaScript.


### Options

`data-expands-class` defines a class (or multiple classes) to apply to the toggle button and expandable element when expanded

`data-expands-text` defines button text to use when expanded. The element carrying this attribute should contain only text; mixing in icons (e.g. an inline `<svg>`) is not recommended. If you do mix them, only the first text node is swapped on expand/collapse, leaving siblings untouched — but the cleaner pattern is to wrap the swappable copy in its own element (e.g. a nested `<span data-expands-text="Close">Open</span>` next to a sibling icon).

`data-expanded` will expand the element by default

`data-expands-haspopup` opts the toggle into `aria-haspopup` and sets its value (e.g. `data-expands-haspopup="menu"` or `data-expands-haspopup="dialog"`). Omit unless the expandable behaves like a menu, dialog, listbox, tree, or grid — the [WAI-ARIA Disclosure pattern](https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/) does not use `aria-haspopup`.

The following options can be set via JavaScript:

```js
new ExpandToggle(el, {
  expandedClasses: "", // string, accepts multiple space-separated classes
  activeToggleText: "", // expanded state toggle button text
  shouldStartExpanded: false, // component starts expanded on init
  ariaHasPopup: false, // false, true, or an ARIA 1.1 value ("menu", "dialog", "listbox", "tree", "grid")
  onReady: null // ready callback function
});
```


### Events

#### ready

Since the `ready` event may be trigger immediately, bind using the `onReady` option:

```js
const toggle = new ExpandToggle(el, {
  onReady: function() {
    console.log('ready');
  }
});
```

#### expand

Triggered when component is expanded

```js
toggle.on('expand', function() {
  console.log('expand');
});
```

#### collapse

Triggered when component is collapsed

```js
toggle.on('collapse', function() {
  console.log('collapse');
});
```

#### destroy

Triggered when component is destroyed

```js
toggle.on('destroy', function() {
  console.log('destroy');
});
```

## Development

```bash
yarn install
yarn test            # run the suite (node --test against happy-dom)
yarn test:coverage   # same, with experimental test coverage
```

There is no build step. `index.js` is the published source.

## License

This is free software and may be redistributed under the terms of the [MIT license](https://github.com/Threespot/expand-toggle/blob/master/LICENSE.md).

## About Threespot

Threespot is an independent digital agency hell-bent on helping those, and only those, who are committed to helping others. Find out more at [https://www.threespot.com](https://www.threespot.com).

[![Threespot](https://avatars3.githubusercontent.com/u/370822?v=3&s=100)](https://www.threespot.com)
