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]

### Changed

- Use `popover` attribute in the popover element

### Fixed

- Do not click menuitem again
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/elements/dialog/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,12 +193,12 @@ describe('Dialog', async () => {
const dialog = twcDialog.querySelector('dialog')!;

trigger.click();
expect(el).to.have.attribute('open');
expect(el.open).to.eq(true);

twcDialog.open = true;
assertDialogShown(twcDialog, dialog);

await sendKeys({ press: 'Escape' });
expect(el).to.have.attribute('open'); // Should not close the popover.
expect(el.open).to.eq(true); // Should not close the popover.
});
});
32 changes: 13 additions & 19 deletions packages/core/src/elements/popover/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,27 +11,21 @@ import 'tailwindcss-elements/elements/popover';
## Usage

```html
<twc-popover class="relative">
<twc-popover>
<button data-target="twc-popover.trigger" type="button">Toggle popover</button>
<div data-target="twc-popover.panel" class="absolute hidden data-[headlessui-state='open']:block">
<div data-target="twc-popover.panel">
Popover contents!
</div>
</twc-popover>
```

[![Edit Popover](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/p/sandbox/popover-yyvw2d)

## Examples

### Default to the open state
**Note:** The popover element uses the [`popover`](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/popover)
attribute and comes with a [polyfill](https://github.com/oddbird/popover-polyfill). Therefore, you should not add any
hidden classes to the popover' panel as it is managed by the browser.

You can set the `open` attribute on the element and it will be open by default.

```html
<twc-popover open>
...
</twc-popover>
```
## Examples

### Programmatically toggling the visibility state

Expand All @@ -45,10 +39,10 @@ You can set the `open` attribute on the element and it will be open by default.
const element = document.querySelector('twc-popover');

// Show
element.open = true;
element.show();

// Hide
element.open = false;
element.hide();
```

### Adding a close button inside the popover
Expand All @@ -59,7 +53,7 @@ will close the parent popover.
```html
<twc-popover>
<button data-target="twc-popover.trigger" type="button">Toggle popover</button>
<div data-target="twc-popover.panel" class="hidden data-[headlessui-state='open']:block">
<div data-target="twc-popover.panel">
Popover contents!
<button type="button" data-action="click->twc-popover#hide">x</button>
</div>
Expand All @@ -71,12 +65,12 @@ will close the parent popover.
```html
<twc-popover>
<button data-target="twc-popover.trigger" type="button">Toggle popover</button>
<div data-target="twc-popover.panel" class="hidden data-[headlessui-state='open']:block">
<div data-target="twc-popover.panel">
Look another popover!

<twc-popover>
<button data-target="twc-popover.trigger" type="button">Toggle nested popover</button>
<div data-target="twc-popover.panel" class="hidden data-[headlessui-state='open']:block">
<div data-target="twc-popover.panel">
Nested popover contents!
</div>
</twc-popover>
Expand All @@ -90,10 +84,10 @@ By default, the positioning logic is not taken care of when you use the `twc-pop
wrapping the trigger and panel with the [`twc-floating-panel`](../floating_panel/README.md) element.

```html
<twc-popover class="relative">
<twc-popover>
<twc-floating-panel>
<button data-target="twc-popover.trigger twc-floating-panel.trigger" type="button">Toggle popover</button>
<div data-target="twc-popover.panel twc-floating-panel.panel" class="absolute hidden data-[headlessui-state='open']:block">
<div data-target="twc-popover.panel twc-floating-panel.panel">
Popover contents!
</div>
</twc-floating-panel>
Expand Down
148 changes: 83 additions & 65 deletions packages/core/src/elements/popover/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,13 @@ import Sinon from 'sinon';
import FloatingPanelElement from '../floating_panel';
import PopoverElement from './index';

function assertPopoverShown(el: PopoverElement, trigger: HTMLButtonElement, panel: Element) {
expect(el).to.have.attribute('open');
function assertPopoverShown(trigger: HTMLButtonElement, panel: Element) {
expect(trigger).to.have.attribute('data-headlessui-state', 'open');
expect(trigger).to.have.attribute('aria-expanded', 'true');
expect(panel).to.have.attribute('data-headlessui-state', 'open');
}

function assertPopoverHidden(el: PopoverElement, trigger: HTMLButtonElement, panel: Element) {
expect(el).not.to.have.attribute('open');
function assertPopoverHidden(trigger: HTMLButtonElement, panel: Element) {
expect(trigger).to.have.attribute('data-headlessui-state', '');
expect(trigger).to.have.attribute('aria-expanded', 'false');
expect(panel).to.have.attribute('data-headlessui-state', '');
Expand All @@ -27,8 +25,6 @@ describe('Popover', () => {
</twc-popover>
`);

expect(el).not.to.have.attribute('open');

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

Expand Down Expand Up @@ -61,12 +57,12 @@ describe('Popover', () => {
const panel = el.querySelector('div')!;

trigger.click();
assertPopoverShown(el, trigger, panel);
assertPopoverShown(trigger, panel);
expect(document.activeElement).to.eq(panel);
expect(shownHandler.calledOnce).to.be.true;

trigger.click();
assertPopoverHidden(el, trigger, panel);
assertPopoverHidden(trigger, panel);
expect(document.activeElement).to.eq(trigger);
expect(hiddenHandler.calledOnce).to.be.true;
});
Expand All @@ -86,11 +82,11 @@ describe('Popover', () => {
const panel = el.querySelector('div')!;

trigger.click();
assertPopoverShown(el, trigger, panel);
assertPopoverShown(trigger, panel);
expect(document.activeElement).to.eq(panel);

await sendKeys({ press: 'Escape' });
assertPopoverHidden(el, trigger, panel);
assertPopoverHidden(trigger, panel);
expect(document.activeElement).to.eq(trigger);
expect(hiddenHandler.calledOnce).to.be.true;
});
Expand All @@ -112,11 +108,11 @@ describe('Popover', () => {
const closeButton = el.querySelector<HTMLButtonElement>('[data-test-id="close-btn"]')!;

trigger.click();
assertPopoverShown(el, trigger, panel);
assertPopoverShown(trigger, panel);
expect(document.activeElement).to.eq(closeButton);

closeButton.click();
assertPopoverHidden(el, trigger, panel);
assertPopoverHidden(trigger, panel);
expect(document.activeElement).to.eq(trigger);
expect(hiddenHandler.calledOnce).to.be.true;
});
Expand All @@ -136,36 +132,10 @@ describe('Popover', () => {
const panel = el.querySelector('div')!;

trigger.click();
assertPopoverShown(el, trigger, panel);
assertPopoverShown(trigger, panel);

el.hide();
assertPopoverHidden(el, trigger, panel);
expect(hiddenHandler.called).to.be.false;
});

it('toggles the popover when setting the open attribute', async () => {
const el = await fixture<PopoverElement>(html`
<twc-popover>
<button type="button" data-target="twc-popover.trigger">Toggle</button>
<div data-target="twc-popover.panel"></div>
</twc-popover>
`);

const shownHandler = Sinon.spy();
el.addEventListener(`${el.identifier}:shown`, shownHandler);

const hiddenHandler = Sinon.spy();
el.addEventListener(`${el.identifier}:hidden`, hiddenHandler);

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

el.open = true;
assertPopoverShown(el, trigger, panel);
expect(shownHandler.called).to.be.false;

el.open = false;
assertPopoverHidden(el, trigger, panel);
assertPopoverHidden(trigger, panel);
expect(hiddenHandler.called).to.be.false;
});

Expand All @@ -184,33 +154,48 @@ describe('Popover', () => {
const panel = el.querySelector('div')!;

trigger.click();
assertPopoverShown(el, trigger, panel);
assertPopoverShown(trigger, panel);

await sendMouse({ type: 'click', position: [0, 0] });
assertPopoverHidden(el, trigger, panel);
assertPopoverHidden(trigger, panel);
expect(hiddenHandler.calledOnce).to.be.true;
});

it('shows the popover initially if the open attribute is set to true', async () => {
it('closes all the nested popovers and the parent popover when parent trigger button is clicked', async () => {
const el = await fixture<PopoverElement>(html`
<twc-popover open>
<button type="button" data-target="twc-popover.trigger">Toggle</button>
<div data-target="twc-popover.panel"></div>
<twc-popover>
<button type="button" data-target="twc-popover.trigger" data-test-id="parent-trigger">Toggle</button>
<div data-target="twc-popover.panel" data-test-id="parent-panel">
<twc-popover>
<button type="button" data-target="twc-popover.trigger">Toggle</button>
<div data-target="twc-popover.panel"></div>
</twc-popover>
</div>
</twc-popover>
`);

const shownHandler = Sinon.spy();
el.addEventListener(`${el.identifier}:shown`, shownHandler);
const trigger = el.querySelector<HTMLButtonElement>('[data-test-id="parent-trigger"]')!;
const panel = el.querySelector('[data-test-id="parent-panel"]')!;
const nestedEl = el.querySelector('twc-popover')!;
const nestedTrigger = nestedEl.querySelector('button')!;
const nestedPanel = nestedEl.querySelector('div')!;

const trigger = el.querySelector('button')!;
const panel = el.querySelector('div')!;
trigger.click();
assertPopoverShown(trigger, panel);

nestedTrigger.click();
assertPopoverShown(nestedTrigger, nestedPanel);
assertPopoverShown(trigger, panel);

assertPopoverShown(el, trigger, panel);
expect(shownHandler.called).to.be.false;
expect(document.activeElement).to.eq(document.body);
await sendMouse({
type: 'click',
position: [trigger.getBoundingClientRect().x, trigger.getBoundingClientRect().y],
});
assertPopoverHidden(trigger, panel);
assertPopoverHidden(nestedTrigger, nestedPanel);
});

it('closes all the nested popovers and the parent popover when parent trigger button is clicked', async () => {
it('closes popover in order when clicked outside', async () => {
const el = await fixture<PopoverElement>(html`
<twc-popover>
<button type="button" data-target="twc-popover.trigger" data-test-id="parent-trigger">Toggle</button>
Expand All @@ -230,18 +215,51 @@ describe('Popover', () => {
const nestedPanel = nestedEl.querySelector('div')!;

trigger.click();
assertPopoverShown(el, trigger, panel);
nestedTrigger.click();

await sendMouse({ type: 'click', position: [0, 0] });
assertPopoverHidden(nestedTrigger, nestedPanel);
assertPopoverShown(trigger, panel);

await sendMouse({ type: 'click', position: [0, 0] });
assertPopoverHidden(trigger, panel);
});

it('closes all open popovers when opening a non-related popover', async () => {
const el = await fixture<PopoverElement>(html`
<div>
<twc-popover>
<button type="button" data-target="twc-popover.trigger" data-test-id="diff-trigger">Toggle</button>
<div data-target="twc-popover.panel" data-test-id="diff-panel"></div>
</twc-popover>
<twc-popover>
<button type="button" data-target="twc-popover.trigger" data-test-id="parent-trigger">Toggle</button>
<div data-target="twc-popover.panel" data-test-id="parent-panel">
<twc-popover data-test-id="nested-popover">
<button type="button" data-target="twc-popover.trigger">Toggle</button>
<div data-target="twc-popover.panel"></div>
</twc-popover>
</div>
</twc-popover>
</div>
`);

const diffTrigger = el.querySelector<HTMLButtonElement>('[data-test-id="diff-trigger"]')!;
const diffPanel = el.querySelector('[data-test-id="diff-panel"]')!;

const trigger = el.querySelector<HTMLButtonElement>('[data-test-id="parent-trigger"]')!;
const panel = el.querySelector('[data-test-id="parent-panel"]')!;
const nestedEl = el.querySelector<PopoverElement>('[data-test-id="nested-popover"]')!;
const nestedTrigger = nestedEl.querySelector('button')!;
const nestedPanel = nestedEl.querySelector('div')!;

trigger.click();
nestedTrigger.click();
assertPopoverShown(nestedEl, nestedTrigger, nestedPanel);
assertPopoverShown(el, trigger, panel);

await sendMouse({
type: 'click',
position: [trigger.getBoundingClientRect().x, trigger.getBoundingClientRect().y],
});
assertPopoverHidden(el, trigger, panel);
assertPopoverHidden(nestedEl, nestedTrigger, nestedPanel);
diffTrigger.click();
assertPopoverHidden(trigger, panel);
assertPopoverHidden(nestedTrigger, nestedPanel);
assertPopoverShown(diffTrigger, diffPanel);
});

it('starts and stops the positioning logic', async () => {
Expand All @@ -256,10 +274,10 @@ describe('Popover', () => {

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

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

el.open = false;
el.hide();
expect(floatingPanel).not.to.have.attribute('active');
});
});
Loading