Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

## [Unreleased]

### Added

- Add Floating panel element

### Fixed

- Export tabs element from `package.json` file
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import 'tailwindcss-elements/elements/dropdown';

- [Dialog](./packages/core/src/elements/dialog/README.md)
- [Dropdown](./packages/core/src/elements/dropdown/README.md)
- [Floating panel](./packages/core/src/elements/floating_panel/README.md)
- [Popover](./packages/core/src/elements/popover/README.md)
- [Switch](./packages/core/src/elements/switch/README.md)
- [Tabs](./packages/core/src/elements/tabs/README.md)
Expand Down
6 changes: 6 additions & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@
"types": "./dist/elements/dropdown/index.d.ts",
"default": "./dist/elements/dropdown/index.js"
},
"./elements/floating_panel": {
"types": "./dist/elements/floating_panel/index.d.ts",
"default": "./dist/elements/floating_panel/index.js"
},
"./elements/popover": {
"types": "./dist/elements/popover/index.d.ts",
"default": "./dist/elements/popover/index.js"
Expand Down Expand Up @@ -60,6 +64,8 @@
},
"dependencies": {
"@ambiki/impulse": "^0.2.0",
"@floating-ui/dom": "^1.5.4",
"composed-offset-position": "^0.0.4",
"tabbable": "^6.2.0"
}
}
18 changes: 18 additions & 0 deletions packages/core/src/elements/dropdown/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,24 @@ element.open = true;
element.open = false;
```

### Positioning the panel

By default, the positioning logic is not taken care of when you use the `twc-dropdown` element. But, you can do so by
wrapping the trigger and panel with the [`twc-floating-panel`](../floating_panel/README.md) element.

```html
<twc-dropdown class="relative">
<twc-floating-panel>
<button type="button" data-target="twc-dropdown.trigger twc-floating-panel.trigger">Toggle dropdown</button>
<div data-target="twc-dropdown.menu twc-floating-panel.panel" class="absolute hidden data-[headlessui-state='open']:block">
<a href="/dashboard" data-target="twc-dropdown.menuItems">Dashboard</a>
<a href="/settings" data-target="twc-dropdown.menuItems">Settings</a>
<a href="/profile" data-target="twc-dropdown.menuItems">Profile</a>
</div>
</twc-floating-panel>
</twc-dropdown>
```

## Styling different states

Each component exposes the `data-headlessui-state` attribute that you can use to conditionally apply the classes. You
Expand Down
22 changes: 22 additions & 0 deletions packages/core/src/elements/dropdown/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { expect, fixture, html } from '@open-wc/testing';
import { sendKeys, sendMouse } from '@web/test-runner-commands';
import Sinon from 'sinon';
import FloatingPanelElement from '../floating_panel';
import DropdownElement from './index';

function assertDropdownShown(el: DropdownElement, trigger: Element, menu: Element) {
Expand Down Expand Up @@ -489,4 +490,25 @@ describe('Dropdown', () => {
el.deactivateAllMenuItems();
expect(el.activeMenuItem).to.be.undefined;
});

it('starts and stops the positioning logic', async () => {
const el = await fixture<DropdownElement>(html`
<twc-dropdown>
<twc-floating-panel>
<button type="button" data-target="twc-dropdown.trigger twc-floating-panel.trigger">Button</button>
<div data-target="twc-dropdown.menu twc-floating-panel.panel">
<a href="#" data-target="twc-dropdown.menuItems">First</a>
</div>
</twc-floating-panel>
</twc-dropdown>
`);

const floatingPanel = el.querySelector('twc-floating-panel')! as FloatingPanelElement;

el.open = true;
expect(floatingPanel).to.have.attribute('active');

el.open = false;
expect(floatingPanel).not.to.have.attribute('active');
});
});
10 changes: 10 additions & 0 deletions packages/core/src/elements/dropdown/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,18 @@ export default class DropdownElement extends ImpulseElement {

openChanged(value: boolean) {
if (value) {
if (this.floatingPanel) {
this.floatingPanel.active = true;
}
this.syncState(true);
} else {
this._focusTrap?.abort();
this.syncState(false);
this.menu.removeAttribute('aria-activedescendant');
this.deactivateAllMenuItems();
if (this.floatingPanel) {
this.floatingPanel.active = false;
}
}
}

Expand Down Expand Up @@ -294,6 +300,10 @@ export default class DropdownElement extends ImpulseElement {
return this.menuItems.filter((menuItem) => !isDisabled(menuItem));
}

get floatingPanel() {
return this.querySelector('twc-floating-panel');
}

private syncState(state: boolean) {
this.trigger.setAttribute('aria-expanded', state.toString());
this.trigger.setAttribute('data-headlessui-state', state ? 'open' : '');
Expand Down
97 changes: 97 additions & 0 deletions packages/core/src/elements/floating_panel/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# Floating panel

A thin wrapper around [Floating UI](https://floating-ui.com/) that positions the floating panel with respect to the
anchor (trigger).

## Imports

```js
import 'tailwindcss-elements/elements/floating_panel';
```

## Usage

```html
<twc-floating-panel>
<button type="button" data-target="twc-floating-panel.trigger">Anchor</button>
<div data-target="twc-floating-panel.panel">Contents!</div>
</twc-floating-panel>
```

```js
const element = document.querySelector('twc-floating-panel');

// Start the positioning logic
element.active = true;

// Stop the positioning logic
element.active = false;

// Update the position of the panel
await element.position();
```

## Examples

### Setting the initial position

By default, the initial placement is set to `bottom-start`, but you can set it to one of `top`, `top-start`, `top-end`,
`right`, `right-start`, `right-end`, `bottom`, `bottom-start`, `bottom-end`, `left`, `left-start`, or `left-end` by
setting the `placement` attibute on the element.

```html
<twc-floating-panel placement="top-start">
...
</twc-floating-panel>
```

For more info, visit the [official docs](https://floating-ui.com/docs/computePosition#placement).

### Setting the positioning strategy

By default, the positioning strategy is set as `absolute`, but you can set it to one of `absolute` or `fixed` by
setting the `strategy` attribute on the element.

```html
<twc-floating-panel strategy="fixed">
...
</twc-floating-panel>
```

For more info, visit the [official docs](https://floating-ui.com/docs/computeposition#strategy).

### Adding space between the trigger and the panel element

You can set the `offset-options` attribute as a number of a JSON object.

```html
<twc-floating-panel offset-options="10">
...
</twc-floating-panel>

<twc-floating-panel offset-options='{"mainAxis": 10, "crossAxis": 20}'>
...
</twc-floating-panel>
```

For more info, visit the [official docs](https://floating-ui.com/docs/offset).

### Syncing the width, height, or both with respect to the trigger element

You can set the `sync` attribute on the element which accepts one of `width`, `height`, or `both`.

- `width` -> Make the width of the panel equal to that of the trigger.
- `height` -> Make the height of the panel equal to that of the trigger.
- `both` -> Make both the width and height of the panel equal to that of the trigger.

```html
<twc-floating-panel sync="width">
...
</twc-floating-panel>
```

## Events

| Name | Bubbles | Description |
| ------ | --------- | ------------ |
| `twc-floating-panel:changed` | `true` | This event fires when the panel is positioned with respect to the trigger element. |
73 changes: 73 additions & 0 deletions packages/core/src/elements/floating_panel/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { expect, fixture, html, waitUntil } from '@open-wc/testing';
import FloatingPanelElement from './index';
import Sinon from 'sinon';

describe('Floating panel', () => {
it('starts and stops the positioning of the panel', async () => {
const el = await fixture<FloatingPanelElement>(html`
<twc-floating-panel>
<button type="button" data-target="twc-floating-panel.trigger">Trigger</button>
<div data-target="twc-floating-panel.panel"></div>
</twc-floating-panel>
`);

const panel = el.querySelector('div')!;

expect(el).not.to.have.attribute('active');
expect(el).not.to.have.attribute('data-current-placement');

el.active = true;
expect(el).to.have.attribute('active');
await waitUntil(() => el.hasAttribute('data-current-placement'));
expect(el).to.have.attribute('data-current-placement', 'bottom-start');
expect(panel).to.have.style('position', 'absolute');

el.active = false;
expect(el).not.to.have.attribute('active');
expect(el).not.to.have.attribute('data-current-placement');
});

it('setting the initial placement', async () => {
const el = await fixture<FloatingPanelElement>(html`
<twc-floating-panel placement="bottom-end">
<button type="button" data-target="twc-floating-panel.trigger">Trigger</button>
<div data-target="twc-floating-panel.panel"></div>
</twc-floating-panel>
`);

el.active = true;
await waitUntil(() => el.hasAttribute('data-current-placement'));
expect(el).to.have.attribute('data-current-placement', 'bottom-end');
});

it('setting the positioning strategy', async () => {
const el = await fixture<FloatingPanelElement>(html`
<twc-floating-panel strategy="fixed">
<button type="button" data-target="twc-floating-panel.trigger">Trigger</button>
<div data-target="twc-floating-panel.panel"></div>
</twc-floating-panel>
`);

const panel = el.querySelector('div')!;

el.active = true;
await waitUntil(() => el.hasAttribute('data-current-placement'));
expect(panel).to.have.style('position', 'fixed');
});

it('emits the changed event after positioning the panel', async () => {
const el = await fixture<FloatingPanelElement>(html`
<twc-floating-panel>
<button type="button" data-target="twc-floating-panel.trigger">Trigger</button>
<div data-target="twc-floating-panel.panel"></div>
</twc-floating-panel>
`);

const handler = Sinon.spy();
document.addEventListener(`${el.identifier}:changed`, handler);

el.active = true;
await waitUntil(() => el.hasAttribute('data-current-placement'));
expect(handler.called).to.be.true;
});
});
Loading