@hashicorp/design-system-components
Version:
Helios Design System Components
299 lines (281 loc) • 11.6 kB
JavaScript
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { assert, warn } from '@ember/debug';
import { next } from '@ember/runloop';
import { guidFor } from '@ember/object/internals';
import { modifier } from 'ember-modifier';
import registerEvent from '../../../modifiers/hds-register-event.js';
import anchoredPositionModifier from '../../../modifiers/hds-anchored-position.js';
import { precompileTemplate } from '@ember/template-compilation';
import { g, i, n } from 'decorator-transforms/runtime';
import { setComponentTemplate } from '@ember/component';
var TEMPLATE = precompileTemplate("{{!\n Copyright (c) HashiCorp, Inc.\n SPDX-License-Identifier: MPL-2.0\n}}\n{{yield\n (hash\n setupPrimitiveContainer=this.setupPrimitiveContainer\n setupPrimitiveToggle=this.setupPrimitiveToggle\n setupPrimitivePopover=this.setupPrimitivePopover\n toggleElement=this._toggleElement\n popoverElement=this._popoverElement\n isOpen=this._isOpen\n showPopover=this.showPopover\n hidePopover=this.hidePopover\n togglePopover=this.togglePopover\n )\n}}");
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
class HdsPopoverPrimitive extends Component {
static {
g(this.prototype, "_isOpen", [tracked]);
}
#_isOpen = (i(this, "_isOpen"), void 0);
static {
g(this.prototype, "_isClosing", [tracked], function () {
return false;
});
}
#_isClosing = (i(this, "_isClosing"), void 0);
static {
g(this.prototype, "_anchoredPositionOptions", [tracked]);
}
#_anchoredPositionOptions = (i(this, "_anchoredPositionOptions"), void 0);
_containerElement;
_toggleElement;
_popoverElement;
// this will enable "soft" events for the toggle ("hover" and "focus")
enableSoftEvents = this.args.enableSoftEvents ?? false;
// this will enable "click" events for the toggle
enableClickEvents = this.args.enableClickEvents ?? false;
_timer;
constructor(owner, args) {
super(owner, args);
this._isOpen = this.args.isOpen ?? false;
}
setupPrimitiveContainer = modifier(element => {
this._containerElement = element;
// we register the "soft" events
if (this.enableSoftEvents) {
// @ts-expect-error: known issue with type of invocation
registerEvent(this._containerElement, ['mouseenter',
// eslint-disable-next-line @typescript-eslint/unbound-method
this.onMouseEnter]);
// @ts-expect-error: known issue with type of invocation
registerEvent(this._containerElement, ['mouseleave',
// eslint-disable-next-line @typescript-eslint/unbound-method
this.onMouseLeave]);
// @ts-expect-error: known issue with type of invocation
// eslint-disable-next-line @typescript-eslint/unbound-method
registerEvent(this._containerElement, ['focusin', this.onFocusIn]);
}
// we always want the focusOut event
// @ts-expect-error: known issue with type of invocation
// eslint-disable-next-line @typescript-eslint/unbound-method
registerEvent(this._containerElement, ['focusout', this.onFocusOut]);
});
setupPrimitiveToggle = modifier(element => {
this._toggleElement = element;
assert(`The toggle element of "Hds::PopoverPrimitive" must be a <button>; element received: <${element.tagName.toLowerCase()}>`, element instanceof HTMLButtonElement);
this._linkToggleAndPopover();
// Return a teardown function to clean up the modifier's side effects.
// This is a safeguard against bugs where this element might be
// cached and re-parented in the DOM, rather than being fully destroyed.
return () => {
element.removeAttribute('aria-controls');
element.removeAttribute('popovertarget');
};
});
setupPrimitivePopover = modifier((element, _positional, named) => {
this._popoverElement = element;
// We need to create a popoverId in order to connect the popover and the toggle with aria-controls
// and an id is needed to implement `onclick` event listeners
if (!this._popoverElement.id) {
this._popoverElement.id = guidFor(this);
}
// this should be an extremely edge case, but in the case the popover needs to be initially forced to be open
// we need to use the "manual" state to support the case of multiple "menus" opened at the same time
// IMPORTANT! if a "popover" is set to "open" with a "manual" state, then it can't be closed via `esc` and `click outside`
if (this.args.isOpen) {
this._popoverElement.popover = 'manual';
this._popoverElement.showPopover();
} else {
this._popoverElement.popover = 'auto';
}
// Register "onBeforeToggle" + "onToggle" callback functions to be called when a native 'toggle' event is dispatched
// @ts-expect-error: known issue with type of invocation
registerEvent(this._popoverElement, ['beforetoggle',
// eslint-disable-next-line @typescript-eslint/unbound-method
this.onBeforeTogglePopover]);
// @ts-expect-error: known issue with type of invocation
// eslint-disable-next-line @typescript-eslint/unbound-method
registerEvent(this._popoverElement, ['toggle', this.onTogglePopover]);
// we need to spread the argument because if it's set via `{{ hash … }}` Ember complains when we overwrite one of its values
this._anchoredPositionOptions = {
...named.anchoredPositionOptions
};
this._linkToggleAndPopover();
});
// Apply the `hds-anchored-position` modifier to the "popover" element
// (notice: this function runs the first time when the element the modifier was applied to is inserted into the DOM, and it autotracks while running.
// Any tracked values that it accesses will be tracked, including the arguments it receives, and if any of them changes, the function will run again)
// This modifiers uses the Floating UI library to provide:
// - positioning of the "popover" in relation to the "toggle"
// - collision detection (optional)
_applyAnchoredPositionModifier() {
if (this._toggleElement !== undefined && this._popoverElement !== undefined && this._anchoredPositionOptions !== undefined) {
// eslint-disable-next-line ember/no-runloop
next(() => {
// @ts-expect-error: known issue with type of invocation
anchoredPositionModifier(this._popoverElement,
// element the modifier is attached to
[this._toggleElement],
// positional arguments
this._anchoredPositionOptions // named arguments
);
});
}
}
_linkToggleAndPopover() {
if (this._toggleElement === undefined || this._popoverElement === undefined) {
return;
}
const popoverId = this._popoverElement.id;
this._toggleElement.setAttribute('aria-controls', popoverId);
if (this.enableClickEvents) {
this._toggleElement.setAttribute('popovertarget', popoverId);
} else {
this._toggleElement.removeAttribute('popovertarget');
}
this._applyAnchoredPositionModifier();
}
showPopover() {
try {
if (this._popoverElement) {
this._popoverElement.showPopover();
}
} catch (error) {
warn(`The invocation of \`showPopover\` for the popover element caused an unexpected error: ${JSON.stringify(error)}`, {
id: 'hds-popover.show-popover-action.invocation-failed'
});
}
}
static {
n(this.prototype, "showPopover", [action]);
}
// the event may be passed by the `on` modifier, so we need to keep it as an argument here
// eslint-disable-next-line @typescript-eslint/no-unused-vars
hidePopover(_event) {
try {
if (this._popoverElement) {
this._popoverElement.hidePopover();
}
} catch (error) {
warn(`The invocation of \`hidePopover\` for the popover element caused an unexpected error: ${JSON.stringify(error)}`, {
id: 'hds-popover.hide-popover-action.invocation-failed'
});
}
}
static {
n(this.prototype, "hidePopover", [action]);
}
togglePopover() {
try {
if (this._popoverElement) {
this._popoverElement.togglePopover();
}
} catch (error) {
warn(`The invocation of \`togglePopover\` for the popover element caused an unexpected error: ${JSON.stringify(error)}`, {
id: 'hds-popover.toggle-popover-action.invocation-failed'
});
}
}
// fired just _before_ the "popover" is shown or hidden
static {
n(this.prototype, "togglePopover", [action]);
}
onBeforeTogglePopover(event) {
if (event.newState === 'closed') {
// we need this flag to check if it's in the "closing" process,
// because the browser automatically returns the focus to the "trigger" button
// and this would re-open immediately the popover because of the `focusin` event
this._isClosing = true;
}
}
// fired just _after_ the "popover" is shown or hidden
static {
n(this.prototype, "onBeforeTogglePopover", [action]);
}
onTogglePopover(event) {
if (event.newState === 'open') {
this._isOpen = true;
// we call the "onOpen" callback if it exists (and is a function)
const {
onOpen
} = this.args;
if (typeof onOpen === 'function') {
onOpen();
}
} else {
this._isOpen = false;
// reset the "isClosing" flag (the `toggle` event is fired _after_ the popover is closed)
this._isClosing = false;
// if the popover was initially forced to be open (using the "manual" state) then revert its status to `auto` once the user interacts with it
if (this.args.isOpen) {
if (this._popoverElement) {
this._popoverElement.popover = 'auto';
}
}
// we call the "onClose" callback if it exists (and is a function)
const {
onClose
} = this.args;
if (typeof onClose === 'function') {
onClose();
}
}
}
static {
n(this.prototype, "onTogglePopover", [action]);
}
onMouseEnter() {
if (this._timer) {
clearTimeout(this._timer);
}
this.showPopover();
}
static {
n(this.prototype, "onMouseEnter", [action]);
}
onFocusIn() {
// don't re-open the popover if the focus is returned because the closing
if (!this._isClosing) {
if (this._timer) {
clearTimeout(this._timer);
}
this.showPopover();
}
}
static {
n(this.prototype, "onFocusIn", [action]);
}
onMouseLeave() {
this._timer = setTimeout(() => this.hidePopover(), 500);
}
static {
n(this.prototype, "onMouseLeave", [action]);
}
onFocusOut(event) {
if (this._containerElement) {
let isFocusStillInside = false;
if (event.relatedTarget &&
// if the related target is not part of the disclosed content we close the disclosed container
this._containerElement.contains(event.relatedTarget)) {
isFocusStillInside = true;
} else if (document.activeElement &&
// due to inconsistent implementation of relatedTarget across browsers we use the activeElement as a fallback
this._containerElement.contains(document.activeElement)) {
isFocusStillInside = true;
}
// if the target receiving the focus is _not_ part of the disclosed content we close the disclosed container
if (!isFocusStillInside) {
this.hidePopover();
}
}
}
static {
n(this.prototype, "onFocusOut", [action]);
}
}
setComponentTemplate(TEMPLATE, HdsPopoverPrimitive);
export { HdsPopoverPrimitive as default };
//# sourceMappingURL=index.js.map