Skip to content

Commit

Permalink
feat: add open/close events for some menus and overlays
Browse files Browse the repository at this point in the history
  • Loading branch information
cuberoot authored and Westbrook committed Jan 11, 2021
1 parent 425f716 commit 17f0a58
Show file tree
Hide file tree
Showing 7 changed files with 154 additions and 2 deletions.
3 changes: 3 additions & 0 deletions packages/dropdown/src/Dropdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ import {
/**
* @slot label - The placeholder content for the dropdown
* @slot {"sp-menu"} - The menu of options that will display when the dropdown is open
*
* @fires sp-open - Announces that the overlay has been opened
* @fires sp-close - Announces that the overlay has been closed
*/
export class DropdownBase extends Focusable {
public static openOverlay = async (
Expand Down
53 changes: 53 additions & 0 deletions packages/dropdown/test/dropdown.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ governing permissions and limitations under the License.
import '../sp-dropdown.js';
import { Dropdown } from '../';
import '@spectrum-web-components/overlay/active-overlay.js';
import { OverlayOpenCloseDetail } from '@spectrum-web-components/overlay'
import '@spectrum-web-components/menu/sp-menu.js';
import '@spectrum-web-components/menu/sp-menu-item.js';
import '@spectrum-web-components/menu/sp-menu-divider.js';
Expand Down Expand Up @@ -614,4 +615,56 @@ describe('Dropdown', () => {
expect(el.open).to.be.false;
expect(mouseenterSpy.calledOnce).to.be.true;
});

it('dispatches events on open/close', async () => {
const openedSpy = spy();
const closedSpy = spy();
const handleOpenedSpy = (event: Event): void => openedSpy(event);
const handleClosedSpy = (event: Event): void => closedSpy(event);

const el = await fixture<Dropdown>(
html`
<sp-dropdown
label="Select a Country with a very long label, too long in fact"
@sp-opened=${handleOpenedSpy}
@sp-closed=${handleClosedSpy}
>
<sp-menu>
<sp-menu-item value="deselect">
Deselect Text
</sp-menu-item>
</sp-menu>
</sp-dropdown>
`
);

await elementUpdated(el);
const menu = el.querySelector('sp-menu') as Menu;
el.open = true;

await elementUpdated(el);
await waitUntil(
() => document.activeElement === menu,
'first item focused'
);

expect(openedSpy.calledOnce).to.be.true;
expect(closedSpy.calledOnce).to.be.false;

const openedEvent = openedSpy.args[0][0] as CustomEvent<OverlayOpenCloseDetail>;
expect(openedEvent.detail.interaction).to.equal('inline');

el.open = false;
await elementUpdated(el);

await waitUntil(
() => closedSpy.calledOnce,
'closed event received'
);

expect(closedSpy.calledOnce).to.be.true;

const closedEvent = closedSpy.args[0][0] as CustomEvent<OverlayOpenCloseDetail>;
expect(closedEvent.detail.interaction).to.equal('inline');
});
});
3 changes: 3 additions & 0 deletions packages/overlay/src/OverlayTrigger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ import overlayTriggerStyles from './overlay-trigger.css.js';
* @slot trigger - The content that will trigger the various overlays
* @slot hover-content - The content that will be displayed on hover
* @slot click-content - The content that will be displayed on click
*
* @fires sp-open - Announces that the overlay has been opened
* @fires sp-close - Announces that the overlay has been closed
*/
export class OverlayTrigger extends LitElement {
private closeClickOverlay?: () => void;
Expand Down
23 changes: 22 additions & 1 deletion packages/overlay/src/overlay-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ governing permissions and limitations under the License.
*/

import { ActiveOverlay } from './ActiveOverlay.js';
import { OverlayOpenDetail } from './overlay-types';
import { OverlayOpenDetail, OverlayOpenCloseDetail } from './overlay-types';
import { OverlayTimer } from './overlay-timer.js';
import '../active-overlay.js';

Expand Down Expand Up @@ -192,6 +192,16 @@ export class OverlayStack {
if (details.receivesFocus === 'auto') {
activeOverlay.focus();
}
details.trigger.dispatchEvent(
new CustomEvent<OverlayOpenCloseDetail>('sp-opened', {
bubbles: true,
composed: true,
cancelable: true,
detail: {
interaction: details.interaction
},
})
);
return false;
}
);
Expand Down Expand Up @@ -351,6 +361,17 @@ export class OverlayStack {

overlay.remove();
overlay.dispose();

overlay.trigger.dispatchEvent(
new CustomEvent<OverlayOpenCloseDetail>('sp-closed', {
bubbles: true,
composed: true,
cancelable: true,
detail: {
interaction: overlay.interaction,
},
})
);
}
}

Expand Down
6 changes: 6 additions & 0 deletions packages/overlay/src/overlay-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ export interface OverlayOpenDetail {
theme: ThemeData;
}

export interface OverlayOpenCloseDetail {
interaction: TriggerInteractions;
}

/**
* Used, via an event, to query details about how an element should be shown in
* an overlay
Expand All @@ -55,5 +59,7 @@ export type OverlayOptions = {
declare global {
interface GlobalEventHandlersEventMap {
'sp-overlay-query': CustomEvent<OverlayDisplayQueryDetail>;
'sp-open': CustomEvent<OverlayOpenCloseDetail>;
'sp-close': CustomEvent<OverlayOpenCloseDetail>;
}
}
14 changes: 14 additions & 0 deletions packages/overlay/src/overlay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,3 +148,17 @@ export class Overlay {
Overlay.overlayStack.closeOverlay(this.overlayElement);
}
}

/**
* Announces that an overlay-based UI element has opened
* @event sp-open
* @type {object}
* @property {TriggerInteractions} interaction type of interaction that triggered the opening
*/

/**
* Announces that an overlay-based UI element has opened
* @event sp-close
* @type {object}
* @property {TriggerInteractions} interaction type of interaction that triggered the closing
*/
54 changes: 53 additions & 1 deletion packages/overlay/test/overlay-trigger.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ OF ANY KIND, either express or implied. See the License for the specific languag
governing permissions and limitations under the License.
*/
import { waitForPredicate, isVisible } from '../../../test/testing-helpers.js';
import { spy } from 'sinon';
import {
fixture,
aTimeout,
Expand All @@ -21,7 +22,7 @@ import {
} from '@open-wc/testing';

import '../overlay-trigger.js';
import { OverlayTrigger, ActiveOverlay, TriggerInteractions } from '../';
import { OverlayTrigger, ActiveOverlay, TriggerInteractions, OverlayOpenCloseDetail } from '../';
import '@spectrum-web-components/button/sp-button.js';
import { Button } from '@spectrum-web-components/button';
import '@spectrum-web-components/popover/sp-popover.js';
Expand Down Expand Up @@ -652,6 +653,7 @@ describe('Overlay Trigger', () => {
'hoverContent should still not be visible'
);
});

it('acquires a `color` and `size` from `sp-theme`', async () => {
const el = await fixture<Theme>(html`
<sp-theme color="dark">
Expand Down Expand Up @@ -696,4 +698,54 @@ describe('Overlay Trigger', () => {
expect(overlay.color).to.not.equal('dark');
expect(overlay.color).to.equal('light');
});

it('dispatches events on open/close', async () => {
const openedSpy = spy();
const closedSpy = spy();

const el = testDiv.querySelector('#trigger') as OverlayTrigger;
const outerButton = testDiv.querySelector(
'#outer-button'
) as HTMLElement;
const outerPopover = testDiv.querySelector('#outer-popover') as Popover;

el.addEventListener('sp-opened',openedSpy);
el.addEventListener('sp-closed',closedSpy);

outerButton.click();

await waitUntil(
() => !(outerPopover.parentElement instanceof OverlayTrigger),
'outer content stolen and reparented'
);

await waitUntil(
() => openedSpy.calledOnce,
'opened event sent'
);

expect(isVisible(outerPopover)).to.be.true;
expect(closed).to.be.false;

const openedEvent = openedSpy.args[0][0] as CustomEvent<OverlayOpenCloseDetail>;
expect(openedEvent.detail.interaction).to.equal('click');

document.body.click();

// Wait for the DOM node to be put back in its original place
await waitUntil(
() => outerPopover.parentElement instanceof OverlayTrigger,
'inner content returned'
);

await waitUntil(
() => closedSpy.calledOnce,
'closed event sent'
);

const closedEvent = closedSpy.args[0][0] as CustomEvent<OverlayOpenCloseDetail>;
expect(closedEvent.detail.interaction).to.equal('click');

expect(isVisible(outerPopover)).to.be.false;
});
});

0 comments on commit 17f0a58

Please sign in to comment.